mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-07-03 19:06:40 -04:00
refactor "notes" into "posts" (only on the backend)
This commit is contained in:
@ -21,7 +21,7 @@ const HitCounter = ({ slug }: HitCounterProps) => {
|
|||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// fail somewhat silently, see error boundary in NoteMeta component
|
// fail somewhat silently, see error boundary in PostMeta component
|
||||||
if (error) {
|
if (error) {
|
||||||
showBoundary(`${error}`);
|
showBoundary(`${error}`);
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./NoteMeta";
|
|
||||||
export { default } from "./NoteMeta";
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./NoteTitle";
|
|
||||||
export { default } from "./NoteTitle";
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./NotesList";
|
|
||||||
export { default } from "./NotesList";
|
|
@ -2,11 +2,11 @@ import { ErrorBoundary } from "react-error-boundary";
|
|||||||
import Link from "../Link";
|
import Link from "../Link";
|
||||||
import Time from "../Time";
|
import Time from "../Time";
|
||||||
import HitCounter from "../HitCounter";
|
import HitCounter from "../HitCounter";
|
||||||
import NoteTitle from "../NoteTitle";
|
import PostTitle from "../PostTitle";
|
||||||
import { FiCalendar, FiTag, FiEdit, FiEye } from "react-icons/fi";
|
import { FiCalendar, FiTag, FiEdit, FiEye } from "react-icons/fi";
|
||||||
import { styled, theme } from "../../lib/styles/stitches.config";
|
import { styled, theme } from "../../lib/styles/stitches.config";
|
||||||
import * as config from "../../lib/config";
|
import * as config from "../../lib/config";
|
||||||
import type { NoteFrontMatter } from "../../types";
|
import type { PostFrontMatter } from "../../types";
|
||||||
|
|
||||||
const Wrapper = styled("div", {
|
const Wrapper = styled("div", {
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
@ -55,9 +55,9 @@ const Tag = styled("span", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type NoteMetaProps = Pick<NoteFrontMatter, "slug" | "date" | "title" | "htmlTitle" | "tags">;
|
export type PostMetaProps = Pick<PostFrontMatter, "slug" | "date" | "title" | "htmlTitle" | "tags">;
|
||||||
|
|
||||||
const NoteMeta = ({ slug, date, title, htmlTitle, tags }: NoteMetaProps) => {
|
const PostMeta = ({ slug, date, title, htmlTitle, tags }: PostMetaProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
@ -116,9 +116,9 @@ const NoteMeta = ({ slug, date, title, htmlTitle, tags }: NoteMetaProps) => {
|
|||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|
||||||
<NoteTitle {...{ slug, title, htmlTitle }} />
|
<PostTitle {...{ slug, title, htmlTitle }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NoteMeta;
|
export default PostMeta;
|
2
components/PostMeta/index.ts
Normal file
2
components/PostMeta/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./PostMeta";
|
||||||
|
export { default } from "./PostMeta";
|
@ -1,7 +1,7 @@
|
|||||||
import Link from "../Link";
|
import Link from "../Link";
|
||||||
import { styled, theme } from "../../lib/styles/stitches.config";
|
import { styled, theme } from "../../lib/styles/stitches.config";
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
import type { NoteFrontMatter } from "../../types";
|
import type { PostFrontMatter } from "../../types";
|
||||||
|
|
||||||
const Title = styled("h1", {
|
const Title = styled("h1", {
|
||||||
margin: "0.3em 0 0.5em -1px", // misaligned left margin, super nitpicky
|
margin: "0.3em 0 0.5em -1px", // misaligned left margin, super nitpicky
|
||||||
@ -18,10 +18,10 @@ const Title = styled("h1", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type NoteTitleProps = Pick<NoteFrontMatter, "slug" | "title" | "htmlTitle"> &
|
export type PostTitleProps = Pick<PostFrontMatter, "slug" | "title" | "htmlTitle"> &
|
||||||
ComponentPropsWithoutRef<typeof Title>;
|
ComponentPropsWithoutRef<typeof Title>;
|
||||||
|
|
||||||
const NoteTitle = ({ slug, title, htmlTitle, ...rest }: NoteTitleProps) => {
|
const PostTitle = ({ slug, title, htmlTitle, ...rest }: PostTitleProps) => {
|
||||||
return (
|
return (
|
||||||
<Title {...rest}>
|
<Title {...rest}>
|
||||||
<Link
|
<Link
|
||||||
@ -37,4 +37,4 @@ const NoteTitle = ({ slug, title, htmlTitle, ...rest }: NoteTitleProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NoteTitle;
|
export default PostTitle;
|
2
components/PostTitle/index.ts
Normal file
2
components/PostTitle/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./PostTitle";
|
||||||
|
export { default } from "./PostTitle";
|
@ -2,7 +2,7 @@ import Link from "../Link";
|
|||||||
import Time from "../Time";
|
import Time from "../Time";
|
||||||
import { styled, theme } from "../../lib/styles/stitches.config";
|
import { styled, theme } from "../../lib/styles/stitches.config";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import type { NotesByYear } from "../../types";
|
import type { PostsByYear } from "../../types";
|
||||||
|
|
||||||
const Section = styled("section", {
|
const Section = styled("section", {
|
||||||
fontSize: "1.1em",
|
fontSize: "1.1em",
|
||||||
@ -55,19 +55,19 @@ const PostDate = styled(Time, {
|
|||||||
color: theme.colors.medium,
|
color: theme.colors.medium,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type NotesListProps = {
|
export type PostsListProps = {
|
||||||
notesByYear: NotesByYear;
|
postsByYear: PostsByYear;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotesList = ({ notesByYear }: NotesListProps) => {
|
const PostsList = ({ postsByYear }: PostsListProps) => {
|
||||||
const sections: ReactElement[] = [];
|
const sections: ReactElement[] = [];
|
||||||
|
|
||||||
Object.entries(notesByYear).forEach(([year, notes]) => {
|
Object.entries(postsByYear).forEach(([year, posts]) => {
|
||||||
sections.push(
|
sections.push(
|
||||||
<Section key={year}>
|
<Section key={year}>
|
||||||
<Year>{year}</Year>
|
<Year>{year}</Year>
|
||||||
<List>
|
<List>
|
||||||
{notes.map(({ slug, date, title, htmlTitle }) => (
|
{posts.map(({ slug, date, title, htmlTitle }) => (
|
||||||
<Post key={slug}>
|
<Post key={slug}>
|
||||||
<PostDate date={date} format="MMM D" />
|
<PostDate date={date} format="MMM D" />
|
||||||
<span>
|
<span>
|
||||||
@ -86,10 +86,10 @@ const NotesList = ({ notesByYear }: NotesListProps) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// grouped notes enter this component ordered chronologically -- we want reverse chronological
|
// grouped posts enter this component ordered chronologically -- we want reverse chronological
|
||||||
const reversed = sections.reverse();
|
const reversed = sections.reverse();
|
||||||
|
|
||||||
return <>{reversed}</>;
|
return <>{reversed}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NotesList;
|
export default PostsList;
|
2
components/PostsList/index.ts
Normal file
2
components/PostsList/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./PostsList";
|
||||||
|
export { default } from "./PostsList";
|
@ -1,5 +1,5 @@
|
|||||||
import { Feed } from "feed";
|
import { Feed } from "feed";
|
||||||
import { getAllNotes } from "./parse-notes";
|
import { getAllPosts } from "./posts";
|
||||||
import * as config from "../config";
|
import * as config from "../config";
|
||||||
import { meJpg } from "../config/favicons";
|
import { meJpg } from "../config/favicons";
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
@ -40,22 +40,22 @@ export const buildFeed = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// add notes separately using their frontmatter
|
// add posts separately using their frontmatter
|
||||||
const notes = await getAllNotes();
|
const posts = await getAllPosts();
|
||||||
notes.forEach((note) => {
|
posts.forEach((post) => {
|
||||||
feed.addItem({
|
feed.addItem({
|
||||||
guid: note.permalink,
|
guid: post.permalink,
|
||||||
link: note.permalink,
|
link: post.permalink,
|
||||||
title: note.title,
|
title: post.title,
|
||||||
description: note.description,
|
description: post.description,
|
||||||
image: note.image && `${baseUrl}${note.image}`,
|
image: post.image && `${baseUrl}${post.image}`,
|
||||||
author: [
|
author: [
|
||||||
{
|
{
|
||||||
name: config.authorName,
|
name: config.authorName,
|
||||||
link: `${baseUrl}/`,
|
link: `${baseUrl}/`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
date: new Date(note.date),
|
date: new Date(post.date),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import { serialize } from "next-mdx-remote/serialize";
|
|
||||||
import { getNoteData } from "./parse-notes";
|
|
||||||
|
|
||||||
import type { NoteWithSource } from "../../types";
|
|
||||||
|
|
||||||
// fully parses MDX into JS and returns *everything* about a note
|
|
||||||
export const compileNote = async (slug: string): Promise<NoteWithSource> => {
|
|
||||||
const { frontMatter, content } = await getNoteData(slug);
|
|
||||||
|
|
||||||
const { remarkGfm, remarkSmartypants, remarkUnwrapImages, rehypeSlug, rehypePrism } = await import(
|
|
||||||
"./remark-rehype-plugins"
|
|
||||||
);
|
|
||||||
|
|
||||||
const { compiledSource } = await serialize(content, {
|
|
||||||
parseFrontmatter: false,
|
|
||||||
mdxOptions: {
|
|
||||||
remarkPlugins: [
|
|
||||||
// @ts-ignore
|
|
||||||
[remarkGfm, { singleTilde: false }],
|
|
||||||
[
|
|
||||||
// @ts-ignore
|
|
||||||
remarkSmartypants,
|
|
||||||
{
|
|
||||||
quotes: true,
|
|
||||||
dashes: "oldschool",
|
|
||||||
backticks: false,
|
|
||||||
ellipses: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// @ts-ignore
|
|
||||||
[remarkUnwrapImages],
|
|
||||||
],
|
|
||||||
rehypePlugins: [
|
|
||||||
// @ts-ignore
|
|
||||||
[rehypeSlug],
|
|
||||||
// @ts-ignore
|
|
||||||
[rehypePrism, { ignoreMissing: true }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
frontMatter,
|
|
||||||
source: {
|
|
||||||
compiledSource,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,90 +0,0 @@
|
|||||||
import fs from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
import glob from "fast-glob";
|
|
||||||
import pMap from "p-map";
|
|
||||||
import pMemoize from "p-memoize";
|
|
||||||
import matter from "gray-matter";
|
|
||||||
import { formatDate } from "./format-date";
|
|
||||||
|
|
||||||
import type { NoteFrontMatter } from "../../types";
|
|
||||||
|
|
||||||
export const getNoteSlugs = async (): Promise<string[]> => {
|
|
||||||
// list all .mdx files in "/notes"
|
|
||||||
const mdxFiles = await glob("*.mdx", {
|
|
||||||
cwd: path.join(process.cwd(), "notes"),
|
|
||||||
dot: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// strip the .mdx extensions from filenames
|
|
||||||
const slugs = mdxFiles.map((fileName) => fileName.replace(/\.mdx$/, ""));
|
|
||||||
|
|
||||||
return slugs;
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns front matter and/or *raw* markdown contents of a given slug
|
|
||||||
export const getNoteData = async (
|
|
||||||
slug: string
|
|
||||||
): Promise<{
|
|
||||||
frontMatter: NoteFrontMatter;
|
|
||||||
content: string;
|
|
||||||
}> => {
|
|
||||||
const fullPath = path.join(process.cwd(), "notes", `${slug}.mdx`);
|
|
||||||
const rawContent = await fs.readFile(fullPath, "utf8");
|
|
||||||
const { data, content } = matter(rawContent);
|
|
||||||
|
|
||||||
const { unified } = await import("unified");
|
|
||||||
const { remarkParse, remarkSmartypants, remarkRehype, rehypeSanitize, rehypeStringify } = await import(
|
|
||||||
"./remark-rehype-plugins"
|
|
||||||
);
|
|
||||||
|
|
||||||
// allow *very* limited markdown to be used in post titles
|
|
||||||
const parseTitle = async (title: string, allowedTags: string[] = []): Promise<string> => {
|
|
||||||
return String(
|
|
||||||
await unified()
|
|
||||||
.use(remarkParse)
|
|
||||||
.use(remarkSmartypants, {
|
|
||||||
quotes: true,
|
|
||||||
dashes: "oldschool",
|
|
||||||
backticks: false,
|
|
||||||
ellipses: false,
|
|
||||||
})
|
|
||||||
.use(remarkRehype)
|
|
||||||
.use(rehypeSanitize, { tagNames: allowedTags })
|
|
||||||
.use(rehypeStringify)
|
|
||||||
.process(title)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// process title as both plain and stylized
|
|
||||||
const [title, htmlTitle] = await Promise.all([
|
|
||||||
parseTitle(data.title),
|
|
||||||
parseTitle(data.title, ["code", "em", "strong"]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// return both the parsed YAML front matter (with a few amendments) and the raw, unparsed markdown content
|
|
||||||
return {
|
|
||||||
frontMatter: {
|
|
||||||
...(data as Partial<NoteFrontMatter>),
|
|
||||||
// zero markdown title:
|
|
||||||
title,
|
|
||||||
htmlTitle,
|
|
||||||
slug,
|
|
||||||
permalink: `${process.env.NEXT_PUBLIC_BASE_URL || ""}/notes/${slug}/`,
|
|
||||||
date: formatDate(data.date), // validate/normalize the date string provided from front matter
|
|
||||||
},
|
|
||||||
content,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns the parsed front matter of ALL notes, sorted reverse chronologically
|
|
||||||
export const getAllNotes = pMemoize(async (): Promise<NoteFrontMatter[]> => {
|
|
||||||
const slugs = await getNoteSlugs();
|
|
||||||
|
|
||||||
// for each slug, query its front matter
|
|
||||||
const data = await pMap(slugs, async (slug) => (await getNoteData(slug)).frontMatter);
|
|
||||||
|
|
||||||
// sort the results by date
|
|
||||||
data.sort((note1, note2) => (note1.date > note2.date ? -1 : 1));
|
|
||||||
|
|
||||||
return data;
|
|
||||||
});
|
|
135
lib/helpers/posts.ts
Normal file
135
lib/helpers/posts.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import path from "path";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { serialize } from "next-mdx-remote/serialize";
|
||||||
|
import glob from "fast-glob";
|
||||||
|
import pMap from "p-map";
|
||||||
|
import pMemoize from "p-memoize";
|
||||||
|
import matter from "gray-matter";
|
||||||
|
import { formatDate } from "./format-date";
|
||||||
|
import type { PostFrontMatter, PostWithSource } from "../../types";
|
||||||
|
|
||||||
|
// path to directory with .mdx files, relative to project root
|
||||||
|
export const POSTS_DIR = "./notes";
|
||||||
|
|
||||||
|
// returns front matter and the **raw & uncompiled** markdown of a given slug
|
||||||
|
export const getPostData = async (
|
||||||
|
slug: string
|
||||||
|
): Promise<{
|
||||||
|
frontMatter: PostFrontMatter;
|
||||||
|
markdown: string;
|
||||||
|
}> => {
|
||||||
|
const fullPath = path.join(process.cwd(), POSTS_DIR, `${slug}.mdx`);
|
||||||
|
const rawContent = await fs.readFile(fullPath, "utf8");
|
||||||
|
const { data, content } = matter(rawContent);
|
||||||
|
|
||||||
|
const { unified } = await import("unified");
|
||||||
|
const { remarkParse, remarkSmartypants, remarkRehype, rehypeSanitize, rehypeStringify } = await import(
|
||||||
|
"./remark-rehype-plugins"
|
||||||
|
);
|
||||||
|
|
||||||
|
// allow *very* limited markdown to be used in post titles
|
||||||
|
const parseTitle = async (title: string, allowedTags: string[] = []): Promise<string> => {
|
||||||
|
return String(
|
||||||
|
await unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkSmartypants, {
|
||||||
|
quotes: true,
|
||||||
|
dashes: "oldschool",
|
||||||
|
backticks: false,
|
||||||
|
ellipses: false,
|
||||||
|
})
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(rehypeSanitize, { tagNames: allowedTags })
|
||||||
|
.use(rehypeStringify)
|
||||||
|
.process(title)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// process title as both plain and stylized
|
||||||
|
const [title, htmlTitle] = await Promise.all([
|
||||||
|
parseTitle(data.title),
|
||||||
|
parseTitle(data.title, ["code", "em", "strong"]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// return both the parsed YAML front matter (with a few amendments) and the raw, unparsed markdown content
|
||||||
|
return {
|
||||||
|
frontMatter: {
|
||||||
|
...(data as Partial<PostFrontMatter>),
|
||||||
|
// zero markdown title:
|
||||||
|
title,
|
||||||
|
htmlTitle,
|
||||||
|
slug,
|
||||||
|
permalink: `${process.env.NEXT_PUBLIC_BASE_URL || ""}/${POSTS_DIR}/${slug}/`,
|
||||||
|
date: formatDate(data.date), // validate/normalize the date string provided from front matter
|
||||||
|
},
|
||||||
|
markdown: content,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// fully parses MDX into JS and returns *everything* about a post
|
||||||
|
export const compilePost = async (slug: string): Promise<PostWithSource> => {
|
||||||
|
const { frontMatter, markdown } = await getPostData(slug);
|
||||||
|
|
||||||
|
const { remarkGfm, remarkSmartypants, remarkUnwrapImages, rehypeSlug, rehypePrism } = await import(
|
||||||
|
"./remark-rehype-plugins"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { compiledSource } = await serialize(markdown, {
|
||||||
|
parseFrontmatter: false,
|
||||||
|
mdxOptions: {
|
||||||
|
remarkPlugins: [
|
||||||
|
// @ts-ignore
|
||||||
|
[remarkGfm, { singleTilde: false }],
|
||||||
|
[
|
||||||
|
// @ts-ignore
|
||||||
|
remarkSmartypants,
|
||||||
|
{
|
||||||
|
quotes: true,
|
||||||
|
dashes: "oldschool",
|
||||||
|
backticks: false,
|
||||||
|
ellipses: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// @ts-ignore
|
||||||
|
[remarkUnwrapImages],
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
// @ts-ignore
|
||||||
|
[rehypeSlug],
|
||||||
|
// @ts-ignore
|
||||||
|
[rehypePrism, { ignoreMissing: true }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
frontMatter,
|
||||||
|
source: {
|
||||||
|
compiledSource,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPostSlugs = pMemoize(async (): Promise<string[]> => {
|
||||||
|
// list all .mdx files in POSTS_DIR
|
||||||
|
const mdxFiles = await glob("*.mdx", {
|
||||||
|
cwd: path.join(process.cwd(), POSTS_DIR),
|
||||||
|
dot: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// strip the .mdx extensions from filenames
|
||||||
|
const slugs = mdxFiles.map((fileName) => fileName.replace(/\.mdx$/, ""));
|
||||||
|
|
||||||
|
return slugs;
|
||||||
|
});
|
||||||
|
|
||||||
|
// returns the parsed front matter of ALL posts, sorted reverse chronologically
|
||||||
|
export const getAllPosts = pMemoize(async (): Promise<PostFrontMatter[]> => {
|
||||||
|
// for each post, query its front matter
|
||||||
|
const data = await pMap(await getPostSlugs(), async (slug) => (await getPostData(slug)).frontMatter);
|
||||||
|
|
||||||
|
// sort the results by date
|
||||||
|
data.sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
@ -2,16 +2,15 @@ import { InView } from "react-intersection-observer";
|
|||||||
import { NextSeo, ArticleJsonLd } from "next-seo";
|
import { NextSeo, ArticleJsonLd } from "next-seo";
|
||||||
import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";
|
import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";
|
||||||
import Content from "../../components/Content";
|
import Content from "../../components/Content";
|
||||||
import NoteMeta from "../../components/NoteMeta";
|
import PostMeta from "../../components/PostMeta";
|
||||||
import Comments from "../../components/Comments";
|
import Comments from "../../components/Comments";
|
||||||
import * as mdxComponents from "../../lib/helpers/mdx-components";
|
import * as mdxComponents from "../../lib/helpers/mdx-components";
|
||||||
import { getNoteSlugs } from "../../lib/helpers/parse-notes";
|
import { getPostSlugs, compilePost } from "../../lib/helpers/posts";
|
||||||
import { compileNote } from "../../lib/helpers/compile-note";
|
|
||||||
import * as config from "../../lib/config";
|
import * as config from "../../lib/config";
|
||||||
import { articleJsonLd } from "../../lib/config/seo";
|
import { articleJsonLd } from "../../lib/config/seo";
|
||||||
import { meJpg } from "../../lib/config/favicons";
|
import { meJpg } from "../../lib/config/favicons";
|
||||||
import type { GetStaticProps, GetStaticPaths, InferGetStaticPropsType } from "next";
|
import type { GetStaticProps, GetStaticPaths, InferGetStaticPropsType } from "next";
|
||||||
import type { NoteWithSource, NoteFrontMatter } from "../../types";
|
import type { PostWithSource, PostFrontMatter } from "../../types";
|
||||||
|
|
||||||
const Note = ({ frontMatter, source }: InferGetStaticPropsType<typeof getStaticProps>) => {
|
const Note = ({ frontMatter, source }: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
return (
|
return (
|
||||||
@ -51,7 +50,7 @@ const Note = ({ frontMatter, source }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
{...articleJsonLd}
|
{...articleJsonLd}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NoteMeta {...frontMatter} />
|
<PostMeta {...frontMatter} />
|
||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
<MDXRemote {...source} components={{ ...(mdxComponents as MDXRemoteProps["components"]) }} />
|
<MDXRemote {...source} components={{ ...(mdxComponents as MDXRemoteProps["components"]) }} />
|
||||||
@ -70,14 +69,14 @@ const Note = ({ frontMatter, source }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<NoteWithSource, Pick<NoteFrontMatter, "slug">> = async ({ params }) => {
|
export const getStaticProps: GetStaticProps<PostWithSource, Pick<PostFrontMatter, "slug">> = async ({ params }) => {
|
||||||
if (!params?.slug) {
|
if (!params?.slug) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { frontMatter, source } = await compileNote(params.slug);
|
const { frontMatter, source } = await compilePost(params.slug);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
@ -88,7 +87,10 @@ export const getStaticProps: GetStaticProps<NoteWithSource, Pick<NoteFrontMatter
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
const slugs = await getNoteSlugs();
|
// get the slug of each .mdx file in /notes
|
||||||
|
const slugs = await getPostSlugs();
|
||||||
|
|
||||||
|
// map slugs into a static paths object required by next.js
|
||||||
const paths = slugs.map((slug) => ({ params: { slug } }));
|
const paths = slugs.map((slug) => ({ params: { slug } }));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { NextSeo } from "next-seo";
|
import { NextSeo } from "next-seo";
|
||||||
import Content from "../../components/Content";
|
import Content from "../../components/Content";
|
||||||
import NotesList from "../../components/NotesList";
|
import PostsList from "../../components/PostsList";
|
||||||
import { getAllNotes } from "../../lib/helpers/parse-notes";
|
import { getAllPosts } from "../../lib/helpers/posts";
|
||||||
import { authorName } from "../../lib/config";
|
import { authorName } from "../../lib/config";
|
||||||
import type { GetStaticProps, InferGetStaticPropsType } from "next";
|
import type { GetStaticProps, InferGetStaticPropsType } from "next";
|
||||||
import type { NotesByYear } from "../../types";
|
import type { PostsByYear } from "../../types";
|
||||||
|
|
||||||
const Notes = ({ notesByYear }: InferGetStaticPropsType<typeof getStaticProps>) => {
|
const Notes = ({ notesByYear }: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||||
return (
|
return (
|
||||||
@ -18,18 +18,18 @@ const Notes = ({ notesByYear }: InferGetStaticPropsType<typeof getStaticProps>)
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
<NotesList notesByYear={notesByYear} />
|
<PostsList postsByYear={notesByYear} />
|
||||||
</Content>
|
</Content>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<{
|
export const getStaticProps: GetStaticProps<{
|
||||||
notesByYear: NotesByYear;
|
notesByYear: PostsByYear;
|
||||||
}> = async () => {
|
}> = async () => {
|
||||||
// parse the year of each note and group them together
|
// parse the year of each note and group them together
|
||||||
const notes = await getAllNotes();
|
const notes = await getAllPosts();
|
||||||
const notesByYear: NotesByYear = {};
|
const notesByYear: PostsByYear = {};
|
||||||
|
|
||||||
notes.forEach((note) => {
|
notes.forEach((note) => {
|
||||||
const year = new Date(note.date).getUTCFullYear();
|
const year = new Date(note.date).getUTCFullYear();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SitemapStream, EnumChangefreq } from "sitemap";
|
import { SitemapStream, EnumChangefreq } from "sitemap";
|
||||||
import { getAllNotes } from "../lib/helpers/parse-notes";
|
import { getAllPosts } from "../lib/helpers/posts";
|
||||||
import { siteDomain } from "../lib/config";
|
import { siteDomain } from "../lib/config";
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import type { SitemapItemLoose } from "sitemap";
|
import type { SitemapItemLoose } from "sitemap";
|
||||||
@ -39,20 +39,20 @@ export const getServerSideProps: GetServerSideProps<Record<string, never>> = asy
|
|||||||
{ url: "/zip/" },
|
{ url: "/zip/" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// push notes separately and use their metadata
|
// push posts separately and use their metadata
|
||||||
const notes = await getAllNotes();
|
const posts = await getAllPosts();
|
||||||
notes.forEach((note) => {
|
posts.forEach((post) => {
|
||||||
pages.push({
|
pages.push({
|
||||||
url: `/notes/${note.slug}/`,
|
url: `/notes/${post.slug}/`,
|
||||||
// pull lastMod from front matter date
|
// pull lastMod from front matter date
|
||||||
lastmod: note.date,
|
lastmod: post.date,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// set lastmod of /notes/ page to most recent post's date
|
// set lastmod of /notes/ page to most recent post's date
|
||||||
pages.push({
|
pages.push({
|
||||||
url: `/notes/`,
|
url: `/notes/`,
|
||||||
lastmod: notes[0].date,
|
lastmod: posts[0].date,
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort alphabetically by URL
|
// sort alphabetically by URL
|
||||||
|
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
@ -1,3 +1,3 @@
|
|||||||
export * from "./note";
|
export * from "./post";
|
||||||
export * from "./project";
|
export * from "./project";
|
||||||
export * from "./stats";
|
export * from "./stats";
|
||||||
|
12
types/note.d.ts → types/post.d.ts
vendored
12
types/note.d.ts → types/post.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||||
|
|
||||||
export type NoteFrontMatter = {
|
export type PostFrontMatter = {
|
||||||
slug: string;
|
slug: string;
|
||||||
permalink: string;
|
permalink: string;
|
||||||
date: string;
|
date: string;
|
||||||
@ -12,14 +12,14 @@ export type NoteFrontMatter = {
|
|||||||
noComments?: boolean;
|
noComments?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NoteWithSource = {
|
export type PostWithSource = {
|
||||||
// yaml metadata
|
// yaml metadata
|
||||||
frontMatter: NoteFrontMatter;
|
frontMatter: PostFrontMatter;
|
||||||
|
|
||||||
// the final, compiled JSX by next-mdx-remote; see lib/helpers/parse-notes.ts
|
// the final, compiled JSX by next-mdx-remote; see lib/helpers/posts.ts
|
||||||
source: Partial<Pick<MDXRemoteSerializeResult<Record<string, never>, Record<string, never>>>>;
|
source: Partial<Pick<MDXRemoteSerializeResult<Record<string, never>, Record<string, never>>>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotesByYear = {
|
export type PostsByYear = {
|
||||||
[year: string]: NoteFrontMatter[];
|
[year: string]: PostFrontMatter[];
|
||||||
};
|
};
|
Reference in New Issue
Block a user