[CODE]

Shipping bilingual Astro pages and the build traps that bit me

Adding clickable service cards and public BBQ/birthday pages to my guesthouse site in zh/en. The interesting part wasn't the markup — it was an apostrophe killing the build, an orphan-class audit, and a watermark bleeding into the hero.

4 min read
astro i18n css build-tooling

I spent a session turning the static service blurbs on my guesthouse site into actual destination pages — BBQ and birthday packages, each in Chinese and English — and wiring the homepage service cards to be clickable. The feature work was mundane. The three bugs after it were the part worth writing down, because every one of them is a class of problem you hit again the moment you maintain parallel zh/en content trees in Astro.

The apostrophe that broke the build

The English BBQ page had a section titled “Ocean’s Best.” That apostrophe took the whole build down.

Astro components are parsed as JSX-adjacent templates, and depending on where a stray quote-like character lands — inside an expression, an attribute, or a fragment boundary — the parser can misread it as a delimiter. In my case the apostrophe sat close enough to some inline expressions that the tokenizer lost the plot and threw a parse error that pointed at a line several nodes away from the actual character. That’s the annoying part: the error location lies to you.

The fix is boring (Ocean's or a curly apostrophe ), but the lesson is structural: English content has typographic characters Chinese content doesn’t. My zh pages never tripped this because they have no apostrophes, no contractions, no &. So the build passed on the zh page and failed on its en twin for content that was “the same.” When you mirror pages across locales, the en side is the one that surprises you — apostrophes, ampersands in copy, em-dashes pasted from a doc.

I now treat raw English prose in .astro as untrusted input. Anything with a contraction or & gets HTML-escaped or moved into a frontmatter string where it’s just a JS string literal and the template parser never touches it.

The orphan-class audit

I run a small check that flags CSS classes referenced in markup but never defined (and the reverse). The new BBQ and birthday pages — all four locale files — failed it. I’d copied class names from an existing page’s structure but hadn’t carried over (or had renamed) the corresponding rules.

This is the predictable tax of duplicating pages per locale. The content differs but the styling should be identical, so people copy markup and the class/style pairing silently drifts. The audit is what makes that drift loud instead of shipping a page that looks half-styled in production. Adding the missing classes was a two-minute fix; the value was that the audit ran at all. If you maintain N copies of a layout for N locales, having a build-time check that markup and CSS agree is non-negotiable, because visual QA across 2×M pages is where attention runs out.

The watermark that bled into the hero

Last one, and the only one tagged security (loosely — it was a visual leak, not an exploit). I had a watermark implemented as a ::after pseudo-element. On most pages it sat where it belonged. On the hero sections with dark backgrounds, it overflowed its container and bled across the image.

The root cause was the usual pseudo-element trap: ::after was positioned relative to an ancestor that didn’t establish the containing block I assumed, and without an overflow boundary on the right element it painted outside. It only became visible against dark hero backgrounds because the watermark color had enough contrast there — on light pages it was invisible noise nobody noticed. So it had probably been wrong for a while and only surfaced when content (the dark hero) changed underneath it.

I removed the overflowing ::after rather than patching its positioning. A decorative watermark isn’t worth a global pseudo-element that participates in every page’s stacking and overflow behavior. The cost of “it’s just CSS, leave it” is that it interacts with every future hero you add.

The actual feature

Underneath all that: the homepage and booking page got a conversion pass. Service cards now link through to their pages instead of being dead text, the RoomCard and Footer got contact CTAs tightened up, and one room’s content files got updated copy — all kept in zh/en lockstep. Nothing clever, but the lockstep is the discipline: every change touched both locale files in the same commit, which is the only way the orphan-class audit and the apostrophe rule stay manageable.

The meta-lesson for a duplicated-locale site: your English content is the build-fragile one, your CSS drift is invisible until audited, and decorative CSS is liability the moment your content changes around it.