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:
55
apps/public-web/lib/posts.ts
Normal file
55
apps/public-web/lib/posts.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user