React Routeri18n除錯靜態網站

Why downloads break on English pages: routes versus static files

···6 min read

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

DestinationURL from an English pageBehavior
Article/en/article/react-chinese-ime-onchangeOpen the English article
Shared ZIP/downloads/evidence/glb-measurements.zipKeep shared path; offer download
Shared Markdown/downloads/evidence/react-ime-2026-07.mdKeep shared path; offer download
HTML utility/downloads/evidence/ime-event-logger.htmlKeep shared path; open in browser
Different destinations on the same site

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.

tsx
// 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 five-row table with columns for the case, React, and prerendered output. After the fix, all five rows match. The two green rows differed before 2026-11-05 and are now fixed. A Chinese page linking to /en/v4 now keeps /en/v4 in both; before, React turned it into the Chinese page /v4. On an English page, a .md file outside /downloads/ now keeps its path and gets download in both
Output of one href in both renderers, based on the repo after the 2026-11-05 fix. Diagram drawn by KeepOnFirst.

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.

bash
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.zip

Run 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

ScenarioExpected check
Click ZIP from both language versionsShared URL; valid archive
Click MarkdownText file, not the SPA shell
Click the HTML utilityUsable page, not a forced download
Compare initial HTML and mounted DOMMatching href and download semantics
Request a nonexistent assetRecognize a 404 or fallback rather than accepting it
Add a query stringDefine the expected behavior separately
Recheck these scenarios after changing link classification

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”.

Sources