Repository navigation
Description
Goal
Replace all Sanity GROQ queries in public blog routes with Drizzle ORM queries. Create BlogPostRenderer server component (replaces the custom 302-line Portable Text renderer). After this PR, the public blog reads entirely from PostgreSQL — no Sanity requests.
Scope
Query layer — frontend/db/queries/blog/
Pattern: follow frontend/db/queries/quizzes/admin-quiz.ts
-
blog-posts.ts:getBlogPosts(locale)— all published posts ordered by publishedAt desc, filtered byisPublished = true AND (scheduledPublishAt IS NULL OR scheduledPublishAt <= NOW())getBlogPostBySlug(slug, locale)— single post with author + categoriesgetBlogPostsByCategory(categorySlug, locale)— filtered by categorygetBlogPostSlugs()— for generateStaticParams
-
blog-categories.ts:getBlogCategories(locale)— all categories ordered by displayOrder
-
blog-authors.ts:getBlogAuthorByName(name, locale)— for /api/blog-author
-
admin-blog.ts— admin variants (all locales, include drafts, pagination) — used in Issue (SP: 2) [Page] Home -> Hero #4
New component — components/blog/BlogPostRenderer.tsx
Server component (no use client). Reads Tiptap JSON, outputs semantic HTML with Tailwind.
Same concept as components/quiz/ExplanationRenderer.tsx.
Node types: paragraph, heading (h1-h6), blockquote, bulletList, orderedList, codeBlock, image, text with marks (bold, italic, code, link).
Files to modify
| File | Change |
|---|---|
app/[locale]/blog/page.tsx |
Replace GROQ + client → getBlogPosts() + getBlogCategories(). Add export const revalidate = 3600. Remove noStore(). |
app/[locale]/blog/[slug]/page.tsx |
generateStaticParams → getBlogPostSlugs(). generateMetadata → getBlogPostBySlug. |
app/[locale]/blog/[slug]/PostDetails.tsx |
Update prop types. Replace entire renderPortableText block with <BlogPostRenderer content={body} />. File shrinks from 302 to ~80 lines. |
app/[locale]/blog/category/[category]/page.tsx |
Use DB slug directly (drop runtime slugify matching). Query getBlogPostsByCategory(). |
app/api/blog-search/route.ts |
Replace GROQ with getBlogPosts(locale) + generateText() from @tiptap/core for text extraction. |
app/api/blog-author/route.ts |
Replace GROQ with getBlogAuthorByName(name, locale). |
lib/blog/image.ts |
Replace isSanityAssetUrl/shouldBypassImageOptimization with isCloudinaryUrl() returning false (Cloudinary works natively with Next.js Image). |
components/blog/BlogCard.tsx |
Fix Link import (next/link → @/i18n/routing). Replace plainTextFromPortableText with generateText(). |
components/blog/BlogFilters.tsx |
Update Post type: _id→id, slug.current→slug. Remove PortableText* types. |
Testing gate before merge
npm run dev— navigate all blog routes (/blog, /blog/[slug], /blog/category/[cat])- Browser network tab: zero requests to
api.sanity.ioorcdn.sanity.io export const revalidate = 3600visible in build output- Blog search works
- Author modal works
Expected impact
- Public blog reads from PostgreSQL — Sanity billing stops
- ISR enabled (revalidate: 3600) — better performance than previous no-cache strategy
- Scheduled posts: automatically appear when
scheduledPublishAt <= NOW()(within 1h ISR window) BlogPostRendereris a server component — no Tiptap JS bundle on public pages
Out of scope
- Admin UI
- Sanity cleanup
Activity