Tienes el blog funcionando. Ahora agrega la tienda. Este paseo recoge exactamente dónde el blog sin cabeza tutorial Se va. Dos nuevos plugins de WordPress, un grupo de ruta se dividió para que las páginas dotadas de WP obtengan su propio diseño de raíz, y un solo encabezado — Cart-Token — bridging your Next.js session to WooCommerce’s Store API. La misma aplicación que está sirviendo su blog ahora está sirviendo un escaparate, con carrito y salida corriendo a través de cada extensión WooCommerce que ya ha instalado.
Compañero repo: github.com/AxisTaylor/nextpress-woographql-quickstart. Es nextpress-quickstart desde el tutorial del blog con todo en este post aplicado en la parte superior – clonarlo, ejecutar npm install && npm run dev, y usted tiene todo el flujo corriendo contra woographqldemo.wpengine.com.

Para quién es
Usted tiene una aplicación Next.js existente — la suya, o la que construyó siguiendo el tutorial del blog. Usted quiere comercio: productos, carrito, checkout, pagos, inventario, impuestos, envío, todo. No te interesa escribir esas últimas seis cosas desde cero. WooCommerce funciona ~5 millones de tiendas en vivo y el mercado de extensión es el mayor en cualquier plataforma de comercio de código abierto. La configuración de WordPress sin cabeza que construyó para el blog también es la configuración sin cabeza WooCommerce — el mismo backend, el mismo proxy, dos plugins más y un refactor de diseño.
Una nota sobre la austeridad: no hay ninguna
Este tutorial pasa por un sólo para invitados escaparate. No tiene página de inicio, no dashboard cuenta, no hay bucle de actualización JWT. El Cart-Token WooCommerce manos de vuelta es en sí mismo el identificador de sesión — un invitado puede navegar, añadir a la cesta, comprobar y buscar su orden con nada más que su email de facturación y la clave de orden de su confirmación. Eso cubre todo el camino feliz para la mayoría de los escaparates. Si quieres cuentas de clientes autenticadas, cablea hacia arriba wp-graphql-headless-login o wp-graphql-jwt-authentication por separado — está diseñado para conectarse al lado del flujo Cart-Token sin perturbarlo.
Qué cambios en el tutorial del blog
- Dos nuevos plugins WP: WooCommerce y WPGraphQL para WooCommerce.
- Grupo de ruta dividido. Suprimir
app/layout.tsx. Mover las rutas de aplicación haciaapp/(main)/con su propio diseño raíz. Mover las rutas rendidas por WP (cart, checkout, blog) enapp/(wordpress-pages)/con un diseño de raíz separado que pone<WPHead />dentro<head>. Los mapas de importación y los módulos de script emitidos por el bloque de control de WC sólo resuelven correctamente cuando la etiqueta importador vive en el documento<head>, y un diseño anidado no puede poner allí si hay un diseño de la raíz externa que posee<html>. - Middleware puentea la sesión. Leer el
sessionTokencookie en cada solicitud WP REST / AJAX proxied, adjuntarla como laCart-TokenCabeza. Capturar cualquier rotaciónCart-Tokensobre la respuesta y persistir en la cookie para que la siguiente solicitud se mantenga en la misma sesión de WC. - GraphQL del lado del servidor muestra el mismo encabezado. Ambos
fetchPageByUriYfetchAssetsByUridebe incluirCart-Token.fetchAssetsByUries el único gotcha más agudo — el bloque de registro de la WC servidor-renders contra un carrito vacío y hornea un"Cannot create order from empty cart"error enwcSettings.checkoutData, y la página está permanentemente atascada. - Una página de producto pulido con un selector de variación para productos variables (igualable por atributo
name, nolabel— hay un gotcha sutil para los atributos locales). - Buscador de pedidos de invitados a través de una acción del servidor.
/view-ordertoma un correo electrónico de facturación + clave de pedido y hace el pedido. La acción del servidor une el correo electrónico a la corrienteCart-Tokensesión porupdateCustomer, luego estrecha la conexión de órdenes por orden clave. Sin contraseña de aplicación, sin cuenta de gestión de compras. - Recibimiento automático
/checkout/order-received/[id]. Un componente cliente captura el correo electrónico de facturación del bloque de verificación WC en una cookie. La página de recepción lee la cookie +?key=en la URL y busca el pedido a través de la misma acción del servidor — ningún formulario para rellenar. - Opiniones de productos a través de un diminuto
/api/product/reviewruta que llama WooGraphQLwriteReviewmutación con el Cart-Token reenviado.
Paso 1 — Instalar dos plugins WP
- WooCommerce — desde el directorio plugin. Ejecute el asistente de configuración, suelte el Stripe Test para pagos, agregue un par de productos demo para que haya algo que preguntar. Confirmación
/carty/checkoutuse las plantillas de bloque (por defecto desde Woo 8.3) en lugar de los códigos cortos heredados — eso es lo que NextPress proxy. - WPGraphQL para WooCommerce — añade el producto, el carrito, el cliente y los tipos de pedido
/graphql. Verificar en Postman (o cualquier cliente HTTP que supere los encabezados de respuesta – GraphiQL los oculta) disparando aladdToCartmutación contra un verdaderoproductIdnoCart-Tokena petición. WP crea una sesión de invitados fresca y devuelve el JWT en elCart-Tokencabeza de respuesta.
mutation BootGuestSession {
addToCart(input: { productId: 13, quantity: 1 }) {
cart {
contents { itemCount }
total
}
}
}El Cart-Token encabezado sólo barcos en carro y sesión mutaciones — addToCart, updateItemQuantity, removeItemsFromCart, applyCoupon, y amigos. Preguntas frecuentes contra /graphql no producir uno, y GraphiQL no mostrará encabezados de respuesta de todos modos, por lo que este paso de verificación utiliza Postman. Una vez que el middleware ve que el encabezado en una respuesta proxida persiste el nuevo valor en el sessionToken cookie, y cada solicitud posterior — REST llama desde el bloque de salida, consultas GraphQL lado servidor en su aplicación Next.js — paseos en él.

