Adicione WooCommerce ao seu aplicativo Next.js em uma tarde com NextPress

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.

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

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 para app/(main)/ com o seu próprio layout raiz. Mover rotas renderizadas por WP (carte, saída, blog) para app/(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 sessionToken cookie em cada pedido WP REST / AJAX, anexá-lo como o Cart-Token Cabeçalho. Capturar qualquer rotação Cart-Token na 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 fetchPageByUri E fetchAssetsByUri deve incluir Cart-TokenSaltando-o fetchAssetsByUri é 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 em wcSettings.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ão label — há uma subtil gotcha para atributos locais).
  • Procura de pedidos por um servidor. /view-order toma um e-mail de faturamento + chave de ordem e faz o pedido. A acção do servidor liga o e- mail ao actual Cart-Token sessão via updateCustomer, 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/review rota que chama WooGraphQL writeReview mutação com o Cart-Token enviado.

Passo 1 — Instalar dois plugins WP

  1. 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 /cart e /checkout use os modelos de bloco (padrão desde Woo 8.3) em vez dos códigos de acesso legados — é isso que NextPress irá proxy.
  2. 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 o addToCart mutação contra um real productId sem Cart-Token sobre o pedido. WP cria uma nova sessão de convidados e retorna o JWT no Cart-Token cabeçalho de resposta.
Postman
mutation BootGuestSession {
  addToCart(input: { productId: 13, quantity: 1 }) {
    cart {
      contents { itemCount }
      total
    }
  }
}

A Cart-Token somente header ships no carrinho e sessão mutaçõesaddToCart, 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.

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

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

src/app/(wordpress-pages)/layout.tsx
import { headers } from 'next/headers';
import { WPHead, WPFooter } from '@axistaylor/nextpress';
import { fetchAssetsByUri, fetchGlobalStyles } from '@/lib/wp';

export const dynamic = 'force-dynamic';

const CRITICAL_STYLESHEETS = [
  'wp-block-library', 'wp-block-library-theme', 'global-styles',
  'classic-theme-styles', 'wc-blocks-style', 'wc-blocks-vendors-style',
];

export default async function WordPressLayout({ children }) {
  const uri = (await headers()).get('x-uri') || '/';
  const [{ scripts, stylesheets, importMap }, globalStyles] = await Promise.all([
    fetchAssetsByUri(uri),
    fetchGlobalStyles(),
  ]);

  return (
    <html lang="en">
      <head>
        <WPHead
          scripts={scripts}
          stylesheets={stylesheets}
          globalStyles={globalStyles}
          importMap={importMap}
          pathname={uri}
          criticalHandles={CRITICAL_STYLESHEETS}
        />
      </head>
      <body>
        <main>{children}</main>
        <WPFooter scripts={scripts} pathname={uri} />
      </body>
    </html>
  );
}

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.

src/proxy.ts
import { NextResponse, NextRequest } from 'next/server';
import {
  proxyByWCR, isProxiedRoute,
  isWCAjaxRequest, isWPAjaxRequest, isWPRestRequest,
} from '@axistaylor/nextpress/proxyByWCR';

