From 6b0c6bd6945042311533045a1e451c70a142ea5e Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Sat, 6 Jun 2026 13:13:16 +0000 Subject: [PATCH] Step 9/11: RSS-Feeds, SEO-Meta-Tags, shared helpers in lib/posts.ts MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit RSS-Feeds (Step 9): - /blog/index.xml — Blog-Gesamt-Feed - /blog/archive/index.xml — Archiv-Feed - /blog/archive/YYYY/index.xml — Jahres-Feeds (dynamisch) - /categories/TERM/index.xml — Kategorie-Feeds (dynamisch) - /tags/TERM/index.xml — Tag-Feeds (dynamisch) SEO-Meta-Tags (Step 9): - BaseLayout: description-Prop, , og:title, og:description, og:type, og:url - [slug].astro: übergibt getSummary(post.body) als description Shared Helpers (Step 11 Refactoring): - src/lib/posts.ts: getAllPosts(), postUrl(), getSummary(), wordCount() - Alle Seiten importieren diese Helpers statt lokale Kopien zu haben - BlogNav, [slug], blog/index, archive/index, [year], categories/[term], tags/[term], index.astro migriert Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 104 +++++++++++++++++++++ package.json | 1 + src/components/BlogNav.astro | 10 +- src/layouts/BaseLayout.astro | 8 +- src/lib/posts.ts | 28 ++++++ src/pages/[slug].astro | 14 +-- src/pages/blog/archive/[year].astro | 18 +--- src/pages/blog/archive/[year]/index.xml.ts | 33 +++++++ src/pages/blog/archive/index.astro | 13 +-- src/pages/blog/archive/index.xml.ts | 25 +++++ src/pages/blog/index.astro | 27 +----- src/pages/blog/index.xml.ts | 25 +++++ src/pages/categories/[term].astro | 16 +--- src/pages/categories/[term]/index.xml.ts | 38 ++++++++ src/pages/index.astro | 10 +- src/pages/tags/[term].astro | 16 +--- src/pages/tags/[term]/index.xml.ts | 38 ++++++++ 17 files changed, 325 insertions(+), 99 deletions(-) create mode 100644 src/lib/posts.ts create mode 100644 src/pages/blog/archive/[year]/index.xml.ts create mode 100644 src/pages/blog/archive/index.xml.ts create mode 100644 src/pages/blog/index.xml.ts create mode 100644 src/pages/categories/[term]/index.xml.ts create mode 100644 src/pages/tags/[term]/index.xml.ts diff --git a/package-lock.json b/package-lock.json index 300edc70..74795f1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "juplo", "version": "0.0.1", "dependencies": { + "@astrojs/rss": "^4.0.18", "astro": "^6.4.4" }, "devDependencies": { @@ -76,6 +77,17 @@ "node": ">=22.12.0" } }, + "node_modules/@astrojs/rss": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.18.tgz", + "integrity": "sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.5.7", + "piccolore": "^0.1.3", + "zod": "^4.3.6" + } + }, "node_modules/@astrojs/telemetry": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", @@ -1124,6 +1136,18 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", @@ -2630,6 +2654,44 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4134,6 +4196,21 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", @@ -4661,6 +4738,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/svgo": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", @@ -5174,6 +5263,21 @@ "node": ">=4" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/package.json b/package.json index 2f12e87f..6dd8a2e5 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "astro": "astro" }, "dependencies": { + "@astrojs/rss": "^4.0.18", "astro": "^6.4.4" }, "devDependencies": { diff --git a/src/components/BlogNav.astro b/src/components/BlogNav.astro index bc3c32ee..a4df2f6e 100644 --- a/src/components/BlogNav.astro +++ b/src/components/BlogNav.astro @@ -1,6 +1,6 @@ --- -import { getCollection } from 'astro:content'; import type { CollectionEntry } from 'astro:content'; +import { getAllPosts, postUrl } from '../lib/posts'; interface Props { currentUrl: string; @@ -8,8 +8,7 @@ interface Props { const { currentUrl } = Astro.props; -const allPosts = (await getCollection('blog', ({ data }) => !data.draft)) - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); +const allPosts = await getAllPosts(); // Group posts by year, years sorted descending const postsByYear: Record[]> = {}; @@ -82,11 +81,6 @@ const activeSection = mainSections.find(s => { return currentUrl.startsWith(s.url); })?.name ?? ''; -// Helper: get post url -function postUrl(post: CollectionEntry<'blog'>) { - return post.data.url ?? `/${post.id}/`; -} - // Determine active year from currentUrl function isActiveYear(year: string) { return currentUrl === `/blog/archive/${year}/` || diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index b818166d..aab29675 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,11 +1,12 @@ --- interface Props { title: string; + description?: string; canonical?: string; lang?: string; } -const { title, canonical, lang = 'en-US' } = Astro.props; +const { title, description, canonical, lang = 'en-US' } = Astro.props; const siteTitle = 'juplo'; const fullTitle = title === siteTitle ? siteTitle : `${title} | ${siteTitle}`; const resolvedCanonical = canonical ?? new URL(Astro.url.pathname, Astro.site).toString(); @@ -17,6 +18,11 @@ const resolvedCanonical = canonical ?? new URL(Astro.url.pathname, Astro.site).t {fullTitle} + {description && } + + {description && } + + diff --git a/src/lib/posts.ts b/src/lib/posts.ts new file mode 100644 index 00000000..45634ddf --- /dev/null +++ b/src/lib/posts.ts @@ -0,0 +1,28 @@ +import { getCollection } from 'astro:content'; + +export async function getAllPosts() { + return (await getCollection('blog', ({ data }) => !data.draft)) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); +} + +export function postUrl(post: { id: string; data: { url?: string } }): string { + return post.data.url ?? `/${post.id}/`; +} + +export function getSummary(body: string): string { + let text = body + .replace(/```[\s\S]*?```/g, '') + .replace(/`[^`]*`/g, '') + .replace(/<[^>]+>/g, '') + .replace(/\{\{[^}]*\}\}/g, '') + .replace(/!\[[^\]]*\]\([^)]*\)/g, '') + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/^#{1,6}\s+/gm, '') + .replace(/[*_]{1,3}([^*_\n]+)[*_]{1,3}/g, '$1'); + const paras = text.split(/\n{2,}/).map(p => p.trim().replace(/\n/g, ' ')).filter(p => p.length > 20); + return paras[0]?.substring(0, 350) ?? ''; +} + +export function wordCount(body: string): number { + return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; +} diff --git a/src/pages/[slug].astro b/src/pages/[slug].astro index 143e2ec7..147466cd 100644 --- a/src/pages/[slug].astro +++ b/src/pages/[slug].astro @@ -1,14 +1,15 @@ --- -import { getCollection, render } from 'astro:content'; +import { render } from 'astro:content'; import BaseLayout from '../layouts/BaseLayout.astro'; import Breadcrumb from '../components/Breadcrumb.astro'; import BlogNav from '../components/BlogNav.astro'; import TaxonomyAside from '../components/TaxonomyAside.astro'; +import { getAllPosts, postUrl as getPostUrl, getSummary } from '../lib/posts'; export async function getStaticPaths() { - const posts = await getCollection('blog', ({ data }) => !data.draft); - return posts.map(post => { - const slug = (post.data.url ?? `/${post.id}/`).replace(/^\/|\/$/g, ''); + const allPosts = await getAllPosts(); + return allPosts.map(post => { + const slug = getPostUrl(post).replace(/^\/|\/$/g, ''); return { params: { slug }, props: { post } }; }); } @@ -18,6 +19,7 @@ const { Content } = await render(post); const { title, date, url, categories = [], tags = [] } = post.data; const postUrl = url ?? `/${post.id}/`; const year = date.getFullYear().toString(); +const description = getSummary(post.body ?? ''); const canonical = new URL(postUrl, Astro.site).toString(); @@ -29,7 +31,7 @@ const crumbs = [ { title }, ]; -const allPosts = await getCollection('blog', ({ data }) => !data.draft); +const allPosts = await getAllPosts(); const categoryCounts = new Map(); const tagCounts = new Map(); for (const p of allPosts) { @@ -55,7 +57,7 @@ const dateIso = date.toISOString().replace(/\.\d{3}Z$/, '+00:00'); const dateDisplay = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); --- - + diff --git a/src/pages/blog/archive/[year].astro b/src/pages/blog/archive/[year].astro index 79c610d7..270f9823 100644 --- a/src/pages/blog/archive/[year].astro +++ b/src/pages/blog/archive/[year].astro @@ -1,18 +1,16 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../../../layouts/BaseLayout.astro'; import Breadcrumb from '../../../components/Breadcrumb.astro'; import BlogNav from '../../../components/BlogNav.astro'; +import { getAllPosts, postUrl, wordCount } from '../../../lib/posts'; export async function getStaticPaths() { - const posts = await getCollection('blog', ({ data }) => !data.draft); - const years = [...new Set(posts.map(p => p.data.date.getFullYear().toString()))]; + const allPosts = await getAllPosts(); + const years = [...new Set(allPosts.map(p => p.data.date.getFullYear().toString()))]; return years.map(year => ({ params: { year }, props: { - posts: posts - .filter(p => p.data.date.getFullYear().toString() === year) - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()), + posts: allPosts.filter(p => p.data.date.getFullYear().toString() === year), }, })); } @@ -22,14 +20,6 @@ const { posts } = Astro.props; const currentUrl = `/blog/archive/${year}/`; -function postUrl(post: any): string { - return post.data.url ?? `/${post.id}/`; -} - -function wordCount(body: string): number { - return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; -} - const crumbs = [ { title: 'Home', url: '/' }, { title: 'Blog', url: '/blog/' }, diff --git a/src/pages/blog/archive/[year]/index.xml.ts b/src/pages/blog/archive/[year]/index.xml.ts new file mode 100644 index 00000000..704d1300 --- /dev/null +++ b/src/pages/blog/archive/[year]/index.xml.ts @@ -0,0 +1,33 @@ +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import { getAllPosts, postUrl, getSummary } from '../../../../lib/posts'; + +export async function getStaticPaths() { + const allPosts = await getAllPosts(); + const years = [...new Set(allPosts.map(p => p.data.date.getFullYear().toString()))]; + return years.map(year => ({ params: { year } })); +} + +export async function GET(context: APIContext) { + const year = context.params.year as string; + const allPosts = await getAllPosts(); + const posts = allPosts.filter(p => p.data.date.getFullYear().toString() === year); + return rss({ + title: `Archive ${year} on juplo`, + description: `Posts from ${year} on juplo`, + site: context.site!, + items: posts.map(post => ({ + title: post.data.title, + link: postUrl(post), + pubDate: post.data.date, + description: getSummary(post.body ?? ''), + author: 'kai@juplo.de (Kai Moritz)', + })), + customData: [ + 'en-US', + 'kai@juplo.de (Kai Moritz)', + 'kai@juplo.de (Kai Moritz)', + 'Kai Moritz', + ].join(''), + }); +} diff --git a/src/pages/blog/archive/index.astro b/src/pages/blog/archive/index.astro index d4df6fc4..ac9dffd5 100644 --- a/src/pages/blog/archive/index.astro +++ b/src/pages/blog/archive/index.astro @@ -1,21 +1,12 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../../../layouts/BaseLayout.astro'; import Breadcrumb from '../../../components/Breadcrumb.astro'; import BlogNav from '../../../components/BlogNav.astro'; +import { getAllPosts, postUrl, wordCount } from '../../../lib/posts'; const currentUrl = '/blog/archive/'; -const posts = (await getCollection('blog', ({ data }) => !data.draft)) - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); - -function postUrl(post: any): string { - return post.data.url ?? `/${post.id}/`; -} - -function wordCount(body: string): number { - return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; -} +const posts = await getAllPosts(); const crumbs = [ { title: 'Home', url: '/' }, diff --git a/src/pages/blog/archive/index.xml.ts b/src/pages/blog/archive/index.xml.ts new file mode 100644 index 00000000..7e6f1c3e --- /dev/null +++ b/src/pages/blog/archive/index.xml.ts @@ -0,0 +1,25 @@ +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import { getAllPosts, postUrl, getSummary } from '../../../lib/posts'; + +export async function GET(context: APIContext) { + const posts = await getAllPosts(); + return rss({ + title: 'Archive on juplo', + description: 'All posts in the archive on juplo', + site: context.site!, + items: posts.map(post => ({ + title: post.data.title, + link: postUrl(post), + pubDate: post.data.date, + description: getSummary(post.body ?? ''), + author: 'kai@juplo.de (Kai Moritz)', + })), + customData: [ + 'en-US', + 'kai@juplo.de (Kai Moritz)', + 'kai@juplo.de (Kai Moritz)', + 'Kai Moritz', + ].join(''), + }); +} diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index aefbc575..cb2d7ae6 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -1,13 +1,12 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../../layouts/BaseLayout.astro'; import Breadcrumb from '../../components/Breadcrumb.astro'; import BlogNav from '../../components/BlogNav.astro'; +import { getAllPosts, postUrl, getSummary, wordCount } from '../../lib/posts'; const currentUrl = '/blog/'; -const allPosts = (await getCollection('blog', ({ data }) => !data.draft)) - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); +const allPosts = await getAllPosts(); const recentPosts = allPosts.slice(0, 3); @@ -32,28 +31,6 @@ const tags = [...tagMap.entries()] .map(([n, c]) => ({ name: n, displayName: capitalize(n), count: c })) .sort((a, b) => a.displayName.localeCompare(b.displayName)); -function postUrl(post: any): string { - return post.data.url ?? `/${post.id}/`; -} - -function getSummary(body: string): string { - let text = body - .replace(/```[\s\S]*?```/g, '') - .replace(/`[^`]*`/g, '') - .replace(/<[^>]+>/g, '') - .replace(/\{\{[^}]*\}\}/g, '') - .replace(/!\[[^\]]*\]\([^)]*\)/g, '') - .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') - .replace(/^#{1,6}\s+/gm, '') - .replace(/[*_]{1,3}([^*_\n]+)[*_]{1,3}/g, '$1'); - const paras = text.split(/\n{2,}/).map(p => p.trim().replace(/\n/g, ' ')).filter(p => p.length > 20); - return paras[0]?.substring(0, 350) ?? ''; -} - -function wordCount(body: string): number { - return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; -} - const crumbs = [ { title: 'Home', url: '/' }, { title: 'Blog' }, diff --git a/src/pages/blog/index.xml.ts b/src/pages/blog/index.xml.ts new file mode 100644 index 00000000..d58574af --- /dev/null +++ b/src/pages/blog/index.xml.ts @@ -0,0 +1,25 @@ +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import { getAllPosts, postUrl, getSummary } from '../../lib/posts'; + +export async function GET(context: APIContext) { + const posts = await getAllPosts(); + return rss({ + title: 'Blog on juplo', + description: 'Recent content in Blog on juplo', + site: context.site!, + items: posts.map(post => ({ + title: post.data.title, + link: postUrl(post), + pubDate: post.data.date, + description: getSummary(post.body ?? ''), + author: 'kai@juplo.de (Kai Moritz)', + })), + customData: [ + 'en-US', + 'kai@juplo.de (Kai Moritz)', + 'kai@juplo.de (Kai Moritz)', + 'Kai Moritz', + ].join(''), + }); +} diff --git a/src/pages/categories/[term].astro b/src/pages/categories/[term].astro index daf16930..9505e9ae 100644 --- a/src/pages/categories/[term].astro +++ b/src/pages/categories/[term].astro @@ -1,11 +1,11 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../../layouts/BaseLayout.astro'; import Breadcrumb from '../../components/Breadcrumb.astro'; import BlogNav from '../../components/BlogNav.astro'; +import { getAllPosts, postUrl, wordCount } from '../../lib/posts'; export async function getStaticPaths() { - const allPosts = await getCollection('blog', ({ data }) => !data.draft); + const allPosts = await getAllPosts(); const termMap = new Map(); for (const post of allPosts) { for (const cat of post.data.categories ?? []) { @@ -15,9 +15,7 @@ export async function getStaticPaths() { } return [...termMap.entries()].map(([term, posts]) => ({ params: { term }, - props: { - posts: posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()), - }, + props: { posts }, })); } @@ -28,14 +26,6 @@ const currentUrl = `/categories/${term}/`; const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); const termDisplay = capitalize(term); -function postUrl(post: any): string { - return post.data.url ?? `/${post.id}/`; -} - -function wordCount(body: string): number { - return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; -} - const crumbs = [ { title: 'Home', url: '/' }, { title: 'Blog', url: '/blog/' }, diff --git a/src/pages/categories/[term]/index.xml.ts b/src/pages/categories/[term]/index.xml.ts new file mode 100644 index 00000000..1b0576b1 --- /dev/null +++ b/src/pages/categories/[term]/index.xml.ts @@ -0,0 +1,38 @@ +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import { getAllPosts, postUrl, getSummary } from '../../../lib/posts'; + +export async function getStaticPaths() { + const allPosts = await getAllPosts(); + const termSet = new Set(); + for (const post of allPosts) { + for (const cat of post.data.categories ?? []) termSet.add(cat); + } + return [...termSet].map(term => ({ params: { term } })); +} + +export async function GET(context: APIContext) { + const term = context.params.term as string; + const allPosts = await getAllPosts(); + const posts = allPosts.filter(p => (p.data.categories ?? []).includes(term)); + const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); + const termDisplay = capitalize(term); + return rss({ + title: `${termDisplay} on juplo`, + description: `Recent content in ${termDisplay} on juplo`, + site: context.site!, + items: posts.map(post => ({ + title: post.data.title, + link: postUrl(post), + pubDate: post.data.date, + description: getSummary(post.body ?? ''), + author: 'kai@juplo.de (Kai Moritz)', + })), + customData: [ + 'en-US', + 'kai@juplo.de (Kai Moritz)', + 'kai@juplo.de (Kai Moritz)', + 'Kai Moritz', + ].join(''), + }); +} diff --git a/src/pages/index.astro b/src/pages/index.astro index a982425e..8d49bc90 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,17 +1,11 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../layouts/BaseLayout.astro'; import BlogNav from '../components/BlogNav.astro'; +import { getAllPosts, postUrl } from '../lib/posts'; const currentUrl = '/'; -const recentPosts = (await getCollection('blog', ({ data }) => !data.draft)) - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) - .slice(0, 5); - -function postUrl(post: any): string { - return post.data.url ?? `/${post.id}/`; -} +const recentPosts = (await getAllPosts()).slice(0, 5); --- diff --git a/src/pages/tags/[term].astro b/src/pages/tags/[term].astro index 0ee71e50..e29b3bf0 100644 --- a/src/pages/tags/[term].astro +++ b/src/pages/tags/[term].astro @@ -1,11 +1,11 @@ --- -import { getCollection } from 'astro:content'; import BaseLayout from '../../layouts/BaseLayout.astro'; import Breadcrumb from '../../components/Breadcrumb.astro'; import BlogNav from '../../components/BlogNav.astro'; +import { getAllPosts, postUrl, wordCount } from '../../lib/posts'; export async function getStaticPaths() { - const allPosts = await getCollection('blog', ({ data }) => !data.draft); + const allPosts = await getAllPosts(); const termMap = new Map(); for (const post of allPosts) { for (const tag of post.data.tags ?? []) { @@ -15,9 +15,7 @@ export async function getStaticPaths() { } return [...termMap.entries()].map(([term, posts]) => ({ params: { term }, - props: { - posts: posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()), - }, + props: { posts }, })); } @@ -28,14 +26,6 @@ const currentUrl = `/tags/${term}/`; const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); const termDisplay = capitalize(term); -function postUrl(post: any): string { - return post.data.url ?? `/${post.id}/`; -} - -function wordCount(body: string): number { - return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; -} - const crumbs = [ { title: 'Home', url: '/' }, { title: 'Blog', url: '/blog/' }, diff --git a/src/pages/tags/[term]/index.xml.ts b/src/pages/tags/[term]/index.xml.ts new file mode 100644 index 00000000..3f6eb5b4 --- /dev/null +++ b/src/pages/tags/[term]/index.xml.ts @@ -0,0 +1,38 @@ +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import { getAllPosts, postUrl, getSummary } from '../../../lib/posts'; + +export async function getStaticPaths() { + const allPosts = await getAllPosts(); + const termSet = new Set(); + for (const post of allPosts) { + for (const tag of post.data.tags ?? []) termSet.add(tag); + } + return [...termSet].map(term => ({ params: { term } })); +} + +export async function GET(context: APIContext) { + const term = context.params.term as string; + const allPosts = await getAllPosts(); + const posts = allPosts.filter(p => (p.data.tags ?? []).includes(term)); + const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); + const termDisplay = capitalize(term); + return rss({ + title: `${termDisplay} on juplo`, + description: `Recent content in ${termDisplay} on juplo`, + site: context.site!, + items: posts.map(post => ({ + title: post.data.title, + link: postUrl(post), + pubDate: post.data.date, + description: getSummary(post.body ?? ''), + author: 'kai@juplo.de (Kai Moritz)', + })), + customData: [ + 'en-US', + 'kai@juplo.de (Kai Moritz)', + 'kai@juplo.de (Kai Moritz)', + 'Kai Moritz', + ].join(''), + }); +} -- 2.39.5