Content Registry

Generate a manifest, RSS feed, and sitemap alongside your Markdown.

Converting posts gives you Markdown files, but a site usually needs an index of them too — to build a listing page, a feed, or a sitemap. The content registry is that index: medmark records every post it writes and emits it in the formats you ask for.

The registry is on by default and produces a registry.json in your output folder.

Configuring it

The registry is configured in your template's getOptions():

medmark/templates/my-template.js
module.exports = {
  getOptions() {
    return {
      registry: {
        enabled: true,
        filename: 'registry',
        formats: ['json', 'ts', 'rss', 'sitemap'],
        includeDrafts: false,
        site: {
          url: 'https://example.com',
          title: 'My Blog',
          description: 'Posts about things.',
          language: 'en',
        },
      },
    };
  },

  // ...getOptions is the only place registry settings live
  render(data) {
    /* ... */
  },
};

Options

OptionTypeDefaultDescription
enabledbooleantrueSet to false to skip registry generation entirely
filenamestring'registry'Base filename (no extension) for the json and ts outputs
formats('json' | 'ts' | 'rss' | 'sitemap')[]['json']Which files to emit
includeDraftsbooleanfalseInclude drafts in the RSS feed and sitemap
siteobject—Site metadata; required for rss and sitemap

site metadata

FieldTypeDescription
urlstringAbsolute base URL of the site. Required by rss and sitemap
titlestringChannel title, used by rss
descriptionstringChannel description, used by rss
languagestringChannel language (e.g. en), used by rss

rss and sitemap need site.url

If site.url is missing, medmark logs a warning and skips those two formats. The rest of the run is unaffected.

Output formats

json → registry.json

A machine-readable manifest. The shape is versioned via the top-level version field.

medmark/output/registry.json
{
  "version": 1,
  "generator": "medmark@0.3.0",
  "generatedAt": "2026-01-14T09:21:04.512Z",
  "count": 2,
  "posts": [
    {
      "slug": "my-first-post",
      "title": "My First Post",
      "description": "A short summary.",
      "date": "2023-04-02",
      "path": "my-first-post/index.md",
      "draft": false,
      "readingTime": "5 min read",
      "authors": ["Brion Mario"],
      "categories": [],
      "tags": ["javascript", "markdown"],
      "bannerImage": "https://cdn-images-1.medium.com/max/1024/...",
      "source": {
        "provider": "medium",
        "url": "https://medium.com/@brionmario/my-first-post-abc123"
      }
    }
  ]
}

ts → registry.ts

The same entries as a typed ESM module, so TypeScript consumers get completions and type errors for free. The default export is the array of entries; the run metadata is exported alongside it:

import registry, { generatedAt, generator } from './medmark/output/registry';
// registry: MedmarkRegistryEntry[]

registry.map(post => post.slug);

The MedmarkRegistryEntry interface is inlined into the generated file, so it has no import-time dependency on the medmark package.

rss → feed.xml

An RSS 2.0 feed built from site plus every non-draft post. Drafts are included only when includeDrafts is true.

sitemap → sitemap.xml

A sitemap of every post URL, resolved against site.url. Each post's slug is appended to the base URL, so a site.url of https://example.com and a slug of my-first-post yields https://example.com/my-first-post.

Registry entry fields

Posts are sorted newest-first by date. Each entry has this shape:

FieldTypeDescription
slugstringThe post's slug
titlestringThe post title
pathstringPath to the generated file, relative to the output root
datestringPublish date as YYYY-MM-DD
descriptionstringPost description
draftbooleanWhether the post is a draft
readingTimestringReading time text
authorsstring[]Author display names
categoriesstring[]Post categories
tagsstring[]Post tags
bannerImagestringFirst image found in the post
source{provider, url}Where the post came from (provider is medium)

Customising the registry

Two optional template hooks let you take over either half of the process.

buildRegistryEntry(data, context)

Called once per generated post. Return a modified entry, or null to leave the post out of the registry.

const { buildDefaultRegistryEntry } = require('medmark');

module.exports = {
  buildRegistryEntry(data, context) {
    // Skip anything that isn't tagged.
    if (!data.tags?.length) return null;

    return {
      ...buildDefaultRegistryEntry(data, context),
      featured: data.tags.includes('featured'),
    };
  },
  // ...
};

context carries:

FieldTypeDescription
optionsMedmarkOptionsThe resolved template options for the run
outputRootstringThe output root directory
relativePathstringPath to the generated file, relative to outputRoot
slugstringThe post's slug

serializeRegistry(registry, context)

Called once at the end of the run with the aggregated registry. Return the list of files to write — this bypasses formats entirely, so you control the output completely.

module.exports = {
  serializeRegistry(registry) {
    return [
      {
        filename: 'posts.js',
        content: `export const posts = ${JSON.stringify(registry.posts, null, 2)};\n`,
      },
    ];
  },
  // ...
};

Each returned file is {filename, content}, written relative to the output root.

Turning it off

getOptions() {
  return {
    registry: { enabled: false },
  };
}

On this page