How a site chooses, loads and serves the frontend its editor pages run. A site's [frontend] source is the built-in React editor, a local path: folder, an https: archive, or a git source (git+https:, tangled:); a remote one is pinned and kept as a checked copy under .lichen/. The same loader fetches the built-in editor's plugins (phase 13), and the front server reuses it for a host-selected dashboard. All of this is new since 09-23.
AST-verified changes
- Item 6's api-layer move (7601a57c) created
api/html.rsandapi/frontend.rs, moving the page and frontend handlers out of lichen-cms'scms.rsandrouter.rs. Those two files are deleted in #35, which holds the rest of the move. Checked by the builder: "3035 items before and after, 146 moved, 25 visibility changes as mapped, 3690 comments compared, equal".verify.shrun FUHHLT: "VERIFY OK (ast 3035 items/146 moved/25 visibility/3690 comments; routes 130=130 ...)". Later VERIFY OK runs: TRNKZ7, S1xZAJ, FZ0qAX, 1wrz6l, 0I9ss7. The adversary's run eMUcJc was VERIFY OK. Allowed: path rewrites (crate::cms/router→crate::api), member visibility, dropped file attributes, lone trailing commas. - OPERATOR-REVIEW-FIXES U1 (1d6fe2d4) moved shared helpers out of
api/html.rsintoapi/api_utils.rsand its neighbours (#35). One check over 8b30050a..f6a10c32: "3004 items, 48 moved, 69 carried, 19 renamed, 2 visibility as mapped, 233 module declarations" (run id not recorded). The adversary's rerun, notify-abab368a, got the same counts.
Roughly 69 of this PR's 3,077 changed lines are code these moves carried verbatim. Every file here was created after 09-23 and changed a good deal after the moves, so in this diff they read as new code.
FRONTEND-API-AND-LOADER made the editor a replaceable frontend over the site's /cms/api, and phase 13 added plugins to the built-in:
- Choosing a frontend (
frontend.rs).[frontend] sourceisbuiltin(the default),path:<folder>(read live, through the site's confined tree), anhttps:archive, orgit+https:/tangled:. Anything that cannot be served serves the built-in with a message saying why: no manifest, another API version, an unpinned or unfetchable source. - Archives (
frontend/remote.rs). Fetched through the SSRF guard and pinned bysha256:over the unpacked files. The copy is kept in.lichen/frontends/<name>@<pin>/, outside git, and checked against its pin once per settings generation. A missing copy is fetched again in the background and kept only if it still matches. Limits: 20 MiB, no symlinks, no path that leaves the frontend, redirects re-checked. The loader has kinds, so the front server's dashboard source (DASHBOARD) and plugins (PLUGIN) use it too. - Git sources (
frontend/git.rs). Pinned by commit and located through the guarded client.gitfetches one commit (--depth 1) in a scratch folder, outside the site's lock, with hooks and submodules off, bounded in time. The files are read object by object, never checked out, with tree and size caps checked before any blob is read. The module's own docs state the DNS-rebinding window the address check leaves. - Plugins (
plugins.rs,plugins/remote.rs). A site's[[plugins]]list, afterlichen.toml's defaults in multi-site mode (inherit_plugins = falsedrops them). Built-in plugins ship in the binary; remote ones are pinned and fetched like frontends into.lichen/plugins/. One plugin that cannot be served is refused alone. - Page URLs (
api/html.rs). Every/cms/page path (item 9) serves the selected frontend.?frontend=builtinis the escape hatch to the built-in, and?format=htmlgives the no-JavaScript editor, now the built-in drawn by React on the server (REPLACE-TERA R1). Whilesettings.tomlcannot be read, pages redirect to its own page (repair mode). - Routes:
POST /cms/api/frontend/updatepins a remote frontend (api/frontend.rs).GET /cms/api/plugins, a plugin's files, and/cms/api/plugins/{name}/updateare inapi/plugins.rs. build.rs— new: release builds watch the embedded folders (the built-in app, its plugins,lichen-ts/dist, the no-JavaScript static files), so a changed bundle rebuilds the binary.
Files (crates/lichen-site/)
src/frontend.rs— choosing what a page serves, and why not. Read this first.src/frontend/remote.rs(838 lines) — archives, pins, copies, and the loader's kinds.src/frontend/git.rs(590 lines) — git sources.src/plugins.rs,src/plugins/remote.rs— plugin lists and remote plugins.src/api/html.rs— page URLs, the escape hatch, the server-drawn editor, repair mode.src/api/frontend.rs,src/api/plugins.rs— their API routes.build.rs— the embedded folders' rebuild watch.
Moved in: all nine files, by an explicit rule. Round 1 put frontend/*, api/html.rs and build.rs in the dashboard PR, because the dashboard source reuses the loader, and api/frontend.rs and api/plugins.rs in site core. Here they are one subject: how a site serves a frontend.
Lives elsewhere
- The rest of the site's API, and the deletions of
cms.rsandrouter.rs: #35. The route table and its permission groups (api.rs): #35. - The SSRF guard (
ssrf_guard.rs) and the settings the loader reads: #37. The front server's dashboard source, which uses this loader: #33. - The server render the no-JavaScript editor uses: #26.
- Not in any PR (base commit): the built-in editor and its plugins (
frontends/builtin/),frontends/no-javascript/,lichen-ts, the built bundles and the tests (frontend/tests/*,plugins/tests/*,tests/plugins/).
Category: The site's frontends and plugins: choosing, loading and serving them
Size: 9 files, +3,077/−0
Rounds: round 0 at stack ae42e3c0. At 29a7c15b, 6b650b2a and b66397c0 none of this PR's files changed, so round 0 is current; the branch was merged up so that the stack's top equals b66397c0.
Plans and ledger: FRONTEND-API-AND-LOADER-2026-09-24.md (the loader; pieces s/23 archives, s/24 git, s/25 update), PHASE-13-UNIT-1A-DESIGN-2026-10-04.md, -UNIT-2-, -UNIT-4- (plugins), OPERATOR-REVIEW-FOLLOWUPS-2026-09-30.md (items 6 and 9), REPLACE-TERA-WITH-SSR-REACT-2026-10-07.md (R1: the server-drawn no-JavaScript editor), DASHBOARD-REACT-2026-10-07.md (the loader's dashboard kind).
Inline #[cfg(test)] modules: none. 5 files point to a test file with #[path] (frontend.rs, frontend/git.rs, frontend/remote.rs, plugins.rs, plugins/remote.rs).
Bucket moves: all nine files are placed by an explicit rule (see Moved in). frontend.rs, plugins.rs and plugins/remote.rs were not in round 1, which had no change to them.
Round 1: #12 (Dashboard) covered frontend/git.rs, frontend/remote.rs, api/html.rs and build.rs; #13 (Site crate core) covered api/frontend.rs and api/plugins.rs.