Templates

Customise the Markdown output shape with a template file.

A template is a plain CommonJS .js file that controls:

  1. Options — how medmark processes each post (image storage, code language default, folder strategy, registry output)
  2. Render — how each post is written to disk (front matter shape, body)
  3. Registry — optionally, what gets recorded per post and how the registry is serialized

Built-in default

If you don't provide a template, medmark uses a sensible built-in default that generates standard YAML front matter and a registry.json manifest.

Starter template

medmark init creates medmark/templates/sample-medmark-template.js:

medmark/templates/sample-medmark-template.js
const { frontMatterToYaml, buildDefaultRegistryEntry, serializeDefaultRegistry } = require('medmark');

module.exports = {
  getOptions() {
    return {
      defaultCodeBlockLanguage: 'js',
      folderForEachSlug: true,
      imagePath: '/resources',
      imageStorageStrategy: 'REMOTE',
      // Generate a machine-readable index of every post that was converted.
      registry: {
        enabled: true,
        // 'json' -> registry.json, 'ts' -> registry.ts, 'rss' -> feed.xml, 'sitemap' -> sitemap.xml
        formats: ['json'],
        // 'rss' and 'sitemap' require `site.url`.
        // site: { url: 'https://example.com', title: 'My Blog', description: '...' },
      },
    };
  },

  // OPTIONAL: customize what gets recorded per post in the registry.
  // Omit to use the built-in default entry shape.
  buildRegistryEntry(data, context) {
    return buildDefaultRegistryEntry(data, context);
  },

  // OPTIONAL: customize how the registry is serialized to disk.
  // Omit to use the built-in serializers driven by `options.registry.formats`.
  serializeRegistry(registry, context) {
    return serializeDefaultRegistry(registry, context);
  },

  render(data) {
    const date = new Date(data.published);
    const prettyDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, 0)}-${date.getDate().toString().padStart(2, 0)}`;

    const frontMatterAsJSON = {
      slug: `/posts/${data.titleForSlug}/`,
      date: prettyDate,
      title: data.title,
      description: data.description,
      authors: data.authors,
      readingTime: data.readingTime,
      draft: data.draft,
      categories: data.categories,
      tags: data.tags,
      bannerImage: data.images.map(image => image.mediumUrl)[0],
      ogImage: data.images.map(image => image.mediumUrl)[0],
      images: data.images.map(image => image.mediumUrl),
    };

    return `---\n${frontMatterToYaml(frontMatterAsJSON)}\n---\n\n${data.body}\n`;
  },
};

Only getOptions() and render() are required. buildRegistryEntry() and serializeRegistry() are optional hooks — leave them out and medmark falls back to its built-in behaviour.

getOptions() reference

OptionTypeDefaultDescription
defaultCodeBlockLanguagestring'js'Language hint for fenced code blocks with no language tag
folderForEachSlugbooleantrueCreate a slug/index.md folder structure instead of slug.md
imagePathstring'/resources'Base path used for image references in the output
imageStorageStrategy'REMOTE' | 'LOCAL''REMOTE'REMOTE keeps Medium CDN URLs; LOCAL downloads images
registryobject{ enabled: true, formats: ['json'] }Content registry output — see Content Registry

render(data) — available data

FieldTypeDescription
data.titlestringPost title
data.titleForSlugstringURL-safe slug derived from the title
data.subtitlestringPost subtitle, when Medium had one
data.descriptionstringMeta description scraped from Medium
data.bodystringConverted Markdown body
data.bodyRawstringThe post body before Markdown conversion
data.publishedstringISO date string
data.canonicalUrlstringThe post's canonical URL on Medium
data.readingTimestringReading time text (e.g. "5 min read")
data.draftbooleanWhether the post is a draft
data.tagsstring[]Post tags
data.categoriesstring[]Post categories
data.authorsAuthor[]Array of {name, url} objects
data.imagesImage[]Array of {mediumUrl, localName, localPath} objects

render() must return the complete file contents as a string — front matter included. medmark writes exactly what you return.

Using a custom template

Pass the path via --template or enter it in the advanced options prompt:

npx medmark --template medmark/templates/my-template.js

The path is resolved relative to your current directory and loaded as a module, so it can require() anything your project has installed.

Writing templates in TypeScript

Templates are imported at runtime, so medmark loads JavaScript only. If you prefer TypeScript, the published types are exported from the package — compile your template to .js and point --template at the output:

my-template.ts
import type { MedmarkTemplate } from 'medmark';

const template: MedmarkTemplate = {
  getOptions() {
    return { folderForEachSlug: false };
  },
  render(data) {
    return `# ${data.title}\n\n${data.body}\n`;
  },
};

export default template;

On this page