From 8de14fd3c0c35c5708f28768c134131a45e340a3 Mon Sep 17 00:00:00 2001 From: hamed Date: Wed, 8 Oct 2025 10:26:22 +0330 Subject: [PATCH] implement canonical URL handling with server and client-side support, add middleware for pathname access, and create hooks for dynamic canonical URL injection --- app/layout.js | 15 ++++++- components/CanonicalHandler.js | 36 +++++++++++++++ hooks/useCanonicalUrl.js | 57 ++++++++++++++++++++++++ lib/getCanonicalUrl.js | 80 ++++++++++++++++++++++++++++++++++ middleware.js | 23 ++++++++++ 5 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 components/CanonicalHandler.js create mode 100644 hooks/useCanonicalUrl.js create mode 100644 lib/getCanonicalUrl.js create mode 100644 middleware.js diff --git a/app/layout.js b/app/layout.js index 5ff8701..f821b6a 100644 --- a/app/layout.js +++ b/app/layout.js @@ -8,11 +8,15 @@ import "react-toastify/dist/ReactToastify.css"; import CustomToastify from "./CustomToastify"; import { ProvinceProvider } from "@/context/ProvinceProvider"; import { getStateInfo } from "@/lib/getStateInfo"; +import { getCanonicalUrl } from "@/lib/getCanonicalUrl"; export async function generateMetadata() { const { matchedCity } = getStateInfo(); + console.log("Matched City in generateMetadata:", matchedCity); - return { + const canonicalUrl = getCanonicalUrl(); + + const baseMetadata = { title: matchedCity ? matchedCity.title : "نوبت724", description: matchedCity ? matchedCity.description @@ -24,6 +28,15 @@ export async function generateMetadata() { 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 }) { diff --git a/components/CanonicalHandler.js b/components/CanonicalHandler.js new file mode 100644 index 0000000..102100d --- /dev/null +++ b/components/CanonicalHandler.js @@ -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 +} \ No newline at end of file diff --git a/hooks/useCanonicalUrl.js b/hooks/useCanonicalUrl.js new file mode 100644 index 0000000..649752d --- /dev/null +++ b/hooks/useCanonicalUrl.js @@ -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 }; +} \ No newline at end of file diff --git a/lib/getCanonicalUrl.js b/lib/getCanonicalUrl.js new file mode 100644 index 0000000..94784bd --- /dev/null +++ b/lib/getCanonicalUrl.js @@ -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; + } +} \ No newline at end of file diff --git a/middleware.js b/middleware.js new file mode 100644 index 0000000..108ef42 --- /dev/null +++ b/middleware.js @@ -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).*)', + ], +}; \ No newline at end of file