Der Blog funktioniert. Fügen Sie nun den Shop hinzu. Dieser Walkthrough nimmt genau dort an, wo Das Headless Blog Tutorial Blätter ab. Zwei neue WordPress-Plugins, eine Routengruppe, so dass die WP-gerenderten Seiten ein eigenes Root-Layout erhalten, und ein einzelner Header – Cart-Token Überbrückung Ihrer Next.js-Sitzung mit der WooCommerce Store API. Die gleiche App, die Ihren Blog bedient, serviert jetzt eine Storefront, wobei Warenkorb und Kasse jede WooCommerce-Erweiterung durchlaufen, die Sie bereits installiert haben.
Companion repo: github.com/AxisTaylor/nextpress-woographql-quickstart. Es ist nextpress-quickstart aus dem Blog-Tutorial mit allem in diesem Beitrag oben angewendet – Klonen Sie es, laufen npm install && npm run dev, und Sie haben den ganzen Fluss läuft gegen woographqldemo.wpengine.com.

Für wen das ist
Sie haben eine bestehende Next.js-App – Ihre oder die, die Sie nach dem Blog-Tutorial erstellt haben. Sie wollen Handel: Produkte, Warenkorb, Kasse, Zahlungen, Inventar, Steuern, Versand, alles. Sie sind nicht daran interessiert, diese letzten sechs Dinge von Grund auf zu schreiben. WooCommerce betreibt ~ 5 Millionen Live-Storefronts und der Erweiterungsmarktplatz ist der größte in jeder Open-Source-Commerce-Plattform. Das Headless WordPress-Setup, das Sie für den Blog erstellt haben, ist auch das Headless WooCommerce-Setup – dasselbe Backend, derselbe Proxy, zwei weitere Plugins und ein Layout-Refactor.
Ein Hinweis auf auth: Es gibt keine
Dieses Tutorial geht durch eine Nur für Gäste Storefront. Keine Anmeldeseite, kein Account-Dashboard, keine JWT-Aktualisierungsschleife. Die Cart-Token WooCommerce Handsback ist selbst die Sitzungskennung – ein Gast kann durchsuchen, zum Warenkorb hinzufügen, auschecken und seine Bestellung mit nichts als seiner Rechnungs-E-Mail und dem Bestellschlüssel aus seiner Bestätigung nachschlagen. Das deckt den gesamten glücklichen Weg für die meisten Storefronts ab. Wenn Sie authentifizierte Kundenkonten wünschen, verkabeln Sie wp-graphql-headless-login oder wp-graphql-jwt-Authentifizierung separat – es ist so konzipiert, dass es neben dem Cart-Token-Fluss angeschlossen wird, ohne ihn zu stören.
Was ändert sich vom Blog-Tutorial
- Zwei neue WP-Plugins: WooCommerce und WPGraphQL für WooCommerce.
- Streckengruppe aufgeschlüsselt. Löschung
app/layout.tsxIn-App-Routen verschiebenapp/(main)/mit eigenem Root-Layout. Verschieben Sie WP-gerenderte Routen (Cart, Checkout, Blog) inapp/(wordpress-pages)/mit einem separaten Root-Layout, das setzt<WPHead />innen<head>Importmaps und Skriptmodule, die vom WC-Checkout-Block ausgestrahlt werden, werden nur dann korrekt aufgelöst, wenn das Importmap-Tag im Dokument gespeichert ist.<head>, und ein verschachteltes Layout kann es nicht dort setzen, wenn es ein äußeres Root-Layout gibt<html>. - Middleware überbrückt die Sitzung. Lesen Sie
sessionTokenCookie auf jeder proxied WP REST / AJAX Anfrage, fügen Sie es als dieCart-TokenHeader. Auffangen beliebig gedrehtCart-Tokenauf die Antwort und beharren sie zurück in das Cookie, so dass die nächste Anfrage in der gleichen WC-Sitzung bleibt. - Serverseitige GraphQL holt den gleichen Header weiter. Beide
fetchPageByUriUNDfetchAssetsByUrimuss enthaltenCart-TokenÜberspringen auffetchAssetsByUriist der einzige schärfste gotcha – die wc checkout block server-renders gegen einen leeren wagen und backt ein."Cannot create order from empty cart"Fehler inwcSettings.checkoutData, und die Seite ist dauerhaft stecken. - Eine polierte Produktseite mit einem Variationsselektor für variable Produkte (nach Attributen abgestimmt)
name, nichtlabelEs gibt ein subtiles gotcha für lokale attribute. - Guest Order Lookup über eine Serveraktion.
/view-ordernimmt eine Rechnungs-E-Mail + Bestellschlüssel und macht die Bestellung. Die Server-Aktion bindet die E-Mail an den aktuellenCart-TokenSitzung viaupdateCustomer, verengt dann die Orderverbindung per Orderschlüssel. Kein Anwendungspasswort, kein Shop-Manager-Account. - Automatischer Empfang bei
/checkout/order-received/[id]. Eine Clientkomponente erfasst die Abrechnungs-E-Mail aus dem WC-Checkout-Block in einem Cookie. Die Quittungsseite liest das Cookie + die?key=auf der URL und sucht die Bestellung durch die gleiche Serveraktion nach – kein Formular zum Ausfüllen. - Produktbewertungen Verdrahtet durch eine kleine
/api/product/reviewRoute, die WooGraphQL aufruftwriteReviewMutation mit dem Cart-Token weitergeleitet.
Schritt 1 – Installieren Sie zwei WP-Plugins
- WooCommerce Aus dem Plugin-Verzeichnis. Führen Sie den Setup-Assistenten aus, lassen Sie den Stripe-Test für Zahlungen fallen, fügen Sie ein paar Demo-Produkte hinzu, damit Sie etwas abfragen können. Bestätigung
/cartund/checkoutVerwenden Sie die Blockvorlagen (Standard seit Woo 8.3) anstelle der Legacy-Shortcodes – das wird NextPress Proxy. - WPGraphQL für WooCommerce – fügt die Produkt-, Warenkorb-, Kunden- und Bestelltypen hinzu
/graphqlÜberprüfen Sie in Postman (oder einem HTTP-Client, der Antwort-Header aufdeckt – GraphiQL verbirgt sie), indem Sie dieaddToCartMutation gegen eine realeproductIdohneCart-Tokenüber den Antrag. WP erstellt eine neue Gästesitzung und gibt die JWT imCart-TokenResponse Header.
mutation BootGuestSession {
addToCart(input: { productId: 13, quantity: 1 }) {
cart {
contents { itemCount }
total
}
}
}Die Cart-Token Header nur Schiffe auf Wagen und Session Mutationen — addToCart, updateItemQuantity, removeItemsFromCart, applyCouponund Freunde. Plain Queries gegen /graphql Produzieren Sie keine, und GraphiQL zeigt sowieso keine Antwort-Header an, weshalb dieser Verifizierungsschritt Postman verwendet. Sobald die Middleware diesen Header auf einer proxied Antwort sieht, bleibt der neue Wert in der sessionToken Cookie und jede nachfolgende Anfrage – REST-Aufrufe aus dem Checkout-Block, serverseitige GraphQL-Abfragen in Ihrer Next.js-App – werden darauf übertragen.

