Creating a Static Site with Next

(Versão em Português)
The GIF of a skull drinking a beverage in an old-school art style.

This post is a continuation of the improvements implemented on the blog. To learn more, check out the first article on how I improved my blog.

After automating the creation of posts on the blog using Markdown files, I realized I also needed to automate the creation of routes.

Previously, for each new post, I had to manually create a new route. This was tedious because I had to copy and paste repeatedly, in addition to creating many folders in the project.

Wouldn't it be great if there was a way to generate dynamic routes automatically, without having to do this manually...

A gif of Alice from Alice in Wonderland patiently waiting

Ladies and gentlemen, let me introduce you to the Slug.

Slug

Before explaining the Slug, it's important to understand how the routing system works in Next.

Next's routing system is file-based, where each file in the app folder corresponds to an accessible route. If I had to manually create a route for each new post (in both English and Portuguese), I would be very sad (and I really was, because that's exactly what I was doing 😔).

This is where the Slug comes in. The Slug is a dynamic way to create routes in Next. Instead of creating a static route for each post, we can use Slug. For example, instead of having a route like /posts/my-new-post, we can use /posts/[slug], where [slug] is dynamically replaced by the unique identifier of the post.

Ok, but how do we do that?

Blog structure

My blog has some important folders. The first one is ./content/posts, where Markdown files are stored and converted into HTML pages. Here's an example of this structure:

./content/post/creating-my-personal-site/

Inside the creating-my-personal-site folder, I have two Markdown files, one in English and one in Portuguese:

  • creating-my-personal-site.en.md
  • criando-meu-site-pessoal.pt-br.md

Each blog post will have its own folder within ./content/posts, containing these two files. The name of each file will be used as the Slug and the page title.

Routes

Within the app folder, we have several routes:

  • / is the site's index route
  • /blog/en is the route for listing posts in English
  • /blog/pt-br is the route for listing posts in Portuguese
  • /blog/en/[slug] is the dynamic route for an English post
  • /blog/pt-br/[slug] is the dynamic route for a Portuguese post

Simply put, the post listing iterates over all files in the ./content/posts folder corresponding to the page's language. From each file, it retrieves the title, post date, and file name.

Here's the code I use to fetch this data:

import fs from "fs";
import matter from "gray-matter";
import { ENCODING_UTF8 } from "@/utils/constants";

export default function getPostMetadata(basePath, language) {
  const folder = basePath + "/";
  const postFolders = fs.readdirSync(folder);

  const posts = postFolders.map((postFolder) => {
    const files = fs.readdirSync(`${basePath}/${postFolder}/`);

    const filename = files.find((file) => file.includes(`.${language}.md`));

    const fileContent = fs.readFileSync(
      `${basePath}/${postFolder}/${filename}`,
      ENCODING_UTF8
    );

    const { title, date } = matter(fileContent).data;

    return {
      title,
      date,
      slug: filename.replace(`.${language}.md`, ""),
    };
  });

  return posts.sort((a, b) => new Date(b.date) - new Date(a.date));
}

The return value of this function is this array:

[
  {
    title: "Creating a Static Site with Next",
    date: "2024-06-15",
    slug: "static-site-generation-with-next",
  },
  {
    title: "Improving My Blog with Gray-matter and React-markdown",
    date: "2024-06-14",
    slug: "improving-my-blog",
  },
  {
    title: "How to Host a Website",
    date: "2024-05-08",
    slug: "how-to-hosting-a-website",
  },
  {
    title: "Creating My Personal Site",
    date: "2024-04-23",
    slug: "creating-my-personal-site",
  },
];

With these three pieces of information, we can display a list of posts sorted from most recent to oldest. The file name serves as the Slug, allowing us to create a link using the corresponding Slug.

But, if there are 4 posts in the example above, how does Next associate each Slug with its respective post? In other words, how does Next retrieve the content and associate it with the corresponding Slug?

Simple, by using Static Site Generation.

Static Site Generation (SSG)

Static Site Generation (SSG) is a feature of Next that allows pre-rendering of HTML pages at build time, rather than on each request.

But how do we achieve this?

To illustrate, let's use the English route for posts: app/blog/en/[slug]/page.js

Inside page.js, we need to export an asynchronous function called generateStaticParams to define the parameters that will be pre-rendered. This function specifies which dynamic URLs should be pre-rendered at build time.

The example used here in the blog:

export const generateStaticParams = async () => {
  const posts = getPostMetadata(CONTENT_FOLDER, EN_LANGUAGE);

  return posts.map((post) => ({ slug: post.slug }));
};

Notice that I'm using the same function from the post listing, but returning only the Slug. This way, all English Slugs will be pre-rendered. The same process happens in the path app/blog/pt-br/[slug]/page.js, but for the Portuguese language.

Therefore, the Slug can be accessed as a property of the page.js component itself.

Remember that the Slug is the name of the Markdown file that contains the post content? To retrieve this content, just use Gray-matter, as we explained in the previous post.

const DynamicPost = (props) => {
  const slug = props.params.slug;
  const { data, content } = getPostContent(slug);

  return <Post data={data} content={content} />;
};

export default DynamicPost;

Using this strategy, all the blog pages are pre-rendered at build time, which is why it's so fast.

Another point is that the blog is highly optimized, as we can see in the lighthouse report:

An image of the report generated by Google's Lighthouse tool. It shows the parameters Performance, Accessibility, Best Practices, and SEO, all with a score of 100

These were all the changes made to the blog. Yes, it took a lot of work to make these adjustments, but now I feel that the blog is becoming increasingly robust.

If you want to dive deeper, the excellent vídeo by Smoljames explains the use of SSG in detail. I used a lot of his code as a reference.