Przejdź do treści
Backend

WordPress jako Headless CMS z Next.js - Kompletny przewodnik

Opublikowano:
·5 min czytania·Autor: MDS Software Solutions Group

WordPress jako Headless

backend

WordPress jako Headless CMS z Next.js

WordPress napedza ponad 40% wszystkich stron internetowych na swiecie. Jednak tradycyjna architektura monolityczna, gdzie WordPress odpowiada zarowno za zarzadzanie trescia, jak i za jej prezentacje, coraz czesciej ustepuje miejsca nowoczesnemu podejsciu headless. W tym artykule pokaze, jak polaczyc potege WordPressa jako systemu zarzadzania trescia z wydajnoscia i elastycznoscia Next.js na frontendzie.

Czym jest Headless CMS?#

Headless CMS to system zarzadzania trescia, ktory oddziela warstwe backendu (tworzenie i przechowywanie tresci) od warstwy frontendowej (prezentacja tresci). Zamiast generowac strony HTML bezposrednio, headless CMS udostepnia tresc poprzez API - najczesciej REST lub GraphQL.

Dlaczego warto uzyc WordPressa jako Headless CMS?#

  • Znajomy interfejs - redaktorzy juz znaja panel administracyjny WordPressa
  • Bogaty ekosystem wtyczek - tysiace wtyczek do rozszerzenia funkcjonalnosci
  • Dojrzalosc platformy - ponad 20 lat rozwoju i stabilnosci
  • Duza spolecznosc - latwy dostep do wsparcia i dokumentacji
  • Pelna kontrola nad frontendem - uzyj dowolnej technologii do prezentacji tresci
  • Lepsza wydajnosc - statyczne strony generowane przez Next.js sa bliskawicznie szybkie
  • Bezpieczenstwo - frontend jest oddzielony od panelu admina

Headless vs Tradycyjny WordPress#

| Cecha | Tradycyjny WordPress | Headless WordPress | |-------|---------------------|-------------------| | Rendering | PHP po stronie serwera | SSG/SSR/ISR w Next.js | | Wydajnosc | Zalezy od serwera i wtyczek | Statyczne strony, CDN | | Elastycznosc frontendu | Ograniczona do szablonow PHP | Pelna swoboda (React, Vue, etc.) | | SEO | Wymaga wtyczek (Yoast) | Wbudowane w Next.js | | Bezpieczenstwo | Panel admina eksponowany | Backend ukryty, frontend statyczny | | Skalowalnosc | Wymaga cache i optymalizacji | Naturalnie skalowalne przez CDN |

WordPress REST API#

WordPress od wersji 4.7 posiada wbudowane REST API, ktore udostepnia wszystkie podstawowe typy tresci. API jest dostepne pod adresem /wp-json/wp/v2/.

Podstawowe endpointy#

# Pobranie postow
GET /wp-json/wp/v2/posts

# Pobranie pojedynczego posta
GET /wp-json/wp/v2/posts/123

# Pobranie stron
GET /wp-json/wp/v2/pages

# Pobranie kategorii
GET /wp-json/wp/v2/categories

# Pobranie mediow
GET /wp-json/wp/v2/media

# Pobranie uzytkownikow
GET /wp-json/wp/v2/users

Filtrowanie i paginacja#

REST API oferuje rozbudowane mozliwosci filtrowania:

# Posty z konkretnej kategorii
GET /wp-json/wp/v2/posts?categories=5

# Wyszukiwanie
GET /wp-json/wp/v2/posts?search=nextjs

# Paginacja (10 postow na strone)
GET /wp-json/wp/v2/posts?per_page=10&page=2

# Sortowanie
GET /wp-json/wp/v2/posts?orderby=date&order=desc

# Osadzanie powiazanych danych (autor, media, kategorie)
GET /wp-json/wp/v2/posts?_embed

Rejestracja wlasnych endpointow#

