Skip to content

(SP: 1) [Frontend] Fix 500 error on blog post pages#383

Merged
ViktorSvertoka merged 2 commits intodevelopfrom
sl/feat/blog-admin
Mar 4, 2026
Merged

(SP: 1) [Frontend] Fix 500 error on blog post pages#383
ViktorSvertoka merged 2 commits intodevelopfrom
sl/feat/blog-admin

Conversation

@LesiaUKR
Copy link
Collaborator

@LesiaUKR LesiaUKR commented Mar 3, 2026

Closes #384

Summary

  • Remove pt::text() from generateMetadata GROQ query — caused Sanity API crash when coalesce fallback returned raw object instead of Portable Text array
  • Replace revalidate + generateStaticParams (ISR/SSG) with force-dynamic (SSR) — ISR was incompatible with useSearchParams() in shared layout components (no Suspense boundaries)
  • Add setRequestLocale() in page and PostDetails for correct next-intl integration

Root cause

PR #367 (Vercel cost optimization) changed blog post page from SSR to ISR/SSG. This exposed two issues:

  1. pt::text() in generateMetadata crashed during ISR revalidation
  2. Shared layout components using useSearchParams() without <Suspense> caused BAILOUT_TO_CLIENT_SIDE_RENDERING error on SSG pages

Files changed

  • frontend/app/[locale]/blog/[slug]/page.tsx
  • frontend/app/[locale]/blog/[slug]/PostDetails.tsx
  • frontend/client.ts

Test plan

  • Blog post pages load without 500 error (production build + next start)
  • Verify on develop environment after deploy
  • Check all blog posts render correctly across all locales (uk/en/pl)

Summary by CodeRabbit

  • New Features

    • Added comprehensive blog management infrastructure with support for posts, authors, and categories across multiple languages.
    • Enhanced internationalization support with improved locale-based content retrieval.
  • Changes

    • Updated blog page rendering to deliver dynamic content.
    • Improved locale handling for better content localization.

@vercel
Copy link
Contributor

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
devlovers-net Ignored Ignored Preview Mar 3, 2026 10:57pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive blog database schema using drizzle-orm with multi-language support, defines seven interconnected tables with proper relationships, and updates existing blog pages to enforce stricter locale handling by removing fallback chains in GROQ queries and adding explicit locale context via setRequestLocale. Additionally, the slug page transitions from static/incremental rendering to force-dynamic rendering.

Changes

Cohort / File(s) Summary
Blog Database Schema
frontend/db/schema/blog.ts, frontend/drizzle/0027_material_bloodscream.sql, frontend/db/schema/index.ts, frontend/drizzle/meta/_journal.json
Introduces complete blog system schema: blogCategories, blogAuthors, blogPosts tables with corresponding translation tables for multi-language support; includes composite foreign keys, cascading deletions, and per-table metadata (timestamps, slugs, display ordering); SQL migration creates normalized schema with JSONB for social media and post body content.
Dynamic Page Rendering
frontend/app/[locale]/blog/[slug]/page.tsx
Removes static page generation (generateStaticParams removed, revalidate removed); adds export const dynamic = 'force-dynamic' to force runtime rendering; invokes setRequestLocale(locale) before rendering; metadata generation now uses strict fallback chain for title field without generic fallback.
Locale-Aware Components & Fallback Removal
frontend/app/[locale]/blog/[slug]/PostDetails.tsx, frontend/app/[locale]/blog/category/[category]/page.tsx, frontend/app/[locale]/blog/page.tsx, frontend/app/api/blog-search/route.ts
Adds setRequestLocale(locale) call in PostDetails component; removes trailing generic fallback arguments from GROQ coalesce expressions across all blog pages/API routes, tightening localization behavior such that missing locale-specific values return undefined rather than falling back to unlocalized variants.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Sanity #145: Modifies same blog locale handling and GROQ projections in blog page files with related fallback field changes.
  • Sanity #119: Updates blog page components (PostDetails and page.tsx) to change localization handling with GROQ projection modifications.
  • Sanity #209: Alters GROQ queries and locale handling in blog PostDetails and category pages, touching similar query structure changes.

Suggested labels

