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:
+14
-1
@@ -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 }) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user