Você tem o blog trabalhando. Agora, adicione a loja. Este passeio começa exatamente onde o tutorial do blog sem cabeça Deixa-me em paz. Dois novos plugins WordPress, um grupo de rota dividido para que as páginas WP-rendered obter o seu próprio layout raiz, e um único cabeçalho – Cart-Token — ligando sua sessão Next.js à API da WooCommerce Store. O mesmo aplicativo que está servindo seu blog está agora servindo uma frente de loja, com carrinho e checkout executando através de cada extensão WooCommerce que você já instalou.
Repo: github.com/AxisTaylor/nextpress-woographql-quickstart. É nextpress-quickstart do tutorial do blog com tudo neste post aplicado no topo — cloná-lo, executar npm install && npm run dev, e você tem todo o fluxo correndo contra woographqldemo.wpengine.com.

Para quem é isto?
Você tem um aplicativo Next.js existente — o seu, ou o que você construiu após o tutorial do blog. Você quer comércio: produtos, carrinho, checkout, pagamentos, inventário, impostos, transporte, tudo. Você não está interessado em escrever essas últimas seis coisas do zero. O WooCommerce executa cerca de 5 milhões de lojas ao vivo e o mercado de extensão é o maior em qualquer plataforma de comércio de código aberto. A configuração WordPress sem cabeça que você construiu para o blog também é a configuração WooCommerce sem cabeça — mesmo backend, mesmo proxy, mais dois plugins e um refator de layout.
Uma nota na autenticação: não há nenhuma
Este tutorial percorre um somente para hóspedes Em frente à loja. Sem página de login, sem painel de contas, sem ciclo de atualização JWT. A Cart-Token WooCommerce mãos de volta é em si o identificador de sessão – um convidado pode navegar, adicionar ao carrinho, verificar, e olhar para cima o seu pedido com nada, mas o seu e-mail de cobrança ea chave de ordem de sua confirmação. Isso cobre todo o caminho feliz para a maioria das lojas. Se você quer contas autenticadas de clientes, ligue wp-graphql-headless-login ou wp-graphql-jwt-autenticação separadamente — é projetado para conectar ao lado do fluxo Cart-Token sem perturbá-lo.
O que muda do tutorial do blog
- Dois novos plugins WP: WooCommerce e WPGraphQL para WooCommerce.
- Grupo de rota dividido. Apagar
app/layout.tsx. Mover rotas no aplicativo paraapp/(main)/com o seu próprio layout raiz. Mover rotas renderizadas por WP (carte, saída, blog) paraapp/(wordpress-pages)/com um layout de raiz separado que coloca<WPHead />dentro<head>. Importar maps e módulos de script emitidos pelo bloco de saída WC só resolvem corretamente quando a tag de importação vive no documento<head>, e um layout aninhado não pode colocá-lo lá se houver um layout de raiz externa possuir<html>. - Middleware liga a sessão. Ler o
sessionTokencookie em cada pedido WP REST / AJAX, anexá-lo como oCart-TokenCabeçalho. Capturar qualquer rotaçãoCart-Tokenna resposta e persisti-la de volta para o cookie para que a próxima solicitação permaneça na mesma sessão WC. - O GraphQL do lado do servidor obtém o mesmo cabeçalho. Ambos
fetchPageByUriEfetchAssetsByUrideve incluirCart-TokenSaltando-ofetchAssetsByUrié o único mais afiado gotcha — o WC checkout bloco servidor-renders contra um carrinho vazio e assa um"Cannot create order from empty cart"erro emwcSettings.checkoutData, e a página está permanentemente presa. - Uma página polida do produto com um seletor de variação para produtos variáveis (emparelhado por atributo
name, nãolabel— há uma subtil gotcha para atributos locais). - Procura de pedidos por um servidor.
/view-ordertoma um e-mail de faturamento + chave de ordem e faz o pedido. A acção do servidor liga o e- mail ao actualCart-Tokensessão viaupdateCustomer, em seguida, reduz a conexão de ordens por chave de ordem. Sem senha do aplicativo, sem conta de gerente de loja. - Recepção automática em
/checkout/order-received/[id]. Um componente cliente captura o e-mail de faturamento do bloco de checkout do WC em um cookie. A página de recibo lê o cookie + o?key=na URL e procura a ordem através da mesma ação do servidor — nenhum formulário para preencher. - Revisão dos produtos ligado através de um minúsculo
/api/product/reviewrota que chama WooGraphQLwriteReviewmutação com o Cart-Token enviado.
Passo 1 — Instalar dois plugins WP
- WooCommerce — do directório dos plugins. Execute o assistente de configuração, solte o Stripe Test para pagamentos, adicione alguns produtos de demonstração para que haja algo para consultar. Confirmar
/carte/checkoutuse os modelos de bloco (padrão desde Woo 8.3) em vez dos códigos de acesso legados — é isso que NextPress irá proxy. - WPGraphQL para WooCommerce — adiciona os tipos de produto, carrinho, cliente e ordem a
/graphql. Verifique no Postman (ou em qualquer cliente HTTP que superficie cabeçalhos de resposta — GraphiQL os esconde) disparando oaddToCartmutação contra um realproductIdsemCart-Tokensobre o pedido. WP cria uma nova sessão de convidados e retorna o JWT noCart-Tokencabeçalho de resposta.
mutation BootGuestSession {
addToCart(input: { productId: 13, quantity: 1 }) {
cart {
contents { itemCount }
total
}
}
}A Cart-Token somente header ships no carrinho e sessão mutações — addToCart, updateItemQuantity, removeItemsFromCart, applyCouponE amigos. Consultas simples contra /graphql não produzir um, e GraphiQL não vai mostrar cabeçalhos de resposta de qualquer maneira, é por isso que esta etapa de verificação usa Postman. Uma vez que o middleware vê que o cabeçalho em uma resposta proxied persiste o novo valor no sessionToken cookie e todas as solicitações subsequentes — chamadas REST do bloco de checkout, consultas GraphQL do lado do servidor em seu aplicativo Next.js — estão nele.

