Implement multi-domain SEO improvements:
- Add metadata to login and login-verify pages to prevent indexing. - Update robots.txt to disallow additional sensitive paths. - Enhance sitemap generation to filter by city and include accurate last modified dates. - Refactor canonical URL generation to support multi-domain architecture, ensuring self-canonicalization for city domains. - Remove deprecated CanonicalHandler component and streamline canonical URL handling. - Introduce safe JSON-LD output to prevent XSS vulnerabilities. - Add payment layout with appropriate metadata to prevent indexing. - Conduct a comprehensive technical SEO audit and implement necessary fixes across the application.
This commit is contained in:
+39
-69
@@ -1,80 +1,50 @@
|
||||
import { headers } from "next/headers";
|
||||
import citiesData from "@/data/city.json";
|
||||
|
||||
const MAIN_DOMAIN = "https://nobat724.com";
|
||||
// استراتژی چند-دامنهای: هر دامنهی شهری یک property مستقل و first-class است.
|
||||
// هر صفحه self-canonical روی هاست جاری میگیرد؛ هرگز به دامنهی اصلی canonical نمیدهیم.
|
||||
|
||||
const DEFAULT_HOST = "nobat724.com";
|
||||
|
||||
export function normalizeHost(rawHost) {
|
||||
return (rawHost || DEFAULT_HOST).toLowerCase().replace(/^www\./, "");
|
||||
}
|
||||
|
||||
export function buildCanonicalPath(pathname) {
|
||||
let path = (pathname || "/").split("?")[0].split("#")[0];
|
||||
if (!path.startsWith("/")) path = `/${path}`;
|
||||
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
||||
return path;
|
||||
}
|
||||
|
||||
export function buildCanonicalUrl(host, pathname) {
|
||||
const normalizedHost = normalizeHost(host);
|
||||
const hostname = normalizedHost.replace(/:\d+$/, "");
|
||||
const protocol =
|
||||
hostname.endsWith("localhost") || hostname === "127.0.0.1" ? "http" : "https";
|
||||
return `${protocol}://${normalizedHost}${buildCanonicalPath(pathname)}`;
|
||||
}
|
||||
|
||||
// origin هاست جاری (بدون trailing slash) — برای ساخت URLهای مطلق در JSON-LD و متادیتا
|
||||
export async function getRequestOrigin() {
|
||||
try {
|
||||
const headersList = await headers();
|
||||
return buildCanonicalUrl(headersList.get("host"), "/").replace(/\/$/, "");
|
||||
} catch {
|
||||
return "https://nobat724.com";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current domain should have a canonical tag
|
||||
* and returns the canonical URL if needed
|
||||
*/
|
||||
export async function getCanonicalUrl() {
|
||||
try {
|
||||
const headersList = await 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;
|
||||
const host = headersList.get("host");
|
||||
const pathname = headersList.get("x-pathname");
|
||||
// مسیرهای خارج از matcher میدلور (panel/dashboard/login) این هدر را ندارند —
|
||||
// آنها noindex هستند و نباید canonical جعلی به ریشه بگیرند.
|
||||
if (!pathname) return null;
|
||||
return buildCanonicalUrl(host, pathname);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getStateInfoClient } from '@/lib/getStateInfoClient';
|
||||
import { getCanonicalUrlClient } from '@/lib/getCanonicalUrl';
|
||||
import { buildCanonicalUrl, buildCanonicalPath, normalizeHost } from '@/lib/getCanonicalUrl';
|
||||
|
||||
function setLocation(loc) {
|
||||
Object.defineProperty(window, 'location', { value: loc, writable: true, configurable: true });
|
||||
@@ -21,22 +21,38 @@ describe('getStateInfoClient (window)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCanonicalUrlClient (window)', () => {
|
||||
beforeEach(() => {
|
||||
setLocation({ host: 'arak-nobat.ir', pathname: '/doctors' });
|
||||
describe('buildCanonicalUrl (self-canonical per-host)', () => {
|
||||
it('دامنهی شهری → self-canonical روی همان دامنه', () => {
|
||||
expect(buildCanonicalUrl('arak-nobat.ir', '/doctors')).toBe('https://arak-nobat.ir/doctors');
|
||||
});
|
||||
|
||||
it('subdomain → canonical به دامنهی اصلی', () => {
|
||||
expect(getCanonicalUrlClient()).toBe('https://nobat724.com/doctors');
|
||||
it('دامنهی اصلی → self-canonical روی دامنهی اصلی', () => {
|
||||
expect(buildCanonicalUrl('nobat724.com', '/doctors')).toBe('https://nobat724.com/doctors');
|
||||
});
|
||||
|
||||
it('دامنهی اصلی → null', () => {
|
||||
setLocation({ host: 'nobat724.com', pathname: '/doctors' });
|
||||
expect(getCanonicalUrlClient()).toBeNull();
|
||||
it('www و حروف بزرگ نرمال میشوند', () => {
|
||||
expect(buildCanonicalUrl('WWW.Nobat724.com', '/x')).toBe('https://nobat724.com/x');
|
||||
});
|
||||
|
||||
it('www و حروف بزرگ نرمال میشوند (دامنهی اصلی) → null', () => {
|
||||
setLocation({ host: 'WWW.Nobat724.com', pathname: '/x' });
|
||||
expect(getCanonicalUrlClient()).toBeNull();
|
||||
it('هاست dev با پورت → http و حفظ پورت', () => {
|
||||
expect(buildCanonicalUrl('yazd-nobat.localhost:3000', '/doctors')).toBe(
|
||||
'http://yazd-nobat.localhost:3000/doctors'
|
||||
);
|
||||
});
|
||||
|
||||
it('query string و trailing slash حذف میشوند', () => {
|
||||
expect(buildCanonicalUrl('arak-nobat.ir', '/doctors/?specialty=قلب')).toBe(
|
||||
'https://arak-nobat.ir/doctors'
|
||||
);
|
||||
});
|
||||
|
||||
it('ریشه دستنخورده میماند', () => {
|
||||
expect(buildCanonicalUrl('arak-nobat.ir', '/')).toBe('https://arak-nobat.ir/');
|
||||
expect(buildCanonicalPath('/')).toBe('/');
|
||||
});
|
||||
|
||||
it('هاست خالی → دامنهی پیشفرض', () => {
|
||||
expect(normalizeHost('')).toBe('nobat724.com');
|
||||
expect(buildCanonicalUrl(null, '/x')).toBe('https://nobat724.com/x');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ export function sanitizeHtml(html) {
|
||||
});
|
||||
}
|
||||
|
||||
// خروجی امن برای <script type="application/ld+json"> — جلوگیری از بستن تگ با دادهی کاربر
|
||||
export function safeJsonLd(obj) {
|
||||
return JSON.stringify(obj).replace(/</g, "\\u003c");
|
||||
}
|
||||
|
||||
export function safeJsonParse(str, fallback = null) {
|
||||
if (!str) return fallback;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user