1409 lines
65 KiB
TypeScript
1409 lines
65 KiB
TypeScript
/**
|
||
* Build a fully static storefront into dist/ for Cloudflare Workers.
|
||
* Reads data/site.json only — no database required on CI.
|
||
*/
|
||
import fs from "node:fs";
|
||
import { assertPersianProductCopy } from "../src/lib/sync/persian-translator";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { getDictionary } from "../src/lib/i18n/dictionaries";
|
||
import {
|
||
KIKO_CATEGORY_ROOTS,
|
||
slugifySegment,
|
||
parseKikoPath,
|
||
translateKikoSegment,
|
||
commercialProductSearchTerm,
|
||
commercialProductType,
|
||
} from "../src/lib/categories/kiko";
|
||
import { formatPrice, parseJsonObject } from "../src/lib/utils";
|
||
import { rankRelatedProducts } from "../src/lib/related-products";
|
||
import {
|
||
ROBOTS_TXT,
|
||
aboutPageJsonLd,
|
||
authenticityPageJsonLd,
|
||
breadcrumbJsonLd,
|
||
contactPageJsonLd,
|
||
buildSitemapXml,
|
||
faqJsonLd,
|
||
jsonLdGraph,
|
||
listingJsonLd,
|
||
organizationJsonLd,
|
||
productBreadcrumbItems,
|
||
productJsonLd,
|
||
seoAboutDescription,
|
||
seoAboutTitle,
|
||
seoAuthenticityDescription,
|
||
seoAuthenticityTitle,
|
||
seoContactDescription,
|
||
seoContactTitle,
|
||
seoCategoryDescription,
|
||
seoCategoryIntro,
|
||
seoHomeDescription,
|
||
seoHomeTitle,
|
||
seoListingTitle,
|
||
seoOffersDescription,
|
||
seoProductDescription,
|
||
seoProductTitle,
|
||
websiteJsonLd,
|
||
} from "../src/lib/seo";
|
||
import {
|
||
buyPanel,
|
||
atelierHeroHtml,
|
||
bestsellersSection,
|
||
beautyInspirationBanner,
|
||
collabBannersSection,
|
||
faqSection,
|
||
layout,
|
||
listProductCard,
|
||
aboutPageBody,
|
||
authenticityPageBody,
|
||
contactPageBody,
|
||
offerProductCard,
|
||
offersSliderSection,
|
||
promoBanner,
|
||
promoHeroSliderHtml,
|
||
reviewsSection,
|
||
topCategoriesSection,
|
||
kikoShopByCategoryItems,
|
||
trustSection,
|
||
whyUsSection,
|
||
productGridCard,
|
||
productListingFilters,
|
||
relatedProductCard,
|
||
renderBlocks,
|
||
pdpGallerySection,
|
||
} from "../vanilla/lib/html";
|
||
import type { HeroContent } from "../src/lib/blocks/types";
|
||
import type { MegaMenuRoot } from "../src/lib/mega-menu";
|
||
import {
|
||
attachColorVariants,
|
||
buildColorVariantMap,
|
||
dedupeByColorFamily,
|
||
displayNameWithoutShade,
|
||
splitProductName,
|
||
type ColorVariant,
|
||
} from "../src/lib/color-variants";
|
||
import {
|
||
normalizeProductCategory,
|
||
persianCategoryLabel,
|
||
withPersianCategory,
|
||
} from "../src/lib/categories/kiko";
|
||
import { productFacts } from "../src/lib/product-facts";
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = path.resolve(__dirname, "..");
|
||
const DIST = path.join(ROOT, "dist");
|
||
const SITE_JSON = path.join(ROOT, "data", "site.json");
|
||
|
||
type SiteProduct = {
|
||
id: string;
|
||
slug: string;
|
||
name: string;
|
||
nameFa?: string | null;
|
||
tagline: string | null;
|
||
description: string | null;
|
||
seoDescription?: string | null;
|
||
price: number;
|
||
compareAtPrice: number | null;
|
||
heroImage: string | null;
|
||
images: string[];
|
||
category: string | null;
|
||
stock: number;
|
||
lowStockThreshold: number;
|
||
featured: boolean;
|
||
updatedAt: string;
|
||
variantGroupId?: string | null;
|
||
shadeCode?: string | null;
|
||
variantCanonical?: boolean;
|
||
related: Array<{
|
||
id: string;
|
||
slug: string;
|
||
name: string;
|
||
nameFa?: string | null;
|
||
price: number;
|
||
heroImage: string | null;
|
||
category: string | null;
|
||
tagline?: string | null;
|
||
colorVariants?: ColorVariant[];
|
||
}>;
|
||
blocks: Array<{ type: string; content: string }>;
|
||
ratingAvg: number;
|
||
reviewCount: number;
|
||
colorVariants?: ColorVariant[];
|
||
};
|
||
|
||
type SiteData = {
|
||
generatedAt: string;
|
||
eurToTomanRate?: number;
|
||
eurToTomanSource?: string;
|
||
megaMenu: MegaMenuRoot[];
|
||
products: SiteProduct[];
|
||
};
|
||
|
||
function esc(s: unknown) {
|
||
return String(s ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """);
|
||
}
|
||
|
||
function write(rel: string, content: string | Buffer) {
|
||
const full = path.join(DIST, rel);
|
||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||
fs.writeFileSync(full, content);
|
||
}
|
||
|
||
function redirectHtml(target: string, title = "Kikomiko") {
|
||
return `<!DOCTYPE html><html lang="fa"><head><meta charset="utf-8"/><meta http-equiv="refresh" content="0;url=${esc(target)}"/><link rel="canonical" href="${esc(target)}"/><link rel="icon" href="/favicon.svg" type="image/svg+xml"/><title>${esc(title)}</title></head><body><p><a href="${esc(target)}">ادامه به فروشگاه</a></p></body></html>`;
|
||
}
|
||
|
||
function productFactsSection(product: SiteProduct) {
|
||
const facts = productFacts(product);
|
||
return `<section class="block-section container-luxury" aria-labelledby="product-facts-title">
|
||
<p class="label-caps text-muted">اطلاعات رسمی محصول</p>
|
||
<h2 id="product-facts-title">مشخصات و نحوه استفاده</h2>
|
||
<dl class="product-facts" lang="fa" dir="rtl">
|
||
${facts.map((fact) => `<div><dt><strong>${esc(fact.label)}</strong></dt><dd${fact.label === "شید" ? ' data-product-fact="shade"' : ""}>${esc(fact.value)}</dd></div>`).join("")}
|
||
</dl>
|
||
</section>`;
|
||
}
|
||
|
||
function productDescriptionHtml(description: string) {
|
||
const [intro, ...rest] = description.split(/\n\s*\n/);
|
||
return `<p class="text-muted product-description" lang="fa"><span data-product-description-intro>${esc(intro)}</span>${rest.length ? `\n\n${esc(rest.join("\n\n"))}` : ""}</p>`;
|
||
}
|
||
|
||
function writeLegacyFaRedirect(rel: string, target: string, title?: string) {
|
||
write(`fa/${rel}`, redirectHtml(target, title));
|
||
}
|
||
|
||
function copyDir(src: string, dest: string) {
|
||
if (!fs.existsSync(src)) return;
|
||
fs.mkdirSync(dest, { recursive: true });
|
||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||
const from = path.join(src, entry.name);
|
||
const to = path.join(dest, entry.name);
|
||
if (entry.isDirectory()) copyDir(from, to);
|
||
else fs.copyFileSync(from, to);
|
||
}
|
||
}
|
||
|
||
function rewriteMegaForStatic(mega: MegaMenuRoot[]): MegaMenuRoot[] {
|
||
return mega.map((root) => {
|
||
const base = root.href.replace(/^\/fa(?=\/|$)/, "").replace(/\?.*$/, "") || "/";
|
||
return {
|
||
...root,
|
||
href: base.endsWith("/") ? base : `${base}/`,
|
||
children: root.children.map((c) => {
|
||
const sub = new URL(c.href, "http://local").searchParams.get("sub") || c.key;
|
||
const slug = slugifySegment(sub) || c.key;
|
||
return {
|
||
...c,
|
||
label: translateKikoSegment(c.label),
|
||
href: `${base.replace(/\/$/, "")}/sub/${slug}/`,
|
||
};
|
||
}),
|
||
};
|
||
});
|
||
}
|
||
|
||
function matchesRoot(category: string | null, lev1: string) {
|
||
const parts = parseKikoPath(category);
|
||
return parts[0]?.toUpperCase() === lev1.toUpperCase();
|
||
}
|
||
|
||
function main() {
|
||
console.log("Building static site → dist/ (from data/site.json)");
|
||
if (!fs.existsSync(SITE_JSON)) {
|
||
throw new Error(
|
||
"Missing data/site.json. Run locally: npm run export:catalog"
|
||
);
|
||
}
|
||
|
||
const site = JSON.parse(fs.readFileSync(SITE_JSON, "utf8")) as SiteData;
|
||
const dict = getDictionary("fa");
|
||
const megaMenu = rewriteMegaForStatic(site.megaMenu || []);
|
||
const products = (site.products || []).map((p) => ({
|
||
...p,
|
||
category: normalizeProductCategory(p.name, p.category) || p.category,
|
||
}));
|
||
products.forEach(assertPersianProductCopy);
|
||
const catalogUpdatedAt = site.generatedAt;
|
||
const eurToTomanRate =
|
||
typeof site.eurToTomanRate === "number" &&
|
||
Number.isFinite(site.eurToTomanRate) &&
|
||
site.eurToTomanRate > 0
|
||
? site.eurToTomanRate
|
||
: undefined;
|
||
const sitemapPaths: string[] = [];
|
||
|
||
function pageLayout(opts: Parameters<typeof layout>[0]) {
|
||
return layout({ catalogUpdatedAt, eurToTomanRate, ...opts });
|
||
}
|
||
|
||
fs.rmSync(DIST, { recursive: true, force: true });
|
||
fs.mkdirSync(DIST, { recursive: true });
|
||
|
||
const listingSource = products.filter((p) => p.variantCanonical !== false);
|
||
const variantMapAll = buildColorVariantMap(
|
||
products.map((p) => ({
|
||
id: p.id,
|
||
slug: p.slug,
|
||
name: p.name,
|
||
category: p.category,
|
||
heroImage: p.heroImage,
|
||
images: p.images,
|
||
stock: p.stock,
|
||
price: p.price,
|
||
compareAtPrice: p.compareAtPrice,
|
||
variantGroupId: p.variantGroupId,
|
||
shadeCode: p.shadeCode,
|
||
variantCanonical: p.variantCanonical,
|
||
}))
|
||
);
|
||
const listingProducts = attachColorVariants(
|
||
dedupeByColorFamily(listingSource),
|
||
variantMapAll
|
||
).map(
|
||
(p) => {
|
||
if (p.colorVariants && p.colorVariants.length > 1) return p;
|
||
const fromExport = products.find((x) => x.id === p.id)?.colorVariants;
|
||
return fromExport && fromExport.length > 1
|
||
? { ...p, colorVariants: fromExport }
|
||
: p;
|
||
}
|
||
);
|
||
|
||
const catalog = {
|
||
generatedAt: site.generatedAt,
|
||
version: "dedupe2",
|
||
products: listingProducts.map((p) => {
|
||
const category = normalizeProductCategory(p.name, p.category);
|
||
return {
|
||
id: p.id,
|
||
slug: p.slug,
|
||
name: p.name,
|
||
nameFa: p.nameFa ?? null,
|
||
price: p.price,
|
||
compareAtPrice: p.compareAtPrice ?? null,
|
||
stock: p.stock,
|
||
heroImage: p.heroImage,
|
||
category,
|
||
categoryFa: persianCategoryLabel(category, p.name),
|
||
featured: p.featured,
|
||
updatedAt: p.updatedAt,
|
||
ratingAvg: p.ratingAvg,
|
||
reviewCount: p.reviewCount,
|
||
colorVariants: p.colorVariants,
|
||
};
|
||
}),
|
||
};
|
||
write("data/catalog.json", JSON.stringify(catalog));
|
||
write("data/catalog.dedupe2.json", JSON.stringify(catalog));
|
||
|
||
copyDir(path.join(ROOT, "vanilla/public/css"), path.join(DIST, "css"));
|
||
copyDir(path.join(ROOT, "vanilla/public/js"), path.join(DIST, "js"));
|
||
copyDir(path.join(ROOT, "vanilla/public/images"), path.join(DIST, "images"));
|
||
for (const rootAsset of [
|
||
"favicon.svg",
|
||
"60162903.txt",
|
||
"37941746.txt",
|
||
"۷۲۶۴۵۵۱۷.txt",
|
||
]) {
|
||
const src = path.join(ROOT, "vanilla/public", rootAsset);
|
||
if (fs.existsSync(src)) {
|
||
fs.copyFileSync(src, path.join(DIST, rootAsset));
|
||
}
|
||
}
|
||
copyDir(path.join(ROOT, "public/fonts"), path.join(DIST, "fonts"));
|
||
// Prefer vanilla/public/videos (storefront source of truth); fall back to public/videos
|
||
const vanillaVideos = path.join(ROOT, "vanilla/public/videos");
|
||
const publicVideos = path.join(ROOT, "public/videos");
|
||
copyDir(
|
||
fs.existsSync(vanillaVideos) ? vanillaVideos : publicVideos,
|
||
path.join(DIST, "videos")
|
||
);
|
||
|
||
const skipUploads = process.env.SKIP_UPLOADS !== "0";
|
||
if (!skipUploads) {
|
||
copyDir(path.join(ROOT, "public/uploads"), path.join(DIST, "uploads"));
|
||
} else {
|
||
console.log("SKIP_UPLOADS (default) — not copying product images");
|
||
}
|
||
|
||
const bestsellers = attachColorVariants(
|
||
dedupeByColorFamily(listingSource.filter((p) => p.featured)),
|
||
variantMapAll
|
||
).slice(0, 12);
|
||
|
||
const offers70 = (() => {
|
||
const eligible = products
|
||
.filter(
|
||
(p) =>
|
||
p.compareAtPrice != null &&
|
||
p.compareAtPrice > p.price &&
|
||
p.price > 0 &&
|
||
p.heroImage &&
|
||
Math.round((1 - p.price / p.compareAtPrice) * 100) >= 30
|
||
)
|
||
.map((p) => ({
|
||
...p,
|
||
discountPercent: Math.round(
|
||
(1 - p.price / (p.compareAtPrice as number)) * 100
|
||
),
|
||
}))
|
||
.sort((a, b) => b.discountPercent - a.discountPercent);
|
||
const attached = attachColorVariants(
|
||
dedupeByColorFamily(eligible),
|
||
variantMapAll
|
||
);
|
||
// Prefer cards that can show shade dots in the first screenful
|
||
const withShades = attached.filter(
|
||
(p) => (p.colorVariants?.length || 0) > 1
|
||
);
|
||
const withoutShades = attached.filter(
|
||
(p) => (p.colorVariants?.length || 0) <= 1
|
||
);
|
||
return [...withShades, ...withoutShades].slice(0, 15);
|
||
})();
|
||
|
||
const HOME_COLLECTION_LIMIT = 20;
|
||
const homeWithShades = listingProducts.filter(
|
||
(p) => (p.colorVariants?.length || 0) > 1
|
||
);
|
||
const homeWithoutShades = listingProducts.filter(
|
||
(p) => (p.colorVariants?.length || 0) <= 1
|
||
);
|
||
const homeCollection = [
|
||
...homeWithShades,
|
||
...homeWithoutShades,
|
||
].slice(0, HOME_COLLECTION_LIMIT);
|
||
|
||
const homeBody = `
|
||
<div class="home-shell">
|
||
<header class="sr-only">
|
||
<h1>خرید محصولات اصل KIKO Milano در ایران</h1>
|
||
<p>فروشگاه تخصصی محصولات کیکو میلانو، واردات مستقیم از ایتالیا با تضمین اصالت و دسترسی به جدیدترین کالکشنها و شیدها.</p>
|
||
</header>
|
||
${atelierHeroHtml(
|
||
products
|
||
.map((p) => p.heroImage)
|
||
.filter((src): src is string => Boolean(src))
|
||
.slice(0, 10)
|
||
)}
|
||
${promoHeroSliderHtml()}
|
||
${topCategoriesSection(kikoShopByCategoryItems(megaMenu))}
|
||
${bestsellersSection(dict.bestsellers, bestsellers, { trailingSlash: true })}
|
||
${collabBannersSection({ trailingSlash: true })}
|
||
${offersSliderSection(dict.offers70, offers70, { trailingSlash: true })}
|
||
${whyUsSection()}
|
||
${beautyInspirationBanner()}
|
||
${reviewsSection()}
|
||
${faqSection()}
|
||
${promoBanner({
|
||
title: "محصولات",
|
||
titleBold: "ویژه و تخفیفدار",
|
||
sub: "فرصت محدود برای کشف بهترین پیشنهادهای اورجینال کیکو با ضمانت اصالت.",
|
||
ctaLabel: "مشاهده پیشنهادها",
|
||
ctaHref: "/offers/",
|
||
})}
|
||
<div class="collection-wrap" id="collection" data-static-collection data-home-limit="${HOME_COLLECTION_LIMIT}">
|
||
<p class="collection-label label-caps">${esc(dict.interested)}</p>
|
||
<div class="filters">
|
||
${productListingFilters({
|
||
megaMenu,
|
||
productCount: homeCollection.length,
|
||
currentSort: "newest",
|
||
mode: "home",
|
||
hash: "collection",
|
||
clientMode: true,
|
||
})}
|
||
</div>
|
||
<div class="collection-title">
|
||
<p class="label-caps text-muted">${esc(dict.featured)}</p>
|
||
<h2>جدیدترین <strong>محصولات</strong></h2>
|
||
</div>
|
||
<div class="product-grid" data-product-grid>
|
||
${homeCollection.map((p) => productGridCard(p, { trailingSlash: true })).join("")}
|
||
</div>
|
||
<div class="collection-footer">
|
||
<a class="btn collection-see-all" href="/collection/">${esc(dict.seeAllProducts)}</a>
|
||
</div>
|
||
</div>
|
||
${trustSection()}
|
||
</div>`;
|
||
|
||
write(
|
||
"index.html",
|
||
pageLayout({
|
||
title: seoHomeTitle(),
|
||
description: seoHomeDescription(),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/",
|
||
ogImage: "/videos/kiko-hero-poster.jpg",
|
||
body: homeBody,
|
||
headExtra: `${jsonLdGraph([organizationJsonLd(), websiteJsonLd(), faqJsonLd()])}
|
||
<link rel="preload" as="image" href="/videos/kiko-hero-poster.jpg?v=3" fetchpriority="high" />`,
|
||
})
|
||
);
|
||
sitemapPaths.push("/");
|
||
writeLegacyFaRedirect("index.html", "/", "Kikomiko");
|
||
|
||
write(
|
||
"collection/index.html",
|
||
pageLayout({
|
||
title: seoListingTitle("همه محصولات"),
|
||
description: seoCategoryDescription("همه محصولات"),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/collection/",
|
||
headExtra: jsonLdGraph([
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: "همه محصولات", path: "/collection/" },
|
||
]),
|
||
listingJsonLd({
|
||
name: "همه محصولات کیکو میلانو",
|
||
description: seoCategoryDescription("همه محصولات"),
|
||
urlPath: "/collection/",
|
||
items: listingProducts.map((p) => ({
|
||
name: withPersianCategory(displayNameWithoutShade(p.name, true), p.category),
|
||
path: `/products/${p.slug}/`,
|
||
})),
|
||
}),
|
||
]),
|
||
body: `<div class="page-pad">
|
||
<div class="container-luxury" style="padding-bottom:1.5rem">
|
||
<p class="label-caps text-muted">${esc(dict.collection)}</p>
|
||
<h1 class="page-title">${esc(dict.seeAllProducts)}</h1>
|
||
</div>
|
||
<div class="container-luxury" style="padding-bottom:2rem">
|
||
<div class="filters" style="margin:0">
|
||
${productListingFilters({
|
||
megaMenu,
|
||
productCount: listingProducts.length,
|
||
mode: "collection",
|
||
clientMode: true,
|
||
})}
|
||
</div>
|
||
</div>
|
||
<div class="container-luxury list-grid" data-product-grid>${listingProducts
|
||
.map((p) => listProductCard(p, { trailingSlash: true }))
|
||
.join("")}</div>
|
||
</div>`,
|
||
})
|
||
);
|
||
sitemapPaths.push("/collection/");
|
||
writeLegacyFaRedirect("collection/index.html", "/collection/", `${dict.collection} | Kikomiko`);
|
||
|
||
const offersListing = (() => {
|
||
const eligible = listingSource
|
||
.filter(
|
||
(p) =>
|
||
p.compareAtPrice != null &&
|
||
p.compareAtPrice > p.price &&
|
||
p.price > 0 &&
|
||
p.heroImage &&
|
||
Math.round((1 - p.price / p.compareAtPrice) * 100) >= 30
|
||
)
|
||
.map((p) => ({
|
||
...p,
|
||
discountPercent: Math.round(
|
||
(1 - p.price / (p.compareAtPrice as number)) * 100
|
||
),
|
||
}))
|
||
.sort((a, b) => b.discountPercent - a.discountPercent);
|
||
return attachColorVariants(dedupeByColorFamily(eligible), variantMapAll);
|
||
})();
|
||
|
||
write(
|
||
"offers/index.html",
|
||
pageLayout({
|
||
title: seoListingTitle("محصولات تخفیفدار"),
|
||
description: seoOffersDescription(),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/offers/",
|
||
headExtra: jsonLdGraph([
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: "با تخفیف", path: "/offers/" },
|
||
]),
|
||
listingJsonLd({
|
||
name: "محصولات تخفیفدار کیکو میلانو",
|
||
description: seoOffersDescription(),
|
||
urlPath: "/offers/",
|
||
items: offersListing.map((p) => ({
|
||
name: withPersianCategory(displayNameWithoutShade(p.name, true), p.category),
|
||
path: `/products/${p.slug}/`,
|
||
})),
|
||
}),
|
||
]),
|
||
body: `<div class="page-pad">
|
||
<div class="container-luxury" style="padding-bottom:1.5rem">
|
||
<p class="label-caps text-muted">${esc(dict.offers70)}</p>
|
||
<h1 class="page-title">${esc(dict.withDiscount)}</h1>
|
||
<p class="text-muted" style="margin-top:0.75rem">محصولات با حداقل ۳۰٪ تخفیف — شامل ۳۰٪، ۵۰٪ و ۷۰٪ و بیشتر</p>
|
||
</div>
|
||
${
|
||
offersListing.length === 0
|
||
? `<div class="container-luxury section-padding" style="text-align:center"><p class="text-muted">${esc(dict.noProducts)}</p></div>`
|
||
: `<div class="container-luxury" style="padding-bottom:1rem">
|
||
<div class="filters" style="margin:0">
|
||
${productListingFilters({
|
||
megaMenu,
|
||
productCount: offersListing.length,
|
||
mode: "offers",
|
||
clientMode: true,
|
||
})}
|
||
</div>
|
||
</div>
|
||
<div class="container-luxury list-grid" data-product-grid>${offersListing
|
||
.map((p) => offerProductCard(p, { trailingSlash: true }))
|
||
.join("")}</div>`
|
||
}
|
||
</div>`,
|
||
})
|
||
);
|
||
sitemapPaths.push("/offers/");
|
||
writeLegacyFaRedirect("offers/index.html", "/offers/", `${dict.withDiscount} | Kikomiko`);
|
||
|
||
for (const root of KIKO_CATEGORY_ROOTS) {
|
||
const inRoot = attachColorVariants(
|
||
dedupeByColorFamily(
|
||
listingSource.filter((p) => matchesRoot(p.category, root.lev1))
|
||
),
|
||
variantMapAll
|
||
);
|
||
write(
|
||
`category/${root.slug}/index.html`,
|
||
pageLayout({
|
||
title: seoListingTitle(root.fa),
|
||
description: seoCategoryDescription(root.fa),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: `/category/${root.slug}/`,
|
||
headExtra: jsonLdGraph([
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: root.fa, path: `/category/${root.slug}/` },
|
||
]),
|
||
listingJsonLd({
|
||
name: root.fa,
|
||
description: seoCategoryDescription(root.fa),
|
||
urlPath: `/category/${root.slug}/`,
|
||
items: inRoot.map((p) => ({
|
||
name: withPersianCategory(displayNameWithoutShade(p.name, true), p.category),
|
||
path: `/products/${p.slug}/`,
|
||
})),
|
||
}),
|
||
]),
|
||
body: categoryBody(dict, root.fa, root.slug, inRoot, null, megaMenu),
|
||
})
|
||
);
|
||
sitemapPaths.push(`/category/${root.slug}/`);
|
||
writeLegacyFaRedirect(
|
||
`category/${root.slug}/index.html`,
|
||
`/category/${root.slug}/`,
|
||
`${root.fa} | Kikomiko`
|
||
);
|
||
|
||
const megaRoot = megaMenu.find((m) => m.href.includes(`/category/${root.slug}`));
|
||
if (!megaRoot) continue;
|
||
for (const child of megaRoot.children) {
|
||
const subSlug = child.href.split("/sub/")[1]?.replace(/\/$/, "") || child.key;
|
||
const landing = categoryLandingFor(subSlug);
|
||
const filtered = attachColorVariants(
|
||
dedupeByColorFamily(
|
||
listingSource.filter((p) => {
|
||
if (!matchesRoot(p.category, root.lev1)) return false;
|
||
const parts = parseKikoPath(p.category);
|
||
return parts.some(
|
||
(part) => slugifySegment(part) === subSlug || part === child.key
|
||
);
|
||
})
|
||
),
|
||
variantMapAll
|
||
);
|
||
write(
|
||
`category/${root.slug}/sub/${subSlug}/index.html`,
|
||
pageLayout({
|
||
title: landing?.title ?? seoListingTitle(child.label),
|
||
description: seoCategoryDescription(child.label),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: `/category/${root.slug}/sub/${subSlug}/`,
|
||
headExtra: jsonLdGraph([
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: root.fa, path: `/category/${root.slug}/` },
|
||
{ name: child.label, path: `/category/${root.slug}/sub/${subSlug}/` },
|
||
]),
|
||
listingJsonLd({
|
||
name: child.label,
|
||
description: seoCategoryDescription(child.label),
|
||
urlPath: `/category/${root.slug}/sub/${subSlug}/`,
|
||
items: filtered.map((p) => ({
|
||
name: withPersianCategory(displayNameWithoutShade(p.name, true), p.category),
|
||
path: `/products/${p.slug}/`,
|
||
})),
|
||
}),
|
||
]),
|
||
body: categoryBody(
|
||
dict,
|
||
child.label,
|
||
root.slug,
|
||
filtered,
|
||
child.label,
|
||
megaMenu,
|
||
subSlug
|
||
),
|
||
})
|
||
);
|
||
sitemapPaths.push(`/category/${root.slug}/sub/${subSlug}/`);
|
||
writeLegacyFaRedirect(
|
||
`category/${root.slug}/sub/${subSlug}/index.html`,
|
||
`/category/${root.slug}/sub/${subSlug}/`,
|
||
`${child.label} | Kikomiko`
|
||
);
|
||
}
|
||
}
|
||
|
||
for (const guide of PRODUCT_TYPE_GUIDES) {
|
||
const root = KIKO_CATEGORY_ROOTS.find((item) => item.slug === guide.rootSlug);
|
||
if (!root) continue;
|
||
const guideProducts = attachColorVariants(
|
||
dedupeByColorFamily(
|
||
listingSource.filter(
|
||
(product) => commercialProductType(product.name, product.category)?.typeFa === guide.typeFa
|
||
)
|
||
),
|
||
variantMapAll
|
||
);
|
||
const path = `/category/${guide.rootSlug}/type/${guide.slug}/`;
|
||
write(
|
||
`category/${guide.rootSlug}/type/${guide.slug}/index.html`,
|
||
pageLayout({
|
||
title: guide.title,
|
||
description: guide.description,
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: path,
|
||
headExtra: jsonLdGraph([
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: root.fa, path: `/category/${root.slug}/` },
|
||
{ name: guide.typeFa, path },
|
||
]),
|
||
listingJsonLd({
|
||
name: guide.h1,
|
||
description: guide.description,
|
||
urlPath: path,
|
||
items: guideProducts.map((product) => ({
|
||
name: withPersianCategory(displayNameWithoutShade(product.name, true), product.category),
|
||
path: `/products/${product.slug}/`,
|
||
})),
|
||
}),
|
||
]),
|
||
body: productTypeGuideBody(dict, guide, guideProducts, megaMenu),
|
||
})
|
||
);
|
||
sitemapPaths.push(path);
|
||
}
|
||
|
||
const maxProducts = process.env.MAX_PRODUCTS
|
||
? Number(process.env.MAX_PRODUCTS)
|
||
: products.length;
|
||
const slice = products.slice(0, maxProducts);
|
||
const byGroup = new Map<string, SiteProduct[]>();
|
||
for (const p of products) {
|
||
if (!p.variantGroupId) continue;
|
||
const list = byGroup.get(p.variantGroupId) || [];
|
||
list.push(p);
|
||
byGroup.set(p.variantGroupId, list);
|
||
}
|
||
|
||
let i = 0;
|
||
for (const product of slice) {
|
||
i += 1;
|
||
if (i % 100 === 0) console.log(` products ${i}/${slice.length}`);
|
||
|
||
if (product.variantCanonical === false && product.variantGroupId) {
|
||
const family = byGroup.get(product.variantGroupId) || [];
|
||
const canonical =
|
||
family.find((p) => p.variantCanonical !== false) || family[0];
|
||
if (canonical && canonical.slug !== product.slug) {
|
||
const shade =
|
||
product.shadeCode || splitProductName(product.name).shade || "";
|
||
const qs = shade ? `?shade=${encodeURIComponent(shade)}` : "";
|
||
const target = `/products/${canonical.slug}/${qs}`;
|
||
write(
|
||
`products/${product.slug}/index.html`,
|
||
`<!DOCTYPE html><html lang="fa"><head><meta charset="utf-8"/><meta http-equiv="refresh" content="0;url=${esc(target)}"/><link rel="canonical" href="${esc(target)}"/><title>${esc(displayNameWithoutShade(product.name, true))} | Kikomiko</title></head><body><p><a href="${esc(target)}">مشاهده محصول</a></p></body></html>`
|
||
);
|
||
writeLegacyFaRedirect(
|
||
`products/${product.slug}/index.html`,
|
||
target,
|
||
`${displayNameWithoutShade(product.name, true)} | Kikomiko`
|
||
);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
const productImages = product.images?.length
|
||
? product.images
|
||
: product.heroImage
|
||
? [product.heroImage]
|
||
: [];
|
||
const galleryImages = [
|
||
...(product.heroImage ? [product.heroImage] : []),
|
||
...productImages.filter((src) => src !== product.heroImage),
|
||
];
|
||
|
||
const heroBlock = product.blocks.find((b) => b.type === "HERO");
|
||
const hero = heroBlock
|
||
? parseJsonObject<HeroContent>(heroBlock.content, {
|
||
headline: product.name,
|
||
image: product.heroImage || "",
|
||
overlay: "dark",
|
||
})
|
||
: null;
|
||
|
||
const pdpDiscount =
|
||
product.compareAtPrice != null &&
|
||
product.compareAtPrice > product.price
|
||
? Math.round(
|
||
(1 - product.price / product.compareAtPrice) * 100
|
||
)
|
||
: 0;
|
||
|
||
const hasGalleryBlock = product.blocks.some((b) => b.type === "GALLERY");
|
||
const factsHtml = productFactsSection(product);
|
||
const blocksHtml = product.blocks.length
|
||
? `${renderBlocks(product.blocks, galleryImages)}${
|
||
!hasGalleryBlock && galleryImages.length
|
||
? `<section class="block-section container-luxury">${pdpGallerySection(galleryImages, pdpDiscount)}</section>`
|
||
: ""
|
||
}${
|
||
product.description
|
||
? `<section class="block-section container-luxury"><h2>معرفی و توضیحات محصول</h2>${productDescriptionHtml(product.description)}</section>`
|
||
: ""
|
||
}${factsHtml}`
|
||
: `<section class="block-section container-luxury">${pdpGallerySection(galleryImages, pdpDiscount)}${
|
||
product.description
|
||
? `<h2>معرفی و توضیحات محصول</h2>${productDescriptionHtml(product.description)}`
|
||
: ""
|
||
}</section>${factsHtml}`;
|
||
|
||
const hasVariants = (product.colorVariants?.length || 0) > 1;
|
||
const pdpTitle = withPersianCategory(
|
||
displayNameWithoutShade(product.name, hasVariants),
|
||
product.category
|
||
);
|
||
|
||
const heroHtml = hero?.image
|
||
? `<section class="pdp-hero">
|
||
<img src="${esc(hero.image)}" alt="${esc(hero.imageAlt || hero.headline || pdpTitle)}" />
|
||
<div class="pdp-hero-overlay"></div>
|
||
<div class="pdp-hero-copy">
|
||
<div>
|
||
<h1>${esc(hero.headline ? withPersianCategory(hero.headline, product.category) : pdpTitle)}</h1>
|
||
${hero.subheadline ? `<p>${esc(hero.subheadline)}</p>` : ""}
|
||
${
|
||
hero.ctaText && hero.ctaLink
|
||
? `<p style="margin-top:1.5rem"><a class="btn btn-light" href="${esc(
|
||
hero.ctaLink.startsWith("#") || hero.ctaLink.startsWith("/")
|
||
? hero.ctaLink
|
||
: `#${hero.ctaLink}`
|
||
)}">${esc(hero.ctaText)}</a></p>`
|
||
: ""
|
||
}
|
||
</div>
|
||
</div>
|
||
</section>`
|
||
: `<div style="padding-top:5rem"></div>`;
|
||
|
||
const sticky = `<div class="sticky-buy">
|
||
<div style="min-width:0">
|
||
<p style="margin:0;font-size:0.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" data-sticky-name>${esc(pdpTitle)}</p>
|
||
<p style="margin:0.2rem 0 0" data-sticky-price>${esc(formatPrice(product.price))}</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="btn"
|
||
data-add-to-cart
|
||
data-product-id="${esc(product.id)}"
|
||
data-slug="${esc(product.slug)}"
|
||
data-name="${esc(pdpTitle)}"
|
||
data-price="${product.price}"
|
||
data-image="${esc(product.heroImage || "")}"
|
||
data-category="${esc(product.category || "")}"
|
||
data-category-fa="${esc(persianCategoryLabel(product.category, product.name) || "")}"
|
||
data-locale="fa"
|
||
${product.stock > 0 ? "" : "disabled"}
|
||
>${product.stock > 0 ? esc(dict.addToCart) : esc(dict.soldOut)}</button>
|
||
</div>`;
|
||
|
||
const relatedProducts = rankRelatedProducts(
|
||
product,
|
||
listingSource.filter((candidate) => Boolean(candidate.heroImage)),
|
||
4
|
||
);
|
||
const relatedHtml =
|
||
!relatedProducts.length
|
||
? ""
|
||
: `<section class="section-padding">
|
||
<div class="container-luxury">
|
||
<p class="label-caps text-muted" style="text-align:center">ادامه کاوش</p>
|
||
<h2 style="text-align:center;margin:0.75rem 0 2.5rem;font-weight:300">${esc(dict.youMayAlsoLike)}</h2>
|
||
<div class="related-grid">
|
||
${relatedProducts
|
||
.map((p) => {
|
||
const colorVariants =
|
||
p.colorVariants && p.colorVariants.length > 1
|
||
? p.colorVariants
|
||
: variantMapAll.get(p.id);
|
||
return relatedProductCard(
|
||
{
|
||
...p,
|
||
colorVariants:
|
||
colorVariants && colorVariants.length > 1
|
||
? colorVariants
|
||
: undefined,
|
||
},
|
||
{ trailingSlash: true }
|
||
);
|
||
})
|
||
.join("")}
|
||
</div>
|
||
</div>
|
||
</section>`;
|
||
|
||
const body = `
|
||
${heroHtml}
|
||
<div class="container-luxury pdp-layout">
|
||
${buyPanel({
|
||
product,
|
||
dict,
|
||
colorVariants: product.colorVariants,
|
||
trailingSlash: true,
|
||
})}
|
||
<div class="pdp-content">${blocksHtml}</div>
|
||
</div>
|
||
<section class="section-padding" style="background:rgba(247,247,247,0.6)" id="reviews">
|
||
<div class="container-luxury">
|
||
<p class="label-caps text-muted">${esc(dict.reviews)}</p>
|
||
<h2 style="margin:0.75rem 0 2rem;font-size:clamp(1.75rem,4vw,2.25rem);font-weight:300">نظرات مشتریان</h2>
|
||
<p class="text-muted">هنوز نظری ثبت نشده است.</p>
|
||
</div>
|
||
</section>
|
||
${relatedHtml}
|
||
${sticky}
|
||
`;
|
||
|
||
write(
|
||
`products/${product.slug}/index.html`,
|
||
pageLayout({
|
||
title: seoProductTitle(pdpTitle),
|
||
description: seoProductDescription({
|
||
name: pdpTitle,
|
||
categoryFa: persianCategoryLabel(product.category, product.name),
|
||
commercialTerm: commercialProductSearchTerm(product.name, product.category),
|
||
existing: product.seoDescription || product.tagline || product.description,
|
||
}),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: `/products/${product.slug}/`,
|
||
ogImage: product.heroImage,
|
||
ogType: "product",
|
||
headExtra: jsonLdGraph([
|
||
organizationJsonLd(),
|
||
breadcrumbJsonLd(
|
||
productBreadcrumbItems({
|
||
name: pdpTitle,
|
||
slug: product.slug,
|
||
category: product.category,
|
||
})
|
||
),
|
||
productJsonLd({
|
||
name: pdpTitle,
|
||
description:
|
||
product.seoDescription || product.tagline || product.description,
|
||
image: product.heroImage,
|
||
extraImages: galleryImages,
|
||
urlPath: `/products/${product.slug}/`,
|
||
sku: product.slug,
|
||
price: product.price,
|
||
compareAtPrice: product.compareAtPrice,
|
||
inStock:
|
||
product.stock > 0 ||
|
||
(product.colorVariants || []).some((v) => Number(v.stock) > 0),
|
||
}),
|
||
]),
|
||
body,
|
||
})
|
||
);
|
||
sitemapPaths.push(`/products/${product.slug}/`);
|
||
writeLegacyFaRedirect(
|
||
`products/${product.slug}/index.html`,
|
||
`/products/${product.slug}/`,
|
||
`${pdpTitle} | Kikomiko`
|
||
);
|
||
}
|
||
|
||
write(
|
||
"search/index.html",
|
||
pageLayout({
|
||
title: `${dict.searchResults} | Kikomiko`,
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/search/",
|
||
noindex: true,
|
||
body: `<div class="page-pad">
|
||
<div class="container-luxury" style="padding-bottom:1.5rem">
|
||
<p class="label-caps text-muted">${esc(dict.searchResults)}</p>
|
||
<h1 class="page-title" data-search-title>${esc(dict.searchResults)}</h1>
|
||
</div>
|
||
<div class="container-luxury" style="padding-bottom:2rem">
|
||
<div class="filters" style="margin:0">
|
||
${productListingFilters({
|
||
megaMenu,
|
||
productCount: 0,
|
||
mode: "search",
|
||
clientMode: true,
|
||
})}
|
||
</div>
|
||
</div>
|
||
<div class="container-luxury list-grid" data-product-grid data-search-results></div>
|
||
<div class="container-luxury section-padding" data-search-empty hidden style="text-align:center">
|
||
<p class="text-muted">${esc(dict.searchEmpty)}</p>
|
||
<p style="margin-top:1.5rem"><a href="/" class="label-caps" style="text-decoration:underline">${esc(dict.backHome)}</a></p>
|
||
</div>
|
||
</div>
|
||
<script type="module" src="/js/search-page.js?v=filterNav1"></script>`,
|
||
})
|
||
);
|
||
writeLegacyFaRedirect("search/index.html", "/search/", `${dict.searchResults} | Kikomiko`);
|
||
|
||
write(
|
||
"cart/index.html",
|
||
pageLayout({
|
||
title: `${dict.cart} | Kikomiko`,
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/cart/",
|
||
noindex: true,
|
||
nofollow: true,
|
||
body: `<div class="page-pad container-luxury" data-cart-page>
|
||
<p class="label-caps text-muted">${esc(dict.cart)}</p>
|
||
<h1 class="page-title">${esc(dict.cart)}</h1>
|
||
<div data-cart-empty hidden style="margin-top:2.5rem;text-align:center">
|
||
<p class="empty-cart">${esc(dict.emptyCart)}</p>
|
||
<p style="margin-top:1.5rem"><a href="/" class="label-caps" style="text-decoration:underline">${esc(dict.continueShopping)}</a></p>
|
||
</div>
|
||
<div data-cart-content class="cart-page-layout">
|
||
<div class="cart-summary cart-summary--page">
|
||
<div data-cart-lines></div>
|
||
<div class="cart-total"><span>جمع</span><strong data-cart-total>۰ تومان</strong></div>
|
||
</div>
|
||
<div class="cart-page-actions">
|
||
<a class="btn btn-block" href="/checkout/" data-cart-checkout>${esc(dict.proceedToCheckout)}</a>
|
||
<p style="margin-top:1rem;text-align:center"><a href="/" class="text-muted" style="text-decoration:underline;font-size:0.875rem">${esc(dict.continueShopping)}</a></p>
|
||
</div>
|
||
</div>
|
||
</div>`,
|
||
})
|
||
);
|
||
writeLegacyFaRedirect("cart/index.html", "/cart/", `${dict.cart} | Kikomiko`);
|
||
|
||
write(
|
||
"checkout/index.html",
|
||
pageLayout({
|
||
title: `تسویه حساب | Kikomiko`,
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/checkout/",
|
||
noindex: true,
|
||
nofollow: true,
|
||
body: `<div class="page-pad container-luxury">
|
||
<p class="label-caps text-muted">تسویه حساب</p>
|
||
<h1 class="page-title">اطلاعات سفارش</h1>
|
||
<div class="checkout-layout" data-checkout>
|
||
<form>
|
||
<div class="form-error" data-checkout-error hidden></div>
|
||
<div class="form-success" data-checkout-success hidden></div>
|
||
<label class="field"><span>نام *</span><input name="firstName" required autocomplete="given-name" /></label>
|
||
<label class="field"><span>نام خانوادگی *</span><input name="lastName" required autocomplete="family-name" /></label>
|
||
<label class="field"><span>ایمیل</span><input type="email" name="email" autocomplete="email" /></label>
|
||
<label class="field"><span>تلفن *</span><input name="phone" type="tel" required placeholder="0912xxxxxxx" autocomplete="tel" /></label>
|
||
<label class="field"><span>آدرس *</span><textarea name="address" rows="3" required autocomplete="street-address"></textarea></label>
|
||
<div class="checkout-loc-row">
|
||
<label class="field"><span>استان *</span>
|
||
<select name="province" required data-province-select>
|
||
<option value="">انتخاب استان</option>
|
||
</select>
|
||
</label>
|
||
<label class="field"><span>شهر *</span>
|
||
<select name="city" required data-city-select disabled>
|
||
<option value="">ابتدا استان را انتخاب کنید</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<label class="field"><span>یادداشت</span><textarea name="notes" rows="2"></textarea></label>
|
||
<button type="submit" class="btn btn-block">ثبت سفارش از طریق واتساپ</button>
|
||
<p class="text-muted" style="font-size:0.8rem;margin-top:0.75rem;line-height:1.6">
|
||
با ثبت سفارش، فاکتور PDF ساخته میشود و واتساپ فروشگاه با لینک فاکتور باز میگردد — فقط ارسال را بزنید.
|
||
</p>
|
||
</form>
|
||
<aside>
|
||
<div class="cart-summary">
|
||
<h2 style="margin-top:0;font-size:1.1rem;font-weight:500">خلاصه سبد</h2>
|
||
<div data-cart-lines></div>
|
||
<div class="cart-total"><span>جمع</span><strong data-cart-total>۰ تومان</strong></div>
|
||
<p style="margin-top:1rem"><a href="/cart/" class="btn btn-block checkout-cart-btn">${esc(dict.cart)}</a></p>
|
||
</div>
|
||
<div class="cart-summary" data-order-receipt hidden style="margin-top:1.5rem">
|
||
<h2 style="margin-top:0;font-size:1.1rem;font-weight:500">رسید سفارش</h2>
|
||
<p class="text-muted" style="margin:0.75rem 0 0;font-size:0.875rem">شماره سفارش</p>
|
||
<p style="margin:0.25rem 0 0" data-receipt-id></p>
|
||
<p class="text-muted" style="margin:0.75rem 0 0;font-size:0.875rem">تاریخ</p>
|
||
<p style="margin:0.25rem 0 0" data-receipt-date></p>
|
||
<p class="text-muted" style="margin:0.75rem 0 0;font-size:0.875rem">نام مشتری</p>
|
||
<p style="margin:0.25rem 0 0" data-receipt-name></p>
|
||
<p class="text-muted" style="margin:0.75rem 0 0;font-size:0.875rem">اقلام</p>
|
||
<div class="receipt-items" data-receipt-items></div>
|
||
<p class="text-muted" style="margin:0.75rem 0 0;font-size:0.875rem">جمع</p>
|
||
<p style="margin:0.25rem 0 0" data-receipt-total></p>
|
||
<p style="margin-top:1.25rem;display:grid;gap:0.6rem">
|
||
<button type="button" class="btn btn-block" data-receipt-whatsapp>ارسال در واتساپ</button>
|
||
<button type="button" class="btn btn-block" data-receipt-pdf style="background:#fff;color:#0a0a0a;border:1px solid #0a0a0a">دانلود فاکتور PDF</button>
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>`,
|
||
})
|
||
);
|
||
writeLegacyFaRedirect("checkout/index.html", "/checkout/", `تسویه حساب | Kikomiko`);
|
||
|
||
write(
|
||
"about/index.html",
|
||
pageLayout({
|
||
title: seoAboutTitle(),
|
||
description: seoAboutDescription(),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/about/",
|
||
headExtra: jsonLdGraph([
|
||
organizationJsonLd(),
|
||
aboutPageJsonLd(),
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: "درباره کیکو میکو", path: "/about/" },
|
||
]),
|
||
]),
|
||
body: aboutPageBody(),
|
||
})
|
||
);
|
||
sitemapPaths.push("/about/");
|
||
writeLegacyFaRedirect("about/index.html", "/about/", "درباره کیکو میکو | کیکومیکو");
|
||
|
||
write(
|
||
"contact/index.html",
|
||
pageLayout({
|
||
title: seoContactTitle(),
|
||
description: seoContactDescription(),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/contact/",
|
||
headExtra: jsonLdGraph([
|
||
organizationJsonLd(),
|
||
contactPageJsonLd(),
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: "تماس با ما", path: "/contact/" },
|
||
]),
|
||
]),
|
||
body: contactPageBody(),
|
||
})
|
||
);
|
||
sitemapPaths.push("/contact/");
|
||
writeLegacyFaRedirect("contact/index.html", "/contact/", "تماس با ما | کیکومیکو");
|
||
|
||
write(
|
||
"authenticity/index.html",
|
||
pageLayout({
|
||
title: seoAuthenticityTitle(),
|
||
description: seoAuthenticityDescription(),
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/authenticity/",
|
||
headExtra: jsonLdGraph([
|
||
organizationJsonLd(),
|
||
authenticityPageJsonLd(),
|
||
breadcrumbJsonLd([
|
||
{ name: "خانه", path: "/" },
|
||
{ name: "اصالت کالا", path: "/authenticity/" },
|
||
]),
|
||
]),
|
||
body: authenticityPageBody(),
|
||
})
|
||
);
|
||
sitemapPaths.push("/authenticity/");
|
||
writeLegacyFaRedirect(
|
||
"authenticity/index.html",
|
||
"/authenticity/",
|
||
"اصالت کالا | کیکومیکو"
|
||
);
|
||
|
||
write(
|
||
"404.html",
|
||
pageLayout({
|
||
title: "یافت نشد | Kikomiko",
|
||
dict,
|
||
megaMenu,
|
||
canonicalPath: "/404.html",
|
||
noindex: true,
|
||
body: `<div class="container-luxury section-padding" style="text-align:center">
|
||
<h1 class="page-title">صفحه پیدا نشد</h1>
|
||
<p style="margin-top:1.5rem"><a href="/" class="label-caps" style="text-decoration:underline">${esc(dict.backHome)}</a></p>
|
||
</div>`,
|
||
})
|
||
);
|
||
|
||
write("robots.txt", ROBOTS_TXT);
|
||
write("sitemap.xml", buildSitemapXml(sitemapPaths, catalogUpdatedAt));
|
||
|
||
console.log(`Done. ${slice.length} product pages in ${DIST}`);
|
||
}
|
||
|
||
function categoryBody(
|
||
dict: ReturnType<typeof getDictionary>,
|
||
title: string,
|
||
rootSlug: string,
|
||
products: SiteProduct[],
|
||
sub: string | null,
|
||
megaMenu: MegaMenuRoot[],
|
||
landingKey?: string
|
||
) {
|
||
const landing = categoryLandingFor(landingKey);
|
||
const introLabel = sub || title;
|
||
const pageTitle = landing?.h1 ?? title;
|
||
const featured = (products.some((p) => p.featured)
|
||
? products.filter((p) => p.featured)
|
||
: products
|
||
).slice(0, 4);
|
||
const landingHtml = landing
|
||
? `<section class="container-luxury section-padding${landingKey === "lips-make-up" ? " lips-guide--compact" : ""}" aria-labelledby="${esc(landingKey)}-guide-title">
|
||
<p class="label-caps text-muted">راهنمای خرید</p>
|
||
<h2 id="${esc(landingKey)}-guide-title" class="page-title" style="font-size:clamp(1.5rem,3vw,2.25rem)">${esc(landing.guideTitle)}</h2>
|
||
<p class="text-muted" style="margin-top:0.9rem;max-width:48rem;line-height:1.9">${esc(landing.intro)}</p>
|
||
<h3 style="margin-top:2rem">دستهبندی محصولات</h3>
|
||
<ul class="footer-links" style="padding-bottom:0;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));margin-top:0.85rem">
|
||
${landing.types.map((item) => `<li><a href="/search/?q=${encodeURIComponent(item.query)}">${esc(item.label)}</a></li>`).join("")}
|
||
</ul>
|
||
<h3 style="margin-top:2rem">محصولات پرفروش</h3>
|
||
<ul class="footer-links" style="padding-bottom:0;margin-top:0.85rem">
|
||
${featured.map((p) => `<li><a href="/products/${esc(p.slug)}/">${esc(displayNameWithoutShade(p.name, Boolean(p.variantGroupId)))}</a></li>`).join("")}
|
||
</ul>
|
||
<h3 style="margin-top:2rem">راهنمای انتخاب رنگ و فینیش</h3>
|
||
<p class="text-muted" style="margin-top:0.75rem;max-width:48rem;line-height:1.9">${esc(landing.guide)}</p>
|
||
${landingKey === "lips-make-up" ? `<div style="margin-top:2rem;max-width:48rem"><p class="text-muted" style="line-height:1.8">${esc(seoCategoryIntro(introLabel))}</p><p style="margin-top:1.25rem"><a class="btn" href="/category/${esc(rootSlug)}/">همهٔ دستهها</a></p></div>` : ""}
|
||
</section>`
|
||
: "";
|
||
return `<div class="page-pad${landingKey === "lips-make-up" ? " page-pad--lips-guide" : ""}">
|
||
<div class="container-luxury" style="padding-bottom:${landingKey === "lips-make-up" ? "0" : "1.5rem"}">
|
||
${landingKey === "lips-make-up" ? "" : `<p class="label-caps text-muted">${esc(dict.collection)}</p>`}
|
||
${landingKey === "lips-make-up" ? "" : `<h1 class="page-title">${esc(pageTitle)}</h1>`}
|
||
${landingKey === "lips-make-up" ? "" : `<p class="text-muted" style="margin-top:0.75rem;max-width:40rem;line-height:1.8">${esc(seoCategoryIntro(introLabel))}</p>`}
|
||
${
|
||
sub && landingKey !== "lips-make-up"
|
||
? `<p style="margin-top:1.25rem"><a class="btn" href="/category/${esc(rootSlug)}/">همهٔ دستهها</a></p>`
|
||
: ""
|
||
}
|
||
</div>
|
||
${landingKey === "lips-make-up" ? landingHtml : ""}
|
||
<div class="container-luxury" style="padding-bottom:2rem">
|
||
<div class="filters" style="margin:0">
|
||
${productListingFilters({
|
||
megaMenu,
|
||
productCount: products.length,
|
||
categorySlug: rootSlug,
|
||
currentSub: sub,
|
||
mode: "category",
|
||
clientMode: true,
|
||
})}
|
||
</div>
|
||
</div>
|
||
${
|
||
products.length === 0
|
||
? `<div class="container-luxury section-padding" style="text-align:center"><p class="text-muted">${esc(dict.noProducts)}</p></div>`
|
||
: `<div class="container-luxury list-grid" data-product-grid>${products
|
||
.map((p) => listProductCard(p, { trailingSlash: true }))
|
||
.join("")}</div>`
|
||
}
|
||
${landingKey === "lips-make-up" ? "" : landingHtml}
|
||
</div>`;
|
||
}
|
||
|
||
type ProductTypeGuide = {
|
||
slug: string;
|
||
rootSlug: "make-up";
|
||
typeFa: string;
|
||
h1: string;
|
||
title: string;
|
||
description: string;
|
||
intro: string;
|
||
needs: Array<{ title: string; text: string; query: string }>;
|
||
comparison: Array<{ title: string; text: string }>;
|
||
colour: string;
|
||
related: Array<{ label: string; href: string }>;
|
||
faq: Array<{ q: string; a: string }>;
|
||
};
|
||
|
||
const PRODUCT_TYPE_GUIDES: ProductTypeGuide[] = [
|
||
{
|
||
slug: "lip-gloss", rootSlug: "make-up", typeFa: "لیپ گلاس", h1: "خرید لیپ گلاس کیکو میلانو",
|
||
title: "خرید لیپ گلاس کیکو میلانو اصل | کیکومیکو", description: "خرید لیپ گلاس کیکو میلانو اصل با رنگها و فینیشهای متنوع در کیکومیکو.",
|
||
intro: "لیپ گلاسهای اصل KIKO Milano را در رنگها و فینیشهای مختلف ببینید؛ از برق ملایم روزانه تا رنگدهی چشمگیرتر.",
|
||
needs: [{ title: "براقترین گزینهها", text: "برای جلوهای درخشان و لبهای پرتر.", query: "lip gloss" }, { title: "روزانه", text: "بافت سبک و رنگ طبیعی برای هر روز.", query: "lip oil" }, { title: "رنگدهی بیشتر", text: "برای رنگ واضحتر و جلوهٔ برجستهتر.", query: "pigmented lip gloss" }],
|
||
comparison: [{ title: "Glossy", text: "برای ظاهر درخشان و حجیمتر." }, { title: "Matte", text: "برای رنگ پررنگتر و بدون درخشش." }],
|
||
colour: "پوست روشن: Nude یا Pink؛ پوست متوسط: Rose یا Mauve؛ تناژ گرم: Peach یا Brown. رنگ نهایی را با توجه به فینیش و شدت رنگ انتخاب کنید.",
|
||
related: [{ label: "رژ لب کیکو", href: "/category/make-up/sub/lips-make-up/" }, { label: "مداد لب کیکو", href: "/category/make-up/sub/lips-make-up/" }],
|
||
faq: [{ q: "برای استفاده روزانه کدام لیپ گلاس مناسبتر است؟", a: "بافتهای سبک با رنگ طبیعی معمولاً برای استفادهٔ روزانه انتخاب راحتتری هستند." }, { q: "لیپ گلاس مات است یا براق؟", a: "لیپ گلاس معمولاً جلوهای براق دارد؛ برای نتیجهٔ بدون درخشش، محصولات مات لب را بررسی کنید." }],
|
||
},
|
||
{
|
||
slug: "mascara", rootSlug: "make-up", typeFa: "ریمل", h1: "خرید ریمل کیکو میلانو",
|
||
title: "خرید ریمل کیکو میلانو اصل | کیکومیکو", description: "خرید ریمل کیکو میلانو اصل برای حجم، طول و فرمدهی مژهها.",
|
||
intro: "ریملهای KIKO Milano را برای حجمدهی، بلندکنندگی یا تفکیک مژهها مقایسه کنید.",
|
||
needs: [{ title: "حجمدهی", text: "برای مژههای پرتر و چشمگیرتر.", query: "volumizing mascara" }, { title: "بلندکننده", text: "برای کشیدهتر دیدهشدن مژهها.", query: "lengthening mascara" }, { title: "ماندگار", text: "برای استفادهٔ طولانی یا روزهای شلوغ.", query: "waterproof mascara" }],
|
||
comparison: [{ title: "Volume", text: "برای تراکم و جلوهٔ پرتر." }, { title: "Length", text: "برای ظاهر بلندتر و تفکیکشده." }],
|
||
colour: "مشکی برای تعریف کلاسیک چشم مناسب است؛ برای ظاهر نرمتر میتوانید سراغ قهوهای یا رنگهای کالکشنهای محدود بروید.",
|
||
related: [{ label: "خط چشم کیکو", href: "/category/make-up/sub/eyes-make-up/" }, { label: "مداد چشم کیکو", href: "/category/make-up/type/eye-pencil/" }],
|
||
faq: [{ q: "ریمل حجمدهنده برای چه کسی مناسب است؟", a: "برای کسانی که جلوهٔ پرتر و مژههای متراکمتر میخواهند." }, { q: "ریمل ضدآب چه زمانی بهتر است؟", a: "برای ماندگاری بیشتر در شرایط مرطوب یا استفادهٔ طولانی." }],
|
||
},
|
||
{
|
||
slug: "foundation", rootSlug: "make-up", typeFa: "کرم پودر", h1: "خرید کرم پودر کیکو میلانو",
|
||
title: "خرید کرم پودر کیکو میلانو اصل | کیکومیکو", description: "خرید کرم پودر کیکو میلانو اصل با پوشش و فینیشهای مختلف.",
|
||
intro: "کرم پودرهای KIKO Milano را بر اساس پوشش، فینیش و تناژ رنگ پوست انتخاب کنید.",
|
||
needs: [{ title: "پوشش طبیعی", text: "برای یکدستشدن سبک پوست.", query: "skin tint foundation" }, { title: "پوشش بیشتر", text: "برای نتیجهٔ کاملتر.", query: "full coverage foundation" }, { title: "ماندگار", text: "برای آرایش طولانیمدت.", query: "long wear foundation" }],
|
||
comparison: [{ title: "Natural / Glow", text: "برای پوست تازه و درخشان." }, { title: "Matte", text: "برای جلوهٔ ماتتر و کنترل برق پوست." }],
|
||
colour: "تناژ را با زیرتون پوست هماهنگ کنید: Rose برای زیرتون سرد، Gold برای گرم و Neutral برای حالت متعادل.",
|
||
related: [{ label: "کانسیلر کیکو", href: "/category/make-up/type/concealer/" }, { label: "پرایمر کیکو", href: "/category/make-up/sub/face-make-up/" }],
|
||
faq: [{ q: "فینیش مات یا درخشان؟", a: "مات برای ظاهر بدون برق و درخشان برای نتیجهٔ طبیعیتر و شادابتر مناسب است." }, { q: "چطور رنگ مناسب را انتخاب کنم؟", a: "شماره و زیرتون محصول را با رنگ گردن و زیرتون پوست مقایسه کنید." }],
|
||
},
|
||
{
|
||
slug: "concealer", rootSlug: "make-up", typeFa: "کانسیلر", h1: "خرید کانسیلر کیکو میلانو",
|
||
title: "خرید کانسیلر کیکو میلانو اصل | کیکومیکو", description: "خرید کانسیلر کیکو میلانو اصل برای پوشاندن تیرگی و یکدستکردن پوست.",
|
||
intro: "کانسیلرهای KIKO Milano را برای پوشاندن تیرگی زیر چشم، لکها و روشنترکردن نقاط صورت بررسی کنید.",
|
||
needs: [{ title: "زیر چشم", text: "برای ظاهر یکنواختتر زیر چشم.", query: "under eye concealer" }, { title: "پوشش بیشتر", text: "برای پوشاندن لک و ناهماهنگی رنگ.", query: "full coverage concealer" }, { title: "روشنکننده", text: "برای نقاط نور صورت.", query: "highlighting concealer" }],
|
||
comparison: [{ title: "Lightweight", text: "برای استفادهٔ روزانه و بافت سبک." }, { title: "Full coverage", text: "برای پوشش بیشتر و نتیجهٔ واضحتر." }],
|
||
colour: "برای زیر چشم، رنگی نزدیک به پوست یا کمی روشنتر انتخاب کنید؛ برای لک، نزدیکترین رنگ به کرم پودر معمولاً طبیعیتر دیده میشود.",
|
||
related: [{ label: "کرم پودر کیکو", href: "/category/make-up/type/foundation/" }, { label: "هایلایتر کیکو", href: "/category/make-up/sub/face-make-up/" }],
|
||
faq: [{ q: "کانسیلر روشنتر یا همرنگ پوست؟", a: "برای زیر چشم کمی روشنتر و برای پوشاندن لک، نزدیک به رنگ پوست انتخاب میشود." }, { q: "چطور از جمعشدن کانسیلر جلوگیری کنم؟", a: "با مقدار کم شروع کنید و آن را با ضربههای سبک محو کنید." }],
|
||
},
|
||
{
|
||
slug: "blush", rootSlug: "make-up", typeFa: "رژگونه", h1: "خرید رژگونه کیکو میلانو",
|
||
title: "خرید رژگونه کیکو میلانو اصل | کیکومیکو", description: "خرید رژگونه کیکو میلانو اصل با رنگها و فینیشهای متنوع.",
|
||
intro: "رژگونههای KIKO Milano برای رنگ طبیعی روزانه یا جلوهٔ پررنگتر مهمانی در دسترساند.",
|
||
needs: [{ title: "ظاهر طبیعی", text: "برای رنگ ملایم و قابلساخت.", query: "natural blush" }, { title: "درخشان", text: "برای جلوهٔ شادابتر پوست.", query: "glow blush" }, { title: "مات", text: "برای رنگ واضح و بدون برق.", query: "matte blush" }],
|
||
comparison: [{ title: "Glow", text: "برای پوست شاداب و درخشان." }, { title: "Matte", text: "برای رنگی نرم و بدون درخشش." }],
|
||
colour: "Pink و Rose معمولاً برای پوستهای روشنتر، و Peach یا Berry برای طیفهای گرمتر و متوسط انتخابهای محبوبی هستند.",
|
||
related: [{ label: "برنزر کیکو", href: "/category/make-up/sub/face-make-up/" }, { label: "هایلایتر کیکو", href: "/category/make-up/sub/face-make-up/" }],
|
||
faq: [{ q: "رژگونه مات یا درخشان؟", a: "مات ظاهر نرمتری میدهد و درخشان، نور و شادابی بیشتری به گونهها اضافه میکند." }, { q: "رنگ رژگونه را چطور انتخاب کنم؟", a: "به زیرتون پوست و شدت رنگی که در آرایش روزانه میپسندید توجه کنید." }],
|
||
},
|
||
{
|
||
slug: "eye-pencil", rootSlug: "make-up", typeFa: "مداد چشم", h1: "خرید مداد چشم کیکو میلانو",
|
||
title: "خرید مداد چشم کیکو میلانو اصل | کیکومیکو", description: "خرید مداد چشم کیکو میلانو اصل با رنگها و فینیشهای متنوع.",
|
||
intro: "مداد چشمهای KIKO Milano برای خط داخل چشم، خط مژه و ساخت آرایشهای محو یا دقیق انتخابهای متنوعی دارند.",
|
||
needs: [{ title: "خط دقیق", text: "برای تعریف خط مژه.", query: "eye pencil" }, { title: "اسموکی", text: "برای محوکردن و عمق بیشتر.", query: "smoky eye pencil" }, { title: "رنگی", text: "برای یک نقطهٔ رنگی در آرایش چشم.", query: "colour eye pencil" }],
|
||
comparison: [{ title: "Matte", text: "برای خطی واضح و کنترلشده." }, { title: "Metallic", text: "برای جلوهای نورانیتر روی چشم." }],
|
||
colour: "Black و Brown برای استفادهٔ روزانه و تعریف چشم مناسباند؛ رنگهای آبی، سبز یا بنفش برای آرایشهای رنگی انتخاب جذابی هستند.",
|
||
related: [{ label: "خط چشم کیکو", href: "/category/make-up/sub/eyes-make-up/" }, { label: "ریمل کیکو", href: "/category/make-up/type/mascara/" }],
|
||
faq: [{ q: "مداد چشم برای خط داخل چشم مناسب است؟", a: "بسته به مشخصات هر محصول، برخی مدادها برای خط داخلی و خارجی چشم طراحی شدهاند." }, { q: "فینیش مات یا متالیک؟", a: "مات برای آرایش کلاسیک و متالیک برای جلوهٔ نورانیتر مناسب است." }],
|
||
},
|
||
];
|
||
|
||
function productTypeGuideBody(
|
||
dict: ReturnType<typeof getDictionary>,
|
||
guide: ProductTypeGuide,
|
||
products: SiteProduct[],
|
||
megaMenu: MegaMenuRoot[]
|
||
) {
|
||
const featured = products.slice(0, 4);
|
||
return `<div class="page-pad">
|
||
<header class="container-luxury type-guide-header">
|
||
<nav class="type-guide-crumb" aria-label="breadcrumb"><a href="/">خانه</a><span>‹</span><a href="/category/${guide.rootSlug}/">آرایش</a><span>‹</span><span>${esc(guide.typeFa)}</span></nav>
|
||
<h1 class="page-title">${esc(guide.h1)}</h1>
|
||
<p class="text-muted type-guide-summary">${esc(guide.intro)}</p>
|
||
</header>
|
||
<div class="container-luxury type-guide-filters"><div class="filters" style="margin:0">${productListingFilters({ megaMenu, productCount: products.length, categorySlug: guide.rootSlug, mode: "category", clientMode: true })}</div></div>
|
||
<div class="container-luxury list-grid" id="products" data-product-grid>${products.map((product) => listProductCard(product, { trailingSlash: true })).join("")}</div>
|
||
<section class="editorial-guide" aria-labelledby="${guide.slug}-guide-title"><div class="container-luxury">
|
||
<div class="editorial-guide-intro"><p class="label-caps text-muted">راهنمای خرید</p><h2 id="${guide.slug}-guide-title">راهنمای خرید ${esc(guide.typeFa)} کیکو</h2><p>${esc(guide.intro)}</p></div>
|
||
<div class="editorial-need-grid">${guide.needs.map((need, index) => `<a class="editorial-need-card" href="/search/?q=${encodeURIComponent(need.query)}"><span>✦ ${String(index + 1).padStart(2, "۰")}</span><h3>${esc(need.title)}</h3><p>${esc(need.text)}</p><b>مشاهده</b></a>`).join("")}</div>
|
||
<div class="editorial-two-col"><section><h3>فینیش مناسب شما کدام است؟</h3><div class="editorial-compare">${guide.comparison.map((item) => `<article><h4>${esc(item.title)}</h4><p>${esc(item.text)}</p></article>`).join("")}</div></section><section><h3>چطور انتخاب کنیم؟</h3><p class="text-muted">${esc(guide.colour)}</p></section></div>
|
||
<section class="editorial-recommended"><h3>پیشنهادهای منتخب</h3><div>${featured.map((product) => `<a href="/products/${esc(product.slug)}/">${esc(displayNameWithoutShade(product.name, Boolean(product.variantGroupId)))}</a>`).join("")}</div></section>
|
||
<section class="editorial-related"><h3>دستههای مرتبط</h3>${guide.related.map((item) => `<a href="${esc(item.href)}">${esc(item.label)} <span>←</span></a>`).join("")}</section>
|
||
<section class="editorial-faq"><h3>سوالات متداول</h3>${guide.faq.map((item) => `<details class="faq-item"><summary>${esc(item.q)}</summary><p>${esc(item.a)}</p></details>`).join("")}</section>
|
||
<div class="editorial-cta"><p>هنوز مطمئن نیستید؟</p><h3>تمام ${esc(guide.typeFa)}های KIKO Milano را ببینید</h3><a class="btn" href="#products">مشاهده همه محصولات</a></div>
|
||
</div></section>
|
||
</div>`;
|
||
}
|
||
|
||
function categoryLandingFor(key?: string) {
|
||
const pages = {
|
||
"face-cleasing": {
|
||
h1: "خرید محصولات پاککننده صورت کیکو میلانو",
|
||
title: "خرید محصولات پاککننده صورت کیکو میلانو | کیکومیکو",
|
||
guideTitle: "راهنمای خرید پاککننده صورت کیکو میلانو",
|
||
intro: "پاککنندههای صورت کیکو میلانو برای پاککردن آرایش، آلودگی و چربی روزانه طراحی شدهاند تا پوست برای ادامهٔ روتین مراقبت آماده و تازه بماند.",
|
||
types: [
|
||
{ label: "میسلار واتر", query: "micellar" },
|
||
{ label: "ژل و فوم شوینده", query: "cleanser" },
|
||
{ label: "روغن و بالم پاککننده", query: "cleansing oil" },
|
||
{ label: "دستمال پاککننده آرایش", query: "makeup remover" },
|
||
],
|
||
guide: "برای پاکسازی روزانه، بافت محصول را متناسب با نوع پوست و میزان آرایش انتخاب کنید: میسلار واتر گزینهای سبک و سریع است، ژل و فوم برای شستوشوی صورت مناسباند و روغن یا بالم پاککننده به حلکردن آرایش مقاوم کمک میکنند. پس از پاکسازی، روتین آبرسانی پوست را ادامه دهید.",
|
||
},
|
||
"lips-make-up": {
|
||
h1: "خرید محصولات آرایش لب کیکو میلانو",
|
||
title: "خرید رژ لب و لیپ گلاس کیکو میلانو اصل | KIKO Milano",
|
||
guideTitle: "راهنمای خرید محصولات آرایش لب کیکو میلانو",
|
||
intro: "برای انتخاب آرایش لب کیکو میلانو، از رژ لبهای مات و کرمی تا لیپ گلاس، روغن لب و بالم لب، رنگ و بافتی را انتخاب کنید که با آرایش روزانه یا مهمانی شما هماهنگ باشد.",
|
||
types: [
|
||
{ label: "رژ لب کیکو", query: "lipstick" },
|
||
{ label: "لیپ گلاس کیکو", query: "lip gloss" },
|
||
{ label: "مداد لب کیکو", query: "lip pencil" },
|
||
{ label: "Lip Oil", query: "lip oil" },
|
||
{ label: "Lip Balm", query: "lip balm" },
|
||
],
|
||
guide: "برای نتیجهای مخملی و ماندگار، finish مات را انتخاب کنید؛ بافت کرمی برای رنگدهی راحت و حس نرم مناسب است و لیپ گلاس یا Lip Oil جلوهای براق و حجیمتر میدهد. برای انتخاب رنگ، تهرنگ پوست و استفادهٔ روزانه یا مهمانی را در نظر بگیرید و پیش از رژ لب، خط لب همخانواده را برای فرمدهی دقیقتر استفاده کنید.",
|
||
},
|
||
"eyes-make-up": {
|
||
h1: "خرید محصولات آرایش چشم کیکو میلانو",
|
||
title: "خرید محصولات آرایش چشم کیکو میلانو اصل | KIKO Milano",
|
||
guideTitle: "راهنمای خرید محصولات آرایش چشم کیکو میلانو",
|
||
intro: "از ریمل و خط چشم تا سایه، مداد چشم و محصولات ابرو، آرایش چشم کیکو میلانو برای ساختن ظاهرهای طبیعی روزانه تا چشمگیر شبانه طراحی شده است.",
|
||
types: [
|
||
{ label: "ریمل کیکو", query: "mascara" },
|
||
{ label: "خط چشم کیکو", query: "eyeliner" },
|
||
{ label: "سایه چشم کیکو", query: "eyeshadow" },
|
||
{ label: "مداد چشم کیکو", query: "eye pencil" },
|
||
{ label: "محصولات ابرو", query: "eyebrow" },
|
||
],
|
||
guide: "برای آرایش طبیعی، رنگهای نود و satin را انتخاب کنید؛ finish مات برای عمق دادن به پلک مناسب است و سایههای shimmer یا metallic نور بیشتری به چشم میدهند. خط چشم مدادی برای محو کردن آسانتر است و خط چشم مایع یا ماژیکی برای خطوط دقیق و ماندگار انتخاب بهتری است.",
|
||
},
|
||
} as const;
|
||
return key ? pages[key as keyof typeof pages] : undefined;
|
||
}
|
||
|
||
try {
|
||
main();
|
||
} catch (err) {
|
||
console.error(err);
|
||
process.exit(1);
|
||
}
|