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

20 KiB
Raw Blame History

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):

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:

{
  "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):

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

{
  "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:

{
  "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.