feat(seo): generate slugs for brands, categories and products, add GET /sitemap and the Postman collection

This commit is contained in:
MastersGame
2026-11-14 11:08:51 +01:00
parent d07c4df52d
commit 657e8a6b30
26 changed files with 750 additions and 12 deletions

View File

@@ -23,6 +23,8 @@ model Category {
name String
/// Display order among its siblings (category_position)
position Int @default(0)
/// URL identifier (SEO): generated once by the sync, never overwritten by it; null until generated
slug String? @unique
/// Displayed on the website (category_enabled_www)
webEnabled Boolean @default(true)
@@ -31,7 +33,8 @@ model Category {
parentId String?
children Category[] @relation("CategoryTree")
products Product[]
products Product[]
slugRedirects SlugRedirect[]
@@map("categories")
}
@@ -44,8 +47,11 @@ model Brand {
enabled Boolean @default(true)
hiboutikId Int @unique
name String
/// URL identifier (SEO): generated once by the sync, never overwritten by it; null until generated
slug String? @unique
products Product[]
products Product[]
slugRedirects SlugRedirect[]
@@map("brands")
}
@@ -84,6 +90,8 @@ model Product {
hiboutikUpdatedAt DateTime?
name String
price Decimal @db.Decimal(10, 2)
/// URL identifier (SEO): generated once by the sync, never overwritten by it; null until generated
slug String? @unique
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
brandId String?
@@ -92,11 +100,42 @@ model Product {
supplier Supplier? @relation(fields: [supplierId], references: [id], onDelete: SetNull)
supplierId String?
versions ProductVersion[]
versions ProductVersion[]
slugRedirects SlugRedirect[]
@@map("products")
}
/// Kind of entity a slug identifies. Slugs are unique per kind (each one has its own URL space).
enum SlugEntityType {
BRAND
CATEGORY
PRODUCT
}
/// Former slug of a brand, category or product, kept so that its old URL can be answered with a 301 redirect.
/// Exactly one of brandId / categoryId / productId is set, matching `entityType`. Deleted with its entity.
model SlugRedirect {
id String @id @default(uuid())
createdAt DateTime @default(now())
entityType SlugEntityType
slug String
brand Brand? @relation(fields: [brandId], references: [id], onDelete: Cascade)
brandId String?
category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId String?
product Product? @relation(fields: [productId], references: [id], onDelete: Cascade)
productId String?
@@unique([entityType, slug])
@@index([brandId])
@@index([categoryId])
@@index([productId])
@@map("slug_redirects")
}
/// Sellable version of a product: one per Hiboutik size, or a single one (hiboutikSizeId 0) for a product without sizes.
model ProductVersion {
id String @id @default(uuid())