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
Part of #147 (parity study 2026-09-11 — content correctness).
Revised in place 2026-09-11 after the maintainer review. The scope now covers every head link that assumes the domain root: the favicon, /myst-theme.css and a full-URL og:image. og:url is built by the same function as the canonical link. The revision is recorded in the comments.
Problem
Two <head> links differ from the Sphinx lecture sites. The missing canonical link affects every page of every site. The favicon 404 affects every page of any site served under a sub-path.
No canonical link. Sphinx's base layout emits <link rel="canonical"> on every page whenever html_baseurl is set, and the study found html.baseurl set in all 21 lecture _config.yml files. The theme emits no canonical link on any page. It already receives the site's public URL as the site_url option, but uses it only for og:url. Upstream @myst-theme/site still marks canonical as a TODO.
The favicon link 404s on sub-path sites. The root route declares the icon as the root-absolute /favicon.ico. The mystmd static export writes favicon.ico at the build root, but the only URLs it rewrites for the base URL are /myst_assets_folder/ ones. So on a BASE_URL build the link points at the domain root instead of the site. lecture-wasm deploys this way today. Its pages emit <link rel="icon" href="/favicon.ico"/>, which points at https://quantecon.github.io/favicon.ico (HTTP 404), while the icon itself is served at https://quantecon.github.io/lecture-wasm/favicon.ico (HTTP 200). The Sphinx sites use a relative _static/lectures-favicon.ico href and are unaffected.
Gap
Lecture sites affected
No canonical link
Every page of every lecture site after cutover (all 21 lecture _config.yml files set html.baseurl, per the study)
Favicon 404
Every page of any site built with BASE_URL. That is lecture-wasm now (theme v2.7.0). lecture-python-programming.fa, .fr and .zh-cn and lecture-python.zh-cn are hosted under quantecon.github.io sub-paths today: their live pages declare canonical URLs there, for example https://quantecon.github.io/lecture-python-programming.fa/intro.html (verified 2026-09-11). They would hit the same 404 if built with BASE_URL at cutover (read from code)
Evidence:
Claim
Where
Sphinx emits canonical from pageurl and the icon from favicon_url
Sphinx 7.4.7 themes/basic/layout.html lines 118-120 and 126-128. pydata-sphinx-theme 0.15.4 layout.html line 7 extends that layout, and the book theme extends pydata. The book theme notes that Sphinx sets pageurl when html_baseurl is given (__init__.py L624-L625)
Canonical link. In app/seo.ts, derive a canonical href from site_url: the siteOrigin origin plus the page path with the base URL in front, which is how og:url is assembled today (app/routes/$.tsx L36-L49). Return it from both page routes' meta as a tagName: 'link' descriptor. hreflangLinks already uses that shape (app/i18n.ts L232-L246, app/routes/$.tsx L63-L64). Remove any base already on the path before adding it, as stripBaseurl does for the hreflang links (app/i18n.ts L203-L210). The browser router is created without a basename (@remix-run/react 1.17.1, dist/browser.js lines 63-73), so on the client the location already carries the base, and prefixing it again would repeat it (read from code). Emit nothing when site_url is unset, as Sphinx does without html_baseurl.
Favicon. Make the icon href include the base URL. The root links() cannot do this as written. In Remix 1.17 it takes no arguments (@remix-run/react 1.17.1, dist/routeModules.d.ts lines 51-53), and BASE_URL reaches the app only through the root loader (app/root.tsx L193-L206). The route meta functions can read that loader's data (the page routes already do, app/routes/$.tsx L36-L39). So can the local Document head (app/components/Document.tsx L114-L146), which receives it at app/root.tsx L238.
The theme stylesheet link. Give /myst-theme.css (app/root.tsx L181) the base URL the same way as the favicon. On lecture-wasm the root-absolute link returns 404, while the copy under the base URL returns 200 (verified 2026-09-11). That copy is only a placeholder today, so nothing visible is lost yet.
A full-URL og:image. Social previews need an absolute image URL, so when site_url is set, prefix the image path with its origin (app/seo.ts L96-L103). Live lecture-wasm emits the root-relative /lecture-wasm/build/graph-….png.
One URL builder. Build og:url with the same function as the canonical href, so the home-page rule (the site root, not the index slug) and the trailing slash apply to both.
Three implementation constraints:
The home page's path on a base-URL build. When a base URL is set, mystmd renders the site's root index.html by requesting /<index slug> (index.ts L44-L53), so the home page's render-time path is the index slug, not /. On lecture-wasm the index is intro (myst.yml L36-L37). The live home page's title has the article route's "page - site" form, and https://quantecon.github.io/lecture-wasm/intro/ returns 404 (verified 2026-09-11). The home page's canonical must therefore be the site root. og:url is built from the same path, so it would carry the same unreachable URL once a sub-path site sets site_url (read from code).
Trailing slash. The export requests each page without a trailing slash and writes it as <slug>/index.html (index.ts L54-L59). GitHub Pages redirects the slashless form: https://quantecon.github.io/lecture-wasm/short-path returns 301 to /lecture-wasm/short-path/ (verified 2026-09-11). A canonical taken straight from the render-time path would name a redirect.
Error pages. Today they get the icon from the root links(), the only route links that also apply when the root error boundary renders (app/root.tsx L172-L176). That boundary is upstream's AppErrorBoundary (app/root.tsx L27), which uses upstream's own Document with no base URL (@myst-theme/site 1.3.0, src/pages/Root.tsx lines 167-178). If the icon moves into the page routes' meta, the article route's own ErrorBoundary pages lose it too (app/routes/$.tsx L88-L90). Remix calls the failing route's meta with that route's loader data, which an erroring route has none of (@remix-run/react 1.17.1, dist/components.js lines 504-511), and that meta then returns nothing (app/routes/$.tsx L24-L25).
Acceptance criteria
With site_url set, every page's server-rendered HTML has exactly one <link rel="canonical">, whose href is that page's absolute public URL.
On a static build deployed with BASE_URL (lecture-wasm), the home page's canonical is the site root (https://quantecon.github.io/lecture-wasm/). Every other page's canonical contains the base path exactly once and returns HTTP 200 without a redirect.
With site_url unset, no canonical link is emitted.
On a static build with BASE_URL set, the icon link's href includes the base URL and returns HTTP 200. On a build without BASE_URL it is still /favicon.ico.
Error pages still declare an icon link, both those from the root error boundary and those from the article route's own ErrorBoundary.
On a static build with BASE_URL set, the myst-theme.css link includes the base URL and returns HTTP 200.
With site_url set, og:image is an absolute URL.
og:url and the canonical href come from the same function and agree on every page, including the home page of a base-URL build.
tests/unit/seo.test.mjs covers the canonical href in these cases: with and without a base URL, with a path that already carries the base, on the home page of a base-URL build, and with site_url unset. The social-meta test (tests/visual/theme.spec.ts L463-L480) asserts the canonical link.
The site_url docs mention the canonical link.
Context
Source: the 2026-09-11 book-theme parity study, row F086 ("Page title and inherited Sphinx head links", verdict partial, relevance high). The report's verification addendum lists it as confirmed on live pages or in code. Report section 3.3 (Metadata, SEO and i18n) recommends emitting the canonical link from site_url and making the favicon href base-URL-aware before cutover.
Not in this issue (same row): rel next/prev/index/search links, the " - " title separator where Sphinx uses an em dash, and OpenSearch (no lecture config sets it). F088's root-relative og:image is now in scope (Proposed change 5); its missing theme-color meta is not.
Same root-absolute pattern, now in scope (Proposed change 4):app/root.tsx L183 links /myst-theme.css. On lecture-wasm that points at https://quantecon.github.io/myst-theme.css (HTTP 404), while https://quantecon.github.io/lecture-wasm/myst-theme.css returns 200 (verified 2026-09-11). lecture-wasm's copy is only the placeholder /* No Custom Stylesheet Provided */, so it loses nothing today. A search of this repo's issues for myst-theme.css found none tracking it.
Part of #147 (parity study 2026-09-11 — content correctness).
Revised in place 2026-09-11 after the maintainer review. The scope now covers every head link that assumes the domain root: the favicon,
/myst-theme.cssand a full-URLog:image.og:urlis built by the same function as the canonical link. The revision is recorded in the comments.Problem
Two
<head>links differ from the Sphinx lecture sites. The missing canonical link affects every page of every site. The favicon 404 affects every page of any site served under a sub-path.No canonical link. Sphinx's base layout emits
<link rel="canonical">on every page wheneverhtml_baseurlis set, and the study foundhtml.baseurlset in all 21 lecture_config.ymlfiles. The theme emits no canonical link on any page. It already receives the site's public URL as thesite_urloption, but uses it only forog:url. Upstream@myst-theme/sitestill marks canonical as a TODO.The favicon link 404s on sub-path sites. The root route declares the icon as the root-absolute
/favicon.ico. The mystmd static export writesfavicon.icoat the build root, but the only URLs it rewrites for the base URL are/myst_assets_folder/ones. So on aBASE_URLbuild the link points at the domain root instead of the site. lecture-wasm deploys this way today. Its pages emit<link rel="icon" href="/favicon.ico"/>, which points at https://quantecon.github.io/favicon.ico (HTTP 404), while the icon itself is served at https://quantecon.github.io/lecture-wasm/favicon.ico (HTTP 200). The Sphinx sites use a relative_static/lectures-favicon.icohref and are unaffected._config.ymlfiles sethtml.baseurl, per the study)BASE_URL. That is lecture-wasm now (theme v2.7.0). lecture-python-programming.fa, .fr and .zh-cn and lecture-python.zh-cn are hosted under quantecon.github.io sub-paths today: their live pages declare canonical URLs there, for example https://quantecon.github.io/lecture-python-programming.fa/intro.html (verified 2026-09-11). They would hit the same 404 if built withBASE_URLat cutover (read from code)Evidence:
pageurland the icon fromfavicon_urlthemes/basic/layout.htmllines 118-120 and 126-128. pydata-sphinx-theme 0.15.4layout.htmlline 7 extends that layout, and the book theme extends pydata. The book theme notes that Sphinx setspageurlwhenhtml_baseurlis given (__init__.pyL624-L625)<link rel="canonical" href="https://python-programming.quantecon.org/about_py.html" />, fromhtml.baseurl. https://python.quantecon.org/intro.html does the samesite_urlfeeds onlyog:url@myst-theme/site1.3.0,src/seo/meta.tsline 12 (// TODO: canonical)favicon.icoat the build root and rewrites only/myst_assets_folder/URLsBASE_URLset to/<repository name>, i.e./lecture-wasmProposed change
app/seo.ts, derive a canonical href fromsite_url: thesiteOriginorigin plus the page path with the base URL in front, which is howog:urlis assembled today (app/routes/$.tsx L36-L49). Return it from both page routes'metaas atagName: 'link'descriptor.hreflangLinksalready uses that shape (app/i18n.ts L232-L246, app/routes/$.tsx L63-L64). Remove any base already on the path before adding it, asstripBaseurldoes for the hreflang links (app/i18n.ts L203-L210). The browser router is created without a basename (@remix-run/react1.17.1,dist/browser.jslines 63-73), so on the client the location already carries the base, and prefixing it again would repeat it (read from code). Emit nothing whensite_urlis unset, as Sphinx does withouthtml_baseurl.links()cannot do this as written. In Remix 1.17 it takes no arguments (@remix-run/react1.17.1,dist/routeModules.d.tslines 51-53), andBASE_URLreaches the app only through the root loader (app/root.tsx L193-L206). The routemetafunctions can read that loader's data (the page routes already do, app/routes/$.tsx L36-L39). So can the local Document head (app/components/Document.tsx L114-L146), which receives it at app/root.tsx L238.site_urldescriptions to say it also sets the canonical link: docs/configuration.md L19, README.md L241 and template.yml L71-L76./myst-theme.css(app/root.tsx L181) the base URL the same way as the favicon. On lecture-wasm the root-absolute link returns 404, while the copy under the base URL returns 200 (verified 2026-09-11). That copy is only a placeholder today, so nothing visible is lost yet.og:image. Social previews need an absolute image URL, so whensite_urlis set, prefix the image path with its origin (app/seo.ts L96-L103). Live lecture-wasm emits the root-relative/lecture-wasm/build/graph-….png.og:urlwith the same function as the canonical href, so the home-page rule (the site root, not the index slug) and the trailing slash apply to both.Three implementation constraints:
index.htmlby requesting/<index slug>(index.ts L44-L53), so the home page's render-time path is the index slug, not/. On lecture-wasm the index isintro(myst.yml L36-L37). The live home page's title has the article route's "page - site" form, and https://quantecon.github.io/lecture-wasm/intro/ returns 404 (verified 2026-09-11). The home page's canonical must therefore be the site root.og:urlis built from the same path, so it would carry the same unreachable URL once a sub-path site setssite_url(read from code).<slug>/index.html(index.ts L54-L59). GitHub Pages redirects the slashless form: https://quantecon.github.io/lecture-wasm/short-path returns 301 to/lecture-wasm/short-path/(verified 2026-09-11). A canonical taken straight from the render-time path would name a redirect.links(), the only route links that also apply when the root error boundary renders (app/root.tsx L172-L176). That boundary is upstream'sAppErrorBoundary(app/root.tsx L27), which uses upstream's own Document with no base URL (@myst-theme/site1.3.0,src/pages/Root.tsxlines 167-178). If the icon moves into the page routes'meta, the article route's ownErrorBoundarypages lose it too (app/routes/$.tsx L88-L90). Remix calls the failing route'smetawith that route's loader data, which an erroring route has none of (@remix-run/react1.17.1,dist/components.jslines 504-511), and thatmetathen returns nothing (app/routes/$.tsx L24-L25).Acceptance criteria
site_urlset, every page's server-rendered HTML has exactly one<link rel="canonical">, whose href is that page's absolute public URL.BASE_URL(lecture-wasm), the home page's canonical is the site root (https://quantecon.github.io/lecture-wasm/). Every other page's canonical contains the base path exactly once and returns HTTP 200 without a redirect.site_urlunset, no canonical link is emitted.BASE_URLset, the icon link's href includes the base URL and returns HTTP 200. On a build withoutBASE_URLit is still/favicon.ico.ErrorBoundary.BASE_URLset, themyst-theme.csslink includes the base URL and returns HTTP 200.site_urlset,og:imageis an absolute URL.og:urland the canonical href come from the same function and agree on every page, including the home page of a base-URL build.tests/unit/seo.test.mjscovers the canonical href in these cases: with and without a base URL, with a path that already carries the base, on the home page of a base-URL build, and withsite_urlunset. Thesocial-metatest (tests/visual/theme.spec.ts L463-L480) asserts the canonical link.site_urldocs mention the canonical link.Context
Source: the 2026-09-11 book-theme parity study, row F086 ("Page title and inherited Sphinx head links", verdict partial, relevance high). The report's verification addendum lists it as confirmed on live pages or in code. Report section 3.3 (Metadata, SEO and i18n) recommends emitting the canonical link from
site_urland making the favicon href base-URL-aware before cutover.Not in this issue (same row): rel next/prev/index/search links, the " - " title separator where Sphinx uses an em dash, and OpenSearch (no lecture config sets it). F088's root-relative
og:imageis now in scope (Proposed change 5); its missingtheme-colormeta is not.Same root-absolute pattern, now in scope (Proposed change 4): app/root.tsx L183 links
/myst-theme.css. On lecture-wasm that points at https://quantecon.github.io/myst-theme.css (HTTP 404), while https://quantecon.github.io/lecture-wasm/myst-theme.css returns 200 (verified 2026-09-11). lecture-wasm's copy is only the placeholder/* No Custom Stylesheet Provided */, so it loses nothing today. A search of this repo's issues formyst-theme.cssfound none tracking it.Related: Phase 6 — Meta/SEO + notebook-output polish #92 (the Meta/SEO phase whose
app/seo.tsbuildsog:urlfromsite_url). See Every undeclared site.options key is dropped by the CLI, so twitter and launch_* never reach the theme #173 and fix(options): declare every site option the theme reads, so they reach it #194: thefaviconoption now reaches the theme, but the link href is still root-absolute. A search of this repo's issues for "canonical" and "favicon" on 2026-09-11 found none tracking either gap.Consumer: QuantEcon/lecture-wasm, pinned to theme v2.7.0.
Migration: canonical links and
og:urlneedsite_urlon every site. The checklist row is on Lecture repos moving off the Sphinx theme have no checklist of what myst.yml and CI must now set #209 (Sphinxhtml.baseurl→site.options.site_url). lecture-wasm setssite_urlin the PR that bumps it to the release carrying this issue.Old URLs at cutover: the new canonical URLs differ from the Sphinx ones (
about_py.htmlbecomesabout-py/). Redirects from the old URLs are The site build cannot redirect old page URLs, so every link to a Sphinx lecture page breaks at the move to mystmd mystmd#113.