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 androbots.txtall print absolute URLs from it.locale— the default language, served without a path prefix. It needs a dictionary atsrc/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.
Navigation, footer and social links
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 atitleand a list oflinks. The Legal group lists one link per file insrc/content/legal/; delete a file and delete its line, or the link 404s.nav.social— icon links.iconis a Lucide name (lucide:github, see lucide.dev/icons). Your own SVG goes insrc/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;--radiusis0.--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— theIssue 04 · Essays · 03 / 07line at the foot of a piece.dropCap— the default for every piece. A single article overrides it withdropCap: truein 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.0drops 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.
Search
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.
Legal notices
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/.