export const proxy = async (request: NextRequest) => {
  const pathname = request.nextUrl.pathname;

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

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.

src/lib/wp.ts (excerpt)
import 'server-only';
import { cookies } from 'next/headers';

const endpoint = process.env.GRAPHQL_ENDPOINT as string;

interface FetchOptions { sessionToken?: string | null }

export async function gqlWithSession<T>(
  query: string,
  variables: Record<string, unknown> | undefined,
  options: FetchOptions = {},
) {
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  if (options.sessionToken) headers['Cart-Token'] = `${options.sessionToken}`;

  const res = await fetch(endpoint, {
    method: 'POST',
    headers,
    body: JSON.stringify({ query, variables }),
    cache: 'no-store',
  });
  const json = await res.json();
  return { data: json.data ?? null, errors: json.errors };
}

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

src/app/api/cart/route.ts (excerpt)
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { gqlWithSession } from '@/lib/wp';

const COOKIE_OPTS = { httpOnly: true, secure: true, sameSite: 'lax' as const, path: '/' };

async function readSession() {
  return { sessionToken: (await cookies()).get('sessionToken')?.value ?? null };
}

interface CartActionPayload {
  action: 'add' | 'update' | 'remove' | 'clear' | 'applyCoupon' | 'removeCoupon';
  productId?: number;
  variationId?: number;
  variation?: { attributeName: string; attributeValue: string }[];
  quantity?: number;
  key?: string;
  code?: string;
}

export async function POST(req: NextRequest) {
  const payload = (await req.json()) as CartActionPayload;
  const auth = await readSession();

  if (payload.action === 'add') {
    const result = await gqlWithSession<{ addToCart: { cart: unknown } }>(
      `mutation Add($input: AddToCartInput!) {
         addToCart(input: $input) { cart { contents { itemCount } total } }
       }`,
      { input: {
        productId: payload.productId,
        variationId: payload.variationId,
        variation: payload.variation,  // local-attribute variations need this
        quantity: payload.quantity ?? 1,
      } },
      auth,
    );
    if (result.errors?.length) {
      return NextResponse.json({ error: result.errors[0].message }, { status: 400 });
    }
    return NextResponse.json({ cart: result.data?.addToCart?.cart ?? null });
  }

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

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

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.

src/app/(main)/products/[slug]/page.tsx (shape)
import { notFound } from 'next/navigation';
import { fetchProductBySlug, fetchProductSlugs } from '@/lib/wp';
import { SimpleProductHero } from '@/components/SimpleProductHero';
import { VariableProductHero } from '@/components/VariableProductHero';
import { ProductTabs } from '@/components/ProductTabs';

export const revalidate = 300;

export async function generateStaticParams() {
  return (await fetchProductSlugs()).map((slug) => ({ slug }));
}

export default async function ProductPage({ params }) {
  const { slug } = await params;
  const product = await fetchProductBySlug(slug);
  if (!product) notFound();

  return (
    <>
      <article className="grid lg:grid-cols-[1.1fr_1fr] gap-12 max-w-wide mx-auto px-x-small py-medium">
        {product.__typename === 'VariableProduct'
          ? <VariableProductHero product={product} />
          : <SimpleProductHero product={product} />}
      </article>
      <ProductTabs product={product} />
    </>
  );
}
Polished product page rendered by Next.js with WooGraphQL data and schema.org JSON-LD.

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ão label. Para atributos globais (taxonomia apoiada: pa_color, pa_size), ambos name e label concordar entre si ProductAttribute e VariationAttribute. Para atributos locais (forma livre no produto, sem taxonomia), ProductAttribute.label é a forma humana (“Logo”) mas VariationAttribute.label é sanitize_title()‘d (“logo”). Os dois label os campos não são comparáveis. name passa sanitize_title() de ambos os lados, por isso sempre concorda — name.
  • Enviar um variation array no add-to-cart. Quando a seleção do cliente corresponde a um real variationId, enviar ambos — permite WooGraphQL fixar o item do carrinho para essa variação, independentemente se os atributos são globais ou locais.
src/lib/variation-helpers.ts
// 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));
}
src/components/VariableProductHero.tsx (shape)
'use client';
import { useState } from 'react';
import { findMatchingVariation } from '@/lib/variation-helpers';
import { AddToCartButton } from './AddToCartButton';

export function VariableProductHero({ product }) {
  const [selection, setSelection] = useState({});
  const current = findMatchingVariation(product.variations.nodes,
    Object.entries(selection).map(([name, value]) => ({ name, value })));

  const price = current?.price ?? product.price;
  const image = current?.image ?? product.image;
  const inStock = (current?.stockStatus ?? product.stockStatus) !== 'OUT_OF_STOCK';
  const variationPayload = Object.entries(selection)
    .map(([attributeName, attributeValue]) => ({ attributeName, attributeValue }));

  return (
    <>
      <ProductGallery image={image} gallery={product.galleryImages.nodes} />
      <ProductInfo price={price} inStock={inStock}>
        {product.attributes.nodes.map((attr) => (
          <AttributePicker
            key={attr.name}
            attribute={attr}
            value={selection[attr.name]}
            onChange={(v) => setSelection((s) => ({ ...s, [attr.name]: v }))}
          />
        ))}
        <AddToCartButton
          productId={product.databaseId}
          variationId={current?.databaseId}
          variation={variationPayload}
          inStock={inStock && !!current}
        />
      </ProductInfo>
    </>
  );
}
Variable product page showing matched variation image, price, and stock badge updating live with attribute-picker selection.

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:

src/app/(wordpress-pages)/cart/page.tsx
import { notFound } from 'next/navigation';
import { Content, nextImageParser } from '@axistaylor/nextpress';
import { fetchPageByUri } from '@/lib/wp';

export default async function CartPage() {
  const page = await fetchPageByUri('/cart');
  if (!page) notFound();
  return (
    <article>
      <Content
        content={page.content}
        contentCssClasses={page.contentCssClasses}
        parsers={[nextImageParser()]}
      />
    </article>
  );
}

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.

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

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.

src/actions/order.ts
'use server';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';
import { gqlWithSession, type Order, ORDER_FRAGMENT } from '@/lib/wp';
import { LOOKUP_COOKIE, ERROR_COOKIE } from '@/utils/constants';

const FIND_ORDER_MUTATION = /* GraphQL */ `
  ${ORDER_FRAGMENT}
  mutation BindEmailAndFindOrder($input: UpdateCustomerInput!) {
    updateCustomer(input: $input) {
      customer {
        orders(first: 100) {
          nodes { ...OrderFields }
        }
      }
    }
  }
`;

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

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

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:

  1. Análise inicial do DOM na montagem — valores de capturas já presentes na hora de renderização (autofill, navegador autocompletar).
  2. MutaçãoObserver ligado document.body com attributeFilter: ['value'] — regista entradas montadas tardiamente e escreve o valor restaurado da sessão.
  3. Fase de captura input/change delegação ligado document — dactilografia ao vivo.
src/components/CheckoutEmailCapture.tsx
'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.

src/app/(wordpress-pages)/checkout/order-received/[id]/page.tsx
import Link from 'next/link';
import { cookies } from 'next/headers';
import { OrderSummary } from '@/components/OrderSummary';
import { lookupOrder } from '@/actions/order';
import { CHECKOUT_EMAIL_COOKIE } from '@/utils/constants';

interface Props {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ key?: string }>;
}

