From 92fb64733b408fe8ed6185fba369c7a445ff8ca4 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 16 Jun 2025 17:11:26 +0200 Subject: [PATCH] folder support --- posts/folder/folder.md | 10 +++ src/app/api/posts/[slug]/route.ts | 9 ++- src/app/api/posts/route.ts | 55 +++++++------ src/app/page.tsx | 82 +++++++++++++++++--- src/app/posts/{[slug] => [...slug]}/page.tsx | 9 ++- 5 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 posts/folder/folder.md rename src/app/posts/{[slug] => [...slug]}/page.tsx (87%) diff --git a/posts/folder/folder.md b/posts/folder/folder.md new file mode 100644 index 0000000..a839c34 --- /dev/null +++ b/posts/folder/folder.md @@ -0,0 +1,10 @@ +--- +title: "Testing Folders" +date: "2024-03-10" +tags: ["test", "feature"] +summary: "A test post to demonstrate folders" +--- + +# Folders + +i hate TS \ No newline at end of file diff --git a/src/app/api/posts/[slug]/route.ts b/src/app/api/posts/[slug]/route.ts index 7f03b89..5a87168 100644 --- a/src/app/api/posts/[slug]/route.ts +++ b/src/app/api/posts/[slug]/route.ts @@ -37,12 +37,15 @@ async function getPostBySlug(slug: string) { export async function GET( request: Request, - { params }: { params: { slug: string } } + { params }: { params: { slug: string[] | string } } ) { try { - const post = await getPostBySlug(params.slug); + // Support catch-all route: slug can be string or string[] + const slugArr = Array.isArray(params.slug) ? params.slug : [params.slug]; + const slugPath = slugArr.join('/'); + const post = await getPostBySlug(slugPath); return NextResponse.json(post); } catch (error) { - return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 }); + return NextResponse.json({ error: 'Fehler beim Laden des Beitrags' }, { status: 500 }); } } \ No newline at end of file diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index 1fbd9a0..04f4b3a 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -13,19 +13,14 @@ function getFileCreationDate(filePath: string): Date { return stats.birthtime; } -async function getPostBySlug(slug: string) { - const realSlug = slug.replace(/\.md$/, ''); - const fullPath = path.join(postsDirectory, `${realSlug}.md`); - const fileContents = fs.readFileSync(fullPath, 'utf8'); +async function getPostByPath(filePath: string, relPath: string) { + const fileContents = fs.readFileSync(filePath, 'utf8'); const { data, content } = matter(fileContents); - const createdAt = getFileCreationDate(fullPath); - - const processedContent = await remark() - .use(html) - .process(content); - + const createdAt = getFileCreationDate(filePath); + const processedContent = await remark().use(html).process(content); return { - slug: realSlug, + type: 'post', + slug: relPath.replace(/\.md$/, ''), title: data.title, date: data.date, tags: data.tags || [], @@ -35,27 +30,31 @@ async function getPostBySlug(slug: string) { }; } -async function getAllPosts() { - const fileNames = fs.readdirSync(postsDirectory); - const allPostsData = await Promise.all( - fileNames - .filter((fileName) => fileName.endsWith('.md')) - .map(async (fileName) => { - const slug = fileName.replace(/\.md$/, ''); - return getPostBySlug(slug); - }) - ); - - return allPostsData.sort((a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); +async function readPostsDir(dir: string, relDir = ''): Promise { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const folders: any[] = []; + const posts: any[] = []; + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relPath = relDir ? path.join(relDir, entry.name) : entry.name; + if (entry.isDirectory()) { + const children = await readPostsDir(fullPath, relPath); + folders.push({ type: 'folder', name: entry.name, path: relPath, children }); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + posts.push(await getPostByPath(fullPath, relPath)); + } + } + // Sort posts by creation date (newest first) + posts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + // Folders first, then posts + return [...folders, ...posts]; } export async function GET() { try { - const posts = await getAllPosts(); - return NextResponse.json(posts); + const tree = await readPostsDir(postsDirectory); + return NextResponse.json(tree); } catch (error) { - return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 }); + return NextResponse.json({ error: 'Fehler beim Laden der Beiträge' }, { status: 500 }); } } \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index d27c4fc..efeb560 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { format } from 'date-fns'; interface Post { + type: 'post'; slug: string; title: string; date: string; @@ -14,35 +15,92 @@ interface Post { createdAt: string; } +interface Folder { + type: 'folder'; + name: string; + path: string; + children: (Folder | Post)[]; +} + +type Node = Folder | Post; + export default function Home() { - const [posts, setPosts] = useState([]); + const [tree, setTree] = useState([]); + const [currentPath, setCurrentPath] = useState([]); useEffect(() => { - // Initial load - loadPosts(); - - // Set up polling for changes - const interval = setInterval(loadPosts, 2000); - - // Cleanup + loadTree(); + const interval = setInterval(loadTree, 2000); return () => clearInterval(interval); }, []); - const loadPosts = async () => { + const loadTree = async () => { try { const response = await fetch('/api/posts'); const data = await response.json(); - setPosts(data); + setTree(data); } catch (error) { console.error('Fehler beim Laden der Beiträge:', error); } }; + // Traverse the tree to the current path + const getCurrentNodes = (): Node[] => { + let nodes: Node[] = tree; + for (const segment of currentPath) { + const folder = nodes.find( + (n) => n.type === 'folder' && n.name === segment + ) as Folder | undefined; + if (folder) { + nodes = folder.children; + } else { + break; + } + } + return nodes; + }; + + const nodes = getCurrentNodes(); + + // Breadcrumbs + const breadcrumbs = [ + { name: 'Startseite', path: [] }, + ...currentPath.map((name, idx) => ({ + name, + path: currentPath.slice(0, idx + 1), + })), + ]; + return (

Sebastian Zinkls - Blog

+
- {posts.map((post) => ( + {/* Folders */} + {nodes.filter((n) => n.type === 'folder').map((folder: any) => ( +
setCurrentPath([...currentPath, folder.name])} + > + 📁 {folder.name} +
+ ))} + {/* Posts */} + {nodes.filter((n) => n.type === 'post').map((post: any) => (

{post.title}

@@ -52,7 +110,7 @@ export default function Home() {

{post.summary}

- {post.tags.map((tag) => ( + {post.tags.map((tag: string) => ( (null); + // Join the slug array to get the full path + const slugPath = Array.isArray(params.slug) ? params.slug.join('/') : params.slug; + useEffect(() => { // Initial load loadPost(); @@ -26,11 +29,11 @@ export default function PostPage({ params }: { params: { slug: string } }) { // Cleanup return () => clearInterval(interval); - }, [params.slug]); + }, [slugPath]); const loadPost = async () => { try { - const response = await fetch(`/api/posts/${params.slug}`); + const response = await fetch(`/api/posts/${encodeURIComponent(slugPath)}`); const data = await response.json(); setPost(data); } catch (error) {