diff --git a/app/[contentType]/[slug]/page.tsx b/app/[contentType]/[slug]/page.tsx index ec742b8..45e5e4b 100644 --- a/app/[contentType]/[slug]/page.tsx +++ b/app/[contentType]/[slug]/page.tsx @@ -1,126 +1,101 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import matter from 'gray-matter'; import type { Metadata } from 'next'; import Image from 'next/image'; import { notFound } from 'next/navigation'; -import rehypeHighlight from 'rehype-highlight'; -import remarkGfm from 'remark-gfm'; -import remarkHtml from 'remark-html'; -import remarkParse from 'remark-parse'; -import { unified } from 'unified'; import { Chips, Container } from '../../../components'; import BackButton from '../../../components/back-button'; import info from '../../../config/index.json' assert { type: 'json' }; -import type { IContentData, IContentType } from '../../../utils/content'; +import { + getContentData, + getContentList, + getContentTypes, + type IContentData, + type IContentType, +} from '../../../utils/content'; +import { contentTypesMap } from '../../../utils/content-types'; import Content from './content'; -type Params = { - params: Promise<{ slug: string; contentType: IContentType }>; -}; - -const contentTypes = ['articles', 'notes', 'works']; - -function getMarkdownContent(contentType: IContentType, slug: string) { - if (!contentTypes.includes(contentType)) { - notFound(); - } - - const filePath = path.join( - process.cwd(), - 'content', - contentType, - `${slug}.md`, +export async function generateMetadata({ + params, +}: { + params: Params; +}): Promise { + const { title, previewImage, description } = await getContentData( + (await params).slug, + (await params).contentType, ); - if (!fs.existsSync(filePath)) { - notFound(); - } - - const fileContent = fs.readFileSync(filePath, 'utf-8'); - const { data, content } = matter(fileContent); - - return { data, content }; -} - -export async function generateMetadata({ params }: Params): Promise { - const { slug, contentType } = await params; - if (!contentTypes.includes(contentType)) { - notFound(); - } - - const filePath = path.join( - path.join(process.cwd(), 'content'), - contentType, - `${slug}.md`, - ); - if (!fs.existsSync(filePath)) { - notFound(); - } - - const { data } = getMarkdownContent(contentType, slug); return { - title: `${data.title} | ${info.site.siteTitle}`, - description: data.description ?? info.site.siteDescription, + title: `${title} | ${info.site.siteTitle}`, + description: description ?? info.site.siteDescription, openGraph: { - title: `${data.title} | ${info.site.siteName}`, - description: data.description ?? info.site.siteDescription, + title: `${title} | ${info.site.siteName}`, + description: description ?? info.site.siteDescription, url: info.site.siteUrl, - images: data.previewImage ?? info.site.siteImage, + images: previewImage ?? info.site.siteImage, siteName: info.site.siteName, }, twitter: { card: 'summary_large_image', creator: info.author.twitterHandle, - images: data.previewImage ?? info.site.siteImage, + images: previewImage ?? info.site.siteImage, }, }; } -export default async function ContentPage({ params }: Params) { - const { slug, contentType } = await params; +/** + * statically generate all content pages + */ +export function generateStaticParams() { + const contentTypes = getContentTypes(); + + return contentTypes.flatMap((contentType) => { + const contentList = getContentList(contentType); + return contentList.map(({ slug }) => { + return { + contentType, + slug, + }; + }); + }); +} - const { data, content } = getMarkdownContent(contentType, slug); +/** + * Renders articles markdown posts + */ - if (data.draft) { - notFound(); - } +async function fetchContentData(slug: string, contentType: IContentType) { + return await getContentData(slug, contentType); +} - const processedContent = await unified() - .use(remarkParse) - .use(remarkHtml, { sanitize: false }) - .use(remarkGfm) - .use(rehypeHighlight) - .process(content); - - const contentHtml = processedContent.toString(); - - if (contentType === 'works') { - return ( - - ); +type Params = { + slug: string; + contentType: IContentType; +}; + +export default async function ContentPage({ params }: { params: Params }) { + const { slug, contentType } = params; + + if (!contentTypesMap.has(contentType)) { + return notFound(); } + const content = await fetchContentData(slug, contentType); + if (content.draft) return notFound(); + + if (contentType === 'works') return ; + return (

- {data.title} + {content.title}

- - {!!data.previewImage && ( + + {content.previewImage && (
- - {data.tags && } + + {content.tags && }
); } @@ -152,7 +127,7 @@ function WorkPage({ work }: { work: IContentData }) { {work.problem && ( - + )} diff --git a/components/cards.tsx b/components/cards.tsx index 6b91257..56be436 100644 --- a/components/cards.tsx +++ b/components/cards.tsx @@ -25,8 +25,7 @@ const Cards = ({ items, basePath }: ICard) => { key={`singleCard-${singleCard.slug}`} >
diff --git a/components/notes/note.tsx b/components/notes/note.tsx index d528752..6f91763 100644 --- a/components/notes/note.tsx +++ b/components/notes/note.tsx @@ -5,8 +5,7 @@ import type { IContent } from '../../utils/content'; export function Note({ date, title, slug, basePath }: IContent) { return (
diff --git a/content/works/code-collab.md b/content/work/code-collab.md similarity index 100% rename from content/works/code-collab.md rename to content/work/code-collab.md diff --git a/content/works/health-track.md b/content/work/health-track.md similarity index 100% rename from content/works/health-track.md rename to content/work/health-track.md diff --git a/content/works/task-buddy.md b/content/work/task-buddy.md similarity index 100% rename from content/works/task-buddy.md rename to content/work/task-buddy.md diff --git a/utils/content.ts b/utils/content.ts index 070067a..67cb7fb 100644 --- a/utils/content.ts +++ b/utils/content.ts @@ -1,10 +1,15 @@ import fs from 'node:fs'; import path from 'node:path'; import matter from 'gray-matter'; +import rehypeHighlight from 'rehype-highlight'; +import gfm from 'remark-gfm'; +import html from 'remark-html'; +import remarkParse from 'remark-parse'; +import { unified } from 'unified'; import { v4 as uuid } from 'uuid'; import { contentTypesMap } from './content-types'; -const workDirectory = path.join(process.cwd(), 'content', 'works'); +const workDirectory = path.join(process.cwd(), 'content', 'work'); const notesDirectory = path.join(process.cwd(), 'content', 'notes'); const articlesDirectory = path.join(process.cwd(), 'content', 'articles'); @@ -103,6 +108,76 @@ export const getAllContentIds = (contentType: IContentType) => { }); }; +/** + * Get data for a given post id + * @param {string} id ID of the post being passed + * @param {string} contentType Type of content + */ + +export const getContentData = async (id: string, contentType: IContentType) => { + let contentTypeDirectory; + let filenames; + switch (contentType.toLowerCase()) { + case 'articles': + filenames = fs.readdirSync(articlesDirectory); + contentTypeDirectory = articlesDirectory; + break; + + case 'notes': + filenames = fs.readdirSync(notesDirectory); + contentTypeDirectory = notesDirectory; + break; + + case 'works': + filenames = fs.readdirSync(workDirectory); + contentTypeDirectory = workDirectory; + break; + + default: + throw new Error('You have to provide a content type'); + } + + // loop through all the content types and compare the slug to get the filename + const match = filenames.filter((filename) => { + const filePath = path.join(contentTypeDirectory, filename); + + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const matterResult = matter(fileContent); + const { slug } = matterResult.data; + + return slug === id; + }); + + // use the returned path to get the fullpath and read the file content + const fullPath = path.join(contentTypeDirectory, match[0]!); + // const fullPath = path.join(contentTypeDirectory, `${id}.md`); + const fileContents = fs.readFileSync(fullPath, 'utf-8'); + + const matterResult = matter(fileContents); + const processedContent = await unified() + .use(remarkParse) + .use(html, { sanitize: false }) + .use(gfm) + .use(rehypeHighlight) + .process(matterResult.content); + + const contentHtml = processedContent.toString(); + + return { + id, + contentHtml, + title: matterResult.data.title, + draft: matterResult.data.draft || false, + date: matterResult.data.date, + previewImage: matterResult.data.previewImage || '', + description: matterResult.data.description || '', + tags: matterResult.data.tags || [], + category: matterResult.data.category || '', + problem: matterResult.data.problem || '', + techStack: matterResult.data.techStack || [], + }; +}; + /** * Get content list for a particular content type * @param {string} contentType Type of content