Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import groq from 'groq';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import { getTranslations, setRequestLocale } from 'next-intl/server';

import { client } from '@/client';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
Expand Down Expand Up @@ -321,24 +321,24 @@ function hashString(input: string) {
const query = groq`
*[_type=="post" && slug.current==$slug][0]{
_id,
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl),
publishedAt,
"mainImage": mainImage.asset->url,
"categories": categories[]->title,
tags,
resourceLink,

"author": author->{
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl, company),
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl, jobTitle),
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl, city),
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl, bio),
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl),
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl),
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl),
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl),
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl),
"image": image.asset->url,
socialMedia[]{ _key, platform, url }
},

"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl)[]{
...,
_type == "image" => {
...,
Expand All @@ -350,16 +350,16 @@ const query = groq`
const recommendedQuery = groq`
*[_type=="post" && defined(slug.current) && slug.current != $slug]{
_id,
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl),
publishedAt,
"mainImage": mainImage.asset->url,
slug,
"categories": categories[]->title,
"author": author->{
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl),
"image": image.asset->url
},
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl)[]{
...,
_type == "image" => {
...,
Expand All @@ -376,6 +376,7 @@ export default async function PostDetails({
slug: string;
locale: string;
}) {
setRequestLocale(locale);
const t = await getTranslations({ locale, namespace: 'blog' });
const tNav = await getTranslations({ locale, namespace: 'navigation' });
const slugParam = String(slug || '').trim();
Expand Down
21 changes: 6 additions & 15 deletions frontend/app/[locale]/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import groq from 'groq';
import { setRequestLocale } from 'next-intl/server';

import { client } from '@/client';

import PostDetails from './PostDetails';

export const revalidate = 3600;

export async function generateStaticParams() {
const slugs = await client.fetch<string[]>(
groq`*[_type == "post" && defined(slug.current)][].slug.current`
);

return slugs.map(slug => ({
slug,
}));
}
export const dynamic = 'force-dynamic';

export async function generateMetadata({
params,
Expand All @@ -25,14 +16,13 @@ export async function generateMetadata({

const post = await client.fetch(
groq`*[_type == "post" && slug.current == $slug][0]{
"title": string(coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title))
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl)
}`,
{ slug, locale }
);

return {
title: post?.title || 'Post',
};
const title = typeof post?.title === 'string' ? post.title : 'Post';
return { title };
}

export default async function Page({
Expand All @@ -41,5 +31,6 @@ export default async function Page({
params: Promise<{ slug: string; locale: string }>;
}) {
const { slug, locale } = await params;
setRequestLocale(locale);
return <PostDetails slug={slug} locale={locale} />;
}
6 changes: 3 additions & 3 deletions frontend/app/[locale]/blog/category/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,17 @@ export default async function BlogCategoryPage({
*[_type == "post" && defined(slug.current) && $category in categories[]->title]
| order(coalesce(publishedAt, _createdAt) desc) {
_id,
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl),
slug,
publishedAt,
"categories": categories[]->title,
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl)[]{
...,
children[]{ text }
},
"mainImage": mainImage.asset->url,
"author": author->{
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl),
"image": image.asset->url
}
}
Expand Down
14 changes: 7 additions & 7 deletions frontend/app/[locale]/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,27 @@ export default async function BlogPage({
*[_type == "post" && defined(slug.current)]
| order(coalesce(publishedAt, _createdAt) desc) {
_id,
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl),
slug,
publishedAt,
tags,
resourceLink,

"categories": categories[]->title,

"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl)[]{
...,
children[]{
text
}
},
"mainImage": mainImage.asset->url,
"author": author->{
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl, company),
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl, jobTitle),
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl, city),
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl, bio),
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl),
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl),
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl),
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl),
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl),
"image": image.asset->url,
socialMedia[]{
_key,
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/api/blog-search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { client } from '@/client';
const searchQuery = groq`
*[_type == "post" && defined(slug.current)] | order(publishedAt desc) {
_id,
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl),
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl)[]{
...,
children[]{ text }
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { createClient } from '@sanity/client';
export const client = createClient({
projectId: '6y9ive6v',
dataset: 'production',
useCdn: false,
useCdn: true,
apiVersion: '2025-11-29',
Comment on lines 3 to 7

Copy link
Copy Markdown
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.

});
198 changes: 198 additions & 0 deletions frontend/db/schema/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { relations } from 'drizzle-orm';
import {
boolean,
integer,
jsonb,
pgTable,
primaryKey,
text,
timestamp,
uuid,
varchar,
} from 'drizzle-orm/pg-core';
Comment on lines +2 to +12

Copy link
Copy Markdown
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.


// ── Blog Categories ──────────────────────────────────────────────

export const blogCategories = pgTable('blog_categories', {
id: uuid('id').defaultRandom().primaryKey(),
slug: varchar('slug', { length: 50 }).notNull().unique(),
displayOrder: integer('display_order').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
});

export const blogCategoryTranslations = pgTable(
'blog_category_translations',
{
categoryId: uuid('category_id')
.notNull()
.references(() => blogCategories.id, { onDelete: 'cascade' }),
locale: varchar('locale', { length: 5 }).notNull(),
title: text('title').notNull(),
description: text('description'),
},
table => ({
pk: primaryKey({ columns: [table.categoryId, table.locale] }),
})
);

// ── Blog Authors ─────────────────────────────────────────────────

export const blogAuthors = pgTable('blog_authors', {
id: uuid('id').defaultRandom().primaryKey(),
slug: varchar('slug', { length: 100 }).notNull().unique(),
imageUrl: text('image_url'),
imagePublicId: text('image_public_id'),
socialMedia: jsonb('social_media').notNull().default([]),
displayOrder: integer('display_order').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow(),
});

export const blogAuthorTranslations = pgTable(
'blog_author_translations',
{
authorId: uuid('author_id')
.notNull()
.references(() => blogAuthors.id, { onDelete: 'cascade' }),
locale: varchar('locale', { length: 5 }).notNull(),
name: text('name').notNull(),
bio: text('bio'),
jobTitle: text('job_title'),
company: text('company'),
city: text('city'),
},
table => ({
pk: primaryKey({ columns: [table.authorId, table.locale] }),
})
);

// ── Blog Posts ───────────────────────────────────────────────────

export const blogPosts = pgTable('blog_posts', {
id: uuid('id').defaultRandom().primaryKey(),
slug: varchar('slug', { length: 200 }).notNull().unique(),
authorId: uuid('author_id').references(() => blogAuthors.id, {
onDelete: 'set null',
}),
mainImageUrl: text('main_image_url'),
mainImagePublicId: text('main_image_public_id'),
tags: text('tags').array().notNull().default([]),
resourceLink: text('resource_link'),
publishedAt: timestamp('published_at', { withTimezone: true }),
scheduledPublishAt: timestamp('scheduled_publish_at', {
withTimezone: true,
}),
isPublished: boolean('is_published').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow(),
});

export const blogPostTranslations = pgTable(
'blog_post_translations',
{
postId: uuid('post_id')
.notNull()
.references(() => blogPosts.id, { onDelete: 'cascade' }),
locale: varchar('locale', { length: 5 }).notNull(),
title: text('title').notNull(),
body: jsonb('body'),
},
table => ({
pk: primaryKey({ columns: [table.postId, table.locale] }),
})
);

// ── Blog Post ↔ Category junction ───────────────────────────────

export const blogPostCategories = pgTable(
'blog_post_categories',
{
postId: uuid('post_id')
.notNull()
.references(() => blogPosts.id, { onDelete: 'cascade' }),
categoryId: uuid('category_id')
.notNull()
.references(() => blogCategories.id, { onDelete: 'cascade' }),
},
table => ({
pk: primaryKey({ columns: [table.postId, table.categoryId] }),
})
);

// ── Relations ───────────────────────────────────────────────────

export const blogCategoriesRelations = relations(
blogCategories,
({ many }) => ({
translations: many(blogCategoryTranslations),
posts: many(blogPostCategories),
})
);

export const blogCategoryTranslationsRelations = relations(
blogCategoryTranslations,
({ one }) => ({
category: one(blogCategories, {
fields: [blogCategoryTranslations.categoryId],
references: [blogCategories.id],
}),
})
);

export const blogAuthorsRelations = relations(blogAuthors, ({ many }) => ({
translations: many(blogAuthorTranslations),
posts: many(blogPosts),
}));

export const blogAuthorTranslationsRelations = relations(
blogAuthorTranslations,
({ one }) => ({
author: one(blogAuthors, {
fields: [blogAuthorTranslations.authorId],
references: [blogAuthors.id],
}),
})
);

export const blogPostsRelations = relations(blogPosts, ({ one, many }) => ({
author: one(blogAuthors, {
fields: [blogPosts.authorId],
references: [blogAuthors.id],
}),
translations: many(blogPostTranslations),
categories: many(blogPostCategories),
}));

export const blogPostTranslationsRelations = relations(
blogPostTranslations,
({ one }) => ({
post: one(blogPosts, {
fields: [blogPostTranslations.postId],
references: [blogPosts.id],
}),
})
);

export const blogPostCategoriesRelations = relations(
blogPostCategories,
({ one }) => ({
post: one(blogPosts, {
fields: [blogPostCategories.postId],
references: [blogPosts.id],
}),
category: one(blogCategories, {
fields: [blogPostCategories.categoryId],
references: [blogCategories.id],
}),
})
);
1 change: 1 addition & 0 deletions frontend/db/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './blog';
export * from './categories';
export * from './emailVerificationTokens';
export * from './notifications';
Expand Down
Loading