Vous avez le blog qui fonctionne. Ajoutez le magasin. Ce passage prend exactement où le tutoriel de blog sans tête laisse tomber. Deux nouveaux plugins WordPress, un groupe de route s’est séparé pour que les pages WP aient leur propre mise en page racine, et un seul en-tête — Cart-Token — la transition de votre session Next.js vers l’API de WooCommerce. La même application qui sert votre blog est maintenant en train de servir un magasin, avec le panier et la caisse fonctionnant à travers chaque extension WooCommerce que vous avez déjà installé.
Compte rendu: github.com/AxisTaylor/nextpress-woographql-quickstartC’est nextpress-quickstart du tutoriel de blog avec tout dans ce post appliqué sur le dessus — clone-le, exécuter npm install && npm run dev, et vous avez tout le flux courant contre woographqldemo.wpengine.com.

C’est pour qui ?
Vous avez une application Next.js existante — la vôtre, ou celle que vous avez construite après le tutoriel de blog. Vous voulez commerce: produits, panier, caisse, paiement, inventaire, taxes, expédition, tout. Vous n’êtes pas intéressé à écrire ces six dernières choses de zéro. WooCommerce fonctionne ~5 millions de magasins en direct et le marché de l’extension est le plus grand dans toute plate-forme de commerce open-source. La configuration WordPress sans tête que vous avez construite pour le blog est également la configuration sans tête WooCommerce — même moteur, même proxy, deux autres plugins et un refacteur de mise en page.
Une note sur auth: il n’y en a pas
Ce tutoriel passe par invité seulement En front de magasin. Pas de page de connexion, pas de tableau de bord de compte, pas de boucle de rafraîchissement JWT. Les Cart-Token WooCommerce mains retour est lui-même l’identificateur de session — un invité peut parcourir, ajouter au panier, vérifier, et rechercher leur commande avec rien que leur email de facturation et la clé de commande de leur confirmation. Cela couvre tout le chemin heureux pour la plupart des magasins. Si vous voulez des comptes clients authentifiés, téléversez wp-graphql-sans tête-login ou wp-graphql-jwt-authentification séparément — il est conçu pour se brancher à côté du flux Cart-Token sans le déranger.
Ce qui change du tutoriel de blog
- Deux nouveaux plugins WP: WooCommerce et WPGraphQL pour WooCommerce.
- Groupe d’itinéraire divisé. Supprimer
app/layout.tsx. Déplacer les itinéraires d’application versapp/(main)/avec leur propre disposition racine. Déplacer les itinéraires WP (cart, checkout, blog) versapp/(wordpress-pages)/avec une mise en page racine séparée qui met<WPHead />intérieur<head>. Importation des cartes et des modules de script émis par le bloc de caisse WC ne résolvent correctement que lorsque la balise importmap vit dans le document<head>, et une mise en page imbriquée ne peut pas l’y mettre s’il y a une mise en page externe propriétaire<html>. - Le Middleware règle la session. Lire
sessionTokencookie sur chaque demande WP REST / AJAX, joignez-le commeCart-Tokenen-tête. Capturer toute rotationCart-Tokensur la réponse et persister dans le cookie de sorte que la demande suivante reste dans la même session WC. - GraphQL côté serveur récupère le même en-tête. Les deux
fetchPageByUriPAYSfetchAssetsByUridoit inclureCart-TokenC’est bon.fetchAssetsByUriest le seul gotcha le plus pointu — le serveur de bloc de caisse WC contre un chariot vide et fait un"Cannot create order from empty cart"erreur danswcSettings.checkoutData, et la page est bloquée en permanence. - Une page produit poli avec un sélecteur de variation pour les produits variables (apparié par attribut
namePaslabel— il y a un gotcha subtil pour les attributs locaux). - Recherche de commande d’invité via une action serveur.
/view-orderprend un email de facturation + touche de commande et rend la commande. L’action du serveur lie l’email au courantCart-Tokensession parupdateCustomer, puis rétrécit la connexion des commandes par la clé d’ordre. Pas de mot de passe d’application, pas de compte de magasin. - Réception automatique à
/checkout/order-received/[id]. Un composant client capture l’e-mail de facturation du bloc de caisse WC dans un cookie. La page de réception lit le cookie + le?key=sur l’URL et recherche l’ordre par la même action du serveur — aucun formulaire à remplir. - Avis sur les produits filé à travers un minuscule
/api/product/reviewitinéraire qui appelle WooGraphQL.writeReviewmutation avec le Cart-Token transmis.
Étape 1 — Installer deux plugins WP
- WooCommerce — à partir du répertoire du plugin. Exécutez l’assistant de configuration, déposez le Stripe Test pour les paiements, ajoutez quelques produits de démo donc il ya quelque chose à requête. Confirmer
/cartet/checkoututiliser les modèles de blocs (par défaut depuis Woo 8.3) plutôt que les shortcodes existants — c’est ce que NextPress va proxy. - WPGraphQL pour WooCommerce — ajoute le produit, le panier, le client et les types de commande à
/graphql. Vérifier dans Postman (ou dans tout client HTTP qui couvre les en-têtes de réponse — GraphiQL les cache) en tirant sur leaddToCartmutation contre une réelleproductIdavecCart-Tokensur demande. WP crée une nouvelle session d’invités et retourne le JWT dans leCart-Tokenen-tête de réponse.
mutation BootGuestSession {
addToCart(input: { productId: 13, quantity: 1 }) {
cart {
contents { itemCount }
total
}
}
}Les Cart-Token header n’expédie que sur le chariot et la session mutations — addToCart, updateItemQuantity, removeItemsFromCart, applyCouponEt des amis. Des questions simples contre /graphql ne produisent pas un, et GraphiQL won=t afficher les en-têtes de réponse de toute façon, c’est pourquoi cette étape de vérification utilise Postman. Une fois que le middleware voit que l’en-tête sur une réponse proxiée, il persiste la nouvelle valeur dans le sessionToken cookie, et toutes les requêtes ultérieures — REST appelle depuis le bloc de caisse, requêtes GraphQL côté serveur dans votre application Next.js — monte dessus.