Schritt 2 — Aufteilung der Streckengruppe
Dies ist der größte Strukturwandel. Löschung app/layout.tsxVerschieben Sie jede Route unter eine von zwei Top-Level-Routengruppen mit jeweils eigenem Root-Layout:
src/app/
├── api/
│ ├── cart/route.ts ← cart mutations (server-only)
│ └── product/review/route.ts ← review submission (server-only)
├── globals.css
├── (main)/ ← in-app routes
│ ├── layout.tsx ← <html>/<body> for the catalog + lookup surface
│ ├── page.tsx ← /
│ ├── products/
│ │ ├── page.tsx ← /products
│ │ └── [slug]/page.tsx ← /products/[slug] (Simple + Variable)
│ └── view-order/page.tsx ← /view-order (guest order lookup)
└── (wordpress-pages)/ ← WP-rendered routes
├── layout.tsx ← <html><head><WPHead /></head><body>
├── cart/page.tsx ← /cart
├── checkout/
│ ├── page.tsx ← /checkout (+ CheckoutEmailCapture)
│ └── order-received/[id]/page.tsx ← auto-receipt
└── blog/
├── page.tsx
└── [slug]/page.tsx
Warum? Next.js erlaubt nur <html> und <body> in Root Layouts. ohne app/layout.tsxJede top-level-routengruppe wird zu einer eigenen wurzel. Die (wordpress-pages) Wurzel kann setzen <WPHead /> direkt innen <head> – wo die <script type="importmap"> Ausgestrahlt für die Modul-Skripte von WC hat zu leben. Versuchen Sie dies mit einem verschachtelten Layout unter einer einzigen Wurzel und die Importmap endet in <body>; Modulskripte lösen sich dann nicht auf @wordpress/plugins und der gesamte Wagenblockbaum wirft auf init.
import { headers } from 'next/headers';
import { WPHead, WPFooter } from '@axistaylor/nextpress';
import { fetchAssetsByUri, fetchGlobalStyles } from '@/lib/wp';
export const dynamic = 'force-dynamic';
const CRITICAL_STYLESHEETS = [
'wp-block-library', 'wp-block-library-theme', 'global-styles',
'classic-theme-styles', 'wc-blocks-style', 'wc-blocks-vendors-style',
];
export default async function WordPressLayout({ children }) {
const uri = (await headers()).get('x-uri') || '/';
const [{ scripts, stylesheets, importMap }, globalStyles] = await Promise.all([
fetchAssetsByUri(uri),
fetchGlobalStyles(),
]);
return (
<html lang="en">
<head>
<WPHead
scripts={scripts}
stylesheets={stylesheets}
globalStyles={globalStyles}
importMap={importMap}
pathname={uri}
criticalHandles={CRITICAL_STYLESHEETS}
/>
</head>
<body>
<main>{children}</main>
<WPFooter scripts={scripts} pathname={uri} />
</body>
</html>
);
}Schritt 3 – Middleware überbrückt den Cart-Token
Aktualisierung src/proxy.ts das Warenkorb-Token an jede proxied WP REST/AJAX-Anfrage anhängen und dann ein beliebiges gedrehtes Token erfassen, das WP auf die Antwort zurückschreibt. Wenn das Cart-Block-Frontend holt /wc/store/v1/cart vom Browser aus geht es durch Ihren NextPress-Proxy – und dieser Proxy muss Ihre sessionToken Cookie in die Cart-Token Header WooCommerce erwartet. WooCommerce dreht gelegentlich den Token (nach einer Warenkorbmutation, einem Coupon usw.); Der Antwort-Header zeigt, wie er den neuen Wert signalisiert.
import { NextResponse, NextRequest } from 'next/server';
import {
proxyByWCR, isProxiedRoute,
isWCAjaxRequest, isWPAjaxRequest, isWPRestRequest,
} from '@axistaylor/nextpress/proxyByWCR';
export const proxy = async (request: NextRequest) => {
const pathname = request.nextUrl.pathname;
// 1. Vorwärts das Cart-Token auf jedem proxied REST / AJAX Anruf, so dass die
// Browserseitige WC-Blöcke landen in der eigentlichen Warenkorbsitzung des Gastes.
if (
isProxiedRoute(pathname) &&
(isWPAjaxRequest(pathname) || isWCAjaxRequest(pathname) || isWPRestRequest(pathname))
) {
const sessionToken = request.cookies.get('sessionToken')?.value;
if (sessionToken) request.headers.set('Cart-Token', sessionToken);
}
// 2. Proxy der Anforderung, dann erfassen Sie alle gedrehten Cart-Token WP schreibt
// zurück auf die Antwort und beharren sie als SessionToken-Cookie.
if (isProxiedRoute(pathname)) {
const response = await proxyByWCR(request);
const rotated = response.headers.get('Cart-Token');
if (rotated) {
const next = new NextResponse(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
next.cookies.set({
name: 'sessionToken',
value: rotated,
path: '/',
maxAge: 30 * 24 * 60 * 60,
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax',
});
return next;
}
return response;
}
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 — GraphQL-Helfer, die das Token weiterleiten
Zwei Helfer in src/lib/wp.ts Sprich mit WPGraphQL: gqlWithSession für Abfragen, die die Warenkorbsitzung benötigen, und fetchAssetsByUri / fetchPageByUri die beide benutzen. Beide müssen den Cart-Token weiterleiten – und der zweite ist der gotcha.
import 'server-only';
import { cookies } from 'next/headers';
const endpoint = process.env.GRAPHQL_ENDPOINT as string;
interface FetchOptions { sessionToken?: string | null }
export async function gqlWithSession<T>(
query: string,
variables: Record<string, unknown> | undefined,
options: FetchOptions = {},
) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (options.sessionToken) headers['Cart-Token'] = `${options.sessionToken}`;
const res = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
cache: 'no-store',
});
const json = await res.json();
return { data: json.data ?? null, errors: json.errors };
}
// Kritisch: vorwärts Cart-Token auch hier. Der assetsByUri Resolver
// Triggert WP-seitige Skriptqueueing, die WCs läuft
// hydrate data from api request für den Checkout-Block. Ohne die
// header, wc()->cart ist für diese Anforderung leer, das Hydrat produziert
// "Kann keine Ordnung aus leerem Wagen erstellen", und dieser Fehler wird in
// wcSettings.checkoutData → der Checkout-Block hatCheckoutError
// Tore feuern dauerhaft.
export async function fetchAssetsByUri(uri: string) {
const c = await cookies();
const sessionToken = c.get('sessionToken')?.value ?? null;
const { data } = await gqlWithSession<{ assetsByUri: any }>(
`query ($uri: String!) {
assetsByUri(uri: $uri) {
importMap(scheme: RELATIVE) { name path }
enqueuedStylesheets(first: 500) {
nodes { handle src version before after }
}
enqueuedScripts(first: 500) {
nodes { handle src strategy version group location type before after
dependencies { handle } }
}
}
}`,
{ uri },
{ sessionToken },
);
const a = data?.assetsByUri;
return a
? { scripts: a.enqueuedScripts.nodes, stylesheets: a.enqueuedStylesheets.nodes, importMap: a.importMap ?? [] }
: { scripts: [], stylesheets: [], importMap: [] };
}
export async function fetchPageByUri(uri: string) {
const c = await cookies();
const sessionToken = c.get('sessionToken')?.value ?? null;
const { data } = await gqlWithSession<{ page: any }>(
`query ($uri: ID!) {
page(id: $uri, idType: URI) { id title content contentCssClasses }
}`,
{ uri },
{ sessionToken },
);
return data?.page ?? null;
}Dieser Kommentar zu fetchAssetsByUri ist nicht Dekoration – es ist der ganze Grund /checkout Sie scheitern still, wenn Sie es vergessen. WPGraphQL ist in Ordnung, die REST-Aufrufe des Browsers funktionieren, der Warenkorb REST gibt die richtigen Elemente zurück – und die Seite macht immer noch einen Leerwagenfehler, weil die serverseitigen vorinstallierten Daten der WC-Blöcke gegen eine leere Warenkorbsitzung generiert wurden. Weiterleitung des Tokens auf beide Der serverseitige GraphQL schließt die Lücke.
Schritt 5 – Cart als API-Route
Add-to-Cart, Update-Quantity, Remove-from-Cart – alle fließen durch einen /api/cart Handler. Die Route lautet: sessionToken Von Cookies läuft die WPGraphQL-Mutation mit Cart-Token und gibt den aktualisierten Warenkorb zurück. Die Middleware aus Schritt 3 kümmert sich darum, jeden gedrehten Token auf dem Weg nach draußen zu erhalten.
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { gqlWithSession } from '@/lib/wp';
const COOKIE_OPTS = { httpOnly: true, secure: true, sameSite: 'lax' as const, path: '/' };
async function readSession() {
return { sessionToken: (await cookies()).get('sessionToken')?.value ?? null };
}
interface CartActionPayload {
action: 'add' | 'update' | 'remove' | 'clear' | 'applyCoupon' | 'removeCoupon';
productId?: number;
variationId?: number;
variation?: { attributeName: string; attributeValue: string }[];
quantity?: number;
key?: string;
code?: string;
}
export async function POST(req: NextRequest) {
const payload = (await req.json()) as CartActionPayload;
const auth = await readSession();
if (payload.action === 'add') {
const result = await gqlWithSession<{ addToCart: { cart: unknown } }>(
`mutation Add($input: AddToCartInput!) {
addToCart(input: $input) { cart { contents { itemCount } total } }
}`,
{ input: {
productId: payload.productId,
variationId: payload.variationId,
variation: payload.variation, // local-attribute variations need this
quantity: payload.quantity ?? 1,
} },
auth,
);
if (result.errors?.length) {
return NextResponse.json({ error: result.errors[0].message }, { status: 400 });
}
return NextResponse.json({ cart: result.data?.addToCart?.cart ?? null });
}
// ...update / remove / applyCoupon / clear folgt dem gleichen Muster
}Anmerkung: variation Array am Eingang. Variable-Produkt-Add-to-Cart benötigt es für Variationen, die globale und lokale Attribute mischen – siehe Schritt 7.

