rezerv
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
export const UPSTREAM_API =
|
||||
process.env.UPSTREAM_API_URL ?? "https://api.modstagram.ir";
|
||||
process.env.UPSTREAM_API_URL ?? "https://api.modstagram.com";
|
||||
|
||||
type ProxyOptions = {
|
||||
stripAuth?: boolean;
|
||||
|
||||
152
src/lib/buildShopSeo.ts
Normal file
152
src/lib/buildShopSeo.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { SITE_URL } from "@/config/pageSeo";
|
||||
import { buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import { getSiteSeoMeta } from "@/lib/i18n/seo";
|
||||
import { DEFAULT_LANGUAGE, type AppLanguage } from "@/lib/i18n/registry";
|
||||
import { IShopProductListing } from "@/types/types";
|
||||
|
||||
type ShopInfo = {
|
||||
_id: string;
|
||||
name: string;
|
||||
logo?: string | null;
|
||||
description?: string | null;
|
||||
address?: string | null;
|
||||
province?: { name: string } | null;
|
||||
city?: { name: string } | null;
|
||||
has_physical_location?: boolean;
|
||||
rating?: { average: number; count: number } | null;
|
||||
};
|
||||
|
||||
function cheapestAndDearest(listing: IShopProductListing) {
|
||||
const prices = (listing.variants || []).map((v) =>
|
||||
v.discount_price != null ? v.discount_price : v.price
|
||||
);
|
||||
if (!prices.length) return { low: null, high: null };
|
||||
return { low: Math.min(...prices), high: Math.max(...prices) };
|
||||
}
|
||||
|
||||
function totalStock(listing: IShopProductListing): number {
|
||||
return (listing.variants || []).reduce((sum, v) => sum + (v.stock || 0), 0);
|
||||
}
|
||||
|
||||
/** Product + Offer/AggregateOffer JSON-LD for a shop product listing page. */
|
||||
export function buildProductJsonLd(
|
||||
listing: IShopProductListing,
|
||||
shop: ShopInfo | null,
|
||||
url: string,
|
||||
lang: AppLanguage = DEFAULT_LANGUAGE
|
||||
) {
|
||||
const siteName = getSiteSeoMeta(lang).name;
|
||||
const images = (listing.images || []).map((img) => buildStorageUrl(img)).filter(Boolean);
|
||||
const { low, high } = cheapestAndDearest(listing);
|
||||
const inStock = totalStock(listing) > 0;
|
||||
const skus = Array.from(
|
||||
new Set((listing.variants || []).map((v) => v.sku).filter(Boolean))
|
||||
);
|
||||
|
||||
const offerBase = {
|
||||
priceCurrency: "IRR",
|
||||
availability: inStock
|
||||
? "https://schema.org/InStock"
|
||||
: "https://schema.org/OutOfStock",
|
||||
url,
|
||||
seller: shop
|
||||
? { "@type": "Organization", name: shop.name }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const offers =
|
||||
low != null && high != null
|
||||
? low === high
|
||||
? { "@type": "Offer", ...offerBase, price: low }
|
||||
: {
|
||||
"@type": "AggregateOffer",
|
||||
...offerBase,
|
||||
lowPrice: low,
|
||||
highPrice: high,
|
||||
offerCount: listing.variants?.length || 1,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
name: listing.title,
|
||||
description: listing.description || undefined,
|
||||
image: images.length ? images : undefined,
|
||||
sku: skus.length === 1 ? skus[0] : undefined,
|
||||
category: listing.category || undefined,
|
||||
brand: shop ? { "@type": "Brand", name: shop.name } : undefined,
|
||||
offers,
|
||||
url,
|
||||
breadcrumb: {
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{ "@type": "ListItem", position: 1, name: siteName, item: SITE_URL },
|
||||
{ "@type": "ListItem", position: 2, name: "فروشگاهها", item: `${SITE_URL}/shops/discover` },
|
||||
...(shop
|
||||
? [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 3,
|
||||
name: shop.name,
|
||||
item: `${SITE_URL}/shops/profile/${shop._id}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: shop ? 4 : 3,
|
||||
name: listing.title,
|
||||
item: url,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** LocalBusiness/Store JSON-LD for a shop's public profile page. */
|
||||
export function buildShopJsonLd(
|
||||
shop: ShopInfo,
|
||||
url: string,
|
||||
lang: AppLanguage = DEFAULT_LANGUAGE
|
||||
) {
|
||||
const siteName = getSiteSeoMeta(lang).name;
|
||||
const address =
|
||||
shop.province?.name || shop.city?.name || shop.address
|
||||
? {
|
||||
"@type": "PostalAddress",
|
||||
addressRegion: shop.province?.name || undefined,
|
||||
addressLocality: shop.city?.name || undefined,
|
||||
streetAddress: shop.address || undefined,
|
||||
addressCountry: "IR",
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const aggregateRating =
|
||||
shop.rating && shop.rating.count > 0
|
||||
? {
|
||||
"@type": "AggregateRating",
|
||||
ratingValue: shop.rating.average,
|
||||
reviewCount: shop.rating.count,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": shop.has_physical_location ? "LocalBusiness" : "OnlineStore",
|
||||
name: shop.name,
|
||||
description: shop.description || undefined,
|
||||
image: shop.logo ? buildStorageUrl(shop.logo) : undefined,
|
||||
url,
|
||||
address,
|
||||
aggregateRating,
|
||||
breadcrumb: {
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{ "@type": "ListItem", position: 1, name: siteName, item: SITE_URL },
|
||||
{ "@type": "ListItem", position: 2, name: "فروشگاهها", item: `${SITE_URL}/shops/discover` },
|
||||
{ "@type": "ListItem", position: 3, name: shop.name, item: url },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user