Pages and layouts
Each page is a small front-matter file in src/content/en/ or src/content/fr/
that points to a layout in src/_includes/layouts/. For example,
src/content/en/pricing.njk:
---
layout: layouts/pricing.njk
permalink: /pricing/index.html
locale: en
title: Pricing
altLangUrl: /fr/tarifs/
altLangLocale: fr
---
The layout holds the markup and both language versions share it. altLangUrl links
each page to its translation, which drives the language switcher and the hreflang
tags.
Translations
Interface strings live in src/_data/i18n.js as flat keys, with one dictionary per
language. Templates use the t filter:
<h1>{{ "pricing.page_title" | t(locale) }}</h1>
The browser app uses the same dictionary through t() in src/js/app.js.
bun run i18n:check fails if a key, a blog post or a page is missing in either
language.
Adding a page
- Create the layout, for example
src/_includes/layouts/about.njk, extendinglayouts/base.njk. - Add
src/content/en/about.njkandsrc/content/fr/a-propos.njkwith matchingaltLangUrlvalues. - Add its strings to both dictionaries in
i18n.js. - Add it to
src/_data/navigation.jsif it belongs in the header.
Pages that shouldn't be indexed, like account and checkout pages, set
noindex: true. They stay out of the sitemap but keep their canonical links.
Blog posts and guides
Blog posts are Markdown files in content/<lang>/blog/, and every post needs a
translationKey shared with its translation. Guides live in content/<lang>/guides/
and are grouped in the sidebar by their section and order front matter.
The browser app
src/js/app.js is one small module. A single click handler dispatches
data-action attributes, and render() updates the page from one state object.
Page-specific code can live in src/js/pages/, registered in PAGE_MODULES and
loaded as a separate chunk only on pages whose layout sets pageModule.