Include product category URLs in sitemaps and document by-slug lookup.

Adds /products/category/{slug} to default sitemap generation and a public categories/by-slug endpoint for storefront category pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-23 00:15:26 +03:30
co-authored by Cursor
parent df17925d3a
commit c4367d8eb0
15 changed files with 182 additions and 10 deletions
+9
View File
@@ -173,4 +173,13 @@ export class PublicCategoriesController {
list(@Param('host') host: string, @Query() query: ListCategoriesDto) {
return this.service.listPublic(host, query);
}
@Get('by-slug/:slug')
getBySlug(
@Param('host') host: string,
@Param('slug') slug: string,
@Query() query: ListCategoriesDto,
) {
return this.service.getPublicBySlug(host, slug, query.entityType);
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { SitemapModule } from '../sitemap/sitemap.module';
import { TenantModule } from '../tenant/tenant.module';
import { CategoriesController, PublicCategoriesController } from './categories.controller';
import { CategoriesService } from './categories.service';
@@ -9,7 +10,7 @@ import { CategoryTechnicalFormService } from './category-technical-form.service'
import { CategoryVariationsService } from './category-variations.service';
@Module({
imports: [AuthModule, TenantModule],
imports: [AuthModule, SitemapModule, TenantModule],
controllers: [CategoriesController, PublicCategoriesController],
providers: [
CategoriesService,
+28
View File
@@ -8,6 +8,7 @@ import { MediaEntityType } from '@prisma/client';
import { AuthUser } from '../auth/auth.types';
import { PermissionsService } from '../auth/permissions.service';
import { PrismaService } from '../prisma/prisma.service';
import { SitemapService } from '../sitemap/sitemap.service';
import { TenantService } from '../tenant/tenant.service';
import { CreateCategoryDto, ListCategoriesDto, UpdateCategoryDto } from './dto/category.dto';
@@ -27,6 +28,7 @@ export class CategoriesService {
private readonly prisma: PrismaService,
private readonly permissions: PermissionsService,
private readonly tenant: TenantService,
private readonly sitemap: SitemapService,
) {}
async list(businessIdRaw: string, query: ListCategoriesDto, actor: AuthUser) {
@@ -65,6 +67,26 @@ export class CategoriesService {
};
}
async getPublicBySlug(host: string, slug: string, entityType?: MediaEntityType) {
const business = await this.tenant.resolveBusinessByDomain(host);
const resolvedType = entityType ?? MediaEntityType.product;
const category = await this.prisma.category.findFirst({
where: {
businessId: business.id,
entityType: resolvedType,
slug,
isActive: true,
},
});
if (!category) {
throw new NotFoundException('Category not found');
}
return { category: this.serialize(category) };
}
async create(businessIdRaw: string, dto: CreateCategoryDto, actor: AuthUser) {
const businessId = BigInt(businessIdRaw);
await this.assertPermission(businessId, actor.id, 'categories.create');
@@ -99,6 +121,8 @@ export class CategoriesService {
},
});
await this.sitemap.invalidateForBusiness(businessId);
return {
message: 'Category created successfully',
category: this.serialize(created),
@@ -178,6 +202,8 @@ export class CategoriesService {
},
});
await this.sitemap.invalidateForBusiness(businessId);
return {
message: 'Category updated successfully',
category: this.serialize(updated),
@@ -205,6 +231,8 @@ export class CategoriesService {
}),
]);
await this.sitemap.invalidateForBusiness(businessId);
return {
message: 'Category deleted successfully',
deletedIds: descendantIds.map((id) => id.toString()),
+4
View File
@@ -12,6 +12,7 @@ import {
resolveAiProvider,
} from '../common/ai-provider.util';
import { PrismaService } from '../prisma/prisma.service';
import { SitemapService } from '../sitemap/sitemap.service';
import { GenerateCategoriesDto } from './dto/category-ai.dto';
type AiCategoryNode = {
@@ -44,6 +45,7 @@ export class CategoryAiService {
private readonly config: ConfigService,
private readonly prisma: PrismaService,
private readonly permissions: PermissionsService,
private readonly sitemap: SitemapService,
) {}
async generateProductCategories(
@@ -67,6 +69,8 @@ export class CategoryAiService {
);
});
await this.sitemap.invalidateForBusiness(businessId);
return {
message: `Created ${created.length} categor${created.length === 1 ? 'y' : 'ies'} with AI.`,
categories: created.map((item) => this.serialize(item)),