Use /{id}/{fa-slug} detail URLs in sitemaps and add public by-id lookups.

Default sitemap templates now emit SEO-friendly Farsi path segments with stable CMS ids; storefronts resolve content via new by-id public endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-22 23:43:25 +03:30
co-authored by Cursor
parent 1b16e79d3f
commit 7cef24336a
16 changed files with 495 additions and 25 deletions
+29
View File
@@ -171,6 +171,35 @@ export class PublicProductsController {
return this.service.listPublic(host, query);
}
@Get('by-id/:productId/variations')
async getVariationsById(
@Param('host') host: string,
@Param('productId') productId: string,
) {
const detail = await this.service.getPublicById(host, productId);
return this.variationValuesService.getPublicForProduct(
detail.product.businessId,
detail.product.id,
);
}
@Get('by-id/:productId/technical-info')
async getTechnicalInfoById(
@Param('host') host: string,
@Param('productId') productId: string,
) {
const detail = await this.service.getPublicById(host, productId);
return this.technicalInfoService.getPublicForProduct(
detail.product.businessId,
detail.product.id,
);
}
@Get('by-id/:productId')
getById(@Param('host') host: string, @Param('productId') productId: string) {
return this.service.getPublicById(host, productId);
}
@Get(':slug/variations')
async getVariations(@Param('host') host: string, @Param('slug') slug: string) {
const { businessId, productId } = await this.service.assertPublishedProductBySlug(
+33
View File
@@ -175,6 +175,39 @@ export class ProductsService {
throw new NotFoundException('Product not found');
}
return this.serializePublicDetail(businessId, product);
}
async getPublicById(host: string, productIdRaw: string) {
const business = await this.tenant.resolveBusinessByDomain(host);
const businessId = business.id;
let productId: bigint;
try {
productId = BigInt(productIdRaw);
} catch {
throw new NotFoundException('Product not found');
}
const product = await this.prisma.product.findFirst({
where: {
businessId,
id: productId,
status: ContentStatus.published,
},
include: productListInclude,
});
if (!product) {
throw new NotFoundException('Product not found');
}
return this.serializePublicDetail(businessId, product);
}
private async serializePublicDetail(
businessId: bigint,
product: Prisma.ProductGetPayload<{ include: typeof productListInclude }>,
) {
const [serializedList, relatedProducts] = await Promise.all([
this.serializePublicListItems(businessId, [product]),
this.loadRelatedPublicProducts(businessId, product),