Hinzufügen von WooCommerce zu Ihrer Next.js-App an einem Nachmittag mit NextPress

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.

Headless WooCommerce on Next.js — in-app product pages, NextPress-proxied cart and checkout, Cart-Token bridging both.

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 verschieben app/(main)/ mit eigenem Root-Layout. Verschieben Sie WP-gerenderte Routen (Cart, Checkout, Blog) in app/(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 sessionToken Cookie auf jeder proxied WP REST / AJAX Anfrage, fügen Sie es als die Cart-Token Header. Auffangen beliebig gedreht Cart-Token auf 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 fetchPageByUri UND fetchAssetsByUri muss enthalten Cart-TokenÜberspringen auf fetchAssetsByUri ist 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 in wcSettings.checkoutData, und die Seite ist dauerhaft stecken.
  • Eine polierte Produktseite mit einem Variationsselektor für variable Produkte (nach Attributen abgestimmt) name, nicht label Es gibt ein subtiles gotcha für lokale attribute.
  • Guest Order Lookup über eine Serveraktion. /view-order nimmt eine Rechnungs-E-Mail + Bestellschlüssel und macht die Bestellung. Die Server-Aktion bindet die E-Mail an den aktuellen Cart-Token Sitzung via updateCustomer, 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/review Route, die WooGraphQL aufruft writeReview Mutation mit dem Cart-Token weitergeleitet.

Schritt 1 – Installieren Sie zwei WP-Plugins

  1. 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 /cart und /checkout Verwenden Sie die Blockvorlagen (Standard seit Woo 8.3) anstelle der Legacy-Shortcodes – das wird NextPress Proxy.
  2. 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 die addToCart Mutation gegen eine reale productId ohne Cart-Token über den Antrag. WP erstellt eine neue Gästesitzung und gibt die JWT im Cart-Token Response Header.
Postman
mutation BootGuestSession {
  addToCart(input: { productId: 13, quantity: 1 }) {
    cart {
      contents { itemCount }
      total
    }
  }
}

Die Cart-Token Header nur Schiffe auf Wagen und Session MutationenaddToCart, 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.

Postman executing an addToCart mutation against /graphql, with the response Headers tab expanded to show the Cart-Token JWT WooCommerce returned for the new guest session.

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/
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.

src/app/(wordpress-pages)/layout.tsx
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.

src/proxy.ts
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|.*\\.).*)',
  ],
};
Chrome DevTools showing the Cart-Token header attached by middleware to a /wc/store/v1/cart request.

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.

src/lib/wp.ts (excerpt)
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.

src/app/api/cart/route.ts (excerpt)
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.

DevTools showing the POST /api/cart request and JSON response after add-to-cart.

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.

src/app/(main)/products/[slug]/page.tsx (shape)
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} />
    </>
  );
}
Polished product page rendered by Next.js with WooGraphQL data and schema.org JSON-LD.

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, nicht label. Für globale Attribute (taxonomiegestützt): pa_color, pa_size, beide name und label Einvernehmen über ProductAttribute und VariationAttribute. Für lokale Attribute (freie Form auf dem Produkt, keine Taxonomie), ProductAttribute.label ist die menschliche Form („Logo), aber VariationAttribute.label ist sanitize_title()’d („Logo). Die beiden label Felder sind nicht vergleichbar. name geht durch sanitize_title() auf beiden Seiten, also stimmt es immer zu – match by name.
  • Senden Sie a variation Array auf Add-to-Cart. Wenn die Auswahl des Kunden einer echten variationIdSenden Sie beide – WooGraphQL kann das Warenkorbelement an diese Variation anheften, unabhängig davon, ob die Attribute global oder lokal sind.
src/lib/variation-helpers.ts
// 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));
}
src/components/VariableProductHero.tsx (shape)
'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>
    </>
  );
}
Variable product page showing matched variation image, price, and stock badge updating live with attribute-picker selection.

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:

src/app/(wordpress-pages)/cart/page.tsx
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.

WooCommerce cart block rendered on a Next.js host via the NextPress proxy — same UI, same Next.js domain.

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.

src/actions/order.ts
'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.

/view-order form and resulting OrderSummary after a successful lookup.

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:

  1. Erster DOM-Scan on mount — fängt bereits zum Renderzeitpunkt vorhandene Werte (Autofill, Browser Autocomplete).
  2. MutationObserver am document.body mit attributeFilter: ['value'] – fängt spät montierte Eingaben und sitzungswiederherstellte Wert schreibt.
  3. Einfangphase input/change Delegation am document — Fänge mit Lebendtypisierung.
src/components/CheckoutEmailCapture.tsx
'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.

src/app/(wordpress-pages)/checkout/order-received/[id]/page.tsx
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.

src/app/api/product/review/route.ts
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.

Order-received page after a successful test checkout — payment processed, inventory updated, email sent, all server-side via WooCommerce; receipt rendered automatically on the Next.js host.

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 – CartOptions allein 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/cart Route 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

Your email address will not be published. Required fields are marked *