From 5e1cbdc2fcb9e6e61f3354247bd2c50ba6c49487 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Fri, 12 Jun 2026 11:21:08 +0300 Subject: [PATCH 1/4] add rss feed Signed-off-by: Jessie Ssebuliba --- src/app/feed.xml/route.ts | 12 +++++ src/app/posts/index.xml/route.ts | 12 +++++ src/app/rss.xml/route.ts | 12 +++++ src/lib/rss.ts | 80 ++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 src/app/feed.xml/route.ts create mode 100644 src/app/posts/index.xml/route.ts create mode 100644 src/app/rss.xml/route.ts create mode 100644 src/lib/rss.ts diff --git a/src/app/feed.xml/route.ts b/src/app/feed.xml/route.ts new file mode 100644 index 00000000..65d6cb57 --- /dev/null +++ b/src/app/feed.xml/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server'; +import { buildPostsRssFeed, rssResponseHeaders } from '@/lib/rss'; + +export const dynamic = 'force-static'; + +export function GET() { + const xml = buildPostsRssFeed('/feed.xml'); + return new NextResponse(xml, { + status: 200, + headers: rssResponseHeaders(), + }); +} diff --git a/src/app/posts/index.xml/route.ts b/src/app/posts/index.xml/route.ts new file mode 100644 index 00000000..a5b4be56 --- /dev/null +++ b/src/app/posts/index.xml/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server'; +import { buildPostsRssFeed, rssResponseHeaders } from '@/lib/rss'; + +export const dynamic = 'force-static'; + +export function GET() { + const xml = buildPostsRssFeed('/posts/index.xml'); + return new NextResponse(xml, { + status: 200, + headers: rssResponseHeaders(), + }); +} diff --git a/src/app/rss.xml/route.ts b/src/app/rss.xml/route.ts new file mode 100644 index 00000000..b521b9c1 --- /dev/null +++ b/src/app/rss.xml/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server'; +import { buildPostsRssFeed, rssResponseHeaders } from '@/lib/rss'; + +export const dynamic = 'force-static'; + +export function GET() { + const xml = buildPostsRssFeed('/rss.xml'); + return new NextResponse(xml, { + status: 200, + headers: rssResponseHeaders(), + }); +} diff --git a/src/lib/rss.ts b/src/lib/rss.ts new file mode 100644 index 00000000..16be3046 --- /dev/null +++ b/src/lib/rss.ts @@ -0,0 +1,80 @@ +import { getAllPosts } from './markdown'; + +const SITE_URL = 'https://open-elements.com'; +const SITE_TITLE = 'Open Elements'; +const SITE_DESCRIPTION = 'Open Source made right'; + +function escapeXml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//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', + }; +} From 3a092d558d857dd5ac59237b4a195d9168394a22 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Fri, 12 Jun 2026 11:52:09 +0300 Subject: [PATCH 2/4] add rss feed url in the page metadata Signed-off-by: Jessie Ssebuliba --- src/app/[locale]/layout.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index c252fdd5..6e1b49ae 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,15 @@ export async function generateMetadata({ 'open source Support', 'Java Support', ], + alternates: { + types: { + 'application/rss+xml': [ + { url: '/feed.xml', title: feedTitle }, + { url: '/rss.xml', title: feedTitle }, + { url: '/posts/index.xml', title: feedTitle }, + ], + }, + }, openGraph: { type: 'website', url: @@ -92,6 +104,7 @@ export default async function LocaleLayout({ suppressHydrationWarning> + Date: Fri, 12 Jun 2026 12:13:28 +0300 Subject: [PATCH 3/4] consolidate RSS feed routes to a single canonical feed Signed-off-by: Jessie Ssebuliba --- src/app/[locale]/layout.tsx | 6 +----- src/app/posts/index.xml/route.ts | 11 ++++------- src/app/rss.xml/route.ts | 11 ++++------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 6e1b49ae..93c0c683 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -52,11 +52,7 @@ export async function generateMetadata({ ], alternates: { types: { - 'application/rss+xml': [ - { url: '/feed.xml', title: feedTitle }, - { url: '/rss.xml', title: feedTitle }, - { url: '/posts/index.xml', title: feedTitle }, - ], + 'application/rss+xml': [{ url: '/feed.xml', title: feedTitle }], }, }, openGraph: { diff --git a/src/app/posts/index.xml/route.ts b/src/app/posts/index.xml/route.ts index a5b4be56..6d3be015 100644 --- a/src/app/posts/index.xml/route.ts +++ b/src/app/posts/index.xml/route.ts @@ -1,12 +1,9 @@ -import { NextResponse } from 'next/server'; -import { buildPostsRssFeed, rssResponseHeaders } from '@/lib/rss'; +import { permanentRedirect } from 'next/navigation'; export const dynamic = 'force-static'; +// Permanent redirect alias to the canonical feed at /feed.xml. +// Keeps existing subscribers working while consolidating to a single source of truth. export function GET() { - const xml = buildPostsRssFeed('/posts/index.xml'); - return new NextResponse(xml, { - status: 200, - headers: rssResponseHeaders(), - }); + permanentRedirect('/feed.xml'); } diff --git a/src/app/rss.xml/route.ts b/src/app/rss.xml/route.ts index b521b9c1..6d3be015 100644 --- a/src/app/rss.xml/route.ts +++ b/src/app/rss.xml/route.ts @@ -1,12 +1,9 @@ -import { NextResponse } from 'next/server'; -import { buildPostsRssFeed, rssResponseHeaders } from '@/lib/rss'; +import { permanentRedirect } from 'next/navigation'; export const dynamic = 'force-static'; +// Permanent redirect alias to the canonical feed at /feed.xml. +// Keeps existing subscribers working while consolidating to a single source of truth. export function GET() { - const xml = buildPostsRssFeed('/rss.xml'); - return new NextResponse(xml, { - status: 200, - headers: rssResponseHeaders(), - }); + permanentRedirect('/feed.xml'); } From 96e2589038322e501f9a4b2a37cfc89076bbe119 Mon Sep 17 00:00:00 2001 From: Jessie Ssebuliba Date: Fri, 12 Jun 2026 12:20:47 +0300 Subject: [PATCH 4/4] remove rss.xml for feed.xml Signed-off-by: Jessie Ssebuliba --- src/app/rss.xml/route.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/app/rss.xml/route.ts diff --git a/src/app/rss.xml/route.ts b/src/app/rss.xml/route.ts deleted file mode 100644 index 6d3be015..00000000 --- a/src/app/rss.xml/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { permanentRedirect } from 'next/navigation'; - -export const dynamic = 'force-static'; - -// Permanent redirect alias to the canonical feed at /feed.xml. -// Keeps existing subscribers working while consolidating to a single source of truth. -export function GET() { - permanentRedirect('/feed.xml'); -}