Step 11: Closing the last gaps
After step 10 the pages scored 94.2% / 90.9%, but whole features were still missing: the comments, sub-menus, the "More posts" lists, and the details around a post (author, categories, tags, previous/next links). None of them can be drawn from the data the renderer had. This step adds that data, one feature at a time.
Add a payload field, plugin and renderer together
The renderer only knows what the payload carries, so a missing feature usually means a missing field. Add it on both sides in one change:
- Plugin: build the field as the anonymous visitor (user 0), like the rest of the payload, so it only ever holds public content. Example:
more_posts, the four latest posts the theme's "More posts" pattern queries (SearchPayloadBuilder::more_posts()). - Renderer: draw the theme's own markup for it (the pattern's rendered HTML, captured from
native.html), escaping every text value and skipping malformed entries. - Make the field optional. When it is absent (an older plugin), the renderer leaves the section out and the page renders as before, so plugin and renderer can be released in either order.
- Write a short decision record for the new field (it changes the request format).
The same pattern brought the post-page details (author_url, categories, tags, previous, next, more_posts) and header sub-menus (children in navigation.primary, up to three levels, opening on hover and keyboard focus with CSS only).
Let WordPress render what plugins hook into
The comments section is different: its reply form is WordPress's own comment_form(), which anti-spam and consent plugins extend with hidden fields and notices. Rebuilding it from data would lose them. Instead, the plugin renders the theme's comments pattern itself and sends the HTML (comments_html), the same trust level as the post content:
- render it as user 0 with the visitor's cookies and query string hidden, so it holds only approved comments and an empty form, the same for every visitor;
- send commenters to normal WordPress: requests with WordPress's comment cookies, or a reply or "awaiting moderation" link, are never rendered by Flx, so a commenter still sees their own pending comment and a filled-in form;
- purge the CDN when a comment is approved, edited or a comment setting changes, if edge caching is on.
Measure the page types you add
The harness compared only the fixture's pages; its posts were captured but never scored. Scoring them (and adding them to the gate) showed post pages at 69.2% / 68.4%, the worst page type, which the post-page details then took to 96.6% / 94.6%.
Three styling lessons
Read the remaining differences row by row (the harness's diff.*.png, or a script that scans the screenshots for rows of content) before guessing.
- The root block gap. Native puts
--wp--style--block-gap(1.2rem) between the top-level parts of the page, including above the footer, unless a part sets its own margin. Missing it made every page 13–38 px shorter. - Tailwind's preflight outranks
:where(). A rule like:where(.x) textarea { … }has zero specificity and loses to preflight'stextarea { font: inherit }. Give overrides one class (.wp-block-comments :where(textarea)). The comment box was 10 px taller until this was fixed. - Only inline styles win in both style modes. In the
wprollback mode, WordPress's unlayered layout CSS beats every Tailwind utility. For a value native sets per element (the "More posts" rows have no gap), an inline style works in both modes.
Check: each change goes through the fidelity gate, pages and posts, before and after.
Result on Twenty Twenty-Five: 98.9% / 97.3% on the measured pages, 99.9% / 99.6% on posts.
Next: a client's site
The tutorial ends with a renderer for the theme's default look. A client's site has its own colours, fonts, header, footer and templates, so each site gets its own renderer: follow the site conversion checklist.
Published from rickey29/twenty-twenty-five at commit 837705d · Flx Modernize 1.11.0 · Flx Render 1.11.0