Passo 2 — Divisão do grupo de rotas
Esta é a maior mudança estrutural. Apagar app/layout.tsx. Mova cada rota sob um dos dois grupos de rotas de nível superior, cada um com seu próprio layout de raiz:
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
Porquê? Next.js só permite <html> e <body> em layouts de raiz. Sem app/layout.tsx, cada grupo de rotas de nível superior torna-se sua própria raiz. A (wordpress-pages) root pode colocar <WPHead /> directamente dentro <head> — que é onde o <script type="importmap"> emitido para scripts de módulo-formulário da WC tem viver. Tente isto com uma disposição aninhada sob uma única raiz e o mapa de importação termina em <body>; os scripts do módulo não conseguem resolver @wordpress/plugins E toda a árvore do bloco de carroças entra.
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 pontes o Cart-Token
Actualizar src/proxy.ts para anexar o token do carrinho a cada solicitação proxied WP REST/AJAX, em seguida, capturar qualquer token girado WP escreve de volta na resposta. Quando a interface do bloco de carrinhos for obtida /wc/store/v1/cart do navegador, ele passa pelo seu proxy NextPress — e esse proxy precisa traduzir seu sessionToken cookie para o Cart-Token cabeçalho WooCommerce espera. WooCommerce irá ocasionalmente girar o token (após uma mutação do carrinho, um cupom aplicar, etc.); o cabeçalho de resposta é como ele sinaliza o novo 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. Encaminhar o Cart-Token em cada chamada REST / AJAX proxied
// O WC do lado do navegador bloqueia a sessão do carrinho do hóspede.
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 o pedido, em seguida, capturar qualquer WP girado Cart-Token escreve
// voltar à resposta e persisti-la como o cookie sessãoToken.
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|.*\\.).*)',
],
};
Passo 4 — Ajudantes do GraphQL que encaminham o símbolo
Dois ajudantes dentro src/lib/wp.ts Fale com o WPGraphQL: gqlWithSession para consultas que necessitam da sessão do carrinho, e fetchAssetsByUri / fetchPageByUri que ambos usam. Ambos devem enviar o Cart-Token – e o segundo é o 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: Encaminhar Cart-Token aqui também. Os activosByUri resolvedor
// gatilhos WP-side script em espera, que executa WC
// hydrate data from api request para o bloco de checkout. Sem a
// header, wc()->cart está vazio para essa solicitação, o hidrato produz
// "Não é possível criar ordem a partir do carrinho vazio", e que o erro envia em
// wcSettings.checkoutData → o bloco de checkout temCheckoutError
// O portão 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;
}Essa observação sobre fetchAssetsByUri não é decoração — é toda a razão /checkout falha silenciosamente se o esqueceres. O WPGraphQL está bem, as chamadas REST do navegador funcionam, o carrinho REST retorna os itens certos — e a página ainda mostra um erro de carrinho vazio porque os dados pré-carregados do lado do servidor do WC foram gerados em uma sessão de carrinho vazia. Encaminhando o item em ambos O GraphQL do lado do servidor consegue fechar a lacuna.
Passo 5 — Carrinho como rota API
Add-to-cart, update-quantity, remove-of-cart — tudo flui através de um /api/cart Traficante. A rota lê- se sessionToken a partir de cookies, executa a mutação WPGraphQL com Cart-Token anexado, e retorna o carrinho atualizado. O middleware do Passo 3 cuida de persistir em qualquer token girado na saída.
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 });
}
// ...atualizar / remover / aplicarCoupon / limpar siga o mesmo padrão
}Notar variation array na entrada. O add-to-cart de produto variável precisa dele para variações que misturam atributos globais e locais — veja Passo 7.

