Shipping an 815-page, 10-language static site on Cloudflare Workers: three things that broke

2026-11-04来源 dev.toEnglish

A quick context note before the technical part: this is a manufacturer's product catalogue site — 815 pages, ten languages, hand-written HTML, no framework, no database, no CMS.

That is a deliberately boring stack. The interesting part is what broke.

The shape of it

site/ ├── index.html ├── 989.html # product page ├── product-a.html ├── zh-articles/ # 36 pages ├── es-articles/ # 35 pages ├── ru-articles/ # 35 ├── fr-articles/ # 35 ├── pt-articles/ # 35 ├── ja-articles/ # 35 ├── ko-articles/ # 35 ├── ar-articles/ # 35 ├── tr-articles/ # 35 └── articles/ # 42 (English) 

457 product and category pages at the root, 358 article pages across ten languages. Deployment is:

# wrangler.toml name = "static-catalogue" compatibility_date = "2026-09-01" [assets] directory = "./" 
npx wrangler deploy 

That is the whole pipeline. No build step, no bundler. Push files, deploy, done. For a catalogue where every page is authored by hand, this is the right trade-off — the alternative was a CMS that nobody on the factory floor would ever open.

Now the three things that actually cost me days.

One: IndexNow is not a "notify everything" button

IndexNow is a protocol where you POST a list of URLs to api.indexnow.org and participating engines (Bing, Yandex, Seznam, Naver) drop by to crawl them. It looks like a gift. It is not.

My first implementation pushed every URL in the sitemap on every deploy. That is 815 URLs per deploy, several deploys a day. Over a couple of weeks the endpoint had received roughly 15,200 submissions for a site with 815 pages.

The result was not what I wanted. With 815 pages live and ~15,200 submissions logged, the sitemap sat in "Processing" in Bing Webmaster Tools and stayed there. I cannot prove the submissions caused that — I have no A/B — but the volume-to-page ratio is indefensible on its face, and the documented purpose of the protocol is to announce changes, not to re-announce everything.

What works instead:

# only submit URLs whose lastmod actually changed since the last successful push last_push = load_state() # {url: lastmod} changed = [] for url, lastmod in current_sitemap(): if last_push.get(url) != lastmod: changed.append(url) if changed: post_to_indexnow(changed) # 815 became ~30  save_state(current_sitemap()) 

The count dropped from 815 to roughly 30 on a normal day, and the ones that did get submitted started getting crawled within hours instead of never.

Rule: IndexNow is a change-notification channel, not a sitemap mirror. If your lastmod is rewritten on every deploy (a very common static-site-generator behaviour), you have no change signal at all and the whole thing collapses into the spam case above.

Two: _headers glob patterns don't work the way the docs suggest

I wanted long cache lifetimes on images and short ones on HTML. Cloudflare's _headers file supports glob matching, so this seems obvious:

/*.jpg Cache-Control: public, max-age=31536000, immutable /*.html Cache-Control: public, max-age=300 

It silently does nothing.

The matcher accepts prefix globs — /images/* works — but suffix globs like /*.jpg do not match. You have to enumerate:

/images/* Cache-Control: public, max-age=31536000, immutable 

Which is fine if your images live in a directory. Mine were flat, alongside the HTML, because that is what a hand-authored site looks like after a few years. I ended up moving assets into /assets/ rather than fighting the matcher.

The second half of this problem is that even when _headers does apply, a Cache Rule configured in the dashboard overrides the origin Cache-Control. So you can spend an afternoon editing _headers, deploy, see no change, and only later discover the actual policy is set elsewhere by someone who has since left.

Verify with the response, not the config. curl -I the deployed URL and read the header that comes back. That is the only thing that is true.

Three: ten languages is an indexing problem, not a translation problem

The instinct is that translated pages are dilution — ten copies of the same content, and the engines will pick one and ignore the rest.

The data says otherwise. Queries in Spanish do not retrieve English pages. They retrieve Spanish pages. If you have no Spanish page, you are not in the answer set at all — not ranked low, absent.

So the ten language trees are not duplicates to be pruned. They are ten separate entry points into the same subject matter. Each one is the only page that can answer a question asked in that language.

What does need care is cross-linking. Each translated page needs hreflang pointing at its siblings, and the canonical must point at itself, not at the English version:

<link rel="alternate" hreflang="en" href="https://example.com/product-a.html"> <link rel="alternate" hreflang="es" href="https://example.com/es-articles/producto-a.html"> <link rel="alternate" hreflang="ja" href="https://example.com/ja-articles/製品-a.html"> <link rel="alternate" hreflang="x-default" href="https://example.com/product-a.html"> <link rel="canonical" href="https://example.com/es-articles/producto-a.html"> 

Getting the canonical wrong here — pointing every translation at the English original — tells the engine that nine of your ten pages are duplicates and should be dropped. Which is exactly the outcome the "translation is dilution" intuition predicts, and it is self-inflicted.

What the numbers look like now

Where things currently stand:

  • 815 pages in the sitemap, 393 with a lastmod from the last 48 hours — that is a real editing cadence, not a rewrite-everything-on-deploy artifact
  • IndexNow submissions down roughly 96% (815 → ~30 on a normal day)
  • All ten language trees carry mutual hreflang and self-referencing canonicals

I am not going to claim a ranking improvement I cannot measure. What I can say is that the three failure modes above were all invisible from the outside, and all three are now fixed and verifiable.

The unglamorous summary

None of these were framework problems. All three were cases where the tool did exactly what it was told and the instruction was wrong:

  • IndexNow: told to submit everything, so it did, and the signal died
  • _headers: told to match a pattern the matcher does not support, so it matched nothing
  • hreflang/canonical: told that every page is a copy of the English one, so the engines believed it

If you are running a static site through Workers Assets at this scale, the two things I would check first are your lastmod behaviour and whether your _headers rules are actually reaching the client. Both fail silently.

原文:https://www.a8q9.cn/en/article/shipping-an-815-page-10-language-static-site-on-cloudflare-workers-three-things--fe1633.html

相关文章