(SP: 1) [Frontend] Fix 500 error on blog post pages#383
(SP: 1) [Frontend] Fix 500 error on blog post pages#383ViktorSvertoka merged 2 commits intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📝 WalkthroughWalkthroughThis 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 Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
frontend/app/[locale]/blog/[slug]/PostDetails.tsxfrontend/app/[locale]/blog/[slug]/page.tsxfrontend/client.tsfrontend/db/schema/blog.tsfrontend/db/schema/index.tsfrontend/drizzle/0027_material_bloodscream.sqlfrontend/drizzle/meta/0027_snapshot.jsonfrontend/drizzle/meta/_journal.json
| "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, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n frontend/app/[locale]/blog/[slug]/page.tsx | head -40Repository: DevLoversTeam/devlovers.net
Length of output: 1198
🏁 Script executed:
rg -n "coalesce.*title" frontend/app --type=tsxRepository: DevLoversTeam/devlovers.net
Length of output: 98
🏁 Script executed:
rg -n "metadata.*title|title.*metadata" frontend/app --type=tsx -A 3 -B 3Repository: 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 -20Repository: DevLoversTeam/devlovers.net
Length of output: 780
🏁 Script executed:
fd -e ts -e tsx -e js "schema|type" . | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 293
🏁 Script executed:
rg "title.*\[" --type-list | head -5Repository: DevLoversTeam/devlovers.net
Length of output: 460
🏁 Script executed:
rg "title\[" -A 2 -B 2 | head -50Repository: 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 -20Repository: DevLoversTeam/devlovers.net
Length of output: 314
🏁 Script executed:
fd "post" studio/ | head -20Repository: 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 fRepository: DevLoversTeam/devlovers.net
Length of output: 97
🏁 Script executed:
rg "export.*post|type.*post|interface.*post" studio/ -A 5 | head -80Repository: DevLoversTeam/devlovers.net
Length of output: 176
🏁 Script executed:
cat -n studio/schemaTypes/post.tsRepository: DevLoversTeam/devlovers.net
Length of output: 3423
🏁 Script executed:
rg "typeof.*title|title.*string" frontend/app/\[locale\]/blog/ -B 2 -A 2Repository: DevLoversTeam/devlovers.net
Length of output: 1133
🏁 Script executed:
cat -n frontend/app/\[locale\]/blog/\[slug\]/PostDetails.tsx | head -60Repository: 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.tsxfrontend/app/[locale]/blog/category/[category]/page.tsxfrontend/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.
| "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.
| export const client = createClient({ | ||
| projectId: '6y9ive6v', | ||
| dataset: 'production', | ||
| useCdn: true, | ||
| useCdn: false, | ||
| apiVersion: '2025-11-29', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all imports of the shared `@/client`
rg -n -C2 "from ['\"]@/client['\"]|import.*@/client" --type=ts --type=tsxRepository: 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=tsxRepository: 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 -100Repository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
#!/bin/bash
# Find all TypeScript files and grep for `@/client` imports
rg -n "@/client" --type tsRepository: DevLoversTeam/devlovers.net
Length of output: 950
🏁 Script executed:
#!/bin/bash
# List TypeScript/TSX files to understand structure
fd -e ts -e tsx | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 1006
🏁 Script executed:
#!/bin/bash
# Search for useCdn mentions and uncached patterns
rg -n "useCdn|uncached" --type tsRepository: 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.tsRepository: 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 -10Repository: 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.tsxRepository: 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.
| 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.
| import { | ||
| boolean, | ||
| integer, | ||
| jsonb, | ||
| pgTable, | ||
| primaryKey, | ||
| text, | ||
| timestamp, | ||
| uuid, | ||
| varchar, | ||
| } from 'drizzle-orm/pg-core'; |
There was a problem hiding this comment.
❓ 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.tsRepository: 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.sqlRepository: 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 -20Repository: 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 onauthorIdblogPostTranslations(line 100): index onpostIdblogPostCategories(line 117): indexes on bothpostIdandcategoryIdblogCategoryTranslations(line 25): index oncategoryIdblogAuthorTranslations(line 57): index onauthorId
🤖 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.
There was a problem hiding this comment.
🧹 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
📒 Files selected for processing (5)
frontend/app/[locale]/blog/[slug]/PostDetails.tsxfrontend/app/[locale]/blog/[slug]/page.tsxfrontend/app/[locale]/blog/category/[category]/page.tsxfrontend/app/[locale]/blog/page.tsxfrontend/app/api/blog-search/route.ts
Closes #384
Summary
pt::text()fromgenerateMetadataGROQ query — caused Sanity API crash when coalesce fallback returned raw object instead of Portable Text arrayrevalidate + generateStaticParams(ISR/SSG) withforce-dynamic(SSR) — ISR was incompatible withuseSearchParams()in shared layout components (no Suspense boundaries)setRequestLocale()in page and PostDetails for correct next-intl integrationRoot cause
PR #367 (Vercel cost optimization) changed blog post page from SSR to ISR/SSG. This exposed two issues:
pt::text()ingenerateMetadatacrashed during ISR revalidationuseSearchParams()without<Suspense>causedBAILOUT_TO_CLIENT_SIDE_RENDERINGerror on SSG pagesFiles changed
frontend/app/[locale]/blog/[slug]/page.tsxfrontend/app/[locale]/blog/[slug]/PostDetails.tsxfrontend/client.tsTest plan
next start)Summary by CodeRabbit
New Features
Changes