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 |
|
| Slot key | Kind | Component | Fallback when empty/unset |
|
||||||
|-------------------|--------|--------------------|----------------------------------------|
|
|-------------------|--------|--------------------|----------------------------------------|
|
||||||
| `slider` | list | `HeroSlider` | 3 local photos in `hero-slider-*.jpg` with authored copy |
|
| `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 |
|
| `four-icon-image` | list(4)| `Features` | 4 local icons + authored copy, per-index override |
|
||||||
| `about-us` | single | `About` | local `hero-2.jpg` |
|
| `about-us` | single | `About` | local `hero-2.jpg` |
|
||||||
| `brands` | list | `Partners` | 6 local partner logos |
|
| `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
|
(`"21:9"` etc, via `aspectRatioToCss`) for slot-driven image containers
|
||||||
instead of hardcoding a Tailwind aspect class — if you resize/recrop a
|
instead of hardcoding a Tailwind aspect class — if you resize/recrop a
|
||||||
*local* fallback image during development, update the corresponding
|
*local* fallback image during development, update the corresponding
|
||||||
component's fallback styling to match, since slot-driven and fallback
|
component's fallback styling **and** the catalog entry in
|
||||||
images render through the same container and must not look broken when
|
`src/app/meshkee/static-image-slots/route.ts` to match, since slot-driven
|
||||||
swapped.
|
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
|
### Dashboard catalog (required for Refresh)
|
||||||
renders through the fallback path everywhere — this is expected, not a bug.
|
|
||||||
|
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 Image from "next/image";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import { getStaticImageSlot } from "@/lib/meshkee";
|
import { aspectRatioToCss, getStaticImageSlot } from "@/lib/meshkee";
|
||||||
|
|
||||||
export default async function About() {
|
export default async function About() {
|
||||||
const slot = await getStaticImageSlot("about-us");
|
const slot = await getStaticImageSlot("about-us");
|
||||||
@@ -9,18 +9,30 @@ export default async function About() {
|
|||||||
const imageSrc = slotImage?.url || "/images/hero-2.jpg";
|
const imageSrc = slotImage?.url || "/images/hero-2.jpg";
|
||||||
const imageAlt = slotImage?.titleFa || "محصولات آراد آریسمان";
|
const imageAlt = slotImage?.titleFa || "محصولات آراد آریسمان";
|
||||||
const objectPositionClass = slotImage ? "object-center" : "object-left";
|
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 (
|
return (
|
||||||
<section id="about" className="py-16 sm:py-20 bg-surface">
|
<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">
|
<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">
|
<div
|
||||||
<Image
|
className="relative rounded-3xl overflow-hidden shadow-xl order-2 lg:order-1"
|
||||||
src={imageSrc}
|
style={{ aspectRatio: aspectRatioToCss(slot?.aspectRatio, "4 / 3") }}
|
||||||
alt={imageAlt}
|
>
|
||||||
fill
|
{slotImage?.linkUrl ? (
|
||||||
className={`object-cover ${objectPositionClass}`}
|
<a href={slotImage.linkUrl} className="absolute inset-0 block" aria-label={imageAlt}>
|
||||||
sizes="(min-width: 1024px) 45vw, 90vw"
|
{image}
|
||||||
/>
|
</a>
|
||||||
|
) : (
|
||||||
|
image
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="order-1 lg:order-2 text-center lg:text-start">
|
<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 Link from "next/link";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import ScrollReveal from "./ScrollReveal";
|
import ScrollReveal from "./ScrollReveal";
|
||||||
import { getCategorySummary, getStaticImageSlot } from "@/lib/meshkee";
|
import { aspectRatioToCss, getCategorySummary, getStaticImageSlot } from "@/lib/meshkee";
|
||||||
import { categoryFallbackImage } from "@/lib/categoryIcons";
|
import { categoryFallbackImage } from "@/lib/categoryIcons";
|
||||||
import { toFa } from "@/lib/format";
|
import { toFa } from "@/lib/format";
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export default async function CategoryShowcase() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const slotImages = slot?.images ?? [];
|
const slotImages = slot?.images ?? [];
|
||||||
|
const tileAspectRatio = aspectRatioToCss(slot?.aspectRatio, "3 / 5");
|
||||||
|
|
||||||
const tiles: Tile[] =
|
const tiles: Tile[] =
|
||||||
slotImages.length > 0
|
slotImages.length > 0
|
||||||
@@ -63,7 +64,8 @@ export default async function CategoryShowcase() {
|
|||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={tile.href}
|
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
|
<Image
|
||||||
src={tile.image}
|
src={tile.image}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export default async function Features() {
|
|||||||
icon: override?.url || f.icon,
|
icon: override?.url || f.icon,
|
||||||
title: override?.titleFa || f.title,
|
title: override?.titleFa || f.title,
|
||||||
desc: override?.subtext || f.desc,
|
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"
|
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">
|
<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>
|
</div>
|
||||||
<h3 className="mt-5 text-lg font-bold text-secondary">{f.title}</h3>
|
<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>
|
<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 {
|
try {
|
||||||
const qs = pageKey ? `?pageKey=${encodeURIComponent(pageKey)}` : "";
|
const qs = pageKey ? `?pageKey=${encodeURIComponent(pageKey)}` : "";
|
||||||
const data = await meshkeeFetch<{ items: StaticImageSlot[] }>(
|
const data = await meshkeeFetch<{ items: StaticImageSlot[] }>(
|
||||||
`/tenants/${TENANT_DOMAIN}/website/static-images${qs}`
|
`/tenants/${TENANT_DOMAIN}/website/static-images${qs}`,
|
||||||
|
60
|
||||||
);
|
);
|
||||||
return data.items;
|
return data.items;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user