$ttl) return null;
return file_get_contents($path);
}
function cacheSitemap(string $path, string $content): bool {
return @file_put_contents($path, $content, LOCK_EX) !== false;
}
function logSitemapEvent(string $level, string $message, array $context = []): void {
$entry = sprintf(
"[%s] [%s] %s %s\n",
date('Y-m-d H:i:s'),
strtoupper($level),
$message,
empty($context) ? '' : json_encode($context, JSON_UNESCAPED_UNICODE)
);
@file_put_contents(LOG_PATH, $entry, FILE_APPEND | LOCK_EX);
}
// =============================================================================
// DATABASE INTEGRATION
// =============================================================================
function getDatabaseConnection(): ?PDO {
if (!DB_DSN || !DB_USER) {
return null;
}
try {
return new PDO(DB_DSN, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4",
PDO::ATTR_PERSISTENT => false,
]);
} catch (PDOException $e) {
logSitemapEvent('ERROR', 'Database connection failed', [
'message' => $e->getMessage(),
'code' => $e->getCode()
]);
return null;
}
}
function fetchPagesForSitemap(PDO $pdo): array {
$data = [
'pages' => [],
'posts' => [],
'categories' => [],
'products' => [],
];
// Main pages with AI relevance signals
$stmt = $pdo->prepare("
SELECT
p.id, p.title, p.slug AS url, p.meta_description, p.main_image,
p.priority, p.changefreq, p.language, p.target_countries,
UNIX_TIMESTAMP(p.updated_at) AS lastmod,
p.ai_relevance_score, p.eeat_signals, p.semantic_keywords
FROM pages p
WHERE p.status = 'published'
AND p.is_sitemap_visible = 1
AND p.noindex != 1
ORDER BY p.priority DESC, p.updated_at DESC
LIMIT 5000
");
$stmt->execute();
$data['pages'] = $stmt->fetchAll();
// Blog posts with publication dates
$stmt = $pdo->prepare("
SELECT
post.id, post.title, post.slug AS url, post.excerpt AS description,
post.featured_image AS image, post.language, post.target_countries,
UNIX_TIMESTAMP(post.published_at) AS lastmod,
post.category_slug, post.reading_time, post.ai_summary
FROM posts post
WHERE post.status = 'published'
AND post.is_sitemap_visible = 1
AND post.noindex != 1
ORDER BY post.published_at DESC
LIMIT 10000
");
$stmt->execute();
$data['posts'] = $stmt->fetchAll();
// Product pages with structured data signals
$stmt = $pdo->prepare("
SELECT
prod.id, prod.name AS title, prod.slug AS url, prod.short_description,
prod.main_image, prod.price, prod.currency, prod.availability,
prod.language, prod.target_markets,
UNIX_TIMESTAMP(prod.updated_at) AS lastmod,
prod.rating_value, prod.review_count, prod.sku
FROM products prod
WHERE prod.status = 'active'
AND prod.is_visible = 1
ORDER BY prod.priority DESC, prod.updated_at DESC
LIMIT 2000
");
$stmt->execute();
$data['products'] = $stmt->fetchAll();
// Category/taxonomy pages
$stmt = $pdo->query("
SELECT
cat.id, cat.name AS title, cat.slug AS url, cat.description,
cat.language, cat.parent_id,
UNIX_TIMESTAMP(cat.updated_at) AS lastmod,
COUNT(rel.object_id) AS item_count
FROM categories cat
LEFT JOIN category_relations rel ON rel.category_id = cat.id
WHERE cat.is_sitemap_visible = 1
GROUP BY cat.id
ORDER BY cat.priority DESC
LIMIT 500
");
$data['categories'] = $stmt->fetchAll();
return $data;
}
// =============================================================================
// URL & HREFLANG UTILITIES
// =============================================================================
function buildAbsoluteUrl(string $path, string $lang = DEFAULT_LANG): string {
$path = ltrim($path, '/');
if ($lang !== DEFAULT_LANG) {
return rtrim(SITE_URL, '/') . '/' . $lang . '/' . $path;
}
return rtrim(SITE_URL, '/') . '/' . $path;
}
function generateHreflangLinks(string $baseUrl, array $languages, string $defaultLang = DEFAULT_LANG): string {
$output = '';
foreach ($languages as $lang) {
$href = ($lang === $defaultLang)
? $baseUrl
: rtrim(SITE_URL, '/') . '/' . $lang . '/' . ltrim(parse_url($baseUrl, PHP_URL_PATH), '/');
$output .= ' ' . "\n";
}
$output .= ' ' . "\n";
return $output;
}
function getChangefreq(string $contentType, ?int $updateInterval = null): string {
$defaults = [
'homepage' => 'daily',
'product' => 'weekly',
'blog' => 'weekly',
'category' => 'monthly',
'static' => 'monthly',
'legal' => 'yearly',
];
return $defaults[$contentType] ?? 'monthly';
}
function calculatePriority(array $signals): float {
$base = 0.5;
if (!empty($signals['is_homepage'])) $base = 1.0;
elseif (!empty($signals['is_product'])) $base = 0.9;
elseif (!empty($signals['is_blog_featured'])) $base = 0.8;
elseif (!empty($signals['is_category'])) $base = 0.7;
if (!empty($signals['ai_relevance_score']) && $signals['ai_relevance_score'] > 0.8) {
$base = min(1.0, $base + 0.1);
}
if (!empty($signals['eeat_strength']) && $signals['eeat_strength'] === 'high') {
$base = min(1.0, $base + 0.05);
}
return round($base, 1);
}
// =============================================================================
// AI & 2027 RANKING SIGNALS
// =============================================================================
function generateAIRelevanceMetadata(array $page): array {
$metadata = [];
if (!empty($page['ai_relevance_score'])) {
$metadata['ai:relevance'] = number_format((float)$page['ai_relevance_score'], 2);
}
if (!empty($page['semantic_keywords'])) {
$keywords = is_string($page['semantic_keywords'])
? json_decode($page['semantic_keywords'], true)
: $page['semantic_keywords'];
if (is_array($keywords) && !empty($keywords)) {
$metadata['ai:keywords'] = implode(', ', array_slice($keywords, 0, 10));
}
}
if (!empty($page['eeat_signals'])) {
$eeat = is_string($page['eeat_signals'])
? json_decode($page['eeat_signals'], true)
: $page['eeat_signals'];
if (is_array($eeat)) {
if (!empty($eeat['expertise'])) $metadata['eeat:expertise'] = $eeat['expertise'];
if (!empty($eeat['authoritativeness'])) $metadata['eeat:authority'] = $eeat['authoritativeness'];
if (!empty($eeat['trustworthiness'])) $metadata['eeat:trust'] = $eeat['trustworthiness'];
}
}
if (!empty($page['reading_time'])) {
$metadata['content:readingTime'] = (int)$page['reading_time'] . 'min';
}
return $metadata;
}
function generateCoreWebVitalsHints(array $page): string {
$hints = [];
if (!empty($page['lcp_element'])) {
$hints[] = 'data-lcp="' . htmlspecialchars($page['lcp_element'], ENT_XML1) . '"';
}
if (!empty($page['cls_score']) && (float)$page['cls_score'] < 0.1) {
$hints[] = 'data-cls="good"';
}
if (!empty($page['fid_ms']) && (int)$page['fid_ms'] < 100) {
$hints[] = 'data-fid="good"';
}
return empty($hints) ? '' : ' ' . implode(' ', $hints);
}
// =============================================================================
// XML GENERATION FUNCTIONS
// =============================================================================
function generateSitemapHeader(): string {
return '' . "\n" .
'' . "\n";
}
function generateUrlEntry(array $page, string $contentType = 'page'): string {
$loc = buildAbsoluteUrl($page['url'], $page['language'] ?? DEFAULT_LANG);
$lastmod = !empty($page['lastmod']) ? gmdate('Y-m-d\TH:i:s+00:00', (int)$page['lastmod']) : gmdate('Y-m-d\TH:i:s+00:00');
$changefreq = $page['changefreq'] ?? getChangefreq($contentType);
$priority = $page['priority'] ?? calculatePriority($page);
$xml = ' ' . "\n";
$xml .= ' ' . htmlspecialchars($loc, ENT_XML1) . '' . "\n";
$xml .= ' ' . $lastmod . '' . "\n";
$xml .= ' ' . $changefreq . '' . "\n";
$xml .= ' ' . $priority . '' . "\n";
if (!empty($page['language'])) {
$languages = !empty($page['target_countries']) && is_string($page['target_countries'])
? array_merge([DEFAULT_LANG], array_diff(SUPPORTED_LANGUAGES, [$page['language']]))
: SUPPORTED_LANGUAGES;
$xml .= generateHreflangLinks($loc, $languages, $page['language']);
}
if (!empty($page['image']) || !empty($page['main_image'])) {
$imageUrl = !empty($page['image']) ? $page['image'] : $page['main_image'];
if (!str_starts_with($imageUrl, 'http')) {
$imageUrl = SITE_URL . ltrim($imageUrl, '/');
}
$xml .= ' ' . "\n";
$xml .= ' ' . htmlspecialchars($imageUrl, ENT_XML1) . '' . "\n";
$xml .= ' ' . htmlspecialchars($page['title'] ?? '', ENT_XML1) . '' . "\n";
if (!empty($page['description']) || !empty($page['meta_description']) || !empty($page['short_description'])) {
$desc = $page['description'] ?? $page['meta_description'] ?? $page['short_description'] ?? '';
$xml .= ' ' . htmlspecialchars(mb_substr($desc, 0, 255), ENT_XML1) . '' . "\n";
}
$xml .= ' ' . "\n";
}
if ($contentType === 'blog' || $contentType === 'news') {
$xml .= ' ' . "\n";
$xml .= ' ' . "\n";
$xml .= ' ' . SITE_NAME . '' . "\n";
$xml .= ' ' . ($page['language'] ?? DEFAULT_LANG) . '' . "\n";
$xml .= ' ' . "\n";
$xml .= ' ' . $lastmod . '' . "\n";
$xml .= ' ' . htmlspecialchars($page['title'], ENT_XML1) . '' . "\n";
$xml .= ' ' . "\n";
}
if (AI_CONTENT_SIGNAL && !empty($page['ai_relevance_score'])) {
$aiMeta = generateAIRelevanceMetadata($page);
foreach ($aiMeta as $attr => $value) {
$xml .= ' <' . $attr . '>' . htmlspecialchars($value, ENT_XML1) . '' . $attr . '>' . "\n";
}
}
$cwhints = generateCoreWebVitalsHints($page);
if ($cwhints) {
$xml .= ' ' . "\n";
}
$xml .= ' ' . "\n";
return $xml;
}
function generateSitemapIndex(array $sitemaps): string {
$xml = '' . "\n";
$xml .= '' . "\n";
$xml .= ' ' . $sitemap['lastmod'] . '' . "\n";
$xml .= ' ' . "\n";
}
$xml .= '' . "\n";
return $xml;
}
// =============================================================================
// MAIN GENERATION LOGIC
// =============================================================================
function generateXMLSitemap(array $data): string {
$xml = generateSitemapHeader();
$urlCount = 0;
$xml .= ' ' . "\n";
$xml .= ' ' . htmlspecialchars(SITE_URL, ENT_XML1) . '' . "\n";
$xml .= ' ' . gmdate('Y-m-d\TH:i:s+00:00') . '' . "\n";
$xml .= ' daily' . "\n";
$xml .= ' 1.0' . "\n";
$xml .= generateHreflangLinks(SITE_URL, SUPPORTED_LANGUAGES);
$xml .= ' ' . "\n";
$urlCount++;
foreach ($data['pages'] as $page) {
$xml .= generateUrlEntry($page, 'page');
$urlCount++;
}
foreach ($data['products'] as $product) {
$product['language'] = $product['language'] ?? DEFAULT_LANG;
$xml .= generateUrlEntry($product, 'product');
$urlCount++;
}
foreach ($data['posts'] as $post) {
$post['language'] = $post['language'] ?? DEFAULT_LANG;
$xml .= generateUrlEntry($post, 'blog');
$urlCount++;
}
foreach ($data['categories'] as $category) {
$category['language'] = $category['language'] ?? DEFAULT_LANG;
$category['priority'] = 0.7;
$category['changefreq'] = 'monthly';
$xml .= generateUrlEntry($category, 'category');
$urlCount++;
}
$xml .= '' . "\n";
logSitemapEvent('INFO', 'Sitemap generated', [
'url_count' => $urlCount,
'languages' => count(SUPPORTED_LANGUAGES),
'ai_signals' => AI_CONTENT_SIGNAL ? 'enabled' : 'disabled'
]);
return $xml;
}
function handleSitemapRequest(): void {
initCacheDir();
$forceRegenerate = isset($_GET['force']) && $_GET['force'] === 'true';
if (!$forceRegenerate) {
$cached = getCachedSitemap(CACHE_PATH, CACHE_TTL);
if ($cached) {
header('Content-Type: application/xml; charset=UTF-8');
header('Cache-Control: public, max-age=' . CACHE_TTL);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime(CACHE_PATH)) . ' GMT');
header('X-Sitemap-Type: xml');
header('X-Sitemap-Source: cache');
header('X-URL-Count: ' . substr_count($cached, ''));
echo $cached;
return;
}
}
$pdo = getDatabaseConnection();
if (!$pdo) {
http_response_code(503);
header('Content-Type: application/xml; charset=UTF-8');
echo '' . "\n";
echo '' . "\n";
echo ' DB_UNAVAILABLE' . "\n";
echo ' Database connection failed' . "\n";
echo ' ' . CACHE_TTL . '' . "\n";
echo '' . "\n";
return;
}
try {
$startTime = microtime(true);
$data = fetchPagesForSitemap($pdo);
$xml = generateXMLSitemap($data);
if (cacheSitemap(CACHE_PATH, $xml)) {
logSitemapEvent('INFO', 'Sitemap cached', [
'size_bytes' => strlen($xml),
'generation_time_ms' => round((microtime(true) - $startTime) * 1000, 2)
]);
}
header('Content-Type: application/xml; charset=UTF-8');
header('Cache-Control: public, max-age=' . CACHE_TTL);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('X-Sitemap-Type: xml');
header('X-Sitemap-Source: generated');
header('X-Generation-Time: ' . round((microtime(true) - $startTime) * 1000, 2) . 'ms');
header('X-URL-Count: ' . substr_count($xml, ''));
echo $xml;
} catch (Throwable $e) {
logSitemapEvent('CRITICAL', 'Sitemap generation failed', [
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
http_response_code(500);
header('Content-Type: application/xml; charset=UTF-8');
echo '' . "\n";
echo '' . "\n";
echo ' GENERATION_FAILED' . "\n";
echo ' ' . htmlspecialchars($e->getMessage(), ENT_XML1) . '' . "\n";
echo '' . "\n";
}
}
// =============================================================================
// EXECUTION
// =============================================================================
if (php_sapi_name() === 'cli') {
parse_str(implode('&', array_slice($argv, 1)), $_GET);
}
handleSitemapRequest();
exit;