Authoring Plugins
Plugins are the primary way to extend Coralite's functionality. The framework's architecture is designed to be highly extensible via the definePlugin function.
With plugins, you can hook deep into the build lifecycle, register custom components globally, bundle external scripts, and inject custom helper functions directly into the frontend.
The Conceptual Flow #
Authoring a plugin involves understanding how Coralite synthesizes server-side rendering and client-side hydration. When you register a plugin, Coralite integrates it through the following flow:
- Registration & Bootstrapping: Coralite loads your plugin via
definePlugin. It initializes bothserverandclientconfigurations, setting up global contexts that will be available to components. - Server Execution: During the build process, Coralite triggers server-side lifecycle hooks (like
onPageSetoronBeforeBuild). If your plugin provides aserver.context, its methods become available to every component'sserverblock. - Discovery & Bundling: Coralite analyzes components and identifies which plugin utilities are being used. It automatically bundles necessary dependencies and prepares the client-side runtime.
- Client Hydration & Execution: In the browser, Coralite initializes the
client.context. Plugin hooks likeonBeforeComponentRenderfire as components mount, and your utilities become available via thecontext['plugin-name']object in componentclientblocks.
Server-Side Lifecycle Hooks #
Plugins can hook into almost every phase of Coralite's build lifecycle to perform actions when files are processed or rendering occurs.
Hooks receive a context object containing the app instance, the plugin's config, and relevant lifecycle data.
export default definePlugin({
name: 'my-hook-plugin',
server: {
// Fire when a new page is discovered
onPageSet: async ({ page, data, app }) => {
console.log(`Processing page: ${data.path.pathname}`);
// You can mutate the page object directly
page.meta.processedBy = 'my-hook-plugin';
},
// Fire before a page is rendered to HTML
onBeforePageRender: async ({ elements, page, app }) => {
// 'elements' is the parsed HTML AST.
// You can inject or modify elements here.
elements.root.children.push({
type: 'tag',
name: 'div',
attribs: { id: 'plugin-injected' },
children: [{ type: 'text', data: 'Hello from Plugin!' }]
});
}
}
});
For a full list of available hooks and their arguments, see the API Reference.
Server-Side Context #
While client.context is for the browser, server.context allows you to provide utilities to the server block of a defineComponent. This is useful for providing data fetching helpers, logging, or shared business logic that should only run during the build.
Just like the client context, server.context uses a Two-Phase Resolver. The first phase allows for global setup and inter-plugin registration, while the second phase provides the actual utilities to the component instance.
// Plugin definition
export default definePlugin({
name: 'data-helper',
server: {
context: (pluginContext) => {
// Phase 1: Global setup. Mutate pluginContext to share with other plugins.
pluginContext.db = initializeDB();
return (instanceContext) => {
// Phase 2: Per-instance resolver.
// Use instanceContext to access other plugins.
const logger = instanceContext['logger-plugin'];
return {
fetchSecure: async (url) => {
logger.log(`Fetching: ${url}`);
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${pluginContext.config.token}` }
});
return response.json();
}
};
}
}
}
});
// Usage in a component
export default defineComponent({
async server({ 'data-helper': helper }) {
const data = await helper.fetchSecure('https://api.example.com/data');
return { data };
}
});
Global Component Registration #
Plugins can distribute their own HTML components. By providing an array of file paths to the components property in your plugin configuration, Coralite automatically reads, parses, and registers these files as global components during initialization.
This means users of your plugin can instantly use your custom tags (e.g., <my-awesome-slider>) without having to copy the HTML files into their own project's src/components/ directory.
Injecting Client Helpers #
The most powerful feature of Coralite plugins is the ability to provide reusable functionality directly to component client blocks using client.context.
Context utilities are namespaced on the context object available in every component's client execution block (e.g. context['my-plugin']). However, because Coralite is a static site generator, there is a strict boundary between server code and client code.
The Two-Phase Resolver #
Helpers must be structured using a Two-Phase Resolver function that executes during setup. The outermost function receives the plugin context (Phase 1, runs on the server and contains the global application instance and plugin configuration), and it must return a second function that receives the local Web Component instance context (Phase 2). This second function then returns an object containing the actual utility functions accessible on the client.
To use client-only APIs (like window, document, or the component's AbortSignal), the returned helper functions encapsulate that logic within their own bodies.
Inter-Plugin Communication #
Because Phase 1 of the resolver runs during initialization, you can use it to share services between plugins. Any property added to the pluginContext object in Phase 1 becomes available to all subsequent plugins.
// Plugin A: Provides a shared service
client: {
context: (pluginContext) => {
pluginContext.mySharedService = { log: (msg) => console.log(msg) };
return (instanceContext) => ({});
}
}
// Plugin B: Consumes the shared service
client: {
context: (pluginContext) => {
const service = pluginContext.mySharedService;
return (instanceContext) => {
return {
doSomething: () => service.log('Doing something...')
};
};
}
}
// Server-side definition in definePlugin
client: {
context: (pluginContext) => {
// Phase 1: Runs on the SERVER and receives pluginContext (including config)
const apiKey = pluginContext.config.apiKey;
// Share data with other plugins in Phase 1
pluginContext.sharedClientData = { version: '1.0' };
return (instanceContext) => {
// Phase 2: Receives the local Web Component instance context (signal, state, etc.)
// and the namespaced proxy to other plugins.
const { signal } = instanceContext;
const uiPlugin = instanceContext['ui-plugin'];
// Phase 3: Return the actual helper object accessible in the browser
return {
trackEvent: (buttonElement, eventName) => {
uiPlugin.showToast(`Tracking: ${eventName}`);
buttonElement.addEventListener('click', () => {
window.navigator.sendBeacon('https://api.analytics.com', JSON.stringify({
key: apiKey,
event: eventName
}));
}, { signal });
}
};
}
}
}
Client-Side Lifecycle Hooks #
Just like on the server, plugins can hook into the client-side lifecycle of every component. This is useful for global monitoring, automatic event tracking, or managing global state.
onBeforeComponentRender(instanceContext): Fires before a component'sclientblock executes.onAfterComponentRender(instanceContext): Fires after a component'sclientblock executes.onDisconnected(instanceContext): Fires when a component is removed from the DOM.
export default definePlugin({
name: 'client-monitor',
client: {
onBeforeComponentRender: (context) => {
console.log(`Component ${context.id} is mounting...`);
},
onDisconnected: (context) => {
console.log(`Component ${context.id} was removed.`);
}
}
});
Zero-Config Dependencies #
Unlike earlier versions of Coralite, V1 removes the need for manual dependency arrays like client.imports or client.components. The underlying AST parser automatically detects dynamic imports (e.g., await import('my-analytics-sdk')) inside the actual runtime logic of your context utilities and bundles them automatically.
Ready to build your plugin? Consult the strict API signatures and configuration options in the definePlugin API Reference.