You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found in the design review of the v2.5.0 deploy (6d773b63c, https://6a9b93b9108dd4beb6bebadc--epic-agnesi-957267.netlify.app), item 5. On a myst build --html deploy, clicking any "On this page" entry — or any in-content cross-reference — and then pressing the browser Back button once destroys the page: the whole document is replaced by a bare white screen reading "Application Error" with a JavaScript stack trace under it. Every lecture page on a static host is one Back press away from this, including the landing page. It is not a Netlify quirk; it needs only that GET /page/?_data=root return the page's HTML instead of loader JSON, which is what every static file server does.
Reproduction
Open any page on a static build, click an outline entry, press Back once. Measured four ways:
Server
MODE
Engine
?_data= responses on Back
Result
Netlify (the reviewed deploy), /python-by-example/
static
Chromium
?_data=root and ?_data=routes%2F%24, both 200 text/html; charset=UTF-8
2 page errors, <title>Application Error!</title>, body is the stack
python3 -m http.server over tests/visual/fixture built with myst build --html
static
Chromium
/?_data=root and /?_data=routes%2F_index, both 200 text/html
Application Error
Same local static build
static
WebKit
same
Application Error
Same fixture under myst start
app
Chromium
both 200 application/json; charset=utf-8
0 errors, article renders normally
The last row is the point: the bug exists only in static builds, and it is reproducible on the repo's own fixture with no Netlify, no CDN and no redirects involved.
The thrown error differs by route, and both strings need to be in anyone's search:
TypeError: Cannot read properties of undefined (reading 'title')
/build/routes/_index-XSHLEO5O.js
The screen is unstyled because two boundaries fail in sequence. app/root.tsx:25 exports AppErrorBoundary from @myst-theme/site, and it does catch the throw — but AppErrorBoundary (node_modules/@myst-theme/site/src/pages/Root.tsx:167-178) renders <Document> again, Document renders <Meta/>, and <Meta/> re-runs the same throwing meta(). The boundary therefore throws during its own render and Remix falls back to RemixRootDefaultErrorBoundary (node_modules/@remix-run/react/dist/errorBoundaries.js:89,98): a bare <html> with <title>Application Error!</title>, an <h1>Application Error</h1> and a <pre> of error.stack. That is exactly what I measured — the page title after Back is literally Application Error!.
Mechanism
myst build --html runs the theme with MODE=static. app/root.tsx:201 reads it from the root loader and app/root.tsx:211 passes staticBuild={MODE === 'static'} to upstream Document. Document (@myst-theme/site 1.3.0, src/pages/Root.tsx:60-69) responds by swapping the link provider to <Link reloadDocument> — but <Scripts /> at src/pages/Root.tsx:150 renders unconditionally, so the Remix data router hydrates in static builds too. The deployed /build/manifest-DADB2520.js carries "hasLoader":true on root, routes/$ and routes/_index, so there are live loaders on a host that cannot serve them.
React Router does short-circuit hash-only navigations — router.js:1658 guards startNavigation with isHashChangeOnly(state.location, location) — but isHashChangeOnly (node_modules/@remix-run/router/dist/router.js:3673-3690) deliberately returns false when the hash is removed, which is exactly what Back from #anchor to the bare URL does. The function's own comment says why: "If the hash is removed the browser will re-perform a request to the server". So the navigation is not short-circuited, and getMatchesToLoad then sets defaultShouldRevalidate = true on the branch at router.js:3126 ("Clicked the same link, resubmitted a GET form") because currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search.
No route module in the theme exports shouldRevalidate — grep -rn shouldRevalidate app/ on 6d773b63c returns nothing — so createShouldRevalidate (node_modules/@remix-run/react/dist/routes.js:110-128) falls through to arg.defaultShouldRevalidate at line 126 and the loaders re-run. The client fetches ?_data=root and ?_data=routes%2F%24; the static host answers 200 text/html; callLoaderOrAction takes the non-JSON branch data = await result.text() (router.js:3391-3395); and the loader value for both routes becomes the page's HTML source as a string. The guard at app/routes/$.tsx:19 (if (!data) return [];) passes, because a non-empty string is truthy, and the next property read throws. app/components/Page.tsx:21 ((data.page.frontmatter as any)?.site) would throw next even if meta() were hardened.
Sphinx baseline
Measured, not assumed. https://python-programming.quantecon.org/python_by_example.html has no client router at all (window.__remixContext is absent), its in-page anchors are bare href="#overview" fragments, and clicking one then pressing Back returns to the intact article. There is no Sphinx value to match here and no design question to answer — the parity target is simply that Back works, so this is a straight regression to repair rather than a divergence to decide.
Fix
Export a shouldRevalidate from the three route modules that render UI — app/root.tsx, app/routes/$.tsx and app/routes/_index.tsx. createShouldRevalidate prefers a route module's own function (@remix-run/react/dist/routes.js:123-124), unconditionally, once the module exports it.
Why this is the right guard, and why it is safe in both modes: a static build (myst build --html) still hydrates a live data router, but its loaders are only servable by a running Remix server, so on a static host ?_data=… returns the page HTML, @remix-run/router turns that into a string loader value (router.js:3391-3395), and every data.page read then throws. React Router short-circuits hash-only navigations except when the hash is removed (isHashChangeOnly, router.js:3687) — exactly what Back from #anchor does — so that POP would otherwise re-run the loaders. These three loaders are pure functions of pathname and search, so declining to revalidate when neither has changed is semantically correct in app mode too, not merely a patch for static builds.
importtype{ShouldRevalidateFunction}from'@remix-run/react';// A navigation that changes neither pathname nor search must not re-run these loaders.exportconstshouldRevalidate: ShouldRevalidateFunction=({
currentUrl,
nextUrl,
formMethod,
actionResult,
defaultShouldRevalidate,})=>{if(formMethod==null&&actionResult===undefined&¤tUrl.pathname===nextUrl.pathname&¤tUrl.search===nextUrl.search){returnfalse;}returndefaultShouldRevalidate;};
I verified this end to end against the live deploy by intercepting /build/root-X3POM4TE.js and /build/routes/$-N6Q43WQ4.js and appending the export: after the outline click, Back issues zero?_data= requests, logs zero errors, and renders the article normally.
All three modules are required, and a partial fix fails silently. Measured on the deploy:
Patched
?_data= requests on Back
What happens
routes/$ only
?_data=root still fires
Page looks correct, zero console errors — but root's loader data is the HTML string, so useLoaderData<SiteLoader>() at app/root.tsx:201 destructures undefined for theme, config, CONTENT_CDN_PORT, MODE and BASE_URL. staticBuild (root.tsx:211) flips to false and <ContentReload port={undefined} /> (root.tsx:210) mounts on a static deploy. Nothing announces it.
root only
?_data=routes%2F%24 still fires
Application Error, unchanged
all three
none
Correct
Two approaches ruled out by measurement, so nobody re-litigates them. Rendering a plain <a href="#id"> in Outline.tsx instead of the provider Link changes nothing — the click is already a native same-document fragment navigation (I measured zero document requests and a surviving JS context on an outline click), and the failure is on the Back POP. And hardening data.page.frontmatter in meta() only changes which error screen you get: app/components/Page.tsx:21 throws on the next render, so you would see myst's styled ErrorUnhandled page instead of the raw stack. The page is destroyed either way.
Regression coverage must ship in the same PR
The current Playwright harness structurally cannot see this. tests/visual/serve.sh:29 is exec myst start --port "${PORT:-3111}", wired as webServer[0].command at playwright.config.ts:70-75 — that is MODE=app, where ?_data= returns real JSON and the bug does not exist. Merge the shouldRevalidate exports alone and the guard against reintroducing this is zero.
The missing piece is a small new Playwright project that serves a static build: copy tests/visual/fixture, substitute the theme into myst.yml the way serve.sh already does, run myst build --html, and serve _build/html with a plain static file server on its own port. I built exactly this while confirming the bug and it took about five minutes with no new tooling — python3 -m http.server over _build/html reproduced the failure in both Chromium and WebKit. The assertion is one line: after click-anchor-then-Back, the page title is not Application Error!. This is a cheap, permanent guard against a class of static-only defects the repo has now hit three times (see #138, #150).
Side effects
No CSS, no DOM, no rendered output changes, so no visual baselines move — neither the -darwin nor the -linux snapshot set needs refreshing.
In MODE=app, a repeated click on the currently-active link no longer refetches. That is the intended behaviour for these loaders, which are pure functions of pathname and search, but it is a behaviour change and deserves a CHANGELOG line.
One consequence worth naming in the PR rather than discovering later: the search index fetcher calls fetcher.load(withBaseurl('/myst.search.json', baseURL)) at app/components/toolbar/Search.tsx:399-403, and there is no dedicated route for that path in app/routes/, so it matches the $.tsx splat. Adding shouldRevalidate to routes/$ therefore also governs that fetcher's revalidation. The effect looks benign-to-desirable — Search.tsx:397 already carries // TODO: this reloads every time the search box is opened. — but it should be checked, not assumed.
Upstream
The shouldRevalidate exports are a workaround and stay here. The underlying design flaw belongs at jupyter-book/myst-theme: Document's staticBuild branch swaps the Link provider to reloadDocument but leaves a loader-bearing data router hydrated, so the client can still initiate loader fetches that a static host cannot answer. A staticBuild document arguably should not render <Scripts /> with live loaders at all, or should default every route to shouldRevalidate: () => false. This warrants an UPSTREAM-CANDIDATES.yml entry in the same PR, status: pending, per the local-first resolution recorded in #145. I searched jupyter-book/myst-theme and found no existing upstream issue for it.
Note that #28 does not cover this and would not repair it. React Router 7 keeps the same hash-removal semantics in isHashChangeOnly, and a static build would still hydrate a loader-bearing data router, so the fix is needed independently of the migration and must survive it.
Related defect: ↑ Top is a full document reload
Found while measuring this and not previously filed. The back-to-top control resolves to a different pathname from every other in-page link on the same page, so it leaves the document instead of scrolling.
Control
Rendered href
Document requests on click
JS context survives?
Outline entry
/python-by-example/#overview
none
yes — in-page scroll
↑ Top
/python-by-example#top
GET /python-by-example → 301 → GET /python-by-example/ → 200
no — full reload
Both come from the same useLinkProvider()Link with a to="#…" value (app/components/Outline.tsx:26 and :63), which looks contradictory until you look at when each is rendered. The SSR'd HTML contains only href="/python-by-example#top" and zero /python-by-example/# hrefs — the outline list does not exist server-side at all, because useHeaders('main h2', 3) (Outline.tsx:46) reads the headings out of the DOM after mount. So the outline's hrefs are resolved against the hydrated pathname /python-by-example/ (with the trailing slash) while BackToTop was resolved server-side against /python-by-example (without), and the SSR'd attribute survives hydration. Netlify then 301s the shorter form. I confirmed all of this on the deploy, including that a window marker set before clicking ↑ Top is gone afterwards while it survives an outline click.
The cost is a needless full page load — losing scroll position, any Thebe/JupyterLite session state, and about a second — on the single control whose whole purpose is to move within the page. It is also the reason back-to-top happens not to trigger the Back bug on lecture pages, while on the landing page, where the paths do match (/#top), it does; I reproduced that too.
Sphinx gets this right for the obvious reason: its back-to-top is a bare href="#top" with no path component at all, against the same id="top" element the theme already renders. Making ↑ Top a plain <a href="#top"> is one line, matches Sphinx, and is simply correct — a fragment link to the current document never needs client routing or path resolution. It is small enough to fold into this PR, and doing so keeps the anchor-navigation behaviour consistent in one change.
Predicate shape. The verified guard opts out whenever pathname and search are unchanged and there is no submission. A narrower alternative would opt out only when MODE === 'static', but a module-level shouldRevalidate export has no clean access to loader data and would need a global read. I recommend the broader predicate: it is simpler, and it is semantically correct for these loaders in both modes.
Whether the ↑ Top href change rides along or gets its own issue. I recommend folding it in — same subsystem, same file, no baseline impact.
Where it sits
A hard cutover gate: this is not a polish item, it is the one thing in the review set that makes deployed pages unusable. It is deliberately not filed under #92 — the throw happens to surface inside a meta() export, but this is a routing and data-revalidation bug, not a metadata gap — and it is not a duplicate of #28 or of #126 (I measured zero console errors on initial load of both the landing page and a lecture page on this deploy; the errors appear only on the Back navigation).
Conflicts are textual only, and small. #174 edits all three of app/root.tsx, app/routes/$.tsx and app/routes/_index.tsx, and #175 edits app/root.tsx; neither contains the string shouldRevalidate, and #174's routes/$.tsx hunk touches only the getMetaTagsForArticle return, leaving both the if (!data) guard and the throwing line alone. Whichever lands second takes a trivial rebase. No binary conflicts with #171 or #174, since no baselines move.
Next action: open a PR with the three shouldRevalidate exports, the static-serve Playwright project and its click-anchor-then-Back assertion, the ↑ Top href change, a CHANGELOG entry, and the UPSTREAM-CANDIDATES.yml row for the myst-theme design flaw.
Found in the design review of the v2.5.0 deploy (
6d773b63c,https://6a9b93b9108dd4beb6bebadc--epic-agnesi-957267.netlify.app), item 5. On amyst build --htmldeploy, clicking any "On this page" entry — or any in-content cross-reference — and then pressing the browser Back button once destroys the page: the whole document is replaced by a bare white screen reading "Application Error" with a JavaScript stack trace under it. Every lecture page on a static host is one Back press away from this, including the landing page. It is not a Netlify quirk; it needs only thatGET /page/?_data=rootreturn the page's HTML instead of loader JSON, which is what every static file server does.Reproduction
Open any page on a static build, click an outline entry, press Back once. Measured four ways:
?_data=responses on Back/python-by-example/static?_data=rootand?_data=routes%2F%24, both200 text/html; charset=UTF-8<title>Application Error!</title>, body is the stackpython3 -m http.serverovertests/visual/fixturebuilt withmyst build --htmlstatic/?_data=rootand/?_data=routes%2F_index, both200 text/htmlstaticmyst startapp200 application/json; charset=utf-8The last row is the point: the bug exists only in static builds, and it is reproducible on the repo's own fixture with no Netlify, no CDN and no redirects involved.
The thrown error differs by route, and both strings need to be in anyone's search:
routes/$)app/routes/$.tsx:23—const page: PageLoader['frontmatter'] = data.page.frontmatter;TypeError: Cannot read properties of undefined (reading 'frontmatter')/build/routes/$-N6Q43WQ4.jsroutes/_index)app/routes/_index.tsx:23—title: config?.title ?? project.title,TypeError: Cannot read properties of undefined (reading 'title')/build/routes/_index-XSHLEO5O.jsThe screen is unstyled because two boundaries fail in sequence.
app/root.tsx:25exportsAppErrorBoundaryfrom@myst-theme/site, and it does catch the throw — butAppErrorBoundary(node_modules/@myst-theme/site/src/pages/Root.tsx:167-178) renders<Document>again,Documentrenders<Meta/>, and<Meta/>re-runs the same throwingmeta(). The boundary therefore throws during its own render and Remix falls back toRemixRootDefaultErrorBoundary(node_modules/@remix-run/react/dist/errorBoundaries.js:89,98): a bare<html>with<title>Application Error!</title>, an<h1>Application Error</h1>and a<pre>oferror.stack. That is exactly what I measured — the page title after Back is literallyApplication Error!.Mechanism
myst build --htmlruns the theme withMODE=static.app/root.tsx:201reads it from the root loader andapp/root.tsx:211passesstaticBuild={MODE === 'static'}to upstreamDocument.Document(@myst-theme/site1.3.0,src/pages/Root.tsx:60-69) responds by swapping the link provider to<Link reloadDocument>— but<Scripts />atsrc/pages/Root.tsx:150renders unconditionally, so the Remix data router hydrates in static builds too. The deployed/build/manifest-DADB2520.jscarries"hasLoader":trueonroot,routes/$androutes/_index, so there are live loaders on a host that cannot serve them.React Router does short-circuit hash-only navigations —
router.js:1658guardsstartNavigationwithisHashChangeOnly(state.location, location)— butisHashChangeOnly(node_modules/@remix-run/router/dist/router.js:3673-3690) deliberately returns false when the hash is removed, which is exactly what Back from#anchorto the bare URL does. The function's own comment says why: "If the hash is removed the browser will re-perform a request to the server". So the navigation is not short-circuited, andgetMatchesToLoadthen setsdefaultShouldRevalidate = trueon the branch atrouter.js:3126("Clicked the same link, resubmitted a GET form") becausecurrentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search.No route module in the theme exports
shouldRevalidate—grep -rn shouldRevalidate app/on6d773b63creturns nothing — socreateShouldRevalidate(node_modules/@remix-run/react/dist/routes.js:110-128) falls through toarg.defaultShouldRevalidateat line 126 and the loaders re-run. The client fetches?_data=rootand?_data=routes%2F%24; the static host answers200 text/html;callLoaderOrActiontakes the non-JSON branchdata = await result.text()(router.js:3391-3395); and the loader value for both routes becomes the page's HTML source as a string. The guard atapp/routes/$.tsx:19(if (!data) return [];) passes, because a non-empty string is truthy, and the next property read throws.app/components/Page.tsx:21((data.page.frontmatter as any)?.site) would throw next even ifmeta()were hardened.Sphinx baseline
Measured, not assumed.
https://python-programming.quantecon.org/python_by_example.htmlhas no client router at all (window.__remixContextis absent), its in-page anchors are barehref="#overview"fragments, and clicking one then pressing Back returns to the intact article. There is no Sphinx value to match here and no design question to answer — the parity target is simply that Back works, so this is a straight regression to repair rather than a divergence to decide.Fix
Export a
shouldRevalidatefrom the three route modules that render UI —app/root.tsx,app/routes/$.tsxandapp/routes/_index.tsx.createShouldRevalidateprefers a route module's own function (@remix-run/react/dist/routes.js:123-124), unconditionally, once the module exports it.Why this is the right guard, and why it is safe in both modes: a static build (
myst build --html) still hydrates a live data router, but its loaders are only servable by a running Remix server, so on a static host?_data=…returns the page HTML,@remix-run/routerturns that into a string loader value (router.js:3391-3395), and everydata.pageread then throws. React Router short-circuits hash-only navigations except when the hash is removed (isHashChangeOnly,router.js:3687) — exactly what Back from#anchordoes — so that POP would otherwise re-run the loaders. These three loaders are pure functions of pathname and search, so declining to revalidate when neither has changed is semantically correct inappmode too, not merely a patch for static builds.I verified this end to end against the live deploy by intercepting
/build/root-X3POM4TE.jsand/build/routes/$-N6Q43WQ4.jsand appending the export: after the outline click, Back issues zero?_data=requests, logs zero errors, and renders the article normally.All three modules are required, and a partial fix fails silently. Measured on the deploy:
?_data=requests on Backroutes/$only?_data=rootstill firesuseLoaderData<SiteLoader>()atapp/root.tsx:201destructuresundefinedfortheme,config,CONTENT_CDN_PORT,MODEandBASE_URL.staticBuild(root.tsx:211) flips tofalseand<ContentReload port={undefined} />(root.tsx:210) mounts on a static deploy. Nothing announces it.rootonly?_data=routes%2F%24still firesTwo approaches ruled out by measurement, so nobody re-litigates them. Rendering a plain
<a href="#id">inOutline.tsxinstead of the providerLinkchanges nothing — the click is already a native same-document fragment navigation (I measured zero document requests and a surviving JS context on an outline click), and the failure is on the Back POP. And hardeningdata.page.frontmatterinmeta()only changes which error screen you get:app/components/Page.tsx:21throws on the next render, so you would see myst's styledErrorUnhandledpage instead of the raw stack. The page is destroyed either way.Regression coverage must ship in the same PR
The current Playwright harness structurally cannot see this.
tests/visual/serve.sh:29isexec myst start --port "${PORT:-3111}", wired aswebServer[0].commandatplaywright.config.ts:70-75— that isMODE=app, where?_data=returns real JSON and the bug does not exist. Merge theshouldRevalidateexports alone and the guard against reintroducing this is zero.The missing piece is a small new Playwright project that serves a static build: copy
tests/visual/fixture, substitute the theme intomyst.ymlthe wayserve.shalready does, runmyst build --html, and serve_build/htmlwith a plain static file server on its own port. I built exactly this while confirming the bug and it took about five minutes with no new tooling —python3 -m http.serverover_build/htmlreproduced the failure in both Chromium and WebKit. The assertion is one line: after click-anchor-then-Back, the page title is notApplication Error!. This is a cheap, permanent guard against a class of static-only defects the repo has now hit three times (see #138, #150).Side effects
No CSS, no DOM, no rendered output changes, so no visual baselines move — neither the
-darwinnor the-linuxsnapshot set needs refreshing.In
MODE=app, a repeated click on the currently-active link no longer refetches. That is the intended behaviour for these loaders, which are pure functions of pathname and search, but it is a behaviour change and deserves a CHANGELOG line.One consequence worth naming in the PR rather than discovering later: the search index fetcher calls
fetcher.load(withBaseurl('/myst.search.json', baseURL))atapp/components/toolbar/Search.tsx:399-403, and there is no dedicated route for that path inapp/routes/, so it matches the$.tsxsplat. AddingshouldRevalidatetoroutes/$therefore also governs that fetcher's revalidation. The effect looks benign-to-desirable —Search.tsx:397already carries// TODO: this reloads every time the search box is opened.— but it should be checked, not assumed.Upstream
The
shouldRevalidateexports are a workaround and stay here. The underlying design flaw belongs at jupyter-book/myst-theme:Document'sstaticBuildbranch swaps the Link provider toreloadDocumentbut leaves a loader-bearing data router hydrated, so the client can still initiate loader fetches that a static host cannot answer. AstaticBuilddocument arguably should not render<Scripts />with live loaders at all, or should default every route toshouldRevalidate: () => false. This warrants anUPSTREAM-CANDIDATES.ymlentry in the same PR,status: pending, per the local-first resolution recorded in #145. I searched jupyter-book/myst-theme and found no existing upstream issue for it.Note that #28 does not cover this and would not repair it. React Router 7 keeps the same hash-removal semantics in
isHashChangeOnly, and a static build would still hydrate a loader-bearing data router, so the fix is needed independently of the migration and must survive it.Related defect:
↑ Topis a full document reloadFound while measuring this and not previously filed. The back-to-top control resolves to a different pathname from every other in-page link on the same page, so it leaves the document instead of scrolling.
/python-by-example/#overview↑ Top/python-by-example#topGET /python-by-example→301→GET /python-by-example/→200Both come from the same
useLinkProvider()Linkwith ato="#…"value (app/components/Outline.tsx:26and:63), which looks contradictory until you look at when each is rendered. The SSR'd HTML contains onlyhref="/python-by-example#top"and zero/python-by-example/#hrefs — the outline list does not exist server-side at all, becauseuseHeaders('main h2', 3)(Outline.tsx:46) reads the headings out of the DOM after mount. So the outline's hrefs are resolved against the hydrated pathname/python-by-example/(with the trailing slash) whileBackToTopwas resolved server-side against/python-by-example(without), and the SSR'd attribute survives hydration. Netlify then 301s the shorter form. I confirmed all of this on the deploy, including that awindowmarker set before clicking↑ Topis gone afterwards while it survives an outline click.The cost is a needless full page load — losing scroll position, any Thebe/JupyterLite session state, and about a second — on the single control whose whole purpose is to move within the page. It is also the reason back-to-top happens not to trigger the Back bug on lecture pages, while on the landing page, where the paths do match (
/#top), it does; I reproduced that too.Sphinx gets this right for the obvious reason: its back-to-top is a bare
href="#top"with no path component at all, against the sameid="top"element the theme already renders. Making↑ Topa plain<a href="#top">is one line, matches Sphinx, and is simply correct — a fragment link to the current document never needs client routing or path resolution. It is small enough to fold into this PR, and doing so keeps the anchor-navigation behaviour consistent in one change.Options
shouldRevalidateexports plus the static-serve Playwright project plus the↑ Tophref, as one PR, ahead of feat: QuantEcon code-token palette and seoul256 text colours (Phase 3) #171 and feat: language switcher, hreflang, RTL and translator credit (Phases 4–5) #174. Recommended. It is the only functional defect in the review set, it is a hard cutover blocker, it moves no baselines, and the conflict cost is three module-level exports.shouldRevalidateexports now and file the static-serve test project as a follow-up. Cheaper this week, but the fix is then unguarded and the next static-only regression finds us the same way Self-hosted stylesheet assets 404 in static builds: url() points at /myst_assets_folder, which myst build --html never creates #138 and Residual #138: route-level stylesheets outside _assets/ still carry absolute /myst_assets_folder asset URLs #150 did.Decisions needed
pathnameandsearchare unchanged and there is no submission. A narrower alternative would opt out only whenMODE === 'static', but a module-levelshouldRevalidateexport has no clean access to loader data and would need a global read. I recommend the broader predicate: it is simpler, and it is semantically correct for these loaders in both modes.↑ Tophref change rides along or gets its own issue. I recommend folding it in — same subsystem, same file, no baseline impact.Where it sits
A hard cutover gate: this is not a polish item, it is the one thing in the review set that makes deployed pages unusable. It is deliberately not filed under #92 — the throw happens to surface inside a
meta()export, but this is a routing and data-revalidation bug, not a metadata gap — and it is not a duplicate of #28 or of #126 (I measured zero console errors on initial load of both the landing page and a lecture page on this deploy; the errors appear only on the Back navigation).Conflicts are textual only, and small. #174 edits all three of
app/root.tsx,app/routes/$.tsxandapp/routes/_index.tsx, and #175 editsapp/root.tsx; neither contains the stringshouldRevalidate, and #174'sroutes/$.tsxhunk touches only thegetMetaTagsForArticlereturn, leaving both theif (!data)guard and the throwing line alone. Whichever lands second takes a trivial rebase. No binary conflicts with #171 or #174, since no baselines move.Next action: open a PR with the three
shouldRevalidateexports, the static-serve Playwright project and its click-anchor-then-Back assertion, the↑ Tophref change, a CHANGELOG entry, and theUPSTREAM-CANDIDATES.ymlrow for the myst-theme design flaw.