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:
hamed
2026-07-05 15:47:47 +03:30
parent 522caf6b1b
commit b04bb45ca1
24 changed files with 595 additions and 329 deletions
+39 -69
View File
@@ -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;
}
}
+29 -13
View File
@@ -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');
});
});
+5
View File
@@ -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 {