feat: implement programmatic SEO for solutions pages
- Add 10 oil-for-concern solution pages with localized slugs - Support 4 languages: sr, en, de, fr with proper canonical URLs - Add solutions hub, by-concern, and by-oil directory pages - Filter bundle products from solutions pages - Add hideLangSwitcher prop to Header component - Update translations for all languages - Fix canonical URLs to include locale prefix
This commit is contained in:
@@ -2,7 +2,10 @@ import { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { ChevronRight, Droplets } from "lucide-react";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { getAllOilForConcernPages, getLocalizedString } from "@/lib/programmatic-seo/dataLoader";
|
||||
import { isValidLocale, DEFAULT_LOCALE } from "@/lib/i18n/locales";
|
||||
|
||||
type Params = Promise<{ locale: string }>;
|
||||
|
||||
@@ -12,11 +15,22 @@ export async function generateMetadata({
|
||||
params: Params;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "Solutions.ByOil" });
|
||||
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||
const t = await getTranslations({ locale: validLocale, namespace: "Solutions.ByOil" });
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
||||
const localePrefix = validLocale === DEFAULT_LOCALE ? "/sr" : `/${validLocale}`;
|
||||
const canonicalUrl = `${baseUrl}${localePrefix}/solutions/by-oil`;
|
||||
|
||||
return {
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
},
|
||||
openGraph: {
|
||||
url: canonicalUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,9 +54,25 @@ interface OilCardProps {
|
||||
concernCount: number;
|
||||
topConcerns: string[];
|
||||
locale: string;
|
||||
firstSolutionSlug: string;
|
||||
bestFor: string;
|
||||
exploreSolutions: string;
|
||||
solutionLabel: string;
|
||||
solutionsLabel: string;
|
||||
}
|
||||
|
||||
function OilCard({ oilSlug, oilName, concernCount, topConcerns, locale }: OilCardProps) {
|
||||
function OilCard({
|
||||
oilSlug,
|
||||
oilName,
|
||||
concernCount,
|
||||
topConcerns,
|
||||
locale,
|
||||
firstSolutionSlug,
|
||||
bestFor,
|
||||
exploreSolutions,
|
||||
solutionLabel,
|
||||
solutionsLabel
|
||||
}: 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">
|
||||
@@ -54,11 +84,11 @@ function OilCard({ oilSlug, oilName, concernCount, topConcerns, locale }: OilCar
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[#666666] mb-4">
|
||||
{concernCount} {concernCount === 1 ? "concern solution" : "concern solutions"} available
|
||||
{concernCount} {concernCount === 1 ? solutionLabel : solutionsLabel}
|
||||
</p>
|
||||
<div className="space-y-2 mb-4">
|
||||
<p className="text-xs uppercase tracking-wider text-[#999999] font-medium">
|
||||
Best for:
|
||||
{bestFor}
|
||||
</p>
|
||||
{topConcerns.slice(0, 3).map((concernName) => (
|
||||
<div key={concernName} className="flex items-center gap-2 text-sm text-[#666666]">
|
||||
@@ -68,10 +98,10 @@ function OilCard({ oilSlug, oilName, concernCount, topConcerns, locale }: OilCar
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href={`/${locale}/solutions/by-oil/${oilSlug}`}
|
||||
href={`/${locale}/solutions/${firstSolutionSlug}`}
|
||||
className="inline-flex items-center text-sm font-medium text-[#1a1a1a] group-hover:text-black transition-colors"
|
||||
>
|
||||
Explore Oil Solutions
|
||||
{exploreSolutions}
|
||||
<ChevronRight className="ml-1 w-4 h-4 transform group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -96,70 +126,80 @@ export default async function ByOilPage({
|
||||
name: getLocalizedString(pages[0].oilName, locale),
|
||||
concernCount: pages.length,
|
||||
topConcerns: pages.slice(0, 3).map((p) => getLocalizedString(p.concernName, locale)),
|
||||
firstSolutionSlug: getLocalizedString(pages[0].localizedSlugs, 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>
|
||||
<>
|
||||
<Header locale={locale} hideLangSwitcher={true} />
|
||||
<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="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 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>
|
||||
|
||||
<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 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}
|
||||
firstSolutionSlug={oil.firstSolutionSlug}
|
||||
bestFor={pageT("oilCard.bestFor")}
|
||||
exploreSolutions={pageT("oilCard.exploreSolutions")}
|
||||
solutionLabel={pageT("oilCard.solutionLabel")}
|
||||
solutionsLabel={pageT("oilCard.solutionsLabel")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{oilsList.length === 0 && (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-[#666666]">{pageT("noResults")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<Footer locale={locale} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user