Published on

Hey, I rebuilt my blog with Next.js


My original blog was built with Ghost and hosted on DigitalOcean. And while I enjoyed my time with Ghost, there were a few things I wanted to explore outside its scope, like writing posts with MDX! Mounting React components in blog posts is really cool! While I did manage to work everything out, the migration was not a cakewalk, however. It's 2020, what could go wrong?


Well... I'm addicted to learning things, can't help it, and saw this as an opportunity to further my experience with React and get more familiar with SSG.

I choose Next.js over Gatsby not because I think its a better product, but because I felt that it was a better fit for my use case. I'm a fan of Gatsby and met some of their devs at last year's JamStack Conf NYC - 2019.

To me, Next.js's filesystem routing was key, and static site gen is one of its strongest points. I can simply create a blog-post.mdx file in the ./pages/ folder and that generates a route to Additionally, my .mdx files contain a meta {} object for the post's frontmatter that gets picked up automatically with the function getStaticProps. I felt that using a GraphQL API (Gatsby) for this was a bit overkill. So, I went with Next.js.

The Migration Experience - (Figuring things out)

There were a few hurdles that I had to overcome migrating from Ghost. In Next.js, how do you generate a sitemap? RSS? AMP? Post tags? Rewrites and redirects? Not going to lie, it did seem a little daunting at first, but I was able to get through it at a decent pace.

First off, the Next.js website/blog repo and Tailwind's blog are treasure troves of information. Reading the official Next.js docs did feel a little lacking at times, and referring to their website's repo did help a ton. Thanks guys!

The Sitemap & RSS

Drawing inspiration from how Next.js' site and the Tailwind blog generate RSS, I wrote a script that generates a sitemap for my blog.

import fs from 'fs';
import tinytime from 'tinytime';
const postDateTemplate = tinytime('{YYYY}-{Mo}-{DD}T{H}:{mm}:{ss}.0Z', { padMonth: true });

function importAll(r) {
    return r.keys().map((fileName) => ({
        link: fileName.substr(1).replace(/\/index\.mdx$/, ''),
        module: r(fileName)

function dateSortDesc(a, b) {
    if (a > b) return -1;
    if (a < b) return 1;
    return 0;

 function getAllPostPreviews() {
    return importAll(require.context('./pages/?preview', true, /\.mdx$/)).sort((a, b) =>

let sitemapItems = '';

getAllPostPreviews().forEach(({ link, module: { meta } }) => {
    sitemapItems += `<url><loc>${`${link}/`}</loc><lastmod>${meta.updated}</lastmod></url>`;

const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
                  <urlset xmlns="">
                    <url><loc></loc><lastmod>${postDateTemplate.render( new Date())}</lastmod></url>

fs.writeFileSync('./public/sitemap.xml', sitemap);

console.log(`✅ sitemap.xml generated!`);

Running this script on build sticks an updated sitemap.xml file in ./public. Pretty sweet!


Tags were probably the most complicated part of the whole process and warrants a post of its own. For brevity, I was able to make it work using Next.js's dynamic routes.

Essentially, I set up a page as: pages/tag/[tag].js where [tag] became a dynamic slug. Using the incoming slug, I check it against a list of known, pre-defined tags, and if it exists in the list, return a list of matched posts; if not, return a 404.

Feel free to look at my blog's repo to see how I wired it up.

Rewrite & Redirects

I found two ways to handle this, and both are straight forward; Write them in your next.config.js or, if your hosting on Vercel, write them in vercel.json.

Ultimately, I ended up writing them in the vercel.json file since I'm hosting my blog on Vercel.

AMP (I felt really gross setting this up)

The only reason I bothered setting up AMP was that Ghost handled it out of the box without any thought, and I didn't mind using it at the time until Google decided to brand the internet and your websites as "," but I digress.

Next.js's AMP support is by page opt-in. Enabling AMP for a blog post was as simple as adding export const config = { amp: 'hybrid' } to a blog-post.mdx file.

However, if you're embedding your styles like me, you'll likely run into this issue outlined here:, where styles won't generate on build for your amp pages, while your standard pages will look fine. Luckily, there's a workaround outlined in that post. :)

Learning Experience

All in all, I'm pretty happy with it. I got all the functionality from my old blog but with a significant benefit of being able to make the examples in my posts a bit more interactive. For example, my post on Simple Child to Parent Communication is showing the embeded React component along with the example code in action!

Source Code Available

Want to check out the code repo for this post?

© 2015-2022