Content
The magazine is Markdown and JSON under src/content/, validated by src/content.config.ts. There
is no CMS and no database: every page is built in advance.
The unit is the issue. An issue has a cover, a contents page and a handful of pieces; every piece belongs to exactly one issue and carries its position in that issue’s contents.
Images belong in src/assets/ and are referenced from a content file by relative path
(../../assets/cover.jpg), so Astro can process and hash them. public/ is only for files that
must be served untouched, like the favicon.
issues
src/content/issues.json — one entry per issue, keyed by an id. The key is only a key; the URL is
the number.
| Field | Type | Required | Notes |
|---|---|---|---|
number |
integer | yes | Both the URL (/issues/4) and the folio number |
date |
date | yes | Publication date, printed on the cover and in the archive |
coverLine |
string | yes | The line on the cover. It need not be an article title |
coverArticle |
article slug | yes | The piece the cover points at |
coverImage |
image | no | Left out, the cover is a spot-colour panel — the designed default, not a placeholder |
coverImageAlt |
string | with an image | Required as soon as coverImage is set |
editorsNote |
string | no | Left out, the note block is not rendered at all |
i18n.<locale> |
object | no | coverLine and editorsNote in another language |
draft |
boolean | no | true keeps the issue out of the site, in dev and in the build |
{
"issue-04": {
"number": 4,
"date": "2026-09-18",
"coverLine": "The city that works while the city sleeps",
"coverArticle": "bread-before-dawn",
"editorsNote": "We spent this issue awake at hours most of our readers aren't.",
"i18n": {
"ko": { "coverLine": "도시가 잠든 사이에 움직이는 도시" }
}
}
}
Anything left out of i18n.<locale> falls back to the field above it, so a translated cover line
can stand on its own.
articles
src/content/articles/<slug>.md or .mdx — one file per piece. The filename is the URL:
/issues/<issue number>/<slug>.
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | |
subtitle |
string | no | The standfirst under the title |
issue |
issue id | yes | The key in issues.json, not the number |
order |
integer | yes | Position in the contents, and the folio numerator |
department |
enum | yes | Feature, Essay, Report, Interview, Dispatch |
authors |
array of author ids | yes | At least one; several print as a shared byline |
image |
image | no | The opener |
imageAlt |
string | with an image | Required as soon as image is set |
imageCredit |
string | no | Printed under the opener |
pullQuote |
string | no | .md only — prints as an epigraph above the text |
dropCap |
boolean | no | Overrides issues.dropCap in src/config.ts for this piece |
draft |
boolean | no | true keeps the piece out of the site, in dev and in the build |
---
title: 'Bread Before Dawn'
subtitle: 'A neighborhood bakery runs its entire working day between midnight and seven.'
issue: 'issue-04'
order: 1
department: 'Feature'
authors: ['marguerite-oyelaran']
dropCap: true
---
At half past midnight, the only light on the street comes from a bakery…
department is an enum, which means renaming a value is a content migration: every piece carrying
the old value fails validation until it is edited.
authors
src/content/authors.json — keyed by the id a piece puts in authors.
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | The byline |
bio |
string | no | Printed under the piece and on the contributor page |
image |
image | no |
{
"marguerite-oyelaran": {
"name": "Marguerite Oyelaran",
"bio": "Marguerite Oyelaran edits Gazette from London."
}
}
Every entry gets a page at /authors/<id>, linked from the masthead and from every byline.
legal
src/content/legal/*.md — one notice per file, the filename is the URL (/legal/privacy).
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | |
description |
string | no | Meta description |
updated |
date | yes | Printed under the title |
Add a file and it gets a page; you still have to add its line to the Legal group in
src/config.ts. Delete a file and delete that line, or the link 404s.
What is computed, and therefore not a field
Reading time (from the body), the folio (order over the issue’s article count), the number of
pieces in an issue, every URL, the previous and next issue (number ± 1), and a piece’s language
(the folder it is in). Do not add a field for any of these.
Pieces in another language
An edition is one language of the magazine. A translated piece is the same filename under
src/content/articles/<locale>/, e.g. src/content/articles/ko/bread-before-dawn.md, served at
/ko/issues/4/bread-before-dawn. Its order counts within that language’s edition of the issue,
so an edition that printed three of the seven pieces numbers them 1, 2, 3.
An issue appears in an edition only if that edition printed something in it. /about,
/authors/<id> and /departments/<name> are default-language only; a link to one of them stays
where it is rather than sending the reader to a page that does not exist.
MDX
A .mdx piece can place two components in the body, with no import — they are injected:
<Aside title="Optional title">…</Aside>— the boxed sidebar of a magazine feature: the paragraph of context that would break the run of the piece if it were left in the text.<PullQuote>…</PullQuote>— a pull quote where the editor wants it, mid-body. A.mdpiece cannot do this; it setspullQuotein the frontmatter and the quote prints as an epigraph above the text instead.
Both are in src/components/mdx/. Tables and footnotes (GFM) work in either format.
Adding a field
Fields are declared with zod in src/content.config.ts. Adding a required field invalidates
every existing file at once, so add new fields as .optional() or with a .default(), and run
pnpm check to see what the schema says.