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

33 KiB
Raw Blame History

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.

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.