# context.md — هدیه مزون (Hediyeh Maison) Living context file for AI assistants and future contributors. **Update this file on every prompt whenever the project changes** (new pages, data source swaps, design decisions). ## What this project is A Next.js rebuild of the original Laravel/Blade "Hediyeh Maison" storefront (`../hediyeh-maison`, a Meshkee web-builder site). The original was a clothing **store**; this rebuild repurposes the same visual identity into a **portfolio / showcase site** — no cart, checkout, or product purchasing. Portfolio content is still local showcase data; Blog listing/detail content is connected to the public Meshkee Website API for `hediehmezon.ir`. While that tenant has no published articles, three clearly labelled local sample articles are shown; they disappear automatically as soon as the CMS returns any published article. - Language: Persian (`fa`) only, `dir="rtl"`. - Framework: Next.js (App Router), TypeScript, Tailwind CSS v4. - Carousel: Swiper (`swiper/react`). - Icons: Font Awesome (`@fortawesome/react-fontawesome` + free-solid / free-regular / free-brands). - Fonts: **Iran Yekan** (self-hosted `next/font/local`, all Persian text — body, UI, every heading) + **Valky** (self-hosted `next/font/local`, Latin-only display face — used *only* for the hero's "Hediyeh" / "MAISON" words). See `src/app/fonts.ts`. ## Fonts — do not use cdn.meshkee.com, do not use Google Fonts for the display face The source theme (`hediyeh-maison/settings.php`, `metadata.php`) references `cdn.meshkee.com/web-builder/fonts/...`. **These links are unreachable from this environment** — confirmed by the user; don't attempt to fetch from `cdn.meshkee.com`, it will hang/fail. - Persian body/UI face: **Iran Yekan**, per explicit user instruction — self-hosted `.woff` files copied from `../../raoofi/src/assets/fonts/` into `src/assets/fonts/`. - Latin display face: the live source site's hero heading computes to `font-family: Valky` at `font-size: 300px` (confirmed via `getComputedStyle` on `http://hediyeh-maison.local:8000/`). The user placed `Valky Regular.ttf` in `public/` — it now lives at `src/assets/fonts/valky-regular.ttf`, loaded via `next/font/local` as the `valky` export (`--font-valky` / `.font-latin` utility class in `globals.css`). **An earlier version of this site used Google's Playfair Display as a substitute — that was wrong and was replaced.** `.font-latin` (Valky) must only wrap genuine Latin/English copy (currently: the Hero's "Hediyeh" and "MAISON"). Every Persian heading uses plain `font-bold`/`font-extrabold` on the default Iran Yekan body font — Valky has no Persian glyphs, so applying it to Persian text silently falls back to a generic serif and looks wrong. If you add new Latin display copy, use `.font-latin`; if it's Persian, don't. ## Content & asset provenance The Blade project's theme-level static images (`hediyeh-maison/assets/img/**`) are **broken Git LFS pointers** (never pulled — `git lfs pull` fails, git.meshkee.com unreachable from this environment). Do not copy from `assets/img/`. The **real, working photography** lives in `hediyeh-maison/assets/files/` (hashed filenames, CMS-uploaded media, valid binaries). These were identified by inspecting the live rendered DOM (`img.currentSrc`) on the local dev site (`http://hediyeh-maison.local:8000`, ports proxied through the Blade app) and copied into `public/images/` with descriptive names. Mapping (source hash → usage): | Source file (`assets/files/`) | Used as | |---|---| | `PGnhGwcJfR.png` | `public/images/brand/logo.png` (header + footer) | | `cYbVH0Fg4Z.png` | `public/images/brand/monogram.png` (small cursive "H" mark, unused so far) | | `X8XbnYcoSl.jpg` | `public/images/hero/hero-1.jpg` | | `VEs3mZuCir.jpg` | `public/images/hero/hero-2.jpg` | | `WfD9E9TJd1.png` | `public/images/categories/trousers.png` (شلوار) | | `DbVccNhN6N.png` | `public/images/categories/sunglasses.png` (عینک آفتابی) | | `VEVtsmcNRu.png` | `public/images/categories/coat.png` (پالتو) | | `PzlA8CwvA8.png` | `public/images/categories/hoodie.png` (هودی) | | `G6U04V8fhX.png` | `public/images/categories/scarf.png` (شال و روسری) | | `ptZvorsSiP.jpg` | `public/images/banners/banner-bags.jpg` (کیف editorial banner) | | `o8x3phkJEQ.jpg` | `public/images/banners/banner-clothing.jpg` (لباس editorial banner) | | `1L3fCdTL2g.png` | `public/images/about/about-image.png` | | `7xIvsBoGth.png` | `public/images/about/signature.png` | | `uT9b3ZoiSs.jpg` | `public/images/about/storefront.jpg` (⚠ shows a *different* real shop's signage — "UTERO" — currently unused in any page; do not display it as Hediyeh Maison's own storefront) | | `2SLidM7KQV.jpg`, `5vCbHMQbmt.jpg`, `6ncMx3U2nt.jpg` | `public/images/interior/*.jpg` (boutique interior shots, reused across about/portfolio/blog) | | `pHcCJphsTp.png`, `oryXTQOfe0.png`, `swJLLftw08.png`, `1t8I5IZoob.png`, `kTwhmXLhxe.png` | `public/images/portfolio/*-alt.png` (duplicate product angles, used as extra portfolio/blog imagery) | Several other files in `assets/files/` are unrelated demo leftovers (cake/bakery photos, duplicate wavy-pattern subheader banners with baked-in **English** copy like "About Us", "Contact Us") and were intentionally **not** used — see next section. ### Subheader banners were rebuilt as SVG, not reused as images The source site's inner-page hero banners (`assets/files/5qiwBQYHMA.jpg` and several duplicates) are a cream background with a wavy line texture **and English text baked into the raster** ("About Us", "Contact Us", "Products", "Articles", "Article Details", "Product Details"). Since this site is Persian/RTL with different headings, reusing those images verbatim would show mismatched/wrong-language text. Instead, `WavePattern.tsx` recreates the line texture as inline SVG, composed in `PageHero.tsx` with our own Persian `

