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:
@@ -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.
|
||||
@@ -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
@@ -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",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user