Mozesz rozszerzyc REST API o wlasne endpointy:

// functions.php
add_action('rest_api_init', function () {
    register_rest_route('custom/v1', '/featured-posts', [
        'methods'  => 'GET',
        'callback' => 'get_featured_posts',
        'permission_callback' => '__return_true',
    ]);
});

function get_featured_posts($request) {
    $posts = get_posts([
        'meta_key'   => 'is_featured',
        'meta_value' => '1',
        'numberposts' => 6,
    ]);

    $data = [];
    foreach ($posts as $post) {
        $data[] = [
            'id'        => $post->ID,
            'title'     => $post->post_title,
            'excerpt'   => get_the_excerpt($post),
            'slug'      => $post->post_name,
            'thumbnail' => get_the_post_thumbnail_url($post, 'large'),
        ];
    }

    return rest_ensure_response($data);
}

WPGraphQL - GraphQL dla WordPressa#

Chociaz REST API jest funkcjonalne, GraphQL oferuje znacznie elastyczniejsze odpytywanie danych. Wtyczka WPGraphQL dodaje pelne wsparcie GraphQL do WordPressa.

Instalacja WPGraphQL#

# Przez WP-CLI
wp plugin install wp-graphql --activate

# Lub pobierz z https://www.wpgraphql.com/

Po instalacji GraphQL endpoint jest dostepny pod /graphql.

Przykladowe zapytania GraphQL#