Étape 2 — Groupe d’itinéraire
C’est le plus grand changement structurel. Supprimer app/layout.tsx. Déplacez chaque itinéraire sous l’un des deux groupes de route de haut niveau, chacun ayant sa propre disposition racine :
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
Pourquoi ? Next.js permet seulement <html> et <body> dans la disposition des racines. Sans app/layout.tsx, chaque groupe de route de haut niveau devient sa propre racine. Les (wordpress-pages) racine peut mettre <WPHead /> directement à l’intérieur <head> — où <script type="importmap"> émis pour les scripts de module-formule WC. a pour vivre. Essayez ceci avec une mise en page imbriquée sous une seule racine et l’importmap finit par <body>; les scripts de module ne parviennent pas à résoudre @wordpress/plugins et tout l’arbre du carreau lance 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>
);
}Étape 3 — Le middleware bridgee le cart-token
Mise à jour src/proxy.ts pour attacher le jeton de panier à chaque requête WP REST/JAX proxiée, puis capturer tout jeton rotatif WP écrit de nouveau sur la réponse. Quand le carreau-bloc frontend récupère /wc/store/v1/cart depuis le navigateur, il passe par votre proxy NextPress — et que proxy doit traduire votre sessionToken cookie dans la Cart-Token header WooCommerce attend. WooCommerce fait parfois tourner le jeton (après une mutation du chariot, un coupon s’applique, etc.); l’en-tête de réponse est la façon dont il signale la nouvelle valeur.
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. Faire suivre le Cart-Token sur chaque appel REST/AJAX
// côté navigateur WC blocs se trouvent dans la session de panier de l'invité.
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 demande, puis capture tout cart-token tournant WP écrit
// retour sur la réponse et le maintenir en tant que cookie session Token.
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|.*\\.).*)',
],
};
Étape 4 — Aides GraphQL qui transmettent le jeton
Deux aides en src/lib/wp.ts Parlez à WPGraphQL: gqlWithSession pour les questions qui nécessitent la session du chariot, et fetchAssetsByUri / fetchPageByUri qui l’utilisent tous les deux. Tous les deux DOIVENT transmettre le Cart-Token — et le second est le 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 };
}
// Critique: Cart-Token avant ici aussi. Les actifsByUri résolveur
// déclenche l'enchaînement de script côté WP, qui exécute WC
// hydrate data from api request pour le bloc de caisse. Sans
// header, wc()->cart est vide pour cette requête, l'hydrate produit
// "Impossible de créer l'ordre à partir d'un panier vide", et cette erreur est
// wcSettings.checkoutDonnées → le bloc de caisse aCheckoutErreur
// La porte tire en permanence.
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;
}Cette remarque sur fetchAssetsByUri n’est pas décoration — c’est toute la raison /checkout échoue silencieusement si vous l’oubliez. WPGraphQL est très bien, le navigateur des appels REST fonctionne, le panier REST retourne les bons éléments — et la page rend toujours une erreur de carte vide parce que les données préchargées côté serveur WC ont été générées contre une session de carte vide. Transmettre le jeton sur les deux côté serveur GraphQL récupère le trou.
Étape 5 — Cartographie comme route API
Add-to-cart, mise à jour-quantité, supprimer-de-cart — tous les flux à travers un /api/cart Maître. L’itinéraire se lit sessionToken de cookies, exécute la mutation WPGraphQL avec Cart-Token joint, et retourne le panier mis à jour. Le middleware de l’étape 3 prend soin de persister tout jeton tournant sur le chemin de sortie.
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 });
}
// ...mise à jour / supprimer / appliquerCoupon / effacer suivre le même modèle
}Remarque : variation tableau sur l’entrée. L’ajout de produits variables à la carte nécessite des variations qui mélangent des attributs globaux et locaux — voir l’étape 7.

