Build online shops
Two shops, built in opposite ways for different reasons.
A print-on-demand poster shop
A Worker renders the home page, the catalogue, each product page and the sitemap on the server, from a single catalogue file. Search engines and AI assistants receive finished HTML. Each product page carries schema.org ProductGroup markup with its 14 size variants, including shipping details and the return policy.
Three problems in it were more interesting than the shop itself.
Fifty-nine per cent of the catalogue was one paragraph. The listings were imported from a marketplace, and 171 of the 196 carried the same five lines about paper and printing. Measured, 59% of all the text in the catalogue was that paragraph repeated, which is exactly what a search engine treats as duplicate content at scale. The Worker now filters those lines when it renders and shows the information once. The rule for new products is that a description contains only what is unique to that piece.
Prices do not follow the exchange rate, on purpose. The shop sells in euros, dollars and pounds. The prices are fixed points, calculated once from a real rate and rounded, not a live conversion. If the site re-priced every day, the price on the page would stop matching the price in the product feed, and Google would disapprove all 2,744 variants for a price mismatch. The currency arrives in the address and is validated again on the server: what the browser sends does not decide what is charged. Those addresses are marked noindex with a canonical link to the clean one, so the catalogue is not indexed three times.
A feed that must not look new. Pinterest creates pins by reading an RSS feed. If the publication date of an item changed on every request, the same product could be taken for a new one and published again. So each product has a fixed date, a base plus one hour per position, and its identifier is the address of its page.
One smaller thing: the catalogue file weighs 410 KB and was being loaded on every page, although outside the shop the cart only needs a title and a thumbnail. A reduced version, 49 KB, is now generated from it automatically.
A site that sells one thing
Numbered places on a public wall, in five languages. Data lives in D1.
The language is chosen without remembering anything. Detection happens on one address only, the root. The Worker reads Accept-Language, orders the languages by weight and redirects to the first one it has. Nothing is stored: no cookie, no local storage. The subtle part is the cache. The Spanish page is cached under a modified key, so it can never be served from cache to someone who should have received a redirect.
Entries are chained, and the chain is public. Each entry’s hash covers its number, its exact text, its publication time and the previous hash.
Three decisions inside that:
- The chain is computed by a scheduled job, not at the moment of publishing. Numbers are assigned under race conditions, and a written hash is final.
- Withdrawing an entry does not change its hash. Withdrawing affects how it is shown, not what was written, and the chain documents the second.
- If a gap appears, the chaining stops. It does not fill the gap silently.
Payment is captured only after review. The card is authorised, not charged. Authorisations expire by themselves after seven days, so the job cancels unreviewed ones at six: nobody pays for something that was never looked at.
Each entry gets two images that never change, rendered with Satori and resvg in WebAssembly, stored in R2 and served through the Worker so that the content policy can stay at img-src 'self'. The job renders three per run. Each image is expensive in CPU, and batches of twenty ran out of budget halfway and left the rest undone.
Deploying did not update the site. Some pages kept showing the old navigation after a deploy. The pages were cached at the edge for an hour, and a deploy did not invalidate them: I measured one page served from a copy 59 minutes old. The fix was to put a build identifier in the cache key. Every deploy now misses the cache on purpose and regenerates at once, and the test script watches for it.
- JavaScript66.8%
- TypeScript18.7%
- CSS7.2%
- HTML5.4%
- Shell1.6%
- Other0.3%