# Pobranie postow z autorem i kategoriami
query GetPosts {
  posts(first: 10, where: { orderby: { field: DATE, order: DESC } }) {
    nodes {
      id
      databaseId
      title
      slug
      excerpt
      date
      featuredImage {
        node {
          sourceUrl
          altText
          mediaDetails {
            width
            height
          }
        }
      }
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
      categories {
        nodes {
          name
          slug
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

# Pobranie pojedynczego posta po slugu
query GetPostBySlug($slug: ID!) {
  post(id: $slug, idType: SLUG) {
    title
    content
    date
    modified
    seo {
      title
      metaDesc
      opengraphImage {
        sourceUrl
      }
    }
  }
}

Zalety GraphQL nad REST API#

  • Pobieranie dokladnie potrzebnych danych - brak over-fetchingu
  • Jedno zapytanie zamiast wielu - mniej requestow HTTP
  • Silne typowanie - lepsza walidacja i autouzupelnianie w IDE
  • Introspekcja - automatyczna dokumentacja schematu

Konfiguracja Next.js z WordPressem#

Inicjalizacja projektu#

npx create-next-app@latest wordpress-frontend --typescript --app
cd wordpress-frontend
npm install graphql-request graphql

Konfiguracja zmiennych srodowiskowych#

# .env.local
NEXT_PUBLIC_WORDPRESS_URL=https://cms.twojadomena.pl
WORDPRESS_GRAPHQL_URL=https://cms.twojadomena.pl/graphql
WORDPRESS_AUTH_REFRESH_TOKEN=your-refresh-token
WORDPRESS_PREVIEW_SECRET=your-preview-secret
REVALIDATION_SECRET=your-revalidation-secret

Klient GraphQL#

// lib/wordpress.ts
import { GraphQLClient, gql } from 'graphql-request';

const client = new GraphQLClient(
  process.env.WORDPRESS_GRAPHQL_URL!,
  {
    headers: {
      'Content-Type': 'application/json',
    },
  }
);

// Typy
export interface WPPost {
  id: string;
  databaseId: number;
  title: string;
  slug: string;
  excerpt: string;
  content: string;
  date: string;
  modified: string;
  featuredImage: {
    node: {
      sourceUrl: string;
      altText: string;
      mediaDetails: {
        width: number;
        height: number;
      };
    };
  } | null;
  author: {
    node: {
      name: string;
      avatar: { url: string };
    };
  };
  categories: {
    nodes: Array<{ name: string; slug: string }>;
  };
  seo?: {
    title: string;
    metaDesc: string;
    opengraphImage?: { sourceUrl: string };
  };
}

// Pobranie wszystkich postow
export async function getAllPosts(first = 20): Promise<WPPost[]> {
  const query = gql`
    query GetAllPosts($first: Int!) {
      posts(first: $first, where: { orderby: { field: DATE, order: DESC } }) {
        nodes {
          id
          databaseId
          title
          slug
          excerpt
          date
          featuredImage {
            node {
              sourceUrl
              altText
              mediaDetails {
                width
                height
              }
            }
          }
          author {
            node {
              name
              avatar {
                url
              }
            }
          }
          categories {
            nodes {
              name
              slug
            }
          }
        }
      }
    }
  `;

  const data = await client.request<{ posts: { nodes: WPPost[] } }>(
    query,
    { first }
  );

  return data.posts.nodes;
}

// Pobranie posta po slugu
export async function getPostBySlug(slug: string): Promise<WPPost | null> {
  const query = gql`
    query GetPostBySlug($slug: ID!) {
      post(id: $slug, idType: SLUG) {
        id
        databaseId
        title
        slug
        content
        excerpt
        date
        modified
        featuredImage {
          node {
            sourceUrl
            altText
            mediaDetails {
              width
              height
            }
          }
        }
        author {
          node {
            name
            avatar {
              url
            }
          }
        }
        categories {
          nodes {
            name
            slug
          }
        }
        seo {
          title
          metaDesc
          opengraphImage {
            sourceUrl
          }
        }
      }
    }
  `;

  const data = await client.request<{ post: WPPost | null }>(
    query,
    { slug }
  );

  return data.post;
}

// Pobranie slugow wszystkich postow (dla generateStaticParams)
export async function getAllPostSlugs(): Promise<string[]> {
  const query = gql`
    query GetAllSlugs {
      posts(first: 1000) {
        nodes {
          slug
        }
      }
    }
  `;

  const data = await client.request<{
    posts: { nodes: Array<{ slug: string }> };
  }>(query);

  return data.posts.nodes.map((node) => node.slug);
}

Strona listy postow#

// app/blog/page.tsx
import { getAllPosts } from '@/lib/wordpress';
import Image from 'next/image';
import Link from 'next/link';

export const revalidate = 3600; // ISR: rewalidacja co godzine

export default async function BlogPage() {
  const posts = await getAllPosts();

  return (
    <main className="container mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
        {posts.map((post) => (
          <article key={post.id} className="bg-white rounded-lg shadow-md overflow-hidden">
            {post.featuredImage && (
              <Image
                src={post.featuredImage.node.sourceUrl}
                alt={post.featuredImage.node.altText || post.title}
                width={post.featuredImage.node.mediaDetails.width}
                height={post.featuredImage.node.mediaDetails.height}
                className="w-full h-48 object-cover"
              />
            )}

            <div className="p-6">
              <div className="flex gap-2 mb-3">
                {post.categories.nodes.map((cat) => (
                  <span key={cat.slug} className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
                    {cat.name}
                  </span>
                ))}
              </div>

              <h2 className="text-xl font-semibold mb-2">
                <Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
                  {post.title}
                </Link>
              </h2>

              <div
                className="text-gray-600 text-sm mb-4"
                dangerouslySetInnerHTML={{ __html: post.excerpt }}
              />

              <div className="flex items-center text-sm text-gray-500">
                <span>{post.author.node.name}</span>
                <span className="mx-2">|</span>
                <time dateTime={post.date}>
                  {new Date(post.date).toLocaleDateString('pl-PL')}
                </time>
              </div>
            </div>
          </article>
        ))}
      </div>
    </main>
  );
}

Strona pojedynczego posta#

// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPostSlugs } from '@/lib/wordpress';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { Metadata } from 'next';

export const revalidate = 3600;

interface PageProps {
  params: Promise<{ slug: string }>;
}

export async function generateStaticParams() {
  const slugs = await getAllPostSlugs();
  return slugs.map((slug) => ({ slug }));
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) return {};

  return {
    title: post.seo?.title || post.title,
    description: post.seo?.metaDesc || post.excerpt.replace(/<[^>]*>/g, ''),
    openGraph: {
      title: post.seo?.title || post.title,
      description: post.seo?.metaDesc || post.excerpt.replace(/<[^>]*>/g, ''),
      images: post.seo?.opengraphImage
        ? [{ url: post.seo.opengraphImage.sourceUrl }]
        : post.featuredImage
          ? [{ url: post.featuredImage.node.sourceUrl }]
          : [],
    },
  };
}

export default async function PostPage({ params }: PageProps) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) notFound();

  return (
    <article className="container mx-auto px-4 py-12 max-w-3xl">
      <header className="mb-8">
        <h1 className="text-4xl font-bold mb-4">{post.title}</h1>

        <div className="flex items-center text-gray-600 mb-6">
          <span>{post.author.node.name}</span>
          <span className="mx-2">|</span>
          <time dateTime={post.date}>
            {new Date(post.date).toLocaleDateString('pl-PL', {
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })}
          </time>
        </div>

        {post.featuredImage && (
          <Image
            src={post.featuredImage.node.sourceUrl}
            alt={post.featuredImage.node.altText || post.title}
            width={post.featuredImage.node.mediaDetails.width}
            height={post.featuredImage.node.mediaDetails.height}
            className="w-full rounded-lg"
            priority
          />
        )}
      </header>

      <div
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: post.content }}
      />
    </article>
  );
}

