Markdown Configuration

Stati uses markdown-it as its markdown processor. The engine is configured with sensible defaults, and you can extend it with plugins or customize rendering behavior.

Configuration Options

Stati provides two options for markdown customization:

// stati.config.js
import { defineConfig } from '@stati/core';

export default defineConfig({
  markdown: {
    // Array of markdown-it plugins
    plugins: [
      'anchor',                    // Plugin name only
      ['prism', { options }],      // Plugin with options
    ],

    // Custom configuration function
    configure: (md) => {
      // Receive markdown-it instance
      // Customize rendering, add plugins, etc.
    },
  },
});

Default Settings

Stati creates markdown-it with these hardcoded settings (not configurable):

  • HTML enabled: HTML tags are allowed in markdown source
  • Linkify enabled: URLs are automatically converted to links
  • Typographer enabled: Smart quotes and typography replacements

These defaults provide a good balance of functionality and security for most use cases.

Markdown Plugins

The plugins option accepts an array of markdown-it plugins. Stati automatically prepends markdown-it- to plugin names.

Plugin Format

Simple plugin (string):

export default defineConfig({
  markdown: {
    plugins: ['anchor', 'footnote', 'emoji'],
  },
});

Plugin with options (array):

export default defineConfig({
  markdown: {
    plugins: [
      ['anchor', {
        slugify: (s) => s.toLowerCase().trim().replace(/[\s\W-]+/g, '-'),
      }],
      ['prism', {
        defaultLanguage: 'javascript',
      }],
    ],
  },
});

Plugin Installation

Plugins must be installed as npm dependencies:

npm install markdown-it-anchor
npm install markdown-it-footnote
npm install @widgetbot/markdown-it-prism

Common Plugins

Plugin Package Purpose
anchor markdown-it-anchor Add IDs to headings
prism @widgetbot/markdown-it-prism Syntax highlighting
toc-done-right markdown-it-toc-done-right Table of contents
footnote markdown-it-footnote Footnote support
emoji markdown-it-emoji Emoji shortcuts :smile:
external-links markdown-it-external-links External link attributes
container markdown-it-container Custom containers
attrs markdown-it-attrs Add attributes to elements

Example: Complete Plugin Setup

export default defineConfig({
  markdown: {
    plugins: [
      // Add IDs to headings for deep linking
      ['anchor', {
        slugify: (s) => s.toLowerCase().trim().replace(/[\s\W-]+/g, '-'),
        permalink: false,
      }],

      // Generate table of contents
      'toc-done-right',

      // Syntax highlighting with Prism.js
      ['prism', {
        defaultLanguage: 'javascript',
      }],

      // Add target="_blank" to external links
      ['external-links', {
        externalTarget: '_blank',
        internalDomains: ['stati.build'],
      }],

      // Support for ::: container syntax
      ['container', 'warning'],
      ['container', 'tip'],

      // Footnote support
      'footnote',

      // Emoji shortcuts
      'emoji',
    ],
  },
});

Custom Configuration

The configure option accepts a function that receives the markdown-it instance. This runs after plugins are loaded, allowing you to override plugin behavior.

Basic Customization

export default defineConfig({
  markdown: {
    configure: (md) => {
      // Modify markdown-it options
      md.set({ breaks: true });

      // Configure linkify
      md.linkify.set({ fuzzyEmail: false });

      // Add plugins programmatically
      md.use(somePlugin, options);
    },
  },
});

Custom Renderer Rules

Customize how specific markdown elements are rendered:

export default defineConfig({
  markdown: {
    configure: (md) => {
      // Customize heading rendering
      md.renderer.rules.heading_open = (tokens, idx) => {
        const level = tokens[idx].tag;
        const label = tokens[idx + 1].content;
        return `<${level} class="heading heading-${level}" data-label="${label}">`;
      };

      // Customize link rendering
      const defaultLinkOpen = md.renderer.rules.link_open ||
        ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));

      md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
        const aIndex = tokens[idx].attrIndex('href');
        if (aIndex >= 0) {
          const href = tokens[idx].attrs[aIndex][1];

          // Add class to external links
          if (href.startsWith('http')) {
            tokens[idx].attrPush(['class', 'external-link']);
            tokens[idx].attrPush(['rel', 'noopener noreferrer']);
          }
        }
        return defaultLinkOpen(tokens, idx, options, env, self);
      };
    },
  },
});