export default async function OrderReceivedPage({ params, searchParams }: Props) {
  const { id } = await params;
  const { key } = await searchParams;
  const email = (await cookies()).get(CHECKOUT_EMAIL_COOKIE)?.value ?? '';
  const fallback = key ? `/view-order?key=${encodeURIComponent(key)}` : '/view-order';

  if (!key || !email) {
    return (
      <main>
        <h1>Thanks for your order</h1>
        <p>We couldn't auto-load the receipt because your billing email isn't
          available on this device. Look up the order with the email you used at
          checkout — the order key from your confirmation email is the shared
          secret.</p>
        <Link href={fallback}>Look up the order</Link>
      </main>
    );
  }

  const { order, error } = await lookupOrder(email, key);
  if (!order) {
    return (
      <main>
        <h1>Thanks for your order</h1>
        <p className="error">{error ?? "We couldn't find that order."}</p>
        <Link href={fallback}>Look up the order</Link>
      </main>
    );
  }

  return (
    <main>
      <OrderSummary
        order={order}
        headline="Thanks for your order"
        intro="Your order has been received. A copy of this receipt is on its way to your inbox."
      />
    </main>
  );
}

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.

src/app/api/product/review/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { gqlWithSession } from '@/lib/wp';

export async function POST(req: NextRequest) {
  const { productId, author, email, content, rating } = await req.json();
  const sessionToken = (await cookies()).get('sessionToken')?.value ?? null;

  const result = await gqlWithSession<{ writeReview: { rating: number } }>(
    `mutation Write($input: WriteReviewInput!) {
       writeReview(input: $input) {
         rating
         review { id }
       }
     }`,
    { input: {
      commentOn: productId,
      author, authorEmail: email,
      content, rating,
    } },
    { sessionToken },
  );

  if (result.errors?.length) {
    return NextResponse.json({ error: result.errors[0].message }, { status: 400 });
  }
  return NextResponse.json({ ok: true, rating: result.data?.writeReview?.rating });
}

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.

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

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 — CartOptions sozinho 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/cart route 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

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