Skip to main content
/

How I Built scriptr.dev

Jacob Coffee & Claude··1.4k words·13 min read·

Migrating from academicpages to a custom Next.js site with MDX, Keystatic CMS, and Claude Code - a deep dive into the architecture, custom components, and AI-assisted development workflow.

After years of using academicpages - a Jekyll template forked from Minimal Mistakes - I decided it was time for something custom. Something that could grow with my needs, showcase my work properly, and let me experiment with modern web tech.

This post covers the full journey: why I migrated, the tech stack choices, the custom component system, and how Claude Code helped build it all.

Why Leave academicpages?

academicpages served me well. It's a solid Jekyll template designed for academics - publications, talks, teaching, CV. But I kept hitting walls:

  • Ruby/Jekyll friction: Gemfile conflicts, slow builds, dependency hell
  • Limited customization: Fighting against Liquid templates and SCSS overrides
  • No component model: Copy-pasting HTML snippets instead of composable pieces
  • Static content: No easy way to reference GitHub issues, PyPI packages, or other dynamic data
  • Design constraints: The Minimal Mistakes aesthetic didn't match my vision

I wanted something that felt like mine - a site that reflected how I actually work and think about technical content.

The New Stack

Next.js 16 + React 19 + Tailwind CSS 4 + Bun + MDX

Why This Combination?

Next.js 16 with static export gives me the best of both worlds - modern React DX with GitHub Pages deployment. No server required, just static HTML/CSS/JS.

React 19 brings Server Components, which means MDX compilation happens at build time. No client-side JavaScript for rendering blog content.

Tailwind CSS 4 with the new CSS-first config. The @theme directive is cleaner than JS config files, and the performance is noticeably better.

Bun because it's fast. bun install in 2 seconds vs npm install in 30. The DX improvement is real.

MDX because I want to write Markdown but embed React components. More on this below.


The Design System

The design leans into a border-grid aesthetic with monospace headers and a terminal-like feel. It fits the infrastructure/engineering vibe I was going for.

Themes

The site ships with four themes:

  • Light - Clean, high contrast for readability
  • Dark - Easy on the eyes, my default
  • PyCon - Based on whatever the styleguide and PyCon US theme are.
  • AMOLED - True black for OLED screens

Theme switching uses next-themes with CSS custom properties. Each theme defines its own --background, --foreground, --primary, etc.


The MDX Component Library

One of the most valuable investments was building a library of inline MDX components for referencing external resources. Instead of plain markdown links, I wanted semantic components that render as styled badges with icons and consistent behavior.

The library covers several categories:

  • Package References - <PyPi /> and <Npm /> for linking to package registries
  • GitHub References - <GhUser />, <GhRepo />, <GhIssue />, <GhPr /> for GitHub entities with avatar previews
  • PEP References - <Pep /> with auto-synced titles from a local cache
  • Internal References - <BlogRef />, <TalkRef />, <CVRef />, <ProjectRef /> for site navigation
  • Content Components - <Callout /> for alerts and <ConversationThread /> for dialogue formatting

For full examples and usage of all components, see the styleguide.


Auto-Extraction

One thing I wanted was automatic metadata extraction. If I use <Pep number={810} /> in a post, it should show up in the sidebar without me adding it to frontmatter.

The build process:

  1. Reads the raw MDX content
  2. Regex extracts <Pep number={X} /> patterns
  3. Merges extracted PEPs with frontmatter-declared PEPs
  4. Dedupes and sorts

Same for external links - markdown [text](url) patterns get extracted and displayed in a "Links Mentioned" section.


Content Architecture

Authors

