diff --git a/.cursor/rules/meshkee-static-images.mdc b/.cursor/rules/meshkee-static-images.mdc new file mode 100644 index 0000000..d11a26c --- /dev/null +++ b/.cursor/rules/meshkee-static-images.mdc @@ -0,0 +1,15 @@ +--- +description: Keep Meshkee static-image slots and their storefront catalog synchronized +alwaysApply: true +--- + +# Meshkee Static Images + +- Use the canonical contract at `https://api.meshkee.com/docs/website`; do not invent upload or dashboard APIs. +- Fetch static-image slots from `/api/v1/tenants/aradarisman.com/website/static-images`. Match placeholders by `slot.key`. +- Use every image for `kind: "list"` and only the first image for `kind: "single"`. +- Render `image.url`, use `titleFa` / `subtext` for copy, and wrap in a link when `linkUrl` is present. +- Keep local placeholder images when a slot is empty or the API request fails. +- Keep `src/app/meshkee/static-image-slots/route.ts` synchronized with every static-image placeholder. +- Whenever a placeholder's rendered dimensions or crop ratio change, update that slot's `aspectRatio` and `recommendedWidth` in the catalog and the component container (`aspectRatioToCss`). +- Update `CONTEXT.md` and `CLAUDE.md` when a prompt changes routes, API integrations, slot keys, or other project architecture. diff --git a/CLAUDE.md b/CLAUDE.md index 970aa52..4a94e0e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ can fill with their own images from the Meshkee dashboard. Current mapping: | Slot key | Kind | Component | Fallback when empty/unset | |-------------------|--------|--------------------|----------------------------------------| | `slider` | list | `HeroSlider` | 3 local photos in `hero-slider-*.jpg` with authored copy | -| `categories` | list | `CategoryShowcase` | icon-based cards derived from live product categories | +| `categories` | list | `CategoryShowcase` | photo cards derived from live product categories | | `four-icon-image` | list(4)| `Features` | 4 local icons + authored copy, per-index override | | `about-us` | single | `About` | local `hero-2.jpg` | | `brands` | list | `Partners` | 6 local partner logos | @@ -44,9 +44,21 @@ Each `StaticImageItem` carries `url`, optional `titleFa`/`subtext`/ (`"21:9"` etc, via `aspectRatioToCss`) for slot-driven image containers instead of hardcoding a Tailwind aspect class — if you resize/recrop a *local* fallback image during development, update the corresponding -component's fallback styling to match, since slot-driven and fallback -images render through the same container and must not look broken when -swapped. +component's fallback styling **and** the catalog entry in +`src/app/meshkee/static-image-slots/route.ts` to match, since slot-driven +and fallback images render through the same container and must not look +broken when swapped. -`aradarisman.com` currently has **zero** slots configured, so the site -renders through the fallback path everywhere — this is expected, not a bug. +### Dashboard catalog (required for Refresh) + +The Meshkee dashboard **Refresh** button does not invent slots. It imports +keys from this site: + +`GET https://aradarisman.com/meshkee/static-image-slots` + +That route is `src/app/meshkee/static-image-slots/route.ts`. Until it is +deployed and the dashboard Refresh is clicked, `/tenants/aradarisman.com/website/static-images` +returns `{ items: [] }` and the storefront correctly shows fallbacks. + +Do not invent upload/CMS APIs. Owners add images in the Meshkee dashboard +after Refresh has imported the catalog. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..de537b0 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,38 @@ +# Arad Arisman Website Context + +## Project + +- Persian RTL storefront for Arad Arisman (livestock/poultry feed supplements). +- Next.js 16 App Router, React 19, TypeScript, Tailwind 4, `src/app`. +- Apex domain: `aradarisman.com`. +- Local images live in `public/images`. + +## Meshkee API + +- Canonical docs: `https://api.meshkee.com/docs/website` +- API base: `https://api.meshkee.com/api/v1` +- Tenant domain: `aradarisman.com` (`TENANT_DOMAIN` in `src/lib/meshkee.ts`) +- All public fetches go through `src/lib/meshkee.ts`. Do not call the API from components. +- Do not invent dashboard/CMS/upload endpoints. + +## Homepage static-image slots + +Consumers read `GET /tenants/aradarisman.com/website/static-images` and match by `key`. Empty or failed slots keep local placeholders. + +| Key | Kind | itemCount | Aspect | Width | Component | +|---|---|---|---|---|---| +| `slider` | list (duplicatable) | null | 16:9 | 1920 | `HeroSlider` (full-viewport `h-svh`; catalog ratio is the recommended crop) | +| `categories` | list (duplicatable) | null | 3:5 | 720 | `CategoryShowcase` | +| `four-icon-image` | list | 4 | 1:1 | 128 | `Features` | +| `about-us` | single | 1 | 4:3 | 960 | `About` | +| `brands` | list (duplicatable) | null | 1:1 | 200 | `Partners` | + +Dashboard import catalog: `GET /meshkee/static-image-slots` (`src/app/meshkee/static-image-slots/route.ts`). + +If a rendered image size or crop ratio changes, update both the component container (`aspectRatioToCss`) and the matching catalog entry. + +## Other conventions + +- Category tiles from live products (`getCategorySummary`) because the raw `/categories` taxonomy has duplicate display names. +- Blog field names are inferred; accessors fall back across `nameFa` / `titleFa` / `title`, etc. +- Next.js `images.remotePatterns` allows `c804387.parspack.net` (Meshkee media CDN). diff --git a/src/app/meshkee/static-image-slots/route.ts b/src/app/meshkee/static-image-slots/route.ts new file mode 100644 index 0000000..00ea86b --- /dev/null +++ b/src/app/meshkee/static-image-slots/route.ts @@ -0,0 +1,63 @@ +/** + * Slot catalog the Meshkee business dashboard imports via Refresh. + * Keep this in sync with the homepage placeholders. If a rendered crop or + * size changes, update `aspectRatio` / `recommendedWidth` here too. + */ +const slots = [ + { + key: "slider", + label: "اسلایدر صفحه اصلی", + kind: "list", + pageKey: "home", + aspectRatio: "16:9", + itemCount: null, + recommendedWidth: 1920, + }, + { + key: "categories", + label: "دسته‌بندی محصولات", + kind: "list", + pageKey: "home", + aspectRatio: "3:5", + itemCount: null, + recommendedWidth: 720, + }, + { + key: "four-icon-image", + label: "چهار آیکون ویژگی‌ها", + kind: "list", + pageKey: "home", + aspectRatio: "1:1", + itemCount: 4, + recommendedWidth: 128, + }, + { + key: "about-us", + label: "تصویر درباره ما", + kind: "single", + pageKey: "home", + aspectRatio: "4:3", + itemCount: 1, + recommendedWidth: 960, + }, + { + key: "brands", + label: "لوگوهای برند و نماد اعتماد", + kind: "list", + pageKey: "home", + aspectRatio: "1:1", + itemCount: null, + recommendedWidth: 200, + }, +] as const; + +export function GET() { + return Response.json( + { slots }, + { + headers: { + "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", + }, + } + ); +} diff --git a/src/components/About.tsx b/src/components/About.tsx index d9bbd8c..cf41c43 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import Container from "./Container"; -import { getStaticImageSlot } from "@/lib/meshkee"; +import { aspectRatioToCss, getStaticImageSlot } from "@/lib/meshkee"; export default async function About() { const slot = await getStaticImageSlot("about-us"); @@ -9,18 +9,30 @@ export default async function About() { const imageSrc = slotImage?.url || "/images/hero-2.jpg"; const imageAlt = slotImage?.titleFa || "محصولات آراد آریسمان"; const objectPositionClass = slotImage ? "object-center" : "object-left"; + const image = ( + {imageAlt} + ); return (
-
- {imageAlt} +
+ {slotImage?.linkUrl ? ( + + {image} + + ) : ( + image + )}
diff --git a/src/components/CategoryShowcase.tsx b/src/components/CategoryShowcase.tsx index 6ffed83..cf70d0c 100644 --- a/src/components/CategoryShowcase.tsx +++ b/src/components/CategoryShowcase.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; import Container from "./Container"; import ScrollReveal from "./ScrollReveal"; -import { getCategorySummary, getStaticImageSlot } from "@/lib/meshkee"; +import { aspectRatioToCss, getCategorySummary, getStaticImageSlot } from "@/lib/meshkee"; import { categoryFallbackImage } from "@/lib/categoryIcons"; import { toFa } from "@/lib/format"; @@ -21,6 +21,7 @@ export default async function CategoryShowcase() { ]); const slotImages = slot?.images ?? []; + const tileAspectRatio = aspectRatioToCss(slot?.aspectRatio, "3 / 5"); const tiles: Tile[] = slotImages.length > 0 @@ -63,7 +64,8 @@ export default async function CategoryShowcase() { >
- + {f.linkUrl ? ( + + + + ) : ( + + )}

{f.title}

{f.desc}

diff --git a/src/lib/meshkee.ts b/src/lib/meshkee.ts index 9534ee8..e9299f3 100644 --- a/src/lib/meshkee.ts +++ b/src/lib/meshkee.ts @@ -38,7 +38,8 @@ export async function getStaticImageSlots(pageKey?: string): Promise( - `/tenants/${TENANT_DOMAIN}/website/static-images${qs}` + `/tenants/${TENANT_DOMAIN}/website/static-images${qs}`, + 60 ); return data.items; } catch {