diff --git a/src/components/blog-post.tsx b/src/components/blog-post.tsx index bc46b83..3717dcb 100644 --- a/src/components/blog-post.tsx +++ b/src/components/blog-post.tsx @@ -1,13 +1,20 @@ // stream.cameron.BlogPost — Renders a single blog post. import { raw } from "hono/html"; -import { getBlogPost, getProfile } from "../data.ts"; +import { getBlogPost, getProfile, type BlogPost as BlogPostData } from "../data.ts"; import { renderMarkdown } from "../markdown.ts"; +import { cacheGet, cacheSet } from "../cache.ts"; import { MarginPanel } from "./margin.tsx"; -export async function BlogPost({ slug }: { slug: string }) { +export async function BlogPost({ + slug, + post: providedPost, +}: { + slug: string; + post?: BlogPostData; +}) { const [post, profile] = await Promise.all([ - getBlogPost(slug), + providedPost ? Promise.resolve(providedPost) : getBlogPost(slug), getProfile(), ]); @@ -42,7 +49,12 @@ export async function BlogPost({ slug }: { slug: string }) { day: "numeric", }); - const html = await renderMarkdown(post.body); + const markdownCacheKey = `markdown:${post.uri}:${post.updatedAt ?? post.publishedAt}`; + let html = (await cacheGet(markdownCacheKey)) as string | undefined; + if (!html) { + html = await renderMarkdown(post.body); + await cacheSet(markdownCacheKey, html, { life: "hours" }); + } const atUri = post.uri; const siteBase = process.env.SITE_URL || "https://cameron.stream"; diff --git a/src/data.ts b/src/data.ts index 4092ace..27e069f 100644 --- a/src/data.ts +++ b/src/data.ts @@ -2,6 +2,7 @@ import { AtUri } from "@atproto/syntax"; import { UnicodeString } from "@atproto/api"; +import { cacheGet, cacheSet } from "./cache.ts"; const PDS = "https://bsky.social"; const COLLECTION = "site.standard.document"; @@ -41,12 +42,21 @@ async function getRecord( collection: string, rkey: string ): Promise | null> { + const cacheKey = `record:${repo}:${collection}:${rkey}`; + const cached = await cacheGet(cacheKey); + if (cached !== undefined) return cached as Record | null; + const res = await fetch( `${PDS}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}` ); - if (!res.ok) return null; + if (!res.ok) { + await cacheSet(cacheKey, null, { life: "minutes" }); + return null; + } const data = await res.json(); - return data.value as Record; + const record = data.value as Record; + await cacheSet(cacheKey, record, { life: "minutes" }); + return record; } async function listRecords( @@ -54,15 +64,23 @@ async function listRecords( collection: string, limit = 100 ): Promise }>> { + const cacheKey = `records:${repo}:${collection}:${limit}`; + const cached = await cacheGet(cacheKey); + if (cached !== undefined) { + return cached as Array<{ uri: string; value: Record }>; + } + const res = await fetch( `${PDS}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&limit=${limit}` ); if (!res.ok) return []; const data = await res.json(); - return (data.records ?? []) as Array<{ + const records = (data.records ?? []) as Array<{ uri: string; value: Record; }>; + await cacheSet(cacheKey, records, { life: "minutes" }); + return records; } // Extract slug from the document's path field. @@ -297,12 +315,19 @@ function parseDocument( export async function getProfile(): Promise { const did = getDid(); + const cacheKey = `profile:${did}`; + const cached = await cacheGet(cacheKey); + if (cached !== undefined) return cached as ProfileData | null; + const res = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}` ); - if (!res.ok) return null; + if (!res.ok) { + await cacheSet(cacheKey, null, { life: "minutes" }); + return null; + } const data = await res.json(); - return { + const profile = { did: data.did, handle: data.handle, displayName: data.displayName, @@ -312,6 +337,8 @@ export async function getProfile(): Promise { followsCount: data.followsCount, postsCount: data.postsCount, }; + await cacheSet(cacheKey, profile, { life: "minutes" }); + return profile; } export async function listBlogPosts(): Promise { diff --git a/src/index.tsx b/src/index.tsx index df5974a..a1a4d12 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,6 +14,7 @@ import { Semble } from "./components/semble.tsx"; import { Bluesky } from "./components/bluesky.tsx"; import { Travel } from "./components/travel.tsx"; import "./types.ts"; +import { getBlogPost } from "./data.ts"; type JSXElement = | import("hono/utils/html").HtmlEscapedString @@ -190,12 +191,11 @@ app.get("/blog/:slug", async (c) => { // Catch-all: blog posts by slug (must be last) app.get("/:slug", async (c) => { const slug = c.req.param("slug"); - const { getBlogPost } = await import("./data.ts"); const post = await getBlogPost(slug); if (!post) return c.notFound(); const stream = renderToReadableStream( - + ); return c.body(stream, {