Add category SEO description for storefront category pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-27 20:46:17 +03:30
co-authored by Cursor
parent 835c8a0ac1
commit b661877d77
6 changed files with 33 additions and 4 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Category" ADD COLUMN IF NOT EXISTS "seoDescription" TEXT NOT NULL DEFAULT '';
+2
View File
@@ -151,6 +151,8 @@ model Category {
packages String[] @default([])
/// Ordered form-builder defs: [{ id, labelFa, labelEn?, type: "text", required? }]
technicalFields Json @default("[]")
/// Long-form SEO / category page copy shown on the storefront
seoDescription String @default("")
products Product[]
@@index([storeId])
+4
View File
@@ -44,6 +44,7 @@ export class CategoriesService {
packages: dto.packages ?? [],
technicalFields: (dto.technicalFields ??
[]) as unknown as Prisma.InputJsonValue,
seoDescription: dto.seoDescription ?? '',
},
});
}
@@ -71,6 +72,9 @@ export class CategoriesService {
dto.technicalFields as unknown as Prisma.InputJsonValue,
}
: {}),
...(dto.seoDescription !== undefined
? { seoDescription: dto.seoDescription }
: {}),
},
});
}
+8
View File
@@ -72,6 +72,10 @@ export class CreateCategoryDto {
@Type(() => TechnicalFieldDto)
technicalFields?: TechnicalFieldDto[];
@IsOptional()
@IsString()
seoDescription?: string;
@IsOptional()
@IsString()
storeId?: string;
@@ -117,4 +121,8 @@ export class UpdateCategoryDto {
@ValidateNested({ each: true })
@Type(() => TechnicalFieldDto)
technicalFields?: TechnicalFieldDto[];
@IsOptional()
@IsString()
seoDescription?: string;
}
+14 -4
View File
@@ -16,13 +16,17 @@ export class PublicController {
@Query('storeSlug') storeSlug?: string,
@Query('name') name?: string,
@Query('categoryId') categoryId?: string,
@Query('minPrice') minPrice?: number,
@Query('maxPrice') maxPrice?: number,
@Query('minPrice') minPrice?: string,
@Query('maxPrice') maxPrice?: string,
) {
const parsedCategoryId =
categoryId !== undefined && categoryId !== ''
? Number(categoryId)
: undefined;
const parsedMin =
minPrice !== undefined && minPrice !== '' ? Number(minPrice) : undefined;
const parsedMax =
maxPrice !== undefined && maxPrice !== '' ? Number(maxPrice) : undefined;
return this.publicService.listProducts({
storeId,
storeSlug,
@@ -31,8 +35,14 @@ export class PublicController {
parsedCategoryId !== undefined && Number.isFinite(parsedCategoryId)
? parsedCategoryId
: undefined,
minPrice,
maxPrice,
minPrice:
parsedMin !== undefined && Number.isFinite(parsedMin)
? parsedMin
: undefined,
maxPrice:
parsedMax !== undefined && Number.isFinite(parsedMax)
? parsedMax
: undefined,
});
}
+3
View File
@@ -9,6 +9,7 @@ export type PublicCategoryTreeNode = {
nameEn: string;
sortOrder: number;
imageUrl: string | null;
seoDescription: string;
children: PublicCategoryTreeNode[];
};
@@ -126,6 +127,7 @@ export class PublicService {
nameFa: true,
nameEn: true,
sortOrder: true,
seoDescription: true,
products: {
where: { published: true },
take: 1,
@@ -156,6 +158,7 @@ export class PublicService {
nameEn: node.nameEn,
sortOrder: node.sortOrder,
imageUrl: cover,
seoDescription: node.seoDescription ?? '',
children: walk(node.id),
};
});