From e46207da3c68822fc109c737ec3038067c7ef77a Mon Sep 17 00:00:00 2001 From: Suradet Pratomsak Date: Sun, 2 Aug 2026 20:11:44 +0700 Subject: [PATCH 1/4] feat: add RSS feed, series, tag index, and category index - Install @astrojs/rss and create /rss.xml endpoint with full post data - Add RSS auto-discovery link in BaseLayout head - Add optional series field to blog content schema - Tag series frontmatter to Frontend Roadmap and Haskell posts - Create series utility for grouping and nav (prev/next) - Create /blog/series index page and /[series] dynamic page - Create /blog/tags index page with tag cloud and post counts - Create /blog/categories index page with category grid --- bun.lock | 19 +++ package.json | 1 + src/content.config.ts | 1 + .../index.mdx | 1 + .../index.mdx | 1 + src/content/blog/haskell-ep1/index.mdx | 1 + src/content/blog/haskell-ep2/index.mdx | 1 + src/layouts/BaseLayout.astro | 1 + src/pages/blog/categories/index.astro | 92 ++++++++++++ src/pages/blog/series/[series].astro | 113 +++++++++++++++ src/pages/blog/series/index.astro | 134 ++++++++++++++++++ src/pages/blog/tags/index.astro | 99 +++++++++++++ src/pages/rss.xml.ts | 27 ++++ src/utils/series.ts | 58 ++++++++ 14 files changed, 549 insertions(+) create mode 100644 src/pages/blog/categories/index.astro create mode 100644 src/pages/blog/series/[series].astro create mode 100644 src/pages/blog/series/index.astro create mode 100644 src/pages/blog/tags/index.astro create mode 100644 src/pages/rss.xml.ts create mode 100644 src/utils/series.ts diff --git a/bun.lock b/bun.lock index 7a1aacb..ce84fb4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@astrojs/check": "^0.9.9", "@astrojs/mdx": "^7.0.0", + "@astrojs/rss": "^4.0.19", "@astrojs/sitemap": "^3.7.2", "@astrojs/vercel": "^11.0.0", "@iconify-json/fa6-brands": "^1.2.6", @@ -83,6 +84,8 @@ "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], + "@astrojs/rss": ["@astrojs/rss@4.0.19", "", { "dependencies": { "fast-xml-parser": "^5.5.7", "piccolore": "^0.1.3", "zod": "^4.3.6" } }, "sha512-e+z5wYeYtffQdHQO8c2tkSd2JEBdAuRXJV4ZEU5IxkYeE6e39woDd7nw1PH1Kk2tEYNCYuKdylnnbhGmt61awA=="], + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.2", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA=="], "@astrojs/telemetry": ["@astrojs/telemetry@3.3.2", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" } }, "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ=="], @@ -359,6 +362,8 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + "@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -693,6 +698,8 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], + "are-docs-informative": ["are-docs-informative@0.0.2", "", {}, "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig=="], "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], @@ -1031,6 +1038,10 @@ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="], + + "fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], @@ -1179,6 +1190,8 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -1485,6 +1498,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], @@ -1673,6 +1688,8 @@ "strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + "strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -1841,6 +1858,8 @@ "xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], + "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], diff --git a/package.json b/package.json index d211c5d..5955658 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@astrojs/check": "^0.9.9", "@astrojs/mdx": "^7.0.0", + "@astrojs/rss": "^4.0.19", "@astrojs/sitemap": "^3.7.2", "@astrojs/vercel": "^11.0.0", "@iconify-json/fa6-brands": "^1.2.6", diff --git a/src/content.config.ts b/src/content.config.ts index f60e9a4..754f55e 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -11,6 +11,7 @@ const blogCollection = defineCollection({ category: z.string(), tags: z.array(z.string()), featured: z.boolean().optional(), + series: z.string().optional(), }), }); diff --git a/src/content/blog/frontend-developer-roadmap-part-1/index.mdx b/src/content/blog/frontend-developer-roadmap-part-1/index.mdx index 7fb7583..1ac4eb7 100644 --- a/src/content/blog/frontend-developer-roadmap-part-1/index.mdx +++ b/src/content/blog/frontend-developer-roadmap-part-1/index.mdx @@ -6,6 +6,7 @@ heroImage: './frontend-roadmap-part-1-cover.png' category: 'Frontend Frameworks' tags: ['React.js', 'JavaScript', 'Web Development'] featured: false +series: 'Frontend Developer Roadmap' --- # Frontend Developer Roadmap (Part 1): React.js diff --git a/src/content/blog/frontend-developer-roadmap-part-2/index.mdx b/src/content/blog/frontend-developer-roadmap-part-2/index.mdx index 8730a69..926f760 100644 --- a/src/content/blog/frontend-developer-roadmap-part-2/index.mdx +++ b/src/content/blog/frontend-developer-roadmap-part-2/index.mdx @@ -6,6 +6,7 @@ heroImage: './frontend-roadmap-part-2-cover.png' category: 'Frontend Frameworks' tags: ['Vue.js', 'JavaScript', 'Web Development'] featured: false +series: 'Frontend Developer Roadmap' --- # Frontend Developer Roadmap (Part 2): Vue.js diff --git a/src/content/blog/haskell-ep1/index.mdx b/src/content/blog/haskell-ep1/index.mdx index fb1430d..0a58e37 100644 --- a/src/content/blog/haskell-ep1/index.mdx +++ b/src/content/blog/haskell-ep1/index.mdx @@ -6,6 +6,7 @@ heroImage: './haskell-ep1-cover.png' category: 'Programming Languages' tags: ['Haskell', 'Functional Programming', 'Beginner'] featured: true +series: 'Haskell' --- diff --git a/src/content/blog/haskell-ep2/index.mdx b/src/content/blog/haskell-ep2/index.mdx index 5ec3cde..ef76cee 100644 --- a/src/content/blog/haskell-ep2/index.mdx +++ b/src/content/blog/haskell-ep2/index.mdx @@ -6,6 +6,7 @@ heroImage: "./haskell-ep2-cover.png" category: "Programming Languages" tags: ["Haskell", "Functional Programming", "GHCi", "Beginner"] featured: false +series: "Haskell" --- ## Ready, set, go! diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index eeb6978..c332444 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -25,6 +25,7 @@ const currentYear = new Date().getFullYear(); {`${title} | rxdevman`} +