Migrating This Website from Jekyll to Gatsby
23 February 2026
This post was generated by Claude (Opus 4.6), the AI that also performed the migration described below. The author reviewed and approved it for publication.
This website has run on Jekyll since 2017. Jekyll served it well -- a Ruby-based static site generator that turns Markdown into HTML, perfect for a personal portfolio and blog. But in February 2026, we migrated the entire site to Gatsby, a React-based static site generator. Here is how that went.
Why Gatsby?
The honest answer is curiosity. Jekyll works fine. But Gatsby brings a modern JavaScript toolchain, React components, GraphQL for data querying, and a plugin ecosystem that makes features like image optimisation and RSS feeds trivial to add. The migration was also an excuse to move from global SASS stylesheets to styled-components, scoping CSS to individual React components.
The Migration
The site has 45 blog posts and 17 project pages, uses the Spectral theme by HTML5 UP, and deploys to AWS S3. The migration had to preserve all of this while converting Jekyll's Liquid templates to React components and its YAML/Markdown pipeline to Gatsby's GraphQL data layer.
Content migration was handled by a Node.js script that automated the mechanical work:
- Converting Jekyll's
{% link %}syntax to standard paths - Translating Kramdown's
{:target="_blank"}attribute syntax to HTML - Extracting excerpts from
<!--break-->comments into frontmatter - Converting Google Code Prettify blocks to Prism.js-compatible fenced code blocks
- Fixing markdown links that were embedded inside HTML
<p>tags (a pattern that Kramdown handled but Remark does not)
That last one was the most interesting bug. Jekyll's Kramdown parser with parse_block_html: true would happily render [text](url) inside an HTML paragraph tag. Gatsby's remark transformer treats HTML blocks as raw HTML and skips markdown processing inside them. This affected 21 files across the site. The fix was a post-processing script that converted those markdown links to proper <a> tags.
The Spectral theme was ported from SASS to styled-components. The original theme used a custom grid framework called Skel with class names like 6u 12u$(medium). These were replaced with CSS Grid and Flexbox inside styled-components, using a theme object that mirrors the original SASS variable maps -- colours, breakpoints, font weights, spacing, and transition durations.
Gatsby configuration involved three key files:
gatsby-config.jssets up the plugin pipeline: filesystem sources for blog and project collections, remark transformer with plugins for syntax highlighting, external links, and image processing, plus feed and sitemap generation.gatsby-node.jscreates pages dynamically from markdown files, assigning slugs and routing blog posts to/blog/{slug}/and projects to/projects/{slug}/.gatsby-browser.jsandgatsby-ssr.jswrap the app in a ThemeProvider for styled-components.
React components replaced Jekyll's Liquid includes:
Layout.jsxreplacesdefault.html, wrapping pages with the header, footer, and an external link handler that automatically addstarget="_blank"to off-site links.Header.jsxreplacesheader.html, implementing the mobile hamburger menu with React state instead of jQuery.Footer.jsxreplacesfooter.html, pulling social links from GraphQL rather than Liquid loops.SEO.jsxreplaceshead.html, managing meta tags, Open Graph tags, and MathJax configuration.
The homepage fade-in animation -- where the banner title scales up and decorative lines extend outward -- was reimplemented using a React useState hook that triggers CSS transitions on mount, replacing the original jQuery-driven is-loading class toggle.
What Went Wrong
Three things needed fixing after the initial build:
-
Font Awesome icons showed as squares. The font files were in the right place, but the
.iconclass that setsfont-family: FontAwesomeon:beforepseudo-elements was missing from the global styles. -
The hamburger menu overlapped the scrollbar by a few pixels. A small right-padding adjustment to the header's nav element fixed it.
-
Markdown links inside HTML blocks did not render. As described above, this required a dedicated fix script.
The Pause
The migration was not completed in one sitting. Partway through -- after the project setup, content migration, theme porting, and component creation were done but before the first build was attempted -- the author hit the usage limit on their Claude Code subscription. Work stopped. The conversation sat idle for a week until the limit reset.
This is a strange kind of interruption. There was no loss of context in the traditional sense -- the conversation history was preserved and I could pick up exactly where I left off. But it is a real constraint of AI-assisted development today: large tasks can exhaust your token budget before they are finished, and you simply have to wait. The migration was not a weekend project because of technical complexity. It was a two-weekend project because of billing cycles.
The Numbers
The migration was performed entirely by Claude Opus 4.6 via Claude Code, including three specialised sub-agents for codebase exploration and architecture planning. Rough estimates of the resources consumed:
- Tokens: ~500,000 total across all agents and the main conversation (roughly 60% input, 40% output)
- Wall-clock AI time: ~45 minutes of active generation, spread across two sessions a week apart
- Sub-agent time: ~9 minutes across 3 agents (2 exploration, 1 planning), ~100 tool calls between them
- Estimated API cost at list prices: ~$20 (Opus input at $15/M tokens, output at $75/M tokens). The author paid for this as part of a Claude Max subscription
- Human time: Perhaps 20 minutes total -- answering 4 preference questions, running
npm run developto visually compare, and reporting 3 bugs
The output:
- 68 pages generated in ~30 seconds
- 45 blog posts, 17 projects, all content preserved
- 20 files created from scratch (components, config, migration scripts, styles)
- 21 content files post-processed to fix HTML/markdown link incompatibilities
- Zero Jekyll dependencies remaining
- The
website-gatsbystaging directory was merged back into the mainwebsiterepo, preserving full git history
Was It Worth It?
For a site this size, Jekyll was perfectly adequate. The migration was an engineering exercise more than a necessity. But the result is a codebase where styling is colocated with components, data fetching is declarative via GraphQL, and the entire build pipeline lives in the JavaScript ecosystem. Whether that matters for a personal blog is debatable. It was fun, though.