Paso 2 – Grupo de ruta división
Este es el cambio estructural más grande. Suprimir app/layout.tsx. Mover cada ruta bajo uno de los dos grupos de rutas de nivel superior, cada uno con su propio diseño raíz:
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
¿Por qué? Next.js sólo permite <html> y <body> en diseños de raíz. Sin una app/layout.tsx, cada grupo de ruta de alto nivel se convierte en su propia raíz. El (wordpress-pages) root can put <WPHead /> directamente dentro <head> – que es donde <script type="importmap"> emitido para los scripts de formato módulo de WC tiene para vivir. Prueba esto con un diseño anidado bajo una sola raíz y el mapa de importación termina en <body>; los scripts del módulo entonces no resuelven @wordpress/plugins y todo el cart-block árbol tira de entrada.
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>
);
}Paso 3 – El middleware puentea el Cart-Token
Actualización src/proxy.ts para adjuntar el token del carrito a cada solicitud WP REST/AJAX proxied, luego capturar cualquier token WP rotado escribe de nuevo en la respuesta. Cuando el cart-block en frente /wc/store/v1/cart desde el navegador, pasa a través de su proxy NextPress — y ese proxy necesita traducir su sessionToken cookie en Cart-Token WooCommerce espera. WooCommerce ocasionalmente rotará el token (después de una mutación de carrito, un cupón aplica, etc.); el encabezado de respuesta es cómo indica el nuevo valor.
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. Adelante el Carrito-Token en cada llamada REST/AJAX proxied para que el
// WC lado del navegador bloquea la tierra en la sesión de carrito real del huésped.
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 solicitud, luego capturar cualquier rotated Cart-Token WP escribe
// volver a la respuesta y persistir como la cookie de sesiónToken.
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|.*\\.).*)',
],
};
Paso 4 – Ayudadores de GraphQL que avanzan el token
Dos ayudantes en src/lib/wp.ts Hable con WPGraphQL: gqlWithSession para las consultas que necesitan la sesión del carrito, y fetchAssetsByUri / fetchPageByUri que ambos lo usan. Ambos DEBE reenviar el Carrito-Token – y el segundo es el 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 };
}
// Crítico: Cart-Token adelante aquí también. Los activosByUri resolver
// activa el script WP-side enqueueing, que corre WC
// hydrate data from api request for the checkout block. Sin
// cabecera, wc()-propiedad está vacía para esa petición, el hidratante produce
// "No se puede crear el orden del carrito vacío", y que los barcos de error
// wcSettings.checkoutData → el bloqueo de salida tieneCheckoutError
// La puerta dispara permanentemente.
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;
}Ese comentario sobre fetchAssetsByUri no es la decoración — es la razón entera /checkout falla silenciosamente si lo olvidas. WPGraphQL está bien, las llamadas REST del navegador funcionan, el carro REST devuelve los elementos correctos, y la página sigue representando un error de carga vacía porque los datos precargados del servidor de bloques de WC se generaron en una sesión de carrito vacía. Hacia adelante la ficha ambos Los fetches de GraphQL del lado del servidor cierran la brecha.
Paso 5 – Carrito como ruta API
Add-to-cart, update-quantity, remove-from-cart — todo el flujo a través de uno /api/cart manejador. La ruta lee sessionToken de cookies, ejecuta la mutación WPGraphQL con Cart-Token adjunta, y devuelve el carro actualizado. El middleware del Paso 3 se encarga de persistir cualquier señal rota en el camino de salida.
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 });
}
// ...actualizar / eliminar / aplicarCoupon / clara seguir el mismo patrón
}Note el variation array en la entrada. Variable-product add-to-cart lo necesita para variaciones que mezclan atributos globales y locales – véase Paso 7.

