From 0e6cb6457e1724ce2b85bc0452d75412e43a2786 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Tue, 7 Jan 2025 23:05:53 -0600 Subject: [PATCH 1/6] refactor blog utils into general utils --- apps/website/app/(home)/blog/[slug]/page.tsx | 15 +++++-- apps/website/app/(home)/blog/readBlogs.tsx | 38 ++--------------- apps/website/app/data/constants.ts | 1 + apps/website/app/utils/getFileContent.ts | 22 ++++++++++ apps/website/app/utils/getHtmlFromMarkdown.ts | 33 +++++++++++++++ apps/website/app/utils/getMarkdownFile.ts | 41 +++++++++++++++++++ apps/website/package.json | 6 ++- package-lock.json | 34 +++++++++++++++ 8 files changed, 149 insertions(+), 41 deletions(-) create mode 100644 apps/website/app/data/constants.ts create mode 100644 apps/website/app/utils/getFileContent.ts create mode 100644 apps/website/app/utils/getHtmlFromMarkdown.ts create mode 100644 apps/website/app/utils/getMarkdownFile.ts diff --git a/apps/website/app/(home)/blog/[slug]/page.tsx b/apps/website/app/(home)/blog/[slug]/page.tsx index da837bf1d..6162e7c87 100644 --- a/apps/website/app/(home)/blog/[slug]/page.tsx +++ b/apps/website/app/(home)/blog/[slug]/page.tsx @@ -2,7 +2,8 @@ import fs from "fs/promises"; import path from "path"; import { notFound } from "next/navigation"; import { Metadata } from "next"; -import { getBlog } from "../readBlogs"; +import { getMarkdownPage } from "~/utils/getMarkdownFile"; +import { BLOG_PATH } from "~/data/constants"; type Params = { params: Promise<{ @@ -13,7 +14,10 @@ type Params = { export default async function BlogPost({ params }: Params) { try { const { slug } = await params; - const { data, contentHtml } = await getBlog(slug); + const { data, contentHtml } = await getMarkdownPage({ + slug, + directory: BLOG_PATH, + }); return (
@@ -41,7 +45,7 @@ export default async function BlogPost({ params }: Params) { export async function generateStaticParams() { try { - const blogPath = path.join(process.cwd(), "app/blog/posts"); + const blogPath = path.join(process.cwd(), BLOG_PATH); // 1) Check if the directory exists const directoryExists = await fs .stat(blogPath) @@ -74,7 +78,10 @@ export async function generateStaticParams() { export async function generateMetadata({ params }: Params): Promise { try { const { slug } = await params; - const { data } = await getBlog(slug); + const { data } = await getMarkdownPage({ + slug, + directory: BLOG_PATH, + }); return { title: data.title, diff --git a/apps/website/app/(home)/blog/readBlogs.tsx b/apps/website/app/(home)/blog/readBlogs.tsx index bfaeb3bae..0c3ea7b7b 100644 --- a/apps/website/app/(home)/blog/readBlogs.tsx +++ b/apps/website/app/(home)/blog/readBlogs.tsx @@ -1,12 +1,10 @@ -import { remark } from "remark"; -import html from "remark-html"; -import { notFound } from "next/navigation"; import path from "path"; import fs from "fs/promises"; import matter from "gray-matter"; -import { BlogSchema, type Blog, BlogFrontmatter } from "./schema"; +import { BlogSchema, type Blog } from "./schema"; +import { BLOG_PATH } from "~/data/constants"; -const BLOG_DIRECTORY = path.join(process.cwd(), "app/blog/posts"); +const BLOG_DIRECTORY = path.join(process.cwd(), BLOG_PATH); async function validateBlogDirectory(): Promise { try { @@ -35,11 +33,6 @@ async function processBlogFile(filename: string): Promise { } } -async function getMarkdownContent(content: string): Promise { - const processedContent = await remark().use(html).process(content); - return processedContent.toString(); -} - export async function getAllBlogs(): Promise { try { const directoryExists = await validateBlogDirectory(); @@ -57,31 +50,6 @@ export async function getAllBlogs(): Promise { } } -export async function getBlog( - slug: string, -): Promise<{ data: BlogFrontmatter; contentHtml: string }> { - try { - const filePath = path.join(BLOG_DIRECTORY, `${slug}.md`); - await fs.access(filePath); - - const fileContent = await fs.readFile(filePath, "utf-8"); - const { data: rawData, content } = matter(fileContent); - const data = BlogSchema.parse(rawData); - - if (!data.published) { - console.log(`Post ${slug} is not published`); - return notFound(); - } - - const contentHtml = await getMarkdownContent(content); - - return { data, contentHtml }; - } catch (error) { - console.error("Error loading blog post:", error); - return notFound(); - } -} - export async function getLatestBlogs(): Promise { const blogs = await getAllBlogs(); return blogs diff --git a/apps/website/app/data/constants.ts b/apps/website/app/data/constants.ts new file mode 100644 index 000000000..4267add82 --- /dev/null +++ b/apps/website/app/data/constants.ts @@ -0,0 +1 @@ +export const BLOG_PATH = "app/(home)/blog/posts"; diff --git a/apps/website/app/utils/getFileContent.ts b/apps/website/app/utils/getFileContent.ts new file mode 100644 index 000000000..d651a0587 --- /dev/null +++ b/apps/website/app/utils/getFileContent.ts @@ -0,0 +1,22 @@ +import path from "path"; +import fs from "fs/promises"; + +type Props = { + filename: string; + directory: string; +}; + +export const getFileContent = async ({ + filename, + directory, +}: Props): Promise => { + try { + const filePath = path.join(directory, filename); + const fileContent = await fs.readFile(filePath, "utf-8"); + return fileContent; + } catch (error) { + throw new Error( + `Failed to read file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } +}; diff --git a/apps/website/app/utils/getHtmlFromMarkdown.ts b/apps/website/app/utils/getHtmlFromMarkdown.ts new file mode 100644 index 000000000..a2e40964f --- /dev/null +++ b/apps/website/app/utils/getHtmlFromMarkdown.ts @@ -0,0 +1,33 @@ +import { unified } from "unified"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import rehypeStringify from "rehype-stringify"; +import { toString } from "mdast-util-to-string"; +import { visit } from "unist-util-visit"; + +function remarkHeadingId() { + return (tree: any) => { + visit(tree, "heading", (node) => { + const text = toString(node); + const id = text + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); + + node.data = { + hName: `h${node.depth}`, + hProperties: { id }, + }; + }); + }; +} + +export async function getHtmlFromMarkdown(markdown: string): Promise { + const htmlString = await unified() + .use(remarkParse) + .use(remarkHeadingId) + .use(remarkRehype) + .use(rehypeStringify) + .process(markdown); + return htmlString.toString(); +} diff --git a/apps/website/app/utils/getMarkdownFile.ts b/apps/website/app/utils/getMarkdownFile.ts new file mode 100644 index 000000000..b3bcd52e7 --- /dev/null +++ b/apps/website/app/utils/getMarkdownFile.ts @@ -0,0 +1,41 @@ +import { getHtmlFromMarkdown } from "~/utils/getHtmlFromMarkdown"; +import { getFileContent } from "~/utils/getFileContent"; +import { notFound } from "next/navigation"; +import { BlogFrontmatter, BlogSchema } from "~/(home)/blog/schema"; +import matter from "gray-matter"; + +type Props = { + slug: string; + directory: string; +}; + +type ProcessedMarkdownPage = { + data: BlogFrontmatter; + contentHtml: string; +}; + +export const getMarkdownPage = async ({ + slug, + directory, +}: Props): Promise => { + try { + const fileContent = await getFileContent({ + filename: `${slug}.md`, + directory, + }); + const { data: rawData, content } = matter(fileContent); + const data = BlogSchema.parse(rawData); + + if (!data.published) { + console.log(`Post ${slug} is not published`); + return notFound(); + } + + const contentHtml = await getHtmlFromMarkdown(content); + + return { data, contentHtml }; + } catch (error) { + console.error("Error loading blog post:", error); + return notFound(); + } +}; diff --git a/apps/website/package.json b/apps/website/package.json index 9ef7bf842..509833cbb 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -10,16 +10,18 @@ "lint": "next lint" }, "dependencies": { + "@repo/types": "*", "@repo/ui": "*", "gray-matter": "^4.0.3", "next": "^15.0.3", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", + "rehype-stringify": "^10.0.1", "remark": "^15.0.1", "remark-html": "^16.0.1", - "zod": "^3.24.1", + "remark-rehype": "^11.1.1", "resend": "^4.0.1", - "@repo/types": "*" + "zod": "^3.24.1" }, "devDependencies": { "@repo/eslint-config": "*", diff --git a/package-lock.json b/package-lock.json index 3afd26bef..1cd26b9c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6399,8 +6399,10 @@ "next": "^15.0.3", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", + "rehype-stringify": "^10.0.1", "remark": "^15.0.1", "remark-html": "^16.0.1", + "remark-rehype": "^11.1.1", "resend": "^4.0.1", "zod": "^3.24.1" }, @@ -16441,6 +16443,21 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", @@ -16490,6 +16507,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-rehype": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", From 854ec580d8da6583842fbce6a9323ac161ffceee Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Tue, 7 Jan 2025 23:16:34 -0600 Subject: [PATCH 2/6] schema --- apps/website/app/(home)/blog/readBlogs.tsx | 2 +- apps/website/app/{(home)/blog => types}/schema.tsx | 8 ++++++-- apps/website/app/utils/getMarkdownFile.ts | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) rename apps/website/app/{(home)/blog => types}/schema.tsx (60%) diff --git a/apps/website/app/(home)/blog/readBlogs.tsx b/apps/website/app/(home)/blog/readBlogs.tsx index 0c3ea7b7b..f1b5be15e 100644 --- a/apps/website/app/(home)/blog/readBlogs.tsx +++ b/apps/website/app/(home)/blog/readBlogs.tsx @@ -1,7 +1,7 @@ import path from "path"; import fs from "fs/promises"; import matter from "gray-matter"; -import { BlogSchema, type Blog } from "./schema"; +import { BlogSchema, type Blog } from "~/types/schema"; import { BLOG_PATH } from "~/data/constants"; const BLOG_DIRECTORY = path.join(process.cwd(), BLOG_PATH); diff --git a/apps/website/app/(home)/blog/schema.tsx b/apps/website/app/types/schema.tsx similarity index 60% rename from apps/website/app/(home)/blog/schema.tsx rename to apps/website/app/types/schema.tsx index 13568c765..378399c0a 100644 --- a/apps/website/app/(home)/blog/schema.tsx +++ b/apps/website/app/types/schema.tsx @@ -1,12 +1,16 @@ import { z } from "zod"; -export const BlogSchema = z.object({ +export const DocumentSchema = z.object({ title: z.string(), + published: z.boolean().default(false), +}); + +export const BlogSchema = DocumentSchema.extend({ date: z.string(), author: z.string(), - published: z.boolean().default(false), }); +export type DocumentFrontmatter = z.infer; export type BlogFrontmatter = z.infer; export type Blog = BlogFrontmatter & { diff --git a/apps/website/app/utils/getMarkdownFile.ts b/apps/website/app/utils/getMarkdownFile.ts index b3bcd52e7..dad08cf08 100644 --- a/apps/website/app/utils/getMarkdownFile.ts +++ b/apps/website/app/utils/getMarkdownFile.ts @@ -1,7 +1,7 @@ import { getHtmlFromMarkdown } from "~/utils/getHtmlFromMarkdown"; import { getFileContent } from "~/utils/getFileContent"; import { notFound } from "next/navigation"; -import { BlogFrontmatter, BlogSchema } from "~/(home)/blog/schema"; +import { DocumentFrontmatter, DocumentSchema } from "~/types/schema"; import matter from "gray-matter"; type Props = { @@ -10,7 +10,7 @@ type Props = { }; type ProcessedMarkdownPage = { - data: BlogFrontmatter; + data: DocumentFrontmatter; contentHtml: string; }; @@ -24,7 +24,7 @@ export const getMarkdownPage = async ({ directory, }); const { data: rawData, content } = matter(fileContent); - const data = BlogSchema.parse(rawData); + const data = DocumentSchema.parse(rawData); if (!data.published) { console.log(`Post ${slug} is not published`); From 0382fea92851121108ba7e8c67cae4b43728d573 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Tue, 7 Jan 2025 23:26:40 -0600 Subject: [PATCH 3/6] make schema super generic for now --- apps/website/app/(home)/blog/readBlogs.tsx | 12 ++++++------ apps/website/app/types/schema.tsx | 10 +++------- apps/website/app/utils/getMarkdownFile.ts | 6 +++--- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/apps/website/app/(home)/blog/readBlogs.tsx b/apps/website/app/(home)/blog/readBlogs.tsx index f1b5be15e..b9762f02e 100644 --- a/apps/website/app/(home)/blog/readBlogs.tsx +++ b/apps/website/app/(home)/blog/readBlogs.tsx @@ -1,8 +1,8 @@ import path from "path"; import fs from "fs/promises"; import matter from "gray-matter"; -import { BlogSchema, type Blog } from "~/types/schema"; import { BLOG_PATH } from "~/data/constants"; +import { PageSchema, type PageData } from "~/types/schema"; const BLOG_DIRECTORY = path.join(process.cwd(), BLOG_PATH); @@ -16,12 +16,12 @@ async function validateBlogDirectory(): Promise { } } -async function processBlogFile(filename: string): Promise { +async function processBlogFile(filename: string): Promise { try { const filePath = path.join(BLOG_DIRECTORY, filename); const fileContent = await fs.readFile(filePath, "utf-8"); const { data } = matter(fileContent); - const validatedData = BlogSchema.parse(data); + const validatedData = PageSchema.parse(data); return { slug: filename.replace(/\.md$/, ""), @@ -33,7 +33,7 @@ async function processBlogFile(filename: string): Promise { } } -export async function getAllBlogs(): Promise { +export async function getAllBlogs(): Promise { try { const directoryExists = await validateBlogDirectory(); if (!directoryExists) return []; @@ -42,7 +42,7 @@ export async function getAllBlogs(): Promise { const blogs = await Promise.all( files.filter((filename) => filename.endsWith(".md")).map(processBlogFile), ); - const validBlogs = blogs.filter(Boolean) as Blog[]; + const validBlogs = blogs.filter(Boolean) as PageData[]; return validBlogs.filter((blog) => blog.published); } catch (error) { console.error("Error reading blog directory:", error); @@ -50,7 +50,7 @@ export async function getAllBlogs(): Promise { } } -export async function getLatestBlogs(): Promise { +export async function getLatestBlogs(): Promise { const blogs = await getAllBlogs(); return blogs .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) diff --git a/apps/website/app/types/schema.tsx b/apps/website/app/types/schema.tsx index 378399c0a..c2fc38ea8 100644 --- a/apps/website/app/types/schema.tsx +++ b/apps/website/app/types/schema.tsx @@ -1,18 +1,14 @@ import { z } from "zod"; -export const DocumentSchema = z.object({ +export const PageSchema = z.object({ title: z.string(), published: z.boolean().default(false), -}); - -export const BlogSchema = DocumentSchema.extend({ date: z.string(), author: z.string(), }); -export type DocumentFrontmatter = z.infer; -export type BlogFrontmatter = z.infer; +export type PageFrontmatter = z.infer; -export type Blog = BlogFrontmatter & { +export type PageData = PageFrontmatter & { slug: string; }; diff --git a/apps/website/app/utils/getMarkdownFile.ts b/apps/website/app/utils/getMarkdownFile.ts index dad08cf08..fe51db4a0 100644 --- a/apps/website/app/utils/getMarkdownFile.ts +++ b/apps/website/app/utils/getMarkdownFile.ts @@ -1,7 +1,7 @@ import { getHtmlFromMarkdown } from "~/utils/getHtmlFromMarkdown"; import { getFileContent } from "~/utils/getFileContent"; import { notFound } from "next/navigation"; -import { DocumentFrontmatter, DocumentSchema } from "~/types/schema"; +import { PageFrontmatter, PageSchema } from "~/types/schema"; import matter from "gray-matter"; type Props = { @@ -10,7 +10,7 @@ type Props = { }; type ProcessedMarkdownPage = { - data: DocumentFrontmatter; + data: PageFrontmatter; contentHtml: string; }; @@ -24,7 +24,7 @@ export const getMarkdownPage = async ({ directory, }); const { data: rawData, content } = matter(fileContent); - const data = DocumentSchema.parse(rawData); + const data = PageSchema.parse(rawData); if (!data.published) { console.log(`Post ${slug} is not published`); From cbeaeec28ece55009cdb23aa152c5e7de7aa7936 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Tue, 7 Jan 2025 23:46:39 -0600 Subject: [PATCH 4/6] Update apps/website/app/utils/getHtmlFromMarkdown.ts bot goes boop? Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/website/app/utils/getHtmlFromMarkdown.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/website/app/utils/getHtmlFromMarkdown.ts b/apps/website/app/utils/getHtmlFromMarkdown.ts index a2e40964f..13635e916 100644 --- a/apps/website/app/utils/getHtmlFromMarkdown.ts +++ b/apps/website/app/utils/getHtmlFromMarkdown.ts @@ -23,11 +23,20 @@ function remarkHeadingId() { } export async function getHtmlFromMarkdown(markdown: string): Promise { - const htmlString = await unified() - .use(remarkParse) - .use(remarkHeadingId) - .use(remarkRehype) - .use(rehypeStringify) - .process(markdown); - return htmlString.toString(); + if (!markdown) { + throw new Error('Markdown content is required'); + } + + try { + const htmlString = await unified() + .use(remarkParse) + .use(remarkHeadingId) + .use(remarkRehype) + .use(rehypeStringify) + .process(markdown); + return htmlString.toString(); + } catch (error) { + console.error('Error processing markdown:', error); + throw new Error('Failed to process markdown content'); + } } From c50463284ed8fc9018b955b5ed5ffb51033b3b82 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Tue, 7 Jan 2025 23:47:39 -0600 Subject: [PATCH 5/6] Update apps/website/app/utils/getMarkdownFile.ts coderrabbit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/website/app/utils/getMarkdownFile.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/website/app/utils/getMarkdownFile.ts b/apps/website/app/utils/getMarkdownFile.ts index fe51db4a0..be65f55b2 100644 --- a/apps/website/app/utils/getMarkdownFile.ts +++ b/apps/website/app/utils/getMarkdownFile.ts @@ -19,6 +19,15 @@ export const getMarkdownPage = async ({ directory, }: Props): Promise => { try { + if (!slug || !directory) { + throw new Error('Both slug and directory are required'); + } + + // Prevent directory traversal + if (slug.includes('..') || directory.includes('..')) { + throw new Error('Invalid path'); + } + const fileContent = await getFileContent({ filename: `${slug}.md`, directory, From 674097a4ffa0380791f40226daef81c1c65f1e33 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Tue, 7 Jan 2025 23:49:51 -0600 Subject: [PATCH 6/6] review feedback (good bot) --- apps/website/app/(home)/blog/[slug]/page.tsx | 2 +- apps/website/app/(home)/blog/readBlogs.tsx | 4 ++-- apps/website/app/utils/getFileContent.ts | 11 +++++++---- apps/website/app/utils/getHtmlFromMarkdown.ts | 3 ++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/website/app/(home)/blog/[slug]/page.tsx b/apps/website/app/(home)/blog/[slug]/page.tsx index 6162e7c87..869f97c79 100644 --- a/apps/website/app/(home)/blog/[slug]/page.tsx +++ b/apps/website/app/(home)/blog/[slug]/page.tsx @@ -45,7 +45,7 @@ export default async function BlogPost({ params }: Params) { export async function generateStaticParams() { try { - const blogPath = path.join(process.cwd(), BLOG_PATH); + const blogPath = path.resolve(process.cwd(), BLOG_PATH); // 1) Check if the directory exists const directoryExists = await fs .stat(blogPath) diff --git a/apps/website/app/(home)/blog/readBlogs.tsx b/apps/website/app/(home)/blog/readBlogs.tsx index b9762f02e..2acc2bbb3 100644 --- a/apps/website/app/(home)/blog/readBlogs.tsx +++ b/apps/website/app/(home)/blog/readBlogs.tsx @@ -18,7 +18,7 @@ async function validateBlogDirectory(): Promise { async function processBlogFile(filename: string): Promise { try { - const filePath = path.join(BLOG_DIRECTORY, filename); + const filePath = path.resolve(BLOG_DIRECTORY, filename); const fileContent = await fs.readFile(filePath, "utf-8"); const { data } = matter(fileContent); const validatedData = PageSchema.parse(data); @@ -42,7 +42,7 @@ export async function getAllBlogs(): Promise { const blogs = await Promise.all( files.filter((filename) => filename.endsWith(".md")).map(processBlogFile), ); - const validBlogs = blogs.filter(Boolean) as PageData[]; + const validBlogs = blogs.filter((blog): blog is PageData => blog !== null); return validBlogs.filter((blog) => blog.published); } catch (error) { console.error("Error reading blog directory:", error); diff --git a/apps/website/app/utils/getFileContent.ts b/apps/website/app/utils/getFileContent.ts index d651a0587..7f63cbe3d 100644 --- a/apps/website/app/utils/getFileContent.ts +++ b/apps/website/app/utils/getFileContent.ts @@ -11,12 +11,15 @@ export const getFileContent = async ({ directory, }: Props): Promise => { try { - const filePath = path.join(directory, filename); + const safeFilename = path.basename(filename); + const filePath = path.join(directory, safeFilename); const fileContent = await fs.readFile(filePath, "utf-8"); return fileContent; } catch (error) { - throw new Error( - `Failed to read file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); + throw error instanceof Error + ? new Error(`Failed to read file ${filename}: ${error.message}`, { + cause: error, + }) + : new Error(`Failed to read file ${filename}: Unknown error`); } }; diff --git a/apps/website/app/utils/getHtmlFromMarkdown.ts b/apps/website/app/utils/getHtmlFromMarkdown.ts index 13635e916..58ec759af 100644 --- a/apps/website/app/utils/getHtmlFromMarkdown.ts +++ b/apps/website/app/utils/getHtmlFromMarkdown.ts @@ -4,9 +4,10 @@ import remarkRehype from "remark-rehype"; import rehypeStringify from "rehype-stringify"; import { toString } from "mdast-util-to-string"; import { visit } from "unist-util-visit"; +import type { Root } from "mdast"; function remarkHeadingId() { - return (tree: any) => { + return (tree: Root) => { visit(tree, "heading", (node) => { const text = toString(node); const id = text