Files

26 KiB
Raw Permalink Blame History

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 <h1>. 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: <h2 class="modern-slider-item__main-heading">, 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.

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 <img src>, linkUrl wraps the item in <a>, 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 <details open> 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 <table> 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

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.