Schritt 6 — Eine polierte Produktseite
Produktseiten konvertieren Browser in Käufer, also verbringen Sie hier echte Designzeit. Server-Komponente, ISR-Cache, einzelne GraphQL-Abfrage für die gesamte Ansicht: Titel, Heldenbild, Galerie (Ehrung des natürlichen Seitenverhältnisses jedes Bildes von) mediaDetails), Preis + Verkaufsabzeichen, Aktienabzeichen, kurze + lange Beschreibungen, verwandte Produkte, Brotkrumen und Produktschema.org JSON-LD für Google Shopping. Die Seite versendet am __typename zu einer von zwei Heldenkomponenten. Beide rendern Mobile-First – der meiste Storefront-Verkehr ist mobil, und der Screenshot unten ist die Small-View-Ansicht, um dies zu beweisen.
import { notFound } from 'next/navigation';
import { fetchProductBySlug, fetchProductSlugs } from '@/lib/wp';
import { SimpleProductHero } from '@/components/SimpleProductHero';
import { VariableProductHero } from '@/components/VariableProductHero';
import { ProductTabs } from '@/components/ProductTabs';
export const revalidate = 300;
export async function generateStaticParams() {
return (await fetchProductSlugs()).map((slug) => ({ slug }));
}
export default async function ProductPage({ params }) {
const { slug } = await params;
const product = await fetchProductBySlug(slug);
if (!product) notFound();
return (
<>
<article className="grid lg:grid-cols-[1.1fr_1fr] gap-12 max-w-wide mx-auto px-x-small py-medium">
{product.__typename === 'VariableProduct'
? <VariableProductHero product={product} />
: <SimpleProductHero product={product} />}
</article>
<ProductTabs product={product} />
</>
);
}
Schritt 7 — Variationen für variable Produkte
VariableProductHero ist eine Client-Komponente, die den Status der Attributauswahl besitzt und den Preis, das Bild und das Aktienabzeichen an die übereinstimmende Variation anhebt. Zwei kleine, aber tragende Details:
- Match von
name, nichtlabel. Für globale Attribute (taxonomiegestützt):pa_color,pa_size, beidenameundlabelEinvernehmen überProductAttributeundVariationAttribute. Für lokale Attribute (freie Form auf dem Produkt, keine Taxonomie),ProductAttribute.labelist die menschliche Form („Logo), aberVariationAttribute.labelistsanitize_title()’d („Logo). Die beidenlabelFelder sind nicht vergleichbar.namegeht durchsanitize_title()auf beiden Seiten, also stimmt es immer zu – match byname. - Senden Sie a
variationArray auf Add-to-Cart. Wenn die Auswahl des Kunden einer echtenvariationIdSenden Sie beide – WooGraphQL kann das Warenkorbelement an diese Variation anheften, unabhängig davon, ob die Attribute global oder lokal sind.
// VariationAttribute.label ist sanitize title()'d für LOCAL Attribute
// (z.B. "Logo" → "Logo") aber ProductAttribute. Label ist die menschliche Form
// ("Logo"). Beide entlarven `name`, die sanitize title()'d auf beiden ist
// Seiten - also passen sie nach "name", nie nach "label".
export interface VariationAttrSel { name: string; value: string }
export function variationMatches(
variationAttrs: { name: string; value: string }[],
selection: VariationAttrSel[],
): boolean {
if (variationAttrs.length === 0) return false;
return variationAttrs.every((va) => {
if (va.value === null) return true; // "any" — accepts everything
const sel = selection.find((s) => s.name === va.name);
return sel?.value === va.value;
});
}
export function findMatchingVariation<V extends {
attributes: { nodes: { name: string; value: string }[] };
}>(
variations: V[],
selection: VariationAttrSel[],
): V | undefined {
return variations.find((v) =>
variationMatches(v.attributes.nodes, selection));
}'use client';
import { useState } from 'react';
import { findMatchingVariation } from '@/lib/variation-helpers';
import { AddToCartButton } from './AddToCartButton';
export function VariableProductHero({ product }) {
const [selection, setSelection] = useState({});
const current = findMatchingVariation(product.variations.nodes,
Object.entries(selection).map(([name, value]) => ({ name, value })));
const price = current?.price ?? product.price;
const image = current?.image ?? product.image;
const inStock = (current?.stockStatus ?? product.stockStatus) !== 'OUT_OF_STOCK';
const variationPayload = Object.entries(selection)
.map(([attributeName, attributeValue]) => ({ attributeName, attributeValue }));
return (
<>
<ProductGallery image={image} gallery={product.galleryImages.nodes} />
<ProductInfo price={price} inStock={inStock}>
{product.attributes.nodes.map((attr) => (
<AttributePicker
key={attr.name}
attribute={attr}
value={selection[attr.name]}
onChange={(v) => setSelection((s) => ({ ...s, [attr.name]: v }))}
/>
))}
<AddToCartButton
productId={product.databaseId}
variationId={current?.databaseId}
variation={variationPayload}
inStock={inStock && !!current}
/>
</ProductInfo>
</>
);
}
Schritt 8 – Warenkorb und Checkout über den NextPress-Proxy
Zwei Routen, jeweils zwölf Linien. Sie leben unter (wordpress-pages) So nehmen sie das WP-Rendering-Layout aus Schritt 2 auf:
import { notFound } from 'next/navigation';
import { Content, nextImageParser } from '@axistaylor/nextpress';
import { fetchPageByUri } from '@/lib/wp';
export default async function CartPage() {
const page = await fetchPageByUri('/cart');
if (!page) notFound();
return (
<article>
<Content
content={page.content}
contentCssClasses={page.contentCssClasses}
parsers={[nextImageParser()]}
/>
</article>
);
}Die (wordpress-pages) Layout holt den Asset Graphen (mit) Cart-Token!), stellt die Seite das WC-Cart-Block-Markup dar, und das Cart-Block-Frontend-Skript übernimmt die Hydratation – mit dem gleichen Cart-Token Middleware überbrückt. Gleiche Form für /checkout/page.tsx; gleiche Form für die Blog-Routen.

