From 264fd923796f0a7d1f82fe518e4bafb7c3b358bf Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Fri, 28 Mar 2025 09:22:04 -0400 Subject: [PATCH] refactor note processing functions --- app/notes/[slug]/opengraph-image.tsx | 4 +- app/notes/[slug]/page.tsx | 4 +- app/notes/page.tsx | 4 +- app/sitemap.ts | 4 +- lib/helpers/build-feed.ts | 16 ++-- lib/helpers/metadata.ts | 5 +- lib/helpers/posts.ts | 128 +++++++++++++++------------ 7 files changed, 96 insertions(+), 69 deletions(-) diff --git a/app/notes/[slug]/opengraph-image.tsx b/app/notes/[slug]/opengraph-image.tsx index 22e2d3d5..690335fc 100644 --- a/app/notes/[slug]/opengraph-image.tsx +++ b/app/notes/[slug]/opengraph-image.tsx @@ -3,7 +3,7 @@ import { notFound } from "next/navigation"; import { join } from "path"; import { existsSync } from "fs"; import { readFile } from "fs/promises"; -import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts"; +import { getSlugs, getFrontMatter } from "../../../lib/helpers/posts"; import { POSTS_DIR, AVATAR_PATH } from "../../../lib/config/constants"; export const contentType = "image/png"; @@ -18,7 +18,7 @@ export const dynamic = "force-static"; export const dynamicParams = false; export const generateStaticParams = async () => { - const slugs = await getPostSlugs(); + const slugs = await getSlugs(); // map slugs into a static paths object required by next.js return slugs.map((slug) => ({ diff --git a/app/notes/[slug]/page.tsx b/app/notes/[slug]/page.tsx index 2dcfdbdb..44cfa8d5 100644 --- a/app/notes/[slug]/page.tsx +++ b/app/notes/[slug]/page.tsx @@ -7,7 +7,7 @@ import Time from "../../../components/Time"; import Comments from "../../../components/Comments"; import Loading from "../../../components/Loading"; import HitCounter from "./counter"; -import { getPostSlugs, getFrontMatter } from "../../../lib/helpers/posts"; +import { getSlugs, getFrontMatter } from "../../../lib/helpers/posts"; import { addMetadata } from "../../../lib/helpers/metadata"; import * as config from "../../../lib/config"; import { BASE_URL } from "../../../lib/config/constants"; @@ -24,7 +24,7 @@ export const dynamicParams = false; export const experimental_ppr = true; export const generateStaticParams = async () => { - const slugs = await getPostSlugs(); + const slugs = await getSlugs(); // map slugs into a static paths object required by next.js return slugs.map((slug) => ({ diff --git a/app/notes/page.tsx b/app/notes/page.tsx index 5aefcdbb..06014fab 100644 --- a/app/notes/page.tsx +++ b/app/notes/page.tsx @@ -1,6 +1,6 @@ import Link from "../../components/Link"; import Time from "../../components/Time"; -import { getAllPosts } from "../../lib/helpers/posts"; +import { getFrontMatter } from "../../lib/helpers/posts"; import { addMetadata } from "../../lib/helpers/metadata"; import * as config from "../../lib/config"; import type { ReactElement } from "react"; @@ -19,7 +19,7 @@ export const metadata = addMetadata({ const Page = async () => { // parse the year of each note and group them together - const notes = await getAllPosts(); + const notes = await getFrontMatter(); const notesByYear: { [year: string]: FrontMatter[]; } = {}; diff --git a/app/sitemap.ts b/app/sitemap.ts index 55c145f8..a7a60174 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,6 +1,6 @@ import path from "path"; import glob from "fast-glob"; -import { getAllPosts } from "../lib/helpers/posts"; +import { getFrontMatter } from "../lib/helpers/posts"; import { BASE_URL } from "../lib/config/constants"; import type { MetadataRoute } from "next"; @@ -36,7 +36,7 @@ const sitemap = async (): Promise => { }); }); - (await getAllPosts()).forEach((post) => { + (await getFrontMatter()).forEach((post) => { routes.push({ url: post.permalink, // pull lastModified from front matter date diff --git a/lib/helpers/build-feed.ts b/lib/helpers/build-feed.ts index 1ff99c14..9c09be7c 100644 --- a/lib/helpers/build-feed.ts +++ b/lib/helpers/build-feed.ts @@ -1,13 +1,16 @@ import { cache } from "react"; import { Feed } from "feed"; -import { getAllPosts, getPostContent } from "./posts"; +import { getFrontMatter, getContent } from "./posts"; import * as config from "../config"; import { BASE_URL } from "../config/constants"; import ogImage from "../../app/opengraph-image.jpg"; +/** + * Returns a `Feed` object, which can then be processed with `feed.rss2()`, `feed.atom1()`, or `feed.json1()`. + * @see https://github.com/jpmonette/feed#example + */ export const buildFeed = cache(async (): Promise => { - // https://github.com/jpmonette/feed#example const feed = new Feed({ id: BASE_URL, link: BASE_URL, @@ -28,8 +31,8 @@ export const buildFeed = cache(async (): Promise => { }); // add posts separately using their frontmatter - const posts = await getAllPosts(); - for (const post of posts.reverse()) { + const posts = await getFrontMatter(); + for (const post of posts) { feed.addItem({ guid: post.permalink, link: post.permalink, @@ -42,7 +45,10 @@ export const buildFeed = cache(async (): Promise => { }, ], date: new Date(post.date), - content: `${await getPostContent(post.slug)}\n\n

