Files
ali-alemi/CONTEXT.md
T
amirhosein.ashourlooandClaude Sonnet 5 8f102a8250 Initial commit: Ali Alemi storefront (Next.js, Meshkee API integration)
Farsi RTL Next.js 16 storefront for استاد علی عالمی, cloned from the
original Meshkee web-builder site and rebuilt with Tailwind 4, live
Meshkee API integration (products, store items, portfolios, blog,
workshops, comments, cart, customer auth), and standalone production
output for the shared-VM deploy.

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

26 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. 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/login actions, matching the real desktop header exactly.
  • 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.
  • 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.

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.