Schritt 9 — Suche nach Gästeaufträgen über eine Serveraktion
Sobald eine Bestellung existiert, benötigt der Kunde eine Möglichkeit, sie zu sehen. Der Standard-WooCommerce-Flow setzt die Quittung auf /checkout/order-received/[id]Es benötigt jedoch auch eine manuelle Lookup-Seite für wiederholte Besuche, verlorene Bestätigungsregisterkarten und E-Mail-Follow-up-Links. /view-order ist diese Seite – eine Serverkomponente mit einem Formular, das in einer Serveraktion veröffentlicht wird.
Die Handlung macht etwas Subtiles: updateCustomer ohne id Ziel ist der Kunde, der an den aktuellen Cart-Token Sitzung. Einstellung billing.email bindet diese Sitzung an die E-Mail; der Kunde orders Die verbindung gibt dann jede bestellung zurück, die gegen diese adresse aufgegeben wurde – einschließlich gastbestellungen, was der ganze punkt ist. Die Aktion verengt sich durch orderKey, die als gemeinsames Geheimnis fungiert: Eine E-Mail allein zu kennen, reicht nicht aus, um die Bestellung eines anderen aufzudecken.
'use server';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';
import { gqlWithSession, type Order, ORDER_FRAGMENT } from '@/lib/wp';
import { LOOKUP_COOKIE, ERROR_COOKIE } from '@/utils/constants';
const FIND_ORDER_MUTATION = /* GraphQL */ `
${ORDER_FRAGMENT}
mutation BindEmailAndFindOrder($input: UpdateCustomerInput!) {
updateCustomer(input: $input) {
customer {
orders(first: 100) {
nodes { ...OrderFields }
}
}
}
}
`;
// Benutzer ohne "id" wendet die Mutation auf die
// Kunden an die aktuelle Cart-Token-Sitzung angehängt. Einstellung
// billing.email bindet diese Sitzung an die E-Mail;
// Und dann gibt er jeden Auftrag zurück, der gegen
// Anschrift — einschließlich Gastbestellungen. Wir verengen durch orderKey, die
// fungiert als gemeinsames Geheimnis: Eine E-Mail allein zu kennen, reicht nicht aus
// Die Ordnung eines anderen aufdecken.
//
// Dies läuft nur über Server-Action-Aufrufe, so dass der GraphQL
// Endpunkt und die kundenbindende Semantik erreichen nie den
// Kunden. Tutorial-Leser brauchen kein Anwendungspasswort - die
// Sessions eigenes Cart-Token authentifiziert die Mutation.
export async function lookupOrder(
email: string,
orderKey: string,
): Promise<{ order?: Order; error?: string }> {
if (!email?.trim() || !orderKey?.trim()) {
return { error: 'Email and order key are required.' };
}
const sessionToken = (await cookies()).get('sessionToken')?.value ?? null;
const result = await gqlWithSession<{
updateCustomer: { customer: { orders: { nodes: Order[] } } | null } | null;
}>(
FIND_ORDER_MUTATION,
{ input: { billing: { email: email.trim() } } },
{ sessionToken },
);
if (result.errors?.length) return { error: result.errors[0].message };
const orders = result.data?.updateCustomer?.customer?.orders?.nodes ?? [];
const match = orders.find((o) => o.orderKey === orderKey.trim());
if (!match) return { error: 'No order found for that email and order key.' };
return { order: match };
}
export async function lookupOrderFormAction(formData: FormData): Promise<void> {
const email = String(formData.get('email') ?? '').trim();
const orderKey = String(formData.get('orderKey') ?? '').trim();
const result = await lookupOrder(email, orderKey);
const jar = await cookies();
if (result.error || !result.order) {
jar.set(ERROR_COOKIE, result.error ?? 'Order not found.',
{ path: '/', sameSite: 'lax', maxAge: 60 });
jar.delete(LOOKUP_COOKIE);
} else {
jar.set(LOOKUP_COOKIE, JSON.stringify({ email, orderKey }), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24,
});
jar.delete(ERROR_COOKIE);
}
revalidatePath('/view-order');
}Was dies vermeidet: Handrollen eines Shop-Manager-Anwendungspassworts in die Demo. Tutorial-Leser haben keine, und das Tutorial mit “Hier, fügen Sie Ihre Admin-Anmeldeinformationen in” .env“Das wäre unverantwortlich.” Das Cart-Token-as-shared-secret-Muster hält alles innerhalb des vorhandenen Sitzungsmodells.

