Building a Bilingual Ghost Theme From Scratch
Week 1 of building a custom Ghost theme that supports English and French content with a digital garden structure. Here is what worked and what did not.
I wanted a personal site that felt like a digital garden — not a reverse-chronological blog. Content that grows over time, organized by type rather than date. And it needed to work in both English and French, because I write for audiences in Europe and Francophone Africa.
Why Ghost?
I considered Astro, Next.js, and even plain HTML. But Ghost gives me a writing environment that stays out of the way, built-in membership features, and enough routing flexibility to build what I need.
The tricky part: Ghost was not designed for bilingual sites. There is no i18n system. No per-post language detection. No built-in translation linking.
The architecture
After two days of prototyping, I settled on this approach:
What surprised me
Ghost's routes.yaml is more powerful than I expected. Collections with NQL filters let me create virtual directories that feel like a traditional folder structure. But there is a catch: a post can only belong to ONE collection. So the type collections must come before the language catch-all.
I also learned that Ghost's translation helper only loads one language file globally — it does not detect per-post language. So the whole locales/ approach is a dead end for bilingual sites. The inline conditional approach is uglier in the template code but actually works.
Current status
The theme is built and deployed. All routes return 200. The CSS design system is complete with fluid typography, dark mode, and content type visual markers. Next up: writing actual content and setting up DNS.
Time spent so far: roughly 40 hours across two weekends.