Design showcase · build notes
The margin is the product.
The Marginalia is a literary magazine whose one idea — readers writing back — is also its one interface idea. These are the build notes: what carries the site, why Astro, and the exact mechanics of the live margin notes.
The idea
Most magazine sites treat the margin as leftover space. This one treats it as the editorial voice: every essay is set in a text column with a wide outer margin, and the margin holds real annotations — the author second-guessing herself, the editor checking a fact. As you read, the note beside the paragraph you’re on wakes up; the rest sit faded, waiting their turn. The homepage repeats the trick everywhere it can: the masthead has its own margin note, the table of contents previews each essay’s opening line in the margin, even the subscribe form keeps its fine print out there.
The stack — why Astro content collections
Five essays, one layout, zero client-side framework: this is exactly the shape Astro is for.
The essays live as markdown in src/content/essays/ with a zod schema
(src/content.config.ts) that types every field the design depends on — title,
dek, section, reading time, the opening line the TOC previews, and the notes themselves:
notes: z.record(
z.object({
kind: z.enum(['author', 'editor']),
text: z.string(),
})
)
In the essay body, [[noteId]] anchors a note to the sentence it annotates.
A ~50-line renderer (src/lib/essay.ts) splits the body into paragraphs and
expands each anchor into a reference mark — the classical compositor’s sequence
* † ‡ § ‖ ¶ — while collecting that paragraph’s notes. Adding an essay is
adding a markdown file; the TOC, pager, folios and margin all follow from the collection.
Signature technique — scroll-synced margin notes
The layout does the hard part. Each paragraph renders as its own two-column grid row, so a note is born beside its paragraph — no JavaScript positioning, no absolute offsets to keep in sync:
.pg {
display: grid;
grid-template-columns: minmax(0, var(--measure)) var(--marg);
column-gap: clamp(2.5rem, 5vw, 4.5rem);
}
.pg > p { grid-column: 1; }
.pg > .mnote { grid-column: 2; grid-row: 1; }
Notes rest at opacity: .38, nudged 10px into the margin. One
IntersectionObserver watches only the paragraphs that carry notes, using a root margin that
describes the reading band of the viewport — roughly where your eyes are:
const io = new IntersectionObserver((entries) => {
for (const e of entries)
e.target.classList.toggle('live', e.isIntersecting);
}, { rootMargin: '-18% 0px -46% 0px' });
flow.querySelectorAll('.pg.has-note')
.forEach((pg) => io.observe(pg)); .js .can-sync .pg.live .mnote {
opacity: 1;
transform: none;
border-color: var(--oxblood); /* the hairline turns oxblood */
}
Below 880px the margin folds in. The same markup becomes a footnote system: notes hide
behind their reference marks (display:none, gated on the .js
class so nothing is ever lost without scripts), and the mark becomes a tap target that
toggles the note open beneath its paragraph with correct
aria-expanded/aria-controls wiring. With
prefers-reduced-motion, all of it stands down: notes are simply always visible,
fully opaque, static — the print experience.
Details that matter
- Drop caps with a fallback.
initial-letter: 3where supported, a hand-tuned::first-letterfloat everywhere else — both in Fraunces Black at opsz 144, in oxblood. - The reading-progress hairline is a 2px fixed bar scaled by
-rect.top / (rect.height - innerHeight)in a rAF-throttled scroll handler — it reaches 1 exactly when the essay’s last line clears the fold. - The paper is drawn, not photographed. An SVG
feTurbulencedata-URI at 6% opacity, multiply-blended over#efe9dd, plus a fixed hairline frame inset from the viewport — the whole site sits inside a printed page. - One easing everywhere:
cubic-bezier(.3,.86,.34,1)— a page turn that lands softly. One accent everywhere: oxblood#7a2233, which passes WCAG AA on this paper at 8.2:1. - Semantics: notes are
<aside role="note">with labels; reference marks are real<button>s; the TOC is an<ol>; focus rings are visible and oxblood.
Ship it
Astro builds straight into docs/ so GitHub Pages can serve from the main
branch — no Actions, no second branch:
// astro.config.mjs
export default defineConfig({
site: 'https://bswxyz.github.io',
base: '/the-marginalia',
outDir: './docs',
}); npm run build
gh repo create bswxyz/the-marginalia --public --source . --push
gh api --method POST /repos/bswxyz/the-marginalia/pages \
-f 'source[branch]=main' -f 'source[path]=/docs'
A .nojekyll in public/ rides along into the build. Internal links
go through import.meta.env.BASE_URL, which is the difference between a site
that works at /the-marginalia/ and one that only worked on localhost.
The Marginalia is a design-showcase concept — the essays, authors and letters are fictional. The README maps what’s demo and what a production magazine would still need.