A Tale of Two Blogs

Over the last couple of months I spent a fair amount of time creating two blogs using Next.js: one which you are reading right now, and the other for the company I work my day job at - Indiana Wholesale Supply. If you read my previous post on Progress, not Perfection, you might know that I have a tendency to overthink most things and delay action as a result. I had the idea to add a blog to this site for a while, but with so many options for blogs or CMS's, it was easy for me to get caught in decision paralysis. Once I gave myself the constraint of sticking within the Next.js/React/Node ecosystem, the decision became a little easier, but I still had one failed attempt at getting a CMS off the ground before getting one that worked. Then, naturally, I got caught up in all the possibilities and decisions around designing a system of structured content within the CMS, and sputtered to a halt. As such, I had the start of this blog already created from that initial effort a while back, so in the redesign of this site (and in an effort to work on my character defects) I decided to make the effort to fully get it off the ground.
A failed attempt: Outstatic
Outstatic CMS is a really cool idea. It doesn't require a data lake or 3rd party server to store your content data, it requests authorization to your Github repository that you are running the CMS app from and will automatically create markdown files and write your generated content to the repository. If provides a nice GUI for creating and managing different types of structured content and relations between content in the same way that a full fledged, hosted CMS would - rich text editor, display and CRUD of all posts or whatever other content types you have, but all of the content is static files housed in your repository. Perhaps obviously, this wouldn't scale to infinity, but a really nice idea and implementation none the less... I'd recommend playing with it if you're curious.
Here's the catch about Outstatic though: locally everything was fine, but the location that Next.js put the created files from Outstatic during its build step when deploying the app to production changed and caused my app to whiff when attempting to retrieve the content. There is a good possibility that this has been addressed and fixed since I had my stab at getting Outstatic to work in my app. I also very well could have spent a while modifying the build output location for these files, but prefer to let most things in the Next/Vercel CD pipeline remain untouched if I can help it. I had also just spent a good chunk of time digging through gists and production logs (thank god Vercel has observability out of the box), so I decided to move on in the hope of finding something that... just worked. I had heard good things about Sanity.io, but got scared away by their registration flow initially because I was certain they'd get me to pay money for it somehow (silly, I know) and didn't love the idea of hosting my content in somebody else's data storage. In my frustrated state after messing with Outstatic though, I got over my reservations and took the leap.
Blog 1 - Sanity.io headless CMS, dynamic Next.js pages
I spent a large portion of my career at Angi (formerly Angie's List), working on their SEO and content pages. Long ago that part of the site was run on a Drupal monolith, with content management and MVC for rendering web pages existing all within the same application.Think Wordpress, if you're unfamiliar with Drupal. Over time, we eventually broke that app up into microservices, with a headless CMS (Contentful) and Next.js serving our webpages. As it turned out, the architecture I ended up with for this first blog was one that I felt right at home in - just replaced Contentful with Sanity, and upgraded a few Next.js versions.
As I mentioned before, I had heard good things about Sanity.io and it seemed to have good adoption as well as support behind it. Perhaps most importantly, it did after all have a pretty generous free tier. They automatically generate a GraphQL API to retrieve your content (though they recommend using GROQ, which is relatively analogous), so creating pages for the CMS data should be familiar enough to most React devs - fetch data in your component and pass it down into your React tree.
Sanity.io
I've grown to really enjoy using Sanity. You can generate the code needed to run a studio GUI for editing content from the CLI similar to create-next-app or create-react-app:

You define the schema and content types via code that you write yourself within the app created:

and then content you create is stored remotely on Sanity's datalake, which is schemaless and doesn't care about what you store there at all (more on that later).
Sanity provides a client package for fetching your content, and though they recommend using GROQ for querying your data, which I thought was weird at first, I've grown to like that way of interfacing with and retrieving my content. It's similar to GraphQL but I think a little more flexible, and provides things like pagination and flattening of nested data without having to write custom resolvers.
Another interesting and cool thing about Sanity is how it allows you to customize rendering of rich content in React. Sanity provides a @portabletext/react
package that you can use to map blocks within your rich text to specific react components. This allows you to control styling for, say, nested unordered list items if you're using tailwind (IYKYK), or utilize next/image for images you place in the rich text.

I did run into a couple hiccups with Sanity that, in fairness, were mostly user error on my part. I do feel I should call them out though in case anybody reading this can learn from and not repeat my mistakes.
- I mentioned above that Sanity's datalake doesn't give a damn about your schema. Because of that, if you modify the type of a field in your schema, you should not expect data that you've already created to update its type accordingly. I made the mistake of having a field be a string instead of rich text when I created my first piece of content for that content type, and my options were a.) run a data migration script, or b.) create and use a new field with the correct type. Not a huge deal, but if I was working with a full team of content creators this would be something I would be careful to take into account.
- Automatically typing the data returned from their API has been difficult for me. I would think that the fact that I defined a schema file would make this much easier, but I really struggled with it. I will say, I am not the greatest TypeScript developer in the world, and this could also be due to the fact that I created two separate apps (one for my Next.js app, one for my Sanity Studio). After fighting with ChatGPT trying to figure out how to properly generate types for my schema, I finally asked Sanity's AI assistant on their site, which appears to be trained on a dataset of all things Sanity, and it led me to the
@sanity/codegen
command, which helped a lot but was still not as usable as I would have liked.
All in all, I've found Sanity to be really enjoyable. If I need a headless CMS that allows for a team of content creators to go in and do their thing after I create the schema for structured content they can create, and the relations between content to build site structure around, I would certainly pay money for it. Even on the free tier for this site it provides a lot of nifty functionality that is easy to use and gives me a polished, professional content creation experience so I can act like I know what I'm doing.
Dynamic Server Side Rendered Pages with Next.js
In my opinion this (Next.js dynamic routes with SSR) is really bread and butter stuff these days, so I won't go too far into the details on Next.js server actions, streaming etc. The long and short of it is that Next.js pages using the app router default to server-side rendering, and if you make the top level component an async function, you can just await a promise to fetch data as if you were declaring a regular old variable. No useEffect, no state management, no getServerSideProps. It's amazing. If you lower your data fetching call into a child component and wrap it in <Suspense>
the rest of the page will render while the component fetching the data loads. These tools make it really easy to build a performant site that works well for SEO purposes. It's really spoiled me in terms of quality of life as a React developer, and naturally there have been other frameworks, like Hydrogen by Shopify, that have similar patterns and capabilities.
I'm aware of a number of complaints about Next.js like wanting a bundler other than Webpack, or how difficult it is to self host. For me, Next.js and its ecosystem of tools haven't caused an issue yet. They just work.
Blog 2 - Markdown as a CMS with Gray Matter, remark, and static Next.js routes
The next adventure started with me managing a Wordpress site that was built in 2015 and relied on a plugin called Divi (as opposed to Elementor) for building and editing pages. One of my many complaints about Wordpress (likely skill issues on my end I admit) is that it doesn't give you enough control. Every customization seems to be another plugin, then plugins conflict with each other, then you still can't get full control over the route to a certain page, or the display of a gallery component. After a while I got sick of the Wordpress shackles and decided to migrate the site to my favorite framework.
My main constraint of rebuilding this site was to keep everything as dead simple as possible while maintaining feature parity and allowing satisfying the requirements that allowed the site to grow in the desired areas. I didn't want a database, unnecessary XHR requests, or really any external dependencies outside of the Next app itself. I knew that I would be the only one generating content and driving SEO or SEM for the site, so a true CMS that a writer or non-technical person could use wasn't necessary. While exploring my options for how to achieve this I came across aa Vercel template that I would highly recommend. This fairly vanilla blog-starter had no external CMS like Contentful or Sanity baked in, while giving a pattern for creating blog content with markdown that allows adding metadata for posts (or "front matter" in gray-matter terms). I was able to extend this pattern to multiple content types that allows relations to product categories and authors.
Markdown and Gray Matter as a CMS
The meat of a blog post is usually just rich text - paragraphs, headers, links, bullets, etc. - stuff you might find in a README file that could be achieved with markdown as an alternative to a rich text editor. What gives a blog value, in my understanding from an SEO standpoint, is the ability to link posts to things like 1.) product categories to establish expertise on the subject of that category and 2.) real people (authors) who are experts on the subject and can be trusted to provide valuable information on it. I'm sure there are other ways it can drive value, but I've found that without meaningful relations and links to other pages and entities within a site it isn't as useful of a tool for winning organic traffic. As such, while a simple blog post very well could be created with markdown alone, in order to have it drive value there needs to be a way to add metadata like relations I mentioned above, as well as other nice to haves like updated date, thumbnail images, and the like.
Enter gray-matter, which parses what they call "front-matter" from a file, allowing you to separate blog post content from related metadata in the same .md
file:

