diff --git a/api/_lib/_dates.ts b/api/_lib/_dates.ts index 38e4b85e..3165a357 100644 --- a/api/_lib/_dates.ts +++ b/api/_lib/_dates.ts @@ -1,7 +1,7 @@ import { format, parse, parseISO } from "date-fns"; -export function niceDate(d: string) { - return format(parse(d, "yyyy-MM-dd", new Date()), "LLLL d, yyyy"); +export function niceDate(d: Date) { + return format(d, "LLLL d, yyyy"); } export function dateString(d: string) { diff --git a/api/blog/post.ts b/api/blog/post.ts index e608496f..5d93773a 100644 --- a/api/blog/post.ts +++ b/api/blog/post.ts @@ -28,7 +28,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { if (!("properties" in post)) throw new Error("No properties"); const { properties = {}, id } = post; const { date, ...props } = getNestedProperties(properties); - const publishDate = niceDate(date); + const publishDate = niceDate(new Date(date)); const htmlContent = await getPostHtmlFromId(id); diff --git a/api/blog/posts.ts b/api/blog/posts.ts index 90d89d23..ba3b37df 100644 --- a/api/blog/posts.ts +++ b/api/blog/posts.ts @@ -1,31 +1,59 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; -import { dateAsNumber, dateString, niceDate } from "../_lib/_dates"; -import { notion, getNestedProperties } from "../_lib/_notion"; +import { niceDate } from "../_lib/_dates"; +import { notion } from "../_lib/_notion"; +import { z } from "zod"; +import { BlogPost, NotionPost } from "shared"; export default async function handler(req: VercelRequest, res: VercelResponse) { const response = await notion.databases.query({ database_id: "b7a09b10aa83485b94092269239a8b38", }); - const posts = response.results.map((page) => { - if (!("properties" in page)) throw new Error("No properties"); - const { properties = {}, id } = page; - const { date, ...props } = getNestedProperties(properties); - if (!("title" in props)) throw new Error("No title"); - if (!("status" in props)) throw new Error("No status"); - if (!("slug" in props)) throw new Error("No slug"); - if (!("description" in props)) throw new Error("No description"); - - if (!date) throw new Error("No date"); - - const sanitizedDate = dateString(date); - const publishDate = niceDate(date); - const rawDate = dateAsNumber(date); - - return { id, rawDate, date: sanitizedDate, publishDate, ...props }; - }); + + const notionPosts = response.results.filter( + isValidNotionPost + ) as unknown as NotionPost[]; + + const posts = notionPosts.map(notionPostToBlogPost); // Cache for 1 week, stale-while-revalidate res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate"); res.json(posts); } + +const postSchema: z.ZodType = z.object({ + id: z.string(), + created_time: z.string(), + properties: z.object({ + title: z.object({ + title: z.array(z.object({ plain_text: z.string() })).min(1), + }), + description: z.object({ + rich_text: z.array(z.object({ plain_text: z.string() })).min(1), + }), + status: z.object({ + status: z.object({ name: z.string() }), + }), + slug: z.object({ + rich_text: z.array(z.object({ plain_text: z.string() })).min(1), + }), + }), +}); + +function isValidNotionPost(post: unknown): post is NotionPost { + const parsed = postSchema.safeParse(post); + if (parsed.success) return true; + return false; +} + +function notionPostToBlogPost(post: NotionPost): BlogPost { + return { + id: post.id, + publishDate: new Date(post.created_time).getTime(), + niceDate: niceDate(new Date(post.created_time)), + description: post.properties.description.rich_text[0].plain_text, + slug: post.properties.slug.rich_text[0].plain_text, + status: post.properties.status.status.name, + title: post.properties.title.title[0].plain_text, + }; +} diff --git a/app/src/pages/Blog.tsx b/app/src/pages/Blog.tsx index 2736cd3c..2afa0a83 100644 --- a/app/src/pages/Blog.tsx +++ b/app/src/pages/Blog.tsx @@ -8,11 +8,13 @@ import { InfoHeader } from "../components/InfoHeader"; import { Box } from "../slang"; import { Page } from "../ui/Shared"; import { SectionTitle } from "../ui/Typography"; +import type { BlogPost } from "shared"; export default function Blog() { const posts = useQuery("posts", getAndPreparePosts, { staleTime: Infinity, suspense: true, }); + return ( <> @@ -41,7 +43,7 @@ export default function Blog() { ); } -function Post({ post }: { post: PostType }) { +function Post({ post }: { post: BlogPost }) { return (
- {post.publishDate} + {post.niceDate} {post.title}
@@ -65,20 +67,8 @@ function Post({ post }: { post: PostType }) { async function getAndPreparePosts() { const response = await axios.get("/api/blog/posts"); - const posts = (response.data as PostType[]) - .sort((a, b) => b.rawDate - a.rawDate) - // only show posts with status "Done" + const posts = (response.data as BlogPost[]) + .sort((a, b) => b.publishDate - a.publishDate) .filter((post) => post.status === "Done"); return posts; } - -export type PostType = { - id: string; - rawDate: number; - date: string; - publishDate: string; - description: string; - status: string; - slug: string; - title: string; -}; diff --git a/app/src/pages/post/Post.tsx b/app/src/pages/post/Post.tsx index f4d14dbf..f1f5ff6a 100644 --- a/app/src/pages/post/Post.tsx +++ b/app/src/pages/post/Post.tsx @@ -6,7 +6,7 @@ import { useParams } from "react-router-dom"; import { Page } from "../../ui/Shared"; import { PageTitle } from "../../ui/Typography"; -import { PostType } from "../Blog"; + import { Helmet } from "react-helmet"; export default function Post() { @@ -78,3 +78,14 @@ async function getPost( const post = response.data; return post; } + +export type PostType = { + id: string; + rawDate: number; + date: string; + publishDate: string; + description: string; + status: string; + slug: string; + title: string; +}; diff --git a/shared/src/blog.ts b/shared/src/blog.ts new file mode 100644 index 00000000..5b0c7926 --- /dev/null +++ b/shared/src/blog.ts @@ -0,0 +1,38 @@ +export type NotionPost = { + id: string; + created_time: string; + properties: NotionPostProperties; +}; + +export type NotionPostProperties = { + title: { + title: { + plain_text: string; + }[]; + }; + description: { + rich_text: { + plain_text: string; + }[]; + }; + status: { + status: { + name: string; + }; + }; + slug: { + rich_text: { + plain_text: string; + }[]; + }; +}; + +export type BlogPost = { + id: string; + publishDate: number; + niceDate: string; + description: string; + slug: string; + status: string; + title: string; +}; diff --git a/shared/src/index.ts b/shared/src/index.ts index b3fe3454..8eb795a1 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,2 +1,3 @@ export * from "./ImportDataFormType"; export * from "./templates"; +export * from "./blog";