26 KiB
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-hostednext/font/local, Latin-only display face — used only for the hero's "Hediyeh" / "MAISON" words). Seesrc/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
.wofffiles copied from../../raoofi/src/assets/fonts/intosrc/assets/fonts/. - Latin display face: the live source site's hero heading computes to
font-family: Valkyatfont-size: 300px(confirmed viagetComputedStyleonhttp://hediyeh-maison.local:8000/). The user placedValky Regular.ttfinpublic/— it now lives atsrc/assets/fonts/valky-regular.ttf, loaded vianext/font/localas thevalkyexport (--font-valky/.font-latinutility class inglobals.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 plainfont-bold/font-extraboldon 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.tsxreproduces this as an absolutely-positioned.font-latinspan using responsive fixed sizes andtext-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.tsxusesaspect-[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__circledecorative 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):
- 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: anaria-hiddenabsolutely-positioned sibling div, same arch shape, offset via negative inset (-inset-3 -bottom-6scaling up to-inset-5 -bottom-12atlg),border border-primary/45. - 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 through42vw→300px→340px→380px. The slide now uses normal-flow top/bottom padding around the artwork, so its height followsslot.aspectRatioinstead 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 straymax-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 laterlg:w-[380px]does not remove an earlier baremax-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 explicitsm: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, emailname@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 — seelib/site-config.ts). - Articles (
/articles) page could not be inspected — it errors locally (TLS error callingwww.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 throughsrc/lib/meshkee.ts; product showcase data remains indata/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.tstoPOST /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.irresolves to business id50and confirms theblogmodule is enabled. Override locally withMESHKEE_WEBSITE_DOMAINwhen necessary. - Home static media is fetched once by
src/app/page.tsxthroughGET /tenants/{websiteDomain()}/website/static-images?pageKey=home. It always uses the project's centralwebsiteDomain()resolver—never a second/hard-coded tenant. The page matchesslider,categories, andtwo-banner-below-slider; list slots render every image and single slots render onlyimages[0]. Empty slots, API errors, or missing keys retain the existing local imagery.urlis rendered as native<img src>,linkUrlwraps the item in<a>, and the APIaspectRatiocontrols the media box. GET /meshkee/static-image-slotspublishes the dashboard catalog: duplicatableslider, duplicatablecategories, and a two-itemtwo-banner-below-slider. Whenever a static media box's rendered size or aspect ratio changes, update this catalog in the same change (aspectRatioand, when relevant,recommendedWidth/itemCount). Never invent upload or dashboard write APIs.src/lib/meshkee.tsowns server-only blog reads. It resolves the tenant first, then usesGET /tenants/{domain}/blogsandGET /tenants/{domain}/blogs/by-id/{id}with five-minute Next data-cache revalidation. Detail pages honor API 404 via NextnotFound(), emit CMSschema.jsonLd, and preferseoMetaTitle/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.tscontains explicitly labelled sample copy because the tenant had zero published articles on 2026-09-29./bloguses it only when the real list is empty; detail fallback ids are also disabled as soon as the CMS list becomes non-empty.DetailPageShell.tsxis 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-textinglobals.css.normalizeArticleHtml()supplies a meaningful fallback alt to CMS images and wraps every incoming<table>in its own focusable.cms-table-scrollregion. The wrapper—not the article/page—owns horizontal scrolling. data/portfolio.tsremains 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 andimages[], 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 fromGET /portfoliosand 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.tsreconstructs normal or chunkedmeshkee_customer_access_tokencookies.GET /api/auth/sessioncalls officialGET /auth/meserver-side and returns only the name and admin flag required by the header. Guests link tocustomer.hediehmezon.ir/login. Logged-in users see “{name} عزیز” with profile and logout; admin/owner roles also seebusiness.hediehmezon.ir.POST /api/auth/logoutonly expires shared access/refresh cookies.next.config.tsalready setsoutput: "standalone"per the brief's mandatory production runtime rule.- Do not add local
public/sitemap.xml,public/robots.txt,app/sitemap.ts, orapp/robots.ts— Meshkee nginx proxies these. scripts/generate-sitemap-config.mjsscanssrc/appbeforedevandbuild, excludes dynamic/private/auth/cart/checkout/admin routes, and publishespublic/meshkee/sitemap-config.json. Keeptemplatesabsent while canonical detail paths match Meshkee defaults.src/lib/paths.tsis 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.jsonLdin 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'scol-lg-3filter +col-lg-9grid.BlogCard:.modern-blog__post— 15px-radius image (padding-bottom:84%), bottom black gradient,category / datemeta + 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 usesbg-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 native272:350ratio 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.swiperitself because Swiper's own CSS resets it. CMS-backed category cards useimages[].titleFabeneath 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 usetitleFa,titleEn, andsubtextand adopt the slot's API-provided aspect ratio.