implement canonical URL handling with server and client-side support, add middleware for pathname access, and create hooks for dynamic canonical URL injection

This commit is contained in:
2025-10-08 10:26:22 +03:30
parent e6893846bd
commit 8de14fd3c0
5 changed files with 210 additions and 1 deletions
+14 -1
View File
@@ -8,11 +8,15 @@ import "react-toastify/dist/ReactToastify.css";
import CustomToastify from "./CustomToastify"; import CustomToastify from "./CustomToastify";
import { ProvinceProvider } from "@/context/ProvinceProvider"; import { ProvinceProvider } from "@/context/ProvinceProvider";
import { getStateInfo } from "@/lib/getStateInfo"; import { getStateInfo } from "@/lib/getStateInfo";
import { getCanonicalUrl } from "@/lib/getCanonicalUrl";
export async function generateMetadata() { export async function generateMetadata() {
const { matchedCity } = getStateInfo(); const { matchedCity } = getStateInfo();
console.log("Matched City in generateMetadata:", matchedCity);
return { const canonicalUrl = getCanonicalUrl();
const baseMetadata = {
title: matchedCity ? matchedCity.title : "نوبت724", title: matchedCity ? matchedCity.title : "نوبت724",
description: matchedCity description: matchedCity
? matchedCity.description ? matchedCity.description
@@ -24,6 +28,15 @@ export async function generateMetadata() {
images: ["https://www.nobat724.com/assets/images/logo.png"], images: ["https://www.nobat724.com/assets/images/logo.png"],
}, },
}; };
// Add canonical URL only if we're not on the main domain and matchedCity domain is not nobat724.com
if (canonicalUrl && (!matchedCity || matchedCity.domain !== 'nobat724.com')) {
baseMetadata.alternates = {
canonical: canonicalUrl,
};
}
return baseMetadata;
} }
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
+36
View File
@@ -0,0 +1,36 @@
'use client';
import { useEffect } from 'react';
import { getCanonicalUrlClient } from '@/lib/getCanonicalUrl';
/**
* Client-side canonical URL handler
* Use this component in pages that need dynamic canonical URLs
* that can't be determined at build time or in generateMetadata
*/
export default function CanonicalHandler() {
useEffect(() => {
// Only run on client side
if (typeof window === 'undefined') return;
const canonicalUrl = getCanonicalUrlClient();
if (canonicalUrl) {
// Check if canonical link already exists
let canonicalLink = document.querySelector('link[rel="canonical"]');
if (canonicalLink) {
// Update existing canonical link
canonicalLink.href = canonicalUrl;
} else {
// Create new canonical link
canonicalLink = document.createElement('link');
canonicalLink.rel = 'canonical';
canonicalLink.href = canonicalUrl;
document.head.appendChild(canonicalLink);
}
}
}, []);
return null; // This component doesn't render anything
}
+57
View File
@@ -0,0 +1,57 @@
'use client';
import { useEffect, useState } from 'react';
import { getCanonicalUrlClient } from '@/lib/getCanonicalUrl';
/**
* Custom hook to get canonical URL on client side
* Useful for client components that need to know the canonical URL
*/
export function useCanonicalUrl() {
const [canonicalUrl, setCanonicalUrl] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const canonical = getCanonicalUrlClient();
setCanonicalUrl(canonical);
setIsLoaded(true);
}, []);
return { canonicalUrl, isLoaded };
}
/**
* Hook to automatically inject canonical URL into document head
* Use this in client components where you need automatic canonical URL injection
*/
export function useCanonicalUrlInjection() {
const { canonicalUrl, isLoaded } = useCanonicalUrl();
useEffect(() => {
if (!isLoaded || !canonicalUrl) return;
// Check if canonical link already exists
let canonicalLink = document.querySelector('link[rel="canonical"]');
if (canonicalLink) {
// Update existing canonical link
canonicalLink.href = canonicalUrl;
} else {
// Create new canonical link
canonicalLink = document.createElement('link');
canonicalLink.rel = 'canonical';
canonicalLink.href = canonicalUrl;
document.head.appendChild(canonicalLink);
}
// Cleanup function
return () => {
const link = document.querySelector(`link[rel="canonical"][href="${canonicalUrl}"]`);
if (link) {
link.remove();
}
};
}, [canonicalUrl, isLoaded]);
return { canonicalUrl, isLoaded };
}
+80
View File
@@ -0,0 +1,80 @@
import { headers } from "next/headers";
import citiesData from "@/data/city.json";
const MAIN_DOMAIN = "https://nobat724.com";
/**
* Determines if the current domain should have a canonical tag
* and returns the canonical URL if needed
*/
export function getCanonicalUrl() {
try {
const headersList = headers();
const host = headersList.get("host") || "";
const pathname = headersList.get("x-pathname") || "/";
// Normalize host (remove www and convert to lowercase)
const normalizedHost = host.toLowerCase().replace(/^www\./, "");
// Check if current domain is the main domain
if (normalizedHost === "nobat724.com") {
return null; // No canonical needed for main domain
}
// Check if this is a subdomain/different domain from our city data
const subdomain = normalizedHost.split(".")[0];
const matchedCity = citiesData.find((city) => {
const cityDomain = city.domain.toLowerCase();
return cityDomain.includes(subdomain) || cityDomain === normalizedHost;
});
// If we found a matching city or any non-main domain, canonicalize to main domain
if (matchedCity || normalizedHost !== "nobat724.com") {
// Ensure pathname starts with /
const cleanPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
return `${MAIN_DOMAIN}${cleanPathname}`;
}
return null;
} catch (error) {
console.error("Error generating canonical URL:", error);
return null;
}
}
/**
* Client-side version for dynamic routes or client components
*/
export function getCanonicalUrlClient() {
if (typeof window === "undefined") return null;
try {
const host = window.location.host;
const pathname = window.location.pathname;
// Normalize host (remove www and convert to lowercase)
const normalizedHost = host.toLowerCase().replace(/^www\./, "");
// Check if current domain is the main domain
if (normalizedHost === "nobat724.com") {
return null; // No canonical needed for main domain
}
// Check if this is a subdomain/different domain from our city data
const subdomain = normalizedHost.split(".")[0];
const matchedCity = citiesData.find((city) => {
const cityDomain = city.domain.toLowerCase();
return cityDomain.includes(subdomain) || cityDomain === normalizedHost;
});
// If we found a matching city or any non-main domain, canonicalize to main domain
if (matchedCity || normalizedHost !== "nobat724.com") {
return `${MAIN_DOMAIN}${pathname}`;
}
return null;
} catch (error) {
console.error("Error generating client canonical URL:", error);
return null;
}
}
+23
View File
@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';
export function middleware(request) {
const response = NextResponse.next();
// Add pathname to headers so we can access it in getCanonicalUrl
response.headers.set('x-pathname', request.nextUrl.pathname);
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};