Building a modern blog with Typescript, React, Tailwind, and Next.js

Guilherme Rossato, December 2023
TLDR; This blog is a Next.js app, pages are server-rendered and frontend-hydrated on load, images are transparently optimized, posts are created on a variation of markdown, the repository is stored privately, a git update hook deploys when changes occurs on main branch, I can add dynamic React components on posts, and I have a contact form.
The previous version of this blog was a simple frontend react progressive web app that I created 20 months ago, it was statically served by a simple Nestjs backend and the previous setup had quite a few flaws that you can read on its own post here. Fast forward to 6 months later (December 2023) a new framework that solves all of my problems surges on the open source community!

Next.js

For the past few months I have been experimenting with the Next.js framework and it is great if you dont mind it being a little resource-intensive: It bundles backend and frontend nicely with typescript, handles routing, server-side rendering, several modern optimizations, and it is easy to expand, deploy, and maintain, at least so far I have been able to test.
The best feature so far has been the Server Actions: It abstracts a http request between the client and the server by allowing the frontend to import the asyncronous function from the backend, which comes with all the correct types: it looks like your frontend can just 'call' the backend function. Internally it transforms that call into a fetch, and even handles errors and hides the stack trace on production.
I have decided to recreate my blog with it to test this framework in a production environment and I will also document how I did it:

Getting started

This blog was bootstrapped using the create-next-app package as recomended by the official Installation tutorial:
npx create-next-app@latest
I decided to store the git repository of the project privately on the same server the app will be running from, so I need to configure the process of downloading the project, pushing changes to it, building and testing it automatically, and deploying the new server manually:

DevOps - Repository Storage

I access the production server using the SSH protocol which is supported by git, so I can use that connection to download (clone) the repository to develop, and then push the changes back to it.
I configured my git server by creating a bare repository with git init --bare, and essentially followed the steps on this tutorial. I can clone the project and push changes to it with these commands:
git clone ssh://username@hostname:port/path/to/project.git
cd project
touch README.md
git add README.md
git push
Github and Bitbucket are way more reliable in storing repositories, they also have a ton of features that are worth using them for, I am choosing to not use them as a learning experiment: Besides gainin deeper understanding of the tools their provide, my deployment process will be much faster and cheaper, since I am using my own hardware. I also do not plan to scale my server any time soon.

DevOps - Continuous Integration and Deployment

To start the continuous integration process when changes are uploaded I enabled the post-update hook on the bare git repository:
# create the file
touch ./hooks/post-update
# give permission for execution
chmod +x ./hooks/post-update
Since the deployment is a long running process I implemented an asyncronous queue structure: When the post-update executes it schedules a build job and exits, meanwhile another script, which I named build-worker, picks up the job and performs the following deployment steps:
  1. Copies the new state of the repository to a new folder
  2. Install dependencies or copies them if they haven't changed
  3. Runs some tests such as npm audit --audit-level=critical
  4. Executes npm run build to create the production-ready app
  5. Requests a restart of the production process to a process manager
The responsability of keeping the production process running is handled by a custom process manager script in which an external worker can send a request to that process manager and it will stop the current production process and start it again on the new production environment.
With this setup I can run git push and my changes go to production in a few seconds! I need to implement a way to see the deployment as it might fail and the information are scaterred in log files on my server. Anyways that's for a later moment, we have other features to tackle:
I also wrote about and implemented a simpler CI/CD process as a tutorial if you want to get a deeper understanding about how it works this blog post.

Support for markdown posts

Markdown files are easy to work with so they are perfect for creating blog posts.
We can add markdown support to our app by following this tutorial, and after adding the @next/mdx and @mdx-js/loader plugins we can create web pages directly in this augmented markdown file, .mdx, that supports importing dynamic react components on posts, and they are server-rendered and then client-hydrated as expected from any normal Next.js page, here's one of those components:
You have to appreciate the simplicity of this setup: I can create a new post by editing ./src/app/blog/how-i-built-my-blog/page.mdx, and it will be available at https://grossato.com.br/blog/how-i-built-my-blog after the deployment, which was triggered by the git push, and the built page is optimized and served rendered and cached from the server.
There's some work to be done regarding the presentation of the page because by default it looks like this unformatted mess:

Applying Style and Layout to Markdown files

My previous blog had this simple design I built using TailwindCSS:
Adding Tailwind to Nextjs is straightforward: I followed this tutorial and in no time had it working.
I have also created a layout.tsx file to center the page content and to add the header to all urls that have the /blog/ prefix.
According to the official configuring mdx Next.js guide, you can provide custom React components to be used on MDX files on a specific file called mdx-components.mdx, it's essential to change this for images and links to take advantage of the framework's optimizations, but its mostly used for styling, here's a snippet of mine:
./src/mdx-components.tsx
1import type MDXComponents from "mdx/types";
2import Link from "next/link";
3
4export function useMDXComponents(components: MDXComponents): MDXComponents {
5  return {
6    h1: ({ children }) => <h1 className="text-4xl leading-16 mt-8 mb-4">{children}</h1>,
7    h2: ({ children }) => <h2 className="text-3xl leading-12 mt-8 mb-4">{children}</h2>,
// Lots of stuff here
104    p: ({ children }) => <p className="my-4 text-xl/8 text-justify">{children}</p>,
105    a: ({ href, children }) => (
106      <Link
107        href={href || ""}
108        className="font-medium text-blue-600 hover:text-blue-800 hover:underline"
109      >
110        {children}
111      </Link>
112    ),
113    ...components,
114  };
115}
Copying previous JSX components was straightfoward and styling content was swift so I decided to challenge myself and implement a feature my previous blog was lacking: A light / dark theme selector!

Implementing a Color Scheme Feature

Tailwind allows you to use dark: and light: prefixes in your element class names to apply specific styles based on the selected theme. I want to implement a light switch button and so I configured tailwind to get the theme configuration from the class of the body element.
The top right button on this page has either a moon or a sun (icons from the free HeroIcons collection) and it toggles this document's body dark class name and in turn the theme of this blog! You can also do that by pressing the L key on your keyboard.
When the user switches his preference we send a Server Action to save it on a cookie. The next time the user requests a page we can render it on the server with the user preference applied: This prevents the first-page flash that would occur if we verified it on the frontend, such as loading from localStorage.
The downside of implementing this feature is that the work on the design will be more time consuming: We now have two variants of every component. I guess that's an okay price for a nice feature, hopefully this decision won't cause too much trouble.

Implementing the Post List for the Home Page

According to nextjs's documentation related to the configuration of modified markdown files (mdx), they can export metadata before the actual document starts, so the beggining of my files look like this:
export const metadata = {
  author: "Guilherme Rossato",
  title: "This is a post",
  description: "This short paragraph goes below the title to describe the post",
  date: "2023-11-20",
  published: true,
}

# This is the main title of my post

This is the first text paragraph
On the server we iterate over the metadata of all posts and filter for published: true, then use that list to create the post list on the main index page.

Closing Thoughts

I really like this setup: it is relatively simple and has all the features I need to experiment with some interesting stuff. The development environment always seem to match the production env nicely. Performance is great and so are the optimizations Next.js provides. Deployment is impressively fast.
The ability to add react components to pages comes in handy, the first paragraph of this post has a relative date in months that will always render the right amount of months, and I can add interactivity with stateful components, the sky is the limit.
These posts are also easy to compose with markdown and easy to deploy with git, and I can add normal Nextjs pages and Server Actions for all sorts of experiments I come up with. This blog is a great test zone.
Anywas thanks for sticking around and hopefully this blog will have more content in the future!