diff --git a/deno.json b/deno.json index 2fbe833..38c6be4 100644 --- a/deno.json +++ b/deno.json @@ -5,7 +5,7 @@ "cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", "manifest": "deno task cli manifest $(pwd)", "start": "deno run --unstable-kv -A --watch=static/,routes/ dev.ts", - "build": "deno run --unstable-kv -A dev.ts build", + "build": "deno run -A scripts/warm-cache.ts && deno run -A --unstable-kv dev.ts build", "preview": "deno run -A main.ts", "update": "deno run -A -r https://fresh.deno.dev/update ." }, diff --git a/routes/[module]/[slug].tsx b/routes/[module]/[slug].tsx index 3db0f14..9f5ba80 100644 --- a/routes/[module]/[slug].tsx +++ b/routes/[module]/[slug].tsx @@ -14,6 +14,7 @@ import { TableOfContents } from "@/islands/TableOfContents.tsx"; import { PostNavigation } from "@/components/PostNavigation.tsx"; import Footer from "@/components/Footer.tsx"; import { MentorshipCard } from "@/components/MentorshipCard.tsx"; +import { getCachedContent } from "@/utils/cache.ts"; export default defineRoute( async (_req, ctx) => { @@ -36,7 +37,7 @@ export default defineRoute( ) : false; - const { html, headings } = await renderMarkdown(post.content); + const { html, headings } = await getCachedContent(post); return ( <> diff --git a/scripts/warm-cache.ts b/scripts/warm-cache.ts new file mode 100644 index 0000000..36c37cc --- /dev/null +++ b/scripts/warm-cache.ts @@ -0,0 +1,16 @@ +import { getPosts } from "@/utils/content/posts.ts"; +import { warmContentCache } from "@/utils/cache.ts"; + +async function main() { + const posts = await getPosts(); + await warmContentCache(posts.flatMap(module => module.posts)); +} + +if (import.meta.main) { + main() + .then(() => Deno.exit(0)) + .catch((error) => { + console.error("Failed to warm cache:", error); + Deno.exit(1); + }); +} \ No newline at end of file diff --git a/utils/cache.ts b/utils/cache.ts new file mode 100644 index 0000000..ac02ebf --- /dev/null +++ b/utils/cache.ts @@ -0,0 +1,79 @@ +// cache.ts +import { kv } from "@/utils/db.ts"; +import { Post } from "@/utils/content/posts.ts"; +import { renderMarkdown } from "@/utils/content/markdow.ts"; + +interface CachedContent { + html: string; + headings: Array<{level: number; text: string}>; + lastModified: number; +} + +interface CachedPost extends Post { + cachedContent?: CachedContent; +} + +const CACHE_PREFIX = "content_cache"; +const CACHE_VERSION = "v1"; +const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours + +export async function getCachedContent(post: Post): Promise { + const cacheKey = [CACHE_PREFIX, CACHE_VERSION, post.moduleSlug, post.slug]; + + // Try to get from cache first + const cached = await kv.get(cacheKey); + + if (cached.value) { + // Check if cache is still valid + const now = Date.now(); + if (now - cached.value.lastModified < CACHE_TTL) { + return cached.value; + } + } + + // Cache miss or expired, generate new content + const { html, headings } = await renderMarkdown(post.content); + const newCache: CachedContent = { + html, + headings, + lastModified: Date.now(), + }; + + // Store in cache + await kv.set(cacheKey, newCache); + + return newCache; +} + +// Function to preload all content into cache +export async function warmContentCache(posts: Post[]) { + console.log("Warming content cache..."); + + const operations = posts.map(async (post) => { + try { + await getCachedContent(post); + console.log(`Cached ${post.moduleSlug}/${post.slug}`); + } catch (error) { + console.error(`Failed to cache ${post.moduleSlug}/${post.slug}:`, error); + } + }); + + await Promise.all(operations); + console.log("Content cache warming complete"); +} + +// Function to invalidate cache for a specific post +export async function invalidateCache(moduleSlug: string, postSlug: string) { + const cacheKey = [CACHE_PREFIX, CACHE_VERSION, moduleSlug, postSlug]; + await kv.delete(cacheKey); +} + +// Function to invalidate entire cache +export async function invalidateAllCache() { + const prefix = [CACHE_PREFIX, CACHE_VERSION]; + const entries = kv.list({ prefix }); + + for await (const entry of entries) { + await kv.delete(entry.key); + } +} \ No newline at end of file