Passo 6 — Uma página polida do produto
Páginas de produto converter navegadores em compradores, então é aqui que você gastar tempo de design real. Componente do servidor, consulta RIS-cached, GraphQL única para toda a visão: título, imagem do herói, galeria (honrando a proporção de aspecto natural de cada imagem de mediaDetails), preço + emblema de venda, emblema de estoque, descrições curtas + longas, produtos relacionados, broadcrumbs, e Product schema.org JSON-LD para Google Shopping. A página envia em __typename para um dos dois componentes herói. Ambos renderizam mobile-first — a maioria do tráfego storefront é móvel, e a captura de tela abaixo é a visão de small-viewport para prová-lo.
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} />
</>
);
}
Etapa 7 — Variações para os produtos variáveis
VariableProductHero é um componente do cliente que possui o estado de seleção de atributos e eleva o preço, imagem e crachá de estoque para a variação correspondente. Dois detalhes pequenos, mas de carga:
- Corresponder por
name, nãolabel. Para atributos globais (taxonomia apoiada:pa_color,pa_size), ambosnameelabelconcordar entre siProductAttributeeVariationAttribute. Para atributos locais (forma livre no produto, sem taxonomia),ProductAttribute.labelé a forma humana (“Logo”) masVariationAttribute.labelésanitize_title()‘d (“logo”). Os doislabelos campos não são comparáveis.namepassasanitize_title()de ambos os lados, por isso sempre concorda —name. - Enviar um
variationarray no add-to-cart. Quando a seleção do cliente corresponde a um realvariationId, enviar ambos — permite WooGraphQL fixar o item do carrinho para essa variação, independentemente se os atributos são globais ou locais.
// VariationAttribute.label is sanitize title()'d for LOCAL atributes
// (por exemplo, "Logo" → "logo") mas ProdutoAtributo. rótulo é a forma humana
// ("Logo"). Ambos expõem `name`, que é sanitize title()'d on ATH
// lados — então coincida com o nome, nunca com o rótulo.
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 — Carrinho e saída através do proxy NextPress
Duas rotas, doze linhas cada. Eles vivem debaixo (wordpress-pages) Então eles pegam o layout WP-rendering do 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>
);
}A (wordpress-pages) o layout obtém o gráfico do ativo (com Cart-Token!), a página renderiza o carrinho WC bloqueio marcação, eo carrinho bloco frontend script assume sobre a hidratação — usando o mesmo Cart-Token O middleware atravessa. Mesmo formato para /checkout/page.tsx; mesma forma para as rotas do blog.

