Extensions

mdorigin does not aim to become a template system. The extension model is code-first: mdorigin owns routing and normalized content semantics, and plugins can customize rendering on top of that kernel.

Use a code config such as mdorigin.config.ts:

import { defineConfig } from "mdorigin";

export default defineConfig({
  plugins: [
    {
      name: "custom-layout",
      renderPage(page, _context, next) {
        if (page.kind !== "listing") {
          return next(page);
        }

        const title = escapeHtml(page.title);
        return [
          "<!doctype html>",
          "<html><body>",
          `<main class="custom-listing"><h1>${title}</h1>${page.bodyHtml}</main>`,
          "</body></html>",
        ].join("");
      },
    },
  ],
});

function escapeHtml(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

Stable Hook Surface

Current stable hooks:

These hooks are intended to be stable extension points. Internal request handling, routing internals, and storage internals are not plugin API.

Core Data Structures

ManagedIndexEntry

This is the normalized item shape used for generated directory indexes and default listing rendering.

type ManagedIndexEntry = {
  kind: "directory" | "article";
  title: string;
  href: string;
  detail?: string;
};

Field meanings:

IndexTransformContext

This is passed to transformIndex.

type IndexTransformContext = {
  mode: "build" | "render";
  directoryPath?: string;
  requestPath?: string;
  sourcePath?: string;
  siteConfig?: ResolvedSiteConfig;
};

Field meanings:

PageRenderModel

This is the stable page model passed into renderPage and included in RenderHookContext.

type PageRenderModel = {
  kind: "page" | "listing";
  requestPath: string;
  sourcePath: string;
  siteTitle: string;
  siteDescription?: string;
  siteUrl?: string;
  favicon?: string;
  socialImage?: string;
  logo?: SiteLogo;
  title: string;
  meta: ParsedDocumentMeta;
  bodyHtml: string;
  summary?: string;
  date?: string;
  showSummary: boolean;
  showDate: boolean;
  topNav: SiteNavItem[];
  footerNav: SiteNavItem[];
  footerText?: string;
  socialLinks: SiteSocialLink[];
  editLink?: EditLinkConfig;
  editLinkHref?: string;
  stylesheetContent?: string;
  canonicalPath?: string;
  alternateMarkdownPath?: string;
  listingEntries: ManagedIndexEntry[];
  listingRequestPath: string;
  listingInitialPostCount: number;
  listingLoadMoreStep: number;
  searchEnabled: boolean;
};

The most important fields in practice are:

RenderHookContext

This is passed to render hooks:

type RenderHookContext = {
  page: PageRenderModel;
  siteConfig: ResolvedSiteConfig;
};

Important contract:

Hook Contracts

transformIndex(entries, context)

Use this to modify generated index entries before they are rendered.

Typical uses:

Example:

transformIndex(entries) {
  return entries.filter((entry) => entry.title !== "Draft Notes");
}

renderHeader(context)

Return a string to replace the built-in header. If multiple plugins return strings, the last returned string wins.

Typical uses:

renderFooter(context)

Return a string to replace the built-in footer. If multiple plugins return strings, the last returned string wins.

Typical uses:

renderPage(page, context, next)

This is the most powerful current hook. It can fully replace page rendering.

Rules:

Typical uses:

Example:

renderPage(page, _context, next) {
  if (page.kind !== "listing") {
    return next(page);
  }

  const title = escapeHtml(page.title);
  return [
    "<!doctype html>",
    "<html><body>",
    `<main class="listing-grid"><h1>${title}</h1>${page.bodyHtml}</main>`,
    "</body></html>",
  ].join("");
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

transformHtml(html, context)

This runs after page rendering and receives the final HTML string plus the final page model.

Rules:

Typical uses:

What Plugins Should And Should Not Do

Plugins should do:

Plugins should not do:

The intended boundary is:

Recommended First Steps

Start with:

  1. renderFooter if you only need footer customization
  2. transformIndex if you want custom listing ordering or grouping
  3. renderPage if you want a fully custom page or listing layout

For the rest of the configuration surface, see Configuration.