bug

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 A schema born with tables new,
Blog posts in languages, translations too,
Locales set before the fetch begins,
No more fallbacks—strict locale wins!
Dynamic pages dance in time,
A rabbit's refactor, simply sublime! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: resolving a 500 error on blog post pages, which aligns perfectly with the PR's primary objective of fixing crashes caused by incorrect GROQ query handling and caching strategy mismatches.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sl/feat/blog-admin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/`[locale]/blog/[slug]/page.tsx:
- Around line 19-26: Remove the trailing bare "title" fallback from the GROQ
coalesce expressions (i.e., change coalesce(title[$locale],
title[lower($locale)], title.uk, title.en, title.pl, title) to
coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl))
in all queries that build localized titles (the queries used by generateMetadata
and PostDetails), and update generateMetadata to use a type guard: if typeof
post?.title !== 'string' then fallback to 'Post' (return { title: 'Post' }),
ensuring metadata.title is always a string at runtime.

In `@frontend/client.ts`:
- Around line 3-7: The shared Sanity client exported as client is currently
created with useCdn: false which disables CDN globally; revert the createClient
call in the client export (the export const client = createClient(...)
initialization) to use the default/true CDN setting, and instead scope uncached
reads by calling client.withConfig({ useCdn: false }) where needed (e.g., the
existing pattern in blog-search/route.ts); update any tests or callers that
assumed global uncached behavior to use .withConfig({ useCdn: false }) on the
client instance.

In `@frontend/db/schema/blog.ts`:
- Around line 2-12: The schema is missing indexes on several foreign-key
columns; add the index import from 'drizzle-orm/pg-core' and create indexes for
each FK: add an index on blogPosts.authorId (symbol: blogPosts + authorId), on
blogPostTranslations.postId (blogPostTranslations + postId), on
blogPostCategories.postId and blogPostCategories.categoryId (blogPostCategories
+ postId/categoryId), on blogCategoryTranslations.categoryId
(blogCategoryTranslations + categoryId), and on blogAuthorTranslations.authorId
(blogAuthorTranslations + authorId); ensure you import index alongside other
imports and call index(...) where tables are defined so those FK columns get DB
indexes.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2789166 and 051f658.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • frontend/app/[locale]/blog/[slug]/PostDetails.tsx
  • frontend/app/[locale]/blog/[slug]/page.tsx
  • frontend/client.ts
  • frontend/db/schema/blog.ts
  • frontend/db/schema/index.ts
  • frontend/drizzle/0027_material_bloodscream.sql
  • frontend/drizzle/meta/0027_snapshot.json
  • frontend/drizzle/meta/_journal.json

Comment on lines 19 to 26
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title)
}`,
{ slug, locale }
);

