Files
ali-alemi/CONTEXT.md
T
amirhosein.ashourlooandClaude Sonnet 5 84a1c0f9cc Header polish: dynamic logo, action button restyle, nav scope, bug fixes
- Header/footer logo now reads from GET /tenants/{domain}/website/favicon
  (logoDarkUrl on the navy bar, logoUrl on the white footer) with a local
  fallback, matching the favicon's existing dynamic behavior.
- Mobile header rebuilt as its own 3-column row: hamburger right, logo
  centered, cart left. Fixed a missing justify-center that left the
  hamburger icon (and its X state) off-center inside its square button.
- Header action buttons (cart, user/login) restyled to solid primary
  pills/circles per a reference screenshot; a search button was tried and
  then removed since the site has no search backend yet.
- Navbar reverted to exactly 5 static items (home, workshops, blog, about,
  contact) per instruction — a categories-driven "محصولات" dropdown was
  built and then removed the same session; /products, /store, /portfolios
  remain real pages, just unlinked from the header for now.
- Hero: second avatar raised so it's not hidden behind the character.
  WhyChooseUs: mirrored the two bottom floating badges into symmetric
  corners (were overlapping). FeatureCards: restored the 4th feature
  ("انتخاب زمان کلاس") that had been silently dropped, gave the icon
  badges a nested white-circle treatment, and gave the cards a visible
  border.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 18:51:24 +03:30

