Hai il blog di lavoro. Ora aggiungi il negozio. Questo passaggio raccoglie esattamente dove il tutorial del blog senza testa Se ne va. Due nuovi plugin WordPress, un gruppo di route diviso in modo che le pagine WP-rendered ottenere il proprio layout radice, e un singolo intestazione — Cart-Token — corrompere la sessione di Next.js all’API Store di WooCommerce. La stessa app che serve il tuo blog è ora al servizio di un negozio, con carrello e checkout in esecuzione attraverso ogni estensione WooCommerce che hai già installato.
Repo di compensazione: github.com/AxisTaylor/nextpress-woographql-quickstart. È nextpress-quickstart dal blog tutorial con tutto in questo post applicato in cima — clone esso, eseguire npm install && npm run deve tu hai tutto il flusso contro woographqldemo.wpengine.com.

Chi è questo per
Hai un app Next.js esistente — il tuo, o quello che hai costruito seguendo il tutorial del blog. Si desidera commercio: prodotti, carrello, checkout, pagamenti, inventario, tasse, spedizione, tutto. Non sei interessato a scrivere quelle ultime sei cose da zero. WooCommerce gestisce ~5 milioni di live storefront e il mercato di estensione è il più grande in qualsiasi piattaforma di commercio open-source. La configurazione WordPress senza testa che hai costruito per il blog è anche la configurazione WooCommerce senza testa — stesso backend, stesso proxy, altri due plugin e un rifattore di layout.
Una nota su auth: non c’è
Questo tutorial cammina attraverso un solo ospite negozio. Nessuna pagina di login, nessun cruscotto account, nessun anello di aggiornamento JWT. The Cart-Token WooCommerce mani indietro è di per sé l’identificatore di sessione — un ospite può navigare, aggiungere al carrello, controllare, e guardare il loro ordine con nulla, ma la loro email di fatturazione e la chiave di ordine dalla loro conferma. Questo copre l’intero percorso felice per la maggior parte dei negozi. Se si desidera account cliente autenticati, collegare wp-graphql-headlesslogin o wp-graphql-jwt-authentition separatamente — è progettato per collegare accanto al flusso Cart-Token senza disturbarlo.
Cosa cambia dal tutorial del blog
- Due nuovi plugin WP: WooCommerce e WPGraphQL per WooCommerce.
- Gruppo di rotta diviso. Cancella
app/layout.tsx. Spostare le rotte in-app inapp/(main)/con il proprio layout radice. Spostare le rotte WP-rendered (cart, checkout, blog) inapp/(wordpress-pages)/con un layout radice separato che mette<WPHead />dentro<head>. Importmaps e moduli di script emessi dal blocco di checkout WC si risolvono correttamente solo quando il tag importmap vive nel documento<head>, e un layout nidiato non può metterlo lì se c’è un layout radice esterno che possiede<html>. - Middleware collega la sessione. Leggi il testo
sessionTokencookie su ogni richiesta WP REST / AJAX, collegarlo comeCart-TokenIntestazione. Cattura qualsiasi ruotatoCart-Tokensulla risposta e persistere nel cookie in modo che la prossima richiesta rimanga nella stessa sessione WC. - Il lato server GraphQL fa avanzare lo stesso intestazione. Entrambi
fetchPageByUriEfetchAssetsByUrideve includereCart-Token. Saltare sufetchAssetsByUriè il singolo più affilato gotcha — il blocco WC server-renders contro un carrello vuoto e cuocere un"Cannot create order from empty cart"errorewcSettings.checkoutData, e la pagina è permanentemente bloccata. - Una pagina di prodotto lucidata con un selettore di variazione per prodotti variabili (in corrispondenza di attributo
name#label— c’è un sottile getcha per gli attributi locali). - Controllo degli ordini degli ospiti tramite un’azione del server.
/view-orderprende una email di fatturazione + chiave d’ordine e rende l’ordine. L’azione del server lega l’e-mail alla correnteCart-TokensessioneupdateCustomer, quindi restringe la connessione degli ordini per chiave d’ordine. Nessuna password dell’applicazione, nessun account shop-manager. - Ricevimento automatico
/checkout/order-received/[id]. Un componente client cattura l’e-mail di fatturazione dal blocco di checkout WC in un cookie. La pagina di ricezione legge il cookie + il?key=sull’URL e cerca l’ordine attraverso la stessa azione server — nessun modulo da compilare. - Recensioni dei prodotti collegato attraverso un piccolo
/api/product/reviewpercorso che chiama WooGraphQL’swriteReviewmutazione con il Cart-Token inoltrato.
Passo 1 — Installare due plugin WP
- WooCommerce — dalla directory plugin. Eseguire la procedura guidata di configurazione, cadere in Stripe Test per i pagamenti, aggiungere un paio di prodotti demo quindi c’è qualcosa da fare. Conferma
/carte/checkoututilizzare i modelli di blocco (default da Woo 8.3) piuttosto che i codici brevi legacy — questo è ciò che NextPress proxy. - WPGraphQL per WooCommerce — aggiunge i tipi di prodotto, carrello, cliente e ordine a
/graphql. Verificare in Postman (o qualsiasi client HTTP che superfici intestazioni di risposta — GraphiQL li nasconde) sparandoaddToCartmutazione contro una realeproductIdcon noCart-Tokensu richiesta. WP crea una nuova sessione degli ospiti e restituisce il JWT nelCart-Tokenintestazione di risposta.
mutation BootGuestSession {
addToCart(input: { productId: 13, quantity: 1 }) {
cart {
contents { itemCount }
total
}
}
}The Cart-Token intestazione solo navi su carrello e sessione mutazioni — addToCart♪ updateItemQuantity♪ removeItemsFromCart♪ applyCoupone amici. quesiti contro /graphql non produrre uno, e GraphiQL non mostrerà intestazioni di risposta comunque, motivo per cui questa fase di verifica utilizza Postman. Una volta che il middleware vede che l’intestazione su una risposta prossima persiste il nuovo valore nel sessionToken cookie, e ogni richiesta successiva — REST chiama dal blocco di checkout, domande GraphQL lato server nella tua app Next.js — cavalca su di esso.

Fase 2 — Gruppo di rotta diviso
Questo è il più grande cambiamento strutturale. Cancella app/layout.tsx. Spostare ogni percorso sotto uno dei due gruppi di percorso di alto livello, ciascuno con il proprio layout radice:
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
Perche’? Next.js permette solo <html> e <body> nei layout delle radici. Senza app/layout.tsx, ogni gruppo di percorso di alto livello diventa la sua radice. The (wordpress-pages) radice può mettere <WPHead /> direttamente dentro <head> — che è dove <script type="importmap"> emesso per gli script modulo WC-forma ha per vivere. Prova questo con un layout nidificato sotto una singola radice e l’importamap finisce in <body>; gli script del modulo non risolvono @wordpress/plugins e l’intero albero di blocco del carrello getta 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>
);
}Passo 3 — Middleware ponti il Cart-Token
Aggiornamento src/proxy.ts per collegare il token del carrello ad ogni richiesta WP REST/AJAX, quindi catturare qualsiasi WP token ruotato scrive sulla risposta. Quando il frontend di blocco del carrello /wc/store/v1/cart dal browser, passa attraverso il proxy NextPress — e che il proxy deve tradurre il sessionToken cookie nel Cart-Token header WooCommerce si aspetta. WooCommerce a volte ruota il token (dopo una mutazione carrello, un coupon applicare, ecc); l’intestazione di risposta è come segnala il nuovo valore.
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. Avanti il Cart-Token su ogni chiamata REST/AJAX proxied in modo che il
// browser-side WC blocca la terra nella sessione carrello reale dell'ospite.
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 la richiesta, quindi catturare qualsiasi ruotato Cart-Token WP scrive
// di nuovo sulla risposta e persiste come il cookie di sessioneToken.
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|.*\\.).*)',
],
};
Fase 4 — Assistenti GraphQL che inoltrano il token
Due aiutanti in src/lib/wp.ts parlare con WPGraphQL: gqlWithSession per le domande che hanno bisogno della sessione carrello, e fetchAssetsByUri / fetchPageByUri che entrambi lo usano. Entrambi questi MUST inoltrare il Cart-Token — e il secondo è il 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 };
}
// Critical: avanti Cart-Token anche qui. I beniByUri risolutore
// attiva lo script WP-side in esecuzione, che esegue
// idratare data da api request per il blocco di checkout. Senza
// intestazione, wc()->cart è vuoto per tale richiesta, l'idrato produce
// "Cannot creare ordine da carrello vuoto", e quell'errore navi in
// wcSettings.checkoutData → il blocco di checkout haCheckoutError
// cancello incendi in modo permanente.
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;
}Questo commento fetchAssetsByUri non è decorazione — è l’intera ragione /checkout fallisce silenziosamente se lo dimentichi. WPGraphQL è a posto, il browser REST chiama il lavoro, il carrello REST restituisce gli elementi giusti — e la pagina rende ancora un errore di vuoto-cart perché i dati precaricati dei blocchi WC sono stati generati contro una sessione di carrello vuoto. Inoltrare il token entrambi Le fetches GraphQL lato server chiudono il divario.
Passo 5 — Cart come percorso API
Add-to-cart, aggiornamento-quantità, rimozione-da-cart — tutto scorre attraverso uno /api/cart Manico. Il percorso legge sessionToken dai cookie, corre la mutazione WPGraphQL con Cart-Token allegato, e restituisce il carrello aggiornato. Il middleware da Passo 3 si prende cura di insistere qualsiasi token ruotato sulla via d’uscita.
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 });
}
// ...aggiornamento / rimuovere / applicareCoupon / chiaro seguire lo stesso modello
}Osservare variation array sull’ingresso. Variable-product add-to-cart ha bisogno di esso per variazioni che mescolano attributi globali e locali — vedere Passo 7.

