Add Torob tags, Meshkee SEO routes, and updated static-image slots.

Product pages emit crawler-readable meta, public URLs use id plus Farsi slugs, and the homepage catalog matches the dashboard slot keys.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-01 16:12:26 +03:30
co-authored by Cursor
parent f42c30a4b7
commit c84147e765
40 changed files with 838 additions and 288 deletions
+53 -26
View File
@@ -44,13 +44,21 @@ export type StaticImageCatalogSlot = {
*/
export const STATIC_IMAGE_KEYS = {
slider: "slider",
bannersBelowSlider: "banners-below-slider",
twoBannerBelowSlider: "two-banner-below-slider",
categories: "categories",
fourBannerSection: "four-banner-section",
threeBannerRow: "three-banner-row",
fourBanner: "four-banner",
twoBannerRow: "two-banner-row",
brands: "brands",
aboutUsImage: "about-us-image",
} as const;
/** Previous catalog keys still returned by the tenant API until dashboard Refresh. */
const SLOT_KEY_ALIASES: Record<string, string[]> = {
"two-banner-below-slider": ["banners-below-slider"],
"four-banner": ["four-banner-section"],
"two-banner-row": ["three-banner-row"],
};
export const STATIC_IMAGE_SLOT_CATALOG: StaticImageCatalogSlot[] = [
{
key: STATIC_IMAGE_KEYS.slider,
@@ -62,8 +70,8 @@ export const STATIC_IMAGE_SLOT_CATALOG: StaticImageCatalogSlot[] = [
recommendedWidth: 1200,
},
{
key: STATIC_IMAGE_KEYS.bannersBelowSlider,
label: "Banners below slider",
key: STATIC_IMAGE_KEYS.twoBannerBelowSlider,
label: "Two banners below slider",
kind: "list",
pageKey: "home",
aspectRatio: "13:5",
@@ -80,7 +88,7 @@ export const STATIC_IMAGE_SLOT_CATALOG: StaticImageCatalogSlot[] = [
recommendedWidth: 160,
},
{
key: STATIC_IMAGE_KEYS.fourBannerSection,
key: STATIC_IMAGE_KEYS.fourBanner,
label: "Four banner section",
kind: "list",
pageKey: "home",
@@ -89,21 +97,30 @@ export const STATIC_IMAGE_SLOT_CATALOG: StaticImageCatalogSlot[] = [
recommendedWidth: 600,
},
{
key: STATIC_IMAGE_KEYS.threeBannerRow,
label: "Three banner row",
key: STATIC_IMAGE_KEYS.twoBannerRow,
label: "Two banner row",
kind: "list",
pageKey: "home",
aspectRatio: "12:5",
itemCount: 3,
recommendedWidth: 480,
itemCount: 2,
recommendedWidth: 600,
},
{
key: STATIC_IMAGE_KEYS.brands,
label: "Brand logos",
kind: "list",
pageKey: "home",
aspectRatio: "2:1",
itemCount: null,
recommendedWidth: 196,
},
{
key: STATIC_IMAGE_KEYS.aboutUsImage,
label: "About us image",
kind: "single",
label: "About us images",
kind: "list",
pageKey: "home",
aspectRatio: "8:5",
itemCount: 1,
itemCount: 2,
recommendedWidth: 720,
},
];
@@ -121,8 +138,14 @@ export function slotAlt(image: StaticImage, fallback = ""): string {
return image.titleFa?.trim() || image.titleEn?.trim() || fallback;
}
export function slotByKey(slots: StaticImageSlot[], key: string): StaticImageSlot | undefined {
const aliases = SLOT_KEY_ALIASES[key] ?? [];
return slots.find((slot) => slot.key === key) ?? slots.find((slot) => aliases.includes(slot.key));
}
export function imagesForSlot(slots: StaticImageSlot[], key: string): StaticImageWithUrl[] {
const slot = slots.find((item) => item.key === key);
const slot = slotByKey(slots, key);
const catalog = STATIC_IMAGE_SLOT_CATALOG.find((item) => item.key === key);
const raw = slot?.images ?? [];
const sorted = [...raw].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
const result: StaticImageWithUrl[] = [];
@@ -131,7 +154,11 @@ export function imagesForSlot(slots: StaticImageSlot[], key: string): StaticImag
if (!url) continue;
result.push({ ...image, url });
}
if (slot?.kind === "single") return result.slice(0, 1);
const kind = catalog?.kind ?? slot?.kind ?? "list";
const limit = catalog?.itemCount ?? (kind === "single" ? 1 : null);
if (kind === "single" || limit === 1) return result.slice(0, 1);
if (limit && limit > 0) return result.slice(0, limit);
return result;
}
@@ -142,10 +169,6 @@ export function firstImageForSlot(
return imagesForSlot(slots, key)[0];
}
export function slotByKey(slots: StaticImageSlot[], key: string): StaticImageSlot | undefined {
return slots.find((slot) => slot.key === key);
}
export async function getStaticImageSlots(pageKey?: string): Promise<StaticImageSlot[]> {
try {
const url = new URL(`${API.base}/tenants/${API.domain}/website/static-images`);
@@ -160,12 +183,16 @@ export async function getStaticImageSlots(pageKey?: string): Promise<StaticImage
}
export async function getStaticImageSlot(key: string): Promise<StaticImageSlot | null> {
try {
const url = new URL(`${API.base}/tenants/${API.domain}/website/static-images/${key}`);
const response = await fetch(url, { next: { revalidate: 120 } });
if (!response.ok) return null;
return (await response.json()) as StaticImageSlot;
} catch {
return null;
const keys = [key, ...(SLOT_KEY_ALIASES[key] ?? [])];
for (const candidate of keys) {
try {
const url = new URL(`${API.base}/tenants/${API.domain}/website/static-images/${candidate}`);
const response = await fetch(url, { next: { revalidate: 120 } });
if (!response.ok) continue;
return (await response.json()) as StaticImageSlot;
} catch {
// try next alias
}
}
return null;
}