Paso 6 – Una página de producto pulido
Las páginas de productos convierten los navegadores en compradores, así que aquí es donde pasas tiempo de diseño real. Componente de servidor, ISR-cached, única consulta GraphQL para toda la vista: título, imagen de héroe, galería (honorando la relación de aspecto natural de cada imagen desde mediaDetails), precio + venta insignia, insignia de stock, breve + largas descripciones, productos relacionados, migas de pan, y producto schema.org JSON-LD para Google Shopping. La página se envía en __typename a uno de los dos componentes de héroe. Ambos renderizar el primer móvil — la mayoría del tráfico de tiendas es móvil, y la captura de pantalla a continuación es la vista de pequeño puerto para probarlo.
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} />
</>
);
}
Paso 7 – Variaciones para productos variables
VariableProductHero es un componente cliente que posee el estado de selección de atributos y eleva el precio, la imagen y la insignia de stock a la variación emparejada. Dos detalles pequeños pero de carga:
- Partido por
name, nolabel. Para los atributos globales (con respaldo de la toxonomía:pa_color,pa_size), ambosnameylabelestar de acuerdoProductAttributeyVariationAttribute. Para atributos locales (forma libre sobre el producto, sin taxonomía)ProductAttribute.labeles la forma humana (“Logo”) peroVariationAttribute.labelessanitize_title()’d (“logo”). Los doslabelcampos no son comparables.namepasasanitize_title()en ambos lados, por lo que siempre está de acuerdo – coincide conname. - Enviar un
variationarray en add-to-cart. Cuando la selección del cliente corresponde a un realvariationId, enviar ambos — permite WooGraphQL pin el artículo del carrito a esa variación independientemente de si los atributos son globales o locales.
// VariaciónAttribute.label es sanitize title()'d for LOCAL attributes
// (por ejemplo "Logo" → "logo") pero ProductAttribute. etiqueta es la forma humana
// ("Logo"). Ambos exponen `nombre`, que es sanitize title()'d on BOTH
// los lados - así que coinciden con el `nombre`, nunca por `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>
</>
);
}
Paso 8 – Carrito y checkout a través del proxy NextPress
Dos rutas, doce líneas cada una. Viven bajo (wordpress-pages) así que recogen el diseño de WP-rendering del Paso 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>
);
}El (wordpress-pages) el diseño del gráfico del activo (con Cart-Token!), la página hace que la marca de bloques de la caja WC, y el script de inicio de la cesta-block se apodera de la hidratación — utilizando el mismo Cart-Token los puentes de middleware. La misma forma para /checkout/page.tsx; la misma forma para las rutas del blog.

