Plugin System API
import {
definePlugin,
createAnalyticsPlugin,
createRSSPlugin,
createCommentsPlugin,
} from "@noxion/core";
import type { NoxionPlugin, PluginFactory, PluginConfig } from "@noxion/core";
definePlugin()
Creates a type-safe plugin factory. It is an identity helper used for better TypeScript inference.
Signature
function definePlugin<Options = unknown, Content = unknown>(
factory: PluginFactory<Options, Content>
): PluginFactory<Options, Content>
Example
import { definePlugin } from "@noxion/core";
const createMyPlugin = definePlugin<{ hideTag?: string }>((options) => {
return {
name: "my-plugin",
transformPosts({ posts }) {
const hideTag = options.hideTag ?? "private";
return posts.filter((post) => !post.metadata.tags?.includes(hideTag));
},
};
});
NoxionPlugin interface
interface NoxionPlugin<Content = unknown> {
name: string;
// Configuration validation
configSchema?: {
validate(opts: unknown): { valid: boolean; errors?: string[] };
};
// Data hooks
loadContent?: () => Promise<Content> | Content;
contentLoaded?: (args: { content: Content; actions: PluginActions }) => Promise<void> | void;
allContentLoaded?: (args: { allContent: AllContent; actions: PluginActions }) => Promise<void> | void;
// Build lifecycle
onBuildStart?: (args: { config: NoxionConfig }) => Promise<void> | void;
postBuild?: (args: { config: NoxionConfig; routes: RouteInfo[] }) => Promise<void> | void;
// Content transformation
transformContent?: (args: { recordMap: ExtendedRecordMap; post: BlogPost }) => ExtendedRecordMap;
transformPosts?: (args: { posts: BlogPost[] }) => BlogPost[];
// SEO / metadata
extendMetadata?: (args: { metadata: NoxionMetadata; post?: BlogPost; config: NoxionConfig }) => NoxionMetadata;
injectHead?: (args: { post?: BlogPost; config: NoxionConfig }) => HeadTag[];
extendSitemap?: (args: { entries: SitemapEntry[]; config: NoxionConfig }) => SitemapEntry[];
// Routing
extendRoutes?: (args: { routes: RouteInfo[]; config: NoxionConfig }) => RouteInfo[];
// v0.2 hooks
registerPageTypes?: () => PageTypeDefinition[];
onRouteResolve?: (route: RouteInfo) => RouteInfo | null;
/** @deprecated */
extendSlots?: (slots: Record<string, unknown>) => Record<string, unknown>;
}
Hooks reference
transformPosts
Called: After all posts are fetched from Notion, before ISR caching.
Use for: Filtering posts, computing derived fields (word count, reading time), sorting overrides.
transformPosts({ posts }) {
return posts.map(post => ({
...post,
frontmatter: {
...post.frontmatter,
readingTime: estimateReadingTime(post),
},
}));
}
registerPageTypes
Called: During plugin initialization.
Use for: Registering custom page types beyond the built-in blog, docs, and portfolio.
registerPageTypes() {
return [{
name: "recipe",
defaultTemplate: "recipe/page",
schemaConventions: {
ingredients: { names: ["Ingredients"] },
prepTime: { names: ["Prep Time"] },
},
}];
}
onRouteResolve
Called: When generating a URL for a page.
Use for: Customizing URL patterns per page type.
onRouteResolve(route) {
if (route.path.startsWith("/recipe/")) {
return { ...route, path: route.path.replace("/recipe/", "/recipes/") };
}
return route;
}
extendSlots
Called: Legacy hook from the pre-v0.3 theme-slot model.
Use for: Backward compatibility only. New themes should expose components directly and be composed in app code.
extendSlots(slots) {
return {
...slots,
readingTimeDisplay: "📖 {{readingTime}}",
};
}
configSchema
Checked: During loadPlugins() when validating plugin options.
configSchema: {
validate(opts: unknown) {
const errors: string[] = [];
if (typeof opts !== "object" || opts === null) {
return { valid: false, errors: ["Options must be an object"] };
}
return { valid: errors.length === 0, errors };
},
},
transformContent
Called: Before a page's recordMap is passed to <NotionPage>.
injectHead
Called: When generating <head> tags for a page. post is undefined on the homepage and tag pages.
extendMetadata
Called: When generating Next.js Metadata for a page.
extendSitemap
Called: When generating sitemap entries.
PluginFactory type
type PluginFactory<Options = unknown, Content = unknown> = (
options: Options
) => NoxionPlugin<Content>;
Recommended pattern for configurable plugins:
export const createMyPlugin: PluginFactory<MyOptions> = (options = {}) => {
return {
name: "my-plugin",
configSchema: { validate(opts) { /* ... */ } },
transformPosts({ posts }) { /* ... */ },
};
};
HeadTag type
interface HeadTag {
tagName: string;
attributes?: Record<string, string>;
innerHTML?: string;
}
SitemapEntry type
interface SitemapEntry {
url: string;
lastmod?: string;
changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
priority?: number;
}
Built-in plugin factories
createAnalyticsPlugin()
createAnalyticsPlugin({
provider: "google" | "plausible" | "umami" | "custom",
trackingId: string,
customScript?: string,
})
See Analytics Plugin.
createRSSPlugin()
createRSSPlugin({
feedPath?: string, // default: "/feed.xml"
limit?: number, // default: 20
})
See RSS Plugin.
createCommentsPlugin()
createCommentsPlugin({
provider: "giscus" | "utterances" | "disqus",
config: { /* provider-specific options */ },
})
See Comments Plugin.