Plugin Hooks Reference

Detailed reference for every lifecycle hook available to tulip plugins.

Build pipeline

Hooks fire in a fixed order during the build. Each hook is optional; implement only what you need:

1. setup(config)              — plugin loaded
2. beforeBuild(ctx)           — build starts
3. transformMarkdown(content) — per page, before markdown → HTML
4. transformContext(ctx)       — per page, before template render
5. head()                     — collect tags for HTML head
6. transformHtml(html)         — per page, after template render
7. generatePages(ctx)         — create new files
8. afterBuild(ctx)            — build complete

TypeScript interfaces

These are the core types used across all hooks:

Page

interface Page {
  slug: string;
  url: string;
  frontmatter: Record<string, any>;
  content: string;
}

BuildContext

interface BuildContext {
  pages: Page[];
  site: {
    name: string;
    base_url: string;
    description: string;
  };
  config: PluginConfig;
}

HeadTag

interface HeadTag {
  tag: string;
  attrs: Record<string, string | boolean>;
}

GeneratedPage

interface GeneratedPage {
  path: string;
  content: string;
}

setup(config)

Called once when the plugin is loaded, before any build work begins. Use it to store configuration, initialize state, or validate config values.

setup(config: PluginConfig) {
  if (!config.site_name) {
    throw new Error("seo plugin requires site_name in config");
  }
  this.config = config;
}
ParameterTypeDescription
configPluginConfigMerged config from plugin.toml defaults and tulip.toml overrides

beforeBuild(ctx)

Called once after all plugins are loaded and before any pages are processed.

beforeBuild(ctx: BuildContext) {
  ctx.pages = ctx.pages.filter(function(p) {
    return !p.frontmatter.draft;
  });
}

transformMarkdown(content, page)

Called for each markdown page before it is converted to HTML. Return the modified markdown string.

transformMarkdown(content: string, page: Page): string {
  let words = content.split(/\s+/).length;
  let minutes = Math.ceil(words / 200);
  return "*" + minutes + " min read*\n\n" + content;
}

Returns: string — the transformed markdown.

transformContext(ctx, page)

Called for each page before template rendering. Receives the template context object and returns a modified version.

transformContext(ctx: object, page: Page): object {
  ctx.year = new Date().getFullYear();
  ctx.is_home = page.slug === "index";
  return ctx;
}

Returns: object — the modified template context.

head()

Called once to collect tags that should be injected into the HTML <head>. Return an array of HeadTag objects:

head(): HeadTag[] {
  return [
    { tag: "script", attrs: { src: "https://cdn.example.com/script.js", defer: true } },
    { tag: "link", attrs: { rel: "stylesheet", href: "/css/extra.css" } },
  ];
}

Each tag is rendered as HTML and injected before the closing </head> tag. Boolean attributes like defer are rendered without a value.

transformHtml(html, page)

Called for each page after template rendering. Receives the final HTML string and returns a modified version.

transformHtml(html: string, page: Page): string {
  let tag = '<meta property="og:title" content="' + page.frontmatter.title + '">';
  return html.replace("</head>", tag + "\n</head>");
}

Returns: string — the transformed HTML.

generatePages(ctx)

Called once after all pages are rendered. Return an array of GeneratedPage objects to create additional output files like RSS feeds, sitemaps, or JSON APIs.

generatePages(ctx: BuildContext): GeneratedPage[] {
  let urls = "";
  for (let i = 0; i < ctx.pages.length; i++) {
    let page = ctx.pages[i];
    urls += "<url><loc>" + ctx.site.base_url + page.url + "</loc></url>\n";
  }

  let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
  sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
  sitemap += urls;
  sitemap += "</urlset>";

  return [{ path: "sitemap.xml", content: sitemap }];
}

Returns: GeneratedPage[] — each entry has a path (relative to the build target) and content (file contents).

afterBuild(ctx)

Called once after the build is complete. Use it for cleanup, logging, or post-build tasks.

afterBuild(ctx: BuildContext) {
  let count = ctx.pages.length;
  console.log("Built " + count + " pages");
}