The Stack

How this template is built — and how to add every kind of content.

Cover image for The Stack
Contents14

This template is a static Astro site: content-driven, framework-free, and built entirely to plain HTML at build time. The first half of this note describes how the pieces fit together; the second half is a practical guide — follow it and you can add every kind of content the site supports.

Architecture

Pages are .astro files, content is Markdown and MDX, and everything renders to plain HTML at build time. There’s no client framework and no runtime JS beyond a few small vanilla scripts (the theme toggle, the p5 sketch loader, the image lightbox, the hero sketch).

Content collections

Content is split by type, not topic, into content collections. Each maps to a section of the site:

CollectionFolderSectionURLFor
projectssrc/content/projects/Explorations/explorations/Interactive & visual work
notessrc/content/notes/Notes/notes/Technical writeups & references
blogsrc/content/blog/Personal Log/log/Longer-form writing
gallerysrc/content/gallery/Gallery/gallery/Image albums

Every collection’s schema lives in src/content.config.ts. Topic is a soft property carried in tags, so adding a new subject never needs a new collection — just a new tag. (The blog collection powers the “Personal Log” section; the internal name is kept as blog.)

Shared front matter

All four collections extend one base shape, so components can render any of them interchangeably:

title: string          # required
subtitle: string       # optional — shown under the title and on cards
date: YYYY-MM-DD        # required — drives sorting and RSS
updated: YYYY-MM-DD     # optional
tags: [a, b, c]         # drives filtering and the generated tag nav
cover: /img/…/x.png     # path under /public — card + social image
summary: string         # meta description / card text
toc: true               # collapsible table of contents at the top
draft: true             # excluded from the build entirely
featured: true          # eligible for the homepage "Featured" rail
unlisted: true          # builds and is reachable by URL, but hidden from lists + RSS

The rest of the stack

  • Images run through Astro’s image pipeline. Source files live beside the content that uses them; at build Astro emits responsive WebP/AVIF variants and fails loudly if a referenced image is missing. Galleries and post figures share a click-to-zoom lightbox.
  • Generative pieces are p5.js sketches in instance mode, mounted lazily and code-split so a page only loads a sketch’s JS once it scrolls into view.
  • Maths is LaTeX, rendered to HTML at build with KaTeX (remark-math + rehype-katex) — no runtime MathJax.
  • Code is highlighted with Shiki, light/dark themes swapping on the theme toggle.
  • Deploy: hosted on GitHub Pages, shipped by a GitHub Actions workflow on every push to main.

One custom build step worth knowing: a small rehype plugin (rehype-drop-leading-h1.mjs) drops a leading # heading from any body, because the layout already renders the front-matter title as the page <h1>. Use ## for your first section heading, not #.


Adding content

Everything below is additive: drop a file in the right folder, fill the front matter, and it appears in its section — no nav edits, no manifest to update. Nav lives in src/site.config.ts and rarely changes.

A standalone note

Create src/content/notes/my-note.md. The filename becomes the URL slug (/notes/my-note/).

---
title: My Note
subtitle: One line on what it is.
date: 2026-07-05
tags: [linux, reference]
summary: A short description for the card and meta tags.
toc: true          # optional — adds a collapsible contents box
---

Intro paragraph. Then start sections at level two.

## First section

Body text, `inline code`, and fenced code blocks:

~~~bash
echo "hello"
~~~

## Maths

Inline $a^2 + b^2 = c^2$ and display maths both render at build:

$$
\int_0^1 x^2 \, dx = \tfrac{1}{3}
$$

That’s a complete note. The same front matter and body rules apply to projects (src/content/projects/, → /explorations/) and blog / Personal Log posts (src/content/blog/, → /log/).

Useful note-only front-matter flags:

  • pinned: true — lifts the note into a “Pinned” side rail on the Notes index (this note uses it). Good for evergreen references.
  • hideDate: true — hides the date on the card and the page. For evergreen content where a publish date is meaningless. The date field is still required (it drives sort order) — it just isn’t shown.

A project (Exploration) with a repo/demo

Projects add three optional fields on top of the base shape:

featured: true                          # eligible for the homepage rail
libs: [p5]                              # client libs the piece needs
repo: https://github.com/you/thing     # "Source ↗" button
demo: https://you.github.io/thing/      # "Live demo ↗" button

Images in a post

Co-locate images next to the note and import them so the pipeline optimises them. The convention is a _media/ folder beside the content file. This requires MDX (.mdx), because you’re using components.

Single image with a caption — use the Figure component:

---
title: My Note
date: 2026-07-05
---

import Figure from '../../components/Figure.astro';
import shot from './_media/screenshot.webp';

<Figure src={shot} alt="Describe the image." caption="Optional caption." />

A grid of images — use Gallery (inside a post; distinct from the Gallery section):

