2024-08-31 Web Development

Understanding Static Site Generation in MDXBlog

By O Wolfson

Static Site Generation (SSG) is a powerful feature of Next.js that allows you to pre-generate HTML pages at build time. This approach enhances performance, improves SEO, and reduces server load by serving pre-rendered content instead of dynamically generating it on every request. In this article, we’ll explore how static rendering works in MDXBlog.

Introduction to Static Site Generation

Static site generation is the process where HTML pages are generated at build time and served as static files. In MDXBlog, this is achieved through a combination of Next.js’s static generation capabilities and the integration of MDX files, which allow for rich, interactive content.

How MDX is Integrated into MDXBlog

MDXBlog uses the @next/mdx package to seamlessly integrate MDX files into the Next.js build process. MDX allows you to write JSX within Markdown, enabling the use of React components in your content. The following configuration in next.config.mjs shows how MDX is set up:

javascript
import createMDX from "@next/mdx";
import remarkGfm from "remark-gfm";
import remarkFrontmatter from "remark-frontmatter";
import rehypeHighlight from "rehype-highlight";

const nextConfig = {
  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};

const withMDX = createMDX({
  options: {
    remarkPlugins: [remarkGfm, remarkFrontmatter],
    rehypePlugins: [rehypeHighlight],
    format: "mdx",
  },
});

export default withMDX(nextConfig);

This configuration allows MDX files to be treated as standard Next.js pages or components. It also enables the use of plugins like remarkGfm and rehypeHighlight to extend the capabilities of your Markdown content.

Understanding the Build Process

During the build process, Next.js compiles your MDX files into React components. These components are then rendered into static HTML files, which are served to users when they request a page.

Dynamic Imports and Static Rendering

In MDXBlog, dynamic imports are used to load MDX files based on a slug. Next.js resolves these imports at build time for pre-defined pages, allowing the MDX content to be statically rendered into HTML. This approach is implemented directly within the page component for each dynamic route:

typescript
// /app/blog/[slug]/page.tsx

import React from "react";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import path from "node:path";
import fs from "node:fs";
import RelatedPostsList from "@/components/related-posts";
import LikeButton from "@/components/like/like-button";
import EditPostButton from "./edit-post-button";
import OpenInVSCode from "./open-in-vs-code-button";
import { isDevMode } from "@/lib/utils/is-dev-mode";

type Props = {
  params: { slug: string };
};

// Dynamically import the MDX file based on the slug
async function loadMdxFile(slug: string) {
  try {
    const mdxPath = path.join(process.cwd(), "content", "posts", `${slug}.mdx`);
    if (!fs.existsSync(mdxPath)) {
      return null;
    }
    const mdxModule = await import(`@/content/posts/${slug}.mdx`);
    return mdxModule;
  } catch (error) {
    console.error("Failed to load MDX file:", error);
    return null;
  }
}

// Generate metadata using the imported metadata from the MDX file
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const mdxModule = await loadMdxFile(params.slug);
  if (!mdxModule) {
    return {
      title: "Post Not Found",
      description: "",
    };
  }
  const { metadata } = mdxModule;

  return {
    title: metadata.title,
    description: metadata.description,
  };
}

export default async function Blog({ params }: Props) {
  const { slug } = params;
  const mdxModule = await loadMdxFile(slug);

  if (!mdxModule) {
    notFound(); // Return a 404 page if the MDX file is not found
  }

  const { default: Content, metadata } = mdxModule;

  // Extract the dates and compare them
  const publishDate = new Date(metadata?.publishDate);
  const modifiedDate = new Date(metadata?.modifiedDate);

  // Choose the date to display
  const displayDate =
    modifiedDate > publishDate ? metadata?.modifiedDate : metadata?.publishDate;

  return (
    <div className="max-w-3xl z-10 w-full items-center justify-between">
      <div className="w-full flex justify-center items-center flex-col gap-6">
        <article className="prose prose-lg md:prose-lg lg:prose-lg mx-auto min-w-full">
          <div className="pb-6">
            <p className="font-semibold text-lg">
              <span className="text-primary pr-1" title="Date last modified">
                {displayDate.split("T")[0]}
              </span>{" "}
              {metadata?.categories?.map((category: string, index: number) => (
                <span key={index + category} title="Post category">
                  {category}
                  {index < metadata?.categories.length - 1 && ", "}
                </span>
              ))}
            </p>
          </div>
          <div className="pb-6">
            <h1 className="text-4xl sm:text-6xl font-black capitalize leading-12">
              {metadata?.title}
            </h1>
            <p className="pt-6 text-xl sm:text-lg">By {metadata?.author}</p>
          </div>
          {/* Render the dynamically loaded MDX content */}
          {isDevMode() && (
            <div className="flex gap-2 mb-4">
              <EditPostButton slug={slug} />
              <OpenInVSCode path={slug} />
            </div>
          )}
          <Content />
        </article>
      </div>
      <div>
        <div>
          <RelatedPostsList relatedSlugs={metadata?.relatedPosts} />
        </div>
      </div>
      <LikeButton postId={metadata?.id} />
    </div>
  );
}

