Server-Side Rendering on Country Subdomains: What We Got Wrong First
Server-Side Rendering on Country Subdomains: What We Got Wrong First 24ad.info is a classifieds site where every country has its own subdomain — pl.24ad.info, de.24ad.info, gb.24ad.info — all served by one app. The fro
Server-Side Rendering on Country Subdomains: What We Got Wrong First
24ad.info is a classifieds site where every country has its own subdomain — pl.24ad.info, de.24ad.info, gb.24ad.info — all served by one app. The frontend is a React 19 single-page app built with Vite, running on Express behind Caddy, with everything behind Cloudflare.
A single-page app has a well-known weakness: the first HTML a visitor (or a search engine) receives is mostly empty. Getting real content and correct metadata into that first response, per country, took a few rounds. Here they are, including the one we rolled back the same day.
Round 1 (April): canonical URLs and the geo-redirect
The first problems were about which URL counts as "the" page.
-
Canonical URLs always pointed at the main domain. A category page on
pl.24ad.infotold search engines its canonical address was the home page of24ad.info. On 21 April that changed: the canonical now uses the host the request actually came in on, sopl.24ad.info/c/…points to itself. -
The geo-redirect. Visitors who arrive at the bare
24ad.infoare sent to their country's subdomain, using Cloudflare's country header; countries we don't support go to the UK site (gb.24ad.info). The redirect became a permanent301instead of a temporary302. -
Who should not be redirected. Search-engine bots, logged-in users (an admin choosing the global view kept being bounced to their own country), and anyone who explicitly picks Global — that choice adds
?global=1, which the redirect respects.
Round 2 (May): injecting data — and rolling half of it back
On 19 May, version 1.4.3 started injecting data into the first response. For the home page and for individual listings, the server fetched the data up front and embedded it in the HTML; the React app picked it up as its initial state. The result: no loading spinner, and the right <title> and meta tags from the very first byte.
For listing pages it worked. For the home page it caused a real bug: home shows the newest listings, and listings are country-specific — but the injected data wasn't. Visitors could get a mix of UK and global listings on their own country's home page. The home-page injection was removed the same day.
The lesson was simple: anything you pre-render on a multi-country site has to be scoped by country exactly as the live page would be, or it's worse than nothing.
Where it stands now
Today the server renders three kinds of page with their own metadata and embedded initial data:
- the home page,
-
listing pages (
/posts/:slug), -
category pages (
/c/:slug),
with listings filtered by the country subdomain the request came in on. Every other route gets the app's HTML template with its own canonical URL and meta tags. Unknown paths return a real 404 marked noindex, instead of quietly showing the home page.
There are no placeholders in the template: the server replaces the existing title, description, Open Graph and canonical tags, and adds a block of initial data. The template itself is never cached; server-rendered pages are cached at the Cloudflare edge for 60 seconds (with a further 5 minutes of serving stale while refreshing), and that cache is purged on every deploy. Static assets under /assets are served directly by Caddy with a one-year immutable cache.
One Caddy site block covers both 24ad.info and *.24ad.info, using a wildcard certificate.
What we'd tell anyone doing the same
- Canonical URLs are per host, not per site, once you use subdomains.
- Decide who the geo-redirect must leave alone — bots, logged-in users and explicit choices — before you ship it.
- Pre-rendered data must be scoped exactly like the live page. If the live page filters by country, the server must too — or don't pre-render it.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.