Add the Kaman Cable Next.js storefront with Meshkee catalog, blog, auth, and static-image slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-31 17:01:01 +03:30
co-authored by Cursor
commit 51fb9f6a26
84 changed files with 12999 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
# Kaman Cable — AI / Project Context
## Source of truth
- Live reference: https://www.kamancable.ir/
- Backend / Meshkee Website API: https://api.meshkee.com/docs/website
- Domain: `kamancable.ir` (`NEXT_PUBLIC_WEBSITE_DOMAIN`)
- Tenant business id: `24` (`GET /tenants/kamancable.ir`)
- Customer portal: `https://customer.kamancable.ir`
- Business dashboard: `https://business.kamancable.ir`
- Catalog and blog content come from Meshkee (`enabledModules`: products, blog). Business-info / logos in the API may be empty — brand copy and some homepage photos still live in `src/data/site.ts` and `public/images/`.
## Stack
- Next.js 15 (App Router) + React 19 + TypeScript
- Tailwind CSS v4 (utility import) + custom CSS in `src/app/globals.css`
- Language: `fa`, direction: `rtl`
- Fonts:
- `iran-yekan` from Meshkee CDN (Persian UI / body)
- `Rajdhani` via `next/font/google` (English UI, class `.en`)
- `Oswald` via `next/font/google` (`--font-en-display` for section titles / phone)
## Color palette
- Dark navy: `#1C1E40`
- White: `#FFFFFF`
- Gold accent: `#E1A34C`
- Highlight yellow: `#FFDE30`
- Muted text: `#C8CCE0`
## Pages
| Route | Purpose |
|-------|---------|
| `/` | Homepage (hero, app CTA, about teaser, contact bar, category cards, blog carousel, clients) |
| `/products` | Product catalog from `GET /tenants/{domain}/products` |
| `/products/{id}/{nameFaSlug}` | Product detail (`GET .../products/by-id/{id}`) |
| `/products/category/{id}/{nameFaSlug}` | Category (`GET .../categories/by-id/{id}`, then products with `categoryId`) |
| `/blog` | Blog index from `GET /tenants/{domain}/blogs` |
| `/blog/{id}/{titleSlug}` | Blog detail (`GET .../blogs/by-id/{id}`) |
| `/about` | درباره ما |
| `/contact` | تماس با ما |
Header (RTL): logo + menu on the right; search, language (FA/EN), and account on the left.
- Signed out: icon links to `https://customer.kamancable.ir`
- Signed in: `{firstName} عزیز` dropdown — پروفایل من, خروج, and مدیریت وبسایت if the user is a website admin
- Session: shared cookies `meshkee_customer_access_token` / refresh, hydrated into localStorage; `/api/auth/me` proxies `GET /auth/me`
Homepage product cards are Meshkee `categories` slot teasers (duplicatable; empty API keeps `src/data/site.ts`). The full catalog is `/products`.
Homepage blog carousel prefers Meshkee blogs, with `src/data/site.ts` as fallback.
## Meshkee API integration
Base: `{NEXT_PUBLIC_MESHKEE_API_BASE}/tenants/{domain}/...`
Helpers: `src/lib/meshkee.ts` → `meshkeeGet`, `meshkeeGetOrNull`, `tenantPath`, `getTenant`, `getBusinessId`
### Products
- List: `GET /tenants/{domain}/products?page&pageSize&categoryId`
- Detail: `GET /tenants/{domain}/products/by-id/{id}` → `{ product, relatedProducts }`
- Category: `GET /tenants/{domain}/categories?entityType=product` and `GET .../categories/by-id/{id}`
- Specs: `GET /tenants/{domain}/products/by-id/{id}/technical-info` — join `form.fields[].id` ↔ `values[].fieldId`. Do not invent field labels from the detail payload.
- Helpers: `src/lib/products.ts`, `src/lib/categories.ts`, `src/lib/technical.ts`
- Detail UI: lightbox gallery, hover zoom, tabs (توضیحات / مشخصات فنی / نظرات), similar products row
- Public slugs are built from `nameFa` / title (Farsi kept, spaces → `-`). Never use the legacy DB `slug` in the URL.
### Blog
- List: `GET /tenants/{domain}/blogs?page&pageSize`
- Detail: `GET /tenants/{domain}/blogs/by-id/{id}` → `{ blog }`
- Helpers: `src/lib/blog.ts`
- Empty API → local posts in `src/data/site.ts`
### Comments
- List: `GET /tenants/{domain}/comments?entityType=product|blog&entityId=`
- Submit: website proxy `POST /api/comments` → Meshkee comments
- UI: authenticated users get a form; guests see a login box to the customer portal (`src/components/CommentBox.tsx`)
### Contact
- Public POST: `/tenants/kamancable.ir/contact-submissions`
- Website proxy: `POST /api/contact`
- Body: `{ title, name, text, email?, cellNumber? }`
- Homepage callback strip also posts through this route
### Auth proxies
- `GET /api/auth/me` → `GET /auth/me`
- `POST /api/auth/handoff` → `POST /auth/handoff` (admin dashboard)
## Static image slots
API: `GET /tenants/{domain}/website/static-images?pageKey=home|about|contact|products|blog`
Docs: https://api.meshkee.com/docs/website (not http://meshkee.com/docs/website)
Catalog: `GET /meshkee/static-image-slots` → `STATIC_IMAGE_SLOTS` in `src/lib/static-images.ts`
Helpers: `fetchStaticImageSlots`, `pickSlot` (aliases for renamed keys), `overlaySlotImages`, `slotRatioStyle`
Homepage placeholders (match Meshkee dashboard keys exactly):
| key | kind | itemCount | aspectRatio | recommendedWidth | section |
| --- | --- | --- | --- | --- | --- |
| `slider` | list (duplicatable) | null | `1920:830` | 1440 | Homepage hero (72vh cover crop) |
| `about-us-image` | single | 1 | `440:609` | 480 | Homepage about photo |
| `categories` | list (duplicatable) | null | `3:4` | 480 | Homepage product cards (`.product-card .thumb`) |
| `partners` | list (duplicatable) | null | `3:2` | 280 | Homepage partner logos |
| `home-blog` | list | 6 | `16:10` | 720 | Homepage blog carousel |
| `about-hero` | single | 1 | `1920:640` | 1440 | About hero |
| `about-gallery` | list | 6 | `4:3` | 720 | About gallery |
| `contact-hero` | single | 1 | `1920:480` | 1440 | Contact hero |
| `products-hero` | single | 1 | `1920:480` | 1440 | Products page hero |
| `blog-hero` | single | 1 | `1920:480` | 1440 | Blog page hero |
Legacy aliases (still resolved by `pickSlot` if the API has not been re-synced): `home-about-image` → `about-us-image`, `home-products` → `categories`, `home-clients` → `partners`.
Rules:
- Use `slot.images` (all if `kind=list`, first if `kind=single`)
- `url` → image src; `linkUrl` → wrap in `SlotLink`
- Empty / fail → keep local fallbacks from `src/data/site.ts` / `public/images/`
- Apply `slot.aspectRatio` as CSS `--slot-ratio` (`slotRatioStyle`); do not invent upload APIs
- **If a slot’s on-page size or aspect ratio changes in CSS, update `STATIC_IMAGE_SLOTS` in the same change** (Meshkee’s static-image reader uses this catalog)
## Content & assets
- Site copy, phones, address, category blurbs: `src/data/site.ts`
- Logo: `public/images/logo/logo.png`
- Hero banners: `public/images/slider/bg1.jpg` … `bg3.jpg`
- Favicon: `/favicon.png`
- Contact: `021-242624` · دفتر تهران، خیابان کریم‌خان زند · کارخانه شهر صنعتی شماره یک زنجان
- App: https://kamancable.com/applanding/
## Important paths
- Layout / shell: `src/app/layout.tsx`, `src/components/Header.tsx`, `src/components/AccountMenu.tsx`, `src/components/Footer.tsx`
- Home: `src/app/page.tsx`, `src/components/HeroSlider.tsx`, `src/components/ContactBar.tsx`
- Products: `src/app/products/page.tsx`, `src/app/products/[id]/[slug]/page.tsx`
- Blog: `src/app/blog/page.tsx`, `src/app/blog/[id]/[slug]/page.tsx`
- Config: `src/lib/config.ts`
## Env
```bash
NEXT_PUBLIC_WEBSITE_DOMAIN=kamancable.ir
NEXT_PUBLIC_MESHKEE_API_BASE=https://api.meshkee.com/api/v1
```
## Commands
```bash
npm install
npm run dev
npm run build
```
## Sitemap + robots
- **Do not** add `public/sitemap.xml` or `public/robots.txt` (nginx proxies both to the Meshkee API)
- Live sitemap is an index: `/sitemap.xml` → `/sitemap-main.xml` + `/sitemap-products.xml`
- Publish `GET /meshkee/sitemap-config.json` (generated at build from app routes)
- Includes public static routes automatically; excludes `api` / `meshkee` / `auth` / `checkout` / `cart` / `admin` / `account` / `login`
- Generated at build: `scripts/generate-sitemap-config.mjs` → `src/lib/sitemap-config.generated.json`
- Run manually: `npm run generate:sitemap-config`
- After deploy: business dashboard → Website → Settings → Sync sitemap config (needed for static pages)
### Canonical CMS URLs (id is canonical; slug is decorative and redirects if stale)
- Product: `/products/{id}/{nameFaSlug}` → `GET /tenants/{domain}/products/by-id/{id}`
- Category: `/products/category/{id}/{nameFaSlug}` → `GET /tenants/{domain}/categories/by-id/{id}` then list products with `categoryId`
- Blog: `/blog/{id}/{titleSlug}` → `GET /tenants/{domain}/blogs/by-id/{id}`
- Portfolio is not enabled for this tenant (`enabledModules`: products, blog, finance)
### On-page SEO rules
- Unique `<title>` + meta description on every crawlable page (`src/lib/seo.ts` → `pageMetadata`); paginated list pages include the page number
- Prefer CMS `nameFa` / `title` / `abstract` / `summary` / `descriptionHtml`; never leave Next.js defaults
- Exactly one `<h1>` per page; section headings use `<h2>` / `<h3>`
- Content images have meaningful `alt` (CMS title when available)
- Slugify: Farsi letters kept, spaces → `-`, other punctuation stripped (`src/lib/slug.ts`)