diff --git a/internal/plugin-gallery/dev-plugin.mjs b/internal/plugin-gallery/dev-plugin.mjs index d0cf967e5..5f96a0170 100644 --- a/internal/plugin-gallery/dev-plugin.mjs +++ b/internal/plugin-gallery/dev-plugin.mjs @@ -109,6 +109,58 @@ export function galleryModel(reg) { } } +// ── THE PRODUCTION EMBED URL, ANSWERED BY THE DEV SERVER ───────────────────────────────────── +// The manifest advertises, and a person copies, tree-relative embed URLs of the shape +// `//index.html` and `///index.html` — served in +// production out of public//. The dev server had NO route for that shape; it served its +// leaf stand-in at `/__leaf//[/]` only. +// +// DRIVER-REPORTED 2026-09-23: he copied an embed URL and opened it against his dev server — +// http://localhost:5174/doxa-simple-map/doxa-simple-map/prayer/index.html +// — and it "did not work". The leaf is real (that exact path serves 200 from the built tree) and +// the embed is healthy (it mounts at 700px in a real iframe). The dev server simply had no route. +// +// ★ AND IT FAILED SILENTLY, which is the part that costs hours. With no route, Vite's SPA fallback +// answers an unknown path with the STUDIO'S HTML at status 200. The reader asks for one map and +// receives a different, entirely working page, with no 404 and nothing in the console. That is the +// same four-surface class as the two other bugs fixed the same day (a manifest 404 returning HTML; +// a ../../ child path that 404'd only from a deeper leaf) — see the assetsBaseUrl and +// MAP_APP_API_URL notes below, both of which are that shape. +// +// The fix REWRITES onto the existing `/__leaf/` renderer rather than copying it. Two renderers +// would drift, and the entire point of the route is that one embed URL renders the SAME thing in +// both environments — a second copy would quietly make that false. +// +// Both prefixes are accepted because both are real: +// /doxa-simple-map/doxa-simple-map/prayer/index.html bare, tree-relative — what the +// manifest's `.//

/…` snippet +// becomes when pasted at the root +// (this is the URL the Driver used) +// /doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html prefixed — the built tree's own +// path under public// +// `treeName` comes from the build config (`name`, default 'doxa-maps'), never hardcoded, so renaming +// the output folder cannot silently strand this route. +// +// Returns the rewritten `/__leaf/…` path, or '' when the URL is not an embed leaf URL (the common +// case — every other request must fall through untouched). +export function leafRewrite(path, treeName = 'doxa-maps') { + if (!path.endsWith('/index.html')) return '' + let segs = path.slice(0, -'/index.html'.length).split('/').filter(Boolean) + if (segs[0] === treeName) segs = segs.slice(1) // tolerate the tree-prefixed form + // / or //. Anything else is not a leaf URL; let it + // fall through to Vite rather than guess (a wrong guess here would shadow a real page). + if (segs.length < 2 || segs.length > 3) return '' + // Traversal guard BEFORE the segments are re-joined into a URL the leaf route will parse. The + // leaf route guards again on its own decoded segments; this one stops an encoded `..` from ever + // being formed into the rewritten path. + for (const s of segs) { + let d = s + try { d = decodeURIComponent(s) } catch { return '' } + if (!d || d === '.' || d === '..' || d.includes('/') || d.includes('\\')) return '' + } + return '/__leaf/' + segs.join('/') +} + export function autoSpaDevPlugin(token = '') { // One generator for both galleries (internal/generate-gallery.mjs), driven by the same build // config the built page bakes. What dev changes: the model comes from discovery, the live @@ -153,7 +205,18 @@ export function autoSpaDevPlugin(token = '') { apply: 'serve', configureServer(server) { server.middlewares.use((req, res, next) => { - const path = (req.url || '').split('?')[0] + const rawPath = (req.url || '').split('?')[0] + // A production embed URL (`/[/]/index.html`) is rewritten onto + // the `/__leaf/` route BEFORE any branch runs, so the existing leaf renderer answers it + // unchanged — see leafRewrite above for why the dev server must know this shape at all. + // The rewrite is applied to `path` (which every branch reads) and to `req.url` (so anything + // downstream sees a consistent request); never via server.middlewares.handle(), which + // replays this same middleware and would loop. + let treeName = 'doxa-maps' + try { treeName = readBuildConfig()?.name || treeName } catch { /* keep the default */ } + const rewritten = leafRewrite(rawPath, treeName) + const path = rewritten || rawPath + if (rewritten) req.url = rewritten + (req.url || '').slice(rawPath.length) // /assets/* — see resolveDevAsset. A miss is a REAL 404, never Vite's HTML // fallback: HTML-for-JSON made three maps die silently at JSON.parse before // they ever created a map, with no console error and no error card.