import Gallery from '../../components/Gallery.astro';
import a from './_media/a.webp';
import b from './_media/b.webp';

<Gallery
  min={14}
  caption="Optional caption."
  items={[
    { src: a, alt: 'First.' },
    { src: b, alt: 'Second.' },
  ]}
/>

A looping video (a lightweight GIF replacement) — use LoopVideo. Import the .mp4 directly; Vite hands back its URL:

import LoopVideo from '../../components/LoopVideo.astro';
import clip from './_media/clip.mp4';

<LoopVideo mp4={clip} alt="Describe the motion." caption="Caption." width="600px" />

Import paths are relative to the file. A note at notes/foo.mdx reaches components with ../../components/…; one nested at notes/series/week-1.mdx needs ../../../components/….

A live interactive sketch

Sketches are two pieces: a module in src/sketches/ and a <P5Sketch> tag in an .mdx post.

1. Write the sketch at src/sketches/my-sketch.ts. It default-exports an instance-mode function:

import type p5 from 'p5';

export default function (p: p5) {
  p.setup = () => {
    p.createCanvas(700, 700);
  };
  p.draw = () => {
    p.background(20);
    p.circle(p.mouseX, p.mouseY, 40);
  };
}

2. Drop it into a post (.mdx). src is the module filename without extension:

import P5Sketch from '../../components/P5Sketch.astro';

<P5Sketch
  src="my-sketch"
  title="My sketch"
  caption="Optional caption under the canvas."
  ratio="1 / 1"
/>

The component code-splits each sketch and only mounts it when it scrolls into view, and gives every sketch a pause/FPS toolbar for free. Sketches that create p5 controls (createSlider, createRadio) get a consistent styled panel — see the existing flow-field and flocking sketches for the pattern.

The Gallery section is its own collection with a different shape: one folder per album, one index.md inside, and the images co-located next to it.

1. Make the folder src/content/gallery/my-album/ and drop the images in (.webp recommended).

2. Write src/content/gallery/my-album/index.md:

---
title: My Album
description: One line describing the set.
date: 2026-07-05
section: Inktober        # groups related albums into one row on /gallery
sectionOrder: 1          # which row comes first (lower = earlier)
order: 1                 # this album's position within its row
cover: ./day-01.webp     # must be one of the images below
images:
  - src: ./day-01.webp
    caption: 'Day 1: Ring'
  - src: ./day-02.webp
    caption: 'Day 2: Bait'
---

Every src is validated at build — a missing or misspelled file fails the build loudly, which is the point. section / sectionOrder / order are all optional; omit them and albums fall back to date (newest first) in an “Other” row.

Optionally cross-link an album to the post that tells its story:

story:
  href: /log/my-story-post/
  label: 'Read the story → My Story Post.'

Multi-page note series

A series is a set of notes with one landing page and several member pages that share a persistent sidebar (series index + on-this-page contents + a back-to-home button) and prev/next links. The Creative Coding notes are the worked example.

The whole thing is data-driven — the sidebar, ordering, and prev/next are all derived at build time from front matter (src/lib/series.ts). There is no manifest to edit. Adding a page is: drop a file with the right two fields.

1. The landing page

Create a folder for the series and an index.mdx that is the series home. Pick a short series id — every page in the set shares it.

---
title: My Course
subtitle: Notes, part by part.
date: 2026-07-05
tags: [reference, notes]
series: my-course        # the shared id for the whole set
seriesHome: true         # marks THIS file as the landing page (one per series)
seriesTitle: My Course   # display name used in the sidebar
summary: What the series covers.
---

Intro to the series. Link the parts however you like — a simple list works:

## Parts

- **[Part 1 — Basics](/notes/my-course/part-1/)** — what it covers.
- **[Part 2 — More](/notes/my-course/part-2/)** — what it covers.

The landing page is listed on the Notes index (it’s the entry point). It lives at /notes/my-course/.

2. Member pages

Each part is its own file in the same folder. Three fields do the work:

---
title: Part 1 — Basics
date: 2026-07-05
series: my-course        # same id → joins this series
part: 1                  # ordering within the series (lower = earlier; gaps are fine)
unlisted: true           # keep it OUT of the Notes index (reached via the series)
---

## A section

Body content — every kind of content above (images, sketches, videos) works here too.

That’s the entire contract:

  • series ties a page to the set.
  • part orders the members in the sidebar and drives prev/next.
  • unlisted hides members from the Notes index and RSS, so only the landing page shows in listings — members are reached through the series navigation.
  • seriesHome + seriesTitle (landing page only) name the set.

Add a new part by dropping in another file with the next part number — it auto-joins the sidebar, the contents, and the prev/next chain. Nothing else to touch.


Colophon

Built with Astro; the architecture, component structure, and image pipeline were wired up with Claude Code, Anthropic’s coding agent.