Add published user products to the storefront sitemap.

Expose /sitemap-user-products.xml when customer_products is enabled, invalidate on listing mutations, and proxy the new path from nginx provision scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-27 07:16:05 +03:30
co-authored by Cursor
parent fb04425df7
commit 0d9c2fd8ea
14 changed files with 144 additions and 5 deletions
+1
View File
@@ -18,6 +18,7 @@ export type ResolvedSitemapConfig = {
| 'video'
| 'instruction'
| 'workshop'
| 'userProduct'
>
>;
};
+3
View File
@@ -111,6 +111,9 @@ export function resolveSitemapConfig(
DEFAULT_SITEMAP_PATH_TEMPLATES.instruction,
workshop:
stored?.templates?.workshop ?? DEFAULT_SITEMAP_PATH_TEMPLATES.workshop,
userProduct:
stored?.templates?.userProduct ??
DEFAULT_SITEMAP_PATH_TEMPLATES.userProduct,
},
};
}
+2
View File
@@ -11,6 +11,7 @@ export const DEFAULT_SITEMAP_PATH_TEMPLATES = {
video: '/videos/{slug}',
instruction: '/instructions/{slug}',
workshop: '/workshops/{slug}',
userProduct: '/user-products/{slug}',
} as const;
export const WEBSITE_SITEMAP_CONFIG_PATH = '/meshkee/sitemap-config.json';
@@ -27,6 +28,7 @@ export const SITEMAP_DYNAMIC_SECTIONS = [
'videos',
'instructions',
'workshops',
'userProducts',
] as const;
export type SitemapDynamicSection = (typeof SITEMAP_DYNAMIC_SECTIONS)[number];
+9
View File
@@ -59,6 +59,15 @@ export class SitemapController {
await this.sendDynamic(host, 'workshops', res);
}
@Get('sitemap-user-products.xml')
@Header('Cache-Control', 'public, max-age=900')
async getUserProductsSitemap(
@Param('host') host: string,
@Res() res: Response,
) {
await this.sendDynamic(host, 'userProducts', res);
}
@Get('robots.txt')
@Header('Cache-Control', 'public, max-age=900')
async getRobots(@Param('host') host: string, @Res() res: Response) {
+37
View File
@@ -41,6 +41,7 @@ const SECTION_MODULE: Record<SitemapDynamicSection, BusinessModuleId> = {
videos: 'videos',
instructions: 'instructions',
workshops: 'workshops',
userProducts: 'customer_products',
};
const SECTION_FILE: Record<SitemapDynamicSection, string> = {
@@ -50,6 +51,7 @@ const SECTION_FILE: Record<SitemapDynamicSection, string> = {
videos: '/sitemap-videos.xml',
instructions: '/sitemap-instructions.xml',
workshops: '/sitemap-workshops.xml',
userProducts: '/sitemap-user-products.xml',
};
@Injectable()
@@ -258,6 +260,14 @@ export class SitemapService {
config.templates.workshop,
),
);
case 'userProducts':
return buildSitemapXml(
await this.loadPublishedUserProducts(
businessId,
config.baseUrl,
config.templates.userProduct,
),
);
}
}
@@ -458,6 +468,33 @@ export class SitemapService {
);
}
private async loadPublishedUserProducts(
businessId: bigint,
baseUrl: string,
template: string,
): Promise<SitemapUrlEntry[]> {
const rows = await this.prisma.userProduct.findMany({
where: {
businessId,
status: ContentStatus.published,
},
select: {
id: true,
slug: true,
updatedAt: true,
},
orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],
});
return rows.map((row) =>
this.toEntry(baseUrl, template, {
id: row.id,
slug: row.slug,
updatedAt: row.updatedAt,
}),
);
}
private toEntry(
baseUrl: string,
template: string,