Flx Render (Storefront)
The renderer for WooCommerce stores on the Storefront theme: a Node.js service that receives page data from the Flx Modernize plugin and returns the finished page. It has been tested with WordPress 7.1, WooCommerce 11.2.1 and Storefront 4.6.2. The live demo is storefront.flexplat.com.
To change how pages look, see Writing a Storefront theme in TypeScript. To learn how it was built, see the tutorial.
Architecture
The plugin keeps WordPress and WooCommerce in charge. At template_redirect it builds the page data, sends a signed request, and prints the HTML it gets back. If anything fails, WordPress renders the page itself.
Most of a catalogue page's middle is WooCommerce's own output, sent as HTML: it goes through WooCommerce's templates and every plugin's hooks. The renderer draws Storefront's header and footer around it, and a theme can replace parts of it. A classic theme's page relies on the theme's own CSS and scripts, so what WordPress prints in the head and footer comes along as it is.
WooCommerce's scripts run in the browser against WordPress, as usual: add to cart, the mini-cart, variation forms and the gallery.
What goes through Flx
| Page | With FLX_MODERNIZE_WOOCOMMERCE |
|
|---|---|---|
Shop, product categories and tags (with /page/N/), products |
Flx | No query string, and the site's template files at a reviewed version |
| Home page | Flx | Storefront's homepage template as the static front page |
| Visitors with a cart, on those pages | The same pages as anyone else | The cart fills in from the browser. On every other page they get normal WordPress |
| A notice waiting to be shown (such as "item removed") | That one page is normal WordPress | |
Sorted, filtered or searched lists, ?add-to-cart |
Normal WordPress | Anything with a query string |
| Cart, Checkout, a guest's My account | Normal WordPress, or the shell below | |
| Logged-in customers, on the catalogue pages and home page above | Normal WordPress, or Flx with FLX_MODERNIZE_LOGGED_IN_CUSTOMERS (plugin 1.10.0) |
The same page a guest gets; nothing about the customer is sent. Leave it off if prices or products differ per customer |
Other logged-in visitors and pages, anyone who can edit content or run the store, order pages, account endpoints, wc-ajax, the Store API, every request other than GET or HEAD |
Normal WordPress | Always |
| WooCommerce "Coming soon", in either mode | Normal WordPress | |
| Ordinary pages and posts | Normal WordPress | This renderer has no route for them: the first one gets a 404 from it, and that page type then skips the call for an hour |
| Any theme but Storefront, or a template this release wasn't reviewed against | Normal WordPress | A theme override of a WooCommerce template counts |
"Storefront" includes a child theme of it, but only WooCommerce's templates and the homepage template are checked for changes: a child theme's own header.php or footer.php isn't reproduced. The home page also requires Storefront 4.6.2 itself.
The shell: Cart, Checkout and My account
With FLX_MODERNIZE_WOOCOMMERCE_SHELL, and FLX_MODERNIZE_WOOCOMMERCE_SHELL_CHECKOUT for Checkout, these pages get the renderer's header and footer around WooCommerce's own forms, on WordPress 6.9 or later:
- The renderer draws a shell with empty slots, ahead of time. A signed request from the site to itself, with no cookies, builds it. It is stored for a day and refreshed every 5 minutes while the pages are in use, so a shopper's request never calls the renderer.
- WordPress fills the slots on your server. It renders its normal page, then places the head, main content, search, cart and footer output into the shell. These parts never leave the server.
- The shell must pass an allowlist before it is stored. No scripts, forms or anything that loads from another site. See the rules. A refused shell is logged (
flx_modernize_shell), and that page's stored shell is removed. - Anything in doubt gives the normal page. No stored shell, markers out of place, or an error while composing.
These pages also get private, no-store, DONOTCACHEPAGE, and a Content-Security-Policy of base-uri 'self', with form-action 'self' on Cart and My account. A plugin that posts a form to another site from those pages, such as some social logins, can be blocked by that: test it. Only GET requests are composed.
Changing the render URL or signing key removes stored shells. It relies on the site being able to request its own pages; where that is blocked, these pages stay normal WordPress. A site whose home URL follows the request's host can't use it safely: the build request's signature would go to a host the visitor chose.
Routes
| Route | Purpose |
|---|---|
POST /api/v1/render/product |
A single product |
POST /api/v1/render/product_archive |
The shop, a product category or a tag |
POST /api/v1/render/home |
Storefront's home page |
POST /api/v1/render/shell |
The shell around Cart, Checkout and My account |
GET /health |
Service status, version and the page types it draws |
GET /liveness, GET /readiness |
Process and readiness checks |
Render routes need a signed request. An unusable payload, or a theme that throws, answers success: false with no HTML, and the plugin serves the normal page.
The data it receives
Everything is built with no visitor: as user 0, with an empty cart and a new customer, and with cookies and the query string hidden from hooks. So a shopper with a cart gets the same page as anyone else, and their cart, cookies and customer details aren't in the request. The exception to watch for is a plugin that prints something based on other parts of the request, such as a price or currency chosen from the visitor's IP address. That output is part of the page data, so check such plugins first, or keep those pages native.
| Field | What it is |
|---|---|
type, title, permalink, language |
The page |
main_html |
WooCommerce's output for the page's middle, with comment markers around sections |
cards |
Data for each product card: see the guide |
product_page |
Data for the single product |
head_html, footer_html, body_class |
What WordPress prints in the head and footer, and the body's classes |
site, menus, shop |
Site name and logo, menus, and the account, cart and privacy links |
storefront |
Storefront's hook and widget output: search, header cart, breadcrumb, footer widgets |
A shell request has only site, menus, shop and part of storefront.
Configuration
| Variable | Needed | Meaning |
|---|---|---|
FLX_RENDER_PUBLIC_URL |
yes on a server | The renderer's public URL |
FLX_RENDER_SIGNING_KEYS |
yes | keyId:secret (comma-separate several keys, so a secret can't contain a comma) |
FLX_RENDER_AUTH_MODE |
yes | enforce in production. Unset means warn, which only logs failures |
FLX_RENDER_DEPLOYMENT |
local only | local allows a localhost URL when FLX_RENDER_PUBLIC_URL is unset |
FLX_RENDER_THEME |
no | The theme's name. Unset or unknown: WooCommerce's and Storefront's own markup |
In wp-config.php:
define( 'FLX_MODERNIZE_WOOCOMMERCE', true ); // Catalogue pages.
define( 'FLX_MODERNIZE_WOOCOMMERCE_SHELL', true ); // Cart and My account in the renderer's header and footer.
define( 'FLX_MODERNIZE_WOOCOMMERCE_SHELL_CHECKOUT', true ); // Checkout too: read "Before using this on a real store".
Set the plugin's Render URL and signing key as in the plugin's documentation.
Deploying on Vercel
- Create a Vercel project from the package (or your repository), with Root Directory
renderer. - Set the Production environment variables above, with
FLX_RENDER_AUTH_MODE=enforce. - Deploy, and check that
/healthlistsproduct,product_archive,homeandshell, and that an unsigned POST to a render route answers 401.
Deploy the renderer before updating the plugin: an older renderer would leave the plugin's section markers on the page as HTML comments.
Running on your own server
cd renderer
npm ci
npm run build
FLX_RENDER_PUBLIC_URL=https://render.example.com \
FLX_RENDER_SIGNING_KEYS=site1:$(cat /path/to/secret) \
FLX_RENDER_AUTH_MODE=enforce \
PORT=3000 node dist/server.js
Security
- Every render request must be signed; use one key per site.
- Cookies, customer data, cart contents and query strings aren't sent. See the data it receives for what a plugin could still add.
- Pages are sent
private, no-store. - The renderer is part of the shop's security: catalogue pages print what it sends, including scripts. See Before using this on a real store.
Speed
Flx showed no speed advantage on Storefront. Measured on the live demo, /shop/ and similar pages against the same pages served by WordPress (Lighthouse median of 3, curl median of 20): time to first byte is 150–260 ms slower, page weight is equal, and mobile LCP and performance scores are within noise. That was with no theme, and before the shell existed; neither has been measured. The reason to use it is writing pages in JavaScript, not speed.
Development
cd renderer
npm install
npm test # unit tests
npm run preview # your theme on sample data, no WordPress
npm run check-theme # what would break, in words
npm run dev # the real service locally (FLX_RENDER_DEPLOYMENT=local)
Published from rickey29/twenty-twenty-five at commit 837705d · Flx Modernize 1.11.0 · Flx Render 1.11.0