Authors live in content/authors/*.json:

{
  "id": "jacob-coffee",
  "name": "Jacob Coffee",
  "bio": "Infrastructure Engineer at PSF...",
  "avatar": "https://github.com/JacobCoffee.png",
  "github": "https://github.com/JacobCoffee",
  "twitter": "https://x.com/_scriptr"
}

Blog posts reference authors by ID. The AuthorSidebar component resolves them and displays bios, avatars, and social links.

Experience (CV)

Experience entries support multiple roles within a company:

{
  "id": "psf",
  "company": "Python Software Foundation",
  "roles": [
    { "title": "Infrastructure Engineer", "period": "Jul 2024 — Present", "current": true },
    { "title": "Infrastructure Staff", "period": "Jan 2024 — Jul 2024" }
  ]
}

The CV page renders these with a timeline, "NEW" badges for recent roles, and collapsible bullet points.

Talks

Talks have rich metadata:

title: "Lazy Imports: PEP 810"
date: "2026-04-11"
time: "11:15"
end_time: "12:15"
timezone: "America/Chicago"
venue: "PyTexas 2026"
location: "Austin, Texas"
slides_url: "..."
video_url: "..."
recording_available: true

The talk page shows countdowns for upcoming talks, calendar export (Google, Outlook, iCal), and video embeds with chapter markers.


Keystatic CMS

For content editing, I integrated Keystatic - a Git-based CMS that works with local files.

Why Keystatic?

  • Git-native: Edits are just file changes, committed to the repo
  • Local mode: Works in dev without any backend
  • Schema validation: TypeScript-powered field definitions
  • MDX support: Inline component insertion in the editor

The Setup

// keystatic.config.tsx
export default config({
  storage: process.env.NODE_ENV === "development"
    ? { kind: "local" }
    : { kind: "github", repo: { owner: "JacobCoffee", name: "scriptr.dev" } },
  ui: {
    brand: { name: "scriptr.dev", mark: ScriptrMark },
    navigation: {
      Content: ["posts", "talks"],
      People: ["authors"],
      CV: ["experience"],
    },
  },
  collections: {
    posts: collection({
      previewUrl: "/blog/{slug}",
      // ... schema
    }),
  },
});

The preview URL feature is key - click a button in Keystatic and jump directly to the rendered page.

Production Caveat

Keystatic requires a server for GitHub mode. Since I deploy to GitHub Pages (static export), the /keystatic route shows an "Access Denied" page in production. Local editing only.


Building with Claude Code

Here's the part that might surprise you: most of this site was built with Claude Code.

The Workflow

  1. Planning: Describe what I want in natural language
  2. Implementation: Claude writes the code, I review
  3. Iteration: "Make the sidebar float right" / "Add dark mode support"
  4. Debugging: Paste errors, get fixes

What Worked Well

  • Component generation: "Create a PyPi badge component that links to pypi.org" → working component in seconds
  • CSS/Tailwind: Claude knows Tailwind classes better than I do
  • Type definitions: Generating TypeScript interfaces from examples
  • Refactoring: "Extract this into a reusable hook" → clean abstraction

What Needed Human Judgment

  • Design decisions: Claude can implement any design, but choosing what to build is still on me
  • Performance tradeoffs: When to lazy-load, what to cache, bundle size concerns
  • Content strategy: What to write about, how to structure information

The Numbers

Looking at the git history:

  • ~45 tasks tracked in plan.json
  • 93% completion rate
  • 7 phases from foundation to enhancement
  • Multiple agent types: ui-engineer, software-architect, github-git-expert

The plan.json file became a coordination mechanism - tracking what's done, what's blocked, what's next.


Deployment

GitHub Pages + Static Export

// next.config.ts
const config: NextConfig = {
  output: "export",
  images: { unoptimized: true },
};

The output: "export" flag tells Next.js to generate static HTML. No server functions, no API routes (except for Keystatic's local dev server).

GitHub Actions

# .github/workflows/deploy.yml
- name: Build
  run: bun run build
 
- name: Deploy to GitHub Pages
  uses: actions/deploy-pages@v4

Push to main → build → deploy. Takes about 2 minutes.

Data Sync Scripts

Some data needs periodic updates:

  • sync:repos - Fetch GitHub star counts
  • sync:sponsors - Pull sponsor data from GitHub Sponsors API
  • sync:experience - Validate experience JSON

These run on schedule via GitHub Actions and commit changes automatically.


What's Next

The site is live, but there's always more:

  • [ ] Sponsor-exclusive early access - Let sponsors see scheduled posts before release
  • [ ] Copy button on titles - Quick sharing of deep links
  • [ ] Search - Full-text search across blog and talks

Lessons Learned

  1. Start with content structure - The data model drives everything else
  2. MDX components are powerful - Invest in a good component library early
  3. AI-assisted development is real - Claude Code genuinely accelerated this project
  4. Static sites scale - GitHub Pages handles traffic fine, no server costs
  5. Own your platform - Custom > template when you have specific needs

If you're thinking about building a personal site, I hope this gives you some ideas.

Questions? Find me on @scriptr.dev or @_scriptr.