Schritt 10 — Automatischer Empfang bei / Checkout / Bestellung
Wenn WooCommerce den Kunden nach einem erfolgreichen Checkout umleitet, ist die URL /checkout/order-received/[id]?key=wc_order_…. key ist der Order Key. Die E-Mail befindet sich nicht in der URL – sie befindet sich im Abrechnungsformular des WC-Checkout-Blocks, das der Kunde gerade ausgefüllt hat. Wir erfassen diese E-Mail-Client-Seite in ein Cookie, dann liest die Quittungsseite Cookie + URL-Schlüssel und läuft gleich lookupOrder Server-Aktion aus Schritt 9.
Die Capture-Komponente klingt einfach (lesen Sie ein paar E-Mail-Eingaben, schreiben Sie an ein Cookie), aber der WC-Checkout-Block ist pingelig: Er mountet seine Felder async, montiert das Abrechnungsfeld, wenn “Versandadresse für die Abrechnung verwenden” umschaltet, UND – die Überraschung – hydratisiert Feldwerte aus seinem eigenen Sitzungsspeicher ohne Feuerung input oder change EreignisseEine delegierte input Der Zuhörer allein wird das Autofill völlig vermissen. Drei Quellen decken die Lücke ab:
- Erster DOM-Scan on mount — fängt bereits zum Renderzeitpunkt vorhandene Werte (Autofill, Browser Autocomplete).
- MutationObserver am
document.bodymitattributeFilter: ['value']– fängt spät montierte Eingaben und sitzungswiederherstellte Wert schreibt. - Einfangphase
input/changeDelegation amdocument— Fänge mit Lebendtypisierung.
'use client';
import { useEffect } from 'react';
import { CHECKOUT_EMAIL_COOKIE } from '@/utils/constants';
// Der WC Checkout Block montiert seine Felder asynchron und re-mounted
// das Abrechnungsfeld, wenn der Kunde umschaltet "Versand verwenden für"
// Abrechnung. Es hydratisiert auch Werte aus seinem eigenen Sitzungsspeicher
// OHNE Eingabe- / Änderungsereignisse - also ein delegierter Zuhörer allein
// Verpasst die automatisch ausgefüllte E-Mail. Wir brauchen drei Quellen:
// 1. Erster DOM-Scan an der Halterung — erfasst bereits vorhandene Werte.
// 2. MutationObserver — fängt spät eingehängte Eingaben und
// Session-restaurierter Wert schreibt.
// 3. Eingabe-/Änderungsdelegation — Fänge Live-Benutzertypisierung.
function isEmailField(el: Element | null): el is HTMLInputElement {
if (!(el instanceof HTMLInputElement)) return false;
if (el.type === 'email') return true;
const probe = `${el.name} ${el.id} ${el.autocomplete}`.toLowerCase();
return probe.includes('email');
}
function writeCookie(value: string): void {
const trimmed = value.trim();
if (!trimmed) return;
const secure = location.protocol === 'https:' ? '; secure' : '';
document.cookie = `${CHECKOUT_EMAIL_COOKIE}=${encodeURIComponent(trimmed)}; ` +
`path=/; max-age=3600; samesite=lax${secure}`;
}
function scan(root: ParentNode): void {
root.querySelectorAll<HTMLInputElement>(
'input[type="email"], input[name*="email" i], input[id*="email" i], input[autocomplete*="email" i]',
).forEach((el) => { if (isEmailField(el) && el.value) writeCookie(el.value); });
}
export function CheckoutEmailCapture() {
useEffect(() => {
scan(document);
const handler = (e: Event) => {
if (e.target instanceof Element && isEmailField(e.target)) {
writeCookie((e.target as HTMLInputElement).value);
}
};
document.addEventListener('input', handler, true);
document.addEventListener('change', handler, true);
const observer = new MutationObserver((muts) => {
for (const m of muts) {
m.addedNodes.forEach((n) => { if (n instanceof Element) scan(n); });
if (m.type === 'attributes' && m.target instanceof HTMLInputElement
&& isEmailField(m.target)) {
writeCookie(m.target.value);
}
}
});
observer.observe(document.body, {
subtree: true, childList: true,
attributes: true, attributeFilter: ['value'],
});
return () => {
document.removeEventListener('input', handler, true);
document.removeEventListener('change', handler, true);
observer.disconnect();
};
}, []);
return null;
}Montieren Sie das auf /checkout/page.tsx Neben dem <Content> Blockieren und das Cookie wird gesetzt, wenn der Kunde auf Place Order klickt. Die Quittungsseite liest es und rendert – kein Formular, kein zusätzlicher Klick.
import Link from 'next/link';
import { cookies } from 'next/headers';
import { OrderSummary } from '@/components/OrderSummary';
import { lookupOrder } from '@/actions/order';
import { CHECKOUT_EMAIL_COOKIE } from '@/utils/constants';
interface Props {
params: Promise<{ id: string }>;
searchParams: Promise<{ key?: string }>;
}
export default async function OrderReceivedPage({ params, searchParams }: Props) {
const { id } = await params;
const { key } = await searchParams;
const email = (await cookies()).get(CHECKOUT_EMAIL_COOKIE)?.value ?? '';
const fallback = key ? `/view-order?key=${encodeURIComponent(key)}` : '/view-order';
if (!key || !email) {
return (
<main>
<h1>Thanks for your order</h1>
<p>We couldn't auto-load the receipt because your billing email isn't
available on this device. Look up the order with the email you used at
checkout — the order key from your confirmation email is the shared
secret.</p>
<Link href={fallback}>Look up the order</Link>
</main>
);
}
const { order, error } = await lookupOrder(email, key);
if (!order) {
return (
<main>
<h1>Thanks for your order</h1>
<p className="error">{error ?? "We couldn't find that order."}</p>
<Link href={fallback}>Look up the order</Link>
</main>
);
}
return (
<main>
<OrderSummary
order={order}
headline="Thanks for your order"
intro="Your order has been received. A copy of this receipt is on its way to your inbox."
/>
</main>
);
}Der Fallback-Link zum /view-order?key=… Angelegenheiten für den “Kunden öffnete die Quittungs-E-Mail drei Tage später auf einem neuen Gerät” Fall – das Cookie ist weg, aber der Bestellschlüssel in der E-Mail funktioniert immer noch für die manuelle Suche.
Schritt 11 — Produktbewertungen
Bewertungen sind ein winziger POST-Handler, der WooGraphQL aufruft writeReview Mutation. Das Kundenformular sendet productId, Autorname, E-Mail, Bewertung und Inhalt; die Route ist beigefügt Cart-Token, führt die Mutation aus, und das WP-Backend übernimmt Moderationswarteschlange, Spam-Check und Speicher.
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { gqlWithSession } from '@/lib/wp';
export async function POST(req: NextRequest) {
const { productId, author, email, content, rating } = await req.json();
const sessionToken = (await cookies()).get('sessionToken')?.value ?? null;
const result = await gqlWithSession<{ writeReview: { rating: number } }>(
`mutation Write($input: WriteReviewInput!) {
writeReview(input: $input) {
rating
review { id }
}
}`,
{ input: {
commentOn: productId,
author, authorEmail: email,
content, rating,
} },
{ sessionToken },
);
if (result.errors?.length) {
return NextResponse.json({ error: result.errors[0].message }, { status: 400 });
}
return NextResponse.json({ ok: true, rating: result.data?.writeReview?.rating });
}Wire it up to a <ReviewForm /> Client-Komponente im Inneren <ProductTabs /> auf der Produktseite. Die Bewertungsliste selbst wird serverseitig aus der gleichen Produktabfrage gerendert; Paginierung ist das, was Sie wollen.

