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:
co-authored by
Cursor
parent
11d0ba64ed
commit
24b5649abd
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user