From feaf5e122acdd4aac3c589290d86343040bc3bb2 Mon Sep 17 00:00:00 2001 From: satnaing Date: Sun, 16 Mar 2025 20:09:03 +0700 Subject: [PATCH] feat: allow blog posts to be organized by subdirectories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blog posts can now be structured into subdirectories, which determine their slugs. Example: - `/src/data/blog/2025/testing.md` → `/posts/2025/testing` Directories prefixed with `_` will be excluded from the slug. Example: - `/src/data/blog/_examples/tailwind-typography.md` → `/posts/tailwind-typography` Closes #470, #366 --- src/components/Card.astro | 11 +++--- src/config.ts | 2 +- src/content.config.ts | 4 ++- .../{ => _examples}/example-draft-post.md | 0 .../{ => _examples}/tailwind-typography.md | 0 .../{ => _examples}/terminal-development.md | 0 src/data/blog/{ => releases}/astro-paper-2.md | 0 src/data/blog/{ => releases}/astro-paper-3.md | 0 src/data/blog/{ => releases}/astro-paper-4.md | 2 +- src/data/blog/{ => releases}/astro-paper-5.md | 2 +- src/layouts/PostDetails.astro | 3 +- src/pages/archives/index.astro | 4 +-- src/pages/index.astro | 14 +++----- src/pages/posts/[...page].astro | 6 +--- .../posts/{[slug] => [...slug]}/index.astro | 4 +-- .../posts/{[slug] => [...slug]}/index.png.ts | 4 +-- src/pages/rss.xml.ts | 5 +-- src/pages/tags/[tag]/[...page].astro | 6 +--- src/utils/getPath.ts | 36 +++++++++++++++++++ 19 files changed, 64 insertions(+), 39 deletions(-) rename src/data/blog/{ => _examples}/example-draft-post.md (100%) rename src/data/blog/{ => _examples}/tailwind-typography.md (100%) rename src/data/blog/{ => _examples}/terminal-development.md (100%) rename src/data/blog/{ => releases}/astro-paper-2.md (100%) rename src/data/blog/{ => releases}/astro-paper-3.md (100%) rename src/data/blog/{ => releases}/astro-paper-4.md (99%) rename src/data/blog/{ => releases}/astro-paper-5.md (98%) rename src/pages/posts/{[slug] => [...slug]}/index.astro (85%) rename src/pages/posts/{[slug] => [...slug]}/index.png.ts (88%) create mode 100644 src/utils/getPath.ts diff --git a/src/components/Card.astro b/src/components/Card.astro index 572eab8..59bc65a 100644 --- a/src/components/Card.astro +++ b/src/components/Card.astro @@ -1,17 +1,16 @@ --- import { slugifyStr } from "@/utils/slugify"; import type { CollectionEntry } from "astro:content"; +import { getPath } from "@/utils/getPath"; import Datetime from "./Datetime.astro"; -export interface Props { +export interface Props extends CollectionEntry<"blog"> { variant?: "h2" | "h3"; - href?: string; - frontmatter: CollectionEntry<"blog">["data"]; } -const { href, frontmatter, variant = "h2" } = Astro.props; +const { variant = "h2", data, id, filePath } = Astro.props; -const { title, pubDatetime, modDatetime, description } = frontmatter; +const { title, pubDatetime, modDatetime, description } = data; const headerProps = { style: { viewTransitionName: slugifyStr(title) }, @@ -21,7 +20,7 @@ const headerProps = {
  • { diff --git a/src/config.ts b/src/config.ts index 4731a27..9c86a05 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,7 +12,7 @@ export const SITE = { showArchives: true, showBackButton: true, // show back button in post detail editPost: { - url: "https://github.com/satnaing/astro-paper/edit/main/src/content/blog", + url: "https://github.com/satnaing/astro-paper/edit/main/src/data/blog", text: "Suggest Changes", appendFilePath: true, }, diff --git a/src/content.config.ts b/src/content.config.ts index cfc3501..d5ebadc 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -2,8 +2,10 @@ import { defineCollection, z } from "astro:content"; import { glob } from "astro/loaders"; import { SITE } from "@/config"; +export const BLOG_PATH = "src/data/blog"; + const blog = defineCollection({ - loader: glob({ pattern: "**/[^_]*.md", base: "./src/data/blog" }), + loader: glob({ pattern: "**/[^_]*.md", base: `./${BLOG_PATH}` }), schema: ({ image }) => z.object({ author: z.string().default(SITE.author), diff --git a/src/data/blog/example-draft-post.md b/src/data/blog/_examples/example-draft-post.md similarity index 100% rename from src/data/blog/example-draft-post.md rename to src/data/blog/_examples/example-draft-post.md diff --git a/src/data/blog/tailwind-typography.md b/src/data/blog/_examples/tailwind-typography.md similarity index 100% rename from src/data/blog/tailwind-typography.md rename to src/data/blog/_examples/tailwind-typography.md diff --git a/src/data/blog/terminal-development.md b/src/data/blog/_examples/terminal-development.md similarity index 100% rename from src/data/blog/terminal-development.md rename to src/data/blog/_examples/terminal-development.md diff --git a/src/data/blog/astro-paper-2.md b/src/data/blog/releases/astro-paper-2.md similarity index 100% rename from src/data/blog/astro-paper-2.md rename to src/data/blog/releases/astro-paper-2.md diff --git a/src/data/blog/astro-paper-3.md b/src/data/blog/releases/astro-paper-3.md similarity index 100% rename from src/data/blog/astro-paper-3.md rename to src/data/blog/releases/astro-paper-3.md diff --git a/src/data/blog/astro-paper-4.md b/src/data/blog/releases/astro-paper-4.md similarity index 99% rename from src/data/blog/astro-paper-4.md rename to src/data/blog/releases/astro-paper-4.md index 68b11d2..89d40ef 100644 --- a/src/data/blog/astro-paper-4.md +++ b/src/data/blog/releases/astro-paper-4.md @@ -4,7 +4,7 @@ pubDatetime: 2024-01-04T09:30:41.816Z title: AstroPaper 4.0 slug: "astro-paper-v4" featured: false -ogImage: ../../assets/images/AstroPaper-v4.png +ogImage: ../../../assets/images/AstroPaper-v4.png tags: - release description: "AstroPaper v4: ensuring a smoother and more feature-rich blogging experience." diff --git a/src/data/blog/astro-paper-5.md b/src/data/blog/releases/astro-paper-5.md similarity index 98% rename from src/data/blog/astro-paper-5.md rename to src/data/blog/releases/astro-paper-5.md index 309a8df..89ec3df 100644 --- a/src/data/blog/astro-paper-5.md +++ b/src/data/blog/releases/astro-paper-5.md @@ -3,7 +3,7 @@ pubDatetime: 2025-03-08T08:18:19.693Z title: AstroPaper 5.0 slug: astro-paper-v5 featured: true -ogImage: ../../assets/images/AstroPaper-v5.png +ogImage: ../../../assets/images/AstroPaper-v5.png tags: - release description: "AstroPaper v5: keep the clean look, updates under the hood." diff --git a/src/layouts/PostDetails.astro b/src/layouts/PostDetails.astro index 720787b..da27ea6 100644 --- a/src/layouts/PostDetails.astro +++ b/src/layouts/PostDetails.astro @@ -8,6 +8,7 @@ import Datetime from "@/components/Datetime.astro"; import EditPost from "@/components/EditPost.astro"; import ShareLinks from "@/components/ShareLinks.astro"; import BackButton from "@/components/BackButton.astro"; +import { getPath } from "@/utils/getPath"; import { slugifyStr } from "@/utils/slugify"; import IconChevronLeft from "@/assets/icons/IconChevronLeft.svg"; import IconChevronRight from "@/assets/icons/IconChevronRight.svg"; @@ -45,7 +46,7 @@ if (typeof initOgImage === "string") { // Use dynamic OG image if enabled and no remote|local ogImage if (!ogImageUrl && SITE.dynamicOgImage) { - ogImageUrl = `/posts/${slugifyStr(title)}/index.png`; + ogImageUrl = `${getPath(post.id, post.filePath)}/index.png`; } // Resolve OG image URL (or fallback to SITE.ogImage / default `og.png`) diff --git a/src/pages/archives/index.astro b/src/pages/archives/index.astro index 72369e6..030d671 100644 --- a/src/pages/archives/index.astro +++ b/src/pages/archives/index.astro @@ -69,8 +69,8 @@ const months = [ new Date(a.data.pubDatetime).getTime() / 1000 ) ) - .map(({ data, id }) => ( - + .map(data => ( + ))} diff --git a/src/pages/index.astro b/src/pages/index.astro index 9df2c32..45fc8e0 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -76,8 +76,8 @@ const recentPosts = sortedPosts.filter(({ data }) => !data.featured); @@ -92,14 +92,8 @@ const recentPosts = sortedPosts.filter(({ data }) => !data.featured);

    Recent Posts

      {recentPosts.map( - ({ data, id }, index) => - index < SITE.postPerIndex && ( - - ) + (data, index) => + index < SITE.postPerIndex && )}
    diff --git a/src/pages/posts/[...page].astro b/src/pages/posts/[...page].astro index 026673e..2c554aa 100644 --- a/src/pages/posts/[...page].astro +++ b/src/pages/posts/[...page].astro @@ -22,11 +22,7 @@ const { page } = Astro.props;
      - { - page.data.map(({ data, id }) => ( - - )) - } + {page.data.map(data => )}
    diff --git a/src/pages/posts/[slug]/index.astro b/src/pages/posts/[...slug]/index.astro similarity index 85% rename from src/pages/posts/[slug]/index.astro rename to src/pages/posts/[...slug]/index.astro index 2ee0f72..340be11 100644 --- a/src/pages/posts/[slug]/index.astro +++ b/src/pages/posts/[...slug]/index.astro @@ -2,6 +2,7 @@ import { type CollectionEntry, getCollection } from "astro:content"; import PostDetails from "@/layouts/PostDetails.astro"; import getSortedPosts from "@/utils/getSortedPosts"; +import { getPath } from "@/utils/getPath"; export interface Props { post: CollectionEntry<"blog">; @@ -9,9 +10,8 @@ export interface Props { export async function getStaticPaths() { const posts = await getCollection("blog", ({ data }) => !data.draft); - const postResult = posts.map(post => ({ - params: { slug: post.id }, + params: { slug: getPath(post.id, post.filePath, false) }, props: { post }, })); diff --git a/src/pages/posts/[slug]/index.png.ts b/src/pages/posts/[...slug]/index.png.ts similarity index 88% rename from src/pages/posts/[slug]/index.png.ts rename to src/pages/posts/[...slug]/index.png.ts index 396cfe6..3295ecb 100644 --- a/src/pages/posts/[slug]/index.png.ts +++ b/src/pages/posts/[...slug]/index.png.ts @@ -1,7 +1,7 @@ import type { APIRoute } from "astro"; import { getCollection, type CollectionEntry } from "astro:content"; +import { getPath } from "@/utils/getPath"; import { generateOgImageForPost } from "@/utils/generateOgImages"; -import { slugifyStr } from "@/utils/slugify"; import { SITE } from "@/config"; export async function getStaticPaths() { @@ -14,7 +14,7 @@ export async function getStaticPaths() { ); return posts.map(post => ({ - params: { slug: slugifyStr(post.data.title) }, + params: { slug: getPath(post.id, post.filePath, false) }, props: post, })); } diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index 3322a07..642e4f2 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -1,5 +1,6 @@ import rss from "@astrojs/rss"; import { getCollection } from "astro:content"; +import { getPath } from "@/utils/getPath"; import getSortedPosts from "@/utils/getSortedPosts"; import { SITE } from "@/config"; @@ -10,8 +11,8 @@ export async function GET() { title: SITE.title, description: SITE.desc, site: SITE.website, - items: sortedPosts.map(({ data, id }) => ({ - link: `posts/${id}/`, + items: sortedPosts.map(({ data, id, filePath }) => ({ + link: getPath(id, filePath), title: data.title, description: data.description, pubDate: new Date(data.modDatetime ?? data.pubDatetime), diff --git a/src/pages/tags/[tag]/[...page].astro b/src/pages/tags/[tag]/[...page].astro index fcb8a4a..cb9dec2 100644 --- a/src/pages/tags/[tag]/[...page].astro +++ b/src/pages/tags/[tag]/[...page].astro @@ -40,11 +40,7 @@ const { page, tagName } = Astro.props; >

    {`Tag:${tag}`}

      - { - page.data.map(({ data, id }) => ( - - )) - } + {page.data.map(data => )}
    diff --git a/src/utils/getPath.ts b/src/utils/getPath.ts new file mode 100644 index 0000000..057b612 --- /dev/null +++ b/src/utils/getPath.ts @@ -0,0 +1,36 @@ +import { BLOG_PATH } from "@/content.config"; +import { slugifyStr } from "./slugify"; + +/** + * Get full path of a blog post + * @param id - id of the blog post (aka slug) + * @param filePath - the blog post full file location + * @param includeBase - whether to include `/posts` in return value + * @returns blog post path + */ +export function getPath( + id: string, + filePath: string | undefined, + includeBase = true +) { + const pathSegments = filePath + ?.replace(BLOG_PATH, "") + .split("/") + .filter(path => path !== "") // remove empty string in the segments ["", "other-path"] <- empty string will be removed + .filter(path => !path.startsWith("_")) // exclude directories start with underscore "_" + .slice(0, -1) // remove the last segment_ file name_ since it's unnecessary + .map(segment => slugifyStr(segment)); // slugify each segment path + + const basePath = includeBase ? "/posts" : ""; + + // Making sure `id` does not contain the directory + const blogId = id.split("/"); + const slug = blogId.length > 0 ? blogId.slice(-1) : blogId; + + // If not inside the sub-dir, simply return the file path + if (!pathSegments || pathSegments.length < 1) { + return [basePath, slug].join("/"); + } + + return [basePath, ...pathSegments, slug].join("/"); +}