What it does

scriptor-markdown-feed serves a syndication feed straight from a directory of dated markdown files. Point it at a news/, blog/, or releases/ track and it answers /news/feed.xml by scanning those files, parsing their frontmatter, and emitting Atom 1.0 (or RSS 2.0) on every request. No build step, no cache, no database table: add an entry, push, and the next poll sees it.

It is the companion to scriptor-markdown-pages: the same .md files that render as pages also back the feed, and the entry links the feed generates match how markdown-pages resolves those files. This site uses it for /news/feed.xml.

A feed is not page-shaped, so the plugin does not hook the page resolver. Instead it activates from one guard line in the theme, the same model as scriptor-simple-router.

Install

Host install. Not on Packagist, so register the VCS repo, then require:

composer config repositories.scriptor-markdown-feed \
  vcs https://github.com/bigin/scriptor-markdown-feed
composer require bigins/scriptor-markdown-feed:^0.1

Docker. Bake it in with the two build args (typically alongside markdown-pages, since it reuses that plugin's content root):

services:
  scriptor:
    build:
      args:
        SCRIPTOR_PLUGIN_REPOS: "https://github.com/bigin/scriptor-markdown-pages https://github.com/bigin/scriptor-markdown-feed"
        SCRIPTOR_PLUGINS:      "bigins/scriptor-markdown-pages:^0.1 bigins/scriptor-markdown-feed:^0.1"

Then activate it from one guard line at the top of your theme's _ext.php, before the theme is built:

if (\Bigins\ScriptorMarkdownFeed\Feed::handle($config)) {
    return;
}

Feed::handle($config) returns true only when the request path matches a configured feed (it has emitted the XML and the caller returns); every other request returns false and the page flow runs untouched.

Configure

Under plugins.markdown_feed in data/settings/custom.scriptor-config.php:

return [
    'plugins' => [
        'markdown_feed' => [
            // Optional. Falls back to plugins.markdown_pages.content_root.
            'content_root' => '/var/www/scriptor/themes/info/content',
            'feeds' => [
                [
                    'track'  => 'news',           // subdirectory of content_root
                    'path'   => '/news/feed.xml', // URL the feed answers on
                    'title'  => 'Scriptor News',  // channel title
                    'max'    => 20,               // newest N (default 20)
                    'format' => 'atom',           // 'atom' (default) or 'rss'
                ],
            ],
        ],
    ],
];
Key Default Effect
content_root plugins.markdown_pages.content_root Where the tracks live.
feeds[].track required Subdirectory of content_root to scan.
feeds[].path required Exact request path the feed answers on.
feeds[].title the track name Channel <title>.
feeds[].max 20 Cap on entries, newest first.
feeds[].format atom atom (Atom 1.0) or rss (RSS 2.0).

Multiple feeds are allowed side by side. A feed entry missing track or path is dropped rather than fatalling the request.

Use

Each .md in the track becomes one entry. _index.md is skipped (it is the landing page, not an entry). The entry slug is the filename without .md, and the link is <siteUrl>/<track>/<slug>/, matching markdown-pages. Sort is by date: frontmatter, newest first, ties on title; a file without a parseable date falls back to its mtime.

---
title: "Developer Guide is content-complete"
date: 2026-05-25
summary: "All four layers of the Developer Guide are now live."
---

# Developer Guide is content-complete

Body markdown…

Keep filenames within [a-z0-9_-]. When the feed is paired with markdown-pages, that plugin sanitises every URL segment to [a-z0-9_-], so a dot in the filename produces a feed link that resolves to a 404. Use 2026-05-22-release-v0-1-7.md, not …v0.1.7.md.