扩展

mdorigin 不打算成为模板系统。扩展模型是代码优先的:mdorigin 拥有路由与规范化内容语义,插件可以在该内核之上定制渲染。

使用代码配置,例如 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;");
}

稳定钩子面

当前稳定钩子:

这些钩子是稳定的扩展点。内部请求处理、路由内部与存储内部不属于插件 API。

核心数据结构

ManagedIndexEntry

这是生成的目录索引与默认列表渲染使用的规范化条目形态。

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

字段含义:

IndexTransformContext

传递给 transformIndex

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

字段含义:

PageRenderModel

这是传入 renderPage 并包含在 RenderHookContext 中的稳定页面模型。

type PageRenderModel = {
  kind: "page" | "listing";
  requestPath: string;
  sourcePath: string;
  locale: string;
  languages: PageLanguage[];
  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;
};

实践中最重要的字段:

PageLanguage

PageRenderModel 上的语言切换器条目使用以下形态:

type PageLanguage = {
  code: string;
  label: string;
  href: string;
  current: boolean;
  translated: boolean;
};

当对应译文存在时,href 指向翻译后的页面;否则回退到该语言的首页。

RenderHookContext

传递给渲染钩子:

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

重要契约:

钩子契约

transformIndex(entries, context)

用它在渲染前修改生成的索引条目。

典型用途:

示例:

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

renderHeader(context)

返回字符串以替换内置 header。多个插件都返回字符串时,最后返回的生效。

典型用途:

renderFooter(context)

返回字符串以替换内置 footer。多个插件都返回字符串时,最后返回的生效。

典型用途:

renderPage(page, context, next)

这是当前最强大的钩子,可以整体替换页面渲染。

规则:

典型用途:

示例:

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)

它在页面渲染之后运行,接收最终 HTML 字符串与最终页面模型。

规则:

典型用途:

插件应该与不应该做什么

插件应该:

插件不应该:

预期边界是:

推荐起步顺序

从以下开始:

  1. 只需要 footer 定制时,用 renderFooter
  2. 想要自定义列表排序或分组时,用 transformIndex
  3. 想要完全自定义页面或列表布局时,用 renderPage

其余配置面见配置