WordPress jako Headless CMS z Next.js - Kompletny przewodnik
WordPress jako Headless
backendWordPress 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:
- WordPress.com Business - zarzadzany hosting, automatyczne aktualizacje
- Kinsta - wysoka wydajnosc, wsparcie PHP 8.x, staging
- WP Engine - dedykowany hosting WordPress, zaawansowane narzedzia
- 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.
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.