# moalem.shop — context for AI agents and tooling Storefront for **فروشگاه موبایل معلم** built on the Meshkee CMS contract. Keep this file current: every prompt that changes structure, data sources or fallbacks should update it. ## Stack - Next.js 16.3 (App Router, Turbopack) · React 19 · TypeScript - Tailwind CSS v4 (`@theme inline` tokens in `src/app/globals.css`) - Swiper 14 for every slider/carousel - fa / RTL, flat design, no gradients ## Data — Meshkee Website API - Base: `https://api.meshkee.com/api/v1`, docs: - Tenant: `moalem.shop` (business id 27, `specialProductsSource: store_item`) - Every public read goes through `src/lib/api.ts`; wrap calls in `safe()` so an empty or failing module degrades one section instead of the page. - Never invent CMS/admin endpoints. If OpenAPI and any prose conflict, OpenAPI wins. ### What the tenant actually returns today (2026-09-06) | Endpoint | State | |---|---| | `enabledModules` (`GET /tenants/moalem.shop`) | `["products", "blog", "finance"]` — **`workshops` is not enabled.** Do not build a workshops page/nav entry until it is. | | `/products` | **479 items**, real titles, images, categories | | `/categories?entityType=product` | **48 items**, 3 levels under «کالای دیجیتال». Products are filed on **leaf** categories only — a parent category page/carousel must query its whole subtree (`subtreeIds`/`subtreeIdsAll` in `src/lib/categoryTree.ts`). | | `/store-items`, `/store-specials` | **empty** → no prices, no discounts, everything `inStock:false`. `/store-items/by-product/{id}` returns a single `{ storeItem }` (or null) — for a product's full variant list use `/store-items?productId=&pageSize=100` (`{ items }`) instead, see `getStoreItemsByProduct` in `src/lib/api.ts`. | | `/website/sliders`, `/website/category-groups` | **empty** | | `/website/static-images` | **filled** — `slider` (2), `categories` (8 of 15), `one/two/three-banner-bg` all have real images. Verified live and rendering correctly; if a future check finds otherwise, suspect a stale build/deploy before suspecting the wiring. | | `/blogs` | **empty** (module enabled) | | `/website/business-info` | phones/addresses/socialMedia still empty; `logoUrl`/`faviconUrl` are now set but point to a **different business's** mark (see "Brand") | | `/products/by-id/{id}/technical-info` | `{ form: null, values: [] }` — empty on every product checked so far | | `/comments` | **empty**, but `POST` works (no bearer required by the API itself) | ### Fallbacks in place until the CMS is filled | Gap | Fallback | Remove when | |---|---|---| | No prices | `src/lib/demoPricing.ts`, gated by `NEXT_PUBLIC_DEMO_PRICING=1` — deterministic price per product id | store-items exist | | No blog posts | `src/data/fallbackBlogs.ts` — the real posts from moalem.shop/articles, shown on the homepage and `/blog` as non-clickable cards (no real detail page exists for them) | `/blogs` returns items | | No category art past slot position 8 | placeholder SVGs in `public/images/categories/{id}.svg`, generated by `scripts/gen-category-placeholders.mjs` for every category at every level, not just level-2 | the `categories` slot has an image for that position | | Wrong logo/favicon in API | `getFavicon()` is wired live (see "Brand") but the uploaded asset itself is wrong; no code fallback needed once the dashboard upload is fixed | the correct asset is uploaded | ## Brand Colors taken from the live moalem.shop theme, not invented: primary `#E51E22`, dark `#B10123`, ink `#2E2E2E`, body `#7C7C7C`. Fonts: IRANYekan (fa, local `public/fonts`) + Montserrat (latin, next/font). - Logo/favicon: `getFavicon()` in `src/lib/api.ts` reads `GET /tenants/moalem.shop/website/favicon` — the documented single source — and `Header.tsx`/`Footer.tsx`/`generateMetadata()` in `layout.tsx` all use it, falling back to the bundled `/images/logo.png` / `/favicon.png` only when the API field is null. **As of 2026-09-06 the dashboard has a logo/ favicon uploaded, but it's the wrong business's** (a home-appliances store's mark, not موبایل معلم) — the wiring is correct and will pick up the right asset automatically the moment it's replaced in the dashboard; don't "fix" this by hardcoding the local fallback again. - Contact data (phone, mobile, branch addresses, Instagram/Telegram) in `src/data/site.ts` is real, pulled from the live theme's footer — `GET /website/business-info` still returns empty `phoneNumbers`/`addresses`/ `socialMedia` for this tenant, so these stay hand-maintained here until that's filled in. Update both together when it is. ## Layout conventions - Content width `--container-boxed: 1200px`, applied with the `boxed` utility. - Homepage order: promo strip → header → hero slider → category carousel → hot deals → product carousels (interleaved with promo banners) → magazine carousel → footer. Exact interleaving is in "Static-image slots" below. - Section header (title + one-line desc on the right, «مشاهده همه» pill on the left) is `src/components/ui/SectionHeader.tsx` — reuse it for every carousel. - Product card: `ui/ProductCard.tsx`, 8px radius, discount badge top-right, strikethrough old price above the current one. The price block is a fixed 44px row and the old-price line is always rendered (empty when there is no discount) so **every card in a carousel is exactly the same height** — keep that invariant when adding badges or meta lines. - Carousel arrows: `ui/CarouselNav.tsx`, passed to `SectionHeader` as `nav` so they sit beside the «مشاهده همه» pill on desktop; the copy under the track is `lg:hidden` for thumbs. - Prices are Toman and rendered with Persian digits (`src/lib/format.ts`). ## Navigation - **Desktop (lg+):** full header — logo, search, account/cart, then the product mega menu and the main nav. - **Mobile:** no hamburger. The header carries the search box alone; everything else lives in `MobileBottomNav.tsx`, a fixed bottom bar with خانه / سبد خرید / دسته‌بندی‌ها / ورود (or the account menu once signed in). «دسته‌بندی‌ها» opens a bottom sheet with the level-2 categories and an accordion for their children. Any new mobile entry point belongs in that bar, not in a drawer. ## Auth / account menu Login itself happens off-site, on the customer portal (`customer.{domain}`) — this storefront never renders a login form. For the header/bottom-nav account menu to show "نام عزیز" instead of "ورود | ثبت‌نام", the customer portal needs to set a **non-HttpOnly cookie on the shared parent domain** (`.{domain}`) named `AUTH_TOKEN_COOKIE` (`src/lib/config.ts`, currently `"meshkee_token"`) holding the bearer access token. Until that exists, `getSession()` in `src/lib/auth.ts` always resolves to `null` and the anonymous view is all anyone will see — this is expected, not a bug, and nothing else breaks. - `src/lib/auth.ts` — **server-only** (`next/headers`): `getSession()` reads the cookie and calls `GET /auth/me`. - `src/lib/authUser.ts` — pure helpers (`displayName`, `isAdmin`), safe to import from client components. **Never import `auth.ts` from a client component** — it pulls in `next/headers` and breaks the build; that's the entire reason this file is split out. - Admin detection (`isAdmin()`) reads `user.isAdmin`/`user.role` defensively — the OpenAPI schema doesn't document a role field on the user object, so this is a best guess. Update it once the real shape is confirmed. - Cart ("افزودن به سبد خرید" in `ProductBuyPanel.tsx`) needs a real `storeItemVariantId`, which only exists once a product has store-items — today that's none of them, so the button always shows "این محصول هنوز برای خرید آنلاین فعال نشده است" regardless of login state. That's correct given the data, not a bug either. ## Static-image slots Slot catalog: `src/app/meshkee/static-image-slots/route.ts`. Keys and where each is consumed: | Key | Kind | Consumed by | |---|---|---| | `slider` | list | `HeroSlider.tsx` — wins over `FALLBACK_SLIDES` when it has images | | `categories` | list | `CategoryCarousel.tsx` — images matched **positionally** to the category list (business fills them in the same order); a category past the end of the slot keeps its SVG placeholder | | `one-banner-bg` | single | `PromoBanners.tsx` → `OneBanner` | | `two-banner-bg` | list (2) | `PromoBanners.tsx` → `TwoBanners` | | `three-banner-bg` | list (3) | `PromoBanners.tsx` → `ThreeBanners` | **Rule:** every `aspectRatio`/`recommendedWidth` in that route file must match what the component actually renders. If you change a banner's size or ratio, update its slot entry in the same commit — that file is the only thing telling the dashboard (and the designer) what to upload. Homepage banner placement (mirrors the logilook.com reference given for this layout): hero → categories → hot deals → **three-banner-bg** → موبایل carousel → **two-banner-bg** → لوازم جانبی → لپ تاپ → **one-banner-bg** → کنسول بازی → مجله. ## Static pages - `/about` — real copy transcribed from the live moalem.shop theme's about-us module (verified against its cached HTML, not invented); brand strip is plain text wordmarks (no scraped logo images — avoids a trademark/asset-licensing headache for names we don't have real logo files for) with real product photos pulled from the API. - `/contact` — `ContactForm.tsx` posts to the real `POST /contact-submissions` (no bearer required); branch cards use `BRANCHES` from `src/data/site.ts` plus a key-less Google Maps `output=embed` iframe per branch (no API key needed for a query-based embed). - `/installments` — flat, brand-colored hero (no photo, no gradient — this site stays flat) instead of the reference design's photographic banner; category highlights reuse the same `categories` static-image slot images as the homepage carousel, matched by the same positional index. One reference category ("خانه هوشمند") has no equivalent on this tenant and is swapped for "لوازم برقی" — see `HIGHLIGHT_IDS` in the page for the mapping. ## Product / blog pages - `/products` — full catalog, `?name=` search, `?page=`. - `/products/category/{id}/{slug}` — category listing; merges every leaf in the subtree (see the leaf-category note above) and paginates in memory. - `/products/{id}/{slug}` — detail: `ProductGallery.tsx` (click-to-lightbox + hover-zoom via a background-position overlay, desktop only), `ProductBuyPanel.tsx` (variants, price, auth-aware cart), `ProductInfoTabs.tsx` (توضیحات / مشخصات فنی / دیدگاه کاربران), related products from `relatedProducts` on the by-id response. - `/blog`, `/blog/{id}/{slug}` — same shape, no variants/tabs; just content + `CommentSection.tsx`. Fallback posts (see table above) only exist in the listing grid, not as real detail pages. - **Technical specs**: `values[]` from `technical-info` carries no label, only a `fieldId` — never render it directly. Join against `form.fields` first via `src/lib/technicalInfo.ts#buildSpecRows`. - **Comments**: `POST /comments` doesn't require a bearer token at the API level, but `CommentSection.tsx` gates the form behind `getSession()` per the page brief — anonymous visitors get a login prompt instead of a textarea. ## URLs (Meshkee canonical shapes) - Product `/products/{id}/{nameFaSlug}` - Category `/products/category/{id}/{nameFaSlug}` - Blog `/blog/{id}/{titleSlug}` - Helpers live in `src/lib/slug.ts` — build links with those, never by hand. ## Published for the platform - `GET /meshkee/static-image-slots` — slot catalog for the dashboard Refresh button - `GET /meshkee/sitemap-config.json` — static pages only, **auto-scanned from `src/app` at request time** (`src/app/meshkee/sitemap-config.json/route.ts`) rather than hand-listed, so it can never advertise a page that doesn't exist yet. Product/category/blog/portfolio detail pages are excluded on purpose — Meshkee resolves those straight from the API. - Do **not** add `app/sitemap.ts` or `app/robots.ts`: nginx proxies `/sitemap.xml` and `/robots.txt` to the API. ## SEO rules for every new page Unique `` + meta description, exactly one `<h1>`, real `<h2>` structure, non-empty `alt` on every content image. ## Scripts ```bash npm run dev # localhost:3000 npm run build node scripts/gen-category-placeholders.mjs # refresh category thumb placeholders ```