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\nContinue 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());
-});