Paso 9 – Búsqueda de pedidos de clientes a través de una acción de servidor
Una vez que existe un pedido, el cliente necesita una manera de verlo. El flujo estándar WooCommerce pone el recibo en /checkout/order-received/[id], pero también necesita una página de búsqueda manual para visitas de repetición, pestañas de confirmación perdidas y enlaces de seguimiento de correo electrónico. /view-order es esa página — un componente servidor con un formulario, publicar en una acción servidor.
La acción hace algo sutil: updateCustomer sin una id blancos del cliente conectado a la corriente Cart-Token sesión. Ajuste billing.email une esa sesión al correo electrónico; el cliente orders La conexión entonces devuelve cada pedido puesto en contra de esa dirección, incluyendo las órdenes de huéspedes, que es todo el punto. La acción se estrecha por orderKey, que actúa como un secreto compartido: conocer un correo electrónico por sí solo no es suficiente para hacer realidad el orden de alguien más.
'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 }
}
}
}
}
`;
// Actualización de ejecuciónCustomer sin un `id' aplica la mutación a la
// cliente adjunto a la actual sesión Cart-Token. Ajuste
// billing.email vincula esa sesión al correo electrónico; el cliente
// orden de conexión entonces devuelve cada orden puesto contra eso
// dirección - incluyendo órdenes de huéspedes. Nos estrechamos por ordenKey, que
// actúa como un secreto compartido: saber un email solo no es suficiente
// enciende la orden de alguien más.
//
// Esto se ejecuta sólo a través de invocaciones de acción del servidor, por lo que el GraphQL
// endpoint y la semántica que un cliente nunca llegan al
// cliente. Los lectores tutoriales no necesitan una contraseña de aplicación:
// La sesión propia Cart-Token autentica la mutación.
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');
}Lo que esto evita: la inscripción manual de una contraseña de aplicación de gestión de tienda en la demo. Los lectores tutoriales no tienen uno, y el envío del tutorial con “aquí, pega tus credenciales de administración en .env” sería irresponsable. El patrón secreto de Cart-Token-as-shared mantiene todo dentro del modelo de sesión existente.

