Why downloads break on English pages: routes versus static files
A subtle failure on this site affected otherwise valid attachments: a link worked from a Chinese article but acquired an /en/ prefix from its English version. The files lived under the shared /downloads/evidence/ directory; no separate English attachment existed. The label looked correct, but the routing layer treated a file as a localized page.
The walkthrough follows the fix at revision 1a6b177. You need browser developer tools and a basic distinction between source files, build output, and deployment. The goal is not merely an HTTP 200: both languages should retrieve the correct shared file, while an HTML utility should still open as a usable page.
Classify the destination before localizing it
| Destination | URL from an English page | Behavior |
|---|---|---|
| Article | /en/article/react-chinese-ime-onchange | Open the English article |
| Shared ZIP | /downloads/evidence/glb-measurements.zip | Keep shared path; offer download |
| Shared Markdown | /downloads/evidence/react-ime-2026-07.md | Keep shared path; offer download |
| HTML utility | /downloads/evidence/ime-event-logger.html | Keep shared path; open in browser |
Two decisions are independent: whether to localize the URL and whether to request a download. A shared HTML utility needs no /en/ prefix, but forcing every file under downloads to download would prevent its intended in-browser use. A vague attachment flag can hide this distinction and turn a ZIP fix into a utility regression.
A native anchor can still have the wrong URL
LocalizedLink converts internal paths to the active language. Even if a navigation performs a full document load, an already rewritten /en/downloads/... href remains wrong. Switching to a document reload is insufficient; the shared asset must bypass page-path localization as well.
// Reduced from the site's link-block handling.
const isDownload = href.endsWith('.zip') || href.endsWith('.md');
const isSharedFile = href.startsWith('/downloads/');
if (isSharedFile || isDownload) {
return <a href={href} download={isDownload}>{label}</a>;
}
return <LocalizedLink to={href}>{label}</LocalizedLink>;This is a reduced form of the inspected implementation, not a universal URL classifier. It covers the site’s shared /downloads/ paths and its .zip and .md links. A URL ending in .zip?revision=2 does not match endsWith(".zip"). Under /downloads/, it keeps the correct path, but this version does not infer the download attribute from that suffix. Enumerate supported URL forms before extending the rule.
For query strings, mixed-case suffixes, or more file types, parse a pathname for classification while preserving the original href, query, and fragment. Cross-origin links need separate treatment. The download attribute is not an unconditional command for arbitrary remote files: origin restrictions, response headers, and browser settings affect the outcome.
A prerendered site has two link outputs to inspect
The site renders articles through React and through a build-time HTML renderer. Fixing only React can leave an incorrect href in the first response. Fixing only HTML can let React restore the wrong URL after startup. Both renderers must preserve shared paths and agree on download behavior.
How the crawler HTML and the React page are kept in line is in “To crawlers, every page on my site was 46 characters”.Compare the href in View Source or the raw response with the href in Elements after startup. If they disagree, resolve the renderer mismatch before blaming caching. Then click from the English article itself. Pasting the correct asset URL into the address bar proves the file exists, not that the article points to it.
Open the input-event logger used in this case (HTML)Download the IME verification notes used in this case (Markdown)Where these two case files came from is explained in “React meets a Chinese IME”.Example: two gaps in this site's rules, now fixed
On 2026-11-05 the site's code was checked again. The React rule is in src/pages/ArticlePage.tsx, lines 352-376. The prerender rule is in scripts/lib/render-article-html.mjs, lines 61-66.
A script applied both rules to every article link on the site. It covered 101 data files and 284 URL and language pairs. One more file could not be loaded by the script and is not counted.
Before the 2026-11-05 fix, two pairs differed. Both are in src/data/globalChartArticles.ts, lines 67-68. A Chinese article links to /en/v4, with text that promises the full English data page.
Before the fix, the prerender helper localizeHref only prefixed English pages, so a Chinese page kept /en/v4. The React helper localizePath first stripped /en, then built a Chinese URL.
After JavaScript started, the link therefore pointed to /v4. That is a Chinese page: public/v4/index.html declares lang="zh-Hant". The same link opened a different language before and after startup.
The second gap affected no article: a .md file outside /downloads/. The prerendered HTML kept the path and added download. On an English page, React added /en/ and omitted download.
Both gaps were fixed on 2026-11-05. localizePath now keeps a link that already starts with /en (src/i18n/routing.ts, line 28). The localizeHref check now matches it (render-article-html.mjs, line 11). React now uses the same isDownload check (ArticlePage.tsx, line 354).
tests/articleLinkLocale.test.ts runs one set of hrefs through both rules. The test fails if the outputs differ. Check both outputs against one shared case table, and write it as a test.
HTTP 200 is not proof that the file arrived
An SPA host can fall back to the application HTML for an unknown path. A status-only check may therefore report success for a broken attachment. Inspect the content type and body as well: a ZIP should not begin with an HTML doctype, and a Markdown download should not contain the website’s navigation shell. For known assets, compare the deployed bytes with the local source using SHA-256.
curl --fail --location \
https://keeponfirst.com/downloads/evidence/glb-measurements.zip \
--output /tmp/glb-measurements.zip
file /tmp/glb-measurements.zip
unzip -t /tmp/glb-measurements.zip
shasum -a 256 public/downloads/evidence/glb-measurements.zip /tmp/glb-measurements.zipRun the final command from a checkout containing the source asset. Matching digests establish byte equality for that download. Without a local copy, at least inspect archive validity, file names, and the README against the article’s description. Matching hashes verify the transferred file, not every technical conclusion made inside it.
A small, practical acceptance matrix
| Scenario | Expected check |
|---|---|
| Click ZIP from both language versions | Shared URL; valid archive |
| Click Markdown | Text file, not the SPA shell |
| Click the HTML utility | Usable page, not a forced download |
| Compare initial HTML and mounted DOM | Matching href and download semantics |
| Request a nonexistent asset | Recognize a 404 or fallback rather than accepting it |
| Add a query string | Define the expected behavior separately |
A routing-system rewrite is not the first step here. Define the page-versus-file boundary, align both outputs, and verify from the reader’s entry point. When an attachment contains the evidence or runnable example behind an article, reliable access to it is part of delivering the article itself.
Another guide in the same set covers keeping article bodies from weighing down the homepage: “Keep article bodies out of the homepage bundle”.