Skip to content

Commit 2d42a74

Browse files
committed
include post content in rss/atom feeds
1 parent a4aa15d commit 2d42a74

File tree

6 files changed

+70
-10
lines changed

6 files changed

+70
-10
lines changed

app/feed.atom/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { buildFeed } from "../../lib/helpers/build-feed";
33
export const dynamic = "force-static";
44

55
export const GET = async () => {
6-
return new Response((await buildFeed()).atom1(), {
6+
const feed = await buildFeed();
7+
8+
return new Response(feed.atom1(), {
79
headers: {
810
"content-type": "application/atom+xml; charset=utf-8",
911
},

app/feed.xml/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { buildFeed } from "../../lib/helpers/build-feed";
33
export const dynamic = "force-static";
44

55
export const GET = async () => {
6-
return new Response((await buildFeed()).rss2(), {
6+
const feed = await buildFeed();
7+
8+
return new Response(feed.rss2(), {
79
headers: {
810
"content-type": "application/rss+xml; charset=utf-8",
911
},

lib/helpers/build-feed.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { cache } from "react";
12
import { Feed } from "feed";
2-
import { getAllPosts } from "./posts";
3+
import { getAllPosts, getPostContent } from "./posts";
34
import * as config from "../config";
45
import { BASE_URL } from "../config/constants";
56

67
import ogImage from "../../app/opengraph-image.jpg";
78

8-
export const buildFeed = async (): Promise<Feed> => {
9+
export const buildFeed = cache(async (): Promise<Feed> => {
910
// https://github.com/jpmonette/feed#example
1011
const feed = new Feed({
1112
id: BASE_URL,
@@ -27,7 +28,8 @@ export const buildFeed = async (): Promise<Feed> => {
2728
});
2829

2930
// add posts separately using their frontmatter
30-
(await getAllPosts()).forEach((post) => {
31+
const posts = await getAllPosts();
32+
for (const post of posts.reverse()) {
3133
feed.addItem({
3234
guid: post.permalink,
3335
link: post.permalink,
@@ -40,8 +42,9 @@ export const buildFeed = async (): Promise<Feed> => {
4042
},
4143
],
4244
date: new Date(post.date),
45+
content: `${await getPostContent(post.slug)}\n\n<p><a href="${post.permalink}"><strong>Continue reading...</strong></a></p>`,
4346
});
44-
});
47+
}
4548

4649
return feed;
47-
};
50+
});

lib/helpers/posts.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { cache } from "react";
22
import path from "path";
33
import glob from "fast-glob";
44
import { unified } from "unified";
5-
import { remarkHtml, remarkParse, remarkSmartypants } from "./remark-rehype-plugins";
5+
import { read } from "to-vfile";
6+
import { remarkHtml, remarkParse, remarkSmartypants, remarkFrontmatter } from "./remark-rehype-plugins";
67
import { decode } from "html-entities";
78
import { BASE_URL, POSTS_DIR } from "../config/constants";
89

@@ -19,7 +20,7 @@ export type FrontMatter = {
1920
};
2021

2122
// returns front matter and the **raw & uncompiled** markdown of a given slug
22-
export const getFrontMatter = cache(async (slug: string): Promise<FrontMatter | null> => {
23+
export const getFrontMatter = cache(async (slug: string): Promise<FrontMatter | undefined> => {
2324
try {
2425
const { frontmatter } = await import(`../../${POSTS_DIR}/${slug}/index.mdx`);
2526

@@ -52,7 +53,7 @@ export const getFrontMatter = cache(async (slug: string): Promise<FrontMatter |
5253
};
5354
} catch (error) {
5455
console.error(`Failed to load front matter for post with slug "${slug}":`, error);
55-
return null;
56+
return undefined;
5657
}
5758
});
5859

@@ -70,6 +71,47 @@ export const getPostSlugs = cache(async (): Promise<string[]> => {
7071
return slugs;
7172
});
7273

74+
// returns the content of a post with very limited processing to include in RSS feeds
75+
// TODO: also remove MDX-related syntax (e.g. import/export statements)
76+
export const getPostContent = cache(async (slug: string): Promise<string | undefined> => {
77+
try {
78+
const content = await unified()
79+
.use(remarkParse)
80+
.use(remarkFrontmatter)
81+
.use(remarkSmartypants)
82+
.use(remarkHtml, {
83+
sanitize: {
84+
tagNames: [
85+
"p",
86+
"a",
87+
"em",
88+
"strong",
89+
"code",
90+
"pre",
91+
"blockquote",
92+
"h1",
93+
"h2",
94+
"h3",
95+
"h4",
96+
"h5",
97+
"h6",
98+
"ul",
99+
"ol",
100+
"li",
101+
"hr",
102+
],
103+
},
104+
})
105+
.process(await read(path.resolve(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`)));
106+
107+
// convert the parsed content to a string
108+
return content.toString().trim();
109+
} catch (error) {
110+
console.error(`Failed to load/parse content for post with slug "${slug}":`, error);
111+
return undefined;
112+
}
113+
});
114+
73115
// returns the parsed front matter of ALL posts, sorted reverse chronologically
74116
export const getAllPosts = cache(async (): Promise<FrontMatter[]> => {
75117
// concurrently fetch the front matter of each post

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"remark-smartypants": "^3.0.2",
6767
"resend": "^4.2.0",
6868
"shiki": "^3.2.1",
69+
"to-vfile": "^8.0.0",
6970
"unified": "^11.0.5",
7071
"unist-util-visit": "^5.0.0",
7172
"zod": "^3.24.2"

pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)