feat: implement programmatic SEO solutions hub

- Add /solutions hub page with 10 category cards
- Add /solutions/by-concern directory page
- Add /solutions/by-oil directory page
- Add Solutions section to Footer with navigation links
- Add Breadcrumb component for solution pages
- Add translations for all solution pages (sr, en, de, fr)
- Fix ExitIntentDetector JSON parsing error
- Update sitemap with solution pages
- Create 3 sample solution pages with data files
This commit is contained in:
Unchained
2026-04-05 05:21:57 +02:00
parent 6caefb420a
commit f6609f07d7
22 changed files with 3263 additions and 8 deletions

View File

@@ -0,0 +1,106 @@
import {
getOilForConcernPage,
getAllSolutionSlugs,
getLocalizedString,
getLocalizedKeywords
} from "@/lib/programmatic-seo/dataLoader";
import { getProducts } from "@/lib/saleor";
import { OilForConcernPageTemplate } from "@/components/programmatic-seo/OilForConcernPage";
import { FAQSchema } from "@/components/programmatic-seo/FAQSchema";
import { isValidLocale, DEFAULT_LOCALE, type Locale } from "@/lib/i18n/locales";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
interface PageProps {
params: Promise<{ locale: string; slug: string }>;
}
export async function generateStaticParams() {
return await getAllSolutionSlugs();
}
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, slug } = await params;
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
const page = await getOilForConcernPage(slug);
if (!page) {
return {
title: "Page Not Found",
};
}
const metaTitle = getLocalizedString(page.metaTitle, validLocale);
const metaDescription = getLocalizedString(page.metaDescription, validLocale);
const keywords = getLocalizedKeywords(page.seoKeywords, validLocale);
const localePrefix = validLocale === DEFAULT_LOCALE ? "" : `/${validLocale}`;
const canonicalUrl = `${baseUrl}${localePrefix}/solutions/${page.slug}`;
return {
title: metaTitle,
description: metaDescription,
keywords: keywords.join(", "),
alternates: {
canonical: canonicalUrl,
languages: {
"sr": `${baseUrl}/solutions/${page.slug}`,
"en": `${baseUrl}/en/solutions/${page.slug}`,
"de": `${baseUrl}/de/solutions/${page.slug}`,
"fr": `${baseUrl}/fr/solutions/${page.slug}`,
},
},
openGraph: {
title: metaTitle,
description: metaDescription,
type: "article",
url: canonicalUrl,
images: [{
url: `${baseUrl}/og-image.jpg`,
width: 1200,
height: 630,
alt: metaTitle,
}],
locale: validLocale,
},
twitter: {
card: "summary_large_image",
title: metaTitle,
description: metaDescription,
images: [`${baseUrl}/og-image.jpg`],
},
};
}
export default async function SolutionPage({ params }: PageProps) {
const { locale, slug } = await params;
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
const [page, products] = await Promise.all([
getOilForConcernPage(slug),
getProducts(validLocale === "sr" ? "SR" : "EN", 4)
]);
if (!page) {
notFound();
}
const basePath = validLocale === DEFAULT_LOCALE ? "" : `/${validLocale}`;
const faqQuestions = page.faqs.map((faq) => ({
question: getLocalizedString(faq.question, validLocale),
answer: getLocalizedString(faq.answer, validLocale),
}));
return (
<>
<FAQSchema questions={faqQuestions} />
<OilForConcernPageTemplate
page={page}
locale={validLocale as Locale}
basePath={basePath}
products={products}
/>
</>
);
}

View File