Custom Post Types i Advanced Custom Fields (ACF)#

Jednym z najmocniejszych aspektow WordPressa sa wlasne typy postow i pola ACF. Aby udostepnic je przez GraphQL, potrzebujesz dodatkowej wtyczki WPGraphQL for ACF.

Rejestracja Custom Post Type#

// functions.php
add_action('init', function () {
    register_post_type('portfolio', [
        'labels' => [
            'name'          => 'Portfolio',
            'singular_name' => 'Projekt',
        ],
        'public'       => true,
        'has_archive'  => true,
        'show_in_rest' => true, // Wazne dla REST API
        'show_in_graphql' => true, // Wazne dla WPGraphQL
        'graphql_single_name' => 'project',
        'graphql_plural_name' => 'projects',
        'supports'     => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
    ]);
});

Konfiguracja ACF z GraphQL#

Po zainstalowaniu WPGraphQL for ACF, pola ACF sa automatycznie dostepne w schemacie GraphQL:

query GetProjects {
  projects(first: 12) {
    nodes {
      title
      slug
      excerpt
      featuredImage {
        node {
          sourceUrl
          altText
        }
      }
      projectFields {
        clientName
        projectUrl
        technologies
        completionDate
        testimonial
      }
    }
  }
}

Pobieranie danych ACF w Next.js#

// lib/wordpress.ts
export interface Project {
  title: string;
  slug: string;
  excerpt: string;
  featuredImage: {
    node: {
      sourceUrl: string;
      altText: string;
    };
  } | null;
  projectFields: {
    clientName: string;
    projectUrl: string;
    technologies: string[];
    completionDate: string;
    testimonial: string;
  };
}

export async function getProjects(): Promise<Project[]> {
  const query = gql`
    query GetProjects {
      projects(first: 50) {
        nodes {
          title
          slug
          excerpt
          featuredImage {
            node {
              sourceUrl
              altText
            }
          }
          projectFields {
            clientName
            projectUrl
            technologies
            completionDate
            testimonial
          }
        }
      }
    }
  `;

  const data = await client.request<{
    projects: { nodes: Project[] };
  }>(query);

  return data.projects.nodes;
}

Optymalizacja obrazow z Next.js Image#

