1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 03:05:24 -04:00

include post content in rss/atom feeds

This commit is contained in:
Jake Jarvis 2025-03-27 18:02:37 -04:00
parent a4aa15d2c5
commit 2d42a7447e
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
6 changed files with 70 additions and 10 deletions

View File

@ -3,7 +3,9 @@ import { buildFeed } from "../../lib/helpers/build-feed";
export const dynamic = "force-static";
export const GET = async () => {
return new Response((await buildFeed()).atom1(), {
const feed = await buildFeed();
return new Response(feed.atom1(), {
headers: {
"content-type": "application/atom+xml; charset=utf-8",
},

View File

@ -3,7 +3,9 @@ import { buildFeed } from "../../lib/helpers/build-feed";
export const dynamic = "force-static";
export const GET = async () => {
return new Response((await buildFeed()).rss2(), {
const feed = await buildFeed();
return new Response(feed.rss2(), {
headers: {
"content-type": "application/rss+xml; charset=utf-8",
},

View File

@ -1,11 +1,12 @@
import { cache } from "react";
import { Feed } from "feed";
import { getAllPosts } from "./posts";
import { getAllPosts, getPostContent } from "./posts";
import * as config from "../config";
import { BASE_URL } from "../config/constants";
import ogImage from "../../app/opengraph-image.jpg";
export const buildFeed = async (): Promise<Feed> => {
export const buildFeed = cache(async (): Promise<Feed> => {
// https://github.com/jpmonette/feed#example
const feed = new Feed({
id: BASE_URL,
@ -27,7 +28,8 @@ export const buildFeed = async (): Promise<Feed> => {
});
// add posts separately using their frontmatter
(await getAllPosts()).forEach((post) => {
const posts = await getAllPosts();
for (const post of posts.reverse()) {
feed.addItem({
guid: post.permalink,
link: post.permalink,
@ -40,8 +42,9 @@ export const buildFeed = async (): Promise<Feed> => {
},
],
date: new Date(post.date),
content: `${await getPostContent(post.slug)}\n\n<p><a href="${post.permalink}"><strong>Continue reading...</strong></a></p>`,
});
});
}
return feed;
};
});

View File

@ -2,7 +2,8 @@ import { cache } from "react";
import path from "path";
import glob from "fast-glob";
import { unified } from "unified";
import { remarkHtml, remarkParse, remarkSmartypants } from "./remark-rehype-plugins";
import { read } from "to-vfile";
import { remarkHtml, remarkParse, remarkSmartypants, remarkFrontmatter } from "./remark-rehype-plugins";
import { decode } from "html-entities";
import { BASE_URL, POSTS_DIR } from "../config/constants";
@ -19,7 +20,7 @@ export type FrontMatter = {
};
// returns front matter and the **raw & uncompiled** markdown of a given slug
export const getFrontMatter = cache(async (slug: string): Promise<FrontMatter | null> => {
export const getFrontMatter = cache(async (slug: string): Promise<FrontMatter | undefined> => {
try {
const { frontmatter } = await import(`../../${POSTS_DIR}/${slug}/index.mdx`);
@ -52,7 +53,7 @@ export const getFrontMatter = cache(async (slug: string): Promise<FrontMatter |
};
} catch (error) {
console.error(`Failed to load front matter for post with slug "${slug}":`, error);
return null;
return undefined;
}
});
@ -70,6 +71,47 @@ export const getPostSlugs = cache(async (): Promise<string[]> => {
return slugs;
});
// returns the content of a post with very limited processing to include in RSS feeds
// TODO: also remove MDX-related syntax (e.g. import/export statements)
export const getPostContent = cache(async (slug: string): Promise<string | undefined> => {
try {
const content = await unified()
.use(remarkParse)
.use(remarkFrontmatter)
.use(remarkSmartypants)
.use(remarkHtml, {
sanitize: {
tagNames: [
"p",
"a",
"em",
"strong",
"code",
"pre",
"blockquote",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"ul",
"ol",
"li",
"hr",
],
},
})
.process(await read(path.resolve(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`)));
// convert the parsed content to a string
return content.toString().trim();
} catch (error) {
console.error(`Failed to load/parse content for post with slug "${slug}":`, error);
return undefined;
}
});
// returns the parsed front matter of ALL posts, sorted reverse chronologically
export const getAllPosts = cache(async (): Promise<FrontMatter[]> => {
// concurrently fetch the front matter of each post

View File

@ -66,6 +66,7 @@
"remark-smartypants": "^3.0.2",
"resend": "^4.2.0",
"shiki": "^3.2.1",
"to-vfile": "^8.0.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"zod": "^3.24.2"

10
pnpm-lock.yaml generated
View File

@ -155,6 +155,9 @@ importers:
shiki:
specifier: ^3.2.1
version: 3.2.1
to-vfile:
specifier: ^8.0.0
version: 8.0.0
unified:
specifier: ^11.0.5
version: 11.0.5
@ -3431,6 +3434,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
to-vfile@8.0.0:
resolution: {integrity: sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg==}
toggle-selection@1.0.6:
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
@ -7705,6 +7711,10 @@ snapshots:
dependencies:
is-number: 7.0.0
to-vfile@8.0.0:
dependencies:
vfile: 6.0.3
toggle-selection@1.0.6: {}
toml@3.0.0: {}