完整 Sitemap 產生器類別
<?php
class SitemapGenerator {
private $filePath;
private $baseUrl;
private $cacheHours = 24;
private $maxUrls = 50000;
public function __construct(string $baseUrl, string $storagePath, int $cacheHours = 24) {
$this->baseUrl = rtrim($baseUrl, '/');
$this->filePath = rtrim($storagePath, '/') . '/sitemap.xml';
$this->cacheHours = $cacheHours;
}
public function generate(): string {
if (!$this->isExpired()) {
return file_get_contents($this->filePath);
}
$urls = $this->collectUrls();
$xml = $this->buildXml($urls);
$this->writeFile($xml);
return $xml;
}
private function isExpired(): bool {
if (!file_exists($this->filePath)) return true;
return (time() - filemtime($this->filePath)) > ($this->cacheHours * 3600);
}
private function collectUrls(): array {
$urls = [];
$now = date('Y-m-d');
// 首頁
$urls[] = [
'loc' => $this->baseUrl . '/',
'lastmod' => $now,
'changefreq' => 'daily',
'priority' => '1.0',
];
// 動態內容:文章、分類、商品等(範例使用 PDO)
$pdo = $this->getPdo();
$stmt = $pdo->query("
SELECT 'post' as type, slug, updated_at FROM posts WHERE status='published'
UNION ALL
SELECT 'category', slug, updated_at FROM categories
UNION ALL
SELECT 'product', sku, updated_at FROM products WHERE status=1
LIMIT {$this->maxUrls}
");
foreach ($stmt as $row) {
$config = match($row['type']) {
'post' => ['path' => '/post/', 'freq' => 'weekly', 'pri' => '0.7'],
'category' => ['path' => '/category/', 'freq' => 'weekly', 'pri' => '0.8'],
'product' => ['path' => '/product/', 'freq' => 'daily', 'pri' => '0.6'],
default => ['path' => '/', 'freq' => 'monthly', 'pri' => '0.5'],
};
$urls[] = [
'loc' => $this->baseUrl . $config['path'] . $row['slug'],
'lastmod' => date('Y-m-d', strtotime($row['updated_at'])),
'changefreq' => $config['freq'],
'priority' => $config['pri'],
];
}
return $urls;
}
private function buildXml(array $urls): string {
$xml = '' . "\n";
$xml .= '' . "\n";
foreach ($urls as $url) {
$xml .= " \n";
$xml .= " " . htmlspecialchars($url['loc'], ENT_QUOTES, 'UTF-8') . "\n";
$xml .= " {$url['lastmod']}\n";
$xml .= " {$url['changefreq']}\n";
$xml .= " {$url['priority']}\n";
$xml .= " \n";
}
$xml .= '';
return $xml;
}
private function writeFile(string $xml): void {
$dir = dirname($this->filePath);
if (!is_dir($dir)) mkdir($dir, 0755, true);
file_put_contents($this->filePath, $xml);
// 可選:設定檔案權限
chmod($this->filePath, 0644);
}
private function getPdo(): PDO {
// 實際專案請從 DI 容器或設定檔取得
return new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
}
}
// 使用
$generator = new SitemapGenerator('https://example.com', __DIR__ . '/public');
echo $generator->generate();
增量更新策略(大型網站)
<?php
// 僅收集最近變更的 URL
private function collectUrls(): array {
$since = date('Y-m-d H:i:s', time() - $this->cacheHours * 3600);
$stmt = $pdo->prepare("
SELECT slug, updated_at FROM posts WHERE status='published' AND updated_at > ?
UNION ALL SELECT slug, updated_at FROM products WHERE status=1 AND updated_at > ?
");
$stmt->execute([$since, $since]);
// ... 組合 URL
// 合併現有 sitemap 中未過期的 URL
if (file_exists($this->filePath)) {
$existing = $this->parseExistingSitemap();
$urls = array_merge($existing, $newUrls);
}
return $urls;
}
超過 50,000 URL:自動分割 Sitemap Index
<?php
public function generateIndex(): string {
$allUrls = $this->collectUrls();
$chunks = array_chunk($allUrls, $this->maxUrls);
$indexXml = '' . "\n";
$indexXml .= '' . "\n";
foreach ($chunks as $i => $chunk) {
$filename = "sitemap-" . ($i + 1) . ".xml";
$this->writeFile($this->buildXml($chunk), $filename);
$indexXml .= " \n";
$indexXml .= " " . $this->baseUrl . "/$filename\n";
$indexXml .= " " . date('Y-m-d') . "\n";
$indexXml .= " \n";
}
$indexXml .= '';
file_put_contents(dirname($this->filePath) . '/sitemap.xml', $indexXml);
return $indexXml;
}
部署建議
- 設定 Cron Job 每小時/每天執行產生
- Nginx 直接服務靜態
sitemap.xml,不經 PHP
- robots.txt 加入
Sitemap: https://example.com/sitemap.xml
- Google Search Console 定期提交/驗證