I was able to extend this pattern to my product category pages, allowing me to add "rich text" content to those pages as well. The PK/FK relation mapping my different content types together is the category slug:

I'm also able to link in things like project showcases (galleries) using that same relationship, but with those stored in JSON files in my site:

There's a super thin "API" for each entity:

And then I can call my data fetching functions within my Next.js server components and carry on like I have a real backend on my site.
Now obviously this doesn't scale well, but the site is currently fairly small (about 30 pages), and I'm the only person managing content for it. Because of that, this pattern nicely fits the use case for the site and has been far more enjoyable and flexible to work in than Wordpress. Working in Next.js and Reaect has given me the ability to control far more about the site and sped up my ability to give a clean and modern UI with more functionality than before. If you're curious about the results of the migration here is a before and after.
Next.js Static Site Generation
I mentioned above how slick Next.js can be with server components, streaming and suspense for performance in a dynamically rendered site. It also has the ability to generate static pages at build time. The static pages are cached in Vercel's CDN when the app is deployed and are BLAZING fast. I'm talking like 100-200ms page load times fast. If you can get away with not needing to make live updates to data (like for messaging, live dashboards, user profiles) for a page outside of when the app is deployed, I would chase static generation hard. What you want to see when running next build
is the circles next to your routes like this:

Dynamic routes will have an 'f' next to them instead. Not the worst thing in the world since there are so many way to optimize dynamic routes in Next.js, but again, I cannot understate the performance gains from making this site entirely static. Now if only i could optimize images better...
One thing to note about this site is that I do have dynamic parameters for some of my routes. Because of that, I need to tell Next.js what routes it needs to generate static pages for at build time:

