各言語でRSSフィード

守矢諏訪子 <suwako@076.moe>、2026-11-11 11:23
コミット 8d4ccf4456963450c1da7bb83d892538ca1ec048 ・親 14ae23c ・ファイルを見る

9個のファイル、+75 -19

変更 README-JP.md
@@ -120,7 +120,7 @@
120120 location "/.well-known/webfinger" {
121121 request rewrite "/index.php"
122122 }
123- location "/blog.atom" {
123+ location match "*.atom" {
124124 request rewrite "/index.php"
125125 }
126126 }
@@ -189,6 +189,10 @@
189189 forward to <httpd> port 8080 check tcp
190190 }
191191 ```
192+
193+### ブログと Atom フィード
194+1つのサイトに複数のブログを置けます。`config/config.php` の `BLOGS` の各ディレクトリ(例:`blog`、`enblog`)がブログになり、それぞれ `/ディレクトリ.atom` の Atom フィードがあります(一覧と検索結果のページの上にリンクがあります)。\
195+`BLOGS` が無ければ、今まで通り `blog` だけです。
192196
193197 ### Cron
194198 定期的なタスク(例えば、新しいブログ投稿を ActivityPub のフォロワーに送る `apfederate`)はシステムの cron で実行します。\
変更 README.md
@@ -118,7 +118,7 @@
118118 location "/.well-known/webfinger" {
119119 request rewrite "/index.php"
120120 }
121- location "/blog.atom" {
121+ location match "*.atom" {
122122 request rewrite "/index.php"
123123 }
124124 }
@@ -187,6 +187,10 @@
187187 forward to <httpd> port 8080 check tcp
188188 }
189189 ```
190+
191+### Blogs and Atom feeds
192+A site can have more than one blog: every directory in `BLOGS` in `config/config.php` (for example `blog`, `enblog`) is a blog with its own Atom feed at `/directory.atom`, linked from the top of its index and search result pages.\
193+Without `BLOGS`, only `blog` exists, as before.
190194
191195 ### Cron
192196 Scheduled tasks (for example `apfederate`, which sends new blog posts to ActivityPub followers) are run by the system cron.\
@@ -43,6 +43,11 @@
4343 'tags' => 'game,tech,developer',
4444 'url' => '',
4545 ]);
46+// ブログ(URLのディレクトリ => 設定)。ブログ毎に「/ディレクトリ.atom」のフィードが出来る
47+// title:フィードの名前(省略するとSITEINFOのtitle)
48+define('BLOGS', [
49+ 'blog' => [],
50+]);
4651 define('MAILINFO', [
4752 'from' => 'hogehoge@ho.ge',
4853 'host' => 'smtp.ho.ge',
@@ -115,3 +115,14 @@
115115 text-align: center;
116116 }
117117 }
118+
119+.feed-link {
120+ text-align: right;
121+ margin: 0 0 10px;
122+}
123+
124+.feed-link a {
125+ display: inline-flex;
126+ align-items: center;
127+ gap: 6px;
128+}
変更 route.php
@@ -131,7 +131,9 @@
131131 }
132132
133133 if (ATOM_ENABLED) {
134- $routes[] = Route::add('GET', 'blog.atom', Atom::class.'@feed');
134+ foreach (array_keys(blogs()) as $blog) {
135+ $routes[] = Route::add('GET', $blog.'.atom', Atom::class.'@feed', ['section' => $blog]);
136+ }
135137 }
136138
137139 if (OPENPROVIDER_ENABLED) {
@@ -65,40 +65,51 @@
6565 */
6666 public function feed(array $params): void {
6767 try {
68+ $section = (string)($params['section'] ?? 'blog');
69+ $blogs = blogs();
70+ if (!isset($blogs[$section])) {
71+ header('HTTP/1.1 404 Not Found');
72+ exit;
73+ }
74+
6875 // 最新の投稿を取得
69- $posts = $this->getPosts('/blog/', null);
76+ $posts = $this->getPosts("/{$section}/", null);
77+
7078 // 最新の5件に制限
7179 $posts = array_slice($this->publicOnly($posts), 0, 5);
72-
80+
7381 // サイトのドメインを取得
7482 $domain = $_SERVER['HTTP_HOST'];
7583 $baseUrl = 'https://'.$domain;
76-
84+
85+ // 「blog」のフィードのIDとリンクは今まで通り(IDが変わると購読者のリーダーで全部新しい記事になる)
86+ $home = $section === 'blog' ? $baseUrl : "{$baseUrl}/{$section}";
87+
7788 // 現在の日時(RFC3339形式)
7889 $published = date('c');
79-
90+
8091 // XMLヘッダーとコンテンツタイプを設定
8192 header('Content-Type: application/atom+xml; charset=utf-8');
82-
93+
8394 // Atomフィードの開始部分
8495 echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
8596 echo '<feed xmlns="http://www.w3.org/2005/Atom">'."\n";
86-
97+
8798 // フィードの基本情報
88- echo ' <title>'.SITEINFO['title'].'</title>'."\n";
89- echo ' <link href="'.$baseUrl.'" />'."\n";
90- echo ' <link href="'.$baseUrl.'/blog.atom" rel="self" />'."\n";
91- echo ' <id>'.$baseUrl.'/</id>'."\n";
99+ echo ' <title>'.htmlspecialchars($blogs[$section]['title'], ENT_XML1, 'UTF-8').'</title>'."\n";
100+ echo ' <link href="'.$home.'" />'."\n";
101+ echo ' <link href="'.$baseUrl.'/'.$section.'.atom" rel="self" />'."\n";
102+ echo ' <id>'.$home.'/</id>'."\n";
92103 echo ' <published>'.$published.'</published>'."\n";
93104 echo ' <updated>'.$published.'</updated>'."\n";
94105 echo ' <author>'."\n";
95106 echo ' <name>'.SITEINFO['title'].'</name>'."\n";
96107 echo ' </author>'."\n";
97-
108+
98109 // 各エントリー(記事)
99110 foreach ($posts as $post) {
100111 // 記事の本文を取得(プレーンテキスト)
101- $path = ROOT.'/blog/'.$post['slug'].'.md';
112+ $path = ROOT."/{$section}/".$post['slug'].'.md';
102113 $content = '';
103114 $postPublished = date('c', strtotime($post['date']));
104115
@@ -107,7 +118,7 @@
107118 $parts = explode('----', $fileContent, 2);
108119 if (count($parts) > 1) {
109120 // 本文をHTMLとして準備
110- $md = new Markdown($post['slug'], '/blog/');
121+ $md = new Markdown($post['slug'], "/{$section}/");
111122 $content = $md->parse();
112123 // HTMLタグを取り除かないようにCDATAで囲む
113124 $content = '<![CDATA['.$content.']]>';
@@ -116,8 +127,8 @@
116127
117128 echo ' <entry>'."\n";
118129 echo ' <title>'.htmlspecialchars($post['title']).'</title>'."\n";
119- echo ' <link href="'.$baseUrl.'/blog/'.$post['slug'].'" />'."\n";
120- echo ' <id>'.$baseUrl.'/blog/'.$post['slug'].'</id>'."\n";
130+ echo ' <link href="'.$baseUrl.'/'.$section.'/'.$post['slug'].'" />'."\n";
131+ echo ' <id>'.$baseUrl.'/'.$section.'/'.$post['slug'].'</id>'."\n";
121132 echo ' <published>'.$postPublished.'</published>'."\n";
122133
123134 // カテゴリ(タグ)
変更 util.php
@@ -369,6 +369,17 @@
369369 return false;
370370 }
371371
372+/**
373+ * ブログの一覧(config.phpのBLOGS、無ければ「blog」だけ)
374+ *
375+ * @return array ディレクトリ名 => ['title' => フィードの名前]
376+ */
377+function blogs(): array {
378+ $blogs = defined('BLOGS') ? BLOGS : ['blog' => []];
379+ foreach ($blogs as $name => $b) $blogs[$name] = $b + ['title' => SITEINFO['title']];
380+ return $blogs;
381+}
382+
372383 function getcookie(string $name): string|null {
373384 return $_COOKIE[$name] ?? null;
374385 }
@@ -50,7 +50,8 @@
5050 {@ endif @}
5151 {@ endif @}
5252 {@ if (ATOM_ENABLED) @}
53- <link rel="alternate" type="application/atom+xml" title="{{ SITEINFO['title'] }} feed" href="/blog.atom" />
53+{$ $feed = isset($section) && array_key_exists($section, blogs()) ? $section : array_key_first(blogs()) $}
54+ <link rel="alternate" type="application/atom+xml" title="{{ blogs()[$feed]['title'] }} feed" href="/{{ $feed }}.atom" />
5455 {@ endif @}
5556 </head>
5657 <body>
@@ -1,5 +1,12 @@
11 {@ include(common/header) @}
22 {@ include(common/search) @}
3+{@ if (ATOM_ENABLED && array_key_exists($section, blogs())) @}
4+ <div class="feed-link">
5+ <a href="/{{ $section }}.atom" title="{{ blogs()[$section]['title'] }}">
6+ <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="2.5" cy="13.5" r="2" fill="currentColor" /><path d="M0.5 6.5a9 9 0 0 1 9 9h-2.5a6.5 6.5 0 0 0-6.5-6.5zM0.5 1a14.5 14.5 0 0 1 14.5 14.5h-2.5a12 12 0 0 0-12-12z" fill="currentColor" /></svg> フィード
7+ </a>
8+ </div>
9+{@ endif @}
310
411 <div class="news-grid">
512 {@ foreach ($posts as $post) @}