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:
co-authored by
Cursor
parent
df17925d3a
commit
c4367d8eb0
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user