- 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>
33 KiB
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, notailwind.config.js),src/app. lang="fa" dir="rtl"set insrc/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 insafe()(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 — seesrc/components/home/Testimonials.tsxandsrc/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 addorder-*overrides here again, that was tried and was wrong), nav links, then phone/cart/login actions, matching the real desktop header exactly. Belowlg: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 owngrid grid-cols-3 lg:hiddenblock, 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 fightingorder-*/justify-*was worse than two small dedicated blocks. SeeHeader.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 atmd:(768px), notlg:— atlg: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 bemd:grid-cols-[280px_minmax(0,1fr)]— not[280px_1fr]: a bare1frtrack let Swiper's internal width calculation blow the column out to ~16.7M px (classic CSS Grid blowout; Swiper needsminmax(0,1fr)to be constrained by its parent). The Swiper's own grid-item wrapper also needsmin-w-0directly (see theRevealaround it) — that's the part that matters belowmd, where there's no explicitgrid-template-columnsat all but the item is still a grid child withmin-width: autoby default, so the same blowout happened on mobile too until that class was added. Any future Swiper placed inside agridorflexcontainer needs this samemin-w-0treatment 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 ("باید با هیرو فاصله داشته باشه"), plainpy-16 md:py-24is correct. - Quick-links row ("چرا استاد علی عالمی؟",
QuickLinks.tsx): real icon assets (icons/play.png,exam.png,price.png,support.png) insm: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 physicalleft-*/right-*, not logicalstart/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-6andبیش از ۱۵۰ ساعت آموزشat-bottom-6 -right-6— an earlierright-1/3on 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 onIntersectionObserver, stands in for the original's WOW.js + animate.css) — requested explicitly ("انیمیشنها ... باید دقیقا شبیه سایت اصلی باشه"). - General lesson: any two-column
image + textgrid using a fixed-height image container should switch atmd:notlg:, or verify thelg:in-between range doesn't leave a tall single-column stack. Applies toWhyChooseUs.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): positionedtop-16 end-0(wasbottom-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) useborder border-ink/15 shadow-md, notring-1 ring-line— the--color-linetoken (#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 fetchesgetFavicon()once and passeslogoUrl/logoDarkUrldown to bothHeaderandFooter(in addition to the favicon it already used for<head>icons).HeaderpreferslogoDarkUrl(the dashboard's dark-background variant) →logoUrl→ the bundled local/images/logo.png; thebrightness-0 invertCSS 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).Footerjust useslogoUrl→ local, no invert (white background, original color is correct there). Both useunoptimizedonnext/imagewhen the src is a remote CMS URL, since that host isn't (and can't be, until a real tenant exists) innext.config.tsimages.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, darktext-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 arbitraryrounded-[50px]. Inactive links staytext-white/80on 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_LINKSinsrc/lib/site.ts): صفحه اصلی، دورههای آموزشی، مقالات، درباره ما، تماس با ما — per explicit instruction, not محصولات/فروشگاه/نمونهکارها. A categories-driven dropdown for "محصولات" (src/components/CategoriesMenu.tsx, fetched viagetCategories({ 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.tsxwas deleted and thecategoriesfetch/prop taken back out oflayout.tsx/Header.tsx./products,/store,/portfoliosare 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 deletedCategoriesMenu.tsxrather than starting from scratch). - Header action buttons (search/cart/user, right side of the desktop row): redesigned to solid
bg-primaryfilled 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 morelightprop — it was only ever used inside this navy header, the conditional was dead weight).UserMenu(src/components/UserMenu.tsx) similarly dropped itslightprop — 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-centerbut notjustify-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 addingjustify-centeralongside the existingjustify-self-start(that one's unrelated — it positions the button itself within the mobile header's 3-column grid row;justify-centercenters 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.tshad 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 inFeatureCards.tsxalso got a nested treatment — an outerh-16 w-16 rounded-2xltone-colored square containing an innerh-10 w-10 rounded-full bg-whitecircle 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.tsxreuses the sameFEATURESarray 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 underpublic/; 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.phplisted each page's frame stack;old-site/custom/fa/frames/*.phpandold-site/frames/**/content.blade.phpheld the actual field data and markup per frame.) old-site/settings.phpgave 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-sitefooter framefooter-footer-hCR1y.php, fieldtext): 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'sResumesection (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 "قم، میدان مفید، ساختمان پاسارگاد، طبقه سوم", phone0912 607 3354, Instagram@Alialemiteacher. Lives insrc/lib/site.ts→CONTACT.
- Bio/credentials (
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, fetchhttps://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'smk7iCwJnYx.pngis byte-identical). There's no separate white variant, soHeader.tsxappliesbrightness-0 invertto render it white on the navy bar;Footer.tsxuses 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 AboutBioIntro).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 fromhttps://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 inold-site/assets/files(never actually uploaded on the live site) — rather than inventing stock photography, those slots became flat gradient backgrounds (StatsBar,PageHeader,CustomerClubBannerall 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) wrapfetchwith Next.js data-cacherevalidate(default 60s; auth/mutating calls passrevalidate: false).safe(promise, fallback)swallows any error/404 and returns the fallback — every list/detail fetch on a page goes throughsafe()so a missing tenant or disabled module renders an empty state, never a crash.MeshkeeApiErrorcarries 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 forproduct,store-item/variant,category, orcommentobjects (confirmed by querying both docs directly this session). Fields likenameFa/name/title,titleImageUrl/imageUrl,discountedPrice/priceare 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 reconciletypes.ts+ thepickText()/normalize*()accessor chains informat.tsagainst the actual field names — don't assume the current guesses are correct.format.ts—pickText(...)tries field-name candidates in order;normalizeImage/normalizeGalleryhandle 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 withslugify(name), never a rawitem.slugfield (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 viaGET /tenants/{domain}/products/by-id/{id}. - Product category:
/products/category/{id}/{nameFaSlug}→GET /tenants/{domain}/categories/by-id/{id}, then list products withcategoryId. - 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}— noby-idresolve 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/headerscookies()). Readsmeshkee_customer_access_token, including the chunked form ({name}_ncount +{name}_0,{name}_1, ...) per AI_PROMPT.md's cookie contract.getSession()callsGET /auth/mewith that token and returns{ user, token } | null; called once insrc/app/layout.tsx(root layout is nowasync) and passed down as props — no client component calls/auth/meitself.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 theuserobject returned by/auth/meisn'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 tocustomerLoginUrl(). Logged-in →"{firstName} عزیز"with a dropdown:پروفایل من(customer.{domain}/profile),مدیریت وبسایت(business.{domain}, only whenisAdmin),خروج(customer.{domain}/logout). All three are plain cross-origin<a>tags (notnext/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.tsimplements AI_PROMPT.md's guest-cart spec exactly:localStoragekeymeshkee-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.idis always astoreItemVariantId— never a product id or store-item id.src/components/providers/CartProvider.tsx(client context, wraps the app inlayout.tsx) holds cart state; readslocalStoragein auseEffectdeferred viaqueueMicrotask(avoids both a hydration mismatch and thereact-hooks/set-state-in-effectlint error — same pattern as the siblingmoallemproject'sCartProvider, keep them consistent if either changes) and writes back to storage on every change.src/components/cart/CartButton.tsx(header icon + quantity badge) andMiniCart.tsx(slide-over popup: line items, qty stepper, remove, total, Continue) —ContinueusescontinueToCheckoutUrl(): if the access-token cookie is present it links tocustomer.{domain}/checkout/cart?guestCart=..., otherwise tocustomer.{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 === nullor> 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 listproduct|blog|portfolio|videoas validentityTypevalues;workshopandstore_itemare passed the same way as a best-effort extension (unconfirmed — verify once live,blog/portfolio/productare the safe ones). Blog and portfolio also have entity-specific/blogs/{id}/commentsroutes 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.tsxshows the comment form only whenuseris non-null (passed from the server page'sgetSession()), otherwise a "برای ثبت نظر ابتدا وارد حساب کاربری خود شوید" box linking tocustomerLoginUrl(pathname). When posting,authorNameis built from the session'sfirstName/lastNameand the access token is sent asAuthorization: Bearertoo (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, orapp/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.tsscanssrc/app/**/page.tsxat 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 viametadata/generateMetadata, preferringseoMetaTitle/seoMetaDescriptionwhen the API provides them. Exactly one<h1>per page:PageHeaderowns 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 realalt(item name, never empty, decorative floaters usearia-hidden+ empty alt instead). src/app/meshkee/static-image-slots/route.tsstill returnsSTATIC_IMAGE_SLOT_CATALOGfromsrc/lib/meshkee.ts(note: different file from the newsrc/lib/meshkee/directory —src/lib/meshkee.tspredates 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
- Once the tenant exists on the backend: fetch one real response per entity (product, store-item, category, comment,
/auth/meuser) and reconcile field names insrc/lib/meshkee/types.ts+ thepickText()/normalize*()chains — several are educated guesses (see "Meshkee API layer" above). - Confirm
entityTypevaluesworkshop/store_itemactually work against/comments, or find/request the real ones. - Confirm whether a
/workshopsendpoint ever ships; if it lands under a different shape (e.g. folded intoinstructionsorvideosinstead of its own module), updatesrc/lib/meshkee/workshops.tsand the "registration row" fallback logic accordingly. - Resolve the tenant (
GET /tenants/{domain}) forbusinessIdand site-wide Organization JSON-LD (business-info.schema.jsonLd) in the root layout — not done yet, only favicon is fetched there. - Decide which homepage/about sections should become dashboard-editable and populate
STATIC_IMAGE_SLOT_CATALOG(src/lib/meshkee.ts) accordingly.
Verification
npm run lintandnpm 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.jsonshould list all static routes (/,/workshops,/products,/store,/portfolios,/blog,/about-us,/contact-us) and none of the[id]/[slug]dynamic ones./meshkee/static-image-slotsshould return{ "slots": [] }until slots are added.