]> juplo.de Git - website/commitdiff
Step 9/11: RSS-Feeds, SEO-Meta-Tags, shared helpers in lib/posts.ts
authorKai Moritz <kai.milan.moritz@googlemail.com>
Sat, 6 Jun 2026 13:13:16 +0000 (13:13 +0000)
committerKai Moritz <kai.milan.moritz@googlemail.com>
Sat, 6 Jun 2026 13:13:16 +0000 (13:13 +0000)
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, <meta name="description">, 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 <noreply@anthropic.com>
17 files changed:
package-lock.json
package.json
src/components/BlogNav.astro
src/layouts/BaseLayout.astro
src/lib/posts.ts [new file with mode: 0644]
src/pages/[slug].astro
src/pages/blog/archive/[year].astro
src/pages/blog/archive/[year]/index.xml.ts [new file with mode: 0644]
src/pages/blog/archive/index.astro
src/pages/blog/archive/index.xml.ts [new file with mode: 0644]
src/pages/blog/index.astro
src/pages/blog/index.xml.ts [new file with mode: 0644]
src/pages/categories/[term].astro
src/pages/categories/[term]/index.xml.ts [new file with mode: 0644]
src/pages/index.astro
src/pages/tags/[term].astro
src/pages/tags/[term]/index.xml.ts [new file with mode: 0644]

index 300edc70f69ca6e6a13614d253c3dd6376c53d74..74795f1fe7a9d133dbb4cd9687e4072bb10c58a3 100644 (file)
@@ -8,6 +8,7 @@
       "name": "juplo",
       "version": "0.0.1",
       "dependencies": {
+        "@astrojs/rss": "^4.0.18",
         "astro": "^6.4.4"
       },
       "devDependencies": {
         "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",
       "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",
         "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",
         "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",
         "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",
         "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",
index 2f12e87f05c68d432c41b8dba27545e21a6f8bd0..6dd8a2e55b79109f9be337baf47ce0948ce65fc2 100644 (file)
@@ -15,6 +15,7 @@
     "astro": "astro"
   },
   "dependencies": {
+    "@astrojs/rss": "^4.0.18",
     "astro": "^6.4.4"
   },
   "devDependencies": {
index bc3c32ee0aab9592828c22c8e57fdc17509691a8..a4df2f6e8496d8de93e9c92cae3706a049cc723e 100644 (file)
@@ -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<string, CollectionEntry<'blog'>[]> = {};
@@ -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}/` ||
index b818166dbf2008b4f01ad1a97d1ae97d5792ec9a..aab296757ed02a301aada205cc2aacf2a17a8db4 100644 (file)
@@ -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
   <script>try{document.documentElement.dataset.layout=localStorage.getItem('layout')||'classic'}catch(e){}</script>
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>{fullTitle}</title>
+  {description && <meta name="description" content={description} />}
+  <meta property="og:title" content={fullTitle} />
+  {description && <meta property="og:description" content={description} />}
+  <meta property="og:type" content="website" />
+  <meta property="og:url" content={resolvedCanonical} />
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
   <link rel="stylesheet" media="only screen" type="text/css" href="/css/screen.css">
   <link rel="stylesheet" media="only screen" type="text/css" href="/css/resurrection.css">
diff --git a/src/lib/posts.ts b/src/lib/posts.ts
new file mode 100644 (file)
index 0000000..45634dd
--- /dev/null
@@ -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;
+}
index 143e2ec744434e190bf429171d241c0fbee159ed..147466cdfdff420c2d2c2a38894866088a82388f 100644 (file)
@@ -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<string, number>();
 const tagCounts = new Map<string, number>();
 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' });
 ---
 
-<BaseLayout title={title} canonical={canonical}>
+<BaseLayout title={title} description={description} canonical={canonical}>
   <Fragment slot="breadcrumb">
     <Breadcrumb crumbs={crumbs} />
   </Fragment>
index 79c610d7fd0a7818f51c3e4b9bcedafe12a0f354..270f9823edd8be9f75210e4536ffe3c239ee05b9 100644 (file)
@@ -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 (file)
index 0000000..704d130
--- /dev/null
@@ -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: [
+      '<language>en-US</language>',
+      '<managingEditor>kai@juplo.de (Kai Moritz)</managingEditor>',
+      '<webMaster>kai@juplo.de (Kai Moritz)</webMaster>',
+      '<copyright>Kai Moritz</copyright>',
+    ].join(''),
+  });
+}
index d4df6fc47c1b9d2bdb55976e6be33f9d1150bfe5..ac9dffd53af56b7f9a139d87cb4675513f6907c9 100644 (file)
@@ -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 (file)
index 0000000..7e6f1c3
--- /dev/null
@@ -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: [
+      '<language>en-US</language>',
+      '<managingEditor>kai@juplo.de (Kai Moritz)</managingEditor>',
+      '<webMaster>kai@juplo.de (Kai Moritz)</webMaster>',
+      '<copyright>Kai Moritz</copyright>',
+    ].join(''),
+  });
+}
index aefbc5750e25f9db0298e045b5539ba30d65e3da..cb2d7ae6e7b488bd5c088fe2eb73591c0e23e9e2 100644 (file)
@@ -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 (file)
index 0000000..d58574a
--- /dev/null
@@ -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: [
+      '<language>en-US</language>',
+      '<managingEditor>kai@juplo.de (Kai Moritz)</managingEditor>',
+      '<webMaster>kai@juplo.de (Kai Moritz)</webMaster>',
+      '<copyright>Kai Moritz</copyright>',
+    ].join(''),
+  });
+}
index daf169306ac224605834bf4b4673e3f993ef5f2b..9505e9aef5de284d19ba5c8ad687395ea607d19a 100644 (file)
@@ -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<string, typeof allPosts>();
   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 (file)
index 0000000..1b0576b
--- /dev/null
@@ -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<string>();
+  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: [
+      '<language>en-US</language>',
+      '<managingEditor>kai@juplo.de (Kai Moritz)</managingEditor>',
+      '<webMaster>kai@juplo.de (Kai Moritz)</webMaster>',
+      '<copyright>Kai Moritz</copyright>',
+    ].join(''),
+  });
+}
index a982425eababc93056140f92bc338882d0d93a2d..8d49bc90f6f7c90d72fbcd620b00f5dc5ab1d98f 100644 (file)
@@ -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);
 ---
 
 <BaseLayout title="juplo">
index 0ee71e504a027ff8bb2f0c8a54a716d7cdb7a2e9..e29b3bf0e7bb9951f5a27aae360d190a6ddfbf32 100644 (file)
@@ -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<string, typeof allPosts>();
   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 (file)
index 0000000..3f6eb5b
--- /dev/null
@@ -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<string>();
+  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: [
+      '<language>en-US</language>',
+      '<managingEditor>kai@juplo.de (Kai Moritz)</managingEditor>',
+      '<webMaster>kai@juplo.de (Kai Moritz)</webMaster>',
+      '<copyright>Kai Moritz</copyright>',
+    ].join(''),
+  });
+}