Paso 10 – Recibimiento automático en /checkout/order-recibido
Cuando WooCommerce redirige al cliente después de un chequeo exitoso, la URL es /checkout/order-received/[id]?key=wc_order_…The key es la clave del orden. El correo electrónico no está en la URL — está en el formulario de facturación del bloque de compra de WC, que el cliente acaba de rellenar. Capturamos ese correo electrónico cliente-side en una cookie, luego la página de recepción lee la clave cookie + URL y ejecuta la misma lookupOrder acción servidor desde Paso 9.
El componente de captura suena directamente (leer un par de entradas de correo electrónico, escribir a una cookie) pero el bloque de verificación WC es fusible: monta sus campos asinc, vuelve a montar el campo de facturación cuando “Use dirección de envío para facturación” toggles, Y — la sorpresa — hidrata valores de campo de su propio almacenamiento de sesión sin disparar input o change eventos. Delegada input El oyente solo perderá el autofill por completo. Tres fuentes cubren la brecha:
- Escaneo inicial DOM en montaje — captura valores ya presentes en el tiempo de renderizado (autofill, navegador autocompleto).
- MutationObserver on
document.bodyconattributeFilter: ['value']— capta los insumos tardíos y escribe el valor almacenado en la sesión. - Capture-phase
input/changedelegación ondocument– las capturas en vivo escribiendo.
'use client';
import { useEffect } from 'react';
import { CHECKOUT_EMAIL_COOKIE } from '@/utils/constants';
// El bloque de control de WC monta sus campos de forma asincrónica y vuelve a montar
// el campo de facturación cuando el cliente se mueve "Use envío para
// Facturación". También hidrata valores de su propio almacenamiento de sesión
// Sin incendiar eventos de entrada y cambio, por lo que un oyente delegado solo
// Echa de menos el correo electrónico autofilado. Necesitamos tres fuentes:
// 1. Escaneo inicial DOM en el montaje — captura valores ya presentes.
// 2. MutationObserver — captura los insumos y
// El valor almacenado en sesión escribe.
// 3. Delegación de entrada/cambio: capturas de usuarios en 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;
}Montar eso /checkout/page.tsx al lado del <Content> bloque y la cookie se establece cuando el cliente haga clic en Place Order. La página de recepción lo lee y renderiza — sin formulario, sin clic extra.
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>
);
}El enlace de retroceso a /view-order?key=… asuntos para el “customer reabrió el correo electrónico de recibo tres días después en un nuevo dispositivo” caso — la cookie se ha ido, pero la clave de pedido en el correo electrónico todavía funciona para la búsqueda manual.
Paso 11 – Comentarios de los productos
Los comentarios son un pequeño manejador de POST que llama WooGraphQL writeReview mutación. El formulario del cliente envía productId, nombre de autor, correo electrónico, calificación y contenido; la ruta adjunta Cart-Token, ejecuta la mutación, y el backend WP maneja la cola de moderación, cheque de spam y almacenamiento.
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 });
}Anímalo a un <ReviewForm /> componente cliente dentro <ProductTabs /> en la página del producto. La lista de revisión en sí se hace lado servidor de la misma consulta de producto; paginación es lo que quieras que sea.

El humo prueba el flujo completo
Final a extremo: visita /products/hoodie, elegir un color + variante del logotipo, haga clic en Añadir al carrito, aterrizar /cart con el elemento que muestra, haga clic en Proceder para el checkout, rellenar la tarjeta de prueba Stripe 4242 4242 4242 4242, colocar la orden, aterrizar en /checkout/order-received/[id]?key=… con el recibo completo entregado automáticamente. Cinco clics, single Next.js host, orquestación completa de WooCommerce backend. Sin login, sin cuenta, sin bucle de refresco JWT.
Si la página de checkout hace “No se puede crear el orden del carrito vacío” — es la fetchAssetsByUri Gotcha del Paso 4. Abra la solicitud /graphql que corre assetsByUri en el lado SSR y verificar que lleva el Cart-Token Cabeza. El carret-block frontend se populará de REST independientemente, pero el checkout bloque precargado wcSettings.checkoutData se genera lado servidor en tiempo de compra de activos y cierra toda la UI.