WordPress przechowuje obrazy na wlasnym serwerze, ale Next.js moze je optymalizowac w locie dzieki komponentowi Image.

Konfiguracja next.config.js#

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cms.twojadomena.pl',
        pathname: '/wp-content/uploads/**',
      },
    ],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};

module.exports = nextConfig;

Komponent obrazu z WordPress#

// components/WordPressImage.tsx
import Image from 'next/image';

interface WordPressImageProps {
  src: string;
  alt: string;
  width: number;
  height: number;
  priority?: boolean;
  className?: string;
  sizes?: string;
}

export function WordPressImage({
  src,
  alt,
  width,
  height,
  priority = false,
  className,
  sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw',
}: WordPressImageProps) {
  return (
    <Image
      src={src}
      alt={alt}
      width={width}
      height={height}
      priority={priority}
      className={className}
      sizes={sizes}
      quality={85}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
    />
  );
}

Incremental Static Regeneration (ISR)#

ISR to kluczowa funkcja Next.js, ktora pozwala aktualizowac statyczne strony bez pelnej przebudowy. Jest idealnym rozwiazaniem dla tresci WordPressowych, ktore zmieniaja sie regularnie.

Rewalidacja czasowa#

// app/blog/page.tsx
export const revalidate = 3600; // Rewalidacja co 1 godzine

Rewalidacja na zadanie (On-Demand)#

Lepszym podejsciem jest rewalidacja wyzwalana przez webhooka z WordPressa:

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidation-secret');

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
  }

  const body = await request.json();
  const { post_type, slug } = body;

  try {
    // Rewalidacja konkretnego posta
    if (slug) {
      revalidatePath(`/blog/${slug}`);
    }

    // Rewalidacja listy postow
    revalidatePath('/blog');

    // Rewalidacja po tagu
    revalidateTag('wordpress-posts');

    return NextResponse.json({
      revalidated: true,
      date: new Date().toISOString(),
    });
  } catch (error) {
    return NextResponse.json(
      { message: 'Error revalidating' },
      { status: 500 }
    );
  }
}

Webhook w WordPressie#

// functions.php
add_action('save_post', function ($post_id, $post) {
    if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
        return;
    }

    if ($post->post_status !== 'publish') {
        return;
    }

    $webhook_url = 'https://twojadomena.pl/api/revalidate';

    wp_remote_post($webhook_url, [
        'headers' => [
            'Content-Type' => 'application/json',
            'x-revalidation-secret' => defined('REVALIDATION_SECRET')
                ? REVALIDATION_SECRET
                : '',
        ],
        'body' => json_encode([
            'post_type' => $post->post_type,
            'slug'      => $post->post_name,
            'post_id'   => $post_id,
        ]),
        'timeout' => 10,
    ]);
}, 10, 2);

Autoryzacja i tryb podgladu#

Tryb podgladu pozwala redaktorom widziec nieopublikowane zmiany w tresci bezposrednio na frontendzie Next.js.

Endpoint podgladu w Next.js#

// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get('secret');
  const slug = request.nextUrl.searchParams.get('slug');
  const postType = request.nextUrl.searchParams.get('post_type') || 'post';

  if (secret !== process.env.WORDPRESS_PREVIEW_SECRET || !slug) {
    return new Response('Invalid token', { status: 401 });
  }

  const draft = await draftMode();
  draft.enable();

  const path = postType === 'page' ? `/${slug}` : `/blog/${slug}`;
  redirect(path);
}

// app/api/exit-preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export async function GET() {
  const draft = await draftMode();
  draft.disable();
  redirect('/');
}

Pobieranie draftow z WordPressa#

