From 102783b9a9658f00541663275e5ee423aa82f36c Mon Sep 17 00:00:00 2001 From: ridhozhr10 Date: Wed, 22 May 2024 13:19:36 +0700 Subject: [PATCH] feat: list post per tags & directory --- src/app/_components/BlogPost/index.tsx | 4 +- src/app/posts/[...path]/page.tsx | 74 +++++++++++++++++--- src/app/posts/page.tsx | 2 +- src/app/tags/[tag]/page.tsx | 58 ++++++++++++++++ src/lib/api.ts | 95 +++++++++++++++++++++++--- 5 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 src/app/tags/[tag]/page.tsx diff --git a/src/app/_components/BlogPost/index.tsx b/src/app/_components/BlogPost/index.tsx index 035249e..9141e4a 100644 --- a/src/app/_components/BlogPost/index.tsx +++ b/src/app/_components/BlogPost/index.tsx @@ -12,7 +12,7 @@ import { BiLogoLinkedinSquare, BiLogoTwitter, BiShare, - BiTag, + BiTagAlt, BiX, } from "react-icons/bi"; import { @@ -112,7 +112,7 @@ export default function BlogPost({
{post.tags.length > 0 && (

- + {post.tags.map((tag) => ( {tag} diff --git a/src/app/posts/[...path]/page.tsx b/src/app/posts/[...path]/page.tsx index 6f269e6..3bb9edb 100644 --- a/src/app/posts/[...path]/page.tsx +++ b/src/app/posts/[...path]/page.tsx @@ -1,17 +1,57 @@ import BlogPost from "@/app/_components/BlogPost"; import BaseLayout from "@/app/_components/layout/BaseLayout"; import { baseURL } from "@/constants"; -import { getAllPosts, getNextPreviousPost, getPostBySlug } from "@/lib/api"; +import { + getNextPreviousPost, + getPostAndDirSlugs, + getPostOrDirBySlug, +} from "@/lib/api"; import mdToHtml from "@/lib/markdown"; +import dayjs from "dayjs"; import { Metadata } from "next"; +import Link from "next/link"; import { notFound } from "next/navigation"; +import "@/app/posts/style.scss"; type Props = { - params: { path: string[]; realPath: string }; + params: { path: string[] }; }; export default async function Post({ params }: Props) { - const post = getPostBySlug(params.path.join("/")); + const data = getPostOrDirBySlug(params.path.join("/")); + if (data.type === "dir") { + if (data.dir.length < 0) { + return notFound(); + } + return ( + +

+

+ Posts on {params.path.join("/")} +

+
+
    + {data.dir.map((post) => ( +
  • + + {post.title} + + {dayjs(post.created_at).format("MMM DD, YYYY")} + + +
  • + ))} +
+
+
+ + ); + } + + const { post } = data; const pagination = getNextPreviousPost(post); if (!post) { return notFound(); @@ -28,14 +68,24 @@ export default async function Post({ params }: Props) { } export function generateMetadata({ params }: Props): Metadata { - const post = getPostBySlug(params.path.join("/")); - - if (!post) { + const data = getPostOrDirBySlug(params.path.join("/")); + if (!data) { return notFound(); } + if (data.type === "dir") { + if (data.dir.length < 0) { + return notFound(); + } + return { + title: `${params.path.join("/")} Posts :: Ridho Azhar`, + }; + } + if (!data.post) { + return notFound(); + } + const post = data.post; const title = `${post.title} :: Ridho Azhar`; - return { title, description: post.description, @@ -54,9 +104,11 @@ export function generateMetadata({ params }: Props): Metadata { } export async function generateStaticParams() { - const posts = getAllPosts(); + const slugs = getPostAndDirSlugs(); - return posts.map((post) => ({ - path: post.path, - })); + return slugs.map((slug) => { + return { + path: slug.split("/"), + }; + }); } diff --git a/src/app/posts/page.tsx b/src/app/posts/page.tsx index 6dea62d..316b355 100644 --- a/src/app/posts/page.tsx +++ b/src/app/posts/page.tsx @@ -12,7 +12,7 @@ export default function Posts() { var postGroups = getPostGroupByYear(); return ( - +

Posts

{postGroups.map((postGroup) => ( diff --git a/src/app/tags/[tag]/page.tsx b/src/app/tags/[tag]/page.tsx new file mode 100644 index 0000000..f2f3e5c --- /dev/null +++ b/src/app/tags/[tag]/page.tsx @@ -0,0 +1,58 @@ +import { getAllTags, getPostGroupByYear } from "@/lib/api"; +import BaseLayout from "@/app/_components/layout/BaseLayout"; +import Link from "next/link"; +import dayjs from "dayjs"; +import "@/app/posts/style.scss"; +import { Metadata } from "next"; + +type Props = { + params: { + tag: string; + }; +}; + +export default function Tag({ params }: Props) { + var postGroups = getPostGroupByYear(params.tag); + + return ( + +
+

{params.tag}

+ {postGroups.map((postGroup) => ( +
+
{postGroup.year}
+
    + {postGroup.data.map((post) => ( +
  • + + {post.title} + + {dayjs(post.date).format("MMM DD")} + + +
  • + ))} +
+
+ ))} +
+
+ ); +} +export function generateMetadata({ params }: Props): Metadata { + const title = `#${params.tag} :: Ridho Azhar`; + + return { + title, + robots: { follow: true, index: true }, + }; +} +export function generateStaticParams() { + const tags = getAllTags(); + return tags.map((tag) => ({ + tag, + })); +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 8a7fe78..79475d0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,6 +1,6 @@ import { Post } from "@/interfaces/post"; import { lstatSync, readFileSync, readdirSync } from "fs"; -import { join, sep } from "path"; +import { dirname, join, sep } from "path"; import matter from "gray-matter"; const postsDir = join(process.cwd(), "_contents", "posts"); @@ -22,16 +22,72 @@ export function getPostSlugs(): string[] { return readDir(postsDir) || []; } -export function getPostBySlug(slug: string): Post { +export function getPostAndDirSlugs(): string[] { + const readDir = (path: string): string[] | undefined => { + const pathSplit = path.split(sep).map((d) => d.replace(/\.md$/, "")); + const postsDirIndex = pathSplit.indexOf("posts"); + + if (lstatSync(path).isDirectory()) { + const currentDirName = pathSplit + .filter((_, i) => i > postsDirIndex) + .join("/"); + return [ + currentDirName, + ...readdirSync(path) + .map((childPath) => readDir(join(path, childPath))) + .flat(), + ] as string[]; + } else { + // get from year only + return [pathSplit.filter((_, i) => i > postsDirIndex).join("/")]; + } + }; + + const res = readDir(postsDir) || []; + return res.filter((d) => d); +} + +interface SinglePost { + type: "post"; + post: Post; +} + +interface Directory { + type: "dir"; + dir: Post[]; +} + +export type PostOrDir = SinglePost | Directory; + +export function getPostOrDirBySlug(slug: string): PostOrDir { const split = slug.split("/").map((d) => d.replace(/\.md$/, "")); const fullPath = join(postsDir, ...split); + try { + if (lstatSync(fullPath).isDirectory()) { + return { + type: "dir", + dir: getAllPosts().filter((d) => { + const split = slug.split("/"); + let res = true; + for (let i = 0; i < split.length; i++) { + res = split[i] === d.path[i]; + if (!res) break; + } + return res; + }), + }; + } + } catch { + // do nothing + } + const fileContents = readFileSync(`${fullPath}.md`, "utf8"); const { data, content } = matter(fileContents); + const post: Post = { ...(data as Post), path: split, content }; return { - ...data, - path: split, - content: content, - } as Post; + post, + type: "post", + }; } export type SinglePagination = { prev?: Post; next?: Post }; @@ -56,8 +112,11 @@ export function getNextPreviousPost(post: Post): SinglePagination { export function getAllPosts(): Post[] { const slugs = getPostSlugs(); - const posts = slugs - .map((slug) => getPostBySlug(slug)) + const postList: SinglePost[] = slugs + .map((slug) => getPostOrDirBySlug(slug)) + .filter((d) => d.type === "post") as SinglePost[]; + const posts = postList + .map((d) => d.post as Post) .sort((a, b) => { return a.path.join("/") > b.path.join("/") ? -1 : 1; }); @@ -73,9 +132,13 @@ type PostGroup = { }[]; }; -export function getPostGroupByYear(): PostGroup[] { +export function getPostGroupByYear(tag?: string): PostGroup[] { const result: PostGroup[] = []; getAllPosts().forEach((post) => { + // filter + if (tag && !post.tags.includes(tag)) { + return; + } const [year] = post.path; const idxResult = result.map((d) => d.year).indexOf(year); const data = { @@ -83,6 +146,7 @@ export function getPostGroupByYear(): PostGroup[] { title: post.title, date: new Date(post.created_at), }; + if (idxResult < 0) { result.push({ year, @@ -94,3 +158,16 @@ export function getPostGroupByYear(): PostGroup[] { }); return result; } + +export function getAllTags(): string[] { + const res: string[] = ["tag6"]; + getAllPosts().forEach((post) => { + post.tags.forEach((tag) => { + if (!res.includes(tag)) { + res.push(tag); + } + }); + }); + + return res; +}