fix(seo): publish the URLs the host actually serves - #172
Open
Batuis wants to merge 3 commits into
Open
Conversation
Pages are directories on disk, and GitHub Pages answers a directory request with a 301 to the slashed form. It cannot be turned off. Every URL the site published named the UNSLASHED form, so all eighteen were addresses it redirects rather than addresses it serves: sitemap https://stabileo.com/en/blog host 301 -> https://stabileo.com/en/blog/ and that page <link rel="canonical" href=".../en/blog"> A canonical pointing at a URL that redirects back to the page declaring it, and an hreflang set built the same way. Google resolves the chain in practice; what it says about the language links is sharper — the canonical and the hreflang have to agree or it may ignore the hreflang, which is the three-language linkage the whole prefix scheme exists to express. `publicHref` now ends every path in a slash, and the sitemap, the canonical, the hreflang set and every internal link follow from it. `parsePublicPath` already read both forms, so links people already hold keep working; they take the one redirect they always did. The root language handoff sends '/es/' rather than '/es', which saves a Spanish or Portuguese visitor a second redirect that was ours to avoid. Also here: a gate that asserts the artifact, not the intention. For every <loc> in the sitemap it requires a real file at that exact path, that the page declares itself canonical at the address it is served from, and that every hreflang target is itself served rather than redirected. It is checked on disk rather than over HTTP on purpose: `vite preview` serves both forms happily, so an HTTP test would have passed on the very defect that reached production. The first draft used `existsSync`, which answers true for a directory — precisely what gets redirected — and passed when the fix was reverted. Caught by running that control; it now requires a file.
Twenty-seven assertions encoded the unslashed form, anchored with $, so they
failed the moment the URLs became the ones the host serves. Updated rather
than loosened: the anchor is what gives them value, so they gained a slash
and kept the anchor.
Three needed a decision rather than a slash:
- The cold deep link carries no language prefix — the shape links handed
out before the prefixes existed — and App.svelte restores those verbatim
rather than rewriting somebody's saved address. That case now accepts
either form, and a new one covers the PREFIXED handoff, which IS
normalised through publicHref so the bar and the page's canonical agree.
- The back-button case booted on '/pt/blog/x'. In production the host 301s
that before the app loads, so it is a state the site cannot be in; vite
preview serves both, which is what let the test sit in it. It boots on
the slashed address now.
- The prerender helper built its expected canonical without the slash.
Also in App.svelte: a route restored from 404.html's ?route= now goes through
publicHref when it carries a locale, instead of being written back exactly as
it arrived.
CI failed this branch with `browser.newContext: Test ended` on a test that never started. Not an assertion — the browser had stopped being able to open contexts. The suite runs 327 cases through ONE browser (`workers: 1`, `fullyParallel: false`, SwiftShader software GL), chosen so WebGL timing is deterministic. It wedges near the end, and every case added brings the wedge forward: the same failure took two unrelated specs down on the parent branch until a seven-context test was removed from it, and this branch tipped it again with one. So the prefixed `?route=` assertion moves into the cold-deep-link test it belongs with. Same coverage, two navigations in one context instead of two contexts. That is a workaround. The suite is at the edge of what one browser survives and the next test to be written will hit this again; sharding the e2e job is the actual fix and is not this PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #171.
The problem
Pages are directories on disk, and GitHub Pages answers a directory request with a 301 to the slashed form. That cannot be turned off. Every URL the site published named the unslashed form, so all eighteen were addresses it redirects rather than addresses it serves:
A canonical pointing at a URL that redirects back to the page declaring it, and an
hreflangset built the same way.Why it matters, stated honestly
This is not a ranking lever. Google resolves the chain in practice, and nobody moves up a position because of it.
It is worth doing for two narrower reasons:
The Search Console reports we are about to rely on will also be readable: sitemap URLs will appear as indexed pages rather than as "page with redirect".
What changed
publicHrefends every path in a slash; the sitemap, the canonical, the hreflang set and every internal link follow from it.parsePublicPathalready read both forms, so links people already hold keep working — they take the one redirect they always did.The root language handoff now sends
/es/instead of/es, which saves a Spanish or Portuguese visitor a second redirect that was ours to avoid.The gate
A test that asserts the artifact rather than the intention. For every
<loc>in the sitemap it requires a real file at that exact path, that the page declares itself canonical at the address it is served from, and that every hreflang target is itself served rather than redirected.Checked on disk, not over HTTP, on purpose:
vite previewserves both forms happily, so an HTTP test would have passed on the very defect that reached production.The first draft used
existsSync, which answers true for a directory — precisely what gets redirected — so it passed when the fix was reverted. Caught by running that control; it now requires a file.Considered and rejected
Keeping the unslashed URLs by emitting flat files (
en/blog.htmlrather thanen/blog/index.html). It works for posts — GitHub Pages serves/404from404.htmlwith no redirect, verified — but not for/en, where anen.htmlfile and anen/directory would have to coexist and the winner is up to the host. Too fragile to save a slash.Gates
test:unit7,132 ·typecheckat baseline 479 ·check:gate·buildwith prerender ·@smoke+@landing.