Custom Block Syntax

Create custom container syntax:

import container from 'markdown-it-container';

export default defineConfig({
  markdown: {
    plugins: [
      ['container', 'warning'],
      ['container', 'tip'],
      ['container', 'danger'],
    ],
  },
});

Usage in markdown:

::: warning
This is a warning message
:::

::: tip
This is a helpful tip
:::

Syntax Highlighting

Stati doesn’t provide built-in syntax highlighting. You have two options:

Use the markdown-it-prism plugin with Prism.js:

Install:

npm install @widgetbot/markdown-it-prism prismjs

Configure:

export default defineConfig({
  markdown: {
    plugins: [
      ['prism', { defaultLanguage: 'javascript' }],
    ],
  },
});

Add to layout:

<link rel="stylesheet" href="/path/to/prism.css">
<script src="/path/to/prism.js"></script>

Pros: Simple setup, many themes available, works well for static sites Cons: Requires JavaScript, flash of unstyled code before JS loads

Option 2: Server-Side

Use any server-side highlighter in the configure function:

import { getHighlighter } from 'shiki';

export default defineConfig({
  markdown: {
    configure: async (md) => {
      const highlighter = await getHighlighter({
        theme: 'nord',
        langs: ['javascript', 'typescript', 'python', 'html', 'css'],
      });

      md.options.highlight = (code, lang) => {
        if (lang && highlighter.getLoadedLanguages().includes(lang)) {
          return highlighter.codeToHtml(code, { lang });
        }
        return ''; // Return empty string for unsupported languages
      };
    },
  },
});

Pros: No JavaScript required, no flash of unstyled code Cons: More complex setup, increases build time

Front Matter

Stati automatically parses YAML front matter from markdown files using gray-matter. This is not configurable.

Example:

---
title: My Page Title
description: Page description
date: 2024-01-15
tags: [stati, markdown]
author: John Doe
---

# Your content here

All front matter fields are available in templates:

<h1><%= stati.page.title %></h1>
<p><%= stati.page.description %></p>
<time><%= new Date(stati.page.date).toLocaleDateString() %></time>

Best Practices

Plugin Selection

  1. Install only what you need - Each plugin adds processing overhead
  2. Check compatibility - Ensure plugins work with your markdown-it version
  3. Test thoroughly - Some plugins may conflict with each other

Custom Rendering

  1. Use configure for overrides - The configure function runs after plugins
  2. Preserve defaults - Store default renderer before overriding
  3. Handle edge cases - Check for null/undefined values in custom rules

Performance

  1. Limit plugins - Too many plugins slow down builds
  2. Cache compiled results - Stati caches in production automatically
  3. Optimize images - Large images slow down markdown parsing

Complete Example

A production-ready markdown configuration:

import { defineConfig } from '@stati/core';

export default defineConfig({
  markdown: {
    plugins: [
      // Heading anchors
      ['anchor', {
        slugify: (s) => s.toLowerCase().trim().replace(/[\s\W-]+/g, '-'),
      }],

      // Table of contents
      'toc-done-right',

      // Syntax highlighting
      ['prism', { defaultLanguage: 'javascript' }],

      // External links
      ['external-links', {
        externalTarget: '_blank',
        internalDomains: ['stati.build'],
      }],

      // Custom containers
      ['container', 'warning'],
      ['container', 'tip'],

      // Footnotes
      'footnote',
    ],

    configure: (md) => {
      // Add custom CSS classes to links
      const defaultLinkOpen = md.renderer.rules.link_open ||
        ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));

      md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
        const aIndex = tokens[idx].attrIndex('href');
        if (aIndex >= 0) {
          const href = tokens[idx].attrs[aIndex][1];
          if (href.startsWith('http')) {
            tokens[idx].attrPush(['class', 'external-link']);
          } else {
            tokens[idx].attrPush(['class', 'internal-link']);
          }
        }
        return defaultLinkOpen(tokens, idx, options, env, self);
      };

      // Enable line breaks
      md.set({ breaks: true });
    },
  },
});

Next Steps