Page MenuHomePhabricator

Restore the server render contract (islands architecture)
Closed, ResolvedPublic3 Estimated Story Points

Description

The Vue Router migration (T428481) moved more state to the client so the router could do the heavy lifting, module body HTML included, through getJsData(). MediaWiki has no Vue SSR, so the client recompiled those server-rendered strings on mount rather than adopting them. render() stopped being the thing that puts a module's body on the page: getJsData() carried the HTML and the client re-injected it. @Dillon spotted that render() was no longer being called server-side, and pulling that thread turned up the rest of the damage: module bodies dropping, the Banner module broken, and a no-JS visitor served a blank page.

We were previously pointing towards islands architecture by accident. The server was already emitting a card frame; the client was already hydrating bodies into it. We had built half of islands by accident and skipped the other half, the part that pins down who owns what, which happens to be the half that keeps it working.

This task restores the contract and the islands split, and ships the logic for 3 modules kinds (server side, progressive enhancement, vue island). It does not convert any default module to server-side rendering yet; putting that logic to work on the default modules is a follow-up.

The three render modes we inherited from GrowthExperiments (desktop, mobile-summary, mobile-details) ran two separate decisions through one string:

  • which platform the server renders for
  • how much detail to show

Splitting them lets the server settle platform and the client derive detail.

This restores a clean split and names the pattern. The server renders each module's card frame with an empty slot, and the client teleports its Vue island into that slot. The only rendering decision the server makes is platform, desktop or mobile; detail level, the compact card versus the full view, is the client's call and never reaches PHP. One serverRendered() flag now distinguishes three kinds of module:

  • Vue island (the default): a server frame with an empty slot the client fills in.
  • Server-static: the whole body is server HTML the client leaves alone (Banner, ReturnToHomepage).
  • Progressively enhanced: a server body plus a behavior module that enhances it in place, no re-render.

Codex ships a server-side PHP library (Wikimedia\Codex), but it covers presentation and form components only: Card, Button, InfoChip, Message, Field, TextInput and the like. Anything with client state, a dialog, a popover, a menu, a combobox, has no PHP builder and lives only in the Vue library. The same no-SSR constraint that broke the old approach also means we cannot server-render one of those and hydrate it; the PHP and Vue libraries are two implementations of one design, not a single isomorphic render.

Progressively enhanced module support fills that gap: a module renders the presentation half server-side so no-JS users get real content, then layers the interactive Vue component on top. The contract supports it here; the first conversion of a default module, Policies & Guidelines, is the follow-up.

Without JavaScript the page renders server frames rather than a blank page: server-static modules show their content, island bodies fill in once JS loads behind a noscript notice, and a module name in the subpath renders that module's focused subpage, the destination the dashboard's in-body links fall through to.

*Acceptance criteria*

Contract

  • render() is the sole source of a module's server HTML; getJsData() carries only bootstrap data (enabled, header, expandable, serverRendered), never body or footer HTML.
  • Island modules emit an empty slot server-side and the client teleports the Vue body into it.
  • Server-static modules (Banner, ReturnToHomepage) render their full body server-side and the client leaves them in place.
  • The contract supports a progressively enhanced module, a server body enhanced in place by a behavior module; no default module is converted in this task.

No-JS

  • A no-JS visitor sees server frames rather than a blank page, with a noscript notice explaining bodies that only fill under JS.
  • A module name in the subpath renders that module's focused subpage, the no-JS destination an expandable card's anchor falls through to.

Coverage

  • PHPUnit covers the contract: getBodyContent() returns the empty slot for an island and the full body for a server-rendered module, and the mobile card wraps in its self-link but drops that anchor on a focused render.

Event Timeline

Change #1312495 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Server frame, client body split

https://gerrit.wikimedia.org/r/1312495

Change #1312497 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Teleport island bodies

https://gerrit.wikimedia.org/r/1312497

Change #1312498 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Server-render Policies module

https://gerrit.wikimedia.org/r/1312498

Change #1312499 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Scope islands to focused module

https://gerrit.wikimedia.org/r/1312499

Change #1312500 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Focused module whole-page view

https://gerrit.wikimedia.org/r/1312500

Change #1312501 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Deep-link the examples dialog

https://gerrit.wikimedia.org/r/1312501

Change #1312502 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Collapse columns in CSS, not JS

https://gerrit.wikimedia.org/r/1312502

Change #1312503 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Test the render contract

https://gerrit.wikimedia.org/r/1312503

Change #1312587 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Route by page title

https://gerrit.wikimedia.org/r/1312587

Change #1312588 had a related patch set uploaded (by Jsn.sherman; author: Jsn.sherman):

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Mobile view from MobileFrontend

https://gerrit.wikimedia.org/r/1312588

Change #1312588 abandoned by Jsn.sherman:

[mediawiki/extensions/PersonalDashboard@master] PersonalDashboard: Mobile view from MobileFrontend

Reason:

collapsing this into If8e0065eae44cfcdbf78a67c2fe7ee2cc1a92bd2

https://gerrit.wikimedia.org/r/1312588

Split the task along the agreed lines in the estimation meeting. Estimate this as a step 1 where we do the contract change and make render a hot method again and adopt the islands architecture but not put in the server side logic yet, but it won't have the server side logic within it. Have a follow up task to add the server side logic and have it work with this task.

Dillon set the point value for this task to 5.Tue, Jul 21, 1:45 PM
Dillon moved this task from To be estimated to Estimated on the Moderator-Tools-Team board.

all of the logic will be there for server side, we just won't update any default mod tools modules to use it yet.

jsn.sherman changed the point value for this task from 5 to 8.
jsn.sherman added a subscriber: Dillon.
jsn.sherman changed the task status from Open to In Progress.Wed, Jul 22, 3:06 PM
jsn.sherman moved this task from Ready to In Progress on the Moderator-Tools-Team (Kanban) board.
jsn.sherman changed the point value for this task from 8 to 3.

Change #1312495 merged by jenkins-bot:

[mediawiki/extensions/PersonalDashboard@master] Server frame, client body split

https://gerrit.wikimedia.org/r/1312495

Change #1312497 merged by jenkins-bot:

[mediawiki/extensions/PersonalDashboard@master] Teleport island bodies

https://gerrit.wikimedia.org/r/1312497

Change #1312503 merged by jenkins-bot:

[mediawiki/extensions/PersonalDashboard@master] Test the render contract

https://gerrit.wikimedia.org/r/1312503

ext.personalDashboard.banner is now serverRendered: true

{
  "name": "ext.personalDashboard.banner",
  "enabled": false,
  "header": "",
  "expandable": false,
  "serverRendered": true
}

while riskyEdits is not:

{
  "name": "ext.personalDashboard.riskyArticleEdits",
  "enabled": true,
  "style": "thin",
  "header": "Review changes",
  "expandable": true,
  "serverRendered": false
}

data-module-name is now present on each module card:

Screenshot from 2026-07-29 10-52-46.png (779×181 px, 56 KB)

A no-JS visitor sees server frames rather than a blank page, with a noscript notice explaining bodies that only fill under JS.

Confirmed.

Can confirm visual parity between Minerva and Vector on test.wikipedia.org on mobile:

Screenshot 2026-08-05 at 11-19-54.png (751×1,557 px, 125 KB)
Screenshot 2026-08-05 at 11-20-38.png (754×1,552 px, 129 KB)

Dillon moved this task from QA to Done on the Moderator-Tools-Team (Kanban) board.