Passo 6 — Una pagina di prodotto lucidato
Le pagine del prodotto convertono i browser in acquirenti, quindi questo è dove si passa il tempo di progettazione reale. Componente del server, ISR-cached, singolo GraphQL query per l’intera vista: titolo, immagine dell’eroe, galleria (honoring ogni rapporto di aspetto naturale dell’immagine da mediaDetails), prezzo + tasso di vendita, tesserino, descrizioni brevi e lunghe, prodotti correlati, pangrattato e schema del prodotto JSON-LD per Google Shopping. La pagina invia __typename a una delle due componenti eroiche. Entrambi rendono mobile-first — la maggior parte del traffico storefront è mobile, e lo screenshot qui sotto è la piccola vista viewport per dimostrarlo.
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} />
</>
);
}
Fase 7 — Variazioni dei prodotti variabili
VariableProductHero è un componente client che possiede lo stato dell’attributo-seleziona e solleva il prezzo, l’immagine e il badge stock alla variazione abbinata. Due piccoli ma dettagli portanti:
- Abbinamento
name#label. Per gli attributi globali (taxonomy-backed:pa_color♪pa_size), entrambinameelabeld’accordoProductAttributeeVariationAttribute. Per gli attributi locali (forma libera sul prodotto, senza tassonomia),ProductAttribute.labelè la forma umana (“Logo”) maVariationAttribute.labelèsanitize_title()’d (“logo”). I duelabeli campi non sono comparabili.namepassa attraversosanitize_title()su entrambi i lati, quindi è sempre d’accordo — partita daname. - Invia una
variationarray su add-to-cart. Quando la selezione del cliente corrisponde ad un realevariationId, inviare entrambi — permette WooGraphQL pin la voce carrello a quella variazione indipendentemente dal fatto che gli attributi siano globali o locali.
// VariazioneAttribute.label è sanitize title()'d per gli attributi LOCAL
// (ad esempio "Logo" → "logo") ma ProductAttribute. etichetta è la forma umana
// ("Logo"). Entrambi espongono `name`, che è sanitize title()'d su BOTH
// lati — così corrispondono da `name`, mai da `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>
</>
);
}
Passo 8 — Carrello e checkout tramite il proxy NextPress
Due rotte, dodici linee ciascuna. Loro vivono sotto (wordpress-pages) in modo da raccogliere il layout WP-rendering dal passo 2:
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>
);
}The (wordpress-pages) il grafico delle risorse (con Cart-Token!), la pagina rende il blocco del carrello WC markup, e lo script frontend del blocco del carrello assume l’idratazione — utilizzando lo stesso Cart-Token i ponti middleware attraverso. Stessa forma per /checkout/page.tsx; stessa forma per le rotte del blog.

