Skip to content

Commit 1ec6b7c

Browse files
committed
docs: add an RSS feed to the blog at /blog/rss.xml
Closes #6522. The route handler renders the non-draft posts newest-first, the blog layout advertises the feed for autodiscovery, and the blog index links it next to the post count.
1 parent b801439 commit 1ec6b7c

3 files changed

Lines changed: 59 additions & 2 deletions

File tree

‎packages/docs/app/blog/layout.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { baseOptions } from "@/app/layout.config";
22
import { GitHubStarPill } from "@/components/github-star-pill";
33
import { HomeLayout } from "fumadocs-ui/layouts/home";
4+
import type { Metadata } from "next";
45
import type { ReactNode } from "react";
56

7+
// feed autodiscovery for the blog index and every post
8+
export const metadata: Metadata = {
9+
alternates: {
10+
types: { "application/rss+xml": [{ url: "https://zod.dev/blog/rss.xml", title: "Zod Blog" }] },
11+
},
12+
};
13+
614
// the star pill takes the navbar's top-right slot; `on: "nav"` keeps it in the bar itself on mobile rather than in the collapsed menu
715
export default function BlogLayout({ children }: { children: ReactNode }) {
816
return (

‎packages/docs/app/blog/page.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { blog, formatDate } from "@/loaders/source";
21
import { InlineCodeTitle } from "@/components/inline-code-title";
2+
import { blog, formatDate } from "@/loaders/source";
33
import Link from "next/link";
44

55
export const revalidate = false;
@@ -14,7 +14,12 @@ export default function BlogIndexPage() {
1414
<div className="mx-auto max-w-4xl">
1515
<h1 className="flex items-baseline justify-between text-[11px] font-medium uppercase tracking-[0.18em] text-fd-muted-foreground">
1616
Blog
17-
<span className="tabular-nums">{posts.length} posts</span>
17+
<span className="flex items-baseline gap-4">
18+
<a href="/blog/rss.xml" className="hover:text-[var(--ui-color)] transition-colors">
19+
RSS
20+
</a>
21+
<span className="tabular-nums">{posts.length} posts</span>
22+
</span>
1823
</h1>
1924
<ul>
2025
{posts.map((post) => {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { stripBackticks } from "@/components/inline-code-title";
2+
import { blog } from "@/loaders/source";
3+
4+
// cached forever
5+
export const revalidate = false;
6+
7+
const escapeXml = (text: string) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8+
9+
export function GET() {
10+
const posts = (blog.getPages() as any[])
11+
.filter((post) => !post.data?.draft)
12+
.sort((a, b) => new Date(b.data?.date ?? 0).getTime() - new Date(a.data?.date ?? 0).getTime());
13+
14+
const items = posts.map((post) => {
15+
const url = `https://zod.dev${post.url}`;
16+
const title = stripBackticks((post.data?.title as string | undefined) ?? post.slugs.join("/"));
17+
const description = (post.data?.description as string | undefined) ?? "";
18+
const pubDate = new Date(post.data?.date ?? 0).toUTCString();
19+
return ` <item>
20+
<title>${escapeXml(title)}</title>
21+
<link>${url}</link>
22+
<guid>${url}</guid>
23+
<pubDate>${pubDate}</pubDate>
24+
<description>${escapeXml(description)}</description>
25+
</item>`;
26+
});
27+
28+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
29+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
30+
<channel>
31+
<title>Zod Blog</title>
32+
<link>https://zod.dev/blog</link>
33+
<description>Release announcements and engineering deep dives from Zod.</description>
34+
<language>en-us</language>
35+
<atom:link href="https://zod.dev/blog/rss.xml" rel="self" type="application/rss+xml"/>
36+
${items.join("\n")}
37+
</channel>
38+
</rss>
39+
`;
40+
41+
return new Response(xml, {
42+
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
43+
});
44+
}

0 commit comments

Comments
 (0)