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([]) packages String[] @default([])
/// Ordered form-builder defs: [{ id, labelFa, labelEn?, type: "text", required? }] /// Ordered form-builder defs: [{ id, labelFa, labelEn?, type: "text", required? }]
technicalFields Json @default("[]") technicalFields Json @default("[]")
/// Long-form SEO / category page copy shown on the storefront
seoDescription String @default("")
products Product[] products Product[]
@@index([storeId]) @@index([storeId])
+4
View File
@@ -44,6 +44,7 @@ export class CategoriesService {
packages: dto.packages ?? [], packages: dto.packages ?? [],
technicalFields: (dto.technicalFields ?? technicalFields: (dto.technicalFields ??
[]) as unknown as Prisma.InputJsonValue, []) as unknown as Prisma.InputJsonValue,
seoDescription: dto.seoDescription ?? '',
}, },
}); });
} }
@@ -71,6 +72,9 @@ export class CategoriesService {
dto.technicalFields as unknown as Prisma.InputJsonValue, 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) @Type(() => TechnicalFieldDto)
technicalFields?: TechnicalFieldDto[]; technicalFields?: TechnicalFieldDto[];
@IsOptional()
@IsString()
seoDescription?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
storeId?: string; storeId?: string;
@@ -117,4 +121,8 @@ export class UpdateCategoryDto {
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => TechnicalFieldDto) @Type(() => TechnicalFieldDto)
technicalFields?: TechnicalFieldDto[]; technicalFields?: TechnicalFieldDto[];
@IsOptional()
@IsString()
seoDescription?: string;
} }
+14 -4
View File
@@ -16,13 +16,17 @@ export class PublicController {
@Query('storeSlug') storeSlug?: string, @Query('storeSlug') storeSlug?: string,
@Query('name') name?: string, @Query('name') name?: string,
@Query('categoryId') categoryId?: string, @Query('categoryId') categoryId?: string,
@Query('minPrice') minPrice?: number, @Query('minPrice') minPrice?: string,
@Query('maxPrice') maxPrice?: number, @Query('maxPrice') maxPrice?: string,
) { ) {
const parsedCategoryId = const parsedCategoryId =
categoryId !== undefined && categoryId !== '' categoryId !== undefined && categoryId !== ''
? Number(categoryId) ? Number(categoryId)
: undefined; : undefined;
const parsedMin =
minPrice !== undefined && minPrice !== '' ? Number(minPrice) : undefined;
const parsedMax =
maxPrice !== undefined && maxPrice !== '' ? Number(maxPrice) : undefined;
return this.publicService.listProducts({ return this.publicService.listProducts({
storeId, storeId,
storeSlug, storeSlug,
@@ -31,8 +35,14 @@ export class PublicController {
parsedCategoryId !== undefined && Number.isFinite(parsedCategoryId) parsedCategoryId !== undefined && Number.isFinite(parsedCategoryId)
? parsedCategoryId ? parsedCategoryId
: undefined, : undefined,
minPrice, minPrice:
maxPrice, 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; nameEn: string;
sortOrder: number; sortOrder: number;
imageUrl: string | null; imageUrl: string | null;
seoDescription: string;
children: PublicCategoryTreeNode[]; children: PublicCategoryTreeNode[];
}; };
@@ -126,6 +127,7 @@ export class PublicService {
nameFa: true, nameFa: true,
nameEn: true, nameEn: true,
sortOrder: true, sortOrder: true,
seoDescription: true,
products: { products: {
where: { published: true }, where: { published: true },
take: 1, take: 1,
@@ -156,6 +158,7 @@ export class PublicService {
nameEn: node.nameEn, nameEn: node.nameEn,
sortOrder: node.sortOrder, sortOrder: node.sortOrder,
imageUrl: cover, imageUrl: cover,
seoDescription: node.seoDescription ?? '',
children: walk(node.id), children: walk(node.id),
}; };
}); });