Étape 6 — Une page de produit poli
Les pages de produit convertissent les navigateurs en acheteurs, donc c’est là que vous passez du temps de conception réel. Composant serveur, requête ISR-cached, unique GraphQL pour l’ensemble de la vue: titre, image héros, galerie (hommage à chaque image , rapport d’aspect naturel de mediaDetails), prix + badge de vente, badge de stock, descriptions courtes + longues, produits connexes, chapelure, et Product schema.org JSON-LD pour Google Shopping. La page envoie le __typename à l’un des deux composants héros. Les deux rendent mobile-premier — la plupart du trafic avant magasin est mobile, et la capture d’écran ci-dessous est la vue petit port de vue pour le prouver.
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} />
</>
);
}
Étape 7 — Variations pour les produits variables
VariableProductHero est une composante client qui possède l’état de sélection des attributs et soulève le prix, l’image et le badge stock à la variation correspondante. Deux petits détails mais porteurs:
- Correspondance par
namePaslabel. Pour les attributs globaux (appuyés par la taxonomie :pa_color,pa_size), les deuxnameetlabeld’accordProductAttributeetVariationAttribute. Pour les attributs locaux (forme libre sur le produit, pas de taxonomie),ProductAttribute.labelest la forme humaine (“Logo”) maisVariationAttribute.labelestsanitize_title()D («logo»). Les deuxlabelles champs ne sont pas comparables.namepasse parsanitize_title()de part et d’autre, il est donc toujours d’accord —name. - Envoyer un
variationtableau sur add-to-cart. Quand la sélection du client correspond à un réelvariationId, envoyer les deux — il permet WooGraphQL pin l’élément du panier à cette variation, que les attributs soient globaux ou locaux.
// VariationAttribute.label est sanitize title()'d pour les attributs LOCAL
// (par exemple "Logo" → "logo") mais ProductAttribute. l'étiquette est la forme humaine
// (Logo). Les deux exposent `name`, qui est sanitize title()'d sur les deux
// les côtés – donc correspondre par «nom», jamais par «étiquette».
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>
</>
);
}
Étape 8 — Panier et commande via le proxy NextPress
Deux routes, douze lignes chacune. Ils vivent sous (wordpress-pages) donc ils prennent la mise en page WP-rendering de l’étape 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>
);
}Les (wordpress-pages) La mise en page récupère le graphique de l’actif (avec Cart-Token!), la page rend le blockup du chariot WC, et le script frontend du chariot prend le dessus sur l’hydratation — en utilisant la même Cart-Token les ponts intermédiaires. Même forme pour /checkout/page.tsx; même forme pour les itinéraires du blog.