return {
title: post?.title || 'Post',
description: post?.description || undefined,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/app/[locale]/blog/[slug]/page.tsx | head -40

Repository: DevLoversTeam/devlovers.net

Length of output: 1198


🏁 Script executed:

rg -n "coalesce.*title" frontend/app --type=tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

rg -n "metadata.*title|title.*metadata" frontend/app --type=tsx -A 3 -B 3

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

find . -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.json" | grep -E "(schema|type|sanity)" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 780


🏁 Script executed:

fd -e ts -e tsx -e js "schema|type" . | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 293


🏁 Script executed:

rg "title.*\[" --type-list | head -5

Repository: DevLoversTeam/devlovers.net

Length of output: 460


🏁 Script executed:

rg "title\[" -A 2 -B 2 | head -50

Repository: DevLoversTeam/devlovers.net

Length of output: 2836


🏁 Script executed:

find . -type d -name "sanity" -o -name "schemas" -o -name "types"

Repository: DevLoversTeam/devlovers.net

Length of output: 108


🏁 Script executed:

find . -path "*/studio/*" -name "*.ts" -o -path "*/schemas/*" -name "*.ts" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 314


🏁 Script executed:

fd "post" studio/ | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 97


🏁 Script executed:

cat studio/schemas/blog/post.ts 2>/dev/null || cat studio/schemaTypes/blog.ts 2>/dev/null || find studio -name "*post*" -type f

Repository: DevLoversTeam/devlovers.net

Length of output: 97


🏁 Script executed:

rg "export.*post|type.*post|interface.*post" studio/ -A 5 | head -80

Repository: DevLoversTeam/devlovers.net

Length of output: 176


🏁 Script executed:

cat -n studio/schemaTypes/post.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 3423


🏁 Script executed:

rg "typeof.*title|title.*string" frontend/app/\[locale\]/blog/ -B 2 -A 2

Repository: DevLoversTeam/devlovers.net

Length of output: 1133


🏁 Script executed:

cat -n frontend/app/\[locale\]/blog/\[slug\]/PostDetails.tsx | head -60

Repository: DevLoversTeam/devlovers.net

Length of output: 1980


Fix GROQ fallback pattern across metadata queries to ensure string titles.

The title field in Sanity is defined as an object with language-specific subfields (en, pl, uk). The GROQ queries end with a bare title fallback, which returns the entire object when localized versions are missing. This causes post?.title to be an object rather than a string, violating Next.js metadata's type contract and creating potential runtime issues.

Remove the bare title fallback from all GROQ queries that include it:

  • frontend/app/[locale]/blog/[slug]/page.tsx (line 19)
  • frontend/app/[locale]/blog/page.tsx
  • frontend/app/[locale]/blog/category/[category]/page.tsx
  • frontend/app/[locale]/blog/[slug]/PostDetails.tsx (two queries)
  • frontend/app/api/blog-search/route.ts

Add a type guard in generateMetadata:

+  const title = typeof post?.title === 'string' ? post.title : 'Post';
   return {
-    title: post?.title || 'Post',
+    title,
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title)
}`,
{ slug, locale }
);
return {
title: post?.title || 'Post',
description: post?.description || undefined,
};
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title)
}`,
{ slug, locale }
);
const title = typeof post?.title === 'string' ? post.title : 'Post';
return {
title,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/blog/[slug]/page.tsx around lines 19 - 26, Remove the
trailing bare "title" fallback from the GROQ coalesce expressions (i.e., change
coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl,
title) to coalesce(title[$locale], title[lower($locale)], title.uk, title.en,
title.pl)) in all queries that build localized titles (the queries used by
generateMetadata and PostDetails), and update generateMetadata to use a type
guard: if typeof post?.title !== 'string' then fallback to 'Post' (return {
title: 'Post' }), ensuring metadata.title is always a string at runtime.

