Files
backend/docs/website-api/AI_PROMPT.md
T
Alireza HassaniandCursor e0cd2327eb Treat website meta tags as label + full HTML snippets.
Public business-info now exposes metaTags as { html }, matching how admins paste complete <meta> tags in Tags & Badges.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 18:39:19 +03:30

428 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Meshkee Website API — AI / designer brief
Copy everything below into a new AI chat when building a Meshkee storefront.
---
## System context (paste this)
You are building a **Meshkee business website (storefront)**. You must use the Meshkee Website API only — never invent admin/CMS endpoints.
**Canonical docs (always prefer these):**
- Hub: https://api.meshkee.com/docs/website
- OpenAPI: https://api.meshkee.com/docs/website/openapi.json
- Postman: https://api.meshkee.com/docs/website/Meshkee-Website-API.postman_collection.json
**API base URL:** `https://api.meshkee.com/api/v1`
(Optional alias if configured: `https://api.<WEBSITE_DOMAIN>/api/v1` — same backend.)
**This website’s apex domain:** `<WEBSITE_DOMAIN>`
(example: `sanihome.ir` — no `www.`, no `api.`, no `customer.`, no `business.`)
### Hard rules
1. Resolve tenant first: `GET /tenants/<WEBSITE_DOMAIN>` → save `businessId` from `id`.
2. All public content uses `/tenants/<WEBSITE_DOMAIN>/...` (no auth).
3. **Auth + checkout are not this website.** Login, register, OTP, shopping-cart process, addresses, and payment already exist for every Meshkee site at `https://customer.<WEBSITE_DOMAIN>`. Do **not** add `/login`, `/register`, `/checkout`, or `/cart` routes (or equivalent pages) on the storefront. Do **not** call `/businesses/{businessId}/cart` or `/cart/checkout` from this site.
4. **Detect login** via parent-domain cookies (see Shared login). Not token handoff, not an API SSO endpoint. If the shopper needs to sign in, **redirect** to the customer dashboard login — do not invent a login UI.
5. Storefront cart = header icon + quantity badge + mini-cart popup + **Continue** redirect (see Shopping cart). Server cart / orders / payment APIs are customer-dashboard only. Favorites may still use Bearer if cookies exist.
6. Customer-dashboard auth APIs (storefronts must **not** call these): register body includes `"domain": "<WEBSITE_DOMAIN>"`. If the cell already exists on another Meshkee site and the password differs, API returns `409` with `CELL_EXISTS_OTHER_SITE:...`. Retry register with `"acknowledgeExistingAccount": true` to link that account (profile unchanged; **password is replaced** with the new signup password), then complete SMS OTP.
7. Cell numbers are E.164 (`+98912...`).
8. Do not call dashboard/CMS routes (`/businesses/.../products` write APIs, media upload, domain-admin, etc.).
9. **Partner SMS** (`POST /public/sms/send`) is for external partner backends with an issued `X-Api-Key` only — not for normal storefront UI. See https://api.meshkee.com/docs/website/SMS.md
10. **Technical details (labels + values):** Product/user-product detail responses may include `technicalValues` with **values only** (`fieldId` + `textValue` / `optionId` / `optionIds` — **no field labels**). To render a label→value specs table you **must** call the matching `.../technical-info` endpoint and join `form.fields[].id` ↔ `values[].fieldId`. Never invent a separate “variation fields” or “category fields” public route — those do not exist on the website API.
11. **Torob:** Do **not** add a Next.js route for `/torob_api`. Meshkee nginx on the store apex proxies `POST /torob_api/v3/products` to the API. Only businesses with the **store** module **and** Store settings → Torob switch on return products (otherwise 404). Storefront UI must not call this endpoint.
12. **Production runtime (required):** Meshkee hosts many Next.js storefronts on one shared websites VM. Every Next site **must** set `output: "standalone"` in `next.config` (`.ts` / `.mjs` / `.js`). Deploy detects `.next/standalone/server.js`, points PM2 at that `server.js`, and **deletes the full `node_modules`**. Target RSS is **~80–120 MB**. Do **not** ship `next start` with a full `node_modules` runtime (that uses ~150–500+ MB and OOMs the host). Do **not** use `output: "export"` unless the project explicitly asks for a static export. Vinext apps (`vinext` in `package.json` / `vinext start`) are a separate intentional stack — do not pretend they use Next standalone.
### Production runtime (Next.js)
```ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Leaner production runtime: deploy uses .next/standalone and drops node_modules.
output: "standalone",
// ...images, rewrites, etc.
};
export default nextConfig;
```
After changing `next.config`, commit, push, and redeploy so PM2 switches to standalone. A correct deploy log says `standalone build detected` / `deploy ok (standalone)`; PM2 script is `.next/standalone/server.js`, not `node_modules/next/dist/bin/next`.
### Typical bootstrap sequence
1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`)
2. `GET /tenants/{domain}/website/favicon` → `faviconUrl` for `<link rel="icon">` / Next.js `metadata.icons` (falls back to `logoUrl` when no dedicated favicon). Also returns `logoUrl`, `logoDarkUrl`, `hasDedicatedFavicon`.
3. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`)
4. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc); each portfolio may include nullable `projectUrl` (external website link). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`).
5. Shopping cart on **this** site only: header icon + badge + mini-cart popup. Persist a **guest cart** and **Continue** to `https://customer.<WEBSITE_DOMAIN>` (see Shopping cart). Do not implement login or checkout here.
6. Bank payment callbacks stay on the store apex via nginx (`https://<WEBSITE_DOMAIN>/meshkee/payments/{gateway}/callback`) — infrastructure only. The website app does **not** implement payment or checkout pages; that UI is the customer dashboard.
### Favicon + logos
- `GET /tenants/{domain}/website/favicon` → `{ faviconUrl, logoUrl, logoDarkUrl, hasDedicatedFavicon }`
- Use `faviconUrl` in layout metadata (Next.js: `icons: [{ url: faviconUrl, type: 'image/png' }]` when set).
- When `hasDedicatedFavicon` is false, `faviconUrl` equals the light `logoUrl` — still safe to use as tab icon.
- Header/footer logos: `logoUrl` on light backgrounds, `logoDarkUrl` on dark (fallback to `logoUrl` in CSS when null).
### Shared login with customer dashboard (`customer.<WEBSITE_DOMAIN>`)
The Meshkee **customer dashboard** lives at `https://customer.<WEBSITE_DOMAIN>` (e.g. `customer.sgemed.com`). **That** app owns login, register, OTP, and password reset for every storefront. The shop and the dashboard stay in sync via **parent-domain cookies** — not a login page on `{apex}`.
**Storefront requirements:**
- Do **not** build login / register / OTP / forgot-password UI.
- If the shopper must authenticate, send them to `https://customer.<WEBSITE_DOMAIN>/login?redirect=…` (relative redirect path only, e.g. `/checkout/cart`).
- On page load: if the access-token cookie exists, treat them as logged in (Continue can skip login).
- Do not invent OAuth/SSO APIs. Do not use `POST /auth/handoff` (staff-only into `business.<WEBSITE_DOMAIN>`).
**Cookie contract** (written by the **customer dashboard** on login; storefronts only **read** them):
| Cookie name | Value |
|-------------|--------|
| `meshkee_customer_access_token` | access JWT (URL-encoded; may be chunked as `name_n` + `name_0`…) |
| `meshkee_customer_refresh_token` | refresh JWT (same) |
| Attribute | Value |
|-----------|--------|
| `Domain` | `.<WEBSITE_DOMAIN>` (leading dot), e.g. `.sgemed.com` |
| `Path` | `/` |
| `SameSite` | `Lax` |
| `Max-Age` | ~30 days (cleared on logout) |
| `Secure` | set on HTTPS |
API calls (favorites, etc.) still send `Authorization: Bearer <accessToken>` from that cookie. Cookies are **not** sent to the API for auth.
**Wrong:** a custom login page on the storefront, or calling `/auth/login` from shop UI.
**Right:** redirect to `customer.<WEBSITE_DOMAIN>` and read apex cookies.
### Shopping cart on the storefront
Website scope is a **mini-cart only**. The full cart, checkout, addresses, and payment run on the customer dashboard.
**UI**
- Header shopping-cart icon.
- Badge = sum of line quantities (hide or `0` when empty).
- Click → popup: lines, qty, remove, total, **Continue**.
**Add to cart** (always a **variant**, never a product id)
1. `GET /tenants/{domain}/store-items/by-product/{productId}` → `{ storeItem: { variants: [...] } | null }`.
2. If `storeItem` is null or `variants` is empty → hide Add to cart (not for sale).
3. **In-stock only:** `stockQuantity === null` (unlimited) or `stockQuantity > 0`. Disable / omit the rest.
4. **If more than one in-stock variant:** list them first (`label` is already joined, e.g. `قرمز · XL` from `selections`). The shopper **must pick one** — do not add until they choose.
5. **If exactly one in-stock variant:** add that variant (no picker required).
6. Push that variant into the guest cart. `id` = variant `id` (`storeItemVariantId`). If that `id` is already in the cart, **increment `quantity`** instead of adding a second line.
7. Selling price: `discountedPrice ?? price`. Optional `originalPrice` = `price` when `discountedPrice` is set.
Do not call `POST /businesses/{id}/cart/items` from the storefront. Mini-cart is local only.
**Guest cart** (must match the customer dashboard):
| | |
|--|--|
| Storage key / cookie name | `meshkee-guest-cart` |
| Persist | `localStorage` **and** cookie `Domain=.<WEBSITE_DOMAIN>`, `Path=/`, `SameSite=Lax`, ~30 days |
| Cookie size | if `encodeURIComponent(json)` is longer than ~3500 chars, skip the cookie and rely on the URL param |
Each line:
```json
{
"id": "<storeItemVariantId>",
"name": "...",
"slug": "...",
"price": 123000,
"originalPrice": 150000,
"image": "...",
"quantity": 1
}
```
`id` **must** be `storeItemVariantId` (not product id, not store-item id). `price` is IRT, numeric.
**URL payload** (always pass on Continue — cookies can be dropped or too large):
```js
function encodeGuestCart(items) {
const json = JSON.stringify(items)
return btoa(unescape(encodeURIComponent(json)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
```
Query/hash param name: `guestCart`.
**Continue** (replace `<WEBSITE_DOMAIN>`; `encoded = encodeGuestCart(items)`):
- Logged in (`meshkee_customer_access_token` present):
`https://customer.<WEBSITE_DOMAIN>/checkout/cart?guestCart=<encoded>`
- Not logged in:
`https://customer.<WEBSITE_DOMAIN>/login?redirect=${encodeURIComponent('/checkout/cart?guestCart=' + encoded)}`
The dashboard reads `guestCart` (query or hash), then `localStorage`, then the shared cookie, and syncs lines into the server cart after login.
**Wrong:** storefront `/cart` or `/checkout` pages; `POST /businesses/{id}/cart/checkout`; a shop-built login.
**Right:** local guest mini-cart → redirect to `customer.<WEBSITE_DOMAIN>`.
### Checkout & payments (customer dashboard — not this website)
OpenAPI **Cart** / checkout / payment-method routes are for `https://customer.<WEBSITE_DOMAIN>`, not storefront JavaScript.
Nginx on the shop apex still proxies bank callbacks (`https://<WEBSITE_DOMAIN>/meshkee/payments/{gateway}/callback`) to the API. Do not add a Next.js page for that path. After pay, the API redirects to the dashboard `returnUrl` (`/checkout/result?status=…`).
### User products (customer listings)
Public marketplace listings owned by customers — not catalog `products`.
- `GET /tenants/{domain}/user-products` — list published (`name`/`q`, `categoryId`, `cityId`, `countryId`, `condition`, `promoted`, pagination). Each item includes `id`, `pathSlug`, `titleFa`/`titleEn`.
- `GET /tenants/{domain}/user-products/by-id/{id}` — **preferred** for storefront pages (slug segment is SEO-only)
- `GET /tenants/{domain}/user-products/by-id/{id}/technical-info` — specs labels + values by id
- `GET /tenants/{domain}/user-products/{slug}` — legacy resolve by DB slug (still supported)
- `GET /tenants/{domain}/user-products/{slug}/technical-info` — legacy specs by slug
Use product categories from `GET /tenants/{domain}/categories?entityType=product` for filters. Creating/editing listings is customer-dashboard only (`/businesses/.../my-user-products`), not website-facing.
### Technical details / specs table (catalog products + user products)
**Do not** render only `technicalValues` from the detail endpoint — users will see bare values (e.g. `SAMP`, `1992`) with no labels.
| Detail page | Specs endpoint (labels + values) |
|-------------|----------------------------------|
| `GET .../products/{slug}` or `.../products/by-id/{id}` | `GET .../products/{slug}/technical-info` or `.../products/by-id/{id}/technical-info` |
| `GET .../user-products/by-id/{id}` (or `{slug}`) | `GET .../user-products/by-id/{id}/technical-info` (or `{slug}/technical-info`) |
**How to render:**
1. Call `technical-info` (same slug/id as the detail page).
2. For each `values[]` entry, find `form.fields` where `field.id === value.fieldId`.
3. Show `field.label` (or localized label if the site uses FA/EN) next to the value (`textValue`, or resolve option labels from `field.options` when `optionId` / `optionIds` are set).
4. Follow `form.fields` `sortOrder` for display order.
5. If `form` is null/empty or `values` is empty, hide the technical section.
**Wrong:** `GET .../product-category-variation-fields?categoryId=…` (does not exist → 404).
**Wrong:** expecting `fieldName` / `fieldNameFa` on each `technicalValues` item in the product detail response.
### Analytics (dashboard charts)
Page views are recorded **automatically** for roughly one count per page load:
- **Home** — `GET /tenants/{domain}/website/static-images?pageKey=home`
- **About / Contact / other static pages** — `GET .../static-images?pageKey=about` (or `contact`, etc.) → counted as `static_page` with `path=/about`
- **Content detail** — product, user-product, blog, portfolio, video, and store-item **detail** GETs
List APIs (`products`, `blogs`, …) and homepage sliders **do not** count (avoids multi-API inflation). Known bots (and empty User-Agent) are skipped. A browser refresh counts again.
Call `static-images?pageKey=…` on About/Contact (and similar) so those pages appear in dashboard statistics.
Optional explicit record (custom pages without static-images):
- `POST /tenants/{domain}/analytics/views` body `{ "kind": "home"|"static_page"|"blog_detail"|"product_detail"|..., "entityId"?: "...", "path"?: "/blog/my-post" }`
- `kind` values: `home`, `static_page`, `portfolio_list`, `portfolio_detail`, `product_list`, `product_detail`, `store_item_list`, `store_item_detail`, `blog_list`, `blog_detail`, `video_list`, `video_detail`, `user_product_list`, `user_product_detail` (legacy aliases `website`/`product`/`portfolio`/`blog` still accepted). Prefer detail / `home` / `static_page` for real page visits.
- Events kept ~6 months for charts; lifetime totals kept forever in counters.
- The business dashboard **Product views** chart counts both `product_detail` and `user_product_detail`.
- The **Pages** statistics row counts `static_page` (about, contact, …).
### Static images
Named slots the business dashboard can replace. Fetch once per page:
- `GET /tenants/{domain}/website/static-images` — all slots
- `GET /tenants/{domain}/website/static-images?pageKey=home` — slots for one page (`home`, `products`, `about`, …)
- `GET /tenants/{domain}/website/static-images/{key}` — one slot (e.g. `home-hero`)
Use `slot.key` in the placeholder. Match `pageKey` to the website page. For a single image: `images[0]?.url`. For a list: map `images` (if `itemCount` is set, that many images are expected). Each image may include `titleFa`, `titleEn`, `subtext`, and `linkUrl`. If `linkUrl` is set, wrap in `<a href={linkUrl}>`. Pick `titleFa` or `titleEn` from the site locale. If `images` is empty, keep the local fallback.
**Also publish a slot catalog on this website** so the business dashboard Refresh button can import keys from the main domain:
`GET https://<WEBSITE_DOMAIN>/meshkee/static-image-slots`
```json
{
"slots": [
{
"key": "slider",
"label": "Homepage slider",
"kind": "list",
"pageKey": "home",
"aspectRatio": "16:9",
"itemCount": null,
"recommendedWidth": 1440
},
{
"key": "home-side-banner",
"label": "Side banner",
"kind": "single",
"pageKey": "home",
"aspectRatio": "12:19",
"itemCount": 1,
"recommendedWidth": 480
}
]
}
```
`kind` is `single` (one image) or `list` (duplicatable). For a fixed row, set `itemCount` (e.g. `2`). Leave `itemCount` null for an unbounded slider. `aspectRatio` must look like `16:9`. Do not invent CMS/upload APIs.
### Sitemap + robots.txt (SEO)
Meshkee generates both files on the API. Nginx on the storefront proxies them — **do not** ship `public/sitemap.xml` or `public/robots.txt` (and do not add Next.js `app/sitemap.ts` / `app/robots.ts` that override these paths).
| URL on this site | Served by |
|------------------|-----------|
| `https://<WEBSITE_DOMAIN>/sitemap.xml` | Meshkee API (proxied) |
| `https://<WEBSITE_DOMAIN>/robots.txt` | Meshkee API (proxied) — includes `Sitemap: https://<WEBSITE_DOMAIN>/sitemap.xml` |
**What the website must publish** (for static pages only):
`GET https://<WEBSITE_DOMAIN>/meshkee/sitemap-config.json`
Prefer generating this **at build time from app routes** (scan public static pages). Do not hand-maintain long lists in prompts.
Minimal example:
```json
{
"baseUrl": "https://<WEBSITE_DOMAIN>",
"staticPages": [
{ "path": "/", "changefreq": "daily", "priority": 1.0 },
{ "path": "/about", "priority": 0.6 },
{ "path": "/contact", "priority": 0.5 }
]
}
```
- 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/{categoryId}/{nameFaSlug}` — build slug from `nameFa` (fallback `name`); resolve via `GET /tenants/{domain}/categories/by-id/{id}`, then list products with `categoryId`.
- User product (customer listing): `/user-products/{id}/{pathSlug}` — use `id` + `pathSlug` from the list/detail API (or slugify `titleFa` / `titleEn`); resolve via `GET /tenants/{domain}/user-products/by-id/{id}` (slug segment is SEO-only).
- Blog: `/blog/{slug}` — use the blog’s `slug` from the API; resolve via `GET /tenants/{domain}/blogs/{slug}` (or list + match). Prefer slug routes over id.
- Portfolio: `/portfolio/{slug}` — use the portfolio’s `slug` from the API; resolve via `GET /tenants/{domain}/portfolios/{slug}`.
- Instruction: `/instruction/{slug}` — use the instruction’s `slug` from the API; resolve via `GET /tenants/{domain}/instructions/{slug}`.
- Video: `/videos/{slug}` — use the video’s `slug` from the API.
- Workshop: `/workshops/{slug}` — use the workshop’s `slug` from the API.
- When linking from lists/cards, use the same slug-based detail paths. Category links use `/products/category/{id}/{nameFaSlug}`. User-product cards use `/user-products/{id}/{pathSlug}`.
- 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** (lists child sitemaps for enabled modules)
- `/sitemap-main.xml` — static pages + product categories
- `/sitemap-products.xml` — published products (when products module is enabled)
- `/sitemap-blogs.xml` — published blogs (when blog module is enabled)
- `/sitemap-portfolios.xml` — published portfolios (when portfolio module is enabled)
- `/sitemap-videos.xml` — published videos (when videos module is enabled)
- `/sitemap-instructions.xml` — published instructions (when instructions module is enabled)
- `/sitemap-workshops.xml` — published workshops (when workshops module is enabled)
- `/sitemap-user-products.xml` — published user products / customer listings (when `customer_products` module is enabled)
- `/robots.txt` — points at `/sitemap.xml`
### On-page SEO (every public page)
These are **required on every crawlable page** (home, listing, product/blog/portfolio detail, about, contact, etc.). Auth/checkout may be noindex.
**Document title (`<title>`)**
- Unique per page; include the primary topic + business name when space allows.
- Prefer CMS fields when present (`title`, `nameFa`/`nameEn`, product title, blog title). Never leave the Next.js default title.
**Meta description**
- Unique `<meta name="description" content="…">` on every public page (roughly 120–160 characters).
- Prefer CMS summary/excerpt/description when available; otherwise write a short page-specific sentence. Never empty, never identical across all pages.
**Headings**
- Exactly **one** `<h1>` per page — the main topic (product name, blog title, page title). Do not hide it with CSS-only “fake” headings.
- Use `<h2>` (then `<h3>`…) for real section structure under the H1. Do not skip levels for styling (don’t use H4 as a visual label without H2/H3).
- Do not use headings for nav logos, button labels, or decorative text.
**Images**
- Every meaningful `<img>` / Next.js `Image` must have a non-empty **`alt`** describing the image (product name, slide title, banner purpose).
- Prefer CMS title / `titleFa` / `titleEn` / media alt when available; for decorative icons use `alt=""` only when the image adds no information.
- Never leave missing `alt` on content images (hero, product gallery, blog cover, static-image slots, sliders).
**Open Graph (recommended)**
- Set `og:title`, `og:description`, and `og:image` on important pages (home + detail pages) from CMS media when available.
### Site-wide meta tags + trust badges (eNamad / Samandehi)
Business admins configure this under **Website → Tags & Badges**.
1. `GET /tenants/{domain}/website/business-info`
2. For each item in `metaTags` (array of `{ html }`), emit the full HTML once in the root layout `<head>`.
```html
{html}
```
Example `html` value: `<meta name="google-site-verification" content="…">` (also supports `property`, `http-equiv`, etc.). Parse attributes into a `<meta />` element, or inject the sanitized string into `<head>`. Do **not** wrap it again as `name`/`content` — `html` is already the whole tag. The dashboard “Name” field is an admin label only and is **not** in this public payload.
3. For each item in `trustBadges` (array of `{ kind, embedHtml }`), render `embedHtml` in the site footer (e.g. `dangerouslySetInnerHTML`). These are HTML widgets only (not meta). Scripts are stripped by the API — still treat the HTML as CMS-controlled content.
- When arrays are empty, omit tags / footer badges.
- Do **not** hardcode eNamad/Samandehi HTML in the website repo when these fields are present.
### 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 (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
7. Site-wide `metaTags` + footer `trustBadges` from business-info when present
If OpenAPI and this brief conflict, **OpenAPI wins**.
---
## What to tell each website team
Replace `<WEBSITE_DOMAIN>` once per project. Everything else is global — same Postman, same OpenAPI, same base URL. Remind them: **`output: "standalone"` is mandatory** for Meshkee Next deploys (shared VM RAM).