feat(blog): add file-based blog with dynamic slugs, MDX content and layout shell

- Introduced blog routing using Next.js App Router
- Implemented dynamic [slug] pages for blog posts
- Added MDX-based content loading via lib/posts
- Integrated shared TopBar layout with navigation
- Established clear content, lib and component separation
This commit is contained in:
PascalSchattenburg
2026-01-22 14:14:15 +01:00
parent b717952234
commit d147843c76
10412 changed files with 2475583 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
export type PostMeta = {
title: string;
date: string;
excerpt?: string;
};
export type Post = {
slug: string;
meta: PostMeta;
content: string;
};
const POSTS_DIR = path.join(process.cwd(), "content", "posts");
function filenameToSlug(filename: string): string {
// "2026-01-21-first-post.mdx" -> "first-post"
const withoutExt = filename.replace(/\.mdx?$/, "");
const parts = withoutExt.split("-");
// first 3 parts are date (YYYY-MM-DD), rest is slug
return parts.slice(3).join("-");
}
export function getAllPosts(): Post[] {
if (!fs.existsSync(POSTS_DIR)) return [];
const files = fs.readdirSync(POSTS_DIR).filter((f) => f.endsWith(".mdx") || f.endsWith(".md"));
const posts = files.map((filename) => {
const fullPath = path.join(POSTS_DIR, filename);
const raw = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(raw);
const slug = filenameToSlug(filename);
const meta: PostMeta = {
title: String(data.title ?? slug),
date: String(data.date ?? "1970-01-01"),
excerpt: data.excerpt ? String(data.excerpt) : undefined
};
return { slug, meta, content };
});
posts.sort((a, b) => (a.meta.date < b.meta.date ? 1 : -1));
return posts;
}
export function getPostBySlug(slug: string): Post | null {
const posts = getAllPosts();
return posts.find((p) => p.slug === slug) ?? null;
}