Lo que no construiste
- Portales de pago — Stripe, PayPal, Square, Klarna, ~50 otros
- Motores fiscales — Impuesto de lana (gratuito), Avalara, TaxJar
- API de tarifas de envío — USPS, UPS, FedEx, DHL, lógica de tarifa plana personalizada
- Suscripciones: ciclos de facturación, ensayos, dunning, actualizaciones prorrateadas
- Inventario: niveles de stock, notificaciones de bajo valor, pedidos atrasados, seguimiento pervariante
- Motor Coupon — límites de uso, segmentos de clientes, restricciones de producto/categoría
- Gestión de pedidos — reembolsos, reembolsos parciales, notas de pedidos, correos electrónicos del cliente
- Moderación de revisión — cheque de spam, filtro de profanidad, cola UI
WooCommerce maneja todo. Usted maneja la página de producto UX y el cromo de la tienda. NextPress conecta los dos sin hacer que elijas entre “mira la forma que quiero” y “trabaja con las extensiones que compré”.
El atajo: un WooGraphQL Pro subscription
Todo arriba es el camino de código abierto. Es unas pocas cientos de líneas de nuevo código en la parte superior del tutorial del blog, sin dependencias adicionales, y los depósitos demo listos para clonar. Si el tiempo de su equipo es mejor gastado en el escaparate UX que en escribir código de vida de gestión de sesión, eso es exactamente lo que a WooGraphQL Pro subscription es para.
Una suscripción agrupa tres cosas:
El plugin WooGraphQL Pro
Un plugin de WordPress que extiende el esquema GraphQL con tipos, consultas y mutaciones para las variantes de productos WooCommerce que realmente vende: Suscripciones, productos compuestos, envases de productos y complementos de productos. El esquema gratuito WooGraphQL sólo expone productos simples y variables; en el momento en que su catálogo incluye un paquete o una suscripción, usted está girando a mano retrocesos REST. Con el plugin Pro, esos tipos de productos son de primera clase en el esquema, y un solo { product(id: …) } la consulta devuelve cualquier forma que el producto resulta ser.
La creación-woonext-app CLI
A andamio que funciona como create-next-app, pero la caldera que genera es una tienda de trabajo Next.js + WooGraphQL. npx create-woonext-app my-shop, apunte a su WP backend, y tiene páginas de producto, carrito, checkout y flujos de cuenta corriendo de extremo a extremo antes de tocar un archivo, todo conectado con los ganchos Pro y componentes abajo.
Paquetes @woographql/* JS
- @woographql/next — un kit de herramientas de generación de componentes que funciona de la misma manera que hace shadcn/ui: ejecutar un comando, el componente aterriza en su repo, usted posee y personaliza el código. La biblioteca de componentes cubre toda la superficie del escaparate —
CartOptionssolo renderiza la interfaz de usuario para cada tipo de producto Soportes Woo (Simple, Variable, Composite, Bundle, Subscription, Add-On), con estado de forma y validación ya cableado. - @woographql/react-hooks — ganchos de tipo que colapsan el código del ciclo de vida que escribió anteriormente.
useSessionManager()Sustituye el rebote de las galletas, la rotación de los cartuchos, y el reenvío del encabezado SSR-time.useCartMutations()reemplaza a los/api/cartruta más el pegamento del cliente, con actualizaciones optimistas y rebote de error por tecla incluido. - @woographql/session-utils — los bloques de construcción de nivel inferior detrás de esos ganchos: encriptación de token, abstracción de cookie / almacenamiento, serialización de estado firmado. Los bits que prefieres no volver a aplicar una vez que hayas enviado tu segundo Woo storefront en Next.
Escoge el camino de código abierto si quieres entender lo que está pasando bajo la capucha — es un gran lugar para empezar, y el código permanece para siempre. Elija la suscripción si prefiere enviar el escaparate y no convertirse en un autor de biblioteca de gestión de sesión. De cualquier manera, la arquitectura en este tutorial es la arquitectura correcta; la suscripción sólo le da la versión pulida del código de ciclo de vida como una dependencia tipo, probada.
Cuando esta es la llamada equivocada
- Proyecto Greenfield sin dependencia comercial y catálogo de productos únicos. Stripe Checkout + una costumbre Realizar las naves frente a la tienda más rápido. Tirar en WP + Woo es sobrematar hasta que usted necesita impuestos, envío, o múltiples SKUs.
- Citas B2B a mano. El flujo de trabajo de cart-builder para “Solicitar una cotización, obtener precios, convertir al pedido” no mapa de forma limpia en el modelo de catálogo de Woo sin trabajo personalizado significativo.
- Mercados multi-vendor. Woo tiene extensiones multi-vendor pero son complejas; una plataforma nativa de mercado como Medusa es generalmente un mejor ajuste.
Para el resto — la mayoría de las aplicaciones que necesitan comercio, especialmente las que ya sirven contenido de WordPress — añadir Woo como backend de comercio y NextPress como el puente te lleva a “ventar producto en el mismo Next.js host como el resto del sitio” en una tarde. Suscripciones, pasarelas de pago personalizadas, multi-currencia, recuperación de cartes abandonados, niveles de fijación de precios B2B — son instalaciones de extensión contra el administrador WP, no refactores contra su aplicación.

Leave a Reply