FlexPlat logoFlx Modernize

Flx Render (Twenty Twenty-Five)

Version 1.0.0 · Node.js 22 · Express 5 · TypeScript · GPL-2.0-or-later

Flx Render turns the page data that the Flx Modernize plugin sends into a finished HTML page in the site's theme. It is stateless: no database, no sessions, no stored content. This package renders sites that use the Twenty Twenty-Five theme.

Architecture

Visitor ──GET /hello-world/──▶ WordPress + Flx Modernize
                                 │  builds page data (as an anonymous visitor)
                                 │  POST /api/v1/render/post  (signed JSON)
                                 ▼
                               Flx Render ──GET theme CSS──▶ WordPress REST (/wp-json/flx-modernize/v1/…)
                                 │  returns { success, html }
                                 ▼
Visitor ◀──HTML (Cache-Control: private, no-store)── WordPress
  • Push model: WordPress sends everything the page needs; the renderer never queries the WordPress database.
  • One exception: the theme's CSS. The payload carries links to the site's global-styles and font CSS. The renderer fetches them (2-second timeout, cached for 24 hours per URL, failures retried after 30 seconds) and inlines them into the page.
  • Fallback: the renderer never decides what is correct. If it fails, the plugin serves the WordPress page.

Routes

Method and path Purpose Auth
POST /api/v1/render/page a page signed
POST /api/v1/render/post a single post signed
POST /api/v1/render/archive blog home page, category and tag archives signed
POST /api/v1/render/search search results signed
POST /api/v1/render/404 the 404 page signed
GET /health, /liveness, /readiness status for monitoring public, no details
GET /static/* the renderer's own CSS and the mobile-menu script public

A render route answers 200 {"success": true, "html": "…"}, 400 for an invalid payload, or 401 for an authentication failure ({"error": "<code>", "message": "…"}, never naming the key).

The data it receives

All fields are plain JSON. HTML fields (content) arrive already rendered by WordPress; text fields are escaped by the renderer.

Field Page Post Archive Search 404
title, site_name ✓ ✓ ✓ (from query) optional
permalink / url ✓ ✓ ✓ ✓
content ✓ ✓
date, author, slug, excerpt ✓
items[] (title, url, excerpt, date) and pagination ✓ ✓
query ✓
message, home_url optional
featured_image (url, width, height, alt) optional optional
navigation.primary[] (label, url, current) optional optional optional optional optional
global_styles, theme_assets.fonts (CSS links) optional optional optional optional optional

There are no cookies, no user data and no visitor query strings. The search words are the one piece of visitor input, sent because the results page shows them. The full type definitions are in src/platforms/wp/twentytwentyfive/*/types.ts.

How pages are built

src/lib/layout.ts assembles the document: <head> with the renderer's stylesheet, then the inlined theme CSS, then the header, main content and footer. The Twenty Twenty-Five module (src/platforms/wp/twentytwentyfive/) supplies the theme's own markup:

File Theme part it reproduces
header.ts parts/header.html: site title and the primary menu (theme Navigation block classes, responsive overlay)
footer.ts parts/footer.html: site title, two link columns, "Designed with WordPress"
page/, post/ single page and post content
archive/ (also used by search/) the query loop of home.html, archive.html, search.html
error404/ templates/404.html: image, heading, message, search form

Because the markup keeps the theme's block classes, the inlined global-styles CSS styles it exactly as on a native page. The few per-page layout rules WordPress normally prints inline (.wp-container-core-…) live in src/styles/tailwind.css, with doubled class selectors so they win over the theme's generic rules regardless of load order.

Configuration

Variable Required Meaning
FLX_RENDER_PUBLIC_URL yes on a server the renderer's public URL, used for its own CSS and script links
FLX_RENDER_SIGNING_KEYS yes keyId:secret (comma-separate several keys)
FLX_RENDER_AUTH_MODE yes enforce in production; warn only logs failures
FLX_RENDER_DEPLOYMENT local only local allows a localhost URL when FLX_RENDER_PUBLIC_URL is unset
PORT no default 3000 (not used on Vercel)

Deploying on Vercel

  1. Create a Vercel project from the package (or your repository), with Root Directory renderer (or the package folder) and Node.js 22. Vercel detects Express; no vercel.json is needed.
  2. Set the Production environment variables above, with FLX_RENDER_AUTH_MODE=enforce.
  3. Add your domain, e.g. yoursite-render.example.com. Point a DNS CNAME at Vercel; with Cloudflare, use DNS only.
  4. Optional, to deploy only from main: set Ignored Build Step to if [ "$VERCEL_GIT_COMMIT_REF" = "main" ]; then exit 1; else exit 0; fi.
  5. Check it: GET /health gives 200, and an unsigned POST /api/v1/render/page gives 401.

Running on your own server

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

Put it behind a TLS reverse proxy. The renderer must be able to reach the WordPress site's /wp-json/flx-modernize/v1/ endpoints.

Security

  • Every render request must be signed (FLX_RENDER_AUTH_MODE=enforce); use one key per site.
  • No diagnostics endpoints; health routes return no configuration, and errors never include secrets or key IDs.
  • The renderer sets no cookies, and stores nothing.
  • The replay-protection nonce store is per instance, so on serverless platforms a replay can be accepted by another instance within the 300-second window. Every request is still signed.
  • Keep npm audit at zero critical findings.

Development

npm ci
npm test          # node:test, ~250 tests
npm run build     # Tailwind CSS + TypeScript
npm run dev       # local server with reload (FLX_RENDER_DEPLOYMENT=local)

Published from rickey29/twenty-twenty-five at commit 2541c1d · Flx Modernize 1.0.0 · Flx Render 1.0.0