Étape 9 — Recherche de la commande client via une action serveur
Une fois qu’une commande existe, le client a besoin d’un moyen de la voir. Le flux standard WooCommerce met le reçu sur /checkout/order-received/[id], mais il a aussi besoin d’une page de recherche manuelle pour les visites répétées, les onglets de confirmation perdus, et les liens de suivi par courriel. /view-order est cette page — un composant serveur avec un formulaire, affichage sur une action serveur.
L’action fait quelque chose de subtil : updateCustomer sans id cible le client attaché au courant Cart-Token séance. Réglage billing.email lie cette session au courriel; le client orders connection retourne ensuite chaque commande passée contre cette adresse – y compris les commandes d’invités, qui est le point entier. L’action se réduit par orderKey, qui agit comme un secret partagé: savoir un email seul n’est pas assez pour faire surface à quelqu’un d’autre.
'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 }
}
}
}
}
`;
// Exécuter la mise à jour du client sans `id` applique la mutation au
// client attaché à la session actuelle Cart-Token. Réglage
// facturation.email lie cette session au courriel; le client
// commande connection retourne ensuite chaque commande passée contre
// adresse — y compris les commandes des clients. Nous rétrécissons par ordreKey, qui
// agit comme un secret partagé: connaître un email seul ne suffit pas à
// Surveille l'ordre de quelqu'un d'autre.
//
// Cela fonctionne uniquement via des invocations d'action serveur, donc le GraphQL
// endpoint et la sémantique liant le client n'atteignent jamais
// client. Les lecteurs de tutoriels n'ont pas besoin d'un mot de passe d'application — le
// Cart-Token authentifie la 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');
}Ce que cela évite : faire rouler à la main un mot de passe d’application shop-manager dans la démo. Tutoriel lecteurs ne ont pas un, et l’expédition du tutoriel avec “ici, coller vos identifiants admin dans .env” serait irresponsable. Le modèle Cart-Token-as-shared-secret garde tout à l’intérieur du modèle de session existant.

Étape 10 — Réception automatique à /checkout/commande reçue
Lorsque WooCommerce redirige le client après une commande réussie, l’URL est /checkout/order-received/[id]?key=wc_order_…. Les key est la clé de l’ordre. L’email n’est pas dans l’URL — il est dans le bloc de paiement WC, le formulaire de facturation, que le client vient de remplir. Nous captons ce côté client dans un cookie, puis la page de réception lit le cookie + clé URL et exécute la même lookupOrder action du serveur depuis l’étape 9.
Le composant de capture sonne simple (lisez quelques entrées de courriel, écrivez à un cookie) mais le bloc de caisse WC est agité: il monte ses champs asynchrone, remonte les champs de facturation quand “Utilisez l’adresse de livraison pour la facturation” toggles, ET — la surprise — hydrate les valeurs de champ de sa propre session de stockage sans tir input ou change événementsUne délégation input auditeur seul manquera entièrement le remplissage automatique. Trois sources couvrent l’écart :
- Analyse initiale DOM au montage — valeurs de capture déjà présentes au moment du rendu (autofill, navigateur automatique).
- Observateur de la mutation le
document.bodyavecattributeFilter: ['value']— capture les entrées montées tardivement et la valeur de session remise écrit. - Phase de capture
input/changedélégation ledocument— capture la frappe en direct.
'use client';
import { useEffect } from 'react';
import { CHECKOUT_EMAIL_COOKIE } from '@/utils/constants';
// Le bloc de caisse WC monte ses champs asynchrones et remonte
// l'ensemble des champs de facturation lorsque le client se lance "Utilisez l'expédition pour
// facturation". Il hydrate également les valeurs de sa propre session de stockage
// SANS lancement d'événements d'entrée/de changement — donc seul un auditeur délégué
// rate le courriel rempli automatiquement. Nous avons besoin de trois sources :
// 1. Scan DOM initial au montage — valeurs de capture déjà présentes.
// 2. Mutation Observer - capture des entrées et des
// session-restaurer la valeur écrit.
// 3. délégation d'entrée/de changement — capture la saisie en direct de l'utilisateur.
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;
}Montez ça /checkout/page.tsx à côté de la <Content> bloc et le cookie est défini au moment où le client clique sur Place Order. La page de réception la lit et rend — aucun formulaire, aucun clic supplémentaire.
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>
);
}Le lien de repli vers /view-order?key=… les questions pour le “client a rouvert l’email de réception trois jours plus tard sur un nouvel appareil” cas — le cookie est parti, mais la clé de commande dans l’email fonctionne toujours pour la recherche manuelle.
Étape 11 — Examens de produits
Les avis sont un petit gestionnaire de POST qui appelle WooGraphQL. writeReview mutation. Le formulaire client envoie productId, nom de l’auteur, email, notation et contenu; l’itinéraire joint Cart-Token, exécute la mutation, et le moteur WP gère la file d’attente de modération, la vérification de spam et le stockage.
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 });
}Faites-le passer à un <ReviewForm /> composante client à l’intérieur <ProductTabs /> sur la page produit. La liste d’examen elle-même est rendue côté serveur de la même requête de produit; pagination est ce que vous voulez qu’il soit.

Essai de fumée du flux total
De bout en bout: visite /products/hoodie, choisissez une variante de couleur + logo, cliquez sur Ajouter au panier /cart avec l’article qui s’affiche, cliquez sur Passer à la caisse, remplissez la carte de test Stripe 4242 4242 4242 4242, commandez, atterrissez sur /checkout/order-received/[id]?key=… avec le reçu complet rendu automatiquement. Cinq clics, un single Next.js host, l’orchestration complète WooCommerce backend. Pas de connexion, pas de compte, pas de boucle de rafraîchissement JWT.
Si la page de caisse rend “Impossible de créer l’ordre à partir d’un panier vide” fetchAssetsByUri obtenu de l’étape 4. Ouvrir la requête à /graphql qui court assetsByUri sur le côté SSR et vérifier qu’il porte Cart-Token en-tête. La façade du bloc-cart s’étendra de REST indépendamment, mais la Paiement bloc préchargé wcSettings.checkoutData est généré côté serveur à l’heure de saisie de l’actif et entre dans toute l’interface utilisateur.

Ce que vous n’avez pas construit
- Portails de paiement — Stripe, PayPal, Square, Klarna, ~50 autres
- Moteurs fiscaux — Taxe WooCommerce (libre), Avalara, TaxJar
- API de taux d’expédition — USPS, UPS, FedEx, DHL, logique de taux forfaitaire personnalisé
- Abonnements — cycles de facturation, essais, mise à niveau au prorata
- Inventaire — niveaux des stocks, notifications de stocks bas, commandes en arrière, suivi par variable
- Moteur à coupon — limites d’utilisation, segments clients, restrictions produits/catégories
- Gestion des commandes — remboursements, remboursements partiels, notes de commande, emails clients
- Révision de la modération — vérification des pourriels, filtre de profanité, interface utilisateur de file d’attente
WooCommerce gère tout ça. Vous manipulez la page produit UX et le chrome en front de magasin. NextPress relie les deux sans vous faire choisir entre « regarde comme je veux » et « fonctionne avec les extensions que j’ai achetées ».
Le raccourci : un WooGraphQL Abonnement pro
Tout au-dessus est le chemin open-source. Il s’agit de quelques centaines de lignes de nouveau code sur le dessus du tutoriel de blog, pas de dépendances supplémentaires, et les bateaux de repo démo prêts à cloner. Si votre équipe est mieux passé sur storefront UX que sur l’écriture du code de cycle de vie de gestion de session, c’est exactement ce que a WooGraphQL Abonnement pro C’est pour.
Un abonnement regroupe trois choses :
Le plugin WooGraphQL Pro
Un plugin WordPress qui étend le schéma GraphQL avec des types, des requêtes et des mutations pour les variantes de produits WooCommerce que vous vendez en fait: Abonnements, produits composites, ensembles de produits et compléments de produits. Le schéma gratuit WooGraphQL expose uniquement les produits Simple et Variable ; au moment où votre catalogue comprend un forfait ou un abonnement, vous êtes hand-rolling REST fallbacks. Avec le plugin Pro, ces types de produits sont de première classe dans le schéma, et un seul { product(id: …) } requête retourne quelle que soit la forme que ce produit se trouve être.
L’application create-woonext CLI
A échafaudage qui fonctionne comme create-next-app, mais la plaque de chaudière qu’elle génère est un Next.js + WooGraphQL de travail. npx create-woonext-app my-shop, pointez-le à votre moteur WP , et vous avez obtenu des pages de produit , panier , caisse , et flux de compte en cours de bout en bout avant de toucher un fichier , tous câblés avec les crochets Pro et les composants ci-dessous .
Les paquets @woographql/* JS
- @woographql/suivant — une boîte à outils de génération de composants qui fonctionne de la même façon que Shadcn/ui : lancez une commande, le composant atterrit dans votre repo, vous possédez et personnalisez le code. La bibliothèque des composants couvre toute la surface de l’entrepôt —
CartOptionsseul rend l’interface utilisateur cart-action pour chaque type de produit pris en charge par Woo (Simple, Variable, Composite, Bundle, Abonnement, Add-On), avec l’état du formulaire et la validation déjà câblé. - @woographql/react-hooks — les crochets tapés qui effondrent le code du cycle de vie que vous avez écrit ci-dessus.
useSessionManager()remplace le cookie-juggling, la rotation du cart-token, et l’en-tête SSR-time.useCartMutations()remplace/api/cartroute plus la colle client, avec des mises à jour optimistes et une erreur de retour par clé inclus. - @woographql/session-utils — les éléments de construction de niveau inférieur derrière ces crochets: chiffrement des jetons, abstraction des cookies/stockage, sérialisation à l’état signé. Les bits que vous préférez ne pas réimplanter une fois que vous avez expédié votre deuxième magasin Woo sur Next.
Choisissez le chemin open-source si vous voulez comprendre ce qui se passe sous le capot — c’est un excellent endroit pour commencer, et le code reste le vôtre pour toujours. Choisissez l’abonnement si vous préférez expédier le magasin et ne pas devenir un auteur de bibliothèque de gestion de session. De toute façon, l’architecture de ce tutoriel est la bonne architecture ; l’abonnement vous donne juste la version polie du code de cycle de vie comme une dépendance dactylographiée, testée.
Quand c’est le mauvais appel
- Projet Greenfield sans dépendances commerciales et catalogue monoproduit. Stripe Checkout + un magasin React personnalisé expédie plus rapidement. Tirer dans WP + Woo est surkill jusqu’à ce que vous avez besoin de taxes, d’expédition, ou plusieurs UGS.
- Devis B2B à prix manuel. Le flux de travail cart-builder pour “demander une soumission, obtenir des prix, convertir à la commande” ne map proprement sur le modèle de catalogue Woo.
- Marchés multivendeurs. Woo a des extensions multi-vendor mais ils sont complexes; une plate-forme native de marché comme Medusa est généralement un meilleur ajustement.
Pour le reste — la plupart des applications qui ont besoin de commerce, en particulier celles qui servent déjà du contenu de WordPress — en ajoutant Woo comme moteur de commerce et NextPress que le pont vous amène à « vendre le produit sur le même Next.js hôte que le reste du site » dans un après-midi. Abonnements, passerelles de paiement personnalisées, multi-monnaie, récupération abandonnée-cart, niveaux de prix B2B — ils sont l’extension installe contre l’administrateur WP, pas de refacteurs contre votre application.

Leave a Reply