Split product sitemap and use Farsi category URLs with ids.
sitemap.xml is now an index of main + products; category loc paths use /products/category/{id}/{nameFaSlug} with a public by-id lookup for storefronts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
c4367d8eb0
commit
ea40ae40d8
@@ -262,9 +262,12 @@ All routes are prefixed with `/api/v1`.
|
||||
| POST | `/auth/verify-otp` | Verify OTP (marks cell verified; no tokens) |
|
||||
| POST | `/auth/handoff/consume` | One-time SSO ticket → tokens (customer → business dashboard) |
|
||||
| GET | `/tenants/:host` | Resolve business from domain (`specialProductsSource`, `enabledModules`, `homeCharts`, `ePayment`) |
|
||||
| GET | `/tenants/:host/sitemap.xml` | XML sitemap (static + categories `/products/category/{slug}` + `/products|blog|portfolios/{id}/{faSlug}`) |
|
||||
| GET | `/tenants/:host/sitemap.xml` | Sitemap **index** → main + products child sitemaps |
|
||||
| GET | `/tenants/:host/sitemap-main.xml` | Static pages + `/products/category/{id}/{faSlug}` + blogs/portfolios |
|
||||
| GET | `/tenants/:host/sitemap-products.xml` | Published products `/products/{id}/{faSlug}` |
|
||||
| GET | `/tenants/:host/robots.txt` | robots.txt pointing to apex `/sitemap.xml` |
|
||||
| GET | `/tenants/:host/categories/by-slug/:slug` | Public category by slug (`?entityType=product` default) |
|
||||
| GET | `/tenants/:host/categories/by-id/:categoryId` | Public category by id (for category landing pages) |
|
||||
| GET | `/tenants/:host/categories/by-slug/:slug` | Public category by CMS slug |
|
||||
| GET | `/tenants/:host/products/by-id/:productId` | Public product detail by id (for `{id}/{nameFaSlug}` storefront routes) |
|
||||
| GET | `/tenants/:host/blogs/by-id/:blogId` | Public blog by id |
|
||||
| GET | `/tenants/:host/portfolios/by-id/:portfolioId` | Public portfolio by id |
|
||||
|
||||
@@ -116,13 +116,16 @@ Minimal example:
|
||||
- Include public static routes automatically; exclude login, checkout, cart, account, and admin paths.
|
||||
- **Canonical detail URLs (Meshkee default for all sites):**
|
||||
- Product: `/products/{id}/{nameFaSlug}` — build `nameFaSlug` from `nameFa` (fallback `title`); resolve page via `GET /tenants/{domain}/products/by-id/{id}` (slug segment is SEO-only; redirect to canonical if it drifts).
|
||||
- Product category: `/products/category/{categorySlug}` — use category `slug` from `GET /tenants/{domain}/categories?entityType=product`; resolve via `GET /tenants/{domain}/categories/by-slug/{slug}?entityType=product`, then list products with `categoryId`.
|
||||
- Product category: `/products/category/{categoryId}/{nameFaSlug}` — build slug from `nameFa` (fallback `name`); resolve via `GET /tenants/{domain}/categories/by-id/{id}`, then list products with `categoryId`.
|
||||
- Blog: `/blog/{id}/{titleSlug}` — `GET /tenants/{domain}/blogs/by-id/{id}`
|
||||
- Portfolio: `/portfolios/{id}/{titleFaSlug}` — `GET /tenants/{domain}/portfolios/by-id/{id}`
|
||||
- When linking from lists/cards, use the same `{id}/{slug}` shape for details (slugify Farsi title: spaces → `-`, keep Persian letters). Category links use `/products/category/{slug}`.
|
||||
- Omit `templates` in `sitemap-config.json` unless this site uses non-default paths (Meshkee sitemap defaults already use `{id}/{slug}` and `/products/category/{slug}`).
|
||||
- After deploy, business owner: **Website → Settings → Sync sitemap config** (imports static pages; does not rebuild XML by itself).
|
||||
- Dynamic URLs come from the CMS automatically. Hitting `/sitemap.xml` serves a Redis-cached XML that regenerates after CMS publish/update/delete (or when cache expires).
|
||||
- When linking from lists/cards, use the same `{id}/{slug}` shape for details (slugify Farsi title: spaces → `-`, keep Persian letters). Category links use `/products/category/{id}/{nameFaSlug}`.
|
||||
- Omit `templates` in `sitemap-config.json` unless this site uses non-default paths.
|
||||
- **Sitemap files (proxied by nginx — do not ship local copies):**
|
||||
- `/sitemap.xml` — sitemap **index**
|
||||
- `/sitemap-main.xml` — static pages + categories + blogs + portfolios
|
||||
- `/sitemap-products.xml` — published products (when products module is enabled)
|
||||
- `/robots.txt` — points at `/sitemap.xml`
|
||||
|
||||
### On-page SEO (every public page)
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
"SEO"
|
||||
],
|
||||
"summary": "XML sitemap for search engines",
|
||||
"description": "Published products, product categories, blogs, and portfolios for this tenant (respects enabled modules), plus static pages and URL templates from the synced website manifest (`GET https://{domain}/meshkee/sitemap-config.json`). Default detail paths: `/products/{id}/{nameFaSlug}`, `/products/category/{categorySlug}`, `/blog/{id}/{titleSlug}`, `/portfolios/{id}/{titleFaSlug}`. Proxied from `https://{domain}/sitemap.xml` on the storefront. Cached in Redis; refreshed after CMS writes and manifest sync.",
|
||||
"description": "Sitemap **index** for this tenant. Points to `/sitemap-main.xml` (static pages, product categories, blogs, portfolios) and `/sitemap-products.xml` (published products when the products module is enabled). Category paths: `/products/category/{id}/{nameFaSlug}`. Product paths: `/products/{id}/{nameFaSlug}`. Proxied from `https://{domain}/sitemap.xml`. Cached in Redis; refreshed after CMS writes and manifest sync.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/domain"
|
||||
@@ -226,6 +226,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/sitemap-main.xml": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"SEO"
|
||||
],
|
||||
"summary": "Main sitemap urlset (static + categories + blogs + portfolios)",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/domain"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sitemap XML (urlset)",
|
||||
"content": {
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/sitemap-products.xml": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"SEO"
|
||||
],
|
||||
"summary": "Products-only sitemap urlset",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/domain"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Sitemap XML (urlset) of published products",
|
||||
"content": {
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/robots.txt": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -446,12 +496,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/categories/by-id/{categoryId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Categories"
|
||||
],
|
||||
"summary": "Category by id (preferred for /products/category/{id}/{nameFaSlug} pages)",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/domain"
|
||||
},
|
||||
{
|
||||
"name": "categoryId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "entityType",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"product",
|
||||
"blog",
|
||||
"portfolio",
|
||||
"video"
|
||||
],
|
||||
"default": "product"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{ category } — use category.id as categoryId when listing products"
|
||||
},
|
||||
"404": {
|
||||
"description": "Category not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/categories/by-slug/{slug}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Categories"
|
||||
],
|
||||
"summary": "Category by slug (for /products/category/{categorySlug} pages)",
|
||||
"summary": "Category by CMS slug (legacy / lookup helper)",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/domain"
|
||||
|
||||
Reference in New Issue
Block a user