Passo 9 — Ricerca dell’ordine degli ospiti tramite un’azione del server
Una volta che un ordine esiste, il cliente ha bisogno di un modo per vederlo. Il flusso WooCommerce standard mette la ricevuta /checkout/order-received/[id], ma ha anche bisogno di una pagina di ricerca manuale per le visite ripetute, schede di conferma perse e collegamenti di follow-up e-mail. /view-order è quella pagina — un componente server con un modulo, postando ad un’azione server.
L’azione fa qualcosa di sottile: updateCustomer senza id mira il cliente attaccato alla corrente Cart-Token sessione. Impostazione billing.email lega tale sessione all’e-mail; il cliente orders connessione poi restituisce ogni ordine posto contro tale indirizzo — compresi gli ordini degli ospiti, che è tutto il punto. L’azione restringe orderKey, che agisce come un segreto condiviso: conoscere un’e-mail da solo non è sufficiente per estrarre l’ordine di qualcun altro.
'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 }
}
}
}
}
`;
// Aggiornamento in esecuzioneCustomer senza un `id` applica la mutazione alla
// cliente attaccato alla sessione corrente Cart-Token. Impostazione
// billing.email lega tale sessione all'e-mail; il cliente
// ordine connessione quindi restituisce ogni ordine posto contro quello
// indirizzo — compresi gli ordini degli ospiti. restringiamo per ordineKey, che
// agisce come un segreto condiviso: conoscere un'email da solo non è sufficiente
// esce dall'ordine di qualcun altro.
//
// Questo viene eseguito solo tramite invocazioni server-action, quindi il GraphQL
// endpoint e la semantica legata al cliente non raggiungono mai
// cliente. I lettori tutorial non hanno bisogno di una password di applicazione — la
// Cart-Token autentica la mutazione.
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');
}Ciò che questo evita: laminazione a mano di una password di applicazione shop-manager nella demo. I lettori tutorial non hanno uno, e spediscono il tutorial con “qui, incollare le credenziali di amministrazione in .env” sarebbe irresponsabile. Il modello Cart-Token-as-shared-secret mantiene tutto all’interno del modello di sessione esistente.

Passo 10 — Ricevuta automatica a /checkout/order-received
Quando WooCommerce reindirizza il cliente dopo un checkout di successo, l’URL è /checkout/order-received/[id]?key=wc_order_…. key è la chiave d’ordine. L’email non è nell’URL — è nel modulo di fatturazione del blocco WC, che il cliente ha appena compilato. Acquisiamo quel client-side di posta elettronica in un cookie, quindi la pagina di ricezione legge cookie + chiave URL ed esegue lo stesso lookupOrder Azione server da Passo 9.
Il componente di cattura suona semplice (leggi un paio di ingressi e-mail, scrivi a un cookie) ma il blocco di checkout WC è fussy: monta i suoi campi asincroni, rimonta il campionario di fatturazione quando “Utilizza l’indirizzo di spedizione per fatturazione” si attiva, E — la sorpresa — idrata i valori di campo dal proprio storage di sessione senza sparare input o change eventi. Un delegato input solo ascoltatore mancherà completamente l’autofill. Tre fonti coprono il divario:
- Controllo DOM iniziale sul montaggio — cattura valori già presenti al tempo di rendering (autofill, browser autocomplete).
- MutationObserver su
document.bodyconattributeFilter: ['value']— cattura ingressi di montaggio tardivo e scrive di valore restaurato di sessione. - Capture-fase
input/changedelegazione sudocument— cattura la digitazione dal vivo.
'use client';
import { useEffect } from 'react';
import { CHECKOUT_EMAIL_COOKIE } from '@/utils/constants';
// Il blocco di checkout WC monta i suoi campi in modo asincrono e rimontaggio
// i campi di fatturazione quando il cliente si attiva "Usa spedizione per
// Billing". Idrata anche i valori dal proprio storage di sessione
// SENZA sparare eventi di input/change — così un ascoltatore delegato da solo
// manca l'e-mail di riempimento automatico. Ci servono tre fonti:
// 1. scansione DOM iniziale al montaggio — cattura i valori già presenti.
// 2. MutationObserver — cattura ingressi e
// scrive il valore restaurato della sessione.
// 3. delegazione di input/change — cattura la digitazione dell'utente dal vivo.
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;
}Mount che su /checkout/page.tsx vicino al <Content> blocco e il cookie viene impostato al momento in cui il cliente clicca su Place Order. La pagina di ricezione lo legge e rende — nessun modulo, nessun clic in più.
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>
);
}Il link per il ritorno /view-order?key=… questioni per il “cliente ha riaperto l’email di ricezione tre giorni dopo su un nuovo dispositivo” caso — il cookie è andato, ma il tasto d’ordine nella e-mail funziona ancora per la ricerca manuale.
Fase 11 — Recensioni dei prodotti
Le recensioni sono un piccolo gestore POST che chiama WooGraphQL writeReview mutazione. Il modulo client invia productId, nome dell’autore, e-mail, valutazione e contenuto; il percorso allega Cart-Token, corre la mutazione, e il backend WP gestisce la coda di moderazione, il controllo dello spam e lo storage.
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 });
}Filare fino a un <ReviewForm /> componente client all’interno <ProductTabs /> sulla pagina del prodotto. La lista di recensione stessa è reso lato server dalla stessa query del prodotto; la paginazione è ciò che si desidera essere.

Fumo-test il flusso completo
Fine-fine: visita /products/hoodie, scegliere una variante di colore + logo, fare clic su Aggiungi al carrello, atterrare /cart con l’elemento che mostra, fare clic su Procedere per il checkout, compilare la scheda Test Stripe 4242 4242 4242 4242, mettere l’ordine, atterrare /checkout/order-received/[id]?key=… con la ricevuta completa resa automaticamente. Cinque click, singolo host Next.js, completa orchestrazione backend WooCommerce. Nessun login, nessun account, nessun anello di aggiornamento JWT.
Se la pagina di checkout rende “Cannot creare ordine da carrello vuoto” — questo è il fetchAssetsByUri Ricevuto dal punto 4. Aprire la richiesta /graphql che corre assetsByUri sul lato SSR e verificarlo trasporta Cart-Token Intestazione. Il frontend del carrello-blocco popula da REST indipendentemente, ma il checkout precaricata del blocco wcSettings.checkoutData è generato server-side a tempo di risparmio-fetch e cancelli l’intero UI.

Cosa non hai costruito
- gateway di pagamento — Stripe, PayPal, Square, Klarna, ~50 altri
- Motori fiscali — WooCommerce Tax (free), Avalara, TaxJar
- API sui prezzi di spedizione — USPS, UPS, FedEx, DHL, logica a tasso piatto personalizzato
- Abbonamenti — cicli di fatturazione, prove, sotterranei, aggiornamenti prorati
- Inventario — livelli di stock, notifiche a basso contenuto di scorte, back-order, monitoraggio per-variante
- Motore Coupon — limiti di utilizzo, segmenti dei clienti, restrizioni di prodotto/categoria
- Gestione dell’ordine — rimborsi, rimborsi parziali, note d’ordine, e-mail del cliente
- Moderazione della recensione — controllo dello spam, filtro della profanità, coda UI
WooCommerce gestisce tutto. Gestisci la pagina del prodotto UX e la barra cromata. NextPress collega i due senza farvi scegliere tra “guarda il modo che voglio” e “lavora con le estensioni che ho comprato.”
La scorciatoia: un WooGraphQL Abbonamento
Tutto sopra è il percorso open-source. Si tratta di poche centinaia di linee di nuovo codice in cima al tutorial del blog, senza dipendenze extra, e le navi repo demo pronti a clonare. Se il tempo del vostro team è meglio speso sul negozio UX che sulla scrittura del codice del ciclo di vita di gestione della sessione, questo è esattamente ciò a WooGraphQL Abbonamento è per.
Un abbonamento raggruppa tre cose:
Il plugin WooGraphQL Pro
Un plugin WordPress che estende lo schema GraphQL con tipi, query e mutazioni per le varianti di prodotto WooCommerce che si vendono: Abbonamenti, prodotti compositi, prodotti Bundles e componenti aggiuntivi del prodotto. Lo schema gratuito WooGraphQL espone solo prodotti semplici e variabili; nel momento in cui il vostro catalogo include un bundle o un abbonamento, siete ricadute a mano REST. Con il plugin Pro, questi tipi di prodotti sono di prima classe nello schema, e un singolo { product(id: …) } query restituisce qualsiasi forma che il prodotto sembra essere.
Il creato-woonext-app CLI
A truffatore che funziona come create-next-app, ma la caldaia che genera è un lavoro Next.js + WooGraphQL storefront. npx create-woonext-app my-shop, puntalo sul tuo backend WP, e hai pagine di prodotto, carrello, checkout e flussi di account in esecuzione end-to-end prima di toccare un file — tutti collegati con i ganci Pro e componenti qui sotto.
I pacchetti @woographql/* JS
- @woographql/next — un toolkit di generazione dei componenti che funziona allo stesso modo in cui shadcn/ui fa: eseguire un comando, il componente atterra nel repo, si possiede e personalizzare il codice. La libreria dei componenti copre la superficie completa del magazzino —
CartOptionsda solo rende la cart-action UI per ogni tipo di prodotto Supporti Woo (Simple, Variable, Composite, Bundle, Abbonamento, Add-On), con stato di forma e validazione già cablato. - @woographql/react-hooks — ganci digitati che crollano il codice del ciclo di vita che hai scritto sopra.
useSessionManager()sostituisce il cookie-juggling, la rotazione di cart-token e l’intestazione SSR-time in avanti.useCartMutations()sostituisce il/api/cartpercorso più la colla client, con aggiornamenti ottimistici e rollback di errore per-chiave inclusi. - @woographql/session-utils — i blocchi di costruzione di livello inferiore dietro quei ganci: crittografia token, astrazione di cookie/storage, serializzazione firmata-stato. I bit che si preferisce non reimplement una volta che hai spedito il tuo secondo Woo Storefront su Next.
Scegli il percorso open-source se vuoi capire cosa sta succedendo sotto il cofano — è un ottimo posto per iniziare, e il codice rimane tuo per sempre. Scegliere l’abbonamento se si preferisce spedire il negozio e non diventare un autore di libreria di gestione sessione. In entrambi i casi, l’architettura in questo tutorial è l’architettura giusta; l’abbonamento ti dà solo la versione lucida del codice del ciclo di vita come una dipendenza digitata e testata.
Quando questa è la chiamata sbagliata
- Progetto Greenfield senza dipendenze commerciali e un catalogo a singolo prodotto. Stripe Checkout + un React personalizzato navi negozio più veloce. Tirare in WP + Woo è overkill fino a quando non hai bisogno di tasse, spedizione o più SKU.
- Citazioni B2B a prezzi a mano. Il workflow cart-builder per “richiedere un preventivo, ottenere prezzi, convertire in ordine” non mappa in modo pulito sul modello di catalogo di Woo senza un lavoro personalizzato significativo.
- Mercati multi-vendor. Woo ha estensioni multi-vendor ma sono complessi; una piattaforma mercato-nativa come Medusa è di solito una soluzione migliore.
Per il resto — la maggior parte delle applicazioni che hanno bisogno di commercio, soprattutto quelli che già servono contenuti da WordPress — l’aggiunta di Woo come il backend di commercio e NextPress come il ponte ti porta a “vendere prodotto sullo stesso ospite Next.js come il resto del sito” in un pomeriggio. Abbonamenti, gateway di pagamento personalizzati, multi-valuta, recupero di carte abbandonate, B2B tier prezzi — sono installazioni di estensione contro l’amministratore WP, non refactors contro la vostra app.

Leave a Reply