159 lines
33 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.
# Ali Alemi Website Context
> Keep this file current. Whenever a prompt changes scope, content, domain, or conventions, update the relevant section below in the same turn.
## Project
- Farsi RTL storefront for استاد علی عالمی (private math/calculus tutor for the Iranian کنکور).
- Next.js 16 App Router, React 19, TypeScript, Tailwind 4 (`@theme inline`, no `tailwind.config.js`), `src/app`.
- `lang="fa" dir="rtl"` set in `src/app/layout.tsx`.
- Apex domain: **`alialemi.com`** (`src/lib/site.ts` → `SITE_DOMAIN`) — confirmed by the user. `SITE_URL`, `CUSTOMER_URL` (`customer.alialemi.com`), `BUSINESS_URL` (`business.alialemi.com`), the sitemap-config fallback, and metadata all derive from it.
- The live Meshkee tenant for this domain does not exist yet on `api.meshkee.com` (every API call currently 404s) — every fetch on the site is wrapped in `safe()` (`src/lib/meshkee/client.ts`) so pages render a friendly empty state instead of crashing. This is expected and will resolve itself once the business is provisioned on the backend; no code change needed then.
- Carousels use **Swiper** (`swiper/react`, `swiper/modules`) per explicit instruction — see `src/components/home/Testimonials.tsx` and `src/components/home/FeatureCards.tsx`.
## Homepage layout fidelity
The user asked for the homepage's structure/layout to closely match the live original (`http://ali-alemi.local:8000`, a local Meshkee web-builder preview blocked by their VPN from this environment — they instead pasted screenshots directly). Corrections made after comparing against those screenshots:
- **Header**: solid `bg-secondary` (navy) bar, not white/blurred. On desktop (`lg:` and up) logo sits first in DOM (renders on the **right** in RTL — do not add `order-*` overrides here again, that was tried and was wrong), nav links, then phone/cart/login actions, matching the real desktop header exactly. **Below `lg:` the header is a completely separate 3-column grid row** (hamburger right / logo centered / cart left — Farsi readers expect the hamburger on the reading-start side, not the logo-adjacent corner) rendered via its own `grid grid-cols-3 lg:hidden` block, not responsive classes bolted onto the desktop flex row — the two arrangements are different enough (centered logo vs. edge logo, hamburger only exists on mobile) that trying to share one row fighting `order-*`/`justify-*` was worse than two small dedicated blocks. See `Header.tsx`.
- **Hero** (`HeroSection.tsx`): character + a large dotted ring backdrop (`public/images/hero/ring.png` — the accent dots are baked into that asset, don't draw separate circle divs), a second smaller student avatar peeking from behind the character, floating grad-cap/paper-plane/shape decorations. No CTA buttons in the hero — the original doesn't have any there. Two-column grid switches at **`md:`** (768px), not `lg:` — at `lg:` the in-between tablet/small-laptop range stacked to a single column and looked broken (large blank gap), since the image side has fixed-height containers.
- **Feature cards** (`FeatureCards.tsx`): a **Swiper carousel** (prev/next arrow buttons, not pagination dots) in a two-column row — cards on the left (wide column), a "ویژگی‌های ما" heading + short paragraph + the arrow buttons in a narrow **280px right-hand column**. This grid must be `md:grid-cols-[280px_minmax(0,1fr)]` — **not** `[280px_1fr]`: a bare `1fr` track let Swiper's internal width calculation blow the column out to ~16.7M px (classic CSS Grid blowout; Swiper needs `minmax(0,1fr)` to be constrained by its parent). The Swiper's own grid-item wrapper also needs `min-w-0` directly (see the `Reveal` around it) — that's the part that matters **below** `md`, where there's no explicit `grid-template-columns` at all but the item is still a grid child with `min-width: auto` by default, so the same blowout happened on mobile too until that class was added. Any future Swiper placed inside a `grid` or `flex` container needs this same `min-w-0` treatment regardless of breakpoint. Also must NOT have negative top margin overlapping the hero — that was tried first and the user flagged it as a bug ("باید با هیرو فاصله داشته باشه"), plain `py-16 md:py-24` is correct.
- **Quick-links row** ("چرا استاد علی عالمی؟", `QuickLinks.tsx`): real icon assets (`icons/play.png`, `exam.png`, `price.png`, `support.png`) in `sm:grid-cols-2 lg:grid-cols-4` — 4-in-a-row on real desktop widths, 2×2 on tablet. Ends with a "درباره ما بیشتر بدانید" CTA linking `/about-us` (that's the original frame's own link field, not invented).
- **"چرا مارا انتخاب کنید؟"** (`WhyChooseUs.tsx`): image on the **left**, text+CTA on the **right** (DOM order text-first renders right in RTL grid). Stat badges (آموزش با کیفیت / نظارت دقیق و ارزیابی عملکرد / بیش از ۱۵۰ ساعت آموزش / ۱۴,۵۰۰ نفر دانشجو+avatars) are **floating cards scattered over the image's corners** (absolute-positioned with physical `left-*`/`right-*`, not logical `start`/`end` — this composition doesn't flip with page direction), not a plain bulleted list in the text column. The two **bottom** badges must sit in mirrored corners — `۱۴,۵۰۰ نفر دانشجو` at `-bottom-6 -left-6` and `بیش از ۱۵۰ ساعت آموزش` at `-bottom-6 -right-6` — an earlier `right-1/3` on the second one drifted under the first at narrower widths (user flagged it as an overlap bug from a screenshot); symmetric corners is the fix, don't reintroduce a fractional/centered offset on either bottom badge.
- **Testimonials**: multi-per-view on desktop (`breakpoints: {768:2, 1024:3}`), pagination dots (not arrows) — opposite of the feature carousel.
- **Stats bar** (`StatsBar.tsx`): light/white background with **teal** numbers — not a navy full-bleed banner (that was tried first and was wrong).
- Scroll-reveal entrance animation on every section via `src/components/Reveal.tsx` (fade + translateY on `IntersectionObserver`, stands in for the original's WOW.js + animate.css) — requested explicitly ("انیمیشن‌ها ... باید دقیقا شبیه سایت اصلی باشه").
- **General lesson**: any two-column `image + text` grid using a fixed-height image container should switch at `md:` not `lg:`, or verify the `lg:` in-between range doesn't leave a tall single-column stack. Applies to `WhyChooseUs.tsx`, `about/BioIntro.tsx`, `contact/ContactInfo.tsx` (already fixed) — check any *new* two-column section against this before shipping.
- **Hero second avatar** (`avatar-1.png`, the girl peeking from behind the character): positioned `top-16 end-0` (was `bottom-6 end-2`, which put almost the whole thing behind the character's body with only a sliver of hair visible) — user asked to raise it so it actually reads as a second figure. If the character illustration or its box size changes, re-check this is still clearly visible, not just technically in the DOM.
- **Feature cards** (`FeatureCards.tsx`) use `border border-ink/15 shadow-md`, not `ring-1 ring-line` — the `--color-line` token (`#ececec`) was too light to read as a border at all against the white card/white section background (flagged from a screenshot as "بوردرهاشون مشخص نیست"). If another white-on-white card ever looks edge-less, this is the fix, not a heavier shadow alone.
## Header — logo/favicon, nav, categories (round 2 polish)
- **Dynamic logo**: `RootLayout` (`src/app/layout.tsx`) now fetches `getFavicon()` once and passes `logoUrl`/`logoDarkUrl` down to both `Header` and `Footer` (in addition to the favicon it already used for `<head>` icons). `Header` prefers `logoDarkUrl` (the dashboard's dark-background variant) → `logoUrl` → the bundled local `/images/logo.png`; the `brightness-0 invert` CSS filter is applied **only** when falling back to the local asset (it's the one known-teal file that needs forcing white for the navy bar — a real CMS-uploaded logo's color is unknown, don't invert it blind). `Footer` just uses `logoUrl` → local, no invert (white background, original color is correct there). Both use `unoptimized` on `next/image` when the src is a remote CMS URL, since that host isn't (and can't be, until a real tenant exists) in `next.config.ts` `images.remotePatterns`. The favicon itself was already dynamic before this round (`generateMetadata`) — this was specifically about the header/footer `<img>` logo not following the same rule yet.
- **Active nav link + phone button styling**: active link and the phone number both render as a solid white pill (`rounded-full bg-white px-4 py-2`, dark `text-secondary`) against the navy bar, not a transparent/tinted-text treatment — this was an explicit visual ask from a screenshot annotation ("دور لینک اکتیو ... یک مستطیل سفید با ردیوس ۵۰ بذار"). `rounded-full` (Tailwind's 9999px) reads as the same fully-pill shape as a literal 50px radius at this button height, so that's the correct utility, not an arbitrary `rounded-[50px]`. Inactive links stay `text-white/80` on transparent. Applies only to the desktop nav row; the mobile dropdown panel keeps its own simpler hover-background treatment.
- **Navbar reverted to exactly 5 static items** (`NAV_LINKS` in `src/lib/site.ts`): صفحه اصلی، دوره‌های آموزشی، مقالات، درباره ما، تماس با ما — per explicit instruction, **not** محصولات/فروشگاه/نمونه‌کارها. A categories-driven dropdown for "محصولات" (`src/components/CategoriesMenu.tsx`, fetched via `getCategories({ entityType: "product" })` in the root layout) was built and then **removed again** the same session once the user clarified the nav should stay to just those 5 until real CMS content exists — `CategoriesMenu.tsx` was deleted and the `categories` fetch/prop taken back out of `layout.tsx`/`Header.tsx`. `/products`, `/store`, `/portfolios` are still real, working pages — just not linked from the header nav right now. If nav-driven category browsing is wanted again later, it needs to be rebuilt (check git history for the deleted `CategoriesMenu.tsx` rather than starting from scratch).
- **Header action buttons** (search/cart/user, right side of the desktop row): redesigned to solid `bg-primary` filled circles/pills per a reference screenshot, not outlined/ghost buttons. **Search was added then removed the same session** — the site has no search backend, so don't re-add a search icon/box to the header without an actual `/products?name=` wiring decision first. `CartButton` (`src/components/cart/CartButton.tsx`) is now always a solid primary circle with a white-on-primary badge (no more `light` prop — it was only ever used inside this navy header, the conditional was dead weight). `UserMenu` (`src/components/UserMenu.tsx`) similarly dropped its `light` prop — both the logged-out "ورود / ثبت‌نام" pill and the logged-in "{name} عزیز" trigger are solid primary pills now.
- **Mobile hamburger button bug**: the button was `flex items-center` but **not** `justify-center`, so the 3-line icon span (and the X it morphs into when open) hugged one edge of the 40×40 square instead of sitting centered — reported from a screenshot ("۳ خط منو داخل مربع نیستن" / X not centered when open). Fix was adding `justify-center` alongside the existing `justify-self-start` (that one's unrelated — it positions the *button itself* within the mobile header's 3-column grid row; `justify-center` centers the icon *inside* the button). If another icon-only square button ever looks off-center, check for this exact same missing class rather than touching the icon's own SVG/span markup.
- **Feature carousel must have all 4 real items**: `src/components/home/features-data.ts` had been trimmed to 3 during the earlier "match the real 3-visible-at-once desktop screenshot" pass, silently dropping "انتخاب زمان کلاس" (choose class time) — the original theme has 4 features, the 4th just wasn't visible in that one screenshot because the carousel only shows 3 at a time on desktop. Restored as the 2nd item (clock icon, secondary tone) so the carousel actually has something to scroll to. Icon badges in `FeatureCards.tsx` also got a nested treatment — an outer `h-16 w-16 rounded-2xl` tone-colored square containing an inner `h-10 w-10 rounded-full bg-white` circle with the icon colored to match the outer tone — per a "flaticon-style" reference screenshot; a flat single-layer colored-square-with-white-icon read as low-effort by comparison. `about/FeatureStrip.tsx` reuses the same `FEATURES` array and benefits from the same fix (was showing an awkward 3-in-a-4-col grid before).
## Scope (current)
Static pages: `/` (home), `/about-us`, `/contact-us` — content from `old-site` (see "Source clone" below).
Live-API-backed catalog pages (built against `api.meshkee.com`, real tenant not provisioned yet so every list/detail page currently shows its empty state — see "Meshkee API" below):
| Module | List | Detail | Notes |
|---|---|---|---|
| Products | `/products` | `/products/{id}/{nameFaSlug}` | description / technical-data / comments tabs, "محصولات مشابه" row. No cart button (informational catalog). |
| Store items | `/store` | `/store/{id}/{nameFaSlug}` | same tabs **plus** add-to-cart with quantity. `{id}` is the **product id**, not a variant id — see "Store items architecture" below. |
| Product category | — | `/products/category/{id}/{nameFaSlug}` | lists products in that category. |
| Workshops | `/workshops` | `/workshops/{slug}` | no confirmed backend endpoint yet — see "Workshops" below. |
| Portfolios | `/portfolios` | `/portfolios/{id}/{titleFaSlug}` | lightbox gallery, "نمونه کارهای مشابه" row. |
| Blog | `/blog` | `/blog/{id}/{titleSlug}` | cover image, HTML content, comments. |
There is **no** `درمانگاران` page — it was in an earlier draft of the brief and the user corrected that mid-session.
## Source clone
- Original site: a Meshkee web-builder (Blade/PHP) export that was temporarily copied into `old-site/` to port content/assets from, then **deleted per explicit instruction** once the rebuild below was complete and verified against it — it no longer exists in this repo or on disk. Everything it contained is now captured either as real content in the pages/components listed below or as extracted files under `public/`; there is no separate archive to fall back on, so treat this file's notes as the record of what came from where. (Its original structure, for history: `old-site/custom/fa/pages/*/main.php` listed each page's frame stack; `old-site/custom/fa/frames/*.php` and `old-site/frames/**/content.blade.php` held the actual field data and markup per frame.)
- `old-site/settings.php` gave the brand palette (`primaryColor #008081`, `secondaryColor #324264`, `tertiaryColor #E7E7E7`) and default font (`iran-yekan`) — both carried over as Tailwind tokens (`src/app/globals.css`) and the self-hosted font (`src/app/fonts.ts`).
- **All page copy is the real content from `old-site`**, including the lorem-ipsum placeholder paragraphs that were literally in the source (per instruction: "all data is valid"). One exception: the About page FAQ (`old-site/custom/fa/frames/public-faq-yOxMt.php`) contained a broken, irrelevant template Q&A ("چگونه می‌توانم محصول شما را با PayPal ادغام کنم؟" on a math-tutor site) — that was replaced with generic, non-fabricated tutoring FAQs (`src/components/about/Faq.tsx`) rather than keeping obviously mismatched boilerplate.
- **Real, non-lorem content that was found and is now used prominently:**
- Bio/credentials (`old-site` footer frame `footer-footer-hCR1y.php`, field `text`): born 1366/12/22, mechanical engineering (Iran University of Science & Technology), aerospace M.Sc. (same university, aerospace structures), rank 243 in the national Konkur, 13 years teaching math/calculus, worked with top Konkur academies. Used in the footer bio list **and** as the About page's `Resume` section (`src/components/about/Resume.tsx`) — this is the standout real content on the site, feature it, don't bury it.
- Contact info (`public-info_1-ZWXoP.php`): address "قم، میدان مفید، ساختمان پاسارگاد، طبقه سوم", phone `0912 607 3354`, Instagram `@Alialemiteacher`. Lives in `src/lib/site.ts` → `CONTACT`.
### Asset provenance (`public/images`, `public/fonts`, `public/brand`)
- `public/fonts/iranyekanweb{light,bold}fanum.woff` — copied from the sibling Meshkee project `../moallem/public/fonts` (same font, already cleared for use across the Meshkee site fleet). `cdn.meshkee.com` (the original font CDN) was unreachable from this environment (TLS handshake blocked) — if that changes and more weights are wanted, fetch `https://cdn.meshkee.com/web-builder/fonts/iran-yekan/iran-yekan.css`.
- `public/images/logo.png` ← `old-site/assets/files/E9jXiAxciH.png` (teal "ALI ALEMI" wordmark; footer's `mk7iCwJnYx.png` is byte-identical). There's no separate white variant, so `Header.tsx` applies `brightness-0 invert` to render it white on the navy bar; `Footer.tsx` uses the original teal color on its white background.
- `public/favicon.png` / `public/images/favicon-mark.png` ← `old-site/assets/files/9B5ryAVpXy.png` (the infinity mark from the logo).
- `public/images/hero/character.png` ← `XrfpuKYbWv.png` — the 3D professor illustration (used in the hero, `WhyChooseUs`, and About `BioIntro`).
- `public/images/hero/{ring,sketch,grad-cap,paper-plane,shape-1..5,avatar-1,avatar-2}.png` ← decorative floaters from the same slider frame (`RHLYjNnYj6`, `JtkpbKjrLA`, `8d3ZPngaUQ`, `jWgzrcRljG`, `QpKmZ80xX1`, `hiDCqXhOI6`, `f8rjoyAIi9`, `wYNzYMZHz2`, `H11Q6Ni7hV`, `QeqN7XLmJY`, `yVeuSypizt`).
- `public/images/icons/{play,exam,price,support}.png` ← `YYrcDvSrtC`, `JGmzMzVo2I`, `wEkbnOuKHg`, `7HZemBIE7S` — the real gradient icons for the "چرا استاد علی عالمی؟" quick-links row.
- `public/images/icons/quote.png` ← `j6FWn5p4aw.png`; `public/images/testimonial/avatar.png` ← `IRj6Zp6N2J.png` (same avatar the original reused across all three testimonial cards — that's the real data, not a shortcut).
- `public/images/contact-photo.jpg` ← `old-site/assets/img/contact/01.jpg` (generic stock photo bundled with the original theme).
- `public/brand/meshkee-logo-white.png` — downloaded directly from `https://safeteb.com/images/footer/MeshkeeLogo-White.png` (see Footer credit below).
- **Substituted with custom inline SVG icons (`src/components/icons.tsx`) instead of a missing asset:** the four "ویژگی‌های ما" feature-card icons, the three small badge icons (pencil/search/check), contact icons (location/phone/instagram/whatsapp/telegram), and the About-page Resume icons. Their source files (e.g. `files/vLZSFuYnkv.png`, `files/otHFQ7PVUQ.jpg`, `files/IyvhvDu45j.jpg`, `files/TXzEEuVLAR.png`, `files/6aCyDwfHgF.png`, `files/6r1iyxcLfi.png`) were referenced in the Blade config but **not present** in `old-site/assets/files` (never actually uploaded on the live site) — rather than inventing stock photography, those slots became flat gradient backgrounds (`StatsBar`, `PageHeader`, `CustomerClubBanner` all use a teal→navy gradient + dotted pattern instead of a missing photo) or the custom icon set.
## Footer Meshkee credit
Matches `https://safeteb.com/` exactly, per instruction — markup, hover-frame animation, and CSS copied over in `src/components/Footer.tsx` / `src/app/globals.css` (`.meshkee-credit*`, `.footer-bottom*` classes). This is the same pattern already used on the sibling `khiyal-studio` project; keep the three in sync if the reference site's credit design ever changes.
## Meshkee API layer (`src/lib/meshkee/`)
- `client.ts` — `meshkeeFetch`/`tenantFetch` (adds `/tenants/{SITE_DOMAIN}` prefix) wrap `fetch` with Next.js data-cache `revalidate` (default 60s; auth/mutating calls pass `revalidate: false`). `safe(promise, fallback)` swallows any error/404 and returns the fallback — **every** list/detail fetch on a page goes through `safe()` so a missing tenant or disabled module renders an empty state, never a crash. `MeshkeeApiError` carries the HTTP status if a caller needs to branch on it (nothing does yet).
- `types.ts` — deliberately loose interfaces. **The public OpenAPI/Postman docs document endpoint shapes but not exact field names for `product`, `store-item`/variant, `category`, or `comment` objects** (confirmed by querying both docs directly this session). Fields like `nameFa`/`name`/`title`, `titleImageUrl`/`imageUrl`, `discountedPrice`/`price` are best-effort guesses at Meshkee's conventions (matching what other modules — blogs, portfolios, user-products — *do* document). **Once a real tenant exists, fetch one live response per entity and reconcile `types.ts` + the `pickText()`/`normalize*()` accessor chains in `format.ts` against the actual field names** — don't assume the current guesses are correct.
- `format.ts` — `pickText(...)` tries field-name candidates in order; `normalizeImage`/`normalizeGallery` handle both bare URL strings and `{url}` objects (unconfirmed which shape the API actually returns); `slugify()` builds canonical-URL slugs (Farsi letters/numbers kept, spaces → `-`, other punctuation stripped) — **always build links with `slugify(name)`, never a raw `item.slug` field** (the legacy DB slug must not appear in public URLs, per instruction); `formatToman`, `formatDate`.
- One file per module: `tenant.ts`, `products.ts`, `store-items.ts`, `portfolios.ts`, `blogs.ts`, `workshops.ts`, `categories.ts`, `comments.ts`, `contact.ts`.
## Canonical URLs
Per the current brief (this supersedes anything the earlier AI_PROMPT.md fetch said about slug-only blog/portfolio routes):
- Product: `/products/{id}/{nameFaSlug}` → resolve via `GET /tenants/{domain}/products/by-id/{id}`.
- Product 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: `/portfolios/{id}/{titleFaSlug}` → `GET /tenants/{domain}/portfolios/by-id/{id}`.
- Store item: `/store/{id}/{nameFaSlug}` — **not** an official Meshkee canonical path (none exists for store-items in the docs); chosen to match the product pattern. `{id}` is the product id (see "Store items architecture").
- Workshop: `/workshops/{slug}` — no `by-id` resolve exists yet (no endpoint at all — see "Workshops"); kept slug-only since there's nothing to redirect against.
Every `{id}/{slug}` detail page fetches by `id`, computes `slugify(name)`, and `redirect()`s to the canonical slug if the URL's slug segment doesn't match (same pattern on all four: products, store, portfolios, blog).
## Store items architecture (read before touching `/store`)
Meshkee's own data model treats `store-items` as **the sellable variant layer under a product**, not an independent content catalog — `GET /store-items/by-product/{productId}` is the only store-item lookup AI_PROMPT.md itself documents, specifically for the mini-cart flow. There is no dedicated "rich content" (gallery/description/technical-info) on a variant; that lives on the **product**.
So `/store/{id}/{slug}` resolves `{id}` as a **product id**: it fetches the product (`getProductById`, for gallery/description/technical-info) **and** `getStoreItemByProduct(id)` (for sellable variants/pricing) in parallel, then renders one page combining both. `/store` (the list) calls `GET /store-items` directly and reads `item.product` for display fields — **this nested `product` field on a store-item list response is unconfirmed** (see types.ts note above); verify against a live tenant.
## Workshops — no confirmed backend endpoint
Checked both `openapi.json` and the Postman collection directly this session: neither documents any workshops CRUD route — only a `/tenants/{domain}/sitemap-workshops.xml` placeholder exists (tag: SEO). `src/lib/meshkee/workshops.ts` still implements `getWorkshops`/`getWorkshopBySlug`/`getWorkshopById` following Meshkee's uniform module shape (same as products/portfolios/blogs) so it's a drop-in once the backend exposes it; until then every call 404s and `safe()` renders the "دوره‌ای برای ثبت‌نام باز نیست" empty state — this is expected, not a bug.
**"Registration row"**: a workshop's `productId` (falls back to the workshop's own `id`) is looked up via `getStoreItemByProduct()`, and if that returns in-stock variants, the *same* `AddToCartBar` used on `/store` renders as the registration control ("ثبت‌نام" = adding the workshop's product variant to the cart, then continuing to the customer-dashboard checkout — there's no separate "enrollment" API). If no store item exists for it, a "برای ثبت‌نام تماس بگیرید" + phone-number fallback renders instead.
## Auth (customer session)
- `src/lib/auth.ts` — server-only (`next/headers` `cookies()`). Reads `meshkee_customer_access_token`, including the **chunked** form (`{name}_n` count + `{name}_0`, `{name}_1`, ...) per AI_PROMPT.md's cookie contract. `getSession()` calls `GET /auth/me` with that token and returns `{ user, token } | null`; called once in `src/app/layout.tsx` (root layout is now `async`) and passed down as props — **no client component calls `/auth/me` itself**.
- `isAdminUser()` is a **best-effort** check (`user.isAdmin || user.role === "admin" || user.role === "owner" || user.roles?.includes("admin") || user.businessRoles?.length`) — the real shape of the `user` object returned by `/auth/me` isn't documented publicly. Verify against a live logged-in session and trim to whichever field actually indicates admin/owner.
- `src/components/UserMenu.tsx`: logged-out → "ورود / ثبت‌نام" linking to `customerLoginUrl()`. Logged-in → `"{firstName} عزیز"` with a dropdown: `پروفایل من` (`customer.{domain}/profile`), `مدیریت وبسایت` (`business.{domain}`, **only when `isAdmin`**), `خروج` (`customer.{domain}/logout`). All three are plain cross-origin `<a>` tags (not `next/link`) — deliberately, they leave the site.
- No local login/register/OTP UI anywhere, per the hard rule — everything auth-related redirects to `customer.{domain}`.
## Cart (guest cart, local-only)
- `src/lib/cart.ts` implements AI_PROMPT.md's guest-cart spec **exactly**: `localStorage` key `meshkee-guest-cart`, mirrored to a cookie (`Domain=.{SITE_DOMAIN}`, `Path=/`, `SameSite=Lax`, ~30 days, skipped if the encoded JSON exceeds ~3500 chars), `encodeGuestCart()` is the identical base64url encoder the customer dashboard uses. `CartLine.id` is **always** a `storeItemVariantId` — never a product id or store-item id.
- `src/components/providers/CartProvider.tsx` (client context, wraps the app in `layout.tsx`) holds cart state; reads `localStorage` in a `useEffect` deferred via `queueMicrotask` (avoids both a hydration mismatch *and* the `react-hooks/set-state-in-effect` lint error — same pattern as the sibling `moallem` project's `CartProvider`, keep them consistent if either changes) and writes back to storage on every change.
- `src/components/cart/CartButton.tsx` (header icon + quantity badge) and `MiniCart.tsx` (slide-over popup: line items, qty stepper, remove, total, **Continue**) — `Continue` uses `continueToCheckoutUrl()`: if the access-token cookie is present it links to `customer.{domain}/checkout/cart?guestCart=...`, otherwise to `customer.{domain}/login?redirect=/checkout/cart?guestCart=...`. No server cart/checkout code exists on this site at all, per the hard rule.
- `AddToCartBar.tsx` (`src/components/catalog/`) is the shared add-to-cart control used on `/store/{id}/{slug}` and workshop registration: filters variants to in-stock (`stockQuantity === null` or `> 0`), shows a `<select>` only when more than one in-stock variant exists, quantity stepper, "این محصول در حال حاضر موجود نیست" when none are in stock.
## Comments
- `GET/POST /tenants/{domain}/comments?entityType=...&entityId=...` — the public docs list `product|blog|portfolio|video` as valid `entityType` values; `workshop` and `store_item` are passed the same way as a best-effort extension (**unconfirmed** — verify once live, `blog`/`portfolio`/`product` are the safe ones). Blog and portfolio also have entity-specific `/blogs/{id}/comments` routes but the generic one is used everywhere for one code path.
- **The API itself does not require auth to POST a comment** (no security scheme on that path in the OpenAPI doc) — but the user explicitly wants comments **gated to authenticated users in the UI**: `src/components/catalog/CommentsPanel.tsx` shows the comment form only when `user` is non-null (passed from the server page's `getSession()`), otherwise a "برای ثبت نظر ابتدا وارد حساب کاربری خود شوید" box linking to `customerLoginUrl(pathname)`. When posting, `authorName` is built from the session's `firstName`/`lastName` and the access token is sent as `Authorization: Bearer` too (harmless if the API ignores it, useful if it actually does attribute comments server-side).
## SEO / sitemap
- No `public/sitemap.xml`, `public/robots.txt`, `app/sitemap.ts`, or `app/robots.ts` — Meshkee nginx proxies `/sitemap.xml` (index) → `/sitemap-main.xml` + `/sitemap-products.xml` (+ other module sitemaps once enabled).
- `src/app/meshkee/sitemap-config.json/route.ts` scans `src/app/**/page.tsx` at request time (build-time-equivalent, since it's a dynamic route) and lists every **static** route, excluding dynamic `[...]` segments automatically — so `/products/category/[id]/[slug]` etc. never appear here; Meshkee's CMS fills those in from its own data per the canonical templates above. After deploy: business dashboard → Website → Settings → Sync sitemap config.
- Every page (static and dynamic) sets a unique `<title>`/description via `metadata`/`generateMetadata`, preferring `seoMetaTitle`/`seoMetaDescription` when the API provides them. Exactly one `<h1>` per page: `PageHeader` owns it; detail-page body sections that repeat the item name use `<h2>` (fixed a duplicate-H1 bug on the products/store/workshop detail pages this session — if you add a new detail page, don't reintroduce it). All content images carry a real `alt` (item name, never empty, decorative floaters use `aria-hidden` + empty alt instead).
- `src/app/meshkee/static-image-slots/route.ts` still returns `STATIC_IMAGE_SLOT_CATALOG` from `src/lib/meshkee.ts` (note: different file from the new `src/lib/meshkee/` **directory** — `src/lib/meshkee.ts` predates this session's API layer and only holds that one constant, currently **empty**). Populate it if/when a homepage image becomes dashboard-editable.
## Contact form
`src/components/contact/ContactForm.tsx` now POSTs to `POST /tenants/{domain}/contact-submissions` (`src/lib/meshkee/contact.ts`) with `{ title, name, cellNumber, text }` (the `title` field is a fixed "پیام از فرم تماس با ما" — the endpoint appears to want a subject line, unconfirmed). On success: a toast (`useToast()`), an inline success banner inside the form for ~4s, and the form resets so the visitor can send another message without leaving the page. On failure: an error toast, fields are preserved (no reset) so nothing is lost.
## Next steps
1. Once the tenant exists on the backend: fetch one real response per entity (product, store-item, category, comment, `/auth/me` user) and reconcile field names in `src/lib/meshkee/types.ts` + the `pickText()`/`normalize*()` chains — several are educated guesses (see "Meshkee API layer" above).
2. Confirm `entityType` values `workshop`/`store_item` actually work against `/comments`, or find/request the real ones.
3. Confirm whether a `/workshops` endpoint ever ships; if it lands under a different shape (e.g. folded into `instructions` or `videos` instead of its own module), update `src/lib/meshkee/workshops.ts` and the "registration row" fallback logic accordingly.
4. Resolve the tenant (`GET /tenants/{domain}`) for `businessId` and site-wide Organization JSON-LD (`business-info.schema.jsonLd`) in the root layout — not done yet, only favicon is fetched there.
5. Decide which homepage/about sections should become dashboard-editable and populate `STATIC_IMAGE_SLOT_CATALOG` (`src/lib/meshkee.ts`) accordingly.
## Verification
- `npm run lint` and `npm run build` (standalone output at `.next/standalone/server.js`).
- Visually check every page in desktop and mobile widths; RTL layout, Swiper carousels, mobile nav toggle, cart badge/popup, user menu (logged-out state at minimum — no test account exists yet), contact form success + reset.
- Every catalog list/detail page should render its **empty state**, not crash, against the current (unprovisioned) tenant — that's the expected state until the backend is live.
- `/meshkee/sitemap-config.json` should list all static routes (`/`, `/workshops`, `/products`, `/store`, `/portfolios`, `/blog`, `/about-us`, `/contact-us`) and none of the `[id]/[slug]` dynamic ones.
- `/meshkee/static-image-slots` should return `{ "slots": [] }` until slots are added.