// lib/wordpress.ts
export async function getPreviewPost(
  id: number,
  authToken: string
): Promise<WPPost | null> {
  const authenticatedClient = new GraphQLClient(
    process.env.WORDPRESS_GRAPHQL_URL!,
    {
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${authToken}`,
      },
    }
  );

  const query = gql`
    query GetPreviewPost($id: ID!) {
      post(id: $id, idType: DATABASE_ID, asPreview: true) {
        title
        content
        slug
        date
        featuredImage {
          node {
            sourceUrl
            altText
            mediaDetails {
              width
              height
            }
          }
        }
      }
    }
  `;

  const data = await authenticatedClient.request<{ post: WPPost | null }>(
    query,
    { id }
  );

  return data.post;
}

Konfiguracja podgladu w WordPressie#

// functions.php
add_filter('preview_post_link', function ($link, $post) {
    $frontend_url = 'https://twojadomena.pl';
    $secret = defined('PREVIEW_SECRET') ? PREVIEW_SECRET : '';
    $slug = $post->post_name ?: $post->ID;

    return sprintf(
        '%s/api/preview?secret=%s&slug=%s&post_type=%s',
        $frontend_url,
        $secret,
        $slug,
        $post->post_type
    );
}, 10, 2);

SEO w Headless WordPressie#

SEO jest jednym z glownych powodow, dla ktorych firmy wybieraja WordPress. W architekturze headless nie tracimy tych mozliwosci - wrecz je ulepszamy.

Integracja z Yoast SEO#

Wtyczka WPGraphQL Yoast SEO udostepnia dane SEO z Yoast przez GraphQL:

query GetPostSEO($slug: ID!) {
  post(id: $slug, idType: SLUG) {
    seo {
      title
      metaDesc
      canonical
      opengraphTitle
      opengraphDescription
      opengraphImage {
        sourceUrl
        mediaDetails {
          width
          height
        }
      }
      twitterTitle
      twitterDescription
      twitterImage {
        sourceUrl
      }
      schema {
        raw
      }
    }
  }
}

Generowanie metadanych w Next.js#

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post?.seo) return {};

  return {
    title: post.seo.title,
    description: post.seo.metaDesc,
    alternates: {
      canonical: post.seo.canonical || `/blog/${slug}`,
    },
    openGraph: {
      title: post.seo.opengraphTitle || post.seo.title,
      description: post.seo.opengraphDescription || post.seo.metaDesc,
      type: 'article',
      publishedTime: post.date,
      modifiedTime: post.modified,
      images: post.seo.opengraphImage
        ? [{
            url: post.seo.opengraphImage.sourceUrl,
            width: post.seo.opengraphImage.mediaDetails?.width,
            height: post.seo.opengraphImage.mediaDetails?.height,
          }]
        : [],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.seo.twitterTitle || post.seo.title,
      description: post.seo.twitterDescription || post.seo.metaDesc,
    },
  };
}

Generowanie sitemap#

// app/sitemap.ts
import { getAllPosts } from '@/lib/wordpress';

export default async function sitemap() {
  const posts = await getAllPosts(1000);

  const blogEntries = posts.map((post) => ({
    url: `https://twojadomena.pl/blog/${post.slug}`,
    lastModified: new Date(post.modified || post.date),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));

  return [
    {
      url: 'https://twojadomena.pl',
      lastModified: new Date(),
      changeFrequency: 'daily' as const,
      priority: 1,
    },
    {
      url: 'https://twojadomena.pl/blog',
      lastModified: new Date(),
      changeFrequency: 'daily' as const,
      priority: 0.9,
    },
    ...blogEntries,
  ];
}

Porownanie wydajnosci: Headless vs Tradycyjny#

Testy przeprowadzone na rzeczywistym projekcie z 500 postami pokazuja znaczace roznice:

| Metryka | Tradycyjny WordPress | Headless (Next.js + WP) | |---------|---------------------|------------------------| | TTFB | 800-1500 ms | 50-150 ms | | LCP | 2.5-4.0 s | 0.8-1.5 s | | FID / INP | 150-300 ms | 30-80 ms | | CLS | 0.1-0.25 | 0.01-0.05 | | Lighthouse Score | 55-75 | 90-100 | | Czas budowania (500 postow) | N/A | ~3-5 min | | Koszt hostingu | $20-50/msc (dobry hosting) | $0-20/msc (Vercel free/pro) |

