The catalogue is the easy half. Stores go wrong at the cart.
Store owners ask for this after reading that headless commerce is faster, which is true, and after being told the migration is much like moving a blog, which is not. A blog has one write path and it belongs to you. A shop has several, they belong to the customer, and each one moves money.
I have scoped enough of these to be blunt about it. Most stores that ask for a headless rebuild should buy something smaller instead, and the smaller thing gets them nearly all of the speed. This piece is about how to tell which one you are.
A store is two halves
Draw a line through your shop. On one side is everything that only reads: the homepage, category listings, product pages, search, reviews, the blog. On the other is everything that writes: adding to cart, applying a coupon, calculating shipping, paying, and looking at an order afterwards.
The read half is where your search traffic lands, and it is the half that migrates cleanly. It is also, on most stores, the half that is slow, because a product page in WooCommerce runs the whole cart bootstrap on every request even for a visitor who has never added anything.
The write half is short. Four or five screens. And it is where the entire budget goes, because those screens carry your tax rules, your shipping zones, your coupon logic, your stock reservation and your payment integration, and every one of them has to be right on the first day rather than improved later.
The catalogue moves cleanly
Products, categories, attributes, images and reviews all come out over an API and render as static pages. Two routes in.
- The REST API ships with WooCommerce and authenticates with a consumer key and secret. Server side only, always. Straightforward, but assembling a product page can take several calls.
- WooGraphQL, which extends WPGraphQL, gets the same page in one round trip with exactly the fields the template uses. On a catalogue of any size this is the one I reach for.
// Product pages are pre-rendered and refreshed on a window, so a hundred// thousand views cost one render rather than a hundred thousand PHP requests.export const revalidate = 900; export async function generateStaticParams() { const products = await getAllProductSlugs(); return products.map((slug) => ({ slug }));} export default async function Page({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const product = await getProduct(slug); if (!product) notFound(); return ( <ProductView product={product} // Price and description are safe to pre-render. Availability is not, // so it is read live and the button waits for it rather than lying. availability={<LiveStock sku={product.sku} />} /> );}Stock is the one catch in an otherwise easy half. A pre-rendered page is a page that was true fifteen minutes ago, and an in stock badge that is fifteen minutes stale is a promise you might have to break at checkout. Either read availability at request time for the badge, or shorten the window on the products that actually run out, or accept the staleness deliberately and make sure the cart re-checks. What you cannot do is pre-render the badge and hope.
Everything else here is ordinary work. Structured data for products, including price and availability, has to be emitted by hand rather than by a plugin, and it matters more on a shop than anywhere else because it drives the rich results in search.
The cart is the project
WooCommerce keeps the cart as session state on the server, tied to a cookie, and it was not designed to be addressed from a different origin. Everything difficult about headless WooCommerce comes out of that one sentence.
There is a supported answer. The Store API, which ships as part of WooCommerce Blocks, exposes public cart endpoints built for exactly this: add an item, update a quantity, apply a coupon, read totals. It hands you a cart token that you carry on subsequent requests. The part that matters is that the totals come back computed by WooCommerce rather than by you.
// The token identifies this shopper's cart to WooCommerce. It comes back on// the first response and every later call has to carry it, or you have// silently started a second, empty cart.export async function addToCart(token: string | null, id: number, quantity: number) { const res = await fetch(`${process.env.STORE_URL}/wp-json/wc/store/v1/cart/add-item`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { 'Cart-Token': token } : {}), }, body: JSON.stringify({ id, quantity }), cache: 'no-store', }); if (!res.ok) throw new Error(await res.text()); return { // Totals are whatever WooCommerce says they are. Tax rules, shipping // zones and coupon logic all live over there, and that is the point. cart: await res.json(), token: res.headers.get('Cart-Token') ?? token, };}The alternative you will see recommended is to hold the cart in React state or local storage and create the order at the end. It demos beautifully. It also means your storefront is now the thing that decides what a customer owes, and WooCommerce is holding the tax table, the shipping zones, the coupon rules and the stock. Those two facts do not coexist quietly for long.
| What the cart has to get right | Where the logic actually lives |
|---|---|
| Line totals and quantities | Trivial either way |
| Tax, by customer address and product class | WooCommerce, against rules you configured |
| Shipping rates and available methods | WooCommerce, per zone and per method |
| Coupons, including usage limits and exclusions | WooCommerce, and the rules are fiddlier than they look |
| Stock at the moment of purchase, not of browsing | WooCommerce, which also reserves it |
| Anything an extension adds to totals | The extension, running inside WooCommerce |
The checkout decision
This is the decision that sets the size of the project, and most stores get it wrong by reflex.
Option one: hand over at the buy button
Next.js serves the homepage, categories, products, search and content. When the shopper buys, they go to the WooCommerce cart and checkout, which you leave exactly as it is.
You get pre-rendered pages on everything that gets indexed and everything that gets browsed, and you take on none of the liability around payment, tax or fraud. You keep every extension that works at checkout. The cost is a visible handover, usually a different path and a different look, and some care over session continuity.
For most stores I look at, this is the right answer, and it costs a fraction of the alternative.
Option two: own the whole flow
Cart, checkout, payment and account pages all in Next.js, driven through the Store API. One consistent interface, full control over the buying experience, and a much larger project with a real test plan behind it.
Worth it when at least two of these are true:
- Checkout conversion is a number somebody is actually accountable for, and you intend to keep testing it.
- The buying flow is unusual enough that the standard checkout genuinely gets in the way.
- You have a maintainer, because a bespoke checkout is not a thing you ship and forget.
- Your payment gateway offers hosted fields or a redirect, so you can build the flow without touching card data.
What this actually costs
Everything in my usual migration breakdown still applies: URL audit, redirect map, templates, metadata, integrations, testing and handover. A store adds three things on top, and they are not small.
- 01The write path. Cart, checkout, account and orders, plus the failure cases. A declined payment, a coupon that expired between add and pay, an item that went out of stock in the last two minutes.
- 02The test plan. Tax across the regions you sell to, every shipping method, every coupon type, and payment in the gateway's test mode and then again for real. This is not optional and it takes longer than the build.
- 03The extension audit. Every WooCommerce extension you run, checked for where it does its work. Front end extensions stop applying. Server side ones survive only if the order still goes through WooCommerce.
Any quote that prices a store like a content site with products bolted on has not been thought about. My line by line breakdown of a normal migration is a reasonable starting point, and then you add the three items above.
Related readingWhat a WordPress to Next.js migration costs, line by lineWhen to leave the store alone
Any one of these on its own is enough to stop.
- The storefront already passes Core Web Vitals. Check the field data rather than a lab score. If it passes, the speed argument is gone and you are proposing a rebuild for its own sake.
- Subscriptions, bookings or memberships carry the revenue. These are the deepest hooks into checkout that WooCommerce has. Rewriting them to make a product page load faster is a bad trade at any price.
- Nobody will own the codebase. A headless store is two systems and a deploy pipeline. Without someone who can run it, an outage means waiting for whoever built it to answer.
- Search is not where your customers come from. If the traffic is paid social straight to a product page, buy speed with better hosting and fewer plugins rather than with an architecture change.
- The catalogue changes constantly. Pricing that moves hourly and stock that runs out mid-session fight against pre-rendering, and every workaround gives back some of the speed you came for.
The honest summary is that headless WooCommerce is a good answer to a narrow question. It suits a store with a large catalogue, real search traffic, a team that can maintain code, and a specific reason the standard front end is holding it back. If that is not you, the catalogue-only build gets you most of the win for a small share of the cost.
If you do it, do it in this order
The sequencing matters more here than on a content site, because you can stop after any phase and still have something better than what you started with.
That pause is the whole point. Launch the read half, watch the field data for a fortnight, and then decide whether the write half is still worth commissioning. A good number of stores that reach that checkpoint look at the numbers and decide they are finished.
Related readingWordPress or Next.js: how to tell which one your site should be onQuestions people ask me
Can I call the WooCommerce REST API from the browser?
No. The consumer key and secret it authenticates with are server credentials, and anything you put in browser JavaScript is public. Product reads happen on the server during rendering, and anything the shopper triggers goes through your own route handler, which holds the keys. The Store API is the exception, because it was designed to be called publicly and is scoped to cart operations only.
Will a headless storefront rank better than WooCommerce?
It can, but not because it is headless. The pages that earn search traffic in a store are category and product pages, and those are exactly the pages that pre-render cleanly, so they get faster. The cart and checkout are not indexed and their speed is a conversion question, not a ranking one.
Do my WooCommerce extensions still work?
It depends where they do their work. An extension that computes something server side, like a shipping rate or a tax rule, keeps working as long as you go through WooCommerce for the cart and the order. An extension that renders on the front end, which is most of the ones affecting product pages, stops applying the moment you stop using the WooCommerce theme.
Can I have a fast storefront without rebuilding checkout?
Yes, and it is what I recommend to most stores. Serve the catalogue, the category pages and the content from Next.js, and hand the shopper over to the WooCommerce cart and checkout at the moment they buy. You get the speed on the pages that carry the traffic and you take on none of the payment and tax liability.
What about WooCommerce Subscriptions?
Leave it alone. Subscriptions hooks deep into checkout, renewal orders and payment method changes, and reimplementing that surface headlessly means owning the failure cases around recurring billing. If subscriptions are your revenue, that is the last thing you should be rewriting to make a page load faster.
How long does a headless WooCommerce build take?
The catalogue is the fast half and usually lands in weeks. A full build with your own cart and checkout is a different order of project and should be scoped in months, with a real test plan for tax, shipping and payment. If a quote treats it like a content migration with products added on, that quote has not been thought through.
Do you take on this work?
Yes, scoped case by case rather than at a standard price, because two stores with the same product count can be completely different projects. A fair share of the time my recommendation is a fast Next.js storefront in front of an untouched WooCommerce checkout, which costs a fraction of a full rebuild.
If you run a store and want a straight read on which of the two builds fits it, send me the address along with your product count, your extension list and where your traffic comes from. You will get a scoped answer rather than a package. The migration service page covers how I work, the free site check will tell you what your product pages are carrying today, and the contact form is the fastest way to start.
Resources
- WooCommerce Store API reference
The public cart and checkout endpoints, including the cart token handling shown above.
- WooCommerce REST API documentation
Products, orders and customers. Server side only, because it authenticates with a key and secret.
- WooGraphQL
WooCommerce data through WPGraphQL, which is how a product page becomes one round trip instead of four.
- Google: product structured data
What your product pages have to emit once the plugin that was emitting it is gone.
- PCI Security Standards Council: SAQ documents
Worth reading the SAQ A criteria before anyone proposes handling card fields yourself.
- web.dev: Core Web Vitals
Check your field data here before commissioning anything on the basis of speed.