Skip to content

(SP: 3) [Frontend] Drizzle query layer + swap blog public routes from Sanity to PostgreSQL #386

Closed

Description

@LesiaUKR

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

  1. blog-posts.ts:

    • getBlogPosts(locale) — all published posts ordered by publishedAt desc, filtered by isPublished = true AND (scheduledPublishAt IS NULL OR scheduledPublishAt <= NOW())
    • getBlogPostBySlug(slug, locale) — single post with author + categories
    • getBlogPostsByCategory(categorySlug, locale) — filtered by category
    • getBlogPostSlugs() — for generateStaticParams
  2. blog-categories.ts:

    • getBlogCategories(locale) — all categories ordered by displayOrder
  3. blog-authors.ts:

    • getBlogAuthorByName(name, locale) — for /api/blog-author
  4. 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.io or cdn.sanity.io
  • export const revalidate = 3600 visible in build output
  • Blog search works
  • Author modal works

Depends on: Issue #384 , #385

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)
  • BlogPostRenderer is a server component — no Tiptap JS bundle on public pages

Out of scope

  • Admin UI
  • Sanity cleanup
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions