feat: implement locale-aware routing with [locale] dynamic segments
Some checks failed
Build and Deploy / build (push) Has been cancelled
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:
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user