feat: implement locale-aware routing with [locale] dynamic segments
Some checks failed
Build and Deploy / build (push) Has been cancelled

WARNING: This change breaks existing SEO URLs for Serbian locale.

Changes:
- Migrated from separate locale folders (src/app/en/, src/app/de/, etc.)
  to [locale] dynamic segments (src/app/[locale]/)
- Serbian is now at /sr/ instead of / (root)
- English at /en/, German at /de/, French at /fr/
- All components updated to generate locale-aware links
- Root / now redirects to /sr (307 temporary redirect)

SEO Impact:
- Previously indexed Serbian URLs (/, /products, /about, /contact)
  will now return 404 or redirect to /sr/* URLs
- This is a breaking change for SEO - Serbian pages should ideally
  remain at root (/) with only non-default locales getting prefix
- Consider implementing 301 redirects from old URLs to maintain
  search engine rankings

Technical Notes:
- next-intl v4 with [locale] structure requires ALL locales to
  have the prefix (cannot have default locale at root)
- Alternative approach would be separate folder structure per locale
This commit is contained in:
Unchained
2026-03-23 20:59:33 +02:00
parent 5bd1a0f167
commit 92b6c830e1
47 changed files with 2175 additions and 2881 deletions

View File

@@ -1,38 +1,45 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { Instagram, Facebook } from "lucide-react";
import { useTranslations } from "next-intl";
const footerLinks = {
shop: [
{ label: "All Products", href: "/products" },
{ label: "Hair Care", href: "/products" },
{ label: "Skin Care", href: "/products" },
{ label: "Gift Sets", href: "/products" },
],
about: [
{ label: "Our Story", href: "/about" },
{ label: "Process", href: "/about" },
{ label: "Sustainability", href: "/about" },
],
help: [
{ label: "FAQ", href: "/contact" },
{ label: "Shipping", href: "/contact" },
{ label: "Returns", href: "/contact" },
{ label: "Contact Us", href: "/contact" },
],
};
interface FooterProps {
locale?: string;
}
export default function Footer() {
export default function Footer({ locale = "sr" }: FooterProps) {
const t = useTranslations("Footer");
const currentYear = new Date().getFullYear();
const localePath = `/${locale}`;
const footerLinks = {
shop: [
{ label: t("allProducts"), href: `${localePath}/products` },
{ label: t("hairCare"), href: `${localePath}/products` },
{ label: t("skinCare"), href: `${localePath}/products` },
{ label: t("giftSets"), href: `${localePath}/products` },
],
about: [
{ label: t("ourStory"), href: `${localePath}/about` },
{ label: t("process"), href: `${localePath}/about` },
{ label: t("sustainability"), href: `${localePath}/about` },
],
help: [
{ label: t("faq"), href: `${localePath}/contact` },
{ label: t("shipping"), href: `${localePath}/contact` },
{ label: t("returns"), href: `${localePath}/contact` },
{ label: t("contactUs"), href: `${localePath}/contact` },
],
};
return (
<footer className="bg-white border-t border-[#e5e5e5]">
{/* Main Footer */}
<div className="container py-16 lg:py-20">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8">
{/* Brand Column */}
<div className="lg:col-span-4">
<Link href="/" className="inline-block mb-6">
<Link href={localePath} className="inline-block mb-6">
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
@@ -42,9 +49,8 @@ export default function Footer() {
/>
</Link>
<p className="text-[#666666] text-sm leading-relaxed max-w-xs mb-6">
Premium natural oils for hair and skin care. Handcrafted with love using traditional methods.
{t("brandDescription")}
</p>
{/* Social Links */}
<div className="flex items-center gap-4">
<a
href="https://instagram.com"
@@ -67,13 +73,11 @@ export default function Footer() {
</div>
</div>
{/* Links Columns - All aligned at top */}
<div className="lg:col-span-8">
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
{/* Shop */}
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
Shop
{t("shop")}
</h4>
<ul className="space-y-3">
{footerLinks.shop.map((link) => (
@@ -89,10 +93,9 @@ export default function Footer() {
</ul>
</div>
{/* About */}
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
About
{t("about")}
</h4>
<ul className="space-y-3">
{footerLinks.about.map((link) => (
@@ -108,10 +111,9 @@ export default function Footer() {
</ul>
</div>
{/* Help */}
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
Help
{t("help")}
</h4>
<ul className="space-y-3">
{footerLinks.help.map((link) => (
@@ -131,18 +133,15 @@ export default function Footer() {
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-[#e5e5e5]">
<div className="container py-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
{/* Copyright */}
<p className="text-xs text-[#999999]">
&copy; {currentYear} ManoonOils. All rights reserved.
&copy; {currentYear} ManoonOils. {t("allRights")}
</p>
{/* Payment Methods */}
<div className="flex items-center gap-3">
<span className="text-xs text-[#999999]">We accept:</span>
<span className="text-xs text-[#999999]">{t("weAccept")}</span>
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#666666] px-2 py-1 border border-[#e5e5e5] rounded">
Visa
@@ -160,4 +159,4 @@ export default function Footer() {
</div>
</footer>
);
}
}

View File

@@ -4,29 +4,28 @@ import { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { AnimatePresence, motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { User, ShoppingBag, Menu, X } from "lucide-react";
import CartDrawer from "@/components/cart/CartDrawer";
const navLinks = [
{ href: "/products", label: "Products" },
{ href: "/about", label: "About" },
{ href: "/contact", label: "Contact" },
];
interface HeaderProps {
locale?: string;
}
export default function Header() {
export default function Header({ locale = "sr" }: HeaderProps) {
const t = useTranslations("Header");
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
const itemCount = getLineCount();
const localePath = `/${locale}`;
// Initialize checkout on mount
useEffect(() => {
initCheckout();
}, [initCheckout]);
// Track scroll for header styling
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50);
@@ -35,7 +34,6 @@ export default function Header() {
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Lock body scroll when mobile menu is open
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = "hidden";
@@ -47,6 +45,12 @@ export default function Header() {
};
}, [mobileMenuOpen]);
const navLinks = [
{ href: `${localePath}/products`, label: t("products") },
{ href: `${localePath}/about`, label: t("about") },
{ href: `${localePath}/contact`, label: t("contact") },
];
return (
<>
<header
@@ -57,16 +61,14 @@ export default function Header() {
}`}
>
<div className="relative flex items-center justify-between h-[72px] px-4 lg:px-6">
{/* Mobile Menu Button */}
<button
className="lg:hidden p-2 -ml-2 hover:bg-black/5 rounded-full transition-colors"
onClick={() => setMobileMenuOpen(true)}
aria-label="Open menu"
aria-label={t("openMenu")}
>
<Menu className="w-5 h-5" />
</button>
{/* Left side - Desktop Nav */}
<nav className="hidden lg:flex items-center gap-10">
{navLinks.map((link) => (
<Link
@@ -80,8 +82,7 @@ export default function Header() {
))}
</nav>
{/* Logo - Centered (absolute on desktop, flex on mobile) */}
<Link href="/" className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<Link href={localePath || "/"} className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
@@ -92,11 +93,10 @@ export default function Header() {
/>
</Link>
{/* Right side - Icons */}
<div className="flex items-center gap-1">
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
aria-label="Account"
aria-label={t("account")}
>
<User className="w-5 h-5" strokeWidth={1.5} />
</button>
@@ -104,7 +104,7 @@ export default function Header() {
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
onClick={toggleCart}
aria-label="Open cart"
aria-label={t("openCart")}
>
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
{itemCount > 0 && (
@@ -117,7 +117,6 @@ export default function Header() {
</div>
</header>
{/* Mobile Menu Overlay */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
@@ -128,9 +127,8 @@ export default function Header() {
className="fixed inset-0 z-[60] bg-white"
>
<div className="container h-full flex flex-col">
{/* Mobile Header */}
<div className="flex items-center justify-between h-[72px]">
<Link href="/" onClick={() => setMobileMenuOpen(false)}>
<Link href={localePath || "/"} onClick={() => setMobileMenuOpen(false)}>
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
@@ -142,13 +140,12 @@ export default function Header() {
<button
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
onClick={() => setMobileMenuOpen(false)}
aria-label="Close menu"
aria-label={t("closeMenu")}
>
<X className="w-6 h-6" />
</button>
</div>
{/* Mobile Navigation */}
<nav className="flex-1 flex flex-col justify-center gap-8">
{navLinks.map((link, index) => (
<motion.div
@@ -168,7 +165,6 @@ export default function Header() {
))}
</nav>
{/* Mobile Footer */}
<div className="py-8 border-t border-[#e5e5e5]">
<div className="flex items-center justify-between">
<button
@@ -179,13 +175,13 @@ export default function Header() {
}}
>
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
Cart ({itemCount})
{t("cart")} ({itemCount})
</button>
<button
className="flex items-center gap-2 text-sm text-[#666666] hover:text-black transition-colors"
>
<User className="w-5 h-5" strokeWidth={1.5} />
Account
{t("account")}
</button>
</div>
</div>
@@ -197,4 +193,4 @@ export default function Header() {
<CartDrawer />
</>
);
}
}