diff --git a/docs/site/concepts/routing.md b/docs/site/concepts/routing.md
index c9cfb25..ee55efd 100644
--- a/docs/site/concepts/routing.md
+++ b/docs/site/concepts/routing.md
@@ -24,6 +24,7 @@ docs/site/
- skill homepage -> `/skill-name/` using `SKILL.md` when neither `index.md` nor `README.md` exists
- `guides/getting-started.md` -> `/guides/getting-started.md`, `/guides/getting-started.html`, `/guides/getting-started`
- sitemap -> `/sitemap.xml`
+- RSS feed -> `/feed.xml` when `siteUrl` is configured and RSS is not disabled
If a directory has no `index.md`, the current runtime can still render a minimal fallback listing for browsing.
@@ -31,6 +32,8 @@ The content tree may also include directory symlinks. `mdorigin` follows them in
`/sitemap.xml` emits canonical HTML URLs, not `.md` source URLs. It requires `siteUrl` so the sitemap can use absolute locations.
+`/feed.xml` also requires `siteUrl`, because feed items use absolute canonical URLs.
+
Rendered HTML also exposes the source markdown path with:
```html
@@ -39,6 +42,12 @@ Rendered HTML also exposes the source markdown path with:
This is a lightweight interoperability hint for agents and tools that want to discover the raw markdown source from the human HTML page.
+When RSS is enabled, rendered HTML also exposes feed autodiscovery:
+
+```html
+
+```
+
## Canonical markdown paths
Directory homepages support `index.md`, `README.md`, and `SKILL.md`, but only one can act as the effective source file for a given directory.
diff --git a/docs/site/guides/getting-started.md b/docs/site/guides/getting-started.md
index 4cc2d51..4d943a4 100644
--- a/docs/site/guides/getting-started.md
+++ b/docs/site/guides/getting-started.md
@@ -185,6 +185,31 @@ The built-in presentation is now fixed to the default atlas baseline. Configure
}
```
+Once `siteUrl` is set, `mdorigin` also enables:
+
+- `/sitemap.xml`
+- `/feed.xml`
+
+If you want to turn RSS off or override feed metadata, add:
+
+```json
+{
+ "rss": {
+ "title": "Example Feed",
+ "description": "Latest updates from Example",
+ "maxItems": 20
+ }
+}
+```
+
+Or disable it entirely:
+
+```json
+{
+ "rss": false
+}
+```
+
If a page contains a managed index block, the default renderer automatically presents it as a structured listing. `mdorigin` no longer exposes built-in theme/template variants as product configuration.
If you want to start using code-based extensions now, switch from JSON config to `mdorigin.config.ts` and export a config object with `plugins`.
diff --git a/docs/site/reference/configuration.md b/docs/site/reference/configuration.md
index c7942e5..7648980 100644
--- a/docs/site/reference/configuration.md
+++ b/docs/site/reference/configuration.md
@@ -118,6 +118,7 @@ The design boundary is:
- `siteUrl` sets the canonical site origin and is used for canonical links in rendered HTML.
- `siteUrl` also enables `/sitemap.xml`, which emits absolute canonical URLs.
+- `siteUrl` also enables `/feed.xml` by default unless RSS is explicitly disabled.
- `favicon` adds a standard favicon link tag.
- `socialImage` emits absolute `og:image` and `twitter:image` metadata when `siteUrl` is set.
- `logo` renders a small site logo in the header.
@@ -136,6 +137,37 @@ Example:
}
```
+## RSS
+
+`mdorigin` can emit a built-in RSS feed at `/feed.xml`.
+
+Rules:
+
+- if `siteUrl` is set, RSS is enabled by default
+- set `"rss": false` to disable the feed
+- the feed emits dated post content, not every page in the tree
+- rendered HTML adds an RSS autodiscovery `` when the feed is enabled
+
+Optional overrides:
+
+```json
+{
+ "rss": {
+ "title": "Example Feed",
+ "description": "Latest updates from Example",
+ "author": "editor@example.com",
+ "maxItems": 20
+ }
+}
+```
+
+Supported fields:
+
+- `rss.title`
+- `rss.description`
+- `rss.author`
+- `rss.maxItems`
+
## Footer
`mdorigin` supports a small set of explicit footer settings:
diff --git a/skills/mdorigin/SKILL.md b/skills/mdorigin/SKILL.md
index 852eeaf..73c2900 100644
--- a/skills/mdorigin/SKILL.md
+++ b/skills/mdorigin/SKILL.md
@@ -29,6 +29,7 @@ npm install --save-dev mdorigin
- Cloudflare Worker bundle output with `mdorigin build cloudflare`
- external binary deployment flow for Cloudflare Assets + R2
- markdown and HTML route behavior, including `Accept: text/markdown`
+- built-in `/sitemap.xml` and `/feed.xml` when `siteUrl` is configured
## Quick commands
diff --git a/src/core/request-handler.test.ts b/src/core/request-handler.test.ts
index 1474cd1..306bea1 100644
--- a/src/core/request-handler.test.ts
+++ b/src/core/request-handler.test.ts
@@ -446,6 +446,113 @@ test('handleSiteRequest returns an error for sitemap.xml when siteUrl is missing
assert.match(String(response.body), /siteUrl/);
});
+test('handleSiteRequest renders feed.xml for dated posts and adds autodiscovery link', async () => {
+ const store = new MemoryContentStore([
+ {
+ path: 'README.md',
+ kind: 'text',
+ mediaType: 'text/markdown; charset=utf-8',
+ text: ['---', 'title: Home', 'summary: Site summary', '---', '', '# Home'].join('\n'),
+ },
+ {
+ path: 'posts/new.md',
+ kind: 'text',
+ mediaType: 'text/markdown; charset=utf-8',
+ text: [
+ '---',
+ 'title: New Post',
+ 'date: 2026-03-22',
+ 'summary: Fresh summary',
+ '---',
+ '',
+ '# New Post',
+ ].join('\n'),
+ },
+ {
+ path: 'posts/old.md',
+ kind: 'text',
+ mediaType: 'text/markdown; charset=utf-8',
+ text: [
+ '---',
+ 'title: Old Post',
+ 'date: 2026-03-20',
+ '---',
+ '',
+ 'First paragraph excerpt.',
+ ].join('\n'),
+ },
+ {
+ path: 'guides/page.md',
+ kind: 'text',
+ mediaType: 'text/markdown; charset=utf-8',
+ text: ['---', 'title: Guide', 'date: 2026-03-21', 'type: page', '---', '', '# Guide'].join('\n'),
+ },
+ {
+ path: 'draft.md',
+ kind: 'text',
+ mediaType: 'text/markdown; charset=utf-8',
+ text: ['---', 'title: Draft', 'date: 2026-03-23', 'draft: true', '---', '', '# Draft'].join('\n'),
+ },
+ ]);
+
+ const siteConfig = {
+ ...TEST_SITE_CONFIG,
+ siteUrl: 'https://example.com',
+ siteDescription: 'Configured site description',
+ };
+
+ const feedResponse = await handleSiteRequest(store, '/feed.xml', {
+ draftMode: 'exclude',
+ siteConfig,
+ });
+
+ assert.equal(feedResponse.status, 200);
+ assert.equal(feedResponse.headers['content-type'], 'application/rss+xml; charset=utf-8');
+ const feedBody = String(feedResponse.body);
+ assert.match(feedBody, /