@@ -0,0 +1,154 @@
import { Metadata } from "next";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { ChevronRight, Search } from "lucide-react";
import { getAllOilForConcernPages, getLocalizedString } from "@/lib/programmatic-seo/dataLoader";
type Params = Promise<{ locale: string }>;
export async function generateMetadata({
params,
}: {
params: Params;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Solutions.ByConcern" });
return {
title: t("metaTitle"),
description: t("metaDescription"),
};
}
function groupByConcern(pages: Awaited<ReturnType<typeof getAllOilForConcernPages>>) {
const concerns = new Map<string, typeof pages>();
pages.forEach((page) => {
const concernSlug = page.concernSlug;
if (!concerns.has(concernSlug)) {
concerns.set(concernSlug, []);
}
concerns.get(concernSlug)?.push(page);
});
return concerns;
}
interface ConcernCardProps {
concernSlug: string;
concernName: string;
oilCount: number;
topOils: string[];
locale: string;
}
function ConcernCard({ concernSlug, concernName, oilCount, topOils, locale }: ConcernCardProps) {
return (
<div className="border border-[#e5e5e5] rounded-lg p-6 hover:border-black transition-colors group">
<h3 className="text-lg font-medium text-[#1a1a1a] mb-2">
{concernName}
</h3>
<p className="text-sm text-[#666666] mb-4">
{oilCount} {oilCount === 1 ? "oil solution" : "oil solutions"} available
</p>
<div className="space-y-2 mb-4">
{topOils.slice(0, 3).map((oilName) => (
<div key={oilName} className="flex items-center gap-2 text-sm text-[#666666]">
<div className="w-1.5 h-1.5 rounded-full bg-amber-400" />
{oilName}
</div>
))}
</div>
<Link
href={`/${locale}/solutions/by-concern/${concernSlug}`}
className="inline-flex items-center text-sm font-medium text-[#1a1a1a] group-hover:text-black transition-colors"
>
View All Solutions
<ChevronRight className="ml-1 w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
</Link>
</div>
);
}
export default async function ByConcernPage({
params,
}: {
params: Params;
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Solutions" });
const pageT = await getTranslations({ locale, namespace: "Solutions.ByConcern" });
const pages = await getAllOilForConcernPages();
const concernsMap = groupByConcern(pages);
const concernsList = Array.from(concernsMap.entries())
.map(([slug, pages]) => ({
slug,
name: getLocalizedString(pages[0].concernName, locale),
oilCount: pages.length,
topOils: pages.slice(0, 3).map((p) => getLocalizedString(p.oilName, locale)),
}))
.sort((a, b) => a.name.localeCompare(b.name));
return (
<div className="min-h-screen bg-white">
<section className="pt-32 pb-16 lg:pt-40 lg:pb-24">
<div className="container">
<nav className="flex items-center gap-2 text-sm text-[#666666] mb-8">
<Link href={`/${locale}`} className="hover:text-black transition-colors">
{t("breadcrumb.home")}
</Link>
<ChevronRight className="w-4 h-4" />
<Link href={`/${locale}/solutions`} className="hover:text-black transition-colors">
{t("breadcrumb.solutions")}
</Link>
<ChevronRight className="w-4 h-4" />
<span className="text-[#1a1a1a]">{t("breadcrumb.byConcern")}</span>
</nav>
<div className="max-w-3xl mb-12">
<h1 className="text-4xl lg:text-5xl font-medium tracking-tight text-[#1a1a1a] mb-6">
{pageT("title")}
</h1>
<p className="text-lg text-[#666666] leading-relaxed">
{pageT("subtitle")}
</p>
</div>
<div className="bg-[#fafafa] border border-[#e5e5e5] rounded-lg p-6 mb-12">
<div className="flex items-center gap-3 text-[#666666]">
<Search className="w-5 h-5" />
<span className="text-sm">
{pageT("stats.availableConcerns", { count: concernsList.length })}
</span>
<span className="text-[#e5e5e5]">|</span>
<span className="text-sm">
{pageT("stats.totalSolutions", { count: pages.length })}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{concernsList.map((concern) => (
<ConcernCard
key={concern.slug}
concernSlug={concern.slug}
concernName={concern.name}
oilCount={concern.oilCount}
topOils={concern.topOils}
locale={locale}
/>
))}
</div>
{concernsList.length === 0 && (
<div className="text-center py-16">
<p className="text-[#666666]">{pageT("noResults")}</p>
</div>
)}
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,165 @@
import { Metadata } from "next";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { ChevronRight, Droplets } from "lucide-react";
import { getAllOilForConcernPages, getLocalizedString } from "@/lib/programmatic-seo/dataLoader";
type Params = Promise<{ locale: string }>;
export async function generateMetadata({
params,
}: {
params: Params;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Solutions.ByOil" });
return {
title: t("metaTitle"),
description: t("metaDescription"),
};
}
function groupByOil(pages: Awaited<ReturnType<typeof getAllOilForConcernPages>>) {
const oils = new Map<string, typeof pages>();
pages.forEach((page) => {
const oilSlug = page.oilSlug;
if (!oils.has(oilSlug)) {
oils.set(oilSlug, []);
}
oils.get(oilSlug)?.push(page);
});
return oils;
}
interface OilCardProps {
oilSlug: string;
oilName: string;
concernCount: number;
topConcerns: string[];
locale: string;
}
function OilCard({ oilSlug, oilName, concernCount, topConcerns, locale }: OilCardProps) {
return (
<div className="border border-[#e5e5e5] rounded-lg p-6 hover:border-black transition-colors group">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
<Droplets className="w-5 h-5 text-amber-700" />
</div>
<h3 className="text-lg font-medium text-[#1a1a1a]">
{oilName}
</h3>
</div>
<p className="text-sm text-[#666666] mb-4">
{concernCount} {concernCount === 1 ? "concern solution" : "concern solutions"} available
</p>
<div className="space-y-2 mb-4">
<p className="text-xs uppercase tracking-wider text-[#999999] font-medium">
Best for:
</p>
{topConcerns.slice(0, 3).map((concernName) => (
<div key={concernName} className="flex items-center gap-2 text-sm text-[#666666]">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
{concernName}
</div>
))}
</div>
<Link
href={`/${locale}/solutions/by-oil/${oilSlug}`}
className="inline-flex items-center text-sm font-medium text-[#1a1a1a] group-hover:text-black transition-colors"
>
Explore Oil Solutions
<ChevronRight className="ml-1 w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
</Link>
</div>
);
}
export default async function ByOilPage({
params,
}: {
params: Params;
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Solutions" });
const pageT = await getTranslations({ locale, namespace: "Solutions.ByOil" });
const pages = await getAllOilForConcernPages();
const oilsMap = groupByOil(pages);
const oilsList = Array.from(oilsMap.entries())
.map(([slug, pages]) => ({
slug,
name: getLocalizedString(pages[0].oilName, locale),
concernCount: pages.length,
topConcerns: pages.slice(0, 3).map((p) => getLocalizedString(p.concernName, locale)),
}))
.sort((a, b) => a.name.localeCompare(b.name));
return (
<div className="min-h-screen bg-white">
<section className="pt-32 pb-16 lg:pt-40 lg:pb-24">
<div className="container">
<nav className="flex items-center gap-2 text-sm text-[#666666] mb-8">
<Link href={`/${locale}`} className="hover:text-black transition-colors">
{t("breadcrumb.home")}
</Link>
<ChevronRight className="w-4 h-4" />
<Link href={`/${locale}/solutions`} className="hover:text-black transition-colors">
{t("breadcrumb.solutions")}
</Link>
<ChevronRight className="w-4 h-4" />
<span className="text-[#1a1a1a]">{t("breadcrumb.byOil")}</span>
</nav>
<div className="max-w-3xl mb-12">
<h1 className="text-4xl lg:text-5xl font-medium tracking-tight text-[#1a1a1a] mb-6">
{pageT("title")}
</h1>
<p className="text-lg text-[#666666] leading-relaxed">
{pageT("subtitle")}
</p>
</div>
<div className="bg-gradient-to-r from-amber-50 to-emerald-50 border border-[#e5e5e5] rounded-lg p-6 mb-12">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-white flex items-center justify-center shadow-sm">
<Droplets className="w-6 h-6 text-amber-600" />
</div>
<div>
<p className="text-sm text-[#666666]">
{pageT("stats.availableOils", { count: oilsList.length })}
</p>
<p className="text-sm text-[#666666]">
{pageT("stats.totalSolutions", { count: pages.length })}
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{oilsList.map((oil) => (
<OilCard
key={oil.slug}
oilSlug={oil.slug}
oilName={oil.name}
concernCount={oil.concernCount}
topConcerns={oil.topConcerns}
locale={locale}
/>
))}
</div>
{oilsList.length === 0 && (
<div className="text-center py-16">
<p className="text-[#666666]">{pageT("noResults")}</p>
</div>
)}
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,291 @@
import { Metadata } from "next";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { ChevronRight, Sparkles, Heart, Leaf, Sun, Moon, Clock, Globe, Users, Droplets, ArrowRight } from "lucide-react";
type Params = Promise<{ locale: string }>;
export async function generateMetadata({
params,
}: {
params: Params;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Solutions.Hub" });
return {
title: t("metaTitle"),
description: t("metaDescription"),
};
}
interface CategoryCardProps {
title: string;
description: string;
href: string;
icon: React.ReactNode;
priority?: boolean;
}
function CategoryCard({ title, description, href, icon, priority }: CategoryCardProps) {
return (
<Link
href={href}
className={`group block p-6 lg:p-8 border border-[#e5e5e5] rounded-lg hover:border-black transition-all duration-300 hover:shadow-lg ${
priority ? "bg-gradient-to-br from-amber-50/50 to-white" : "bg-white"
}`}
>
<div className="flex items-start gap-4">
<div className={`flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center ${
priority ? "bg-amber-100 text-amber-700" : "bg-[#f5f5f5] text-[#666666] group-hover:bg-black group-hover:text-white"
} transition-colors`}>
{icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h3 className="text-lg font-medium text-[#1a1a1a] group-hover:text-black transition-colors">
{title}
</h3>
{priority && (
<span className="px-2 py-0.5 text-[10px] uppercase tracking-wider font-medium bg-amber-100 text-amber-700 rounded-full">
Popular
</span>
)}
</div>
<p className="text-sm text-[#666666] leading-relaxed mb-4">
{description}
</p>
<span className="inline-flex items-center text-sm font-medium text-[#1a1a1a] group-hover:text-black transition-colors">
{priority ? "Explore Solutions" : "Learn More"}
<ArrowRight className="ml-1 w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
</span>
</div>
</div>
</Link>
);
}
interface QuickLinkProps {
title: string;
href: string;
count?: number;
}
function QuickLink({ title, href, count }: QuickLinkProps) {
return (
<Link
href={href}
className="flex items-center justify-between p-4 border-b border-[#e5e5e5] hover:bg-[#fafafa] transition-colors group"
>
<span className="text-[#1a1a1a] group-hover:text-black transition-colors">
{title}
</span>
<div className="flex items-center gap-2">
{count !== undefined && (
<span className="text-xs text-[#999999]">{count} solutions</span>
)}
<ChevronRight className="w-4 h-4 text-[#999999] group-hover:text-black transition-colors" />
</div>
</Link>
);
}
export default async function SolutionsHubPage({
params,
}: {
params: Params;
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Solutions" });
const hubT = await getTranslations({ locale, namespace: "Solutions.Hub" });
const categories = [
{
title: hubT("categories.oilForConcern.title"),
description: hubT("categories.oilForConcern.description"),
href: `/${locale}/solutions/by-concern`,
icon: <Droplets className="w-5 h-5" />,
priority: true,
},
{
title: hubT("categories.ageSkinRoutine.title"),
description: hubT("categories.ageSkinRoutine.description"),
href: `/${locale}/solutions/age-skin-routine`,
icon: <Clock className="w-5 h-5" />,
},
{
title: hubT("categories.ingredientPairings.title"),
description: hubT("categories.ingredientPairings.description"),
href: `/${locale}/solutions/ingredient-pairings`,
icon: <Sparkles className="w-5 h-5" />,
},
{
title: hubT("categories.bodyPartConcerns.title"),
description: hubT("categories.bodyPartConcerns.description"),
href: `/${locale}/solutions/body-part-concerns`,
icon: <Heart className="w-5 h-5" />,
},
{
title: hubT("categories.oilComparisons.title"),
description: hubT("categories.oilComparisons.description"),
href: `/${locale}/solutions/oil-comparisons`,
icon: <Users className="w-5 h-5" />,
},
{
title: hubT("categories.routineStepSkinType.title"),
description: hubT("categories.routineStepSkinType.description"),
href: `/${locale}/solutions/routine-step-skin-type`,
icon: <Leaf className="w-5 h-5" />,
},
{
title: hubT("categories.seasonalSkincare.title"),
description: hubT("categories.seasonalSkincare.description"),
href: `/${locale}/solutions/seasonal-skincare`,
icon: <Sun className="w-5 h-5" />,
},
{
title: hubT("categories.timeOfDayConcerns.title"),
description: hubT("categories.timeOfDayConcerns.description"),
href: `/${locale}/solutions/time-of-day-concerns`,
icon: <Moon className="w-5 h-5" />,
},
{
title: hubT("categories.naturalAlternatives.title"),
description: hubT("categories.naturalAlternatives.description"),
href: `/${locale}/solutions/natural-alternatives`,
icon: <Leaf className="w-5 h-5" />,
},
{
title: hubT("categories.culturalBeautySecrets.title"),
description: hubT("categories.culturalBeautySecrets.description"),
href: `/${locale}/solutions/cultural-beauty-secrets`,
icon: <Globe className="w-5 h-5" />,
},
];
return (
<div className="min-h-screen bg-white">
<section className="pt-32 pb-16 lg:pt-40 lg:pb-24">
<div className="container">
<nav className="flex items-center gap-2 text-sm text-[#666666] mb-8">
<Link href={`/${locale}`} className="hover:text-black transition-colors">
{t("breadcrumb.home")}
</Link>
<ChevronRight className="w-4 h-4" />
<span className="text-[#1a1a1a]">{t("breadcrumb.solutions")}</span>
</nav>
<div className="max-w-3xl">
<h1 className="text-4xl lg:text-5xl font-medium tracking-tight text-[#1a1a1a] mb-6">
{hubT("title")}
</h1>
<p className="text-lg text-[#666666] leading-relaxed">
{hubT("subtitle")}
</p>
</div>
</div>
</section>
<section className="pb-16 lg:pb-24">
<div className="container">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 lg:gap-6">
{categories.map((category) => (
<CategoryCard key={category.href} {...category} />
))}
</div>
</div>
</section>
<section className="pb-16 lg:pb-24">
<div className="container">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
<div className="border border-[#e5e5e5] rounded-lg overflow-hidden">
<div className="p-6 bg-[#fafafa] border-b border-[#e5e5e5]">
<h2 className="text-lg font-medium text-[#1a1a1a]">
{hubT("quickAccess.byConcern")}
</h2>
<p className="text-sm text-[#666666] mt-1">
{hubT("quickAccess.byConcernDesc")}
</p>
</div>
<div className="divide-y divide-[#e5e5e5]">
<QuickLink
title={hubT("quickAccess.links.wrinkles")}
href={`/${locale}/solutions/by-concern/wrinkles`}
/>
<QuickLink
title={hubT("quickAccess.links.acne")}
href={`/${locale}/solutions/by-concern/acne`}
/>
<QuickLink
title={hubT("quickAccess.links.drySkin")}
href={`/${locale}/solutions/by-concern/dry-skin`}
/>
<QuickLink
title={hubT("quickAccess.links.darkSpots")}
href={`/${locale}/solutions/by-concern/dark-spots`}
/>
<QuickLink
title={hubT("quickAccess.links.viewAll")}
href={`/${locale}/solutions/by-concern`}
/>
</div>
</div>
<div className="border border-[#e5e5e5] rounded-lg overflow-hidden">
<div className="p-6 bg-[#fafafa] border-b border-[#e5e5e5]">
<h2 className="text-lg font-medium text-[#1a1a1a]">
{hubT("quickAccess.byOil")}
</h2>
<p className="text-sm text-[#666666] mt-1">
{hubT("quickAccess.byOilDesc")}
</p>
</div>
<div className="divide-y divide-[#e5e5e5]">
<QuickLink
title={hubT("quickAccess.links.rosehipOil")}
href={`/${locale}/solutions/by-oil/rosehip-oil`}
/>
<QuickLink
title={hubT("quickAccess.links.arganOil")}
href={`/${locale}/solutions/by-oil/argan-oil`}
/>
<QuickLink
title={hubT("quickAccess.links.jojobaOil")}
href={`/${locale}/solutions/by-oil/jojoba-oil`}
/>
<QuickLink
title={hubT("quickAccess.links.seaBuckthornOil")}
href={`/${locale}/solutions/by-oil/sea-buckthorn-oil`}
/>
<QuickLink
title={hubT("quickAccess.links.viewAll")}
href={`/${locale}/solutions/by-oil`}
/>
</div>
</div>
</div>
</div>
</section>
<section className="pb-16 lg:pb-24">
<div className="container">
<div className="bg-[#1a1a1a] rounded-2xl p-8 lg:p-12 text-center">
<h2 className="text-2xl lg:text-3xl font-medium text-white mb-4">
{hubT("cta.title")}
</h2>
<p className="text-[#999999] max-w-xl mx-auto mb-8">
{hubT("cta.description")}
</p>
<Link
href={`/${locale}/products`}
className="inline-flex items-center justify-center px-8 py-3 bg-white text-[#1a1a1a] font-medium rounded-full hover:bg-[#f5f5f5] transition-colors"
>
{hubT("cta.button")}
</Link>
</div>
</div>
</section>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import { MetadataRoute } from "next";
import { getProducts, filterOutBundles } from "@/lib/saleor";
import { getAllOilForConcernPages } from "@/lib/programmatic-seo/dataLoader";
import { SUPPORTED_LOCALES, type Locale } from "@/lib/i18n/locales";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
@@ -105,5 +106,35 @@ export default async function sitemap(): Promise<SitemapEntry[]> {
}
}
return [...staticPages, ...productUrls];
let solutionPages: any[] = [];
try {
solutionPages = await getAllOilForConcernPages();
} catch (e) {
console.log("Failed to fetch solution pages for sitemap during build");
}
const solutionUrls: SitemapEntry[] = [];
for (const page of solutionPages) {
const hreflangs: Record<string, string> = {};
for (const locale of SUPPORTED_LOCALES) {
const path = locale === "sr" ? `/solutions/${page.slug}` : `/${locale}/solutions/${page.slug}`;
hreflangs[locale] = `${baseUrl}${path}`;
}
for (const locale of SUPPORTED_LOCALES) {
const localePrefix = locale === "sr" ? "" : `/${locale}`;
solutionUrls.push({
url: `${baseUrl}${localePrefix}/solutions/${page.slug}`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
alternates: {
languages: hreflangs,
},
});
}
}
return [...staticPages, ...productUrls, ...solutionUrls];
}

View File

@@ -32,11 +32,17 @@ export default function ExitIntentDetector() {
try {
const response = await fetch("/api/geoip");
if (response.ok) {
const data = await response.json();
setCountry(data.country);
setCountryCode(data.countryCode);
setCity(data.city || "");
setRegion(data.region || "");
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const text = await response.text();
if (text && text.trim()) {
const data = JSON.parse(text);
setCountry(data.country);
setCountryCode(data.countryCode);
setCity(data.city || "");
setRegion(data.region || "");
}
}
}
} catch (error) {
console.error("Failed to get country:", error);

View File

@@ -21,6 +21,12 @@ export default function Footer({ locale = "sr" }: FooterProps) {
{ label: t("skinCare"), href: `${localePath}/products` },
{ label: t("giftSets"), href: `${localePath}/products` },
],
solutions: [
{ label: t("allSolutions"), href: `${localePath}/solutions` },
{ label: t("byConcern"), href: `${localePath}/solutions/by-concern` },
{ label: t("byOil"), href: `${localePath}/solutions/by-oil` },
{ label: t("skincareGuide"), href: `${localePath}/solutions` },
],
about: [
{ label: t("ourStory"), href: `${localePath}/about` },
{ label: t("process"), href: `${localePath}/about` },
@@ -74,7 +80,7 @@ export default function Footer({ locale = "sr" }: FooterProps) {
</div>
<div className="lg:col-span-8">
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
{t("shop")}
@@ -93,6 +99,24 @@ export default function Footer({ locale = "sr" }: FooterProps) {
</ul>
</div>
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
{t("solutions")}
</h4>
<ul className="space-y-3">
{footerLinks.solutions.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-sm text-[#666666] hover:text-black transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
{t("about")}

View File

@@ -0,0 +1,16 @@
import { generateFAQPageSchema } from "@/lib/programmatic-seo/schema";
interface FAQSchemaProps {
questions: Array<{ question: string; answer: string }>;
}
export function FAQSchema({ questions }: FAQSchemaProps) {
const schema = generateFAQPageSchema(questions);
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}

View File

@@ -0,0 +1,157 @@
import type { Locale } from "@/lib/i18n/locales";
import type { OilForConcernPage } from "@/lib/programmatic-seo/types";
import type { Product } from "@/types/saleor";
import { getLocalizedString, getLocalizedArray } from "@/lib/programmatic-seo/dataLoader";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import ProductReviews from "@/components/product/ProductReviews";
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
import ProductsGrid from "./ProductsGrid";
import Link from "next/link";
import { ArrowRight, Check, Clock, Droplets } from "lucide-react";
interface OilForConcernPageProps {
page: OilForConcernPage;
locale: Locale;
basePath: string;
products: Product[];
}
export function OilForConcernPageTemplate({ page, locale, basePath, products }: OilForConcernPageProps) {
const pageTitle = getLocalizedString(page.pageTitle, locale);
const oilName = getLocalizedString(page.oilName, locale);
const concernName = getLocalizedString(page.concernName, locale);
const whyThisWorks = getLocalizedString(page.whyThisWorks, locale);
const keyBenefits = getLocalizedArray(page.keyBenefits, locale);
const howToApply = getLocalizedArray(page.howToApply, locale);
const expectedResults = getLocalizedString(page.expectedResults, locale);
const timeframe = getLocalizedString(page.timeframe, locale);
const productsHref = locale === "sr" ? "/products" : `/${locale}/products`;
return (
<>
<Header locale={locale} />
<main className="min-h-screen bg-white">
<section className="bg-[#FAF9F7] pt-[180px] lg:pt-[200px] pb-16">
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto text-center">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-[#E8DFD0] rounded-full mb-6">
<Droplets className="w-4 h-4 text-[#8B7355]" />
<span className="text-sm text-[#5C4D3C] font-medium">
{locale === "sr" ? "Prirodno rešenje" :
locale === "de" ? "Natürliche Lösung" :
locale === "fr" ? "Solution naturelle" : "Natural Solution"}
</span>
</div>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-light text-[#1A1A1A] mb-6 leading-tight">
{pageTitle}
</h1>
<p className="text-lg text-[#666666] max-w-2xl mx-auto mb-8">
{whyThisWorks.substring(0, 150)}...
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href={productsHref}
className="inline-flex items-center justify-center gap-2 px-8 py-4 bg-[#1A1A1A] text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
>
{locale === "sr" ? "Kupi proizvode sa " :
locale === "de" ? "Produkte mit " :
locale === "fr" ? "Acheter des produits avec " : "Shop products with "}
{oilName}
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
</div>
</section>
<ProductReviews locale={locale} productName={oilName} />
<section className="py-16 lg:py-24">
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl md:text-4xl font-light text-[#1A1A1A] mb-6">
{locale === "sr" ? "Zašto " :
locale === "de" ? "Warum " :
locale === "fr" ? "Pourquoi " : "Why "}
{oilName}
{locale === "sr" ? " deluje protiv " :
locale === "de" ? " gegen " :
locale === "fr" ? " contre " : " works for "}
{concernName.toLowerCase()}
</h2>
<p className="text-[#666666] text-lg leading-relaxed mb-8">
{whyThisWorks}
</p>
<div className="flex items-center gap-3 p-4 bg-[#F8F7F5] rounded-lg">
<Clock className="w-5 h-5 text-[#8B7355]" />
<div>
<span className="text-sm font-medium text-[#1A1A1A]">
{locale === "sr" ? "Vreme rezultata: " :
locale === "de" ? "Zeit bis zum Ergebnis: " :
locale === "fr" ? "Délai des résultats: " : "Results timeframe: "}
</span>
<span className="text-sm text-[#666666]">{timeframe}</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{keyBenefits.slice(0, 4).map((benefit, index) => (
<div key={index} className="p-6 bg-[#FAF9F7] rounded-lg">
<Check className="w-6 h-6 text-[#8B7355] mb-3" />
<p className="text-[#1A1A1A] font-medium">{benefit}</p>
</div>
))}
</div>
</div>
</div>
</section>
<BeforeAfterGallery />
<section className="py-16 lg:py-24">
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl md:text-4xl font-light text-[#1A1A1A] text-center mb-12">
{locale === "sr" ? "Kako koristiti" :
locale === "de" ? "Anwendung" :
locale === "fr" ? "Comment utiliser" : "How to use"}
</h2>
<div className="max-w-3xl mx-auto">
<div className="space-y-6">
{howToApply.map((step, index) => (
<div key={index} className="flex gap-4 items-start">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-[#1A1A1A] text-white flex items-center justify-center font-medium">
{index + 1}
</div>
<div className="pt-2">
<p className="text-[#1A1A1A] text-lg">{step}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
<section className="py-16 lg:py-24 bg-[#FAF9F7]">
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-light text-[#1A1A1A] mb-6">
{locale === "sr" ? "Šta možete očekivati" :
locale === "de" ? "Was Sie erwarten können" :
locale === "fr" ? "Ce que vous pouvez attendre" : "What to expect"}
</h2>
<p className="text-lg text-[#666666] leading-relaxed">
{expectedResults}
</p>
</div>
</div>
</section>
<ProductsGrid products={products} locale={locale} />
</main>
<Footer locale={locale} />
</>
);
}

View File

@@ -0,0 +1,159 @@
"use client";
import { motion } from "framer-motion";
import Image from "next/image";
import { useState } from "react";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { useAnalytics } from "@/lib/analytics";
import type { Product } from "@/types/saleor";
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
import { isValidLocale, getSaleorLocale } from "@/lib/i18n/locales";
import { useTranslations } from "next-intl";
import Link from "next/link";
interface ProductsGridProps {
products: Product[];
locale: string;
}
function ProductCardWithAddToCart({ product, index, locale }: { product: Product; index: number; locale: string }) {
const t = useTranslations("ProductCard");
const tProduct = useTranslations("Product");
const [isAdding, setIsAdding] = useState(false);
const { addLine, openCart, setLanguageCode } = useSaleorCheckoutStore();
const { trackAddToCart } = useAnalytics();
const image = getProductImage(product);
const price = getProductPrice(product);
const saleorLocale = isValidLocale(locale) ? getSaleorLocale(locale) : "SR";
const localized = getLocalizedProduct(product, saleorLocale);
const variant = product.variants?.[0];
const isAvailable = (variant?.quantityAvailable || 0) > 0;
const productHref = locale === "sr" ? `/products/${localized.slug}` : `/${locale}/products/${localized.slug}`;
const handleAddToCart = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!variant?.id) return;
if (isValidLocale(locale)) {
setLanguageCode(locale);
}
setIsAdding(true);
try {
await addLine(variant.id, 1);
const priceAmount = variant?.pricing?.price?.gross?.amount || 0;
const currency = variant?.pricing?.price?.gross?.currency || "RSD";
trackAddToCart({
id: product.id,
name: localized.name,
price: priceAmount,
currency,
quantity: 1,
variant: variant.name,
});
openCart();
} finally {
setIsAdding(false);
}
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="group"
>
<Link href={productHref} className="block">
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
{image ? (
<Image
src={image}
alt={localized.name}
fill
className="object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
loading={index < 4 ? "eager" : "lazy"}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
<span className="text-sm">{t("noImage")}</span>
</div>
)}
{!isAvailable && (
<div className="absolute inset-0 bg-white/80 flex items-center justify-center">
<span className="text-sm uppercase tracking-[0.1em] text-[#666666]">
{t("outOfStock")}
</span>
</div>
)}
</div>
</Link>
<div className="text-center">
<Link href={productHref}>
<h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
{localized.name}
</h3>
</Link>
<p className="text-[14px] text-[#666666] mb-3">
{price || t("contactForPrice")}
</p>
{isAvailable ? (
<button
onClick={handleAddToCart}
disabled={isAdding}
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isAdding ? tProduct("adding") : tProduct("addToCart")}
</button>
) : (
<div className="w-full py-3 bg-[#f8f9fa] text-[#666666] text-xs uppercase tracking-[0.1em]">
{t("outOfStock")}
</div>
)}
</div>
</motion.div>
);
}
export default function ProductsGrid({ products, locale }: ProductsGridProps) {
const t = useTranslations("Solutions");
const validProducts = products.filter(p => p && p.id);
return (
<section className="py-16 lg:py-24 bg-[#1A1A1A]">
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-light text-white mb-4">
{t("completeYourRoutine")}
</h2>
<p className="text-[#999999] max-w-2xl mx-auto">
{t("discoverProducts")}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{validProducts.map((product, index) => (
<ProductCardWithAddToCart
key={product.id}
product={product}
index={index}
locale={locale}
/>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,57 @@
"use client";
import Link from "next/link";
import { ChevronRight, Home } from "lucide-react";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
locale: string;
showHome?: boolean;
}
export default function Breadcrumb({ items, locale, showHome = true }: BreadcrumbProps) {
const allItems = showHome
? [{ label: "Home", href: `/${locale}` }, ...items]
: items;
return (
<nav className="flex items-center gap-2 text-sm text-[#666666]" aria-label="Breadcrumb">
<ol className="flex items-center gap-2 flex-wrap">
{allItems.map((item, index) => {
const isLast = index === allItems.length - 1;
return (
<li key={index} className="flex items-center gap-2">
{index > 0 && <ChevronRight className="w-4 h-4 flex-shrink-0" />}
{isLast || !item.href ? (
<span className={isLast ? "text-[#1a1a1a]" : ""} aria-current={isLast ? "page" : undefined}>
{index === 0 && showHome ? (
<Home className="w-4 h-4" />
) : (
item.label
)}
</span>
) : (
<Link
href={item.href}
className="hover:text-black transition-colors"
>
{index === 0 && showHome ? (
<Home className="w-4 h-4" />
) : (
item.label
)}
</Link>
)}
</li>
);
})}
</ol>
</nav>
);
}

View File

@@ -180,6 +180,11 @@
"hairCare": "Haarpflege",
"skinCare": "Hautpflege",
"giftSets": "Geschenksets",
"solutions": "Lösungen",
"allSolutions": "Alle Lösungen",
"byConcern": "Nach Problem",
"byOil": "Nach Öl",
"skincareGuide": "Hautpflege-Guide",
"about": "Über uns",
"ourStory": "Unsere Geschichte",
"process": "Prozess",
@@ -310,6 +315,10 @@
"quickAdd": "Schnell hinzufügen",
"contactForPrice": "Preis anfragen"
},
"Product": {
"adding": "Wird hinzugefügt...",
"addToCart": "In den Warenkorb"
},
"ProductDetail": {
"home": "Startseite",
"outOfStock": "Nicht auf Lager",
@@ -439,5 +448,107 @@
"description": "Bezahlen Sie per Banküberweisung",
"comingSoon": "Demnächst verfügbar"
}
},
"Solutions": {
"breadcrumb": {
"home": "Startseite",
"solutions": "Lösungen",
"byConcern": "Nach Problem",
"byOil": "Nach Öl"
},
"Hub": {
"metaTitle": "Natürliche Hautpflege-Lösungen | ManoonOils",
"metaDescription": "Entdecken Sie natürliche Öl-Lösungen für jedes Hautproblem. Durchsuchen Sie nach Problem, Öltyp oder erkunden Sie unsere umfassenden Hautpflege-Guides.",
"title": "Natürliche Hautpflege-Lösungen",
"subtitle": "Entdecken Sie die perfekten natürlichen Öl-Lösungen für Ihre Hautprobleme. Unsere fachkundig erstellten Guides helfen Ihnen, die richtigen Öle für Falten, Akne, Trockenheit und mehr zu finden.",
"categories": {
"oilForConcern": {
"title": "Öl für Problem",
"description": "Finden Sie die besten natürlichen Öle für spezifische Hautprobleme wie Falten, Akne, dunkle Flecken und mehr."
},
"ageSkinRoutine": {
"title": "Alter-Haut Routine",
"description": "Personalisierte Hautpflege-Routinen basierend auf Ihrem Alter und Hauttyp für optimale Ergebnisse."
},
"ingredientPairings": {
"title": "Inhaltsstoff-Kombinationen",
"description": "Lernen Sie, welche natürlichen Inhaltsstoffe am besten zusammenwirken für verbesserte Hautpflege-Vorteile."
},
"bodyPartConcerns": {
"title": "Körperteil-Probleme",
"description": "Gezielte Lösungen für spezifische Körperbereiche wie Gesicht, Hals, Hände und mehr."
},
"oilComparisons": {
"title": "Öl-Vergleiche",
"description": "Vergleiche nebeneinander, um Ihnen bei der Wahl zwischen verschiedenen natürlichen Ölen zu helfen."
},
"routineStepSkinType": {
"title": "Routine nach Hauttyp",
"description": "Schritt-für-Schritt-Anleitung, zugeschnitten auf Ihren spezifischen Hauttyp und Ihre Probleme."
},
"seasonalSkincare": {
"title": "Saisonale Hautpflege",
"description": "Passen Sie Ihre Routine den Jahreszeiten an für gesunde Haut das ganze Jahr über."
},
"timeOfDayConcerns": {
"title": "Tageszeit",
"description": "Morgen- und Abend-Hautpflege-Routinen für maximale Wirksamkeit."
},
"naturalAlternatives": {
"title": "Natürliche Alternativen",
"description": "Entdecken Sie natürliche Alternativen zu synthetischen Hautpflege-Inhaltsstoffen."
},
"culturalBeautySecrets": {
"title": "Kulturelle Schönheitsgeheimnisse",
"description": "Uralte Schönheitsweisheit aus der ganzen Welt mit natürlichen Ölen."
}
},
"quickAccess": {
"byConcern": "Nach Problem durchsuchen",
"byConcernDesc": "Finden Sie Lösungen für Ihre spezifischen Hautprobleme",
"byOil": "Nach Öl durchsuchen",
"byOilDesc": "Erkunden Sie die Vorteile verschiedener natürlicher Öle",
"links": {
"wrinkles": "Falten & Aging",
"acne": "Akne & Unreinheiten",
"drySkin": "Trockene Haut",
"darkSpots": "Dunkle Flecken",
"viewAll": "Alle ansehen →",
"rosehipOil": "Hagebuttenöl",
"arganOil": "Arganöl",
"jojobaOil": "Jojobaöl",
"seaBuckthornOil": "Sanddornöl"
}
},
"cta": {
"title": "Bereit, Ihre Haut zu verwandeln?",
"description": "Durchstöbern Sie unsere Kollektion von Premium-Naturölen und beginnen Sie noch heute Ihre Reise zu gesünderer, strahlender Haut.",
"button": "Naturöle kaufen"
}
},
"ByConcern": {
"metaTitle": "Hautpflege-Lösungen nach Problem | ManoonOils",
"metaDescription": "Durchsuchen Sie natürliche Öl-Lösungen nach Hautproblem organisiert. Finden Sie das perfekte Heilmittel für Falten, Akne, Trockenheit und mehr.",
"title": "Lösungen nach Problem",
"subtitle": "Erkunden Sie unsere umfassende Kollektion natürlicher Öl-Lösungen, organisiert nach Hautproblem, um Ihnen zu helfen, genau das zu finden, was Sie brauchen.",
"stats": {
"availableConcerns": "{count} Hautprobleme abgedeckt",
"totalSolutions": "{count} fachkundig kuratierte Lösungen"
},
"noResults": "Keine Probleme gefunden. Bitte schauen Sie später für neue Lösungen vorbei."
},
"ByOil": {
"metaTitle": "Hautpflege-Lösungen nach Öl | ManoonOils",
"metaDescription": "Entdecken Sie die Vorteile verschiedener natürlicher Öle für verschiedene Hautprobleme. Finden Sie heraus, welches Öl das richtige für Sie ist.",
"title": "Lösungen nach Öl",
"subtitle": "Lernen Sie die einzigartigen Eigenschaften jedes natürlichen Öls kennen und entdecken Sie, welche am besten für Ihre Hautprobleme geeignet sind.",
"stats": {
"availableOils": "{count} natürliche Öle",
"totalSolutions": "{count} fachkundig kuratierte Lösungen"
},
"noResults": "Keine Öle gefunden. Bitte schauen Sie später für neue Lösungen vorbei."
},
"completeYourRoutine": "Vervollständigen Sie Ihre Routine",
"discoverProducts": "Entdecken Sie unsere Premium-Produkte mit natürlichen Inhaltsstoffen"
}
}

View File

@@ -319,6 +319,11 @@
"hairCare": "Hair Care",
"skinCare": "Skin Care",
"giftSets": "Gift Sets",
"solutions": "Solutions",
"allSolutions": "All Solutions",
"byConcern": "By Concern",
"byOil": "By Oil",
"skincareGuide": "Skincare Guide",
"about": "About",
"ourStory": "Our Story",
"process": "Process",
@@ -339,6 +344,10 @@
"quickAdd": "Quick Add",
"contactForPrice": "Contact for price"
},
"Product": {
"adding": "Adding...",
"addToCart": "Add to Cart"
},
"ProductDetail": {
"home": "Home",
"outOfStock": "Out of Stock",
@@ -494,5 +503,107 @@
"goHome": "Go Home",
"lookingFor": "Can't find what you're looking for?",
"searchSuggestion": "Try browsing our product collection or contact us for assistance."
},
"Solutions": {
"breadcrumb": {
"home": "Home",
"solutions": "Solutions",
"byConcern": "By Concern",
"byOil": "By Oil"
},
"Hub": {
"metaTitle": "Natural Skincare Solutions | ManoonOils",
"metaDescription": "Discover natural oil solutions for every skin concern. Browse by concern, oil type, or explore our comprehensive skincare guides.",
"title": "Natural Skincare Solutions",
"subtitle": "Discover the perfect natural oil solutions for your skin concerns. Our expertly crafted guides help you find the right oils for wrinkles, acne, dryness, and more.",
"categories": {
"oilForConcern": {
"title": "Oil for Concern",
"description": "Find the best natural oils for specific skin concerns like wrinkles, acne, dark spots, and more."
},
"ageSkinRoutine": {
"title": "Age-Skin Routine",
"description": "Personalized skincare routines based on your age and skin type for optimal results."
},
"ingredientPairings": {
"title": "Ingredient Pairings",
"description": "Learn which natural ingredients work best together for enhanced skincare benefits."
},
"bodyPartConcerns": {
"title": "Body Part Concerns",
"description": "Targeted solutions for specific body areas like face, neck, hands, and more."
},
"oilComparisons": {
"title": "Oil Comparisons",
"description": "Side-by-side comparisons to help you choose between different natural oils."
},
"routineStepSkinType": {
"title": "Routine Step by Skin Type",
"description": "Step-by-step guidance tailored to your specific skin type and concerns."
},
"seasonalSkincare": {
"title": "Seasonal Skincare",
"description": "Adjust your routine with the seasons for year-round healthy skin."
},
"timeOfDayConcerns": {
"title": "Time of Day Concerns",
"description": "Morning and evening skincare routines for maximum effectiveness."
},
"naturalAlternatives": {
"title": "Natural Alternatives",
"description": "Discover natural alternatives to synthetic skincare ingredients."
},
"culturalBeautySecrets": {
"title": "Cultural Beauty Secrets",
"description": "Ancient beauty wisdom from around the world using natural oils."
}
},
"quickAccess": {
"byConcern": "Browse by Concern",
"byConcernDesc": "Find solutions for your specific skin concerns",
"byOil": "Browse by Oil",
"byOilDesc": "Explore benefits of different natural oils",
"links": {
"wrinkles": "Wrinkles & Aging",
"acne": "Acne & Blemishes",
"drySkin": "Dry Skin",
"darkSpots": "Dark Spots",
"viewAll": "View All →",
"rosehipOil": "Rosehip Oil",
"arganOil": "Argan Oil",
"jojobaOil": "Jojoba Oil",
"seaBuckthornOil": "Sea Buckthorn Oil"
}
},
"cta": {
"title": "Ready to Transform Your Skin?",
"description": "Browse our collection of premium natural oils and start your journey to healthier, more radiant skin today.",
"button": "Shop Natural Oils"
}
},
"ByConcern": {
"metaTitle": "Skincare Solutions by Concern | ManoonOils",
"metaDescription": "Browse natural oil solutions organized by skin concern. Find the perfect remedy for wrinkles, acne, dryness, and more.",
"title": "Solutions by Concern",
"subtitle": "Explore our comprehensive collection of natural oil solutions, organized by skin concern to help you find exactly what you need.",
"stats": {
"availableConcerns": "{count} skin concerns covered",
"totalSolutions": "{count} expert-curated solutions"
},
"noResults": "No concerns found. Please check back later for new solutions."
},
"ByOil": {
"metaTitle": "Skincare Solutions by Oil | ManoonOils",
"metaDescription": "Discover the benefits of different natural oils for various skin concerns. Find which oil is right for you.",
"title": "Solutions by Oil",
"subtitle": "Learn about the unique properties of each natural oil and discover which ones are best suited for your skin concerns.",
"stats": {
"availableOils": "{count} natural oils",
"totalSolutions": "{count} expert-curated solutions"
},
"noResults": "No oils found. Please check back later for new solutions."
},
"completeYourRoutine": "Complete Your Routine",
"discoverProducts": "Discover our premium products with natural ingredients"
}
}

View File

@@ -180,6 +180,11 @@
"hairCare": "Soins Capillaires",
"skinCare": "Soins Cutanés",
"giftSets": "Coffrets Cadeaux",
"solutions": "Solutions",
"allSolutions": "Toutes les Solutions",
"byConcern": "Par Problème",
"byOil": "Par Huile",
"skincareGuide": "Guide Soins",
"about": "À Propos",
"ourStory": "Notre Histoire",
"process": "Processus",
@@ -310,6 +315,10 @@
"quickAdd": "Ajout Rapide",
"contactForPrice": "Contacter pour le prix"
},
"Product": {
"adding": "Ajout en cours...",
"addToCart": "Ajouter au Panier"
},
"ProductDetail": {
"home": "Accueil",
"outOfStock": "Rupture de Stock",
@@ -439,5 +448,107 @@
"description": "Payez par virement bancaire",
"comingSoon": "Bientôt disponible"
}
},
"Solutions": {
"breadcrumb": {
"home": "Accueil",
"solutions": "Solutions",
"byConcern": "Par Problème",
"byOil": "Par Huile"
},
"Hub": {
"metaTitle": "Solutions Naturelles pour la Peau | ManoonOils",
"metaDescription": "Découvrez les solutions à base d'huiles naturelles pour chaque problème de peau. Parcourez par problème, type d'huile ou explorez nos guides complets de soins.",
"title": "Solutions Naturelles pour la Peau",
"subtitle": "Découvrez les solutions d'huiles naturelles parfaites pour vos problèmes de peau. Nos guides créés par des experts vous aident à trouver les bonnes huiles pour les rides, l'acné, la sécheresse et plus encore.",
"categories": {
"oilForConcern": {
"title": "Huile pour Problème",
"description": "Trouvez les meilleures huiles naturelles pour des problèmes de peau spécifiques comme les rides, l'acné, les taches sombres et plus encore."
},
"ageSkinRoutine": {
"title": "Routine Âge-Peau",
"description": "Routines de soins personnalisées basées sur votre âge et type de peau pour des résultats optimaux."
},
"ingredientPairings": {
"title": "Associations d'Ingrédients",
"description": "Apprenez quels ingrédients naturels fonctionnent le mieux ensemble pour des bienfaits améliorés."
},
"bodyPartConcerns": {
"title": "Problèmes par Partie du Corps",
"description": "Solutions ciblées pour des zones spécifiques comme le visage, le cou, les mains et plus encore."
},
"oilComparisons": {
"title": "Comparaisons d'Huiles",
"description": "Comparaisons côte à côte pour vous aider à choisir entre différentes huiles naturelles."
},
"routineStepSkinType": {
"title": "Routine par Type de Peau",
"description": "Guide étape par étape adapté à votre type de peau spécifique et à vos problèmes."
},
"seasonalSkincare": {
"title": "Soins Saisonniers",
"description": "Adaptez votre routine aux saisons pour une peau saine toute l'année."
},
"timeOfDayConcerns": {
"title": "Moment de la Journée",
"description": "Routines de soins matinales et du soir pour une efficacité maximale."
},
"naturalAlternatives": {
"title": "Alternatives Naturelles",
"description": "Découvrez des alternatives naturelles aux ingrédients synthétiques de soins."
},
"culturalBeautySecrets": {
"title": "Secrets de Beauté Culturels",
"description": "Sagesse beauté ancestrale du monde entier utilisant des huiles naturelles."
}
},
"quickAccess": {
"byConcern": "Parcourir par Problème",
"byConcernDesc": "Trouvez des solutions pour vos problèmes de peau spécifiques",
"byOil": "Parcourir par Huile",
"byOilDesc": "Explorez les bienfaits des différentes huiles naturelles",
"links": {
"wrinkles": "Rides & Vieillissement",
"acne": "Acné & Imperfections",
"drySkin": "Peau Sèche",
"darkSpots": "Taches Sombres",
"viewAll": "Voir Tout →",
"rosehipOil": "Huile de Rose Musquée",
"arganOil": "Huile d'Argan",
"jojobaOil": "Huile de Jojoba",
"seaBuckthornOil": "Huile d'Argousier"
}
},
"cta": {
"title": "Prêt à Transformer Votre Peau?",
"description": "Parcourez notre collection d'huiles naturelles premium et commencez votre voyage vers une peau plus saine et éclatante dès aujourd'hui.",
"button": "Acheter les Huiles Naturelles"
}
},
"ByConcern": {
"metaTitle": "Solutions Soins par Problème | ManoonOils",
"metaDescription": "Parcourez les solutions d'huiles naturelles organisées par problème de peau. Trouvez le remède parfait pour les rides, l'acné, la sécheresse et plus encore.",
"title": "Solutions par Problème",
"subtitle": "Explorez notre collection complète de solutions d'huiles naturelles, organisées par problème de peau pour vous aider à trouver exactement ce dont vous avez besoin.",
"stats": {
"availableConcerns": "{count} problèmes de peau couverts",
"totalSolutions": "{count} solutions sélectionnées par des experts"
},
"noResults": "Aucun problème trouvé. Veuillez vérifier plus tard pour de nouvelles solutions."
},
"ByOil": {
"metaTitle": "Solutions Soins par Huile | ManoonOils",
"metaDescription": "Découvrez les bienfaits des différentes huiles naturelles pour divers problèmes de peau. Trouvez quelle huile est la bonne pour vous.",
"title": "Solutions par Huile",
"subtitle": "Apprenez les propriétés uniques de chaque huile naturelle et découvrez lesquelles conviennent le mieux à vos problèmes de peau.",
"stats": {
"availableOils": "{count} huiles naturelles",
"totalSolutions": "{count} solutions sélectionnées par des experts"
},
"noResults": "Aucune huile trouvée. Veuillez vérifier plus tard pour de nouvelles solutions."
},
"completeYourRoutine": "Complétez votre routine",
"discoverProducts": "Découvrez nos produits premium aux ingrédients naturels"
}
}

View File

@@ -319,6 +319,11 @@
"hairCare": "Nega kose",
"skinCare": "Nega kože",
"giftSets": "Poklon setovi",
"solutions": "Rešenja",
"allSolutions": "Sva rešenja",
"byConcern": "Po problemu",
"byOil": "Po ulju",
"skincareGuide": "Vodič za negu",
"about": "O nama",
"ourStory": "Naša priča",
"process": "Proces",
@@ -335,10 +340,14 @@
},
"ProductCard": {
"noImage": "Nema slike",
"outOfStock": "Nema na stanju",
"outOfStock": "Nema na zalihama",
"quickAdd": "Brzo dodavanje",
"contactForPrice": "Kontaktirajte za cenu"
},
"Product": {
"adding": "Dodavanje...",
"addToCart": "Dodaj u korpu"
},
"ProductDetail": {
"home": "Početna",
"outOfStock": "Nema na stanju",
@@ -493,5 +502,107 @@
"goHome": "Početna Strana",
"lookingFor": "Ne možete da pronađete ono što tražite?",
"searchSuggestion": "Pokušajte da pregledate našu kolekciju proizvoda ili nas kontaktirajte za pomoć."
},
"Solutions": {
"breadcrumb": {
"home": "Početna",
"solutions": "Rešenja",
"byConcern": "Po problemu",
"byOil": "Po ulju"
},
"Hub": {
"metaTitle": "Prirodna rešenja za negu kože | ManoonOils",
"metaDescription": "Otkrijte prirodna uljana rešenja za svaki problem kože. Pretražujte po problemu, vrsti ulja ili istražite naše sveobuhvatne vodiče za negu kože.",
"title": "Prirodna rešenja za negu kože",
"subtitle": "Otkrijte savršena prirodna uljana rešenja za vaše probleme sa kožom. Naši stručno izrađeni vodiči pomažu vam da pronađete prava ulja za bore, akne, suvu kožu i još mnogo toga.",
"categories": {
"oilForConcern": {
"title": "Ulje za problem",
"description": "Pronađite najbolja prirodna ulja za specifične probleme kože poput bora, akni, tamnih fleka i još mnogo toga."
},
"ageSkinRoutine": {
"title": "Rutina prema uzrastu",
"description": "Personalizovane rutine nege kože na osnovu vašeg uzrasta i tipa kože za optimalne rezultate."
},
"ingredientPairings": {
"title": "Kombinacije sastojaka",
"description": "Saznajte koji prirodni sastojci najbolje rade zajedno za poboljšane koristi za kožu."
},
"bodyPartConcerns": {
"title": "Problemi po delovima tela",
"description": "Ciljana rešenja za specifične delove tela poput lica, vrata, ruku i još mnogo toga."
},
"oilComparisons": {
"title": "Poređenje ulja",
"description": "Poređenja jedno pored drugog da vam pomognu da izaberete između različitih prirodnih ulja."
},
"routineStepSkinType": {
"title": "Rutina prema tipu kože",
"description": "Vodič korak po korak prilagođen vašem specifičnom tipu kože i problemima."
},
"seasonalSkincare": {
"title": "Sezonska nega kože",
"description": "Prilagodite svoju rutinu godišnjim dobima za zdravu kožu tokom cele godine."
},
"timeOfDayConcerns": {
"title": "Vreme dana",
"description": "Jutarnje i večernje rutine nege kože za maksimalnu efikasnost."
},
"naturalAlternatives": {
"title": "Prirodne alternative",
"description": "Otkrijte prirodne alternative sintetičkim sastojcima za negu kože."
},
"culturalBeautySecrets": {
"title": "Kulturne tajne lepote",
"description": "Drevna mudrost lepote iz celog sveta korišćenjem prirodnih ulja."
}
},
"quickAccess": {
"byConcern": "Pretraži po problemu",
"byConcernDesc": "Pronađi rešenja za svoje probleme sa kožom",
"byOil": "Pretraži po ulju",
"byOilDesc": "Istraži prednosti različitih prirodnih ulja",
"links": {
"wrinkles": "Bore i starenje",
"acne": "Akne i nesavršenstva",
"drySkin": "Suva koža",
"darkSpots": "Tamne fleke",
"viewAll": "Pogledaj sve →",
"rosehipOil": "Ulje divlje ruže",
"arganOil": "Arganovo ulje",
"jojobaOil": "Jojoba ulje",
"seaBuckthornOil": "Ulje pasjeg trna"
}
},
"cta": {
"title": "Spremni za transformaciju kože?",
"description": "Pregledajte našu kolekciju premium prirodnih ulja i započnite svoje putovanje ka zdravijoj, sjajnijoj koži već danas.",
"button": "Kupi prirodna ulja"
}
},
"ByConcern": {
"metaTitle": "Rešenja za negu kože po problemu | ManoonOils",
"metaDescription": "Pregledajte prirodna uljana rešenja organizovana po problemima kože. Pronađite savršen lek za bore, akne, suvu kožu i još mnogo toga.",
"title": "Rešenja po problemu",
"subtitle": "Istražite našu sveobuhvatnu kolekciju prirodnih uljanih rešenja, organizovanih po problemima kože da vam pomognemo da pronađete tačno ono što vam treba.",
"stats": {
"availableConcerns": "{count} problema kože pokriveno",
"totalSolutions": "{count} stručno odabranih rešenja"
},
"noResults": "Nema pronađenih problema. Proverite ponovo kasnije za nova rešenja."
},
"ByOil": {
"metaTitle": "Rešenja za negu kože po ulju | ManoonOils",
"metaDescription": "Otkrijte prednosti različitih prirodnih ulja za različite probleme kože. Pronađite koje ulje je pravo za vas.",
"title": "Rešenja po ulju",
"subtitle": "Saznajte o jedinstvenim svojstvima svakog prirodnog ulja i otkrijte koja su najpogodnija za vaše probleme sa kožom.",
"stats": {
"availableOils": "{count} prirodnih ulja",
"totalSolutions": "{count} stručno odabranih rešenja"
},
"noResults": "Nema pronađenih ulja. Proverite ponovo kasnije za nova rešenja."
},
"completeYourRoutine": "Dovršite svoju rutinu",
"discoverProducts": "Otkrijte naše premium proizvode sa prirodnim sastojcima"
}
}

View File

@@ -0,0 +1,79 @@
import { readFile } from "fs/promises";
import { join } from "path";
import type {
OilForConcernPage,
LocalizedSEOKeywords
} from "./types";
const DATA_DIR = join(process.cwd(), "data");
export function getLocalizedString(
localized: { sr: string; en: string; de: string; fr: string },
locale: string
): string {
return localized[locale as keyof typeof localized] || localized.en;
}
export function getLocalizedArray(
localized: { sr: string[]; en: string[]; de: string[]; fr: string[] },
locale: string
): string[] {
return localized[locale as keyof typeof localized] || localized.en;
}
export function getLocalizedKeywords(
seoKeywords: LocalizedSEOKeywords,
locale: string
): string[] {
const keywords = seoKeywords[locale as keyof LocalizedSEOKeywords] || seoKeywords.en;
return [...keywords.primary, ...keywords.secondary, ...keywords.longTail];
}
export async function getOilForConcernPage(slug: string): Promise<OilForConcernPage | null> {
try {
const filePath = join(DATA_DIR, "oil-for-concern", `${slug}.json`);
const content = await readFile(filePath, "utf-8");
return JSON.parse(content) as OilForConcernPage;
} catch (error) {
console.error(`Failed to load oil-for-concern page: ${slug}`, error);
return null;
}
}
export async function getAllOilForConcernSlugs(): Promise<string[]> {
try {
const { readdir } = await import("fs/promises");
const dirPath = join(DATA_DIR, "oil-for-concern");
const files = await readdir(dirPath);
return files
.filter((file) => file.endsWith(".json"))
.map((file) => file.replace(".json", ""));
} catch (error) {
console.error("Failed to load oil-for-concern slugs:", error);
return [];
}
}
export async function getAllOilForConcernPages(): Promise<OilForConcernPage[]> {
const slugs = await getAllOilForConcernSlugs();
const pages = await Promise.all(
slugs.map((slug) => getOilForConcernPage(slug))
);
return pages.filter((page): page is OilForConcernPage => page !== null);
}
export async function getAllSolutionSlugs(): Promise<Array<{ locale: string; slug: string }>> {
const slugs = await getAllOilForConcernSlugs();
const result: Array<{ locale: string; slug: string }> = [];
for (const slug of slugs) {
for (const locale of ["sr", "en", "de", "fr"] as const) {
result.push({
locale,
slug
});
}
}
return result;
}

View File

@@ -0,0 +1,30 @@
export interface FAQPageSchema {
"@context": "https://schema.org";
"@type": "FAQPage";
mainEntity: Array<{
"@type": "Question";
name: string;
acceptedAnswer: {
"@type": "Answer";
text: string;
};
}>;
}
export function generateFAQPageSchema(
questions: Array<{ question: string; answer: string }>
): FAQPageSchema {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: questions.map((q) => ({
"@type": "Question",
name: q.question,
acceptedAnswer: {
"@type": "Answer",
text: q.answer,
},
})),
};
}

View File

@@ -0,0 +1,388 @@
export interface LocalizedString {
sr: string;
en: string;
de: string;
fr: string;
}
export interface LocalizedArray {
sr: string[];
en: string[];
de: string[];
fr: string[];
}
export interface SEOKeywords {
primary: string[];
secondary: string[];
longTail: string[];
}
export interface LocalizedSEOKeywords {
sr: SEOKeywords;
en: SEOKeywords;
de: SEOKeywords;
fr: SEOKeywords;
}
export interface FAQ {
question: LocalizedString;
answer: LocalizedString;
}
export interface BreadcrumbItem {
name: string;
url: string;
}
export interface RoutineStep {
step: number;
name: LocalizedString;
description: LocalizedString;
products: string[];
technique?: LocalizedString;
}
export interface ComparisonPoint {
category: LocalizedString;
oilAWins: boolean;
oilBWins: boolean;
explanation: LocalizedString;
}
export interface Testimonial {
quote: LocalizedString;
name: string;
age?: number;
skinType?: string;
timeframe?: string;
location?: string;
}
export interface OilForConcernPage {
slug: string;
oilSlug: string;
concernSlug: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
oilName: LocalizedString;
concernName: LocalizedString;
whyThisWorks: LocalizedString;
keyBenefits: LocalizedArray;
howToApply: LocalizedArray;
expectedResults: LocalizedString;
timeframe: LocalizedString;
complementaryIngredients: string[];
productsToShow: string[];
customerResults: Testimonial[];
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
relatedPages: {
otherOilsForSameConcern: string[];
sameOilForOtherConcerns: string[];
};
}
export interface AgeSkinTypeRoutinePage {
slug: string;
ageRange: string;
skinType: string;
season?: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
ageRangeLabel: LocalizedString;
skinTypeLabel: LocalizedString;
seasonLabel?: LocalizedString;
skinChangesAtThisAge: LocalizedArray;
whyThisRoutineWorks: LocalizedString;
morningRoutine: RoutineStep[];
eveningRoutine: RoutineStep[];
weeklyTreatments: RoutineStep[];
keyIngredients: string[];
productsToAvoid: LocalizedArray;
expectedResults: LocalizedString;
timeframe: LocalizedString;
productBundle: string[];
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
}
export interface IngredientPairingPage {
slug: string;
ingredientA: string;
ingredientB: string;
benefitSlug: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
ingredientAName: LocalizedString;
ingredientBName: LocalizedString;
benefitName: LocalizedString;
whyTheyWorkTogether: LocalizedString;
synergyExplanation: LocalizedString;
ingredientAContribution: LocalizedString;
ingredientBContribution: LocalizedString;
howToMix: LocalizedArray;
applicationSteps: LocalizedArray;
bestTimeToUse: LocalizedString;
skinTypesBestFor: string[];
whoShouldAvoid: LocalizedArray;
productsWithBoth: string[];
diyRecipe?: LocalizedString;
expectedResults: LocalizedString;
timeframe: LocalizedString;
customerTestimonials: Testimonial[];
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
alternativePairings: string[];
}
export interface BodyPartConcernPage {
slug: string;
bodyPart: string;
concernSlug: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
bodyPartName: LocalizedString;
concernName: LocalizedString;
whyThisAreaIsDifferent: LocalizedString;
commonCauses: LocalizedArray;
topOilsForThisArea: {
oilSlug: string;
reason: LocalizedString;
rank: number;
}[];
applicationTechnique: LocalizedString;
massageSteps: LocalizedArray;
frequency: LocalizedString;
complementaryTreatments: LocalizedArray;
lifestyleTips: LocalizedArray;
productsSpecificallyForArea: string[];
beforeAfterGallery: boolean;
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
}
export interface OilComparisonPage {
slug: string;
oilA: string;
oilB: string;
concernSlug: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
oilAName: LocalizedString;
oilBName: LocalizedString;
concernName: LocalizedString;
quickVerdict: LocalizedString;
winner: string | "tie" | "depends";
comparisonTable: ComparisonPoint[];
oilAPros: LocalizedArray;
oilACons: LocalizedArray;
oilBPros: LocalizedArray;
oilBCons: LocalizedArray;
chooseOilAIf: LocalizedArray;
chooseOilBIf: LocalizedArray;
detailedComparison: LocalizedString;
priceComparison: LocalizedString;
effectivenessComparison: LocalizedString;
gentlenessComparison: LocalizedString;
bestProducts: {
oilA: string[];
oilB: string[];
};
canYouUseBoth: LocalizedString;
howToCombine: LocalizedArray;
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
relatedComparisons: string[];
}
export interface RoutineStepSkinTypePage {
slug: string;
routineStep: string;
skinType: string;
concernSlug: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
routineStepName: LocalizedString;
skinTypeName: LocalizedString;
concernName: LocalizedString;
whyThisStepMatters: LocalizedString;
whatToLookFor: LocalizedArray;
ingredientsToAvoid: LocalizedArray;
topRecommendations: {
productName: string;
productSlug: string;
whyItWorks: LocalizedString;
keyIngredients: string[];
bestFor: LocalizedString;
rank: number;
}[];
applicationTips: LocalizedArray;
commonMistakes: LocalizedArray;
fullRoutineContext: {
previousStep?: LocalizedString;
nextStep?: LocalizedString;
};
expectedResults: LocalizedString;
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
}
export interface SeasonalSkincarePage {
slug: string;
season: string;
target: string;
targetType: "skinType" | "ageRange" | "concern";
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
seasonName: LocalizedString;
targetName: LocalizedString;
seasonalChallenges: LocalizedArray;
howSkinChanges: LocalizedString;
routineAdjustments: {
add: LocalizedArray;
remove: LocalizedArray;
modify: LocalizedArray;
};
morningRoutine: RoutineStep[];
eveningRoutine: RoutineStep[];
keyIngredients: string[];
ingredientsToAvoidThisSeason: LocalizedArray;
lifestyleTips: LocalizedArray;
nutritionTips: LocalizedArray;
seasonalProductBundle: string[];
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
}
export interface TimeOfDayConcernPage {
slug: string;
timeOfDay: string;
productType: string;
concernSlug: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
timeOfDayName: LocalizedString;
productTypeName: LocalizedString;
concernName: LocalizedString;
whyTimingMatters: LocalizedString;
skinBehaviorAtThisTime: LocalizedString;
whatHappensOvernight?: LocalizedString;
whyMorningRoutineMatters?: LocalizedString;
keyIngredientsToLookFor: LocalizedArray;
ingredientsToAvoid: LocalizedArray;
topRecommendations: {
productName: string;
productSlug: string;
whyItWorks: LocalizedString;
keyIngredients: string[];
rank: number;
}[];
applicationTips: LocalizedArray;
layeringOrder: LocalizedArray;
complementaryProducts: string[];
expectedResults: LocalizedString;
timeframe: LocalizedString;
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
relatedTimeSpecificPages: string[];
}
export interface NaturalAlternativePage {
slug: string;
syntheticIngredient: string;
concernSlug: string;
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
syntheticName: LocalizedString;
concernName: LocalizedString;
whyPeopleWantAlternatives: LocalizedString;
syntheticHowItWorks: LocalizedString;
syntheticSideEffects: LocalizedArray;
naturalAlternativeName: LocalizedString;
naturalAlternativeSlug: string;
howNaturalAlternativeWorks: LocalizedString;
effectivenessComparison: LocalizedString;
timelineComparison: LocalizedString;
gentlenessComparison: LocalizedString;
costComparison: LocalizedString;
whoShouldSwitch: LocalizedArray;
transitionGuide: LocalizedArray;
whatToExpect: LocalizedString;
bestProductsWithNaturalAlternative: string[];
diyOption?: LocalizedString;
customerStories: Testimonial[];
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
otherNaturalAlternatives: string[];
}
export interface CulturalBeautySecretPage {
slug: string;
region: string;
focus: string;
focusType: "ingredient" | "technique" | "ritual";
pageTitle: LocalizedString;
metaTitle: LocalizedString;
metaDescription: LocalizedString;
regionName: LocalizedString;
focusName: LocalizedString;
culturalBackground: LocalizedString;
historicalContext: LocalizedString;
traditionalUse: LocalizedString;
modernScienceValidation: LocalizedString;
keyIngredients: string[];
traditionalPreparation: LocalizedString;
modernAdaptation: LocalizedString;
applicationRitual: LocalizedArray;
bestTimeToUse: LocalizedString;
skinTypesBestFor: string[];
expectedResults: LocalizedString;
timeframe: LocalizedString;
productsInspiredByTradition: string[];
whereSourceFrom: LocalizedString;
sustainabilityNotes: LocalizedString;
customerExperiences: Testimonial[];
faqs: FAQ[];
seoKeywords: LocalizedSEOKeywords;
relatedCulturalPages: string[];
}
export type ProgrammaticPageType =
| "oil-for-concern"
| "age-skin-routine"
| "ingredient-pairing"
| "body-part-concern"
| "oil-comparison"
| "routine-step-skin"
| "seasonal-skincare"
| "time-of-day-concern"
| "natural-alternative"
| "cultural-beauty-secret";
export type ProgrammaticPage =
| OilForConcernPage
| AgeSkinTypeRoutinePage
| IngredientPairingPage
| BodyPartConcernPage
| OilComparisonPage
| RoutineStepSkinTypePage
| SeasonalSkincarePage
| TimeOfDayConcernPage
| NaturalAlternativePage
| CulturalBeautySecretPage;
export interface DataLoader<T> {
getAll(): Promise<T[]>;
getBySlug(slug: string): Promise<T | null>;
getBySlugs(slugs: string[]): Promise<T[]>;
}