Publish Meshkee static-image slot catalog so dashboard Refresh can import keys.

The storefront already consumed slots, but GET /meshkee/static-image-slots was missing, so Meshkee never registered slider, categories, four-icon-image, about-us, or brands for this domain.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 09:58:06 +03:30
co-authored by Cursor
parent 26b8b64bf3
commit 7a0de3c1f4
8 changed files with 169 additions and 19 deletions
+15
View File
@@ -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.
+18 -6
View File
@@ -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.
+38
View File
@@ -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).
@@ -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",
},
}
);
}
+21 -9
View File
@@ -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 = (
<Image
src={imageSrc}
alt={imageAlt}
fill
className={`object-cover ${objectPositionClass}`}
sizes="(min-width: 1024px) 45vw, 90vw"
/>
);
return (
<section id="about" className="py-16 sm:py-20 bg-surface">
<Container className="grid grid-cols-1 lg:grid-cols-2 items-center gap-12">
<div className="relative aspect-[4/3] rounded-3xl overflow-hidden shadow-xl order-2 lg:order-1">
<Image
src={imageSrc}
alt={imageAlt}
fill
className={`object-cover ${objectPositionClass}`}
sizes="(min-width: 1024px) 45vw, 90vw"
/>
<div
className="relative rounded-3xl overflow-hidden shadow-xl order-2 lg:order-1"
style={{ aspectRatio: aspectRatioToCss(slot?.aspectRatio, "4 / 3") }}
>
{slotImage?.linkUrl ? (
<a href={slotImage.linkUrl} className="absolute inset-0 block" aria-label={imageAlt}>
{image}
</a>
) : (
image
)}
</div>
<div className="order-1 lg:order-2 text-center lg:text-start">
+4 -2
View File
@@ -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() {
>
<Link
href={tile.href}
className="group relative block aspect-[3/5] overflow-hidden rounded-2xl shadow-md transition-all duration-300 hover:-translate-y-1.5 hover:shadow-xl"
className="group relative block overflow-hidden rounded-2xl shadow-md transition-all duration-300 hover:-translate-y-1.5 hover:shadow-xl"
style={{ aspectRatio: tileAspectRatio }}
>
<Image
src={tile.image}
+8 -1
View File
@@ -35,6 +35,7 @@ export default async function Features() {
icon: override?.url || f.icon,
title: override?.titleFa || f.title,
desc: override?.subtext || f.desc,
linkUrl: override?.linkUrl || null,
};
});
@@ -48,7 +49,13 @@ export default async function Features() {
className="group rounded-2xl border border-black/5 bg-white p-7 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all"
>
<div className="flex h-16 w-16 items-center justify-center rounded-xl bg-primary/5">
<Image src={f.icon} alt="" width={40} height={40} />
{f.linkUrl ? (
<a href={f.linkUrl} aria-label={f.title}>
<Image src={f.icon} alt="" width={40} height={40} />
</a>
) : (
<Image src={f.icon} alt="" width={40} height={40} />
)}
</div>
<h3 className="mt-5 text-lg font-bold text-secondary">{f.title}</h3>
<p className="mt-2 text-sm leading-6 text-foreground">{f.desc}</p>
+2 -1
View File
@@ -38,7 +38,8 @@ export async function getStaticImageSlots(pageKey?: string): Promise<StaticImage
try {
const qs = pageKey ? `?pageKey=${encodeURIComponent(pageKey)}` : "";
const data = await meshkeeFetch<{ items: StaticImageSlot[] }>(
`/tenants/${TENANT_DOMAIN}/website/static-images${qs}`
`/tenants/${TENANT_DOMAIN}/website/static-images${qs}`,
60
);
return data.items;
} catch {