v1.6: RSS-Feed für den Speiseplan + Badge für offene Rezeptvorschläge
app/feed.xml: RSS-2.0-Feed mit einem Eintrag pro Tag (aktuelle + nächste Woche), Gerichte je Essenszeit im CDATA-Beschreibungstext. Immer auf Deutsch, da Feedreader kein Sprach-Cookie mitschicken. Autodiscovery-Link im <head> (app/layout.tsx) sowie sichtbares RSS-Icon im Header der öffentlichen Speisekarte (neben Sprachumschalter/ Anmelden – die einzige Stelle, die auf allen Bildschirmgrößen sichtbar ist). AppHeader: "Vorschläge"-Navigationspunkt zeigt jetzt die Anzahl offener Rezeptvorschläge als Zahl im roten Kreis (GET /api/dish-suggestions, bereits staff/admin-beschränkt), aktualisiert sich bei Navigation innerhalb des internen Bereichs.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { query } from "@/lib/db";
|
||||
import { loadMenuEntries } from "@/lib/db/menu-entries";
|
||||
import { addDays, entriesFor, formatPrice, mondayOf, todayISO } from "@/lib/menu";
|
||||
import { localized, SITE_TITLE } from "@/lib/types";
|
||||
import type { Section } from "@/lib/types";
|
||||
|
||||
// RSS-Feed mit dem Speiseplan – ein Eintrag pro Tag mit Gerichten,
|
||||
// damit man ihn in einem Feedreader abonnieren kann und täglich sieht,
|
||||
// was es gibt. Immer auf Deutsch: Feedreader senden kein Sprach-Cookie,
|
||||
// eine feste Sprache ist hier verständlicher als ein zufälliger Locale-Default.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function escapeXml(text: string): string {
|
||||
return text.replace(/[&<>"']/g, (c) => {
|
||||
switch (c) {
|
||||
case "&":
|
||||
return "&";
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case '"':
|
||||
return """;
|
||||
default:
|
||||
return "'";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatFeedDate(iso: string): string {
|
||||
const d = new Date(`${iso}T00:00:00Z`);
|
||||
return d.toLocaleDateString("de-DE", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const appUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
||||
const monday = mondayOf(todayISO());
|
||||
// Aktuelle + nächste Woche, wie auch die öffentliche Speisekarte per
|
||||
// Wochennavigation anzeigt.
|
||||
const rangeEnd = addDays(monday, 11);
|
||||
|
||||
const [sections, entries] = await Promise.all([
|
||||
query<Section>("select * from sections order by sort"),
|
||||
loadMenuEntries(monday, rangeEnd),
|
||||
]);
|
||||
|
||||
const days = Array.from(new Set(entries.map((e) => e.menu_date))).sort();
|
||||
|
||||
const items = days
|
||||
.map((iso) => {
|
||||
const dayContent = sections
|
||||
.map((s) => {
|
||||
const dishes = entriesFor(entries, iso, s.key);
|
||||
if (dishes.length === 0) return "";
|
||||
const list = dishes
|
||||
.map(
|
||||
(e) =>
|
||||
`<li>${escapeXml(localized(e.dish, "de"))} – ${formatPrice(e.dish.price)}</li>`
|
||||
)
|
||||
.join("");
|
||||
return `<p><strong>${escapeXml(localized(s, "de"))} (${escapeXml(
|
||||
s.time_range
|
||||
)})</strong></p><ul>${list}</ul>`;
|
||||
})
|
||||
.join("");
|
||||
if (!dayContent) return "";
|
||||
|
||||
const [y, m, d] = iso.split("-").map(Number);
|
||||
const pubDate = new Date(Date.UTC(y, m - 1, d, 6, 0, 0)).toUTCString();
|
||||
|
||||
return ` <item>
|
||||
<title>${escapeXml(formatFeedDate(iso))}</title>
|
||||
<link>${appUrl}/?woche=${mondayOf(iso)}</link>
|
||||
<guid isPermaLink="false">${appUrl}/feed.xml#${iso}</guid>
|
||||
<pubDate>${pubDate}</pubDate>
|
||||
<description><![CDATA[${dayContent}]]></description>
|
||||
</item>`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>${escapeXml(SITE_TITLE)} – Speiseplan</title>
|
||||
<link>${appUrl}</link>
|
||||
<description>Täglicher Speiseplan der Kantine</description>
|
||||
<language>de</language>
|
||||
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
|
||||
${items}
|
||||
</channel>
|
||||
</rss>
|
||||
`;
|
||||
|
||||
return new Response(xml, {
|
||||
headers: {
|
||||
"Content-Type": "application/rss+xml; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=1800",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user