Files
backend/docs/website-api/AI_PROMPT.md
T
Alireza HassaniandCursor cf459807d8 Document storefront mini-cart and customer-dashboard checkout.
Website agents must add variants to a local guest cart and redirect to customer.{domain} for login and checkout instead of building those pages on the shop.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 08:55:11 +03:30

315 lines
20 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.
### 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)
- `GET /tenants/{domain}/user-products/{slug}` — details + gallery (`technicalValues` = values only, no labels)
- `GET /tenants/{domain}/user-products/{slug}/technical-info` — **required for specs UI**: `{ form: { fields: [{ id, label, key, type, ... }] }, values: [...] }`
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/{slug}` | `GET .../user-products/{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** when the website calls the normal public list/detail APIs (blogs, products, portfolios, videos, store-items, and homepage sliders). No extra website code is required for those.
Optional explicit record (e.g. custom home without sliders):
- `POST /tenants/{domain}/analytics/views` body `{ "kind": "home"|"blog_detail"|"product_detail"|..., "entityId"?: "...", "path"?: "/blog/my-post" }`
- `kind` values: `home`, `portfolio_list`, `portfolio_detail`, `product_list`, `product_detail`, `store_item_list`, `store_item_detail`, `blog_list`, `blog_detail`, `video_list`, `video_detail` (legacy aliases `website`/`product`/`portfolio`/`blog` still accepted)
- Events kept ~6 months for charts; lifetime totals kept forever in counters.
### 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/{slug}` — use the listing’s `slug` from the API; resolve via `GET /tenants/{domain}/user-products/{slug}`.
- 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/{id}/{nameFaSlug}`. User-product cards use `/user-products/{slug}`.
- 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.
**Checklist before shipping a page**
1. Unique `<title>` and meta description
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
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.