Continue reading...

`, + content: ` + ${await getContent(post.slug)} +

Continue reading...

+ `.trim(), }); } diff --git a/lib/helpers/metadata.ts b/lib/helpers/metadata.ts index e619eaa8..de7b85b4 100644 --- a/lib/helpers/metadata.ts +++ b/lib/helpers/metadata.ts @@ -1,7 +1,10 @@ import defaultMetadata from "../config/metadata"; import type { Metadata } from "next"; -// helper function to deep merge a page's metadata into the default site metadata +/** + * Helper function to deep merge a page's metadata into the default site metadata + * @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata + */ export const addMetadata = (metadata: Metadata): Metadata => { return { ...defaultMetadata, diff --git a/lib/helpers/posts.ts b/lib/helpers/posts.ts index 4f90d384..ec83c655 100644 --- a/lib/helpers/posts.ts +++ b/lib/helpers/posts.ts @@ -19,46 +19,8 @@ export type FrontMatter = { noComments?: boolean; }; -// returns front matter and the **raw & uncompiled** markdown of a given slug -export const getFrontMatter = cache(async (slug: string): Promise => { - try { - const { frontmatter } = await import(`../../${POSTS_DIR}/${slug}/index.mdx`); - - // process markdown title to html... - const htmlTitle = await unified() - .use(remarkParse) - .use(remarkSmartypants) - .use(remarkHtml, { - sanitize: { - // allow *very* limited markdown to be used in post titles - tagNames: ["code", "em", "strong"], - }, - }) - .process(frontmatter.title) - .then((result) => result.toString().trim()); - - // ...and then (sketchily) remove said html for a plaintext version: - // https://css-tricks.com/snippets/javascript/strip-html-tags-in-javascript/ - const title = decode(htmlTitle.replace(/<[^>]*>/g, "")); - - return { - ...(frontmatter as Partial), - // plain title without html or markdown syntax: - title, - // stylized title with limited html tags: - htmlTitle, - slug, - date: new Date(frontmatter.date).toISOString(), // validate/normalize the date string provided from front matter - permalink: `${BASE_URL}/${POSTS_DIR}/${slug}`, - }; - } catch (error) { - console.error(`Failed to load front matter for post with slug "${slug}":`, error); - return undefined; - } -}); - -// use filesystem to get a simple list of all post slugs -export const getPostSlugs = cache(async (): Promise => { +/** Use filesystem to get a simple list of all post slugs */ +export const getSlugs = cache(async (): Promise => { // list all .mdx files in POSTS_DIR const mdxFiles = await glob("*/index.mdx", { cwd: path.join(process.cwd(), POSTS_DIR), @@ -71,10 +33,76 @@ export const getPostSlugs = cache(async (): Promise => { 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 => { +// overloaded to return either the front matter of a single post or ALL posts +export const getFrontMatter: { + /** + * Parses and returns the front matter of ALL posts, sorted reverse chronologically + */ + (): Promise; + /** + * Parses and returns the front matter of a given slug, or undefined if the slug does not exist + */ + (slug: string): Promise; +} = cache( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (slug?: any): Promise => { + if (typeof slug === "string") { + try { + const { frontmatter } = await import(`../../${POSTS_DIR}/${slug}/index.mdx`); + + // process markdown title to html... + const htmlTitle = await unified() + .use(remarkParse) + .use(remarkSmartypants) + .use(remarkHtml, { + sanitize: { + // allow *very* limited markdown to be used in post titles + tagNames: ["code", "em", "strong"], + }, + }) + .process(frontmatter.title) + .then((result) => result.toString().trim()); + + // ...and then (sketchily) remove said html for a plaintext version: + // https://css-tricks.com/snippets/javascript/strip-html-tags-in-javascript/ + const title = decode(htmlTitle.replace(/<[^>]*>/g, "")); + + return { + ...(frontmatter as Partial), + // plain title without html or markdown syntax: + title, + // stylized title with limited html tags: + htmlTitle, + slug, + // validate/normalize the date string provided from front matter + date: new Date(frontmatter.date).toISOString(), + permalink: `${BASE_URL}/${POSTS_DIR}/${slug}`, + } as FrontMatter; + } catch (error) { + console.error(`Failed to load front matter for post with slug "${slug}":`, error); + return undefined; + } + } + + if (!slug) { + // concurrently fetch the front matter of each post + const slugs = await getSlugs(); + const posts = await Promise.all(slugs.map(getFrontMatter)); + + // sort the results reverse chronologically and return + return posts.sort( + (post1, post2) => new Date(post2!.date).getTime() - new Date(post1!.date).getTime() + ) as FrontMatter[]; + } + + throw new Error(`getFrontMatter() called with invalid argument.`); + } +); + +/** Returns the content of a post with very limited processing to include in RSS feeds */ +export const getContent = cache(async (slug: string): Promise => { try { + // TODO: also remove MDX-related syntax (e.g. import/export statements) const content = await unified() .use(remarkParse) .use(remarkFrontmatter) @@ -104,20 +132,10 @@ export const getPostContent = cache(async (slug: string): Promise

", "").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 => { - // concurrently fetch the front matter of each post - const slugs = await getPostSlugs(); - const posts = (await Promise.all(slugs.map(getFrontMatter))) as FrontMatter[]; - - // sort the results reverse chronologically and return - return posts.sort((post1, post2) => new Date(post1.date).getTime() - new Date(post2.date).getTime()); -});