A Docs Migration Story: Moving Hugo to Gatsby
Documentation is rarely the flashiest part of a developer platform, but it's often the first thing developers interact with. When the Cloudflare Workers docs team decided to migrate their site from Hugo to Gatsby, the motivation wasn't just about aesthetics — it was about freeing the content from the constraints of one templating system so it could evolve into something more interactive.
Why Leave Hugo Behind
Hugo had been good to the Workers docs. It served pages quickly and built fast, which made it an excellent choice for a straightforward static site. Problems only surfaced when the team wanted to push beyond simple pages. Making the docs more interactive and growing the volume of generated content exposed the limits of Hugo's templating approach.
The crux of the issue: after working with JSX and TypeScript, dropping back to string-based templating languages feels like a step backward. Complex tasks like building a sidebar — logic that's trivial in a component-based framework — become genuinely hard to parse in Hugo's template syntax. The code required to render a single list item in a sidebar involved so much template logic that it was difficult to even identify which element was being rendered.
Compare that to the equivalent React component. Gatsby, at its core, is a static site generator built on React, which means you can leverage component architecture and TypeScript throughout. The same sidebar item becomes a small, readable piece of TSX. That readability mattered because the team had bigger plans for the docs: a redesign with interactivity, support for multiple coding languages, and features like a template gallery pulling from a registry service via an API.
Go Templates provide an extremely simple template language that adheres to the belief that only the most basic of logic belongs in the template or view layer.
That's a direct quote from the Hugo documentation. For a docs site that needed complex logic — dynamic sidebars, interactive code samples, personalized content — the simplicity became a ceiling. Gatsby, with React underneath, didn't have that ceiling.
Migrate, Don't Rewrite
The obvious alternative would have been to scrap the Hugo site entirely and rebuild from scratch. It's tempting to start with a blank page, but that would mean throwing away years of carefully tuned styling, SEO, tagging, and analytics setup — and reintroducing bugs along the way. The team decided on a different approach: keep the existing design and content as intact as possible, and convert the site piece-by-piece, template-by-template, into React components.
Preserving the existing markdown files and their frontmatter was critical. Not only did that maintain the version history of each document, but it also meant that other Cloudflare teams still using Hugo for their own docs could eventually move their content into the Gatsby repository without needing to restructure anything.
The Migration Mechanics
Getting Markdown into Gatsby
Gatsby's data layer runs on GraphQL. During startup, plugins feed content and data into the GraphQL schema, and pages query that data when they're created. That's a departure from Hugo's model, where you drop markdown into a content folder and the templating system figures out what to do with it.
The migration used MDX, Gatsby's plugin for parsing markdown. Configuration in gatsby-config.js tells the plugin to pick up all .md and .mdx files from the src/content folder and create nodes in GraphQL. Then, in gatsby-node.js, a page is created for each node, using a template component called markdownTemplate.tsx. On every Gatsby run, the onCreateNode hook fires for each node; if it's MDX, its content and fields get passed to the template component for rendering.
The resulting page template component is minimal: it receives the MDX content, parses it through the MDX renderer, and returns a React page component. A few lines of code replace an entire pipeline of template logic.
The Sidebar: A Case Study
The sidebar was the hardest part of the migration — and the most instructive. In Hugo, determining what CSS classes to apply to an li element required understanding predefined variables like .sect, IsSection, Params.head, and Params.Hidden. The logic was all there, but parsing it took time, and tracking down variable definitions was its own chore.
React simplifies this by making the logic explicit. Instead of interpreting template variables, you write code that directly determines which classes to add:
parent— for top-level sectionsalways-open— for sections that should stay expandedactive— for the current page
These can be expressed in a local variable string that gets applied to the li element. The React Location component handles determining which page is active. For ancestor relationships, Hugo had a built-in isAncestor feature; in React that logic is written out locally, which is arguably clearer since you don't need to look up framework-specific definitions.
Handling Children in the Sidebar
The trickiest part was rendering child pages. Hugo's template logic for grabbing the first layer of children involved complex calls into the site's data model, and much of it went unused in practice. In Gatsby, the approach is data-driven: query all markdown pages, filter for those whose parent path matches the current page, and render them as a list. The sidebar component uses a static GraphQL query to get all page data, so Sidebar.tsx needs no props from the parent component — the data layer handles state, and Location handles the active path.
Lessons Learned Along the Way
This wasn't a single-step process. Every Hugo partial, template, and interactive element had to be converted into React components. That was an enjoyable challenge, but a few hard-won lessons stood out:
- Be careful, don't be scared. There's a difference between carefully preserving what works and being afraid to improve things because a rewrite might break them. use the old templates as a reference, but not as gospel. staging environments exist for testing — write things the right way, not the old way.
- Migrate content in stages. Trying to move everything at once creates chaos. Get a few pages of each type fully working before shifting the bulk of the content over; this prevents intermediate pull requests from breaking things.
- Compromise carefully. It's fine to drop a feature temporarily during the migration — but verify with real users that it's actually expendable. Simple things like header anchor tags (which Hugo generated automatically but Gatsby requires custom MDX components for) can be more valued than assumed.
- GraphQL has a learning curve. Even experienced React developers will need time to adjust to Gatsby's data layer. Once the initial setup is complete, though, it's a very smooth experience.
The payoff came after the migration was complete. Implementing the redesign on a React and Gatsby codebase is far simpler than it would have been on Hugo templates. The site also gained flexibility that helps with performance and SEO, leveraging the framework's configuration options rather than being locked into a generator's defaults.



