For developers
A brief appendix for self-builders — build commands, design tokens, file map, and the i18n workflow. Deeper architectural detail lives in the repo README and source.
This page is the short reference for developers customizing Meridian from source. Most site editors won't need anything here — uploading the pre-built meridian.zip is enough. If you want to fork the theme, change design tokens, or rebuild after edits, read on.
Custom code is overwritten on theme update
If you modify theme files directly and then upload an updated meridian.zip through Ghost Admin, your changes are erased. Fork the repo, commit your changes to your fork, and rebuild your own zip — don't edit files in place on a production install. For small CSS or JavaScript tweaks, prefer Ghost's Settings → Code injection which survives theme updates.
Build requirements
- Node.js v22.12.0 or later
- Yarn (any recent v1.x)
- Ghost v6.0.0 or later for the running site that will host the built theme
Build commands
All commands run from the theme root.
| Command | What it does |
|---|---|
yarn install | Install dependencies. |
yarn dev | Vite dev server with HMR. Useful when iterating on CSS or JS in a local Ghost install pointed at the theme directory. |
yarn build | Production build to assets/built/ (minified, no sourcemaps, ES2020 target). |
yarn zip | Run lint + build, then package meridian.zip for upload. Excludes node_modules, .git, docs, scripts, and other source-only files. |
yarn lint | Runs lint:i18n, lint:theme (GScan), lint:css (Stylelint), and lint:js (ESLint) in order. |
yarn lint:i18n | The translation-parity check (see below). |
yarn lint:theme | GScan compliance check against Ghost's theme-API expectations. |
yarn test | Alias for yarn lint. |
Design tokens
Meridian's design system lives as CSS variables in assets/css/index.css under the @theme block. The token families:
| Token family | Purpose | Swapped by |
|---|---|---|
--color-paper, --color-paper-2, --color-paper-3 | Page surfaces (body bg, raised cards, deeper recess) | background_palette theme setting |
--color-rule, --color-rule-2 | Horizontal rules and borders | background_palette theme setting |
--color-ink, --color-ink-2, --color-ink-3, --color-ink-mute, --color-ink-faint | Text colours, from heaviest body to lightest caption | Constant across palettes |
--color-accent, --color-accent-ink, --color-accent-tint, --color-brand | Accent colour family | Ghost's --ghost-accent-color (Brand → Accent color in Ghost Admin) |
--ff-* | Per-family font stacks for all 11 selectable fonts | font_heading / font_body theme settings |
Every paper × ink × accent combination is independently audited for WCAG AAA contrast on body text. Adding a new background or accent preset means: add a new CSS rule against the data-palette or data-accent attribute on <html>, override only the relevant token family, and verify body contrast clears 15:1.
File map
The repo's root templates and key partials at a glance:
Templates (root)
| File | Purpose |
|---|---|
default.hbs | Master layout — declares the data-color-scheme, data-palette, data-accent attributes on <html>; conditional font preloads; FOUC-prevention inline script. |
home.hbs | Homepage: secondary aside + lead + rail; Editor's Picks strip; tag-dispatched section rows; from-the-archive tail; Membership CTA band. |
post.hbs | Article page: hero, byline, reading progress, body, paywall, comments, related coverage, member CTA, author card, sticky toolbar. |
page.hbs | Standard page template. |
page-bookmarks.hbs | Custom template for the Ghost page with slug bookmarks. |
tag.hbs, author.hbs | Tag / author archives. |
index.hbs | Index archive (chronological). |
error.hbs, error-404.hbs | Error pages. |
Key partials
| Partial | Purpose |
|---|---|
partials/header.hbs | Masthead, utility row, drawer trigger. |
partials/footer.hbs | Footer columns, dark-mode logo, newsletter form. |
partials/post-card.hbs | Reusable article card, 7 variants. |
partials/archive-grid.hbs | 18-post asymmetric grid. |
partials/editors-picks-strip.hbs | Homepage featured-post carousel. |
partials/membership-cta.hbs | Homepage member band (sources copy from page slug membership-cta). |
partials/homepage-sections.hbs | Tag-dispatched section rows. |
partials/related-coverage.hbs | Up to 4 same-tag posts on the post page. |
partials/font-slug.hbs | Maps font label → CSS slug. |
partials/reading-progress.hbs | Post-page progress bar. |
partials/social-links.hbs | Footer + author social icons (hydrated client-side from meta tags / data div). |
Assets
| Path | Purpose |
|---|---|
assets/css/index.css | Tailwind v4 entry, design tokens, font imports, Koenig card overrides. |
assets/js/index.js | Alpine.js entry, stores, components, init helpers. |
assets/js/social-icons.js | Inline-SVG icon registry for the custom-social-links feature. |
assets/js/social-links.js | Hydrates meridian-social-* meta tags + data-meridian-social-links div into the footer / author surfaces. |
assets/js/preference-sync.js | Cross-tab localStorage listener (theme, fontSize, bookmarks). |
assets/js/navigation.js | Primary + secondary nav hydration, drawer logic. |
vite.config.js | Vite build config. |
i18n workflow
The yarn lint:i18n parity check is enforced — never add a {{t "..."}} call to a template without also adding the matching key to locales/en.json.
The mechanics
scripts/check-i18n.mjswalks every*.hbsfile in the theme.- It extracts every
{{t "..."}}and subexpression(t "...")call. - It diffs the extracted key set against
locales/en.json. - It fails the build on any drift — missing key, orphan key, mismatched value.
The discipline
- Add a translation key first, then reference it in the template.
- Use Ghost's interpolation syntax (
{{t "Members of {site}" [email protected]}}) instead of string concatenation, so word order is correct in non-English locales. - Pluralization uses Ghost's
{{plural}}helper around thetcall (e.g.{{plural comment.count empty=(t "No Comments") singular=(t "1 Comment") plural=(t "% Comments")}}). - Never hardcode user-visible English in a template.
See also: Publication Language for the reader-facing side of the same system.
CSP & the inline FOUC script
Meridian ships one inline <script> block in <head> — the FOUC-prevention script that reads localStorage + prefers-color-scheme to apply the saved appearance class before the page paints. Every other script is loaded as a regular module from assets/built/.
If your site enforces a strict Content Security Policy that disallows script-src 'unsafe-inline', you'll need to whitelist this script's SHA-256 hash:
curl -s https://your-site.example.com/ \
| perl -ne 'print $1 if /<script>([\s\S]*?)<\/script>/' \
| head -c -1 \
| openssl dgst -sha256 -binary \
| base64The output is the hash you need to add to your script-src directive (prefixed with 'sha256-').
The script's behaviour is intentionally stable across releases — but the hash will change if the script bytes change, so re-run the command after every theme update on a strict-CSP site.