Add category SEO description for storefront category pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
835c8a0ac1
commit
b661877d77
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Category" ADD COLUMN IF NOT EXISTS "seoDescription" TEXT NOT NULL DEFAULT '';
|
||||||
@@ -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])
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user