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
generateStaticParamsAPI.
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: