Files

376 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
### 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 `<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
```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.