When I set out to build a comprehensive guide site for traditional Indian card games, I knew two things: it had to be fast, and it had to serve three languages natively — English, Hindi, and Bengali. What I didn't know was how deep the rabbit hole of multilingual SEO goes.
Here's the full technical breakdown of building DesiTaashGuide.com with Astro 5 on Cloudflare Pages, and every i18n lesson I learned the hard way.
Why Astro 5?
I evaluated Next.js, Nuxt, and SvelteKit before landing on Astro. The deciding factors:
Content-first architecture. My site is 113+ long-form articles. Astro's content collections with MDX support meant I could write articles in
.mdx, validate frontmatter with Zod schemas, and get type-safe content at build time. No runtime overhead for what is fundamentally a content site.Zero JS by default. Each article page ships roughly 0 KB of client-side JavaScript unless I explicitly opt in with
client:loadorclient:visible. For a content-heavy site targeting users across India — including tier-2 and tier-3 cities with inconsistent 4G — this matters enormously.Built-in i18n routing. Astro 5's
i18nconfig handles locale-prefixed routes out of the box. English lives at/games/teen-patti-hand-rankings, Hindi at/hi/games/teen-patti-hand-rankings-hindi, and Bengali at/bn/games/teen-patti-hand-rankings-bengali.
The i18n Architecture
Here's the simplified routing config:
// astro.config.mjs
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'hi', 'bn'],
routing: {
prefixDefaultLocale: false,
},
},
});
English pages have no prefix (cleaner URLs, better for the primary audience). Hindi and Bengali get /hi/ and /bn/ prefixes respectively.
Content Collections per Locale
Each locale has its own content directory:
src/content/games/
teen-patti-hand-rankings.mdx # English
teen-patti-hand-rankings-hindi.mdx # Hindi
teen-patti-hand-rankings-bengali.mdx # Bengali
The frontmatter includes a locale field and a translationKey that links the three versions together:
---
title: "Teen Patti Hand Rankings"
locale: "en"
translationKey: "teen-patti-hand-rankings"
---
This translationKey is what powers the hreflang generation.
Hreflang Triangulation — The Hard Part
Getting hreflang right is deceptively difficult. Google's documentation makes it sound simple: add <link rel="alternate" hreflang="x" href="..." /> tags. In practice, it's a directed graph problem.
The rule: Every page in a translation group must reference every other page in that group, including itself. If page A links to page B as its Hindi alternate, page B must link back to page A as its English alternate. Miss one link, and Google may ignore the entire set.
I built a utility that, at build time, resolves the full translation cluster for each page:
function getHreflangLinks(currentPage: CollectionEntry) {
const key = currentPage.data.translationKey;
const cluster = allPages.filter(p => p.data.translationKey === key);
return cluster.map(page => ({
hreflang: page.data.locale,
href: getAbsoluteUrl(page),
}));
}
Each page's <head> then renders the full set:
<link rel="alternate" hreflang="en" href="https://www.desitaashguide.com/games/teen-patti-hand-rankings" />
<link rel="alternate" hreflang="hi" href="https://www.desitaashguide.com/hi/games/teen-patti-hand-rankings-hindi" />
<link rel="alternate" hreflang="bn" href="https://www.desitaashguide.com/bn/games/teen-patti-hand-rankings-bengali" />
<link rel="alternate" hreflang="x-default" href="https://www.desitaashguide.com/games/teen-patti-hand-rankings" />
The x-default tag points to the English version as the fallback for any locale not explicitly served.
Validation
I wrote a build-time check that fails the deploy if any hreflang cluster is incomplete. It catches the most common mistake: adding a Hindi article but forgetting to update the English and Bengali counterparts.
Schema.org for MobileApplication Reviews
Since many articles review specific gaming apps, I implemented MobileApplication and Review schema markup:
{
"@context": "https://schema.org",
"@type": "Review",
"itemReviewed": {
"@type": "MobileApplication",
"name": "Teen Patti Gold",
"operatingSystem": "Android, iOS",
"applicationCategory": "GameApplication"
},
"reviewRating": {
"@type": "Rating",
"ratingValue": "4.2",
"bestRating": "5"
}
}
This generates rich snippets in Google search results — star ratings, app names, and platform info appear directly in the SERP.
Performance: The Lighthouse 100 Journey
Getting a perfect Lighthouse score on a content-heavy site requires discipline at every layer.
Images. All images use Astro's <Image /> component, which generates WebP/AVIF variants at build time with proper srcset and sizes attributes.
Fonts. I subset Noto Sans Devanagari and Noto Sans Bengali to only the glyphs actually used. This reduced font payloads from ~180 KB to ~45 KB per script.
Cloudflare Pages. The CDN edge caching means users in Mumbai, Kolkata, and Dhaka hit local PoPs. Time to First Byte is consistently under 50ms.
The result: Lighthouse 100 across all four categories on every page I've tested.
The Open-Source Calculator
I open-sourced a Teen Patti probability calculator as a standalone tool. It computes exact probabilities for all hand types from the C(52,3) = 22,100 possible combinations. Try the live demo.
It's vanilla JS, no frameworks, deployable anywhere. Fork it if you're building anything card-game related.
Lessons Learned
- hreflang is fragile. One missing backlink breaks the entire cluster. Automate validation.
- MDX 3 is strict. Curly braces, angle brackets, and certain Unicode characters crash the parser.
-
Build-time validation saves deploys. Zod schemas for frontmatter, hreflang checks, and broken link detection — all run before
astro buildsucceeds. - Subset your Indic fonts. Your users on Jio connections will thank you.
- Content collections scale well. At 113 articles across three languages, build times are still under 30 seconds.
Check out the live site at desitaashguide.com. Full probability math guide here.
If you're building multilingual content sites with Astro, happy to answer questions in the comments.
Top comments (0)