Skip to content
Engineering

Migrating My Blog from Jekyll to Next.js and Tailwind CSS

A modern web development workflow for blogging

After a few years of on , I decided it was time for an upgrade. Jekyll served me well, but as my needs evolved, I found myself wanting more flexibility, better performance, and a more modern development workflow. Enter , , and a host of other modern web development tools that have transformed my blogging experience. Here's a rundown of the migration process and the benefits I’ve enjoyed since making the switch.

Jekyll is a static site generator that is simple, reliable, and supported by GitHub Pages. However, it has its limitations:

  • Performance: While Jekyll is fast for simple blogs, it can become sluggish as your content grows.
  • Customization: Advanced customization often requires diving into Liquid templates and custom plugins, which can be cumbersome.
  • Modern Workflow: Jekyll lacks some modern development features like hot-reloading and advanced state management.

I decided to use a more "modern" stack with Next.js, which is a React-based framework that offers the perfect balance of flexibility and performance with the following features:

  • and : Next.js allows you to choose between SSR and SSG, optimizing performance and SEO.
  • : Easy creation of dynamic routes without complex configuration.
  • API Routes: Built-in API routes simplify backend logic integration.

Starting from the Tailwind Nextjs Starter Blog

I believe in reusing existing solutions to save time and effort, and a quick search led me to a great starting point: Timothy Lin's . It is a 13+ and blogging template that comes preconfigured for technical writing, and it was a solid foundation to build on. I should be clear that what runs this blog today has been heavily reworked since: the routing, layouts, content pipeline, and the entire component layer have all been rewritten or replaced. But the template is what got me moving, and several of its ideas survive in the current codebase.

The main features I relied on and extended are:

  • Easy styling customization and mobile-friendly views with
  • for managing content
  • Server-side syntax highlighting with line numbers and line highlighting
  • Math display supported via
  • Citation and bibliography support via
  • Automatic image optimization via
  • Preconfigured security headers
  • SEO friendly with RSS feed and sitemaps

A note on the content pipeline: Contentlayer itself has since become unmaintained, so the blog now runs on , a maintained fork, together with next-contentlayer2.

How the Blog Is Structured

Let's walk through the different folders that make up the blog as it exists today. The site uses the Next.js App Router exclusively; there is no pages directory.

  • public/static: Next.js serves static files, like images, from the public folder in the root directory. Files inside public can be referenced by your code starting from the base URL /. The static folder contains images used in the blog, logos, icons, and favicons.
  • scripts: This folder contains scripts that help with the development and maintenance of the blog.
    • The build.mjs script performs a bunch of steps after the build command, like generating the RSS feed and the tags and categories pages.
    • The rss.mjs script generates the RSS feed for the blog.
  • data: The data folder contains the site metadata, authors, and blog posts in .mdx.
    • authors: contains the author information used to populate the about page. This can be extended to allow multiple authors.
    • blog: contains the blog posts in mdx format. The folders are used to categorize the blog posts.
    • meta: contains the site metadata like navigation links, site title, description, JSON-LD entities, and the metadata generators.
  • layouts: The layouts folder contains the templates used to render content: the post layout that wraps blog articles, plus list, archive, and featured layouts for the index pages.
  • lib: The lib folder contains utility functions and configurations used throughout the blog.
    • contentLayer: contains the Contentlayer "plugins" for the content pipeline.
      • computedFields.js: contains the computed fields for the blog posts. Those are fields that are generated based on the content of the blog post or its metadata, like the readingTime or toc (table of contents).
      • contentFields.js: defines the fields that appear in the frontmatter for the different types of content (author, post, project, etc.).
      • generateTaxonomies.mjs: loops through all the posts and generates the categories and tags for the blog.
      • projectFields.js: contains the project fields for the projects page.
    • mdx: contains the remark plugins for the MDX pipeline, like remark-code-title.js, which adds a title to code blocks.
  • app: the app folder contains the application routes and application shell: the blog pages, the API routes (like the ), the sitemap, and the robots configuration.

One structural decision worth calling out: the reusable React components, MDX components, design tokens, and Tailwind preset do not live in this repository at all. They were extracted into a separate design-system package (@gaudi/design-system), which the blog installs as a pinned dependency. The root tailwind.config.js consumes the design system's Tailwind preset and scans both the app and the installed package for classes. This keeps the blog repository focused on content and routing, and lets the visual layer evolve independently.

Was It Worth It?

Absolutely. The migration took real effort, especially untangling years of Jekyll-specific templates and plugins, but the result is a codebase I actually enjoy working in. Content is type-checked at build time through the Contentlayer schema, components are reusable and tested in isolation through Storybook, and features that would have required fighting Liquid templates (dynamic OG images, , interactive MDX components) are now just React code. If you are still on Jekyll and feeling the friction, a starter template plus incremental rework is a very manageable path across.

The opinions and views expressed on this blog are solely my own and do not reflect the opinions, views, or positions of my employer or any affiliated organizations. All content provided on this blog is for informational purposes only.