Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/site/concepts/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ 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.

The content tree may also include directory symlinks. `mdorigin` follows them in local preview and build-time processing, while keeping the published URL based on the visible path inside the content root.

`/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
Expand All @@ -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
<link rel="alternate" type="application/rss+xml" href="https://example.com/feed.xml">
```

## 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.
Expand Down
25 changes: 25 additions & 0 deletions docs/site/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
32 changes: 32 additions & 0 deletions docs/site/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 `<link rel="alternate" ...>` 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:
Expand Down
1 change: 1 addition & 0 deletions skills/mdorigin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
107 changes: 107 additions & 0 deletions src/core/request-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<title>Test Site<\/title>/);
assert.match(feedBody, /<link>https:\/\/example\.com<\/link>/);
assert.match(feedBody, /<atom:link href="https:\/\/example\.com\/feed\.xml"/);
assert.match(feedBody, /<title>New Post<\/title>[\s\S]*<title>Old Post<\/title>/);
assert.match(feedBody, /<guid isPermaLink="true">https:\/\/example\.com\/posts\/new<\/guid>/);
assert.match(feedBody, /<description>Fresh summary<\/description>/);
assert.match(feedBody, /<description>First paragraph excerpt\.<\/description>/);
assert.doesNotMatch(feedBody, /Guide/);
assert.doesNotMatch(feedBody, /Draft/);

const htmlResponse = await handleSiteRequest(store, '/posts/new.html', {
draftMode: 'exclude',
siteConfig,
});
assert.equal(htmlResponse.status, 200);
assert.match(
String(htmlResponse.body),
/<link rel="alternate" type="application\/rss\+xml" title="Test Site" href="https:\/\/example\.com\/feed\.xml">/,
);
});

test('handleSiteRequest returns 404 for feed.xml when siteUrl is missing or rss is disabled', async () => {
const store = new MemoryContentStore([]);

const missingSiteUrl = await handleSiteRequest(store, '/feed.xml', {
draftMode: 'exclude',
siteConfig: TEST_SITE_CONFIG,
});
assert.equal(missingSiteUrl.status, 404);

const disabledRss = await handleSiteRequest(store, '/feed.xml', {
draftMode: 'exclude',
siteConfig: {
...TEST_SITE_CONFIG,
siteUrl: 'https://example.com',
rss: {
enabled: false,
maxItems: 20,
},
},
});
assert.equal(disabledRss.status, 404);
});

test('handleSiteRequest serves OpenAPI schema for search', async () => {
const response = await handleSiteRequest(
new MemoryContentStore([]),
Expand Down
Loading