20 Minuten. Fünf Dateien. A create-next-app Das Projekt zieht Beiträge von einer WordPress-Installation über WPGraphQL, der Editor besitzt immer noch jeden Absatz und next build Schiffe statische Seiten mit revalidate Setzen Sie auf die gewünschte Nummer. Der Walk-Through unter den Punkten woographqldemo.wpengine.com – Swap in Ihrer eigenen Domain am Ende und die gleichen sechs Schritte gelten.
Was ist in der Box
@axistaylor/nextpress ist ein npm-Paket und ungefähr 6.000 Zeilen TypeScript. Es trägt drei Teile, die Sie in eine Next.js 16-App verkabeln:
withWCR— aNextConfigWrapper, der die WP Reverse-Proxy-Domäne einrichtet und weiterleitet/wp-content,/wp-admin, und/graphqlDurch die nächste Laufzeit.<Content>– eine React-Komponente, die das rohe HTML-WordPress zurückgibt und als echte React-Knoten darstellt, plus das pro Block benötigte CSS.nextImageParser– ein Parser, der alle tauscht<img>in WP-Gehalt für einenext/imagemitwidth,height, undsizesAus den Blockattributen gezogen.
Backend Anforderung: WordPress mit WPGraphQL aktiv. Jeder Host funktioniert – WP Engine, Pantheon, Bluehost, ein selbstverwalteter VPS – solange /graphql antwortet.
Schritt 1 – Bootstrap die nächste App
Bestand create-next-app mit der TypeScript-Vorlage, dann hinzufügen @axistaylor/nextpress ab npm:
npx create-next-app@latest nextpress-quickstart
cd nextpress-quickstart
npm install @axistaylor/nextpressSchritt 2 — next.config.ts
withWCR Es gibt drei Argumente: die Basis Next config, den WordPress-Ursprung und den Frontend-Ursprung. Der Wrapper schreibt jede absolute WordPress-URL, die der Editor verfasst hat, in einen Frontend-relativen Pfad um, so dass Links in gerenderten Post-Inhalten auf Ihren nächsten Routen landen.
import type { NextConfig } from "next";
import { withWCR } from "@axistaylor/nextpress/withWCR";
const wpDomain = "woographqldemo.wpengine.com";
const wpProtocol = "https";
const nextConfig: NextConfig = {
images: {
formats: ["image/avif", "image/webp"],
remotePatterns: [{ protocol: "https", hostname: wpDomain }],
},
env: {
GRAPHQL_ENDPOINT: `${wpProtocol}://${wpDomain}/graphql`,
},
};
export default withWCR(
nextConfig,
{
wpDomain,
wpProtocol,
wpHomeUrl: `${wpProtocol}://${wpDomain}`,
wpSiteUrl: `${wpProtocol}://${wpDomain}`,
},
{
frontendDomain: "localhost:3000",
frontendProtocol: "http",
},
);Schritt 3 — src/proxy.ts
Nächste 16 umbenannt middleware.ts bis proxy.tsDie Rolle und das Matcher-Verhalten haben sich nicht geändert. Der Handler macht zwei Dinge – vorwärts /wp-* Wege durch proxyByWCR So berührt der Browser nie den WordPress-Ursprung und legt eine x-uri Header für jede nicht bereitgestellte Anfrage, damit die Serverkomponenten nachgelagert wissen, welche URI das Backend nach enqueued Skripten und Stylesheets fragen soll:
import { NextResponse, NextRequest } from "next/server";
import { proxyByWCR, isProxiedRoute } from "@axistaylor/nextpress/proxyByWCR";
export const proxy = async (request: NextRequest) => {
const pathname = request.nextUrl.pathname;
if (isProxiedRoute(pathname)) {
return proxyByWCR(request);
}
const headers = new Headers(request.headers);
headers.set("x-uri", pathname);
return NextResponse.next({ request: { headers } });
};
export const config = {
matcher: [
"/atx/:instance/proxiee",
"/atx/:instance/wp",
"/atx/:instance/wc",
"/atx/:instance/wp-internal-assets/:path*",
"/atx/:instance/wp-assets/:path*",
"/atx/:instance/wp-json/:path*",
"/((?!_next|api|favicon.ico|sw.js|.*\\.).*)",
],
};Schritt 4 — src/lib/wp.ts
Drei async-Funktionen, eine geteilt fetch Wrapper. fetchPosts füttert die Indexseite, fetchPostBySlug füttert die Postseite, fetchPostSlugs Antriebe generateStaticParams. next: { revalidate: 60 } Hinweis auf die fetch ist die einzige Cache-Steuerung, die Sie benötigen – Next übernimmt den Rest:
const ENDPOINT = process.env.GRAPHQL_ENDPOINT!;
async function gql<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
next: { revalidate: 60 },
});
if (!res.ok) throw new Error(`WPGraphQL ${res.status}`);
const { data, errors } = await res.json();
if (errors?.length) throw new Error(errors[0].message);
return data as T;
}
export interface PostSummary {
slug: string;
title: string;
date: string;
excerpt: string | null;
featuredImage: { sourceUrl: string; altText: string } | null;
}
export interface Post extends PostSummary {
content: string;
contentCssClasses: string;
}
export async function fetchPosts(first = 20): Promise<PostSummary[]> {
const data = await gql<{ posts: { nodes: PostSummary[] } }>(`
query Posts($first: Int!) {
posts(first: $first, where: { status: PUBLISH, orderby: { field: DATE, order: DESC } }) {
nodes {
slug title date excerpt
featuredImage { node { sourceUrl altText } }
}
}
}`, { first });
return data.posts.nodes;
}
export async function fetchPostSlugs(): Promise<string[]> {
const data = await gql<{ posts: { nodes: { slug: string }[] } }>(`
{ posts(first: 100, where: { status: PUBLISH }) { nodes { slug } } }`);
return data.posts.nodes.map(n => n.slug);
}
export async function fetchPostBySlug(slug: string): Promise<Post | null> {
const data = await gql<{ post: Post | null }>(`
query Post($slug: ID!) {
post(id: $slug, idType: SLUG) {
slug title date excerpt
content(format: RENDERED)
contentCssClasses
featuredImage { node { sourceUrl altText } }
}
}`, { slug });
return data.post;
}Schritt 5 — /blog Index
Eine Serverkomponente, eine awaitKeine Client-Hooks. Das Post-Array ist nur JSX:
import Link from "next/link";
import { fetchPosts } from "@/lib/wp";
export const metadata = { title: "Blog" };
export default async function BlogIndex() {
const posts = await fetchPosts(20);
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
<time>{new Date(post.date).toLocaleDateString()}</time>
</li>
))}
</ul>
);
}Schritt 6 — /blog/[slug]
generateStaticParams verwandelt jede veröffentlichte Slug in eine Build-Time-Route. Das Rendering besteht aus zwei Linien: einer Überschrift und einer <Content>.
import { notFound } from "next/navigation";
import { Content, nextImageParser } from "@axistaylor/nextpress";
import { fetchPostBySlug, fetchPostSlugs } from "@/lib/wp";
export async function generateStaticParams() {
const slugs = await fetchPostSlugs();
return slugs.map((slug) => ({ slug }));
}
export default async function PostPage({ params }: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetchPostBySlug(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<Content
content={post.content}
contentCssClasses={post.contentCssClasses}
parsers={[nextImageParser()]}
/>
</article>
);
}Fünf Dateien, zwei Domänen, eine npx. Der Editor bewegt sich nie; die Bereitstellungsschicht ändert sich darunter.

