From a17197c655fea8d6ea114a489cdfed6b763bc867 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 13:18:11 +0700 Subject: [PATCH 01/13] extract post per page value --- src/utils/post.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils/post.ts b/src/utils/post.ts index 1ea6e9a..2de06bf 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -33,6 +33,8 @@ interface GetAllPostsParams { value?: string; } +const POST_PER_PAGE = 5; + export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { const dateComparator = (post1: PostData, post2: PostData) => new Date(post1.date) > new Date(post2.date) ? -1 : 1; @@ -77,11 +79,10 @@ export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { throw new Error("Page number must be positive."); } - const postPerPage = 5; - const totalPage = Math.ceil(allPosts.length / postPerPage); + const totalPage = Math.ceil(allPosts.length / POST_PER_PAGE); - const startIdx = (page - 1) * postPerPage; - const posts = allPosts.slice(startIdx, startIdx + postPerPage); + const startIdx = (page - 1) * POST_PER_PAGE; + const posts = allPosts.slice(startIdx, startIdx + POST_PER_PAGE); return { posts, totalPage }; } From 8a9c917bfedc232c5bba7e15608506f3b00d3b1b Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 13:19:55 +0700 Subject: [PATCH 02/13] change language type to use array const --- src/utils/post.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/post.ts b/src/utils/post.ts index 2de06bf..b060d91 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -8,13 +8,13 @@ import matter from "gray-matter"; import { type ImageData, getContentDir, getImageData } from "./file"; import { stripInlineMarkdown } from "./markdown"; -type Language = "en" | "id"; +const ALL_LANG = ["en", "id"] as const; interface FrontmatterData { date: string; featuredImage: string; tags: ReadonlyArray; - lang: Language; + lang: (typeof ALL_LANG)[number]; title: string; } From ddf16fdfc9f104485413c79310f0cb810ac6775d Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 13:20:19 +0700 Subject: [PATCH 03/13] add getAllBlogRoutes util function --- src/utils/post.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/utils/post.ts b/src/utils/post.ts index b060d91..d61cbdd 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -5,6 +5,7 @@ import fs from "fs"; import path from "path"; import matter from "gray-matter"; +import kebabCase from "lodash.kebabcase"; import { type ImageData, getContentDir, getImageData } from "./file"; import { stripInlineMarkdown } from "./markdown"; @@ -158,3 +159,32 @@ export function getAllPostTags() { const tags = Array.from(new Set(posts.map(post => post.tags).flat())); return tags; } + +export function getAllBlogRoutes() { + const createPaginatedRoutes = (basePath: string, postCount: number) => { + const totalPage = Math.ceil(postCount / POST_PER_PAGE); + return Array.from({ length: totalPage }).map( + (_, index) => `${basePath}${index > 0 ? `${index + 1}/` : ""}` + ); + }; + + const { posts } = getAllPosts(); + const postRoutes = posts.map(post => post.slug); + const blogRoutes = createPaginatedRoutes("/blog/", posts.length); + + const allTags = getAllPostTags(); + const tagRoutes = allTags.flatMap(tag => { + const { posts } = getAllPosts({ type: "tag", value: tag }); + const basePath = `/blog/tag/${kebabCase(tag)}/`; + return createPaginatedRoutes(basePath, posts.length); + }); + + const langRoutes = ALL_LANG.flatMap(lang => { + const { posts } = getAllPosts({ type: "lang", value: lang }); + const basePath = `/blog/tag/${kebabCase(lang)}/`; + return createPaginatedRoutes(basePath, posts.length); + }); + + const allRoutes = [...blogRoutes, ...tagRoutes, ...langRoutes, ...postRoutes]; + return allRoutes; +} From 93edeabc36d64f0f710d9c6d4e82c4e78fc7a7bf Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 21:18:45 +0700 Subject: [PATCH 04/13] move PageType to utils post --- src/utils/metadata.ts | 4 +--- src/utils/post.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts index 8ff93c5..ba25018 100644 --- a/src/utils/metadata.ts +++ b/src/utils/metadata.ts @@ -3,15 +3,13 @@ // Read the LICENSE file in the repository root for full license text. import { type ImageData } from "./file"; -import { getAllPostTags } from "./post"; +import { getAllPostTags, type PageType } from "./post"; export const langMap = new Map([ ["id", "Bahasa Indonesia"], ["en", "English"] ]); -type PageType = "Blog" | "Tag" | "Language"; - interface PageMetadataParams { type: PageType; filterValue?: string; diff --git a/src/utils/post.ts b/src/utils/post.ts index d61cbdd..8dbf9cb 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -11,6 +11,8 @@ import { stripInlineMarkdown } from "./markdown"; const ALL_LANG = ["en", "id"] as const; +export type PageType = "Blog" | "Tag" | "Language"; + interface FrontmatterData { date: string; featuredImage: string; @@ -30,7 +32,7 @@ export interface PostData extends FrontmatterData { interface GetAllPostsParams { page?: number; - type?: "tag" | "lang"; + type?: PageType; value?: string; } @@ -47,7 +49,7 @@ export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { if (type != null) { if ( - type === "tag" && + type === "Tag" && post.tags .map(tag => tag.toLowerCase()) .includes(value?.replaceAll("-", " ").toLowerCase() ?? "") @@ -55,7 +57,7 @@ export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { return true; } - if (type === "lang" && post.lang === value) { + if (type === "Language" && post.lang === value) { return true; } @@ -174,13 +176,13 @@ export function getAllBlogRoutes() { const allTags = getAllPostTags(); const tagRoutes = allTags.flatMap(tag => { - const { posts } = getAllPosts({ type: "tag", value: tag }); + const { posts } = getAllPosts({ type: "Tag", value: tag }); const basePath = `/blog/tag/${kebabCase(tag)}/`; return createPaginatedRoutes(basePath, posts.length); }); const langRoutes = ALL_LANG.flatMap(lang => { - const { posts } = getAllPosts({ type: "lang", value: lang }); + const { posts } = getAllPosts({ type: "Language", value: lang }); const basePath = `/blog/tag/${kebabCase(lang)}/`; return createPaginatedRoutes(basePath, posts.length); }); From 9e8b5ca0cd95cafc272f13c7b06ccd6af538f02a Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 21:26:28 +0700 Subject: [PATCH 05/13] move getAllBlogPageRoutes function to page utils --- src/utils/page.ts | 35 +++++++++++++++++++++++++++++++++++ src/utils/post.ts | 35 ++--------------------------------- 2 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 src/utils/page.ts diff --git a/src/utils/page.ts b/src/utils/page.ts new file mode 100644 index 0000000..487c33c --- /dev/null +++ b/src/utils/page.ts @@ -0,0 +1,35 @@ +// Copyright (c) Gagah Pangeran Rosfatiputra (GPR) . +// Licensed under The MIT License. +// Read the LICENSE file in the repository root for full license text + +import kebabCase from "lodash.kebabcase"; +import { ALL_LANG, POST_PER_PAGE, getAllPosts, getAllPostTags } from "./post"; + +export function getAllBlogPageRoutes() { + const createPaginatedRoutes = (basePath: string, postCount: number) => { + const totalPage = Math.ceil(postCount / POST_PER_PAGE); + return Array.from({ length: totalPage }).map( + (_, index) => `${basePath}${index > 0 ? `${index + 1}/` : ""}` + ); + }; + + const { posts } = getAllPosts(); + const postRoutes = posts.map(post => post.slug); + const blogRoutes = createPaginatedRoutes("/blog/", posts.length); + + const allTags = getAllPostTags(); + const tagRoutes = allTags.flatMap(tag => { + const { posts } = getAllPosts({ type: "Tag", value: tag }); + const basePath = `/blog/tag/${kebabCase(tag)}/`; + return createPaginatedRoutes(basePath, posts.length); + }); + + const langRoutes = ALL_LANG.flatMap(lang => { + const { posts } = getAllPosts({ type: "Language", value: lang }); + const basePath = `/blog/tag/${kebabCase(lang)}/`; + return createPaginatedRoutes(basePath, posts.length); + }); + + const allRoutes = [...blogRoutes, ...tagRoutes, ...langRoutes, ...postRoutes]; + return allRoutes; +} diff --git a/src/utils/post.ts b/src/utils/post.ts index 8dbf9cb..44a006b 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -5,11 +5,11 @@ import fs from "fs"; import path from "path"; import matter from "gray-matter"; -import kebabCase from "lodash.kebabcase"; import { type ImageData, getContentDir, getImageData } from "./file"; import { stripInlineMarkdown } from "./markdown"; -const ALL_LANG = ["en", "id"] as const; +export const ALL_LANG = ["en", "id"] as const; +export const POST_PER_PAGE = 5; export type PageType = "Blog" | "Tag" | "Language"; @@ -36,8 +36,6 @@ interface GetAllPostsParams { value?: string; } -const POST_PER_PAGE = 5; - export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { const dateComparator = (post1: PostData, post2: PostData) => new Date(post1.date) > new Date(post2.date) ? -1 : 1; @@ -161,32 +159,3 @@ export function getAllPostTags() { const tags = Array.from(new Set(posts.map(post => post.tags).flat())); return tags; } - -export function getAllBlogRoutes() { - const createPaginatedRoutes = (basePath: string, postCount: number) => { - const totalPage = Math.ceil(postCount / POST_PER_PAGE); - return Array.from({ length: totalPage }).map( - (_, index) => `${basePath}${index > 0 ? `${index + 1}/` : ""}` - ); - }; - - const { posts } = getAllPosts(); - const postRoutes = posts.map(post => post.slug); - const blogRoutes = createPaginatedRoutes("/blog/", posts.length); - - const allTags = getAllPostTags(); - const tagRoutes = allTags.flatMap(tag => { - const { posts } = getAllPosts({ type: "Tag", value: tag }); - const basePath = `/blog/tag/${kebabCase(tag)}/`; - return createPaginatedRoutes(basePath, posts.length); - }); - - const langRoutes = ALL_LANG.flatMap(lang => { - const { posts } = getAllPosts({ type: "Language", value: lang }); - const basePath = `/blog/tag/${kebabCase(lang)}/`; - return createPaginatedRoutes(basePath, posts.length); - }); - - const allRoutes = [...blogRoutes, ...tagRoutes, ...langRoutes, ...postRoutes]; - return allRoutes; -} From dfc81529793f1d36659e5e233aee37bf3a00fa9b Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 22:35:23 +0700 Subject: [PATCH 06/13] change PageType to lowercase --- src/utils/metadata.ts | 6 +++--- src/utils/page.ts | 6 +++--- src/utils/post.ts | 6 +++--- test/utils/metadata.test.ts | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts index ba25018..5861844 100644 --- a/src/utils/metadata.ts +++ b/src/utils/metadata.ts @@ -22,14 +22,14 @@ export function getPageMetadata({ let metadata = null; switch (type) { - case "Blog": + case "blog": metadata = { title: "Blog", description: "All Posts in Blog" }; break; - case "Language": + case "lang": const lang = langMap.get(filterValue); if (lang != null) { metadata = { @@ -39,7 +39,7 @@ export function getPageMetadata({ } break; - case "Tag": + case "tag": const tags = getAllPostTags(); const tag = tags.find( tag => diff --git a/src/utils/page.ts b/src/utils/page.ts index 487c33c..a2dce5a 100644 --- a/src/utils/page.ts +++ b/src/utils/page.ts @@ -19,14 +19,14 @@ export function getAllBlogPageRoutes() { const allTags = getAllPostTags(); const tagRoutes = allTags.flatMap(tag => { - const { posts } = getAllPosts({ type: "Tag", value: tag }); + const { posts } = getAllPosts({ type: "tag", value: tag }); const basePath = `/blog/tag/${kebabCase(tag)}/`; return createPaginatedRoutes(basePath, posts.length); }); const langRoutes = ALL_LANG.flatMap(lang => { - const { posts } = getAllPosts({ type: "Language", value: lang }); - const basePath = `/blog/tag/${kebabCase(lang)}/`; + const { posts } = getAllPosts({ type: "lang", value: lang }); + const basePath = `/blog/lang/${kebabCase(lang)}/`; return createPaginatedRoutes(basePath, posts.length); }); diff --git a/src/utils/post.ts b/src/utils/post.ts index 44a006b..227f620 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -11,7 +11,7 @@ import { stripInlineMarkdown } from "./markdown"; export const ALL_LANG = ["en", "id"] as const; export const POST_PER_PAGE = 5; -export type PageType = "Blog" | "Tag" | "Language"; +export type PageType = "blog" | "tag" | "lang"; interface FrontmatterData { date: string; @@ -47,7 +47,7 @@ export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { if (type != null) { if ( - type === "Tag" && + type === "tag" && post.tags .map(tag => tag.toLowerCase()) .includes(value?.replaceAll("-", " ").toLowerCase() ?? "") @@ -55,7 +55,7 @@ export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { return true; } - if (type === "Language" && post.lang === value) { + if (type === "lang" && post.lang === value) { return true; } diff --git a/test/utils/metadata.test.ts b/test/utils/metadata.test.ts index afd3fcd..a44e480 100644 --- a/test/utils/metadata.test.ts +++ b/test/utils/metadata.test.ts @@ -14,7 +14,7 @@ afterAll(() => { describe("Test getPageMetadata function", () => { test("Blog type", () => { - const result = getPageMetadata({ type: "Blog" }); + const result = getPageMetadata({ type: "blog" }); const expectedResult = { title: "Blog", @@ -26,7 +26,7 @@ describe("Test getPageMetadata function", () => { test("Tag type", () => { const result = getPageMetadata({ - type: "Tag", + type: "tag", filterValue: "Cloud" }); @@ -40,7 +40,7 @@ describe("Test getPageMetadata function", () => { test("Tag type invalid", () => { const result = getPageMetadata({ - type: "Tag", + type: "tag", filterValue: "Programming" }); @@ -49,7 +49,7 @@ describe("Test getPageMetadata function", () => { test("Language type", () => { const result = getPageMetadata({ - type: "Language", + type: "lang", filterValue: "en" }); @@ -63,7 +63,7 @@ describe("Test getPageMetadata function", () => { test("Language type invalid", () => { const result = getPageMetadata({ - type: "Language", + type: "lang", filterValue: "xx" }); From cfe06b602d151b7f462229c00459675042668958 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 22:51:15 +0700 Subject: [PATCH 07/13] add type blog filter in getAllPosts util function --- src/utils/post.ts | 4 ++++ test/utils/post.test.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/utils/post.ts b/src/utils/post.ts index 227f620..a5e8011 100644 --- a/src/utils/post.ts +++ b/src/utils/post.ts @@ -46,6 +46,10 @@ export function getAllPosts({ page, type, value }: GetAllPostsParams = {}) { } if (type != null) { + if (type == "blog") { + return true; + } + if ( type === "tag" && post.tags diff --git a/test/utils/post.test.ts b/test/utils/post.test.ts index 9bb258c..fe9889b 100644 --- a/test/utils/post.test.ts +++ b/test/utils/post.test.ts @@ -45,6 +45,38 @@ describe("test getAllPosts function", () => { expect(posts).toMatchObject(expected); }); + test("Filter by blog", () => { + const expected = [ + { + title: "Dolor Sit Amet", + date: "March 14, 2025", + featuredImage: "./img/solid.png", + tags: ["Dolor", "Sit", "Amet"], + lang: "id", + slug: "/blog/dolor-sit-amet/" + }, + { + title: "Test Blog Post", + date: "January 29, 2025", + featuredImage: "./img/thumbnail.png", + tags: ["Story", "Cloud"], + lang: "en", + slug: "/blog/my-post/" + }, + { + title: "Lorem Ipsum", + date: "December 14, 2024", + featuredImage: "./img/solid.png", + tags: ["Lorem", "Ipsum"], + lang: "en", + slug: "/blog/lorem-ipsum/" + } + ]; + + const { posts } = getAllPosts({ page: 1, type: "blog" }); + expect(posts).toMatchObject(expected); + }); + test("Filter by tag", () => { const { posts } = getAllPosts({ page: 1, type: "tag", value: "Lorem" }); const expected = [ From 9d8331f64db95b0f1965724002515aaa3f23c4d9 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 23:07:52 +0700 Subject: [PATCH 08/13] add getBlogPageDataBySlug utils page --- src/utils/page.ts | 75 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/utils/page.ts b/src/utils/page.ts index a2dce5a..1735642 100644 --- a/src/utils/page.ts +++ b/src/utils/page.ts @@ -3,7 +3,30 @@ // Read the LICENSE file in the repository root for full license text import kebabCase from "lodash.kebabcase"; -import { ALL_LANG, POST_PER_PAGE, getAllPosts, getAllPostTags } from "./post"; +import { + type PostData, + type PageType, + ALL_LANG, + POST_PER_PAGE, + getAllPosts, + getAllPostTags, + getPostBySlug +} from "./post"; + +interface PostPageData { + kind: "post"; + post: PostData; +} + +interface BlogPageData { + kind: "blog"; + posts: PostData[]; + totalPage: number; + pageNumber: number; + paginationPath: string; + type: PageType; + filterValue?: string; +} export function getAllBlogPageRoutes() { const createPaginatedRoutes = (basePath: string, postCount: number) => { @@ -33,3 +56,53 @@ export function getAllBlogPageRoutes() { const allRoutes = [...blogRoutes, ...tagRoutes, ...langRoutes, ...postRoutes]; return allRoutes; } + +export function getBlogPageDataBySlug( + slug?: string[] +): BlogPageData | PostPageData | null { + let params: { page: number; type: PageType; value?: string } | undefined; + + if (slug == null || slug.length === 0) { + params = { page: 1, type: "blog" }; + } else if (Number.isInteger(Number(slug[0]))) { + params = { page: Number(slug[0]), type: "blog" }; + } else if (slug[0] === "tag" || slug[0] === "lang") { + const filterValue = slug[1]; + + if (slug[2] == null) { + params = { page: 1, type: slug[0], value: filterValue }; + } else if (Number.isInteger(Number(slug[2]))) { + params = { page: Number(slug[2]), type: slug[0], value: filterValue }; + } + } + + if (params != null) { + const { posts, totalPage } = getAllPosts(params); + + let paginationPath = "/blog/"; + if (params.type !== "blog") { + paginationPath += `${params.type}/${params.value}/`; + } + + return { + kind: "blog", + posts, + totalPage, + pageNumber: params.page, + paginationPath, + type: params.type, + filterValue: params.value + }; + } + + const post = getPostBySlug(slug?.[0] ?? ""); + + if (post != null) { + return { + kind: "post", + post + }; + } + + return null; +} From 9a21e304ec7d82df5778a4abe08ef631c9979b0a Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 23:08:18 +0700 Subject: [PATCH 09/13] remove all page component in blog route --- src/app/blog/[slug]/page.tsx | 81 ---------------------------- src/app/blog/lang/[...slug]/page.tsx | 72 ------------------------- src/app/blog/page.tsx | 43 --------------- src/app/blog/tag/[...slug]/page.tsx | 72 ------------------------- 4 files changed, 268 deletions(-) delete mode 100644 src/app/blog/[slug]/page.tsx delete mode 100644 src/app/blog/lang/[...slug]/page.tsx delete mode 100644 src/app/blog/page.tsx delete mode 100644 src/app/blog/tag/[...slug]/page.tsx diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx deleted file mode 100644 index f37649c..0000000 --- a/src/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Gagah Pangeran Rosfatiputra (GPR) . -// Licensed under The MIT License. -// Read the LICENSE file in the repository root for full license text. - -import { type Metadata } from "next"; -import { notFound } from "next/navigation"; -import BlogPost from "@/templates/Post"; -import Blog from "@/templates/Blog"; -import { - getPageMetadata, - getOtherMetadata, - notFoundMetadata -} from "@/utils/metadata"; -import { getAllPosts, getPostBySlug } from "@/utils/post"; - -interface Props { - params: Promise<{ slug: string }>; -} - -export async function generateMetadata({ params }: Props): Promise { - const { slug } = await params; - const page = Number(slug); - - if (isNaN(page) || page < 0) { - const postData = getPostBySlug(slug); - - if (postData == null) { - return notFoundMetadata; - } - - const { title, description, image } = postData; - return { - title, - description, - ...getOtherMetadata(title, description, image) - }; - } - - const metadata = getPageMetadata({ type: "Blog" }); - - if (metadata == null) { - return notFoundMetadata; - } - - return metadata; -} - -export default async function BlogPagination({ params }: Props) { - const { slug } = await params; - const page = Number(slug); - - if (isNaN(page) || page < 0) { - const postData = getPostBySlug(slug); - - if (postData == null) { - notFound(); - } - - return ; - } - - const { posts, totalPage } = getAllPosts({ page }); - const metadata = getPageMetadata({ type: "Blog" }); - - if (metadata == null) { - notFound(); - } - - const { title, description } = metadata; - - return ( - - ); -} diff --git a/src/app/blog/lang/[...slug]/page.tsx b/src/app/blog/lang/[...slug]/page.tsx deleted file mode 100644 index 5dd4ec6..0000000 --- a/src/app/blog/lang/[...slug]/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Gagah Pangeran Rosfatiputra (GPR) . -// Licensed under The MIT License. -// Read the LICENSE file in the repository root for full license text. - -import { type Metadata } from "next"; -import Blog from "@/templates/Blog"; -import { getPageMetadata, notFoundMetadata } from "@/utils/metadata"; -import { getAllPosts } from "@/utils/post"; -import { notFound } from "next/navigation"; - -interface Props { - params: Promise<{ slug: string[] }>; -} - -export async function generateMetadata({ params }: Props): Promise { - const { slug } = await params; - const langValue = slug[0]; - - const metadata = getPageMetadata({ - type: "Language", - filterValue: langValue - }); - - if (metadata == null) { - return notFoundMetadata; - } - - return metadata; -} - -export default async function BlogLang({ params }: Props) { - const { slug } = await params; - - if (slug.length > 2) { - notFound(); - } - - const langValue = slug[0]; - const page = Number(slug[1] ?? 1); - - if (isNaN(page) || page < 0) { - notFound(); - } - - const { posts, totalPage } = getAllPosts({ - page, - type: "lang", - value: langValue - }); - - const metadata = getPageMetadata({ - type: "Language", - filterValue: langValue - }); - - if (metadata == null) { - notFound(); - } - - const { title, description } = metadata; - - return ( - - ); -} diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx deleted file mode 100644 index ed771af..0000000 --- a/src/app/blog/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Gagah Pangeran Rosfatiputra (GPR) . -// Licensed under The MIT License. -// Read the LICENSE file in the repository root for full license text. - -import { type Metadata } from "next"; -import Blog from "@/templates/Blog"; -import { getPageMetadata, notFoundMetadata } from "@/utils/metadata"; -import { getAllPosts } from "@/utils/post"; -import { notFound } from "next/navigation"; - -export async function generateMetadata(): Promise { - const metadata = getPageMetadata({ type: "Blog" }); - - if (metadata == null) { - return notFoundMetadata; - } - - return metadata; -} - -export default function BlogPage() { - const pageNumber = 1; - const { posts, totalPage } = getAllPosts({ page: pageNumber }); - - const metadata = getPageMetadata({ type: "Blog" }); - - if (metadata == null) { - notFound(); - } - - const { title, description } = metadata; - - return ( - - ); -} diff --git a/src/app/blog/tag/[...slug]/page.tsx b/src/app/blog/tag/[...slug]/page.tsx deleted file mode 100644 index 5e5f61f..0000000 --- a/src/app/blog/tag/[...slug]/page.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Gagah Pangeran Rosfatiputra (GPR) . -// Licensed under The MIT License. -// Read the LICENSE file in the repository root for full license text. - -import { type Metadata } from "next"; -import Blog from "@/templates/Blog"; -import { getPageMetadata, notFoundMetadata } from "@/utils/metadata"; -import { getAllPosts } from "@/utils/post"; -import { notFound } from "next/navigation"; - -interface Props { - params: Promise<{ slug: string[] }>; -} - -export async function generateMetadata({ params }: Props): Promise { - const { slug } = await params; - const tagValue = slug[0]; - - const metadata = getPageMetadata({ - type: "Tag", - filterValue: tagValue - }); - - if (metadata == null) { - return notFoundMetadata; - } - - return metadata; -} - -export default async function BlogTag({ params }: Props) { - const { slug } = await params; - - if (slug.length > 2) { - notFound(); - } - - const tagValue = slug[0]; - const page = Number(slug[1] ?? 1); - - if (isNaN(page) || page < 0) { - notFound(); - } - - const { posts, totalPage } = getAllPosts({ - page, - type: "tag", - value: tagValue - }); - - const metadata = getPageMetadata({ - type: "Tag", - filterValue: tagValue - }); - - if (metadata == null) { - notFound(); - } - - const { title, description } = metadata; - - return ( - - ); -} From 59271b0afc540df39ab35c5520b1291bce0651cb Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 23:08:44 +0700 Subject: [PATCH 10/13] add dynamic catch all route in blog page --- src/app/blog/[[...slug]]/page.tsx | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/app/blog/[[...slug]]/page.tsx diff --git a/src/app/blog/[[...slug]]/page.tsx b/src/app/blog/[[...slug]]/page.tsx new file mode 100644 index 0000000..34c7cc5 --- /dev/null +++ b/src/app/blog/[[...slug]]/page.tsx @@ -0,0 +1,90 @@ +// Copyright (c) Gagah Pangeran Rosfatiputra (GPR) . +// Licensed under The MIT License. +// Read the LICENSE file in the repository root for full license text. + +import { type Metadata } from "next"; +import { notFound } from "next/navigation"; +import Blog from "@/templates/Blog"; +import Post from "@/templates/Post"; +import { + getOtherMetadata, + getPageMetadata, + notFoundMetadata +} from "@/utils/metadata"; +import { getAllBlogPageRoutes, getBlogPageDataBySlug } from "@/utils/page"; + +interface Props { + params: Promise<{ slug?: string[] }>; +} + +export const dynamicParams = false; + +export async function generateStaticParams() { + const allRoutes = getAllBlogPageRoutes(); + const allSlugs = allRoutes.map(route => { + const [_, ...slug] = route.split("/").filter(Boolean); + return { slug }; + }); + return allSlugs; +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + const data = getBlogPageDataBySlug(slug); + + if (data == null) { + return notFoundMetadata; + } + + if (data.kind === "post") { + const { title, description, image } = data.post; + return { + title, + description, + ...getOtherMetadata(title, description, image) + }; + } + + const { type, filterValue } = data; + const metadata = getPageMetadata({ type, filterValue }); + + if (metadata == null) { + return notFoundMetadata; + } + + return metadata; +} + +export default async function BlogPage({ params }: Props) { + const { slug } = await params; + const data = getBlogPageDataBySlug(slug); + + if (data == null) { + notFound(); + } + + if (data.kind === "post") { + return ; + } + + const { posts, totalPage, pageNumber, paginationPath, type, filterValue } = + data; + + const metadata = getPageMetadata({ type, filterValue }); + if (metadata == null) { + notFound(); + } + + const { title, description } = metadata; + + return ( + + ); +} From 4f023975068a1c1cadffab9e98d178ce785bf53f Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 22 Feb 2025 23:55:34 +0700 Subject: [PATCH 11/13] add revalidate 0 in changelog page to prevent caching --- src/app/changelog/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index f326ba2..31a3d74 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -6,6 +6,8 @@ import Link from "next/link"; import Page from "@/templates/Page"; import { getAllReleases } from "@/utils/changelog"; +export const revalidate = 0; + const title = "Changelog"; const desc = "All release changelog"; From 90f304929c885c739ebc38d8c5b1cdfbd78dbf93 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sun, 23 Feb 2025 00:48:26 +0700 Subject: [PATCH 12/13] add test for page utils --- test/utils/page.test.ts | 122 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 test/utils/page.test.ts diff --git a/test/utils/page.test.ts b/test/utils/page.test.ts new file mode 100644 index 0000000..4de5fbf --- /dev/null +++ b/test/utils/page.test.ts @@ -0,0 +1,122 @@ +// Copyright (c) Gagah Pangeran Rosfatiputra (GPR) . +// Licensed under The MIT License. +// Read the LICENSE file in the repository root for full license text. + +import { getAllBlogPageRoutes, getBlogPageDataBySlug } from "@/utils/page"; + +describe("test getAllBlogPageRoutes function", () => { + test("get all routes", () => { + const expected = [ + "/blog/", + "/blog/tag/dolor/", + "/blog/tag/sit/", + "/blog/tag/amet/", + "/blog/tag/story/", + "/blog/tag/cloud/", + "/blog/tag/lorem/", + "/blog/tag/ipsum/", + "/blog/lang/en/", + "/blog/lang/id/", + "/blog/dolor-sit-amet/", + "/blog/my-post/", + "/blog/lorem-ipsum/" + ]; + + const result = getAllBlogPageRoutes(); + expect(result).toMatchObject(expected); + }); +}); + +describe("test getBlogPageDataBySlug function", () => { + test("blog page", () => { + const expected = { + kind: "blog", + posts: [ + { + title: "Dolor Sit Amet", + slug: "/blog/dolor-sit-amet/" + }, + { + title: "Test Blog Post", + slug: "/blog/my-post/" + }, + { + title: "Lorem Ipsum", + slug: "/blog/lorem-ipsum/" + } + ], + totalPage: 1, + pageNumber: 1, + paginationPath: "/blog/", + type: "blog" + }; + + const result = getBlogPageDataBySlug([]); + expect(result).toMatchObject(expected); + }); + + test("tag page", () => { + const expected = { + kind: "blog", + posts: [ + { + title: "Dolor Sit Amet", + tags: ["Dolor", "Sit", "Amet"], + slug: "/blog/dolor-sit-amet/" + } + ], + totalPage: 1, + pageNumber: 1, + paginationPath: "/blog/tag/dolor/", + type: "tag", + filterValue: "dolor" + }; + + const result = getBlogPageDataBySlug(["tag", "dolor"]); + expect(result).toMatchObject(expected); + }); + + test("lang page", () => { + const expected = { + kind: "blog", + posts: [ + { + title: "Test Blog Post", + lang: "en", + slug: "/blog/my-post/" + }, + { + title: "Lorem Ipsum", + lang: "en", + slug: "/blog/lorem-ipsum/" + } + ], + totalPage: 1, + pageNumber: 1, + paginationPath: "/blog/lang/en/", + type: "lang", + filterValue: "en" + }; + + const result = getBlogPageDataBySlug(["lang", "en"]); + expect(result).toMatchObject(expected); + }); + + test("post page", () => { + const expected = { + kind: "post", + post: { + title: "Test Blog Post", + slug: "/blog/my-post/" + } + }; + + const result = getBlogPageDataBySlug(["my-post"]); + expect(result).toMatchObject(expected); + }); + + test("random page", () => { + const result = getBlogPageDataBySlug(["random-post"]); + expect(result).toBeNull(); + }); +}); From 31903664fd8315b018648b45f314c635bc2ba771 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 25 Feb 2025 00:14:20 +0700 Subject: [PATCH 13/13] simplify Blog template props using spread syntax --- src/app/blog/[[...slug]]/page.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/app/blog/[[...slug]]/page.tsx b/src/app/blog/[[...slug]]/page.tsx index 34c7cc5..eaa8b9e 100644 --- a/src/app/blog/[[...slug]]/page.tsx +++ b/src/app/blog/[[...slug]]/page.tsx @@ -67,24 +67,12 @@ export default async function BlogPage({ params }: Props) { return ; } - const { posts, totalPage, pageNumber, paginationPath, type, filterValue } = - data; + const { type, filterValue } = data; const metadata = getPageMetadata({ type, filterValue }); if (metadata == null) { notFound(); } - const { title, description } = metadata; - - return ( - - ); + return ; }