Glowne czynniki poprawy wydajnosci:

  • Statyczne strony - HTML generowany z wyprzedzeniem, serwowany z CDN
  • Optymalizacja obrazow - automatyczna konwersja do WebP/AVIF, lazy loading
  • Code splitting - tylko niezbedny JavaScript ladowany na kazdej stronie
  • Edge caching - tresc serwowana z najblizszego wezla CDN

Strategie wdrozeniowe#

Architektura docelowa#

[Redaktor] --> [WordPress CMS] --> [Webhook]
                     |                  |
                     v                  v
              [WPGraphQL API]    [On-Demand ISR]
                     |                  |
                     v                  v
              [Next.js Build]   [Vercel Edge CDN]
                     |                  |
                     v                  v
              [Statyczne strony] <------+
                     |
                     v
              [Uzytkownik]

Hosting WordPressa#

Rekomendowane opcje hostingu dla WordPress jako headless CMS:

  1. WordPress.com Business - zarzadzany hosting, automatyczne aktualizacje
  2. Kinsta - wysoka wydajnosc, wsparcie PHP 8.x, staging
  3. WP Engine - dedykowany hosting WordPress, zaawansowane narzedzia
  4. DigitalOcean Droplet - pelna kontrola, niski koszt

Wdrozenie Next.js na Vercel#

# Instalacja Vercel CLI
npm install -g vercel

# Wdrozenie
vercel --prod

# Konfiguracja zmiennych srodowiskowych
vercel env add WORDPRESS_GRAPHQL_URL
vercel env add REVALIDATION_SECRET
vercel env add WORDPRESS_PREVIEW_SECRET

Docker Compose dla srodowiska deweloperskiego#

# docker-compose.yml
services:
  wordpress:
    image: wordpress:6.4-php8.2-apache
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: secret
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
      - ./wp-plugins:/var/www/html/wp-content/plugins
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: secret
      MYSQL_ROOT_PASSWORD: rootsecret
    volumes:
      - db_data:/var/lib/mysql

  nextjs:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      WORDPRESS_GRAPHQL_URL: http://wordpress:80/graphql
    depends_on:
      - wordpress

volumes:
  wp_data:
  db_data:

Podsumowanie#

WordPress jako headless CMS w polaczeniu z Next.js to potezna kombinacja, ktora laczy znajomy interfejs zarzadzania trescia z nowoczesna, wydajna warstwa prezentacyjna. Kluczowe korzysci to:

  • Wydajnosc - statyczne strony serwowane z CDN, czas ladowania ponizej 1 sekundy
  • SEO - pelna kontrola nad metadanymi, server-side rendering, automatyczne sitemapy
  • Elastycznosc - dowolna technologia frontendowa, wielokanalowa dystrybucja tresci
  • Bezpieczenstwo - oddzielenie panelu admina od publicznej strony
  • Skalowalnosc - tresc na CDN, backend tylko do edycji

W MDS Software Solutions Group specjalizujemy sie w budowie nowoczesnych rozwiazan webowych opartych na architekturze headless. Pomagamy firmom w migracji z tradycyjnego WordPressa do architektury headless, projektujemy i wdrazamy frontendy w Next.js, integrujemy systemy CMS z aplikacjami mobilnymi i innymi kanalami dystrybucji.

Potrzebujesz wydajnej strony opartej o WordPress i Next.js? Skontaktuj sie z nami - nasi eksperci pomoga Ci wybrac najlepsza architekture i wdrozyc rozwiazanie dopasowane do Twoich potrzeb.

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

WordPress jako Headless CMS z Next.js - Kompletny przewodnik | MDS Software Solutions Group | MDS Software Solutions Group