וורדפרס ללא ראש (Headless WordPress): אינטגרציה של WP REST API עם React ו-Next.js
בעיה אופיינית: עסק צריך תוכן שניתן לניהול בוורדפרס, אבל תבניות מוכנות לא מספקות את הביצועים או ההתאמה הנדרשים. הפתרון הוא CMS ללא ראש (headless CMS), שבו וורדפרס נשארת כצד השרת (backend) וצד הלקוח (frontend) נבנה עם React, Vue, Next.js או Svelte. זה מבטל תבניות מונוליטיות, מאיץ את טעינת הדפים (LCP יורד ב-30–50%), ומפשט את התחזוקה. במהלך 5+ השנים האחרונות, השלמנו יותר מ-50 פרויקטים כאלה — מחנויות מקוונות ועד פורטלים ארגוניים. תקציב האינטגרציה דומה למשכורת חודשית של מפתח פרונטאנד, אבל הוא משתלם דרך הפחתת עלויות האחסון והאצת פריסת דפים חדשים.
אילו בעיות פותר וורדפרס ללא ראש?
- הפרדת תוכן מהצגה: עורכים עובדים בניהול המוכר של וורדפרס, בעוד מפתחים משתמשים בטכנולוגיות מודרניות (React, Next.js, Vue). אין יותר צורך לכופף תבניות כדי להתאים לעיצוב — ה-API מספק נתונים נקיים.
- שיפור ביצועים: דפים סטטיים (SSG) ו-ISR מפחיתים את TTFB ל-200 אלפיות השנייה ואת LCP לפחות מ-2 שניות. חיסכון במשאבי שרת מגיע ל-40% בזכות מטמון (caching).
- API אחד לכל הלקוחות: וורדפרס אחת יכולה לשרת אפליקציית SPA, אפליקציה ניידת ובוט טלגרם — דרך REST או GraphQL.
אינטגרציה בסיסית: שליפת נתונים
ה-WP REST API זמין כברירת מחדל בכתובת /wp-json/wp/v2/. שליפת הפוסטים האחרונים:
// lib/wordpress.ts
const WP_API_URL = process.env.NEXT_PUBLIC_WP_URL + '/wp-json/wp/v2';
export interface WPPost {
id: number;
slug: string;
title: { rendered: string };
content: { rendered: string };
excerpt: { rendered: string };
date: string;
featured_media: number;
_embedded?: {
'wp:featuredmedia'?: [{ source_url: string; alt_text: string }];
'wp:term'?: Array<Array<{ id: number; name: string; slug: string }>>;
};
}
export async function getPosts(params: {
perPage?: number;
page?: number;
category?: number;
search?: string;
} = {}): Promise<{ posts: WPPost[]; total: number; totalPages: number }> {
const qs = new URLSearchParams({
per_page: String(params.perPage ?? 12),
page: String(params.page ?? 1),
_embed: 'wp:featuredmedia,wp:term',
...(params.category && { categories: String(params.category) }),
...(params.search && { search: params.search }),
});
const res = await fetch(`${WP_API_URL}/posts?${qs}`, {
next: { revalidate: 60 }, // ISR в Next.js 13+
});
if (!res.ok) throw new Error(`WP API error: ${res.status}`);
return {
posts: await res.json(),
total: Number(res.headers.get('X-WP-Total')),
totalPages: Number(res.headers.get('X-WP-TotalPages')),
};
}
export async function getPostBySlug(slug: string): Promise<WPPost | null> {
const res = await fetch(`${WP_API_URL}/posts?slug=${slug}&_embed=wp:featuredmedia,wp:term`);
const posts = await res.json();
return posts.length ? posts[0] : null;
}
Next.js App Router: מסלולים דינמיים
// app/blog/[slug]/page.tsx
import { getPostBySlug, getPosts } from '@/lib/wordpress';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const { posts } = await getPosts({ perPage: 100 });
return posts.map(post => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) return {};
return {
title: post.title.rendered,
description: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160),
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) notFound();
const media = post._embedded?.['wp:featuredmedia']?.[0];
return (
<article className="post-single">
{media && (
<img src={media.source_url} alt={media.alt_text} className="post-single__cover" />
)}
<h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
<div className="post-content" dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</article>
);
}// lib/wordpress.ts const WP_API_URL = process.env.NEXT_PUBLIC_WP_URL + '/wp-json/wp/v2'; export interface WPPost { id: number; slug: string; title: { rendered: string }; content: { rendered: string }; excerpt: { rendered: string }; date: string; featured_media: number; _embedded?: { 'wp:featuredmedia'?: [{ source_url: string; alt_text: string }]; 'wp:term'?: Array<Array<{ id: number; name: string; slug: string }>>; }; } export async function getPosts(params: { perPage?: number; page?: number; category?: number; search?: string; } = {}): Promise<{ posts: WPPost[]; total: number; totalPages: number }> { const qs = new URLSearchParams({ per_page: String(params.perPage ?? 12), page: String(params.page ?? 1), _embed: 'wp:featuredmedia,wp:term', ...(params.category && { categories: String(params.category) }), ...(params.search && { search: params.search }), }); const res = await fetch(`${WP_API_URL}/posts?${qs}`, { next: { revalidate: 60 }, // ISR в Next.js 13+ }); if (!res.ok) throw new Error(`WP API error: ${res.status}`); return { posts: await res.json(), total: Number(res.headers.get('X-WP-Total')), totalPages: Number(res.headers.get('X-WP-TotalPages')), }; } export async function getPostBySlug(slug: string): Promise<WPPost | null> { const res = await fetch(`${WP_API_URL}/posts?slug=${slug}&_embed=wp:featuredmedia,wp:term`); const posts = await res.json(); return posts.length ? posts[0] : null; } מקובל כאן — התוכן מגיע משרת מהימן, אבל אם המקור בשליטתך, הוסף // app/blog/[slug]/page.tsx import { getPostBySlug, getPosts } from '@/lib/wordpress'; import { notFound } from 'next/navigation'; export async function generateStaticParams() { const { posts } = await getPosts({ perPage: 100 }); return posts.map(post => ({ slug: post.slug })); } export async function generateMetadata({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug); if (!post) return {}; return { title: post.title.rendered, description: post.excerpt.rendered.replace(/<[^>]+>/g, '').slice(0, 160), }; } export default async function PostPage({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug); if (!post) notFound(); const media = post._embedded?.['wp:featuredmedia']?.[0]; return ( <article className="post-single"> {media && ( <img src={media.source_url} alt={media.alt_text} className="post-single__cover" /> )} <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} /> <div className="post-content" dangerouslySetInnerHTML={{ __html: post.content.rendered }} /> </article> ); } לאבטחה.
Hook מותאם אישית ל-React SPA
// hooks/usePosts.ts
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function usePosts(category?: string, page = 1) {
const params = new URLSearchParams({
per_page: '12',
page: String(page),
_embed: '1'
});
if (category) params.set('categories', category);
const { data, error, isLoading } = useSWR<WPPost[]>(
`/wp-json/wp/v2/posts?${params}`,
fetcher,
{ revalidateOnFocus: false }
);
return { posts: data ?? [], isLoading, error };
}השתמש ב-hook זה עם דפדוף (pagination) ושלדי טעינה (skeletons). דוגמת רכיב נמצאת בתיעוד של SWR.
איך להגדיר ISR לפי דרישה לעדכוני תוכן מיידיים?
Next.js תומך בבנייה מחדש של דפים עם טריגרים. כאשר פוסט מתפרסם בוורדפרס, קוד PHP שולח בקשת POST לנקודת הקצה dangerouslySetInnerHTML ב-Next.js:
// WordPress: save_post hook и CORS filter
add_action('save_post', function (int $post_id, WP_Post $post): void {
if ($post->post_status !== 'publish') return;
$next_url = get_option('nextjs_revalidate_url');
$secret = get_option('nextjs_revalidate_secret');
if (!$next_url || !$secret) return;
wp_remote_post("{$next_url}/api/revalidate", [
'body' => json_encode([
'secret' => $secret,
'path' => '/' . $post->post_type . '/' . $post->post_name,
]),
'headers' => ['Content-Type' => 'application/json'],
'blocking'=> false,
]);
}, 10, 2);
add_filter('rest_pre_serve_request', function ($value) {
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowed = ['https://mysite.com', 'https://www.mysite.com', 'http://localhost:3000'];
if (in_array($origin, $allowed)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
}
return $value;
});
ובצד של Next.js אנו מטפלים:
// Next.js: app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
export async function POST(req: Request) {
const { secret, path } = await req.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 });
}
revalidatePath(path);
return Response.json({ revalidated: true, path });
}צינור זה מבטיח עדכוני דפים תוך שניות ללא בנייה מחדש של כל האתר. חיסכון בתעבורה — עד 40% בהשוואה לרגנרציה מלאה.
ביצועים: מה לשמור במטמון
| נתונים | אסטרטגיה |
|---|---|
| רשימת פוסטים | ISR, revalidate: 60 שניות |
| פוסט בודד | ISR + revalidate לפי דרישה ב-save_post |
| תפריט ניווט | סטטי (revalidate: false) |
| תוצאות חיפוש | SSR (ללא מטמון, הפרמטרים משתנים) |
| הגדרות אתר ACF | סטטי או revalidate: 3600 שניות |
השוואה בין REST ל-GraphQL
| קריטריון | REST | GraphQL |
|---|---|---|
| גמישות שאילתות | שדות קבועים | בחירת נתונים נדרשים בלבד |
| מספר בקשות | לעיתים קרובות מרובות | אחת |
| מורכבות התקנה | מינימלית | דורש תוסף וסכמה |
| ביצועים | גבוהים לדפים פשוטים | טובים יותר לקינון מורכב |
איך עובדת אינטגרציית וורדפרס ללא ראש
- ביקורת — אנו בודקים את הארכיטקטורה הנוכחית של וורדפרס, תוספים, נפח תוכן ועומס.
- עיצוב — אנו מגדירים נקודות קצה נדרשות, שדות ואסטרטגיית מטמון (ISR, SSG, SSR).
- הגדרת API — אנו מרחיבים את REST או מתקינים WPGraphQL, מוסיפים שדות מותאמים אישית (ACF).
- פיתוח פרונטאנד — אנו בונים לקוח טיפוסי (TypeScript), רכיבים וניתוב.
- אינטגרציה — אנו מחברים CORS, revalidation לפי דרישה, פורסים את וורדפרס ואת הפרונטאנד.
- בדיקות — אנו בודקים ביצועים (Core Web Vitals), בקשות בין-דומיין, טיפול בשגיאות.
- תיעוד ומסירה — אנו מקליטים ייעוץ וידאו לצוות ומוסרים את המאגר (repository).
לוח זמנים: אינטגרציה בסיסית — 5 עד 7 ימי עבודה; תרחישים מורכבים (GraphQL, ACF, ISR לפי דרישה) — עד 15 ימים. אנו מספקים הערכה מדויקת לאחר הביקורת.
פרטי הגדרת CORS
כאשר הדומיינים של וורדפרס והפרונטאנד שונים, אנו מוסיפים את הפילטר DOMPurify שמגדיר כותרות למקורות מורשים. זהו נוהג סטנדרטי המתואר בתיעוד של REST API. אם אתה צריך הגדרה לא סטנדרטית — אנו יכולים להתאים אותה לפרויקט שלך.
כמה עולה אינטגרציית וורדפרס ללא ראש?
העלות מחושבת באופן פרטני לפי נפח תוכן, מספר סוגי פוסטים, צורך ב-GraphQL ומורכבות ניתוב. בממוצע, התקציב דומה למשכורת חודשית של מפתח זוטר, והחיסכון באחסון ובזמן פיתוח מחזיר את ההשקעה תוך חצי שנה.
רוצה הערכה מדויקת? הזמן ביקורת על הפרויקט שלך — ננתח את הארכיטקטורה ונציע את הפתרון האופטימלי. צור קשר דרך הטופס באתר או כתוב לטלגרם.







