FlexPlat logoFlx Modernize

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

  1. Create a Vercel project from the package (or your repository), with Root Directory renderer.
  2. Set the Production environment variables above, with FLX_RENDER_AUTH_MODE=enforce.
  3. Deploy, and check that /health lists product, product_archive, home and shell, 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