Polish storefront SEO, specials, and homepage UX.

Publish Meshkee sitemap-config, map product-source specials without variants, restore hero autoplay, and tighten page heroes, product images, and section order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-22 21:48:00 +03:30
co-authored by Cursor
parent 11d0ba64ed
commit 24b5649abd
22 changed files with 735 additions and 298 deletions
+28
View File
@@ -0,0 +1,28 @@
{
"baseUrl": "https://brb-store.com",
"staticPages": [
{
"path": "/",
"changefreq": "daily",
"priority": 1
},
{
"path": "/about",
"priority": 0.6
},
{
"path": "/blog",
"changefreq": "weekly",
"priority": 0.8
},
{
"path": "/contact",
"priority": 0.5
},
{
"path": "/products",
"changefreq": "daily",
"priority": 0.9
}
]
}
+1 -1
View File
@@ -104,7 +104,7 @@ export const STATIC_IMAGE_SLOTS: StaticImageSlotCatalogItem[] = [
label: "Full-row contact banner",
kind: "single",
pageKey: "home",
aspectRatio: "32:9",
aspectRatio: "64:9",
itemCount: 1,
recommendedWidth: 1920,
},
+102 -17
View File
@@ -30,6 +30,8 @@ export type StoreSpecialProduct = {
productTitle?: string | null;
productNameFa?: string | null;
productImage?: string | null;
isActive?: boolean;
sortOrder?: number | null;
variants?: StoreVariant[];
};
@@ -48,6 +50,12 @@ export type CatalogProduct = {
categoryNameFa?: string | null;
categoryName?: string | null;
commentCount?: number;
store?: {
variantCount?: number | null;
minPrice?: number | null;
maxPrice?: number | null;
inStock?: boolean | null;
} | null;
};
export type Paged<T> = {
@@ -97,16 +105,37 @@ export function variantToProduct(variant: StoreVariant): Product {
};
}
export function specialItemToProduct(item: StoreSpecialProduct): Product | null {
export function specialItemToProduct(item: StoreSpecialProduct, slug?: string | null): Product | null {
if (item.isActive === false) return null;
const variant = item.variants?.[0];
if (!variant) return null;
return variantToProduct({
...variant,
productId: item.productId ?? variant.productId,
productTitle: item.productTitle ?? variant.productTitle,
productNameFa: item.productNameFa ?? variant.productNameFa,
productImage: item.productImage ?? variant.productImage,
});
if (variant) {
return variantToProduct({
...variant,
productId: item.productId ?? variant.productId,
productTitle: item.productTitle ?? variant.productTitle,
productNameFa: item.productNameFa ?? variant.productNameFa,
productImage: item.productImage ?? variant.productImage,
});
}
// Product-source specials often ship with empty variants[].
const title = item.productNameFa?.trim() || item.productTitle?.trim();
if (!title && !item.productImage) return null;
const id = String(item.productId ?? item.id);
const href = slug
? `/products/${encodeURIComponent(slug)}`
: `/products?q=${encodeURIComponent(title || id)}`;
return {
id,
href,
title: title || "محصول",
brand: "",
image: item.productImage || PLACEHOLDER,
inStock: true,
};
}
export async function fetchStoreItems(params: {
@@ -144,14 +173,33 @@ export async function fetchStoreItemByProduct(productId: string) {
}
export async function fetchStoreSpecials() {
const data = await meshkeeGet<{ items?: StoreSpecial[] }>(tenantPath("/store-specials"), { items: [] });
const data = await meshkeeGet<{ items?: StoreSpecial[]; source?: string }>(tenantPath("/store-specials"), {
items: [],
});
return data.items ?? [];
}
export async function fetchSpecialProducts(key: string) {
const specials = await fetchStoreSpecials();
const special = specials.find((item) => item.key === key);
const products = (special?.items ?? []).map(specialItemToProduct).filter((item): item is Product => Boolean(item));
const rawItems = [...(special?.items ?? [])]
.filter((item) => item.isActive !== false)
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
const products = (
await Promise.all(
rawItems.map(async (item) => {
if (item.variants?.[0]) return specialItemToProduct(item);
const catalog = await fetchCatalogProductByTitle(
item.productNameFa || item.productTitle,
item.productId,
);
return specialItemToProduct(item, catalog?.slug);
}),
)
).filter((item): item is Product => Boolean(item));
return { special, products };
}
@@ -167,22 +215,59 @@ export function galleryUrls(product?: CatalogProduct | null, fallback?: string |
return urls.length ? urls : [PLACEHOLDER];
}
export async function fetchCatalogProducts(params: {
page?: number;
pageSize?: number;
categoryId?: string;
name?: string;
} = {}) {
const query = new URLSearchParams();
query.set("page", String(params.page ?? 1));
query.set("pageSize", String(params.pageSize ?? 20));
if (params.categoryId) query.set("categoryId", params.categoryId);
if (params.name) query.set("name", params.name);
return meshkeeGet<Paged<CatalogProduct>>(tenantPath(`/products?${query.toString()}`), {
items: [],
total: 0,
page: 1,
pageSize: params.pageSize ?? 20,
});
}
export function catalogProductToCard(product: CatalogProduct): Product {
const price = asNumber(product.store?.minPrice);
return {
id: String(product.id),
href: product.slug ? `/products/${encodeURIComponent(product.slug)}` : `/products?q=${encodeURIComponent(product.nameFa || product.title || product.id)}`,
title: product.nameFa?.trim() || product.title?.trim() || "محصول",
brand: brandName(product) || product.categoryNameFa || "",
price: price ?? undefined,
image: product.thumbnailUrl || product.image || product.thumbnail || PLACEHOLDER,
inStock: product.store?.inStock !== false,
};
}
export async function fetchCatalogProductByTitle(title?: string | null, productId?: string | null) {
if (!title?.trim()) return null;
if (!title?.trim() && !productId) return null;
const query = title?.trim() || String(productId);
const data = await meshkeeGet<Paged<CatalogProduct>>(
tenantPath(`/products?name=${encodeURIComponent(title.trim())}&pageSize=8`),
{ items: [], total: 0, page: 1, pageSize: 8 },
tenantPath(`/products?name=${encodeURIComponent(query)}&pageSize=12`),
{ items: [], total: 0, page: 1, pageSize: 12 },
);
const match =
data.items.find((item) => String(item.id) === String(productId)) ??
data.items[0] ??
null;
if (!match?.slug) return match;
const detail = await meshkeeGet<{ product?: CatalogProduct }>(
tenantPath(`/products/${encodeURIComponent(match.slug)}`),
return fetchCatalogProductBySlug(match.slug);
}
export async function fetchCatalogProductBySlug(slug: string) {
const data = await meshkeeGet<{ product?: CatalogProduct }>(
tenantPath(`/products/${encodeURIComponent(slug)}`),
{},
);
return detail.product ?? match;
return data.product ?? null;
}
export function brandName(product?: CatalogProduct | null) {