In the file your page component lives in, just return an array of all expected values the dynamic parameter can have that should render pages. This will lengthen your time to build the app, but is well worth it at run time.
Outro
So, that's about it. I wouldn't say either of the two patterns I outlined above is better or worse, right or wrong. As with most things in software development, it comes down to use cases:
- Are you going to be working with a content team? Then use a true headless CMS (like Sanity).
- Do you want to be able to publish pieces of content independent of an app deployment? Then don't rely on SSG for blog post pages.
- Can you write markdown and are somewhat technical and too lazy to spin up a true CMS or create schema for structured content? Try gray-matter for dead-simple blog post creation.
I could go on with nuances of use cases, but I'll stop there.
If you're like I was and are overwhelmed by the myriad of options for creating a blog site, I hope this helped clear the water some. At the very least I hope maybe you were able to pick something new up about one of the tools I tried to highlight here. One thing I love about this field of work is being able to pass on stuff I learn to the next person, as I've picked up so much from my peers that came before me.
If you're stuck in analysis paralysis, just start with something. If you get stuck or fail then try a different spin on it, but just keep going. Iterate and make progress, nothing is ever perfect, complete, or permanent.
And now I'm going to stop writing.
Cheers,
Jack

Written by Jack Amato
Jack Amato is a web developer with over a decade of experience building software, leading teams, and figuring things out on the fly. He spent several years as a Senior Engineer and Engineering Manager at Angi, and holds degrees in Science and Technology Entrepreneurship from Notre Dame. Based in Indianapolis, Jack balances code with coaching high school lacrosse, playing the cello, and chasing after his two daughters.