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
+47 -9
View File
@@ -87,13 +87,18 @@ export type ApiCategory = {
name: string;
nameFa: string | null;
slug: string;
description?: string | null;
isActive?: boolean;
};
export type ApiVariant = {
id: string;
productId?: string;
productTitle?: string | null;
productNameFa?: string | null;
productImage?: string | null;
label?: string | null;
price: number;
price: number | null;
discountedPrice?: number | null;
stockQuantity?: number | null;
sortOrder?: number;
@@ -152,6 +157,18 @@ export async function getProduct(slug: string) {
}
}
export async function getProductById(productId: string) {
try {
const data = await apiGet<{ product: ApiProduct }>(
`${tenant()}/products/by-id/${encodeURIComponent(productId)}`,
);
return data.product ?? null;
} catch (error) {
if (error instanceof ApiError && error.status === 404) return null;
throw error;
}
}
export async function getBlogs(query: { page?: number; pageSize?: number; title?: string } = {}) {
return apiGet<Paginated<ApiBlog>>(`${tenant()}/blogs`, query);
}
@@ -166,6 +183,16 @@ export async function getBlog(slug: string) {
}
}
export async function getBlogById(blogId: string) {
try {
const data = await apiGet<{ blog: ApiBlog }>(`${tenant()}/blogs/by-id/${encodeURIComponent(blogId)}`);
return data.blog ?? null;
} catch (error) {
if (error instanceof ApiError && error.status === 404) return null;
throw error;
}
}
export async function getStoreSpecials() {
const data = await apiGet<{ items: ApiStoreSpecial[] }>(`${tenant()}/store-specials`);
return data.items ?? [];
@@ -173,8 +200,10 @@ export async function getStoreSpecials() {
export async function getStoreItemByProduct(productId: string) {
try {
const data = await apiGet<{ storeItem: ApiStoreItem }>(`${tenant()}/store-items/by-product/${productId}`);
return data.storeItem;
const data = await apiGet<{ storeItem: ApiStoreItem | null }>(
`${tenant()}/store-items/by-product/${productId}`,
);
return data.storeItem ?? null;
} catch (error) {
if (error instanceof ApiError && error.status === 404) return null;
throw error;
@@ -195,6 +224,21 @@ export async function getCategoryBySlug(slug: string) {
return categories.find((category) => category.slug === slug) ?? null;
}
export async function getCategoryById(categoryId: string) {
try {
const data = await apiGet<{ category: ApiCategory }>(
`${tenant()}/categories/by-id/${encodeURIComponent(categoryId)}`,
);
return data.category ?? null;
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
const categories = await getCategories();
return categories.find((category) => category.id === categoryId) ?? null;
}
throw error;
}
}
export type ApiTenant = {
id: string | number;
name: string;
@@ -219,9 +263,3 @@ export async function getComments(entityType: "product" | "blog", entityId: stri
return [];
}
}
export async function resolveProductSlug(productId: string, name: string) {
if (!name) return "";
const { items } = await getProducts({ name, pageSize: 20 });
return items.find((item) => item.id === productId)?.slug ?? "";
}