Rauchprüfung des Vollstroms
End-to-End: Besuch /products/hoodie, wählen Sie eine Farbe + Logo-Variante, klicken Sie zum Warenkorb hinzufügen, landen auf /cart mit dem Element zeigt, klicken Sie auf Weiter zum Checkout, füllen Sie Stripe Testkarte 4242 4242 4242 4242, geben Sie den Auftrag, landen auf /checkout/order-received/[id]?key=… Die vollständige Quittung wird automatisch ausgeführt. Fünf Klicks, Single Next.js Host, volle WooCommerce Backend Orchestrierung. Kein Login, kein Account, keine JWT Refresh Loop.
Wenn die Checkout-Seite “Kann keine Bestellung aus leerem Warenkorb erstellen” darstellt, ist dies die fetchAssetsByUri Gotcha aus Schritt 4. Öffnen Sie den Antrag auf /graphql das läuft assetsByUri auf der SSR-Seite und überprüfen Sie es trägt die Cart-Token Header. Das Cart-Block-Frontend wird unabhängig von REST ausfüllen, aber die Kasse Block vorinstalliert wcSettings.checkoutData wird zur Asset-Fetch-Zeit serverseitig generiert und überträgt die gesamte Benutzeroberfläche.

Was du nicht gebaut hast
- Zahlungsgateways – Stripe, PayPal, Square, Klarna, ~50 andere
- Steuermotoren — WooCommerce Tax (kostenlos), Avalara, TaxJar
- Versandraten-APIs — USPS, UPS, FedEx, DHL, benutzerdefinierte Flatrate-Logik
- Abonnements — Abrechnungszyklen, Testfahrten, Mahnarbeiten, anteilige Upgrades
- Inventar — Lagerbestände, Meldungen zu geringen Lagerbeständen, Nachbestellungen, Nachverfolgung nach Varianten
- Coupon-Engine — Nutzungsgrenzen, Kundensegmente, Produkt-/Kategoriebeschränkungen
- Bestellverwaltung — Rückerstattungen, Teilrückerstattungen, Bestellscheine, Kunden-E-Mails
- Moderation überprüfen — Spam-Check, Profanitätsfilter, Warteschlange Benutzeroberfläche
WooCommerce übernimmt alles. Sie behandeln die Produktseite UX und das Storefront-Chrom. NextPress verbindet die beiden, ohne dass Sie zwischen “sieht aus, wie ich will” und “funktioniert mit den Erweiterungen, die ich gekauft habe”.
Die Abkürzung: a WooGraphQL Pro Abonnement
Alles oben ist der Open-Source-Pfad. Es sind ein paar hundert Zeilen neuen Codes auf dem Blog-Tutorial, keine zusätzlichen Abhängigkeiten und das Demo-Repo ist bereit zum Klonen. Wenn die Zeit Ihres Teams besser für Storefront UX als für das Schreiben von Session-Management-Lifecycle-Code ausgegeben wird, ist das genau das, was Sie tun. a WooGraphQL Pro Abonnement ist für.
Ein Abonnement bündelt drei Dinge:
Das WooGraphQL Pro Plugin
Ein WordPress-Plugin, das das GraphQL-Schema um Typen, Abfragen und Mutationen für die WooCommerce-Produktvarianten erweitert, die Sie tatsächlich verkaufen: Abonnements, Composite-Produkte, Produktpakete und Produkt-Add-OnsDas kostenlose WooGraphQL-Schema zeigt nur einfache und variable Produkte; In dem Moment, in dem Ihr Katalog ein Paket oder ein Abonnement enthält, rollen Sie REST-Ausfälle von Hand. Mit dem Pro-Plugin sind diese Produkttypen im Schema erstklassig und eine einzige { product(id: …) } Eine Abfrage gibt die Form des Produkts zurück.
Die create-woonext-App CLI
A Gerüst, das funktioniert wie create-next-app, aber das Boilerplate, das es erzeugt, ist eine funktionierende Next.js + WooGraphQL Storefront. npx create-woonext-app my-shopZeigen Sie es auf Ihr WP-Backend, und Sie haben Produktseiten, Warenkorb, Checkout und Kontoflüsse, die Ende-zu-Ende ausgeführt werden, bevor Sie eine Datei berühren – alle mit den Pro-Hooks und Komponenten unten verdrahtet.
Die @woographql/* JS-Pakete
- @woographql/next — ein Toolkit zur Komponentengenerierung, das genauso funktioniert wie shadcn/ui: Führen Sie einen Befehl aus, die Komponente landet in Ihrem Repo, Sie besitzen und passen den Code an. Die Komponentenbibliothek deckt die gesamte Storefront-Oberfläche ab –
CartOptionsallein stellt die Cart-Action-Benutzeroberfläche für jeden Produkttyp dar, den Woo unterstützt (Simple, Variable, Composite, Bundle, Subscription, Add-On), wobei Formularzustand und Validierung bereits verdrahtet sind. - @woographql/react-hooks — getippte Hooks, die den Lifecycle-Code, den Sie oben geschrieben haben, zusammenbrechen lassen.
useSessionManager()ersetzt das Cookie-Jongling, die Cart-Token-Rotation und die SSR-Zeit-Header-Weiterleitung.useCartMutations()ersetzt die/api/cartRoute plus Client-Klebstoff, mit optimistischen Updates und Rollback pro Schlüssel. - @woographql/session-utils – die unteren Bausteine hinter diesen Hooks: Token-Verschlüsselung, Cookie / Storage-Abstraktion, Signatur-State-Serialisierung. Die Bits, die Sie lieber nicht reimplementieren würden, sobald Sie Ihre zweite Woo Storefront auf Next versendet haben.
Wählen Sie den Open-Source-Pfad, wenn Sie verstehen wollen, was unter der Haube passiert – es ist ein großartiger Ort, um anzufangen, und der Code bleibt für immer bei Ihnen. Wählen Sie das Abonnement, wenn Sie lieber die Storefront versenden und nicht Autor einer Session-Management-Bibliothek werden möchten. Wie auch immer, die Architektur in diesem Tutorial ist die richtige Architektur; Das Abonnement gibt Ihnen nur die polierte Version des Lifecycle-Codes als typisierte, getestete Abhängigkeit.
Wenn dies der falsche Anruf ist
- Greenfield-Projekt ohne Handelsabhängigkeiten und einen Einzelproduktkatalog. Stripe Checkout + eine benutzerdefinierte React Storefront Schiffe schneller. Das Einziehen von WP + Woo ist übertrieben, bis Sie Steuern, Versand oder mehrere SKUs benötigen.
- Hand-Preis B2B-Quotes. Der Cart-Builder-Workflow für “Anfordern eines Angebots, Erhalten von Preisen, Konvertieren in Bestellung” ordnet sich nicht sauber auf das Woo-Katalogmodell ohne signifikante benutzerdefinierte Arbeit ab.
- Multi-Vendor-Marktplätze. Woo hat Erweiterungen mit mehreren Anbietern, aber sie sind komplex; Eine Marktplatz-native Plattform wie Medusa passt normalerweise besser.
Für den Rest – die meisten Apps, die Commerce benötigen, insbesondere solche, die bereits Inhalte von WordPress anbieten – fügt Woo als Commerce-Backend hinzu und NextPress als Bridge bringt Sie dazu, “Produkt auf dem gleichen Next.js-Host wie der Rest der Website” an einem Nachmittag zu verkaufen. Abonnements, benutzerdefinierte Zahlungsgateways, Multi-Währungen, Rücknahme von Warenkorbs, B2B-Preisstufen – es handelt sich um Erweiterungsinstallationen gegen den WP-Admin, nicht um Refaktorisierungen gegen Ihre App.

Leave a Reply