Maintenance

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.

CommandWhat it does
yarn installInstall dependencies.
yarn devVite dev server with HMR. Useful when iterating on CSS or JS in a local Ghost install pointed at the theme directory.
yarn buildProduction build to assets/built/ (minified, no sourcemaps, ES2020 target).
yarn zipRun lint + build, then package meridian.zip for upload. Excludes node_modules, .git, docs, scripts, and other source-only files.
yarn lintRuns lint:i18n, lint:theme (GScan), lint:css (Stylelint), and lint:js (ESLint) in order.
yarn lint:i18nThe translation-parity check (see below).
yarn lint:themeGScan compliance check against Ghost's theme-API expectations.
yarn testAlias 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 familyPurposeSwapped by
--color-paper, --color-paper-2, --color-paper-3Page surfaces (body bg, raised cards, deeper recess)background_palette theme setting
--color-rule, --color-rule-2Horizontal rules and bordersbackground_palette theme setting
--color-ink, --color-ink-2, --color-ink-3, --color-ink-mute, --color-ink-faintText colours, from heaviest body to lightest captionConstant across palettes
--color-accent, --color-accent-ink, --color-accent-tint, --color-brandAccent colour familyGhost's --ghost-accent-color (Brand → Accent color in Ghost Admin)
--ff-*Per-family font stacks for all 11 selectable fontsfont_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)

FilePurpose
default.hbsMaster layout — declares the data-color-scheme, data-palette, data-accent attributes on <html>; conditional font preloads; FOUC-prevention inline script.
home.hbsHomepage: secondary aside + lead + rail; Editor's Picks strip; tag-dispatched section rows; from-the-archive tail; Membership CTA band.
post.hbsArticle page: hero, byline, reading progress, body, paywall, comments, related coverage, member CTA, author card, sticky toolbar.
page.hbsStandard page template.
page-bookmarks.hbsCustom template for the Ghost page with slug bookmarks.
tag.hbs, author.hbsTag / author archives.
index.hbsIndex archive (chronological).
error.hbs, error-404.hbsError pages.

Key partials

PartialPurpose
partials/header.hbsMasthead, utility row, drawer trigger.
partials/footer.hbsFooter columns, dark-mode logo, newsletter form.
partials/post-card.hbsReusable article card, 7 variants.
partials/archive-grid.hbs18-post asymmetric grid.
partials/editors-picks-strip.hbsHomepage featured-post carousel.
partials/membership-cta.hbsHomepage member band (sources copy from page slug membership-cta).
partials/homepage-sections.hbsTag-dispatched section rows.
partials/related-coverage.hbsUp to 4 same-tag posts on the post page.
partials/font-slug.hbsMaps font label → CSS slug.
partials/reading-progress.hbsPost-page progress bar.
partials/social-links.hbsFooter + author social icons (hydrated client-side from meta tags / data div).

Assets

PathPurpose
assets/css/index.cssTailwind v4 entry, design tokens, font imports, Koenig card overrides.
assets/js/index.jsAlpine.js entry, stores, components, init helpers.
assets/js/social-icons.jsInline-SVG icon registry for the custom-social-links feature.
assets/js/social-links.jsHydrates meridian-social-* meta tags + data-meridian-social-links div into the footer / author surfaces.
assets/js/preference-sync.jsCross-tab localStorage listener (theme, fontSize, bookmarks).
assets/js/navigation.jsPrimary + secondary nav hydration, drawer logic.
vite.config.jsVite 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

  1. scripts/check-i18n.mjs walks every *.hbs file in the theme.
  2. It extracts every {{t "..."}} and subexpression (t "...") call.
  3. It diffs the extracted key set against locales/en.json.
  4. 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 the t call (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 \
  | base64

The 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.