Headless

Headless Next.js Storefront with WordPress Content and Shopify Commerce, plus a Shopify App

One Next.js front end that reads content from WordPress and products from Shopify, with a unified cart, a custom embedded Shopify app, and webhook-driven revalidation.

Client
DTC brand with a WordPress marketing site and a Shopify store
My role
Full-stack developer (Next.js, WordPress and Shopify app)
  • Next.js App Router
  • WordPress REST
  • WPGraphQL
  • ACF
  • Shopify Storefront API
  • Shopify Admin app
  • ISR
  • TypeScript
front end for WordPress content and Shopify commerce
1

front end for WordPress content and Shopify commerce

mandatory GDPR webhooks implemented
3

mandatory GDPR webhooks implemented

product and order webhooks registered
3

product and order webhooks registered

PCI scope kept by using Shopify's hosted checkout
SAQ A

PCI scope kept by using Shopify's hosted checkout

The challenge

A DTC brand wanted a marketing site on WordPress and a storefront on Shopify, with a single front end that felt like one brand. Their previous setup was two disconnected sites with duplicate navigation and a checkout that dropped people onto a different-looking domain.

What I built

  1. 01

    Next.js App Router front end

    Content comes from WordPress (REST and WPGraphQL) and commerce from Shopify (Storefront API, GraphQL). Marketing pages use ISR from WordPress, while product and collection pages are server-rendered from Shopify with revalidation on webhook.

  2. 02

    Unified cart

    The Shopify cart ID is persisted in an HTTP-only cookie and cart mutations go through the Storefront API. Checkout is handed off to Shopify's hosted checkout, which keeps PCI scope at SAQ A.

  3. 03

    Embedded Shopify app (Smartlink Shopify app)

    An embedded admin app syncs product data to an external service. It registers webhooks for products/create, products/update and orders/paid, and uses session tokens for authenticated admin requests.

  4. 04

    App install and compliance

    The app has an OAuth install flow, HMAC verification on every webhook, and the mandatory GDPR webhook set: customers/data_request, customers/redact and shop/redact.

  5. 05

    WordPress endpoints for the front end

    Custom fields are exposed with register_rest_field, a WPGraphQL schema extension covers ACF fields, and a preview endpoint lets editors see drafts in the headless front end through Next.js draft mode.

  6. 06

    Webhook-driven revalidation

    Shopify product and collection updates, and WordPress publish events, call a Next.js revalidation route with a shared secret. The front end updates without a full rebuild.

Gotchas and how I fixed them

  • Read the raw body before verifying the HMAC

    Shopify signs the exact request body. Call req.text() before any JSON parsing, compute the HMAC from that raw string, and compare with timingSafeEqual.

  • Multi-currency needs @inContext

    Without the @inContext directive on Storefront API queries, every market sees base-currency prices.

  • Expose only what the front end renders

    Returning just the needed ACF fields through register_rest_field keeps payloads small and avoids leaking admin-only field groups.

Code from the build

Shopify webhook verification in Next.js (raw body first)ts
import crypto from 'crypto';

export async function POST(req: Request) {
  const raw = await req.text(); // MUST be before JSON.parse
  const hmac = req.headers.get('x-shopify-hmac-sha256') ?? '';

  const digest = crypto
    .createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET!)
    .update(raw, 'utf8')
    .digest('base64');

  if (
    digest.length !== hmac.length ||
    !crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(hmac))
  ) {
    return new Response('Unauthorized', { status: 401 });
  }

  const topic = req.headers.get('x-shopify-topic');
  const payload = JSON.parse(raw);

  // Respond fast; do the work in the background.
  if (topic === 'products/update') {
    await revalidateProduct(payload.handle);
    await revalidateCollectionsForProduct(payload.id);
  }

  return new Response('ok', { status: 200 });
}

Awaiting req.text() before any parsing is the whole trick, and timingSafeEqual is what makes this a real signature check.

Cost-aware Storefront API queryts
const PRODUCT_QUERY = `
  query Product($handle: String!, $country: CountryCode, $language: LanguageCode)
  @inContext(country: $country, language: $language) {
    product(handle: $handle) {
      id
      title
      descriptionHtml
      featuredImage { url altText width height }
      priceRange { minVariantPrice { amount currencyCode } }
      variants(first: 50) {
        nodes { id title availableForSale price { amount currencyCode } selectedOptions { name value } }
      }
    }
  }
`;

@inContext is not optional for multi-currency, because without it every market sees base-currency prices.

WordPress REST field for headless consumptionphp
add_action( 'rest_api_init', function () {
    register_rest_field( 'post', 'acf_fields', [
        'get_callback' => function ( $post_arr ) {
            return [
                'hero'    => get_field( 'hero', $post_arr['id'] ),
                'excerpt' => get_field( 'custom_excerpt', $post_arr['id'] ),
            ];
        },
        'schema' => [
            'description' => 'ACF fields for the headless front end.',
            'type'        => 'object',
        ],
    ] );
} );

Exposing only the fields the front end renders keeps the payload small and avoids leaking admin-only field groups.

Results

  • One Next.js front end replaced two disconnected sites, so navigation and branding are shared instead of duplicated.
  • Checkout stays on Shopify's hosted checkout, keeping PCI scope at SAQ A.
  • The Shopify app registers 3 webhooks (products/create, products/update, orders/paid) and implements all 3 mandatory GDPR webhooks, with HMAC verification on every request.
  • Product, collection and WordPress content updates reach the site through webhook-triggered revalidation, with no full rebuild.

Questions people ask

Can I use WordPress for content and Shopify for products on one Next.js site?

Yes. The Next.js App Router front end pulls content from WordPress through REST and WPGraphQL and commerce from the Shopify Storefront API. Marketing pages use ISR from WordPress, while product and collection pages are server-rendered from Shopify.

How does checkout work in a headless Shopify setup?

The Shopify cart ID is kept in an HTTP-only cookie, cart changes go through the Storefront API, and checkout is handed off to Shopify's hosted checkout. That keeps PCI scope at SAQ A.

How do I verify Shopify webhooks in Next.js?

Read the raw body with await req.text() before any JSON parsing, compute an HMAC SHA-256 with your webhook secret, base64-encode it, and compare it to the x-shopify-hmac-sha256 header with crypto.timingSafeEqual. Return 401 on a mismatch.

How do editors preview WordPress drafts on a headless front end?

A preview endpoint on the WordPress side works with Next.js draft mode, so editors can see unpublished content on the real front end.

What does a custom Shopify app need to pass review basics?

This app uses an OAuth install flow, session tokens for admin requests, HMAC verification on every webhook, and the mandatory GDPR webhooks: customers/data_request, customers/redact and shop/redact.