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
+1
View File
@@ -23,6 +23,7 @@ const BASE_URL_PATTERN = /^https:\/\/[a-z0-9.-]+(?::\d+)?\/?$/i;
const TEMPLATE_KEYS = [
'product',
'productCategory',
'blog',
'portfolio',
'storeItemByProduct',
+1 -1
View File
@@ -11,7 +11,7 @@ export type ResolvedSitemapConfig = {
templates: Required<
Pick<
import('../business-settings/business-settings.types').SitemapPathTemplatesConfig,
'product' | 'blog' | 'portfolio'
'product' | 'productCategory' | 'blog' | 'portfolio'
>
>;
};
+3
View File
@@ -98,6 +98,9 @@ export function resolveSitemapConfig(
templates: {
product:
stored?.templates?.product ?? DEFAULT_SITEMAP_PATH_TEMPLATES.product,
productCategory:
stored?.templates?.productCategory ??
DEFAULT_SITEMAP_PATH_TEMPLATES.productCategory,
blog: stored?.templates?.blog ?? DEFAULT_SITEMAP_PATH_TEMPLATES.blog,
portfolio:
stored?.templates?.portfolio ??
+1
View File
@@ -6,6 +6,7 @@ export const SITEMAP_URLSET_NS = 'http://www.sitemaps.org/schemas/sitemap/0.9';
/** Default storefront path templates (overridable via /meshkee/sitemap-config.json). */
export const DEFAULT_SITEMAP_PATH_TEMPLATES = {
product: '/products/{id}/{slug}',
productCategory: '/products/category/{slug}',
blog: '/blog/{id}/{slug}',
portfolio: '/portfolios/{id}/{slug}',
} as const;
+36 -1
View File
@@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { ContentStatus, Prisma } from '@prisma/client';
import { ContentStatus, MediaEntityType, Prisma } from '@prisma/client';
import { normalizeBusinessSettings } from '../business-settings/business-settings.util';
import type { BusinessModuleId } from '../business-settings/business-settings.types';
import { PrismaService } from '../prisma/prisma.service';
@@ -119,6 +119,13 @@ export class SitemapService {
}));
if (this.isModuleEnabled(enabledModules, 'products')) {
entries.push(
...(await this.loadProductCategories(
businessId,
config.baseUrl,
config.templates.productCategory,
)),
);
entries.push(
...(await this.loadPublishedProducts(businessId, config.baseUrl, config.templates.product)),
);
@@ -143,6 +150,34 @@ export class SitemapService {
return buildSitemapXml(entries);
}
private async loadProductCategories(
businessId: bigint,
baseUrl: string,
template: string,
): Promise<SitemapUrlEntry[]> {
const rows = await this.prisma.category.findMany({
where: {
businessId,
entityType: MediaEntityType.product,
isActive: true,
},
select: {
id: true,
slug: true,
updatedAt: true,
},
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
});
return rows.map((row) =>
this.toEntry(baseUrl, template, {
id: row.id,
slug: row.slug,
updatedAt: row.updatedAt,
}),
);
}
private async loadPublishedProducts(
businessId: bigint,
baseUrl: string,