// Generate static paths for all slugs based on MDX files in the posts directory
export async function generateStaticParams() {
  const files = fs.readdirSync(path.join("content", "posts"));
  const params = files
    .filter((filename) => filename.endsWith(".mdx")) // Only process MDX files
    .map((filename) => ({
      slug: filename.replace(".mdx", ""),
    }));

  return params;
}

Handling Dynamic Routes with generateStaticParams

For dynamic routes like /blog/[slug], MDXBlog uses the generateStaticParams function to pre-generate paths for all MDX files in the content/posts directory. This ensures that these pages are also pre-rendered during the build process, maintaining the benefits of SSG even for dynamic content.

typescript
export async function generateStaticParams() {
  const files = fs.readdirSync(path.join("content", "posts"));
  const params = files
    .filter((filename) => filename.endsWith(".mdx"))
    .map((filename) => ({
      slug: filename.replace(".mdx", ""),
    }));

  return params;
}

Exploring the Key Components

Custom MDX Components

Your mdx-components.tsx file customizes how different HTML elements and components are rendered within MDX content. This customization ensures that your content is styled consistently and utilizes the power of React components:

typescript
import React from "react";
import YouTube from "@/components/mdx/youtube";
import Code from "@/components/mdx/code";
import InlineCode from "@/components/mdx/inline-code";
import Pre from "@/components/mdx/pre";
import Image from "@/components/mdx/image";
import type { MDXComponents } from "mdx/types";

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
    YouTube,
    Image,
    pre: Pre,
    code: (props) => {
      const { className, children } = props;
      if (className) {
        return <Code {...props} />;
      }
      return <InlineCode>{children}</InlineCode>;
    },
    h1: (props) => (
      <h1 className="text-4xl font-black pb-4 w-full" {...props} />
    ),
    h2: (props) => <h2 className="text-3xl font-bold pb-4 w-full" {...props} />,
    h3: (props) => (
      <h3 className="text-2xl font-semibold pb-4 w-full" {...props} />
    ),
    h4: (props) => (
      <h4 className="text-xl font-medium pb-4 w-full" {...props} />
    ),
    h5: (props) => (
      <h5 className="text-lg font-normal pb-4 w-full" {...props} />
    ),
    h6: (props) => (
      <h6 className="text-base font-light pb-4 w-full" {...props} />
    ),
    p: (props) => <p className="text-lg mb-4 w-full" {...props} />,
    li: (props) => <li className="pb-1" {...props} />,
    ul: (props) => <ul className="list-disc pl-6 pb-4 w-full" {...props} />,
    ol: (props) => <ol className="list-decimal pl-6 pb-4 w-full" {...props} />,
    hr: (props) => (
      <hr className="my-4 border-t border-gray-300 w-full" {...props} />
    ),
    blockquote: (props) => (
      <blockquote
        style={{ paddingBottom: 0 }}
        className="border-l-4 pl-4 my-4"
        {...props}
      />
    ),
    a: (props) => <a className="hover:underline font-semibold" {...props} />,
  };
}

These custom components are applied during the MDX compilation process, ensuring that when the content is rendered, it adheres to your design and functionality specifications.

The Role of generateMetadata

In a page component, the generateMetadata function plays a critical role in extracting and setting metadata from the MDX file. Here’s how it works:

typescript
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const mdxModule = await loadMdxFile(params.slug);
  if (!mdxModule) {
    return {
      title: "Post Not Found",
      description: "",
    };
  }
  const { metadata } = mdxModule;

  return {
    title: metadata.title,
    description: metadata.description,
  };
}

This function ensures that the correct metadata, such as the title and description, is included in the pre-rendered HTML, which is essential for SEO and user experience.

Why Static Rendering Matters

Static rendering offers several benefits:

  • Performance: Pre-rendered pages load faster because they don’t need to be generated on the fly.
  • SEO: Search engines can easily index static pages, improving your site’s visibility.
  • Scalability: Serving static content reduces server load, making it easier to handle large volumes of traffic.

In MDXBlog, static site generation combines with the dynamic capabilities of React and MDX, providing a powerful platform that balances flexibility with performance.

Conclusion

In MDXBlog, static site generation is achieved through a combination of MDX processing, dynamic imports resolved at build time, and custom component rendering. This approach ensures that your content is served as fast, SEO-friendly static pages while retaining the flexibility and interactivity of modern React applications.

By understanding how static rendering works in MDXBlog, we can better optimize our content delivery and ensure a seamless user experience across all pages. Whether you're creating new content or extending the functionality of a blog, this static-first approach will serve as a solid foundation for future growth.