diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index c252fdd5..93c0c683 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -36,6 +36,9 @@ export async function generateMetadata({ de: 'Open Source, aber richtig - Open Elements ist ein modernes Unternehmen mit einem Fokus auf Open Source und Java', }; + const feedTitle = + locale === 'de' ? 'Open Elements – Artikel' : 'Open Elements – Articles'; + return { title: titles[locale as keyof typeof titles] || titles.en, description: @@ -47,6 +50,11 @@ export async function generateMetadata({ 'open source Support', 'Java Support', ], + alternates: { + types: { + 'application/rss+xml': [{ url: '/feed.xml', title: feedTitle }], + }, + }, openGraph: { type: 'website', url: @@ -92,6 +100,7 @@ export default async function LocaleLayout({ suppressHydrationWarning> + /g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function toRfc822(dateInput: string | Date): string { + const date = new Date(dateInput); + if (Number.isNaN(date.getTime())) { + return new Date(0).toUTCString(); + } + return date.toUTCString(); +} + +/** + * Build an RSS 2.0 feed XML string for the posts of a given locale. + * The `feedPath` is the absolute path at which this feed is served (used for the + * atom self-link), e.g. "/feed.xml" or "/posts/index.xml". + */ +export function buildPostsRssFeed(feedPath: string, locale = 'en'): string { + const posts = getAllPosts(locale); + const localePath = locale === 'en' ? '' : `/${locale}`; + const channelLink = `${SITE_URL}${localePath}/posts`; + const feedUrl = `${SITE_URL}${feedPath}`; + const language = locale === 'de' ? 'de-de' : 'en-us'; + const lastBuildDate = posts.length + ? toRfc822(posts[0].frontmatter.date) + : new Date().toUTCString(); + + const items = posts + .map(post => { + const url = `${SITE_URL}${localePath}/posts/${post.slug}`; + const title = escapeXml(post.frontmatter.title ?? ''); + const description = escapeXml(post.frontmatter.excerpt ?? ''); + const pubDate = toRfc822(post.frontmatter.date); + const author = escapeXml(post.frontmatter.author ?? 'Open Elements'); + return [ + ' ', + ` ${title}`, + ` ${escapeXml(url)}`, + ` ${pubDate}`, + ` ${escapeXml(url)}`, + ` ${description}`, + ` ${author}`, + ' ', + ].join('\n'); + }) + .join('\n'); + + return [ + '', + '', + ' ', + ` ${escapeXml(SITE_TITLE)}`, + ` ${escapeXml(channelLink)}`, + ` ${escapeXml(SITE_DESCRIPTION)}`, + ` ${language}`, + ` ${lastBuildDate}`, + ` `, + items, + ' ', + '', + ].join('\n'); +} + +export function rssResponseHeaders(): HeadersInit { + return { + 'Content-Type': 'application/rss+xml; charset=utf-8', + 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', + }; +}