Comment on lines 3 to 7
export const client = createClient({
projectId: '6y9ive6v',
dataset: 'production',
useCdn: true,
useCdn: false,
apiVersion: '2025-11-29',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all imports of the shared `@/client`
rg -n -C2 "from ['\"]@/client['\"]|import.*@/client" --type=ts --type=tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Also check if uncachedClient or similar patterns already exist
rg -n "uncached|withConfig.*useCdn" --type=ts --type=tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Look for context on why useCdn was set to false
git log -p --all -S "useCdn" -- "frontend/client.ts" | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Find all TypeScript files and grep for `@/client` imports
rg -n "@/client" --type ts

Repository: DevLoversTeam/devlovers.net

Length of output: 950


🏁 Script executed:

#!/bin/bash
# List TypeScript/TSX files to understand structure
fd -e ts -e tsx | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 1006


🏁 Script executed:

#!/bin/bash
# Search for useCdn mentions and uncached patterns
rg -n "useCdn|uncached" --type ts

Repository: DevLoversTeam/devlovers.net

Length of output: 184


🏁 Script executed:

#!/bin/bash
# Check blog-search route context - it already uses withConfig pattern
sed -n '15,30p' frontend/app/api/blog-search/route.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 370


🏁 Script executed:

#!/bin/bash
# Check layout.tsx to understand its client usage
grep -n "client\." frontend/app/[locale]/layout.tsx | head -10

Repository: DevLoversTeam/devlovers.net

Length of output: 135


🏁 Script executed:

#!/bin/bash
# Look at the full layout usage to understand scope
sed -n '1,50p' frontend/app/[locale]/layout.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 1625


Scope uncached Sanity reads instead of disabling CDN globally.

At Line 6, changing the shared client to useCdn: false impacts every consumer of @/client, including the root layout which caches blog category fetches. The codebase already implements the scoped .withConfig({ useCdn: false }) pattern in blog-search/route.ts (line 22). Revert the global setting and apply uncached reads only where necessary.

💡 Proposed refactor
 export const client = createClient({
   projectId: '6y9ive6v',
   dataset: 'production',
-  useCdn: false,
+  useCdn: true,
   apiVersion: '2025-11-29',
 });

The .withConfig({ useCdn: false }) pattern in blog-search/route.ts should remain unchanged.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const client = createClient({
projectId: '6y9ive6v',
dataset: 'production',
useCdn: true,
useCdn: false,
apiVersion: '2025-11-29',
export const client = createClient({
projectId: '6y9ive6v',
dataset: 'production',
useCdn: true,
apiVersion: '2025-11-29',
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/client.ts` around lines 3 - 7, The shared Sanity client exported as
client is currently created with useCdn: false which disables CDN globally;
revert the createClient call in the client export (the export const client =
createClient(...) initialization) to use the default/true CDN setting, and
instead scope uncached reads by calling client.withConfig({ useCdn: false })
where needed (e.g., the existing pattern in blog-search/route.ts); update any
tests or callers that assumed global uncached behavior to use .withConfig({
useCdn: false }) on the client instance.

Comment on lines +2 to +12
import {
boolean,
integer,
jsonb,
pgTable,
primaryKey,
text,
timestamp,
uuid,
varchar,
} from 'drizzle-orm/pg-core';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

# First, let's check if the file exists and its structure
fd -t f 'blog.ts' frontend/

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

frontend/db/schema/blog.ts

Script executed:

# Let's also check the migration file
fd -t f '0027_material_bloodscream.sql' frontend/

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

frontend/drizzle/0027_material_bloodscream.sql

Script executed:

# Check drizzle config to understand schema location
fd -t f 'drizzle-config' frontend/

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0


Script executed:

# Read the schema file to see current state
cat -n frontend/db/schema/blog.ts

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

     1	import { relations } from 'drizzle-orm';
     2	import {
     3	  boolean,
     4	  integer,
     5	  jsonb,
     6	  pgTable,
     7	  primaryKey,
     8	  text,
     9	  timestamp,
    10	  uuid,
    11	  varchar,
    12	} from 'drizzle-orm/pg-core';
    13	
    14	// ── Blog Categories ──────────────────────────────────────────────
    15	
    16	export const blogCategories = pgTable('blog_categories', {
    17	  id: uuid('id').defaultRandom().primaryKey(),
    18	  slug: varchar('slug', { length: 50 }).notNull().unique(),
    19	  displayOrder: integer('display_order').notNull().default(0),
    20	  createdAt: timestamp('created_at', { withTimezone: true })
    21	    .notNull()
    22	    .defaultNow(),
    23	});
    24	
    25	export const blogCategoryTranslations = pgTable(
    26	  'blog_category_translations',
    27	  {
    28	    categoryId: uuid('category_id')
    29	      .notNull()
    30	      .references(() => blogCategories.id, { onDelete: 'cascade' }),
    31	    locale: varchar('locale', { length: 5 }).notNull(),
    32	    title: text('title').notNull(),
    33	    description: text('description'),
    34	  },
    35	  table => ({
    36	    pk: primaryKey({ columns: [table.categoryId, table.locale] }),
    37	  })
    38	);
    39	
    40	// ── Blog Authors ─────────────────────────────────────────────────
    41	
    42	export const blogAuthors = pgTable('blog_authors', {
    43	  id: uuid('id').defaultRandom().primaryKey(),
    44	  slug: varchar('slug', { length: 100 }).notNull().unique(),
    45	  imageUrl: text('image_url'),
    46	  imagePublicId: text('image_public_id'),
    47	  socialMedia: jsonb('social_media').notNull().default([]),
    48	  displayOrder: integer('display_order').notNull().default(0),
    49	  createdAt: timestamp('created_at', { withTimezone: true })
    50	    .notNull()
    51	    .defaultNow(),
    52	  updatedAt: timestamp('updated_at', { withTimezone: true })
    53	    .notNull()
    54	    .defaultNow(),
    55	});
    56	
    57	export const blogAuthorTranslations = pgTable(
    58	  'blog_author_translations',
    59	  {
    60	    authorId: uuid('author_id')
    61	      .notNull()
    62	      .references(() => blogAuthors.id, { onDelete: 'cascade' }),
    63	    locale: varchar('locale', { length: 5 }).notNull(),
    64	    name: text('name').notNull(),
    65	    bio: text('bio'),
    66	    jobTitle: text('job_title'),
    67	    company: text('company'),
    68	    city: text('city'),
    69	  },
    70	  table => ({
    71	    pk: primaryKey({ columns: [table.authorId, table.locale] }),
    72	  })
    73	);
    74	
    75	// ── Blog Posts ───────────────────────────────────────────────────
    76	
    77	export const blogPosts = pgTable('blog_posts', {
    78	  id: uuid('id').defaultRandom().primaryKey(),
    79	  slug: varchar('slug', { length: 200 }).notNull().unique(),
    80	  authorId: uuid('author_id').references(() => blogAuthors.id, {
    81	    onDelete: 'set null',
    82	  }),
    83	  mainImageUrl: text('main_image_url'),
    84	  mainImagePublicId: text('main_image_public_id'),
    85	  tags: text('tags').array().notNull().default([]),
    86	  resourceLink: text('resource_link'),
    87	  publishedAt: timestamp('published_at', { withTimezone: true }),
    88	  scheduledPublishAt: timestamp('scheduled_publish_at', {
    89	    withTimezone: true,
    90	  }),
    91	  isPublished: boolean('is_published').notNull().default(false),
    92	  createdAt: timestamp('created_at', { withTimezone: true })
    93	    .notNull()
    94	    .defaultNow(),
    95	  updatedAt: timestamp('updated_at', { withTimezone: true })
    96	    .notNull()
    97	    .defaultNow(),
    98	});
    99	
   100	export const blogPostTranslations = pgTable(
   101	  'blog_post_translations',
   102	  {
   103	    postId: uuid('post_id')
   104	      .notNull()
   105	      .references(() => blogPosts.id, { onDelete: 'cascade' }),
   106	    locale: varchar('locale', { length: 5 }).notNull(),
   107	    title: text('title').notNull(),
   108	    body: jsonb('body'),
   109	  },
   110	  table => ({
   111	    pk: primaryKey({ columns: [table.postId, table.locale] }),
   112	  })
   113	);
   114	
   115	// ── Blog Post ↔ Category junction ───────────────────────────────
   116	
   117	export const blogPostCategories = pgTable(
   118	  'blog_post_categories',
   119	  {
   120	    postId: uuid('post_id')
   121	      .notNull()
   122	      .references(() => blogPosts.id, { onDelete: 'cascade' }),
   123	    categoryId: uuid('category_id')
   124	      .notNull()
   125	      .references(() => blogCategories.id, { onDelete: 'cascade' }),
   126	  },
   127	  table => ({
   128	    pk: primaryKey({ columns: [table.postId, table.categoryId] }),
   129	  })
   130	);
   131	
   132	// ── Relations ───────────────────────────────────────────────────
   133	
   134	export const blogCategoriesRelations = relations(
   135	  blogCategories,
   136	  ({ many }) => ({
   137	    translations: many(blogCategoryTranslations),
   138	    posts: many(blogPostCategories),
   139	  })
   140	);
   141	
   142	export const blogCategoryTranslationsRelations = relations(
   143	  blogCategoryTranslations,
   144	  ({ one }) => ({
   145	    category: one(blogCategories, {
   146	      fields: [blogCategoryTranslations.categoryId],
   147	      references: [blogCategories.id],
   148	    }),
   149	  })
   150	);
   151	
   152	export const blogAuthorsRelations = relations(blogAuthors, ({ many }) => ({
   153	  translations: many(blogAuthorTranslations),
   154	  posts: many(blogPosts),
   155	}));
   156	
   157	export const blogAuthorTranslationsRelations = relations(
   158	  blogAuthorTranslations,
   159	  ({ one }) => ({
   160	    author: one(blogAuthors, {
   161	      fields: [blogAuthorTranslations.authorId],
   162	      references: [blogAuthors.id],
   163	    }),
   164	  })
   165	);
   166	
   167	export const blogPostsRelations = relations(blogPosts, ({ one, many }) => ({
   168	  author: one(blogAuthors, {
   169	    fields: [blogPosts.authorId],
   170	    references: [blogAuthors.id],
   171	  }),
   172	  translations: many(blogPostTranslations),
   173	  categories: many(blogPostCategories),
   174	}));
   175	
   176	export const blogPostTranslationsRelations = relations(
   177	  blogPostTranslations,
   178	  ({ one }) => ({
   179	    post: one(blogPosts, {
   180	      fields: [blogPostTranslations.postId],
   181	      references: [blogPosts.id],
   182	    }),
   183	  })
   184	);
   185	
   186	export const blogPostCategoriesRelations = relations(
   187	  blogPostCategories,
   188	  ({ one }) => ({
   189	    post: one(blogPosts, {
   190	      fields: [blogPostCategories.postId],
   191	      references: [blogPosts.id],
   192	    }),
   193	    category: one(blogCategories, {
   194	      fields: [blogPostCategories.categoryId],
   195	      references: [blogCategories.id],
   196	    }),
   197	  })
   198	);

Script executed:

# Read the migration file
cat frontend/drizzle/0027_material_bloodscream.sql

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

CREATE TABLE "blog_author_translations" (
	"author_id" uuid NOT NULL,
	"locale" varchar(5) NOT NULL,
	"name" text NOT NULL,
	"bio" text,
	"job_title" text,
	"company" text,
	"city" text,
	CONSTRAINT "blog_author_translations_author_id_locale_pk" PRIMARY KEY("author_id","locale")
);
--> statement-breakpoint
CREATE TABLE "blog_authors" (
	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
	"slug" varchar(100) NOT NULL,
	"image_url" text,
	"image_public_id" text,
	"social_media" jsonb DEFAULT '[]'::jsonb NOT NULL,
	"display_order" integer DEFAULT 0 NOT NULL,
	"created_at" timestamp with time zone DEFAULT now() NOT NULL,
	"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
	CONSTRAINT "blog_authors_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "blog_categories" (
	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
	"slug" varchar(50) NOT NULL,
	"display_order" integer DEFAULT 0 NOT NULL,
	"created_at" timestamp with time zone DEFAULT now() NOT NULL,
	CONSTRAINT "blog_categories_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
CREATE TABLE "blog_category_translations" (
	"category_id" uuid NOT NULL,
	"locale" varchar(5) NOT NULL,
	"title" text NOT NULL,
	"description" text,
	CONSTRAINT "blog_category_translations_category_id_locale_pk" PRIMARY KEY("category_id","locale")
);
--> statement-breakpoint
CREATE TABLE "blog_post_categories" (
	"post_id" uuid NOT NULL,
	"category_id" uuid NOT NULL,
	CONSTRAINT "blog_post_categories_post_id_category_id_pk" PRIMARY KEY("post_id","category_id")
);
--> statement-breakpoint
CREATE TABLE "blog_post_translations" (
	"post_id" uuid NOT NULL,
	"locale" varchar(5) NOT NULL,
	"title" text NOT NULL,
	"body" jsonb,
	CONSTRAINT "blog_post_translations_post_id_locale_pk" PRIMARY KEY("post_id","locale")
);
--> statement-breakpoint
CREATE TABLE "blog_posts" (
	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
	"slug" varchar(200) NOT NULL,
	"author_id" uuid,
	"main_image_url" text,
	"main_image_public_id" text,
	"tags" text[] DEFAULT '{}' NOT NULL,
	"resource_link" text,
	"published_at" timestamp with time zone,
	"scheduled_publish_at" timestamp with time zone,
	"is_published" boolean DEFAULT false NOT NULL,
	"created_at" timestamp with time zone DEFAULT now() NOT NULL,
	"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
	CONSTRAINT "blog_posts_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
ALTER TABLE "blog_author_translations" ADD CONSTRAINT "blog_author_translations_author_id_blog_authors_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."blog_authors"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "blog_category_translations" ADD CONSTRAINT "blog_category_translations_category_id_blog_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."blog_categories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "blog_post_categories" ADD CONSTRAINT "blog_post_categories_post_id_blog_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."blog_posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "blog_post_categories" ADD CONSTRAINT "blog_post_categories_category_id_blog_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."blog_categories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "blog_post_translations" ADD CONSTRAINT "blog_post_translations_post_id_blog_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."blog_posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "blog_posts" ADD CONSTRAINT "blog_posts_author_id_blog_authors_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."blog_authors"("id") ON DELETE set null ON UPDATE no action;

Script executed:

# Verify Drizzle index syntax and check if there are other unindexed FKs
rg -n 'references\(' frontend/db/schema/blog.ts | head -20

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

30:      .references(() => blogCategories.id, { onDelete: 'cascade' }),
62:      .references(() => blogAuthors.id, { onDelete: 'cascade' }),
80:  authorId: uuid('author_id').references(() => blogAuthors.id, {
105:      .references(() => blogPosts.id, { onDelete: 'cascade' }),
122:      .references(() => blogPosts.id, { onDelete: 'cascade' }),
125:      .references(() => blogCategories.id, { onDelete: 'cascade' }),

Add FK indexes across all foreign key columns for consistent performance at scale.

The schema has 6 foreign key columns without supporting indexes: blog_posts.author_id, blog_post_categories.post_id and category_id, blog_post_translations.post_id, blog_category_translations.category_id, and blog_author_translations.author_id. Missing indexes on FK columns causes avoidable sequential scans during joins and constraint validation on larger datasets.

Add the index import and define indexes for all FK columns:

Proposed comprehensive fix
 import {
   boolean,
+  index,
   integer,
   jsonb,
   pgTable,
   primaryKey,
   text,
   timestamp,
   uuid,
   varchar,
 } from 'drizzle-orm/pg-core';

Apply the index pattern to each table:

  • blogPosts (line 77): index on authorId
  • blogPostTranslations (line 100): index on postId
  • blogPostCategories (line 117): indexes on both postId and categoryId
  • blogCategoryTranslations (line 25): index on categoryId
  • blogAuthorTranslations (line 57): index on authorId
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/db/schema/blog.ts` around lines 2 - 12, The schema is missing
indexes on several foreign-key columns; add the index import from
'drizzle-orm/pg-core' and create indexes for each FK: add an index on
blogPosts.authorId (symbol: blogPosts + authorId), on
blogPostTranslations.postId (blogPostTranslations + postId), on
blogPostCategories.postId and blogPostCategories.categoryId (blogPostCategories
+ postId/categoryId), on blogCategoryTranslations.categoryId
(blogCategoryTranslations + categoryId), and on blogAuthorTranslations.authorId
(blogAuthorTranslations + authorId); ensure you import index alongside other
imports and call index(...) where tables are defined so those FK columns get DB
indexes.

@LesiaUKR LesiaUKR changed the base branch from main to develop March 3, 2026 22:59
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/app/[locale]/blog/[slug]/PostDetails.tsx (1)

379-379: Consider removing duplicate request-locale setup.

setRequestLocale(locale) is already called in the parent page component, so keeping it in one place would reduce redundancy and avoid split ownership of request-scoped setup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/blog/[slug]/PostDetails.tsx at line 379, Remove the
duplicate request-locale setup by deleting the setRequestLocale(locale) call
inside the PostDetails component; ensure the parent page component remains the
single place that invokes setRequestLocale(locale) so request-scoped locale
setup is not duplicated or split between components (look for setRequestLocale
usage in PostDetails and the parent page component and remove the call from
PostDetails).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@frontend/app/`[locale]/blog/[slug]/PostDetails.tsx:
- Line 379: Remove the duplicate request-locale setup by deleting the
setRequestLocale(locale) call inside the PostDetails component; ensure the
parent page component remains the single place that invokes
setRequestLocale(locale) so request-scoped locale setup is not duplicated or
split between components (look for setRequestLocale usage in PostDetails and the
parent page component and remove the call from PostDetails).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 051f658 and 68c8cf4.

📒 Files selected for processing (5)
  • frontend/app/[locale]/blog/[slug]/PostDetails.tsx
  • frontend/app/[locale]/blog/[slug]/page.tsx
  • frontend/app/[locale]/blog/category/[category]/page.tsx
  • frontend/app/[locale]/blog/page.tsx
  • frontend/app/api/blog-search/route.ts

@ViktorSvertoka ViktorSvertoka merged commit 635499e into develop Mar 4, 2026
7 checks passed
@ViktorSvertoka ViktorSvertoka deleted the sl/feat/blog-admin branch March 4, 2026 05:09
@LesiaUKR LesiaUKR restored the sl/feat/blog-admin branch March 4, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants