部落格、新聞站、Podcast 需對外提供 Feed。本文示範使用 RSSBuilder 類別產生 RSS 2.0,並擴充支援 Atom 1.0、iTunes、Media RSS 等命名空間。
<?php
require_once 'RSSBuilder.php'; // 假設已有此類別
$rss = new RSSBuilder('UTF-8', 'https://example.com', '我的部落格', '技術分享與生活感悟', 'https://example.com/logo.png');
$rss->addDCData('', 'https://example.com', date('r'));
// 文章資料 (通常來自資料庫)
$posts = [
['id' => 1, 'title' => 'PHP 8.2 新特性', 'url' => 'https://example.com/post/1', 'desc' => '效能提升...', 'date' => '2024-01-15'],
['id' => 2, 'title' => 'Laravel 11 遷移指南', 'url' => 'https://example.com/post/2', 'desc' => '破壞性變更...', 'date' => '2024-01-10'],
];
foreach ($posts as $post) {
$rss->addItem(
$post['url'],
$post['title'],
$post['url'] . '?from=rss',
$post['desc'],
$post['title'],
$post['date']
);
}
header('Content-Type: application/xml; charset=UTF-8');
header('Cache-Control: public, max-age=3600');
$rss->outputRSS();
class RSSBuilder {
private $encoding, $link, $title, $description, $imageUrl;
private $items = [];
private $namespaces = [];
public function __construct($encoding, $link, $title, $description, $imageUrl = '') {
$this->encoding = $encoding;
$this->link = $link;
$this->title = $title;
$this->description = $description;
$this->imageUrl = $imageUrl;
}
public function addDCData($creator, $link, $date) {
$this->namespaces['dc'] = 'https://purl.org/dc/elements/1.1/';
$this->dcData = compact('creator', 'link', 'date');
}
public function addItem($link, $title, $guid, $description, $author, $pubDate) {
$this->items[] = compact('link', 'title', 'guid', 'description', 'author', 'pubDate');
}
public function addNamespace(string $prefix, string $uri) {
$this->namespaces[$prefix] = $uri;
}
public function outputRSS() {
$xml = new XMLWriter();
$xml->openMemory();
$xml->setIndent(true);
$xml->startDocument('1.0', $this->encoding);
$xml->startElement('rss');
$xml->writeAttribute('version', '2.0');
$xml->writeAttribute('xmlns:dc', 'https://purl.org/dc/elements/1.1/');
$xml->writeAttribute('xmlns:atom', 'https://www.w3.org/2005/Atom');
foreach ($this->namespaces as $prefix => $uri) {
$xml->writeAttribute("xmlns:$prefix", $uri);
}
$xml->startElement('channel');
$xml->writeElement('title', $this->title);
$xml->writeElement('link', $this->link);
$xml->writeElement('description', $this->description);
$xml->writeElement('language', 'zh-TW');
$xml->writeElement('lastBuildDate', date('r'));
$xml->writeElement('atom:link', ['href' => $this->link . '/feed', 'rel' => 'self', 'type' => 'application/rss+xml']);
if ($this->imageUrl) {
$xml->startElement('image');
$xml->writeElement('url', $this->imageUrl);
$xml->writeElement('title', $this->title);
$xml->writeElement('link', $this->link);
$xml->endElement();
}
if (!empty($this->dcData)) {
$xml->writeElement('dc:creator', $this->dcData['creator']);
$xml->writeElement('dc:date', $this->dcData['date']);
}
foreach ($this->items as $item) {
$xml->startElement('item');
$xml->writeElement('title', $item['title']);
$xml->writeElement('link', $item['link']);
$xml->writeElement('guid', ['isPermaLink' => 'true'], $item['guid']);
$xml->writeElement('description', $item['description']);
$xml->writeElement('dc:creator', $item['author']);
$xml->writeElement('pubDate', date('r', strtotime($item['pubDate'])));
$xml->endElement(); // item
}
$xml->endElement(); // channel
$xml->endElement(); // rss
echo $xml->outputMemory(true);
}
}
$rss->addNamespace('itunes', 'https://www.itunes.com/dtds/podcast-1.0.dtd');
$rss->addNamespace('content', 'https://purl.org/rss/1.0/modules/content/');
// 在 channel 內加入
$xml->writeElement('itunes:author', '作者名');
$xml->writeElement('itunes:summary', 'Podcast 簡介');
$xml->writeElement('itunes:type', 'episodic');
$xml->writeElement('itunes:owner', [
'itunes:name' => '擁有者',
'itunes:email' => 'email@example.com',
]);
$xml->writeElement('itunes:explicit', 'false');
$xml->writeElement('itunes:category', ['text' => 'Technology']);
$xml->writeElement('itunes:image', ['href' => 'https://example.com/cover.jpg']);
// 在 item 內加入
$xml->writeElement('itunes:title', '單集標題');
$xml->writeElement('itunes:summary', '單集簡介');
$xml->writeElement('itunes:duration', '45:30');
$xml->writeElement('itunes:explicit', 'false');
$xml->writeElement('enclosure', [
'url' => 'https://example.com/ep1.mp3',
'type' => 'audio/mpeg',
'length' => '12345678',
]);
$xml->writeElement('content:encoded', '');
function outputAtom($posts, $siteUrl, $title) {
$xml = new XMLWriter();
$xml->openMemory();
$xml->startDocument('1.0', 'UTF-8');
$xml->startElement('feed');
$xml->writeAttribute('xmlns', 'https://www.w3.org/2005/Atom');
$xml->writeElement('title', $title);
$xml->writeElement('link', ['href' => $siteUrl, 'rel' => 'alternate']);
$xml->writeElement('link', ['href' => $siteUrl . '/atom', 'rel' => 'self']);
$xml->writeElement('updated', date('c'));
$xml->writeElement('id', $siteUrl);
$xml->writeElement('author', ['name' => '作者']);
foreach ($posts as $post) {
$xml->startElement('entry');
$xml->writeElement('title', $post['title']);
$xml->writeElement('link', ['href' => $post['url'], 'rel' => 'alternate']);
$xml->writeElement('id', $post['url']);
$xml->writeElement('updated', date('c', strtotime($post['date'])));
$xml->writeElement('summary', $post['desc']);
$xml->writeElement('content', ['type' => 'html'], $post['content'] ?? $post['desc']);
$xml->endElement();
}
$xml->endElement();
header('Content-Type: application/atom+xml; charset=UTF-8');
echo $xml->outputMemory(true);
}
feed.xml、atom.xml 放入 public/,Nginx 直接服務靜態檔。Cache-Control: public, max-age=3600、ETag、Last-Modified,支援條件請求。?page=2 或依標準 rel="next" 連結。