eigenslur
← Back to blog

A Template System for Research Writing

How I built a reusable architecture for publishing paper breakdowns and blog posts on one site — composable primitives, data-driven layouts, and responsive typography.

5 min read
  • engineering
  • design systems
  • next.js

I kept rewriting the same layout every time I wanted to publish a paper breakdown. Different fonts, inconsistent spacing, a slightly different navigation each time. Eventually I gave up and built a small template system on top of Next.js — and it made writing so much easier that I want to explain the moving parts.

The goal: one cohesive design language that can render both paper breakdowns and short blog posts, stay pleasant on mobile and desktop, and be easy to extend without touching layout code.

Three layers

Everything in this site is split into three layers that map cleanly onto Next.js conventions:

  • Layout: PaperLayout (hero + side nav + column) or PostLayout (centered editorial column). Both share a SiteHeader and SiteFooter.
  • Primitives: Section, Prose, Callout, CalloutGrid, KeyConceptGrid, Roadmap, CodeBlock, MathEquation, CTASection, Citation.
  • Registry: A single lib/content/registry.ts drives the index pages. To add a paper or post, drop an entry in and create a page file.

Composition over configuration

Instead of a giant config object that a single renderer interprets, each post or paper is just a React component. The PostLayout handles the masthead, the body column, and the footer. Everything inside is composed with primitive components.

Why not MDX?

MDX is lovely for mixed markdown/JSX, but for rich layouts — side nav, scroll-snap, animated grids — I find pure React components easier to reason about. When I need pure prose, <Prose> gives me the same typographic defaults as the generated content.

A minimal blog post template

Here is the entire shape of a new post. Drop this file into app/blog/your-slug/page.tsx, add an entry to the registry, and you're done — responsive design, tags, reading time and dates are all handled:

app/blog/your-post/page.tsx
python
// app/blog/your-post/page.tsx
import { PostLayout } from "@/components/layouts"
import { Lede, Prose, Callout, CodeBlock } from "@/components/content"

export default function YourPost() {
  return (
    <PostLayout
      title="Your title here"
      description="A short subtitle."
      date="2026-04-16"
      readingMinutes={5}
      tags={["engineering"]}
    >
      <Lede>Opening paragraph...</Lede>

      <Callout title="A point worth highlighting">
        Any ReactNode content goes here.
      </Callout>

      <CodeBlock language="python" code={"print('hello')"} />
    </PostLayout>
  )
}

Responsive by default

Both layouts start from a mobile-first column and add breakpoints as they grow:

  • Paper: Side navigation dots collapse into a bottom-right hamburger below lg; content column centers itself on mobile and shifts right of the rail on desktop.
  • Post: Single 2xl-max column with generous line-height; masthead adapts to any title length via text-balance.
  • Cards: Content grids use one column on phones, two from lg, and keep consistent gap spacing.

Where next

The template will keep evolving. If you want to read actual content built on top of it, try the two papers: Topos-Theoretic Probability and Categorical Probability. Both render through the exact same primitives as this post.