Skip to content

Pages emit no rel=canonical, and the favicon link 404s on sites served under a sub-path #207

Description

@mmcky

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)
A live Sphinx page has one (verified 2026-09-11) https://python-programming.quantecon.org/about_py.html emits <link rel="canonical" href="https://python-programming.quantecon.org/about_py.html" />, from html.baseurl. https://python.quantecon.org/intro.html does the same
site_url feeds only og:url app/seo.ts L91-L97, template.yml L71-L76, docs/configuration.md L19
Upstream has no canonical support @myst-theme/site 1.3.0, src/seo/meta.ts line 12 (// TODO: canonical)
The icon href is root-absolute app/root.tsx L166-L171
The export writes favicon.ico at the build root and rewrites only /myst_assets_folder/ URLs index.ts L77-L82, index.ts L125
lecture-wasm builds main with BASE_URL set to /<repository name>, i.e. /lecture-wasm ci.yml L59-L65; theme pinned at myst.yml L120-L122
Live lecture-wasm has no canonical link and a 404 icon (verified 2026-09-11) https://quantecon.github.io/lecture-wasm/short-path/

Proposed change

  1. 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.
  2. 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.
  3. Update the site_url descriptions to say it also sets the canonical link: docs/configuration.md L19, README.md L241 and template.yml L71-L76.
  4. 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.
  5. 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.
  6. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is wrong or broken in a lecture or build

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions