/** * 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 `${esc(title)}

ادامه به فروشگاه

`; } function productFactsSection(product: SiteProduct) { const facts = productFacts(product); return `

اطلاعات رسمی محصول

مشخصات و نحوه استفاده

${facts.map((fact) => `
${esc(fact.label)}
${esc(fact.value)}
`).join("")}
`; } function productDescriptionHtml(description: string) { const [intro, ...rest] = description.split(/\n\s*\n/); return `

${esc(intro)}${rest.length ? `\n\n${esc(rest.join("\n\n"))}` : ""}

`; } 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[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 = `

خرید محصولات اصل KIKO Milano در ایران

فروشگاه تخصصی محصولات کیکو میلانو، واردات مستقیم از ایتالیا با تضمین اصالت و دسترسی به جدیدترین کالکشن‌ها و شیدها.

${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/", })}

${esc(dict.interested)}

${productListingFilters({ megaMenu, productCount: homeCollection.length, currentSort: "newest", mode: "home", hash: "collection", clientMode: true, })}

${esc(dict.featured)}

جدیدترین محصولات

${homeCollection.map((p) => productGridCard(p, { trailingSlash: true })).join("")}
${trustSection()}
`; write( "index.html", pageLayout({ title: seoHomeTitle(), description: seoHomeDescription(), dict, megaMenu, canonicalPath: "/", ogImage: "/videos/kiko-hero-poster.jpg", body: homeBody, headExtra: `${jsonLdGraph([organizationJsonLd(), websiteJsonLd(), faqJsonLd()])} `, }) ); 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: `

${esc(dict.collection)}

${esc(dict.seeAllProducts)}

${productListingFilters({ megaMenu, productCount: listingProducts.length, mode: "collection", clientMode: true, })}
${listingProducts .map((p) => listProductCard(p, { trailingSlash: true })) .join("")}
`, }) ); 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: `

${esc(dict.offers70)}

${esc(dict.withDiscount)}

محصولات با حداقل ۳۰٪ تخفیف — شامل ۳۰٪، ۵۰٪ و ۷۰٪ و بیشتر

${ offersListing.length === 0 ? `

${esc(dict.noProducts)}

` : `
${productListingFilters({ megaMenu, productCount: offersListing.length, mode: "offers", clientMode: true, })}
${offersListing .map((p) => offerProductCard(p, { trailingSlash: true })) .join("")}
` }
`, }) ); 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(); 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`, `${esc(displayNameWithoutShade(product.name, true))} | Kikomiko

مشاهده محصول

` ); 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(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 ? `
${pdpGallerySection(galleryImages, pdpDiscount)}
` : "" }${ product.description ? `

معرفی و توضیحات محصول

${productDescriptionHtml(product.description)}
` : "" }${factsHtml}` : `
${pdpGallerySection(galleryImages, pdpDiscount)}${ product.description ? `

معرفی و توضیحات محصول

${productDescriptionHtml(product.description)}` : "" }
${factsHtml}`; const hasVariants = (product.colorVariants?.length || 0) > 1; const pdpTitle = withPersianCategory( displayNameWithoutShade(product.name, hasVariants), product.category ); const heroHtml = hero?.image ? `
${esc(hero.imageAlt || hero.headline || pdpTitle)}

${esc(hero.headline ? withPersianCategory(hero.headline, product.category) : pdpTitle)}

${hero.subheadline ? `

${esc(hero.subheadline)}

` : ""} ${ hero.ctaText && hero.ctaLink ? `

${esc(hero.ctaText)}

` : "" }
` : `
`; const sticky = `

${esc(pdpTitle)}

${esc(formatPrice(product.price))}

`; const relatedProducts = rankRelatedProducts( product, listingSource.filter((candidate) => Boolean(candidate.heroImage)), 4 ); const relatedHtml = !relatedProducts.length ? "" : `

ادامه کاوش

${esc(dict.youMayAlsoLike)}

`; const body = ` ${heroHtml}
${buyPanel({ product, dict, colorVariants: product.colorVariants, trailingSlash: true, })}
${blocksHtml}

${esc(dict.reviews)}

نظرات مشتریان

هنوز نظری ثبت نشده است.

${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: `

${esc(dict.searchResults)}

${esc(dict.searchResults)}

${productListingFilters({ megaMenu, productCount: 0, mode: "search", clientMode: true, })}
`, }) ); 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: `

${esc(dict.cart)}

${esc(dict.cart)}

`, }) ); writeLegacyFaRedirect("cart/index.html", "/cart/", `${dict.cart} | Kikomiko`); write( "checkout/index.html", pageLayout({ title: `تسویه حساب | Kikomiko`, dict, megaMenu, canonicalPath: "/checkout/", noindex: true, nofollow: true, body: `

تسویه حساب

اطلاعات سفارش

با ثبت سفارش، فاکتور PDF ساخته می‌شود و واتساپ فروشگاه با لینک فاکتور باز می‌گردد — فقط ارسال را بزنید.

`, }) ); 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: `

صفحه پیدا نشد

${esc(dict.backHome)}

`, }) ); write("robots.txt", ROBOTS_TXT); write("sitemap.xml", buildSitemapXml(sitemapPaths, catalogUpdatedAt)); console.log(`Done. ${slice.length} product pages in ${DIST}`); } function categoryBody( dict: ReturnType, 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 ? `

راهنمای خرید

${esc(landing.guideTitle)}

${esc(landing.intro)}

دسته‌بندی محصولات

محصولات پرفروش

راهنمای انتخاب رنگ و فینیش

${esc(landing.guide)}

${landingKey === "lips-make-up" ? `

${esc(seoCategoryIntro(introLabel))}

همهٔ دسته‌ها

` : ""}
` : ""; return `
${landingKey === "lips-make-up" ? "" : `

${esc(dict.collection)}

`} ${landingKey === "lips-make-up" ? "" : `

${esc(pageTitle)}

`} ${landingKey === "lips-make-up" ? "" : `

${esc(seoCategoryIntro(introLabel))}

`} ${ sub && landingKey !== "lips-make-up" ? `

همهٔ دسته‌ها

` : "" }
${landingKey === "lips-make-up" ? landingHtml : ""}
${productListingFilters({ megaMenu, productCount: products.length, categorySlug: rootSlug, currentSub: sub, mode: "category", clientMode: true, })}
${ products.length === 0 ? `

${esc(dict.noProducts)}

` : `
${products .map((p) => listProductCard(p, { trailingSlash: true })) .join("")}
` } ${landingKey === "lips-make-up" ? "" : landingHtml}
`; } 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, guide: ProductTypeGuide, products: SiteProduct[], megaMenu: MegaMenuRoot[] ) { const featured = products.slice(0, 4); return `

${esc(guide.h1)}

${esc(guide.intro)}

${productListingFilters({ megaMenu, productCount: products.length, categorySlug: guide.rootSlug, mode: "category", clientMode: true })}
${products.map((product) => listProductCard(product, { trailingSlash: true })).join("")}

راهنمای خرید

راهنمای خرید ${esc(guide.typeFa)} کیکو

${esc(guide.intro)}

فینیش مناسب شما کدام است؟

${guide.comparison.map((item) => `

${esc(item.title)}

${esc(item.text)}

`).join("")}

چطور انتخاب کنیم؟

${esc(guide.colour)}

سوالات متداول

${guide.faq.map((item) => `
${esc(item.q)}

${esc(item.a)}

`).join("")}

هنوز مطمئن نیستید؟

تمام ${esc(guide.typeFa)}‌های KIKO Milano را ببینید

مشاهده همه محصولات
`; } 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); }