Was <Content> tut es für dich
Der Post Body ist eine Folge von WordPress Block HTML. Einfügen in eine dangerouslySetInnerHTML Würde es machen, aber Sie würden verlieren drei Dinge in den Prozess. <Content> behandelt sie:
- Zerlegt das HTML in einen React Tree. Alle
<p>,<figure>,<blockquote>ist ein echter React-Knoten, den Sie mit einem Parser überschreiben können – so ist esnextImageParserSwaps<img>fürnext/imagemit null Markup-Änderungen vorgelagert. - Zieht das CSS jedes Blocks in die Antwort. Block unterstützt Inline-Regeln, theme.json-Palette-Token, Plugin-injizierte Blätter – sie werden inline mit der Seite ausgeliefert, so dass es nach der Hydratation keine Layoutverschiebung gibt.
- Bestände
<script type="application/json">Blobs intakt. So werden interaktive Blöcke (Motion Engines, Akkordeons, alles mit einem View-Script) ohne separate API-Roundtrip in ihren clientseitigen Code eingegeben.
Nächste Schritte
Die gleichen sechs Dateien erstrecken sich in einige offensichtliche Richtungen. Kategorien und Tags sind eine weitere WPGraphQL where Argumentation fetchPosts. Kommentare und Paginierung laufen auf derselben Abfrage. Search trifft die search Argumentation und Sie liefern ein /search?q=... Route in 20 Minuten.
Vollständige Quelle am GitHubPaketdokumente unter /docs/nextpressWenn Sie das architektonische Warum hinter dem Halten von WordPress und dem Hinzufügen von Next.js als Bereitstellungsschicht wollen, kommt dieses Stück.

Leave a Reply