Customization

Everything site-specific lives in src/config.ts and src/styles/theme.css. You should not have to open a component for any of the changes below.

Site identity

src/config.ts, the site object:

  • name — the nameplate in the sticky bar, the footer wordmark, and page titles. It is set type, not an image file: change the word here and the font pairing sets it.
  • description — the tagline under the footer wordmark, the meta description, and the feed.
  • url — your production URL. Set it before you deploy: canonical links, the feed, the sitemap and robots.txt all print absolute URLs from it.
  • locale — the default language, served without a path prefix. It needs a dictionary at src/i18n/<locale>.ts.
  • author — the editor’s name, printed in the footer line and in the JSON-LD.
  • email — the editorial address, printed on the masthead page (/about).
  • defaultOgImage — the share card used wherever there is no piece-specific one.

src/config.ts, the nav object:

  • nav.header — the links in the sticky bar. Three or four fit a phone beside the nameplate.
  • nav.footer — groups, each a title and a list of links. The Legal group lists one link per file in src/content/legal/; delete a file and delete its line, or the link 404s.
  • nav.social — icon links. icon is a Lucide name (lucide:github, see lucide.dev/icons). Your own SVG goes in src/icons/ and is referenced by its filename without the extension.

A label is either a string or one string per language: { en: 'Archive', ko: '지난 호' }. This is the one place you write the words yourself; everything else the theme prints lives in the dictionaries.

Colours and other tokens

src/styles/theme.css is your override file. Every token and its default is in src/styles/tokens.css — read that file, do not edit it. Declarations in theme.css are unlayered, so they win without specificity tricks:

