Skip to content
Mira Okonkwo

documentation / customization.md

Customization

Site settings live in src/config.ts, colours and spacing in src/styles/, fonts in astro.config.mjs. Nothing site-specific is hardcoded in components. For notes, fields and the gutter markers, see content.md.

Site identity

src/config.ts, site:

Key What it is
name The site’s name: the header, page titles, the feed. For a personal site, usually your name.
description The home page’s meta description and the feed description.
url Your domain, https://…, no trailing slash. Sitemap, canonical links and social cards use it.
locale The page language, en, ko, de …
author Your name in the footer and in structured data.
defaultOgImage The social card for pages without their own: a 1200×630 PNG in public/.
lede The line at the top of the home page, above the table of notes. '' leaves it out.

seo.titleTemplate sets page titles (%s · Your Name). seo.jsonLd says whether the site belongs to a Person or an Organization.

nav.header in src/config.ts: a list of { label, href }. Labels are printed as paths, so notes reads /notes; keep them short and lowercase. The current section is in the text colour, the others are grey. The name in the header always links home.

Three items fit on one line at 360px wide next to a 12-character name. With a longer name or more items, the links wrap to a second line under the name.

The footer holds the copyright line, the feed link and the light/dark switch: src/components/common/Footer.astro. Pro’s demo adds a theme line from nav.theme; empty that list on your own site.

The home page

monoline in src/config.ts:

Key What it does
homeCount How many notes the home table lists. With more notes than that, all N notes → links to /notes.
gutter false hides the source gutter markers everywhere (content.md).

The home page is site.lede and the table. When there are no notes, the table is left out.

The source gutter

The markers are added by src/lib/monoline-plugin.ts when the site is built, and drawn by src/styles/monoline.css, which also holds every other rule for a note’s body. The gutter is 4rem (--gutter-width on .note-column); markers hang in it from 48rem up, and fold into a prefix on the first line below that.

monoline.gutter: false adds no-gutter to the body, which hides the markers but keeps the column and the spacing.

Colours and other tokens

Every colour is a token in src/styles/tokens.css, written as light-dark(<light>, <dark>). Do not edit that file: override tokens in src/styles/theme.css, which loads after it.

:root {
  --primary: light-dark(#a23a1c, #f0a184); /* links, the author on social cards, focus rings */
}
Token Used for
--background The page.
--surface Code blocks.
--foreground Text, titles, the rule under table headers.
--muted Gutter markers, dates, tags, meta lines, footnotes, the footer.
--border Hairlines between table rows and above footnotes.
--primary Links in text, focus rings, the search-match underline, the subscribe button.
--primary-hover Links on hover.
--content-width The text column, 38rem (about 66 characters).
--wide-width The page frame for the header, tables and footer, 52rem.

Run pnpm check:contrast after changing a colour. It checks every text colour against every background, in light and dark, and in every preset, and fails below 4.5:1.

Code blocks keep their own syntax colours (github-light-high-contrast and github-dark-high-contrast, set in astro.config.mjs under shikiConfig) on --surface.

Colour presets (Pro)

Three palettes ship in src/styles/. Turn one on by uncommenting its line at the top of src/styles/theme.css:

@import './tokens-paper.css';
Preset Page Links
paper warm rust
forest white deep green
graphite grey dark grey, told apart by underline

Your own rules go below the import and still win. The social cards pick up the preset too.

Dark mode

The site follows the reader’s system setting; the switch at the end of the footer cycles system, light and dark and remembers the choice. Set features.darkMode to false to remove the switch (the system setting still applies).

Fonts

astro.config.mjs loads the fonts through Astro’s font API from Fontsource, Latin subset, all under the SIL Open Font License. There are two roles, each a CSS variable:

Variable Role Default
--font-body Reading text Atkinson Hyperlegible Next
--font-heading Everything structural: navigation, paths, dates, titles, the gutter, code Atkinson Hyperlegible Mono

Keep --font-heading a monospace face: the tables line up their dates in ch, and the gutter is sized for it. To use another face, change name in the pairings entry and run pnpm dev. For a font that needs a wider character set, change subsets.

Pro has a second pairing: set theme.fonts in src/config.ts to 'source' for Source Serif 4 and Source Code Pro. That pairing is scaled to Atkinson’s x-height with font-size-adjust (the html[data-fonts='source'] rule in src/styles/global.css), so it reads at the size the theme was set in. The default pairing has no such rule, on purpose: it would shift the text while the fonts load.

Search (Pro)

Pagefind indexes the text of every note at the end of pnpm build. The gutter markers, the links under a note and the subscribe line are not indexed. The index is written to dist/pagefind/, so search works on the built site (pnpm build && pnpm preview), not in pnpm dev. The way in is the /search link beside the title of /notes. features.search: false removes the page, the link and the index.

Subscribe form (Pro)

newsletter in src/config.ts. Set username to your Buttondown username and every note ends with a one-line subscribe form that opens Buttondown’s confirmation in a new tab. An empty username removes the form. pitch is the sentence above it.

For another provider, replace the form’s action and the email field’s name in src/components/pro/Subscribe.astro with the values from that provider’s embed code.

Demo builds. With the environment variable DEMO_FORMS=1 the form sends nothing: the email field has no name, so the address never leaves the page, and the browser lands on /subscribed. Use it for a public demo; never for your real site.

Comments (Pro)

Giscus stores comments in GitHub Discussions. Install the Giscus app on a public repository, enable Discussions, then copy the four values from giscus.app into giscus in src/config.ts. Until all four are set, nothing renders. The widget loads when a reader scrolls to it.

Social cards (Pro)

seo.ogImages: true draws a 1200×630 card per note at build time (/og/notes/<slug>.png): the path line, the title with its # in the gutter, the note’s first line, your name and the reading time. Cards use the current colour preset and always the Atkinson fonts. The drawing is in src/lib/pro/og.ts. With ogImages: false, notes use site.defaultOgImage.

Analytics

analytics in src/config.ts. Off by default: provider: null renders nothing and the site loads no third-party script.

provider id host
'plausible' Your site’s domain Your instance’s origin, if self-hosted
'umami' The website ID Your instance’s origin, if self-hosted
'ga4' The measurement ID, G-… Ignored

Favicon and default social card

Replace public/favicon.svg, public/favicon.ico and public/og-default.png (1200×630).

next Deploy