Dev.to · 4 min read

I shipped 174 pages across 7 languages. Two were silently broken.

I shipped 174 pages across 7 languages. Two were silently broken.

Started this as an SEO task. Turned into about six hours of debugging that I want to write down before I forget the details. The setup A SaaS site with a service catalog. Prices come from the database and change, so the pages have to be server-rendered. A crawler hitting an empty sees nothing worth indexing. Built one renderer, wired it to seven language configs: zh, en, id, pt, vi, ar, ru Chinese at the root. The rest get /en, /id, /pt, /vi, /ar, /ru. Each language: a homepage, a pricing page, and 15 platform pages. The sitemap came out around 174 URLs. Felt good. Then I started actually checking things. Failure 1: the hreflang cluster wasn't a cluster I wrote hreflang tags per page, roughly: That was the Chinese page. English had the same three lines with different hrefs. Fine. Then I added Indonesian, Portuguese, Vietnamese, Arabic, Russian. And I appended them... to the pages I was working on at the time. Checked the Arabic page: all seven languages listed. Checked the English page: three. The old three, untouched. Here's what I didn't understand. hreflang isn't seven independent declarations, it's one cluster. If /en says "my alternatives are zh, en, x-default" while /id says "my alternatives are all seven plus x-default", the cluster contradicts itself. Google's documented behavior in that situation is to ignore the conflicting set — not to trust whichever page looks more complete. So probably none of my hreflang was doing anything. The fix was boring. Generate from one array, use it everywhere, and include the current language in its own list: const LANGS = ['zh', 'en', 'id', 'pt', 'vi', 'ar', 'ru']; const hreflangsFor = (path) => LANGS .map(l => ({ lang: LANGS_META[l].code, href: `${BASE}${LANGS_META[l].prefix}${path}` })) .concat([{ lang: 'x-default', href: `${BASE}${path}` }]); Every page calls it. Adding an eighth language later is one array entry, not six template edits. (Yes, a page should list itself in its own hreflang set. That part confused me at first.) Failure 2: the dir="rtl" that never made it to the page Arabic needs RTL. I knew that going in and wrote code for it. Then I curled the live page: No dir. Arabic rendering left-to-right. What I'd done was two separate string replacements — one for the language, one to inject dir: let out = html.replace(/' # # is there real text in there, or just a shell? curl -s https://example.com/ar | sed 's/]*>/ /g' | tr -s ' ' | wc -c # ~6100 That second command is the one I now run on every page before calling anything done. Because my English page passed every SEO check I had and was still an empty shell. Body text: 599 bytes. count: 0. HTTP status: 200. Title, description, canonical, hreflang, two JSON-LD blocks — all present and correct, because a prerender step was filling in and then stopping. Google had been happily indexing it as a thin page for weeks. A 200 tells you the server responded. It tells you nothing about whether there's a page there. Strip the tags, count the characters. Under a few hundred and you've shipped a shell with good metadata. What I'd change next time One source of truth for hreflang, from the very first language. The copy-paste approach holds up fine for two and quietly falls apart at three. And verify the rendered output, not the code that produces it. Both of these bugs read fine as code. Neither showed up until I fetched the actual URLs and looked at the bytes. I'm building this for XNOW PRO, a social media growth panel — the multilingual layer covers those seven languages now. The pricing page is where the server-rendering earns its keep: real catalog prices in the HTML, no JavaScript required to see them.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News