Back to roadmaps nextjs Course

Project: MDX Portfolio Website

In this project, we will build a personal Portfolio website using Next.js App Router. The application will scan a local folder for Markdown (or MDX) project sheets, parse the frontmatter, and render the static pages.


1. Project Specifications

  • Content Store: Store portfolio files inside src/content/projects/.
  • Routing: Set up a dynamic route at src/app/projects/[slug]/page.tsx.
  • Static Generation: Pre-render all pages at build time using the generateStaticParams API.

2. Implementing the Project Reader

First, install standard parser libraries (like gray-matter for parsing frontmatter) in your Next.js project.

Create a helper utility file named projects.ts inside src/lib/:

// src/lib/projects.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";

const projectsDirectory = path.join(process.cwd(), "src/content/projects");

export interface ProjectMetadata {
  slug: string;
  title: string;
  date: string;
  description: string;
  content: string;
}

// Read all project files
export function getAllProjects(): ProjectMetadata[] {
  if (!fs.existsSync(projectsDirectory)) return [];

  const fileNames = fs.readdirSync(projectsDirectory);
  return fileNames
    .filter(name => name.endsWith(".md"))
    .map(fileName => {
      const slug = fileName.replace(/\.md$/, "");
      const fullPath = path.join(projectsDirectory, fileName);
      const fileContents = fs.readFileSync(fullPath, "utf8");
      
      const { data, content } = matter(fileContents);

      return {
        slug,
        title: data.title || "Untitled",
        date: data.date || "",
        description: data.description || "",
        content
      };
    });
}

3. Creating the Dynamic Page

Create the page component at src/app/projects/[slug]/page.tsx to load and render each post:

// src/app/projects/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getAllProjects } from "@/lib/projects";

interface ProjectPageProps {
  params: Promise<{ slug: string }>;
}

// Pre-render slugs at build time (SSG)
export async function generateStaticParams() {
  const projects = getAllProjects();
  return projects.map((p) => ({
    slug: p.slug,
  }));
}

export default async function ProjectPage({ params }: ProjectPageProps) {
  const resolvedParams = await params;
  const slug = resolvedParams.slug;

  const projects = getAllProjects();
  const project = projects.find((p) => p.slug === slug);

  if (!project) {
    notFound();
  }

  return (
    <article className="max-w-2xl mx-auto p-8">
      <header className="border-b pb-4">
        <h1 className="text-4xl font-bold">{project.title}</h1>
        <p className="text-gray-500 mt-2">Published: {project.date}</p>
      </header>
      
      {/* Render Markdown text body */}
      <div className="prose mt-6">
        <p>{project.content}</p>
      </div>
    </article>
  );
}

This setup pre-renders all project files statically at build time, resulting in instantaneous page load times for users.

Published on Last updated: