Devlog posts and project entries are both markdown files with YAML frontmatter, read off disk at request time (not bundled by Vite) via content.ts's shared readEntryFile/toDateString helpers, then rendered independently by devlog.ts and projects.ts.
content.ts is deliberately small: only the identical file-reading and date-normalization logic is shared. devlog.ts's and projects.ts's actual toMeta()/getAll()/get() functions are separate, since their output shapes genuinely differ (devlog has series/toc/searchText/footnotes; projects has name/href/live) — this was a conscious choice not to force a shared abstraction over two things that only look similar at the file-I/O layer.
gray-matter (via js-yaml) parses an unquoted frontmatter date like date: 2026-06-01 into a native JS Date, not a string. String(date) on that yields a verbose, timezone-dependent format that doesn't sort lexicographically — toDateString() normalizes any such value to YYYY-MM-DD so list sorting and prev/next comparisons work correctly regardless of how the date was written in frontmatter.
Devlog rendering (devlog.ts)
- Reading time: word count / 200wpm, rounded, minimum 1 minute.
- Search text:
toSearchText()strips markdown syntax (code fences, inline code, images, links, footnote refs/defs, HTML tags, remaining markdown punctuation) down to lowercased plain text, so the devlog list page can search post bodies client-side without shipping raw markdown into the match. - Footnotes: hand-rolled (not a
markedplugin) —applyFootnotes()extracts[^label]: textblock definitions and rewrites[^label]refs into numbered<sup>anchors, run on raw markdown beforemarked.parse. Chosen to stay independent ofmarked's renderer API rather than pulling in a plugin for something this narrow in scope. - Heading anchors / TOC:
addHeadingAnchors()post-processes rendered HTML (not amarkedrenderer hook, same rationale as footnotes) to give every<h2>/<h3>a stable, de-duplicated slug id, building the table-of-contents list as a side effect.decodeHtmlEntities()exists because heading text also gets used as plain text (TOC labels via Svelte's{text}interpolation, which doesn't decode entities the way{@html}does) — without it, a heading like--prod doesn't mean...would show a literal'in the TOC. - Drafts:
draft: truein frontmatter excludes a post fromgetAllDevlogEntries()— which backs the list page, RSS, sitemap, tag pages, and prev/next/related computations, so setting it once hides the post everywhere at once.getDevlogEntry(slug)(the single-post loader) deliberately does not filter drafts, so a draft post is still viewable by direct URL — useful for previewing before publishing, but be aware an unlisted draft is not actually access-controlled.
Markdown posts can embed interactive Svelte components via <div data-embed="Name"></div>. Available components (Counter, Callout, Terminal, TerminalReplay, CodeDiff, BeforeAfter, Mermaid — see src/lib/components/devlog-embeds/) are registered by name in registry.ts. mount-embeds.ts is a Svelte action that scans rendered post HTML client-side and mounts the matching component into each placeholder div.
Adding a new embeddable component requires two steps: create the component in devlog-embeds/, and register it by name in registry.ts.
Devlog/project frontmatter's cover and images (gallery) fields, and any image embedded in a post body, are just URL strings — nothing in the content pipeline itself uploads or validates them. The admin editors (+layout.svelte and friends under src/routes/admin/devlog/src/routes/admin/projects) source those URLs from the media library (media.ts, MEDIA_DIR — see environment.md) via MediaPicker.svelte/MediaLibrary.svelte: a "Browse…" button fills cover, "Add image…" appends to images (multi-select), and pasting/dropping an image into the body textarea uploads it and inserts  at the cursor. This replaced an earlier manual workflow of dropping a file into static/ and typing its path — static/ is baked into the Docker image at build time (see deployment.md), so it never worked from a running prod session and didn't survive a redeploy either. Media URLs are served publicly (no login) from src/routes/media/[filename]/+server.ts, since cover/gallery/body images appear on public pages.
og.ts generates per-post/per-project Open Graph preview images (used by [slug]/og.png/+server.ts routes) using satori + resvg — this needs real font bytes rather than system fonts, which is why the font files in src/lib/server/fonts/ are read from disk at request time and explicitly copied into the Docker runtime image (see deployment.md), rather than routed through Vite's asset pipeline like other static assets.
Those bytes are the site's own three faces (see design.md § Typography), cut from the exact @fontsource packages app.css imports — so a social preview and the page it links to are set in the same type. satori reads ttf/otf/woff but not woff2, and @fontsource ships Archivo and JetBrains Mono as variable woff2 only, so those two are checked in as static TTFs instanced at weight 400; Instrument Serif ships a plain .woff and is copied straight across. To regenerate them after a @fontsource bump (needs pip install fonttools brotli):
from fontTools.ttLib import TTFont
from fontTools.varLib import instancer
for pkg, stem, out in [
("@fontsource-variable/archivo", "archivo-latin-wght-normal", "Archivo-Regular.ttf"),
("@fontsource-variable/jetbrains-mono", "jetbrains-mono-latin-wght-normal", "JetBrainsMono-Regular.ttf"),
]:
f = TTFont(f"node_modules/{pkg}/files/{stem}.woff2")
static = instancer.instantiateVariableFont(f, {"wght": 400}, inplace=True, updateFontNames=True)
static.flavor = None # drop the woff2 wrapper satori can't read
static.save(f"src/lib/server/fonts/{out}")cp node_modules/@fontsource/instrument-serif/files/instrument-serif-latin-400-normal.woff src/lib/server/fonts/InstrumentSerif-Regular.woff handles the serif. These are the latin subsets, so a title needing latin-ext would render tofu — og.test.ts renders a card per shape and asserts a real PNG comes back, which is the only thing that catches a font that stopped loading (satori doesn't throw; it just drops the glyphs).
xml.ts is a shared helper for the RSS feeds (devlog/rss.xml, projects/rss.xml) and sitemap.xml.