:root {
  --primary: light-dark(#8c1f2f, #e08792);
  --content-width: 42rem;
}

Colours use light-dark(<light>, <dark>). Overriding with a plain colour changes both modes at once. The tokens worth knowing:

  • --background, --surface, --foreground, --muted, --border, --primary, --primary-hover, --on-primary — paper, ink, and one spot colour. There is no second accent by design: a coloured section background or a second hue breaks the two-colour press run the theme is built on.
  • --rule-hair, --rule-strong, --rule-heavy — the lines that draw the structure. This theme has no cards and no shadows; --radius is 0.
  • --leader-dot, --leader-color — the dotted leader in a contents row.
  • --content-width (the 65ch article measure), --wide-width (the page container).

Change a colour and run pnpm check:contrast. It measures every text-on-background pair, light and dark, in every preset, against WCAG AA and prints the ratios.

Colour presets

src/config.ts, theme.preset: 'petrol' (default), 'cinnabar', 'ultramarine', or 'newsprint'. The value is printed on <html> as data-preset.

The default has no block of its own — it is the tokens.css palette, so if you stay on petrol and override a token in theme.css nothing else interferes. Every other preset is a block in src/styles/presets.css, and an attribute selector outranks :root, so on those presets you retheme by editing the preset block rather than theme.css.

To add a preset: copy a block in presets.css, rename the attribute value, keep one token per line with a light-dark() pair of six-digit hex (the contrast script parses them), add the name to the union in config.ts, and run pnpm check:contrast.

Font pairings

src/config.ts, theme.fonts: 'didone' (default — Bodoni Moda covers, Archivo kickers and UI, Literata text), 'grotesque' (Archivo sets the covers too), or 'bookish' (Literata throughout). Printed on <html> as data-fonts; the blocks are at the foot of src/styles/presets.css.

A pairing redistributes the three faces the theme already loads. It does not load a fourth.

Previewing a variant

In dev, /styleguide shows the tokens, the type scale and the signature rows. /styleguide?preset=cinnabar&fonts=bookish previews a combination without editing config.ts. The route is dev-only: it is a design tool, not a page you ship.

Webfonts

astro.config.mjs, the fonts: block (Astro’s fontsource provider, latin subsets, self-hosted at build). The three faces are bound to --font-body, --font-heading and --font-display. Do not add a font CDN link; add a face here instead.

The last entries in each fallbacks list are system Korean faces. No latin subset carries Hangul and a CJK webfont is megabytes, so a second-language edition is set in the reader’s own system face. To ship one yourself, add it to this block and point --font-body at it in theme.css.

The contents rows

The signature. src/config.ts, the contents object:

  • style — 'leaders' draws the dotted rule between a title and its byline; 'plain' drops the fill span, and the dots with it.
  • numbering — the article numbers hung in the left margin.

The rule itself is .leaders / .leaders__fill in src/styles/global.css: one border-block-end: dotted.

Issues and articles

src/config.ts, the issues object:

  • articlesPerIssue — an editorial guide only; nothing enforces it.
  • showFolio — the Issue 04 · Essays · 03 / 07 line at the foot of a piece.
  • dropCap — the default for every piece. A single article overrides it with dropCap: true in its frontmatter.

src/config.ts, the article object:

  • toc — the contents box a piece opens with. It prints from three ## sections up.
  • relatedCount — pieces from the same department in other issues, printed under the article. 0 drops the section.

src/config.ts, the archive object: issuesPerPage and showReadingTime.

UI strings

src/i18n/<locale>.ts. Every word the theme prints itself is a key there, read in a component with useT(Astro.currentLocale). Page copy — the masthead prose, an article, a legal notice — is content, and lives in the page or in src/content/.

To reword something, edit the dictionary. Every dictionary needs the same keys: ko.ts is checked against en.ts by the type checker.

Languages

src/config.ts, i18n.locales — a map of code to display name, e.g. { en: 'English', ko: '한국어' }. site.locale is served unprefixed; every other key is served under /<key>/. Each one needs a dictionary at src/i18n/<key>.ts and its pieces under src/content/articles/<key>/.

i18n.descriptions holds the tagline per language. A language left out falls back to site.description.

The picker in the header follows the count. Two languages: one link to the other edition, which is the whole choice. Three or more: a disclosure that names the edition being read and lists the rest under it, so the header keeps its line. A page with no counterpart in a language is marked no translation and links to that language’s cover instead; a language that has not printed an issue yet is left out of the picker altogether.

features.i18nRouting: false turns the whole thing off, along with the language picker.

src/config.ts, features.search. On, pnpm build writes a Pagefind index at the end of the build and /search exists in every edition. Off, the route, the header link and the indexing step all go away.

Only what carries data-pagefind-body is indexed — the piece itself, never the header, the contents rows or the folio. Search does not work in pnpm dev; the index is a build artifact, so run pnpm build && pnpm preview to try it.

Share cards (OG images)

src/config.ts, theme.ogImages. On, src/lib/og.ts draws a 1200×630 PNG per piece at build time with satori, served from /og/<slug>.png; everything else uses site.defaultOgImage (/og-default.png, drawn the same way).

The OG fonts in src/assets/fonts/og/ are latin subsets, like the webfonts. A piece whose title needs another script keeps the default card rather than printing empty boxes.

Newsletter

src/config.ts, the newsletter object. provider is null (renders nothing), 'buttondown' or 'kit'; id is your Buttondown username, or the Kit form id from app.kit.com/forms/<id>/.

The block sits at the foot of the cover and at the foot of a piece. It is a plain form POST: no script, no third-party JavaScript, and the reader lands on the provider’s own confirmation page.

Comments

src/config.ts, features.comments plus the comments object (repo, repoId, category, categoryId). Enable Discussions on a public GitHub repository, install the giscus app, then read the four values off giscus.app.

This is the one place the theme loads someone else’s script, which is why it is off by default. The thread follows your light/dark toggle and the language of the page.

Analytics

src/config.ts, the analytics object. provider is null (nothing is rendered), 'plausible', 'ga4' or 'umami'. id is the domain you registered with Plausible, the GA4 measurement id (G-XXXXXXX), or the Umami website id. The snippet lives in src/components/common/Analytics.astro — swap in another provider’s tag there.

host is for a self-hosted Plausible or Umami: the origin that serves the script, with no trailing slash (https://stats.example.com). Leave it empty for the hosted service, and for GA4.

Until provider and id are both filled in, the built pages load no third-party script at all.

src/content/legal/*.md — the filename is the URL (/legal/privacy). They are written about what this theme actually does, which is very little; read them before you publish them, and have a lawyer read them if your site does more. Delete a file and delete its line from the footer group in config.ts.

SEO

src/config.ts, the seo object: titleTemplate (e.g. '%s · Gazette'), twitterHandle, and jsonLd ({ type: 'Person' | 'Organization', name }). The <head> itself is src/components/common/SEO.astro.

Favicon

public/favicon.svg and public/favicon.ico. public/ is for files that must be served byte-for-byte; images that belong to content go in src/assets/.