Passo 9 — Pesquisa de pedidos de hóspedes através de uma ação do servidor
Uma vez que um pedido existe, o cliente precisa de uma maneira de vê-lo. O fluxo padrão WooCommerce coloca o recibo em /checkout/order-received/[id], mas também precisa de uma página de pesquisa manual para visitas repetidas, guias de confirmação perdidas e links de acompanhamento de e-mail. /view-order é essa página — um componente servidor com um formulário, postando em uma ação servidor.
A ação faz algo sutil: updateCustomer sem id visa o cliente ligado ao atual Cart-Token sessão. Configuração billing.email vincula essa sessão ao email; ao cliente orders a conexão retorna então cada ordem colocada contra esse endereço — incluindo as ordens de hóspedes, que é o ponto todo. A acção estreita-se por orderKey, que age como um segredo compartilhado: saber um e-mail sozinho não é suficiente para emergir a ordem de outra pessoa.
'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 }
}
}
}
}
`;
// Actualização em execuçãoCliente sem um 'id' aplica a mutação à
// cliente anexado à sessão atual do Cart-Token. Configuração
// billing.email vincula essa sessão ao email; o cliente
// a conexão de pedidos então retorna cada ordem colocada contra isso
// Endereço — incluindo encomendas de hóspedes. Nós estreitamos por ordemKey, que
// age como um segredo compartilhado: saber um email sozinho não é suficiente para
// Surge a ordem de outra pessoa.
//
// Isto é executado apenas através de invocações de ação do servidor, de modo que o GraphQL
// endpoint e a semântica de ligação ao cliente nunca alcançar o
// Cliente. Os leitores tutoriais não precisam de uma senha de aplicação — o
// O próprio Cart-Token autentica a mutação.
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');
}O que isso evita: rolar manualmente uma senha de aplicativo de gerenciamento de loja na demonstração. Tutorial leitores não têm um, e enviar o tutorial com “aqui, colar suas credenciais de administrador em .env” seria irresponsável. O padrão Cart-Token-as-shared-secret mantém tudo dentro do modelo de sessão existente.

Passo 10 — Recepção automática em /checkout/recebido por encomenda
Quando WooCommerce redireciona o cliente após um checkout de sucesso, a URL é /checkout/order-received/[id]?key=wc_order_…. key é a chave da ordem. O e-mail não está na URL — está no formulário de faturamento do WC, que o cliente acabou de preencher. Capturamos esse lado cliente de email em um cookie, em seguida, a página de recibo lê cookie + URL chave e executa o mesmo lookupOrder acção do servidor a partir do Passo 9.
O componente de captura soa simples (leia algumas entradas de e-mail, escreva para um cookie) mas o bloco de saída do WC é exigente: ele monta seus campos em sincronia, monta novamente o conjunto de campos de faturamento quando “Use o endereço de envio para faturamento” alterna, E — a surpresa — hidrata os valores de campo a partir de seu próprio armazenamento de sessão sem disparo input ou change eventosUm delegado input Só o ouvinte vai perder o preenchimento automático. Três fontes cobrem a lacuna:
- Análise inicial do DOM na montagem — valores de capturas já presentes na hora de renderização (autofill, navegador autocompletar).
- MutaçãoObserver ligado
document.bodycomattributeFilter: ['value']— regista entradas montadas tardiamente e escreve o valor restaurado da sessão. - Fase de captura
input/changedelegação ligadodocument— dactilografia ao vivo.
'use client';
import { useEffect } from 'react';
import { CHECKOUT_EMAIL_COOKIE } from '@/utils/constants';
// O bloco de checkout WC monta seus campos assíncrono e re-monta
// o campo de faturamento quando o cliente alterna "Use o envio para
// faturamento". Ele também hidrata os valores de seu próprio armazenamento de sessão
// SEM disparar eventos de entrada/mudança — apenas um ouvinte delegado
// perde o e-mail preenchido automaticamente. Precisamos de três fontes:
// 1. Varredura inicial do DOM na montagem — valores de capturas já presentes.
// 2. MutaçãoObserver — captura entradas montadas tardiamente e
// o valor restaurado da sessão escreve.
// 3. Delegação de entrada/mudança – captura a digitação ao vivo do usuário.
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;
}Monta isso em /checkout/page.tsx ao lado da <Content> bloquear e o cookie é definido pelo tempo em que o cliente clica em Colocar Ordem. A página de recibos lê-lo e renderiza — nenhum formulário, nenhum clique 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>
);
}O link de retorno para /view-order?key=… questões para o “cliente reabriu o e-mail de recibo três dias depois em um novo dispositivo” caso — o cookie se foi, mas a chave de ordem no e-mail ainda funciona para pesquisa manual.
Etapa 11 — Revisão dos produtos
Comentários são um pequeno manipulador POST que chama WooGraphQL writeReview mutação. O formulário do cliente envia productId, nome do autor, e-mail, classificação e conteúdo; a rota anexa Cart-Token, executa a mutação, e a infraestrutura WP lida com fila de moderação, verificação de spam e armazenamento.
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 });
}Liga-o a um <ReviewForm /> componente do cliente dentro <ProductTabs /> na página do produto. A lista de revisão em si é renderizada lado servidor da mesma consulta de produto; paginação é o que você quiser que seja.

Teste de fumaça o fluxo total
Fim a fim: visita /products/hoodie, escolha uma variante de cor + logotipo, clique em Adicionar ao carrinho, /cart com o item mostrando, clique em Continuar para checkout, preencha o cartão de teste Stripe 4242 4242 4242 4242, colocar a ordem, pousar em /checkout/order-received/[id]?key=… com o recibo completo emitido automaticamente. Cinco cliques, único anfitrião Next.js, completa orquestração WooCommerce backend. Sem login, sem conta, sem ciclo de atualização JWT.
Se a página de checkout renderiza “Não é possível criar ordem do carrinho vazio” — que é o fetchAssetsByUri Apanhei-te do Passo 4. Abra o pedido para /graphql que corre assetsByUri no lado SSR e verificar se carrega o Cart-Token Cabeçalho. A interface do bloco de carrinhos irá preencher do REST independentemente, mas o checkout Pré-carregado do bloco wcSettings.checkoutData é gerado servidor-side no tempo de obtenção de ativos e portões de toda a UI.

O que você não construiu
- Gateways de pagamento — Stripe, PayPal, Square, Klarna, ~50 outros
- Motores fiscais — imposto WooCommerce (gratuito), Avalara, TaxJar
- APIs de taxa de envio — USPS, UPS, FedEx, DHL, lógica de taxa fixa personalizada
- Subscrições — ciclos de facturação, ensaios, dunning, actualizações
- Inventário — níveis de existências, notificações de existências reduzidas, pedidos de diferimento, acompanhamento por cada variável
- Motor de cupão — limites de utilização, segmentos de clientes, restrições de produto/categoria
- Gestão de pedidos — reembolsos, reembolsos parciais, notas de encomendas, e-mails do cliente
- moderação de revisão — verificação de spam, filtro de profanação, UI de fila
O WooCommerce trata de tudo. Você lida com a página do produto UX e o cromado storefront. NextPress conecta os dois sem fazer você escolher entre “parece como eu quero” e “funciona com as extensões que eu comprei”.
O atalho: um WooGraphQL Subscrição Pro
Tudo acima é o caminho do código aberto. São algumas centenas de linhas de novo código no topo do tutorial do blog, sem dependências extras, e os navios de demonstração prontos para clonar. Se o tempo da sua equipe é melhor gasto em UX storefront do que em escrever o código de vida de gerenciamento de sessão, é exatamente isso um WooGraphQL Subscrição Pro é para.
Uma assinatura agrupa três coisas:
O plugin WooGraphQL Pro
Um plugin WordPress que estende o esquema GraphQL com tipos, consultas e mutações para as variantes do produto WooCommerce que você realmente vende: Subscrições, Produtos Compósitos, Pacotes de Produtos e Suplementos de Produtos. O esquema gratuito WooGraphQL só expõe produtos simples e variáveis; no momento em que o seu catálogo inclui um pacote ou uma assinatura, você está jogando mão-recursos REST. Com o plugin Pro, esses tipos de produto são de primeira classe no esquema, e um único { product(id: …) } a consulta devolve qualquer forma que esse produto seja.
O CLI criado-woonext-app
A andaime que funciona como create-next-app, mas a placa de caldeira que gera é um próximo trabalho.js + WooGraphQL storefront. npx create-woonext-app my-shop, aponte para sua infraestrutura WP, e você tem páginas de produto, carrinho, checkout e fluxos de conta rodando de ponta a ponta antes de tocar em um arquivo — tudo conectado com os ganchos Pro e componentes abaixo.
Os pacotes @woographql/* JS
- @woographql/next — um kit de ferramentas de geração de componentes que funciona da mesma forma que o shadcn/ui faz: execute um comando, o componente pousa em seu repo, você possui e personalize o código. A biblioteca de componentes abrange toda a superfície frontal da loja —
CartOptionssozinho renderiza o carrinho de ação UI para cada tipo de produto Woo suporta (Simples, Variável, Composite, Bundle, Subscription, Add-On), com estado de formulário e validação já com fio. - @woographql/react-hooks — ganchos digitados que colapsam o código de ciclo de vida que escreveu acima.
useSessionManager()substitui o malabarismo de cookies, a rotação da porta do carrinho e o encaminhamento do cabeçalho SSR-time.useCartMutations()substitui a/api/cartroute plus the client cola, com atualizações otimistas e rollback de erro por chave incluído. - @woographql/session-utils — os blocos de construção de nível inferior por trás desses ganchos: criptografia de token, abstração de cookies/armazenamento, serialização de estado assinado. Os bits que você prefere não reimplementar uma vez que você enviou sua segunda loja Woofront em Next.
Escolha o caminho do código aberto se você quiser entender o que está acontecendo sob o capô — é um ótimo lugar para começar, e o código permanece seu para sempre. Escolha a assinatura se preferir enviar a frente da loja e não se tornar um autor de biblioteca de gerenciamento de sessão. De qualquer forma, a arquitetura neste tutorial é a arquitetura certa; a assinatura apenas lhe dá a versão polida do código do ciclo de vida como uma dependência digitada e testada.
Quando esta é a chamada errada
- Projeto Greenfield sem dependências comerciais e um catálogo de produto único. Stripe Checkout + um personalizado React storefront navios mais rápido. Parar em WP + Woo é exagero até que você precisa de impostos, transporte, ou vários SKUs.
- Citações B2B à mão. O fluxo de trabalho cart-builder para “pedir uma cotação, obter preços, converter para a ordem” não mapeia limpamente para o modelo de catálogo Woo sem trabalho personalizado significativo.
- Mercados multivendores. Woo tem extensões multi-vendor, mas eles são complexos; uma plataforma de mercado-nativo como Medusa é geralmente um melhor ajuste.
Para o resto — a maioria dos aplicativos que precisam de comércio, especialmente os que já servem conteúdo do WordPress — adicionar Woo como backend de comércio e NextPress como a ponte leva você a “vender produto no mesmo anfitrião Next.js como o resto do site” em uma tarde. Subscrições, gateways de pagamento personalizados, multi-moeda, recuperação de carrinho abandonado, níveis de preços B2B — eles são instalações de extensão contra o administrador WP, não refatores contra seu aplicativo.

Leave a Reply