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:
Alireza Hassani
2026-09-09 23:30:24 +03:30
co-authored by Cursor
parent db305c58a4
commit aaf7a83742
33 changed files with 1395 additions and 188 deletions
+63 -1
View File
@@ -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**.
+42 -5
View File
@@ -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)."
}
}
}