Headless WooCommerce is not a new shop. wp-admin, stock, orders and gateways stay. The HTML the customer downloads is no longer a PHP theme. Astro 7 prints the catalog. Checkout, if you are sane, stays on WooCommerce.
A classic WooCommerce request runs PHP, the object cache, the plugin list, and the theme, for a product page that has not changed since Tuesday. HTML cache in front of origin fixes a lot of that. Headless is the next cut: the catalog is a static tree, the cart is a small island, the money still hits the origin you already PCI-scoped.
This article is the split. Cache tiers, TTFB and field LCP live in Headless WooCommerce with Astro, e-commerce performance. The WooCommerce developer and Astro developer pillars are the commercial surfaces. Architecture without the cart is in the headless architecture guide.
Portent measured conversion falling about 4.42% per extra second of load. That is a slope, not a promise that Astro prints money. We go headless when the theme and the plugin list are the ceiling, not when a PageSpeed screenshot is the brief.
What actually stays in WooCommerce
The back office does not move. Products, variations, stock, coupons, orders, refunds, reports. Stripe, Klarna, PayPal, local gateways. Table-rate shipping. Tax plugins. ERP connectors that write to orders. Those are PHP, and they should stay PHP.
What leaves is the storefront theme: header.php, the product template, the mini-cart widget, Elementor on the homepage, the zoom plugin, the “complete the look” carousel that enqueues 200 KB to show three SKUs.
Editors keep the same wp-admin. If they cannot, you have not gone headless. You have built a second PIM and you will hate it by month three.
xmlrpc and the public REST users route still need the same hardening as any WordPress origin. Headless does not hide wp-admin. Put Access in front of it. The catalog CDN is not a firewall.
The plugin that dies when the theme dies
This is the inventory nobody wants to do, and it is the whole risk.
Dies with the theme (rebuild as an island, or drop):
- Variation swatches that replace the dropdown with colour dots
- Quick view / ajax add-to-cart in the loop
- Product image zoom and 360 viewers
- Upsell popups, fake-scarcity bars, “people also bought” widgets that print in
woocommerce_after_shop_loop - Page builders on the homepage and landing SKUs
- One-page checkout plugins that take over
checkouttemplates
Stays (API or webhook, no theme):
- Payment gateways
- Shipping and pickup (InPost, local pickup, table rates)
- Tax (including OSS)
- Subscriptions, memberships, as long as status is queried, not painted by the plugin’s CSS
- Mailchimp / Klaviyo via order hooks
- ERP (see the WooCommerce ERP integration pillar)
If the store’s conversion tricks live in the first list, headless is a redesign, not a speed project. Price that as a redesign. The WooCommerce developer scope is honest about that split. We have watched a shop “go headless” and then reinstall the old theme for checkout because the one-page plugin was the actual product. That is two storefronts. Do not do that.
A useful test: disable the theme on staging and load a product URL as ?theme=noop or a blank theme. What is still there (price, stock, add to cart via Store API) is what you keep. What vanished (swatches, badges, delivery estimator HTML) is work. If the vanished list is the reason customers buy, budget islands. If it is decoration, drop it.
Composite products, bundles, and product add-ons are the grey zone. Some expose REST, some only filter woocommerce_add_to_cart. If add-to-cart of a bundle 404s on Store API, that SKU is not catalog-static until someone writes the mapping. Do not discover this on launch morning.
Astro 7 for the catalog, not for the shop
Astro 7 default is HTML and CSS. JavaScript only where you mark an island.
Catalog that should be static:
- Home, category, product, content, policy pages
getStaticPathsfrom the product list at build- AVIF from the media library,
sizesthat match the grid, not100vwon a 320 px thumb
Islands:
- Add to cart, quantity, variation picker
- Mini-cart
- Search that hits an index, not PHP
?s= - Maybe Stripe Elements on a custom payment step, if you really cannot redirect
client:visible on add-to-cart. client:idle on search. A product page that imports a full icon pack into the island has already lost the point. Same rule as any other Astro 7 front: the island is a component, not an excuse to hydrate the page.
Rebuilds: a product updated webhook rebuilds that path, or a small set of paths (product, its categories, home if it merchandises). Full-catalog rebuilds every ten minutes are how people discover they have 40,000 SKUs. Incremental or on-demand. Cloudflare or the host you actually use. Not “Vercel because the tutorial said so” if the rest of the stack is already on Cloudflare.
Search: PHP ?s= is a PHP hit. On a static catalog, search is either a client index (tiny catalogs), an external index (Algolia, Meilisearch, Typesense) filled from the same payload as SSG, or a Store API / REST query through an island. Do not send the search box to WordPress if the rest of the shop never does. Facets that need live stock belong next to the buy box, not in the SSG HTML.
Related products: if they are merchandised in wp-admin, they are data. Query them at build. If they are “woocommerce_output_related_products” from the theme, they were never data, they were a template. Rebuild as a static strip from the same category, or drop.
Store API versus GraphQL for cart and catalog
Two pipes. Mixing them is the usual self-inflicted wound.
Catalog build: WPGraphQL + WooGraphQL, or the REST products endpoint, at build time. One query per product page. You want name, prices, stock status, images, categories, attributes. You do not want the cart.
Cart and session: WooCommerce Store API (/wp-json/wc/store/v1/cart). It owns the cart cookie, nonces, coupons, shipping calculation. That cookie has to be on the origin that will later take checkout. If the Astro site is shop.example.com and Woo is cms.example.com, you have a cookie problem before you have a React problem. Same site, or a documented proxy.
We see teams add-to-cart through WooGraphQL mutations because the catalog already speaks GraphQL. Then the checkout page on WooCommerce shows an empty basket. The session never moved. Store API for the cart. GraphQL for the SSG payload. Write that on the wall.
Same-site cookies: if Astro is on the apex and WordPress is on wp., set the cart cookie domain explicitly and proxy /wp-json/wc/store/ to origin. CORS on Store API is not a substitute for the cookie. SameSite=Lax on a cross-site checkout redirect will look like “Klarna works on staging” (same host) and “empty cart in production” (two hosts). One hostname for the shopper, internal hostname for wp-admin.
Nonce expiry on a static page is a real bug. The cart island must fetch a fresh nonce from Store API on hydrate, not bake one into SSG HTML from this morning’s build. Baked nonces 403 after a few hours and the add-to-cart button “does nothing.”
Stock at build is a snapshot. A sold-out size still in HTML is a refund. Webhook woocommerce_product_set_stock (or the variation equivalent) must invalidate that path. If you cannot do that, do not SSG the buy box. Render the buy box from Store API at request time, still as a small island. Catalog HTML can stay static.
Keep checkout on WooCommerce, not in an Astro island
This is the operating choice we keep repeating, and it is the one that splits a demo from a shop that still takes Klarna next month.
Redirect to /checkout/. The catalog and the cart island live on Astro. “Checkout” is a link to the WooCommerce checkout template, bypassed at the HTML cache, with the Store API cookie already set. Every gateway plugin, every tax plugin, every “order received” email keeps working. PCI scope stays where it was. Trusted Shops, invoice PDFs, ERP on woocommerce_thankyou, all still fire.
Custom checkout in Astro means you reimplement:
- Address validation and country fields
- Shipping methods that change with postcode
- Tax
- Gateway JS (Stripe, Klarna, PayPal, local methods)
- 3-D Secure returns
- Failed payment retries
- Order-pay links from emails
We have done it for stores that needed a branded step no PHP template could hold. It is a product. It is not the default. The default is hybrid: static catalog, island cart, PHP checkout.
A UK or US shop we rebuilt kept PayPal and Klarna on Woo checkout and still cut PHP off the product URL. The conversion argument was “the product page is the bounce.” It was. The checkout was not. Do not “finish the headless” by rewriting the one template that already works.
VAT and OSS still calculate on origin. A static price in EUR on the product page is a merchandising price. The checkout line is the legal one. Do not SSG per-country prices unless you also SSG per-country paths and you can invalidate them. Most stores show a catalog price and let Woo apply tax at checkout. That is allowed. Showing a stale UK VAT-inclusive figure to an EU guest is how finance emails you.
Order-received and account pages can stay on WooCommerce too. “Headless” does not mean “every URL is Astro.” It means the URLs that get the advertising traffic are files. /my-account/ behind Access or behind login is fine as PHP.
Webhook URLs (/?wc-api=, Stripe, Klarna) must hit origin, not the Astro CDN. Cache skip those paths. The same skip list discipline as a WAF: each skip has a name.
Stock, cache and the lie of “unlimited CDN users”
Static files on a CDN do scale. Stock does not. Two customers buying the last SKU is a store problem, not a PageSpeed problem.
Pattern that holds:
- Catalog HTML is static, including price, until a webhook says otherwise.
- Buy box asks Store API for
stock_statuswhen the island hydrates, or you accept a short stale window and compensate in checkout (WooCommerce already refuses oversell if you configured it). - Order creation is origin. ERP and mail fire there.
“Unlimited concurrent users” is true for the HTML. It is false for checkout, for stock, for the database. Size PHP for the checkout peak you actually have (a drop, a newsletter, Black Friday). The CDN does not take the card.
HTML cache in front of a classic theme is still the first lever. If TTFB is already low and LCP is the hero image, go read the performance sibling and the Core Web Vitals guide. Headless will not unbreak a 3 MB PNG.
When not to go headless
- Catalog under a few dozen SKUs, no traffic problem, customizer edits every week
- The storefront is the plugin list (builders, one-page checkout, live chat baked into the theme)
- Nobody on the team will own the Astro repo after launch
- The brief is “PageSpeed 100” and the product page already has HTML cache and AVIF
Then: plugin diet, HTML cache, image pipeline, consent slot. Cheaper, reversible. Headless is a split of systems. You now have two deploys, two previews, two failure modes. Worth it when the theme is the bottleneck and the catalog is mostly read.
Subscriptions and memberships: the entitlement lives in Woo. The Astro page can show “you have access” if it can ask. If the membership plugin only paints content by wrapping the_content in PHP, that content is not in your SSG. Either expose it or keep those URLs on WordPress.
Migration sequence that does not dual-run forever
- Inventory. Plugin list split into stays / dies / unknown. Unknown becomes a spike, not a surprise at launch.
- API on the live shop. Store API is already there on modern WooCommerce. GraphQL if the catalog build wants it. Customers see nothing.
- Astro catalog on a staging host, real product data, no checkout yet. Compare permalinks. You will find trailing-slash and category-base fights here, not in week eight.
- Cart island + cookie against staging origin. Add to cart, coupon, empty cart. Then, and only then, link checkout.
- Checkout still on Woo. Bypass cache. Gateways in a real sandbox. Order emails. Refund path.
- SEO. 301 map, canonicals, product JSON-LD from the same fields as the page, sitemap of Astro URLs. Inspect a product URL before cutover. Do not invent lastmod on 10,000 SKUs.
- Cut catalog DNS. Keep
wp-adminand checkout on origin. Watch stock webhooks for a week.
Blue-green of the catalog is easy (it is files). Blue-green of checkout is the same as today: you already have one. Do not invent a second.
Cutover week is mostly DNS and 301s, not React. Keep a short window where the old theme still answers a header bypass so you can compare a SKU. The moment both storefronts take add-to-cart against the same Store API, you will double-add from old tabs. Drain the old theme: 301, then 410 on the theme assets, then delete. Dual-run “just in case” is how the customizer comes back.
Audit and further reading
Field LCP, cache tiers and Store API payload weight: performance guide. The split of systems: this page. The WooCommerce developer pillar is the commercial catalog work. Astro developer is the front. ERP integration is what must not break when the theme dies. Headless architecture is the same split without a cart.
A confidential implementation (Cloudflare, Store API, checkout left on Woo) is in the headless WooCommerce case study. Numbers that are not in that write-up are not numbers we have.
Conclusion
WooCommerce as the shop. Astro 7 as the catalog. Cart as an island on Store API. Checkout on PHP. Plugins that painted the theme get rebuilt or dropped. Plugins that ran the business stay. GraphQL builds pages. GraphQL does not hold the basket.
Write with the plugin list and whether checkout may stay on WooCommerce. If the answer is “it must look identical to the theme’s one-page checkout,” you are buying a redesign. The WooCommerce developer surface is where that scope starts.
Preview: editors need to see a draft product on the Astro host. A webhook or a preview token that fetches one product from origin is enough. If preview means “log into wp-admin and use the theme customizer,” you have not finished the split, and they will keep asking for the old theme.
Images stay in the WordPress media library. Astro reads URLs, or a build step copies AVIF. Do not invent a second DAM unless you already have one. The LCP image is still the hero of the product page. Headless does not pick the format for you. A WebP from a page builder is still a bad LCP, even on a CDN.






