Add Schema.org JSON-LD, SEO meta overrides, and resilient bulk SMS.
Expose Organization/LocalBusiness and Product/BlogPosting jsonLd, optional seoMeta fields on content/categories, and continue mass SMS after per-recipient failures. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
db305c58a4
commit
aaf7a83742
@@ -319,11 +319,73 @@ These are **required on every crawlable page** (home, listing, product/blog/port
|
||||
**Open Graph (recommended)**
|
||||
- Set `og:title`, `og:description`, and `og:image` on important pages (home + detail pages) from CMS media when available.
|
||||
|
||||
### Site-wide Schema.org JSON-LD (Organization / LocalBusiness)
|
||||
|
||||
Business admins configure this under **Website → Settings → Structured data**. The API builds a ready `jsonLd` object for you.
|
||||
|
||||
1. `GET /tenants/{domain}/website/business-info`
|
||||
2. If `schema.enabled` and `schema.jsonLd` is an object, render once in the root layout (or every public page):
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
…paste schema.jsonLd…
|
||||
</script>
|
||||
```
|
||||
|
||||
In Next.js App Router, put it in the root layout via `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema.jsonLd) }} />` when `jsonLd` is non-null.
|
||||
|
||||
- Do **not** invent Organization fields locally when `jsonLd` is present — use the CMS object.
|
||||
- When `schema.enabled` is false, `jsonLd` is `null` — omit the script tag.
|
||||
- Tenant resolve also exposes `{ schema: { enabled, type } }` for lightweight checks; full payload is on business-info.
|
||||
- Social profile URLs from business profile are merged into `sameAs` automatically; admin can add extra https URLs.
|
||||
|
||||
### Product + blog Schema.org JSON-LD (auto)
|
||||
|
||||
When **Website → Settings → Structured data** is enabled, detail endpoints include a ready `schema.jsonLd` (no admin fields to edit):
|
||||
|
||||
| Page | Endpoint | `@type` |
|
||||
|------|----------|---------|
|
||||
| Product detail | `GET /tenants/{domain}/products/by-id/{id}` (or by slug) | `Product` (+ `Offer` when store min price exists) |
|
||||
| Blog detail | `GET /tenants/{domain}/blogs/{slug}` (or by id) | `BlogPosting` |
|
||||
|
||||
On the product/blog page:
|
||||
|
||||
```tsx
|
||||
{product.schema?.jsonLd ? (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(product.schema.jsonLd) }}
|
||||
/>
|
||||
) : null}
|
||||
```
|
||||
|
||||
- Emit **in addition to** the site-wide Organization/LocalBusiness script from business-info.
|
||||
- When structured data is disabled, `schema.jsonLd` is `null` — omit the tag.
|
||||
- Do not invent Product/Article fields locally when `jsonLd` is present.
|
||||
|
||||
### Per-page SEO meta overrides (optional)
|
||||
|
||||
CMS forms expose optional **SEO title** / **SEO description** on products, blogs, portfolios, videos, instructions, workshops, user-products, and categories.
|
||||
|
||||
Public detail payloads include:
|
||||
|
||||
- `seoMetaTitle` (`string | null`)
|
||||
- `seoMetaDescription` (`string | null`)
|
||||
|
||||
Website rules:
|
||||
|
||||
1. Prefer `seoMetaTitle` for `<title>` when non-null; else use the normal title/`nameFa`.
|
||||
2. Prefer `seoMetaDescription` for `<meta name="description">` when non-null; else summary/abstract/about.
|
||||
3. Product/blog `schema.jsonLd` already prefers these overrides when set.
|
||||
4. Empty/null means “no override” — never show placeholder text as meta.
|
||||
|
||||
**Checklist before shipping a page**
|
||||
1. Unique `<title>` and meta description
|
||||
1. Unique `<title>` and meta description (use `seoMeta*` when present)
|
||||
2. One clear H1 + sensible H2 sections
|
||||
3. All content images have alt text
|
||||
4. Public URL is included via CMS sitemap and/or `sitemap-config.json` static pages
|
||||
5. Site-wide Organization/LocalBusiness JSON-LD from `business-info.schema.jsonLd` when enabled
|
||||
6. Product/blog detail pages emit `product.schema.jsonLd` / `blog.schema.jsonLd` when present
|
||||
|
||||
If OpenAPI and this brief conflict, **OpenAPI wins**.
|
||||
|
||||
|
||||
@@ -195,6 +195,18 @@
|
||||
"store_item"
|
||||
],
|
||||
"description": "Where homepage special-product carousels read from. Defaults to store_item when the store module is enabled, otherwise product."
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"description": "Site-wide Schema.org flags. Full jsonLd is on GET /tenants/{domain}/website/business-info.",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["Organization", "LocalBusiness"]
|
||||
}
|
||||
},
|
||||
"required": ["enabled", "type"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -530,7 +542,32 @@
|
||||
}
|
||||
},
|
||||
"socialMedia": { "type": "object" },
|
||||
"addresses": { "type": "array", "items": { "type": "object" } }
|
||||
"addresses": { "type": "array", "items": { "type": "object" } },
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"description": "Site-wide Schema.org settings + ready-to-emit jsonLd (Phase A).",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["Organization", "LocalBusiness"]
|
||||
},
|
||||
"name": { "type": "string", "nullable": true },
|
||||
"nameFa": { "type": "string", "nullable": true },
|
||||
"description": { "type": "string", "nullable": true },
|
||||
"priceRange": { "type": "string", "nullable": true },
|
||||
"sameAs": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"jsonLd": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"description": "Null when schema.enabled is false. Otherwise emit as application/ld+json on every public page (or at least the layout)."
|
||||
}
|
||||
},
|
||||
"required": ["enabled", "type", "sameAs", "jsonLd"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1001,7 +1038,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{ product, relatedProducts } — same shape as product-by-slug"
|
||||
"description": "{ product, relatedProducts }. `product.schema.jsonLd` is Product Schema.org JSON-LD when website structured data is enabled (null otherwise). Related products omit schema."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1080,7 +1117,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{ product, relatedProducts } — relatedProducts uses the product list-card shape (incl. store). Ranked by relativity (same category, then same brand) with availability beside it (in-stock first within each group). Up to 8 items; excludes the current product."
|
||||
"description": "{ product, relatedProducts }. `product.schema.jsonLd` is Product Schema.org JSON-LD when website structured data is enabled (null otherwise). Related products omit schema. relatedProducts uses the product list-card shape (incl. store). Ranked by relativity (same category, then same brand) with availability beside it (in-stock first within each group). Up to 8 items; excludes the current product."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1622,7 +1659,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{ blog }"
|
||||
"description": "{ blog }. `blog.schema.jsonLd` is BlogPosting Schema.org JSON-LD when website structured data is enabled (null otherwise)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1648,7 +1685,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "{ blog }"
|
||||
"description": "{ blog }. `blog.schema.jsonLd` is BlogPosting Schema.org JSON-LD when website structured data is enabled (null otherwise)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user