From e09bc4de2aca9f525da1fd3f3ba1c07d59fbd188 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Tue, 14 Apr 2026 15:43:34 +0200 Subject: [PATCH] feat(posts): search, upcoming events, RSS feed - Client-side search on /posts/ (full archive, ~2-char threshold) - Upcoming-events block at top of /posts/ from isEvent+eventDate posts - RSS feed at /posts/rss.xml via @astrojs/rss - Layout head: rss autodiscovery link - RSS subscribe link below the listing - Style tweaks on QuoteBlock and PostOverviewBlock --- package.json | 1 + src/components/BlogOverview.svelte | 118 ++++++++++++++++++ .../blocks/PostOverviewBlock.svelte | 4 +- src/components/blocks/QuoteBlock.svelte | 6 +- src/layouts/Layout.astro | 1 + src/pages/posts/index.astro | 27 ++++ src/pages/posts/rss.xml.ts | 33 +++++ yarn.lock | 35 ++++++ 8 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 src/pages/posts/rss.xml.ts diff --git a/package.json b/package.json index b20c06f..a97f774 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "firebase-deploy:nocache": "yarn build && firebase deploy --only hosting" }, "dependencies": { + "@astrojs/rss": "^4.0.18", "@astrojs/sitemap": "^3.7.2", "@astrojs/svelte": "^8.0.4", "@fontsource-variable/lora": "^5.2.8", diff --git a/src/components/BlogOverview.svelte b/src/components/BlogOverview.svelte index ad1611c..3da3cf3 100644 --- a/src/components/BlogOverview.svelte +++ b/src/components/BlogOverview.svelte @@ -14,6 +14,20 @@ color?: string; } + type SearchIndexEntry = { + slug: string; + headline: string; + excerpt: string; + subheadline: string; + tags: string[]; + image: string | null; + created: string | null; + isEvent: boolean; + eventDate: string | null; + }; + + type EventPost = PostEntry & { eventDate?: string; isEvent?: boolean }; + let { posts = [], tags = [], @@ -23,6 +37,8 @@ totalPosts = 0, basePath = "/posts", translations = {}, + upcomingEvents = [], + searchIndex = null, }: { posts: PostEntry[]; tags: TagEntry[]; @@ -32,8 +48,37 @@ totalPosts?: number; basePath?: string; translations?: Translations | null; + upcomingEvents?: EventPost[]; + searchIndex?: SearchIndexEntry[] | null; } = $props(); + let query = $state(""); + const normalizedQuery = $derived(query.trim().toLowerCase()); + const isSearching = $derived(normalizedQuery.length >= 2); + + const searchResults = $derived.by(() => { + if (!isSearching || !searchIndex) return []; + const q = normalizedQuery; + return searchIndex + .filter((p) => { + const hay = `${p.headline} ${p.excerpt} ${p.subheadline} ${p.tags.join(" ")}`.toLowerCase(); + return hay.includes(q); + }) + .slice(0, 40); + }); + + function searchResultHref(slug: string): string { + const norm = (slug ?? "").replace(/^\//, "").trim(); + return norm ? `/post/${encodeURIComponent(norm)}` : "/post"; + } + + function formatEventDate(iso: string | null | undefined): string { + if (!iso) return ""; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return ""; + return d.toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric" }); + } + /** Immer Übersetzungen von der Astro-Seite nutzen (SSR + Insel), damit Keys wie blog_tag_all ankommen. */ function t(key: string, replacements?: Record) { return tStatic(translations ?? null, key, replacements); @@ -48,6 +93,74 @@
+ {#if searchIndex} +
+ +
+ {/if} + + {#if !isSearching && upcomingEvents.length > 0} +
+

Nächste Termine

+ +
+ {/if} + + {#if isSearching} +
+ {searchResults.length} Treffer für „{query}" +
+ {#if searchResults.length > 0} + + {:else} +
+ Keine Treffer. Versuche einen anderen Suchbegriff. +
+ {/if} + {:else} {#if tags.length > 0}
+ + + {/if} diff --git a/src/components/blocks/PostOverviewBlock.svelte b/src/components/blocks/PostOverviewBlock.svelte index 25fcdc1..af34005 100644 --- a/src/components/blocks/PostOverviewBlock.svelte +++ b/src/components/blocks/PostOverviewBlock.svelte @@ -59,8 +59,8 @@ {/if} {#if posts.length > 0} -
- +
+ {t(T.post_overview_all)} diff --git a/src/components/blocks/QuoteBlock.svelte b/src/components/blocks/QuoteBlock.svelte index 1ac2ff6..479c133 100644 --- a/src/components/blocks/QuoteBlock.svelte +++ b/src/components/blocks/QuoteBlock.svelte @@ -9,10 +9,10 @@
-
-

{block.quote ?? ""}

+
+

{block.quote ?? ""}

{#if block.author} - {block.author} + » {block.author} {/if}
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index fe2eca4..6d8ba3a 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -460,6 +460,7 @@ const cmsFromCache = getCmsFromCache(); + p.isEvent && p.eventDate && new Date(p.eventDate).getTime() >= nowMs) + .sort((a, b) => new Date(a.eventDate ?? 0).getTime() - new Date(b.eventDate ?? 0).getTime()) + .slice(0, 4); + +// Lightweight index for client-side search over the full archive. +const searchIndex = posts.map((p) => ({ + slug: p.slug ?? p._slug ?? "", + headline: p.headline ?? "", + excerpt: p.excerpt ?? "", + subheadline: p.subheadline ?? "", + tags: (p.postTag ?? []).map((t) => (typeof t === "string" ? t : (t as { name?: string; _slug?: string }).name ?? (t as { _slug?: string })._slug ?? "")).filter(Boolean), + image: (p as PostWithEvent)._resolvedImageUrl ?? null, + created: p.created ?? null, + isEvent: Boolean((p as PostWithEvent).isEvent), + eventDate: (p as PostWithEvent).eventDate ?? null, +})); + const translations = Astro.locals.translations ?? {}; --- @@ -79,6 +103,9 @@ const translations = Astro.locals.translations ?? {}; totalPosts={filtered.length} basePath="/posts" translations={translations} + upcomingEvents={upcomingEvents} + searchIndex={searchIndex} + client:load /> )}
diff --git a/src/pages/posts/rss.xml.ts b/src/pages/posts/rss.xml.ts new file mode 100644 index 0000000..23207cb --- /dev/null +++ b/src/pages/posts/rss.xml.ts @@ -0,0 +1,33 @@ +import rss from "@astrojs/rss"; +import type { APIContext } from "astro"; +import { getPosts } from "../../lib/cms"; +import { sortPostsByDate, filterHiddenPosts, postHref } from "../../lib/blog-utils"; + +export const prerender = true; + +export async function GET(context: APIContext) { + const site = context.site ?? new URL("https://windwiderstand.de"); + let posts: Awaited> = []; + try { + posts = filterHiddenPosts(sortPostsByDate(await getPosts())); + } catch { + posts = []; + } + + return rss({ + title: "Bürgerinitiative Vachdorf – Beiträge", + description: "Aktuelles zur Windkraft, Gesundheit und Region.", + site, + items: posts.map((p) => { + const createdRaw = (p as { _created?: string; created?: string }).created ?? (p as { _created?: string })._created; + const pubDate = createdRaw ? new Date(createdRaw) : undefined; + return { + title: p.headline ?? p.slug ?? "", + description: p.excerpt ?? p.subheadline ?? "", + link: postHref(p), + pubDate: pubDate && !Number.isNaN(pubDate.getTime()) ? pubDate : undefined, + }; + }), + customData: `de-de`, + }); +} diff --git a/yarn.lock b/yarn.lock index 22eba69..86c2041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,6 +66,15 @@ dependencies: prismjs "^1.30.0" +"@astrojs/rss@^4.0.18": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@astrojs/rss/-/rss-4.0.18.tgz#9ce80003be5668545418d7e6ad53fe00e7a94d16" + integrity sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg== + dependencies: + fast-xml-parser "^5.5.7" + piccolore "^0.1.3" + zod "^4.3.6" + "@astrojs/sitemap@^3.7.2": version "3.7.2" resolved "https://registry.yarnpkg.com/@astrojs/sitemap/-/sitemap-3.7.2.tgz#6647a3f42f75435970abf72d456e1e9d8360dcdc" @@ -2484,6 +2493,22 @@ fast-uri@^3.0.1: resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== +fast-xml-builder@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz#0c407a1d9d5996336c0cd76f7ff785cac6413017" + integrity sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg== + dependencies: + path-expression-matcher "^1.1.3" + +fast-xml-parser@^5.5.7: + version "5.5.12" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.5.12.tgz#6e50e5a5bbb03c1dc72a9268a9bfe677b1b623b2" + integrity sha512-nUR0q8PPfoA/svPM43Gup7vLOZWppaNrYgGmrVqrAVJa7cOH4hMG6FX9M4mQ8dZA1/ObGZHzES7Ed88hxEBSJg== + dependencies: + fast-xml-builder "^1.1.4" + path-expression-matcher "^1.5.0" + strnum "^2.2.3" + faye-websocket@0.11.4: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" @@ -3657,6 +3682,11 @@ parse5@^7.0.0, parse5@^7.3.0: dependencies: entities "^6.0.0" +path-expression-matcher@^1.1.3, path-expression-matcher@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz#3b98545dc88ffebb593e2d8458d0929da9275f4a" + integrity sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ== + pathe@^2.0.1, pathe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" @@ -4177,6 +4207,11 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +strnum@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.2.3.tgz#0119fce02749a11bb126a4d686ac5dbdf6e57586" + integrity sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg== + supports-color@^10.2.2: version "10.2.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-10.2.2.tgz#466c2978cc5cd0052d542a0b576461c2b802ebb4"