Commit 16eb5ab

HPCesia <me@hpcesia.com>
2026-09-18 15:21:20
feed: add per-locale feeds
1 parent b3c86a9
Changed files (5)
src
components
layouts
pages
utils
src/components/Header.astro
@@ -2,7 +2,7 @@
 import { getIntlayer, type Locale } from "intlayer";
 
 import { SITE_CONFIG } from "@/config.ts";
-import { getRelativeLocaleUrl, url } from "@/utils/url.ts";
+import { getRelativeLocaleUrl } from "@/utils/url.ts";
 
 import LocaleSwitcher from "@/components/widgets/LocaleSwitcher.astro";
 
@@ -21,7 +21,7 @@ const globalI18n = getIntlayer("global", locale);
     <ul>
       <li><a href={getRelativeLocaleUrl("/posts", locale)}>{globalI18n.posts}</a></li>
       <li><a href={getRelativeLocaleUrl("/about", locale)}>{globalI18n.about}</a></li>
-      <li><a href={url("/feed.atom")}>{globalI18n.feed}</a></li>
+      <li><a href={getRelativeLocaleUrl("/feed.atom", locale).slice(0, -1)}>{globalI18n.feed}</a></li>
     </ul>
     <LocaleSwitcher availableLocales={availableLocales} />
   </nav>
src/layouts/BaseLayout.astro
@@ -3,7 +3,12 @@ import { Font } from "astro:assets";
 import { getIntlayer, type Locale } from "intlayer";
 
 import { DEFAULT_LOCALE, SITE_CONFIG } from "@/config";
-import { getPathWithoutLocale, getRelativeLocaleUrl } from "@/utils/url.ts";
+import {
+  getPathWithoutLocale,
+  getRelativeLocaleUrl,
+  joinUrl,
+  url,
+} from "@/utils/url.ts";
 
 import Footer from "@/components/Footer.astro";
 import Header from "@/components/Header.astro";
@@ -55,6 +60,13 @@ const getAbsoluteLocaleUrl = (
     <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
     <link rel="icon" href="/favicon.ico" />
     <link rel="sitemap" href="/sitemap-index.xml" />
+    <link
+      rel="alternate"
+      type="application/atom+xml"
+      hreflang={locale ?? DEFAULT_LOCALE}
+      title="Atom Feed"
+      href={url(joinUrl(locale ?? DEFAULT_LOCALE, "feed.atom"))}
+    />
     <link
       rel="alternate"
       type="application/atom+xml"
src/pages/[locale]/feed.atom.ts
@@ -0,0 +1,37 @@
+import type { APIRoute, GetStaticPaths } from "astro";
+import { getCollection } from "astro:content";
+import type { Locale } from "intlayer";
+
+import { SITE_CONFIG } from "@/config.ts";
+import { createFeed, localePostUrl } from "@/utils/feed.ts";
+import { getRelativeLocaleUrl, joinUrl, url } from "@/utils/url.ts";
+
+export const getStaticPaths = (() => {
+  return SITE_CONFIG.locales.map((locale) => ({ params: { locale } }));
+}) satisfies GetStaticPaths;
+
+export const GET: APIRoute = async ({ site, params }) => {
+  if (!site) {
+    throw new Error("`site` must be configured in astro.config to build feeds");
+  }
+  const origin = site.origin;
+  const locale = params.locale as Locale;
+  const posts = (await getCollection("posts")).filter((post) =>
+    post.id.startsWith(`${locale}/`)
+  );
+  const feed = await createFeed({
+    id: new URL(getRelativeLocaleUrl("/", locale), origin).toString(),
+    feedUrl: new URL(url(joinUrl(locale, "feed.atom")), origin).toString(),
+    favicon: new URL(url("/favicon.ico"), origin).toString(),
+    title: SITE_CONFIG.title,
+    description: SITE_CONFIG.description[locale],
+    language: locale,
+    site: origin,
+    posts,
+    postUrl: (post) => localePostUrl(post, origin),
+  });
+
+  return new Response(feed.atom1(), {
+    headers: { "Content-Type": "application/atom+xml" },
+  });
+};
src/pages/feed.atom.ts
@@ -1,8 +1,8 @@
 import type { APIRoute } from "astro";
 import { getCollection } from "astro:content";
 
-import { SITE_CONFIG } from "@/config.ts";
-import { createFeed } from "@/utils/feed.ts";
+import { DEFAULT_LOCALE, SITE_CONFIG } from "@/config.ts";
+import { createFeed, localePostUrl } from "@/utils/feed.ts";
 import { url } from "@/utils/url.ts";
 
 export const GET: APIRoute = async ({ site }) => {
@@ -15,11 +15,11 @@ export const GET: APIRoute = async ({ site }) => {
     feedUrl: new URL(url("/feed.atom"), origin).toString(),
     favicon: new URL(url("/favicon.ico"), origin).toString(),
     title: SITE_CONFIG.title,
-    description: SITE_CONFIG.description,
-    language: SITE_CONFIG.locale,
+    description: SITE_CONFIG.description[DEFAULT_LOCALE],
+    language: DEFAULT_LOCALE,
     site: origin,
     posts: await getCollection("posts"),
-    postUrl: (post) => new URL(url(`/posts/${post.id}`), origin).toString(),
+    postUrl: (post) => localePostUrl(post, origin),
   });
 
   return new Response(feed.atom1(), {
src/utils/feed.ts
@@ -2,6 +2,9 @@ import type { CollectionEntry } from "astro:content";
 import { Feed } from "feed";
 
 import { SITE_CONFIG } from "@/config.ts";
+import { getPostLocaleSlug } from "@/utils/content.ts";
+import { getRelativeLocaleUrl } from "@/utils/url.ts";
+
 import { rehypeAbsoluteLinks } from "@/utils/plugins/absolute-links.ts";
 import { rehypeCleanEndnotes } from "@/utils/plugins/clean-endnotes.ts";
 import { rehypeCleanProperties } from "@/utils/plugins/clean-properties.ts";
@@ -85,3 +88,12 @@ export async function createFeed(data: FeedData): Promise<Feed> {
 
   return feed;
 }
+
+/** Absolute URL of a post's page in the locale it was written for. */
+export function localePostUrl(
+  post: CollectionEntry<"posts">,
+  origin: string,
+): string {
+  const { locale, slug } = getPostLocaleSlug(post);
+  return new URL(getRelativeLocaleUrl(slug, locale), origin).toString();
+}