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:
106
src/app/[locale]/solutions/[slug]/page.tsx
Normal file
106
src/app/[locale]/solutions/[slug]/page.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
154
src/app/[locale]/solutions/by-concern/page.tsx
Normal file
154
src/app/[locale]/solutions/by-concern/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
165
src/app/[locale]/solutions/by-oil/page.tsx
Normal file
165
src/app/[locale]/solutions/by-oil/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
291
src/app/[locale]/solutions/page.tsx
Normal file
291
src/app/[locale]/solutions/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user