`. This is the pattern used for About/Portfolio/Blog/Contact hero banners. ### Hero — rebuilt to match the live source structure exactly An earlier version of the hero was a loose reinterpretation (wrong font, a single centered tagline + invented CTA button, MAISON pinned to the bottom, opacity-faded giant text) and the user flagged it as "nothing like the original." It was rebuilt from the *actual* live DOM of `http://hediyeh-maison.local:8000/` (`.modern-slider-item__*` classes), read via `getComputedStyle`/`getBoundingClientRect`, not from a screenshot guess. Ground truth: - Giant background word: `

`, `font-family: Valky`, `font-size: 300px`, `color: rgb(180,144,118)` (= `--color-primary`, **full opacity**, not faded) — full-bleed, allowed to overflow both edges of the viewport since it's wider than the container. `Hero.tsx` reproduces this as an absolutely-positioned `.font-latin` span using responsive fixed sizes and `text-primary` (no `/opacity`). The current compact desktop direction caps it at 220px/240px rather than the source's 300px. - "MAISON": also Valky, white, `font-size: 48px` (≈16% of the main heading), positioned around **64% down the photo**, not flush to the bottom. - The arch photo's real aspect ratio is `480:820` (tall/narrow, ≈0.585), not a generic 3:4 crop — `Hero.tsx` uses `aspect-[480/820]`. - The two Persian white pill labels that flanked the central image were removed per the current design direction; do not restore them unless explicitly requested. - There is **no CTA button** in the source hero — the earlier "مشاهده نمونه‌کارها" button was invented and has been removed; portfolio access stays in the nav. - `hero-2.jpg` (`VEs3mZuCir.jpg`) is a genuinely black-and-white editorial photo in the source data, not a bug — don't "fix" it to color. - A small `.modern-slider-item__circle` decorative badge (~117×117px, top-right of the photo) exists in the source but its image asset is itself a broken/404 link even on the live site — intentionally not reproduced. If the hero still looks off, re-diff against the live site with the same `getComputedStyle`/DOM-read approach rather than eyeballing a screenshot — the CMS positions things with absolute px/percent values that are easy to misjudge visually. **Round 2 fixes** (user supplied a screenshot of the real source hero at a wide desktop viewport and it still looked "nothing like" this site — correctly, two things were still wrong): 1. **Missing outline ring.** The source photo has a second, larger arch-shaped thin outline offset outward around it (visible clearly at the top and, more, below the photo's own bottom edge). This isn't reproducible from the source DOM (the one plausible source element, `.modern-slider-item__circle`, is a small unrelated ~117×117 badge whose own image 404s even on the live site) — it's rebuilt directly as a CSS decoration: an `aria-hidden` absolutely-positioned sibling div, same arch shape, offset via negative inset (`-inset-3 -bottom-6` scaling up to `-inset-5 -bottom-12` at `lg`), `border border-primary/45`. 2. **Photo/text sized like the 753px-wide measurement, everywhere.** The first pass measured the source at a narrow ~753px pane and used those proportions (photo capped ~260px, text via `vw`) at *every* viewport. But the source uses **fixed, non-fluid** px values above some breakpoint — re-measuring the source at 1585px showed the image at its **native 480×820**, not scaled down. The current compact implementation steps the photo width through `42vw` → `300px` → `340px` → `380px`. The slide now uses normal-flow top/bottom padding around the artwork, so its height follows `slot.aspectRatio` instead of relying on hard-coded slide heights. This keeps the image close to the header and the pagination close to the image even if the CMS ratio changes. Also: a stray `max-w-[210px]` on the photo wrapper was accidentally left active at *every* breakpoint (Tailwind doesn't auto-clear an unprefixed utility at larger breakpoints — a later `lg:w-[380px]` does not remove an earlier bare `max-w-[210px]`), silently capping the photo at 210px regardless of viewport. Watch for this pattern elsewhere: any bare (unprefixed) sizing utility mixed with responsive overrides needs an explicit `sm:max-w-none`-style reset, or just don't set it unprefixed at all. **Round 3 fix (exact source values, read via `getComputedStyle`)**: the photo `img` has `border-radius: 1000px` on *all* corners (a full pill — rounded bottom too, not just the top arch), and the outline ring is `.modern-slider-item__image::before` with `border-radius: 1000px`, `1px solid rgb(199,189,182)` (`#c7bdb6`), `inset: -20px` uniformly (same offset at the bottom as the top — an earlier version wrongly extended it lower). `Hero.tsx` now uses `rounded-[1000px]` on both the photo and the ring, ring color `border-[#c7bdb6]`. When verifying the hero visually, take the screenshot at a genuinely wide viewport (`resize_window` with an explicit `width`/`height`, e.g. 1280×760+ — not the Browser pane's default responsive size, which is much narrower and misrepresents how the fixed-px hero reads at real desktop widths) — and take it twice if the first one looks blank/broken, since this pane occasionally serves a stale/mid-transition frame during Swiper autoplay. ### Meshkee footer credit (bottom bar) Per user instruction, the footer's bottom row matches **https://safeteb.com/** exactly: site name on one side, a `meshkee-credit` link (hover border animation, Meshkee logo, "Designed And Developed By" / team name) on the other, linking to `https://meshkee.com`. CSS was extracted live from safeteb.com and ported into `globals.css` (`.footer-bottom-bar`, `.meshkee-credit*` rules). The logo asset was downloaded from `https://safeteb.com/images/footer/MeshkeeLogo-White.png` → `public/images/brand/meshkee-logo-white.png`. Implementation: `components/layout/Footer.tsx`. ### Real site content vs. placeholder data The source Blade site (`hediyeh-maison/custom/fa/pages.php`, `frames/*/config.php`) is mostly the **Meshkee web-builder demo/default theme** — most config-level text is literal placeholder ("متن تستی", lorem ipsum, fake nav items like "صفحه ی آماده 1..29"). The actually meaningful content came from what renders live on the dev server (DB-driven overrides), captured via browser inspection: - Home: hero taglines, feature strip (support/variety/secure payment — adapted, see below), 5 category tiles (شلوار/عینک آفتابی/پالتو/هودی/شال و روسری with real counts), 2 editorial banners (کیف/لباس), newsletter section. - About: founded year 1986, 3× 95% stats, tagline "از طبیعت، پاستیل رنگی و فعالیت‌های روزانه الهام گرفته‌ایم." - Contact: address "قم، سالاریه، میدان پیچک", phone `09123456789`, email `name@info.com` (footer's placeholder address differed from the contact page's; the contact page's address was kept as the single canonical one across footer + contact, per normal practice — see `lib/site-config.ts`). - Articles (`/articles`) page could not be inspected — it errors locally (TLS error calling `www.meshkee.ir/api/v1/get-articles`). **Per explicit user instruction, blog and portfolio card designs are original work**, not cloned from a live reference. Blog data now comes from Meshkee through `src/lib/meshkee.ts`; product showcase data remains in `data/portfolio.ts`. ### Deliberate content adaptations (store → portfolio) Since the site is no longer a store: - Nav "محصولات" (Products) → "نمونه‌کارها" (Portfolio); "مقالات" kept as Blog. - Feature strip's "اکنون بخرید، بعدا پرداخت کنید" (buy-now-pay-later) dropped — replaced with "مشاوره تخصصی" (expert consultation), keeping support + variety as-is. - Category tile counts relabeled "XX مورد" → "XX نمونه‌کار". - Editorial banner CTAs "خرید" / "همین حالا خرید کنید" → "مشاهده نمونه‌کارها". - Newsletter/"club" CTA "عضویت" kept (generic, still applies to a portfolio site). - Newsletter remains client-side only. Contact submissions are proxied server-side by `src/app/api/contact/route.ts` to `POST /tenants/{domain}/contact-submissions`, with validation and reset/success states. ## Design tokens (`src/app/globals.css`, `@theme` block) `--color-primary`/`secondary`/`tertiary` come from the original theme's `settings.php`. `--color-cream` is **not** a guess — it's the live-computed background color of the source hero section (`getComputedStyle` → `rgb(226,219,214)`), so this now matches the original's actual muted taupe-grey rather than a warmer generic "cream": ``` --color-cream: #e2dbd6 body background (source hero bg, computed live) --color-cream-deep: #d5c9c0 section/footer background (darker shade of the above) --color-primary: #b49076 CTA, accents, big display text (source primaryColor) --color-primary-dark: #8f6f58 hover state --color-secondary: #8fa06c sparse accent (source secondaryColor, muted) --color-tertiary: #2a9fc9 sparse accent (source tertiaryColor, muted) --color-ink: #241f1a body text --color-ink-soft: #6c6259 muted text --color-slate: #5c6873 one-off accent — the source hero's right-side tagline text color (computed live), used only there --color-border: #d3c5bb ``` ## Page inventory | Route | Notes | |---|---| | `/` | Hero (Swiper), feature strip, category circles, editorial banners, newsletter | | `/portfolios` | Portfolio-backed products/sample works. Sidebar category list + 3-col card grid, reads `?category=` | | `/portfolios/[id]/[titleFaSlug]` | Canonical portfolio detail path via `portfolios/by-id/{id}` with local fallback while CMS is empty; stale title slugs redirect; right-side interactive gallery with a large image, clickable thumbnails, slide navigation, hover zoom and an accessible full-screen viewer | | `/blog` | Real Meshkee CMS articles; temporary sample cards only while the API list is empty | | `/blog/[id]/[titleSlug]` | CMS detail from `blogs/by-id/{id}`; redirects a stale title slug to its canonical path; accessible breadcrumb, rich text/tables and sticky related accordion | | `/about` | Founded-year stat, story, 3 stats, closing image band | | `/contact` | Info list, embedded Google Maps iframe (no API key needed, `output=embed`), contact form | ## Meshkee Website API integration Per `https://api.meshkee.com/docs/website/AI_PROMPT.md` (the Meshkee Website API brief — read this before touching data fetching): - Canonical tenant domain: `hediehmezon.ir`; `GET /tenants/hediehmezon.ir` resolves to business id `50` and confirms the `blog` module is enabled. Override locally with `MESHKEE_WEBSITE_DOMAIN` when necessary. - Home static media is fetched once by `src/app/page.tsx` through `GET /tenants/{websiteDomain()}/website/static-images?pageKey=home`. It always uses the project's central `websiteDomain()` resolver—never a second/hard-coded tenant. The page matches `slider`, `categories`, and `two-banner-below-slider`; list slots render every image and single slots render only `images[0]`. Empty slots, API errors, or missing keys retain the existing local imagery. `url` is rendered as native ``, `linkUrl` wraps the item in ``, and the API `aspectRatio` controls the media box. - `GET /meshkee/static-image-slots` publishes the dashboard catalog: duplicatable `slider`, duplicatable `categories`, and a two-item `two-banner-below-slider`. **Whenever a static media box's rendered size or aspect ratio changes, update this catalog in the same change** (`aspectRatio` and, when relevant, `recommendedWidth` / `itemCount`). Never invent upload or dashboard write APIs. - `src/lib/meshkee.ts` owns server-only blog reads. It resolves the tenant first, then uses `GET /tenants/{domain}/blogs` and `GET /tenants/{domain}/blogs/by-id/{id}` with five-minute Next data-cache revalidation. Detail pages honor API 404 via Next `notFound()`, emit CMS `schema.jsonLd`, and prefer `seoMetaTitle` / `seoMetaDescription`. - Related articles are fetched with the current article's `categoryId`, current id excluded, and at most five items rendered. The native `
` accordion avoids shipping a Client Component solely for collapse behavior. - `src/data/fallback-blogs.ts` contains explicitly labelled sample copy because the tenant had zero published articles on 2026-09-29. `/blog` uses it only when the real list is empty; detail fallback ids are also disabled as soon as the CMS list becomes non-empty. - `DetailPageShell.tsx` is shared by blog and product canonical detail pages; do not re-create detail-page max width, horizontal padding, vertical padding, or breadcrumb placement inside individual detail pages. - CMS rich text uses `.cms-rich-text` in `globals.css`. `normalizeArticleHtml()` supplies a meaningful fallback alt to CMS images and wraps every incoming `` in its own focusable `.cms-table-scroll` region. The wrapper—not the article/page—owns horizontal scrolling. - `data/portfolio.ts` remains temporary local presentation data while this tenant's public portfolio list is empty. These visible "products" are portfolio/sample-work records, not store products; their public URLs follow `/portfolios/{id}/{titleFaSlug}`. - Portfolio detail imagery is rendered by the isolated client component `components/portfolio/PortfolioGallery.tsx`; the page and CMS fetching remain server-side. It consumes every unique URL assembled from the portfolio's title image, thumbnail, legacy image and `images[]`, shows one large image with a thumbnail rail when more than one image exists, supports Swiper drag/keyboard controls, pointer-position hover zoom, and an accessible full-screen viewer. Do not return to the old two-column grid of equally sized images. - The detail page's “محصولات دیگر” row is `RelatedPortfolioCarousel.tsx`: two cards on mobile, three on tablet and five on desktop, with touch/keyboard dragging and clickable pagination. CMS-backed pages fetch the current category dynamically from `GET /portfolios` and exclude the current record; when that category has too few results they append unique items from the full portfolio list. This is data-driven—do not restore a fixed four-card grid or hard-code future CMS products. - This is explicitly **not an e-commerce site** — do not add local login/register/cart/checkout. Shared customer authentication is still reflected in the header from the parent-domain Meshkee cookies; the actual login/profile UI remains on `customer.hediehmezon.ir`. - `src/lib/auth.ts` reconstructs normal or chunked `meshkee_customer_access_token` cookies. `GET /api/auth/session` calls official `GET /auth/me` server-side and returns only the name and admin flag required by the header. Guests link to `customer.hediehmezon.ir/login`. Logged-in users see “{name} عزیز” with profile and logout; admin/owner roles also see `business.hediehmezon.ir`. `POST /api/auth/logout` only expires shared access/refresh cookies. - `next.config.ts` already sets `output: "standalone"` per the brief's mandatory production runtime rule. - Do not add local `public/sitemap.xml`, `public/robots.txt`, `app/sitemap.ts`, or `app/robots.ts` — Meshkee nginx proxies these. - `scripts/generate-sitemap-config.mjs` scans `src/app` before `dev` and `build`, excludes dynamic/private/auth/cart/checkout/admin routes, and publishes `public/meshkee/sitemap-config.json`. Keep `templates` absent while canonical detail paths match Meshkee defaults. - `src/lib/paths.ts` is the single source for Persian-preserving canonical slug generation. - Every public page has a unique title/description/canonical URL and exactly one visible H1. - The remaining site-wide integration is Organization/LocalBusiness JSON-LD from `business-info.schema.jsonLd` in the root layout, and per-page SEO meta overrides (`seoMetaTitle`/`seoMetaDescription`) per the brief. ## Running locally ```bash npm run dev # Next dev server (Turbopack) npm run build # regenerates sitemap-config, then creates the standalone production build ``` A `.claude/launch.json` exists at the `hediyeh-mason/` repo root (one level up) to preview this app via the `hediyeh-maison-website` config (`npm --prefix website run dev`). ## Product & article cards (source-derived) The visible products are sample works backed by the Meshkee portfolio module. Until portfolio records are published, the existing cards use local presentation data under the canonical `/portfolios/{id}/{titleFaSlug}` path. **Never write "نمونه‌کار" in the UI** — categories/counts say "محصول" ("۲۶۵ محصول"), nav says "محصولات". Cards were rebuilt from the source theme's blade + CSS (the live `/products` and `/articles` pages error locally, so read `frames/product_show` / `frames/article_show` and `assets/css/theme7b30.css`): - `PortfolioCard`: `.product-grid-item` — portrait image (`padding-bottom:122.6%`, square corners), second image cross-fades on hover, hover "مشاهده محصول" button, 18px title, Latin name (`titleEn`), category tag. No price/rating/cart. `PortfolioGrid` = sidebar category list (counts) + 3-col grid, like the source's `col-lg-3` filter + `col-lg-9` grid. - `BlogCard`: `.modern-blog__post` — 15px-radius image (`padding-bottom:84%`), bottom black gradient, `category / date` meta + white title overlaid at the bottom. No excerpt/read-time. ## Home sections — source-accurate details (round 4) The **page background is white** (`body` has no background in the source theme); only the hero (`#e2dbd6`), inner-page subheaders and the category pills use the taupe. Newsletter band is `#fafafa`, footer has no background + `border-top:#dedede`. Header is taupe over the home hero (until scrolled) and white elsewhere. - **Mobile header/navigation**: the header is 64px tall (`h-16`); the logo sits left and the hamburger/login controls sit right, with the hamburger first from the left. Navigation opens as an animated white drawer from the right over a blurred overlay. The active route uses `bg-cream-deep/50`, not the full-strength cream-deep background. - **Feature strip** (`.features--modern`): 3 items spread in one row (first flush start, last flush end), primary-colored 36px icon, 22px title, and **no vertical dividers**. The next category section owns the shared hairline divider at every breakpoint. - **Categories** (`CategoryCircles`): a real Swiper carousel with 2 slides per view below 640px, 3 from 640px, and 5 from 1024px. It supports touch/drag and keyboard navigation. Mobile/tablet show clickable pagination dots below the cards; desktop hides pagination because all five cards fit. Preserve the category artwork's native `272:350` ratio and keep labels in normal flow below it—do not compress the art or pull labels upward with negative margins. This section owns the divider above it (visible at every breakpoint) and keeps responsive top padding on a wrapper outside Swiper between that divider and the artwork; do not put this spacing on `.swiper` itself because Swiper's own CSS resets it. CMS-backed category cards use `images[].titleFa` beneath the image and omit the local product count. Titles keep the source's literal tatweel ("شلــــــــــوار", "پالتــــــــــو", "هـــــــــــودی") — no CSS dash. Product PNGs already contain their own pill background, so they're rendered directly (no wrapper background, or you get a double ring). - **Banners** (`.modern-banners`): **two banners side by side** (`col-lg-6`), 15px radius, image ratio 84.5%, centered content: 100px title, 370px description, 34px underline under the link. Copy: "کیف / شیک و مجلسی، فقط برای شما" and "لباس / پوشاک، کفش، کیف و لوازم جانبی، اقلام ضروری برای این فصل". CMS-backed banners use `titleFa`, `titleEn`, and `subtext` and adopt the slot's API-provided aspect ratio.