For developers
Build Meridian from source — yarn commands, CSS design tokens, the template file map, the i18n parity check, and CSP hashes for the inline scripts.
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 — see Code injection for the stable selectors Meridian exposes for exactly this.
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 preset means: add a new CSS rule against the data-palette attribute on <html>, override only the relevant token family, and verify body contrast clears 15:1. (Accent has no <html> attribute — it flows through Ghost's --ghost-accent-color variable.)
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 and data-palette 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, 8 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 scripts
With ads off (the default), 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. Turning on Enable ads with Ad consent mode adds a second inline block: the Consent Mode v2 deny-by-default baseline, emitted before {{ghost_head}}. Every other script is loaded as a regular module from assets/built/ (the AdSense loader, when a publisher ID is set, is an external src script, not inline).
If your site enforces a strict Content Security Policy that disallows script-src 'unsafe-inline', you'll need to whitelist each inline script's SHA-256 hash — one hash with ads off, two with ads + consent mode on:
curl -s https://your-site.example.com/ \
| perl -ne 'print "$1\n" if /<script>([\s\S]*?)<\/script>/g' \
| while IFS= read -r s; do printf %s "$s" | openssl dgst -sha256 -binary | base64; doneEach output line is a hash to add to your script-src directive (prefixed with 'sha256-').
The scripts' behaviour is intentionally stable across releases — but a hash will change if the script bytes change, so re-run the command after every theme update on a strict-CSP site.