How to Create and Register a Custom Astro Integration for Extending Functionality
To create and register a custom Astro integration, export a factory function that returns an object implementing the AstroIntegration interface—containing a name string and a hooks map—then import and invoke that function within the integrations array of your astro.config.mjs file.
Astro's extensibility is built around a robust plugin system defined in the withastro/astro repository. By learning how to create and register a custom Astro integration, you can inject renderers, add middleware, manipulate the build pipeline, and extend the framework's core behavior at specific lifecycle points.
Understanding the AstroIntegration Interface
The contract for all integrations is defined in packages/astro/src/types/public/integrations.ts. According to the Astro source code, an integration is a plain JavaScript or TypeScript module that exports a factory function returning an object with two required properties: name (a string identifier) and hooks (a map of lifecycle callbacks).
The hooks object implements the BaseIntegrationHooks interface, which provides type signatures for every extension point—from configuration setup to build completion. Key helper types defined alongside this interface include AstroRenderer for adding view layers and AstroIntegrationMiddleware for request handling.
Step-by-Step: Creating a Custom Integration
Step 1: Define the Integration Factory
Create a new file in your project (e.g., src/integrations/custom-integration.ts) that exports a default function returning an AstroIntegration object. This factory pattern allows consumers to pass configuration options and return the integration instance.
// src/integrations/custom-integration.ts
import type { AstroIntegration, AstroRenderer, AstroIntegrationMiddleware } from 'astro';
Step 2: Implement Lifecycle Hooks
Populate the hooks map with the specific lifecycle events you need to intercept. The most commonly used hook is 'astro:config:setup', which runs during configuration loading and provides utilities like addRenderer, addMiddleware, and injectTypes.
For example, to register a custom renderer similar to the test fixture in packages/astro/test/fixtures/custom-renderer, you implement the renderer definition and pass it to addRenderer:
const myRenderer = (): AstroRenderer => ({
name: 'my-renderer',
clientEntrypoint: '@my/renderer/client',
serverEntrypoint: '@my/renderer/server',
});
const requestLogger: AstroIntegrationMiddleware = {
order: 'pre',
entrypoint: new URL('./middleware/logger.ts', import.meta.url).href,
};
export default function customIntegration(): AstroIntegration {
return {
name: 'custom-integration',
hooks: {
'astro:config:setup': async ({ addRenderer, addMiddleware }) => {
addRenderer(myRenderer());
addMiddleware(requestLogger);
},
'astro:server:start': async ({ address, logger }) => {
logger.info(`🚀 Dev server listening on http://${address.address}:${address.port}`);
},
'astro:config:done': async ({ injectTypes, logger }) => {
injectTypes({
filename: 'my-types.d.ts',
content: `declare module '@my/renderer/client';`,
});
logger.info('✅ Custom integration types injected');
},
},
};
}
Step 3: Register in astro.config.mjs
Import your integration factory and invoke it inside the integrations array exported by your configuration file. This registration step connects your custom code to Astro's build and development lifecycles.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import customIntegration from './src/integrations/custom-integration.js';
export default defineConfig({
integrations: [customIntegration()],
});
Core Lifecycle Hooks and Integration Helpers
As implemented in withastro/astro, the hook system covers distinct phases of the application lifecycle. Each hook receives a context object containing specific helpers:
astro:config:setup: Runs during config loading. UseaddRendererto register view layers,addMiddlewareto inject request handlers,injectScriptto add global client-side code, andinjectTypesto generate TypeScript definitions.astro:config:done: Fires after configuration resolution. UseinjectTypesfor final type definitions or validate the resolved configuration.astro:server:start: Executes when the dev server begins listening. Access the serveraddressand a scopedloggerinstance to output startup information.astro:server:setup: Runs before Vite's dev server initializes. Ideal for registering custom Vite plugins or dev tooling.astro:build:startandastro:build:done: Bookend the static generation process. Manipulate build output, generate additional files, or implement custom prerendering logic.astro:route:setup: Called for each route as it is added. Modify route metadata or configure redirects.astro:routes:resolved: Fires after all routes are collected. Validate or transform the final route list before rendering begins.
The packages/astro/src/integrations/hooks.ts file provides the internal machinery that creates logger instances scoped to each integration name, ensuring clean output prefixes during CLI execution.
Example: Custom Middleware Implementation
When adding middleware via addMiddleware, you must provide an entrypoint URL and optionally an order ('pre' or 'post'). The middleware module itself follows the standard Astro middleware signature:
// src/integrations/middleware/logger.ts
import type { IncomingMessage, ServerResponse } from 'http';
export default async function logger(
request: IncomingMessage,
response: ServerResponse,
next: () => Promise<void>
) {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`);
await next();
}
This middleware executes before every request when registered with order: 'pre' in the astro:config:setup hook.
Summary
- Create and register a custom Astro integration by exporting a factory function that returns an
AstroIntegrationobject withnameandhooksproperties, as defined inpackages/astro/src/types/public/integrations.ts. - Implement hooks like
astro:config:setupto access helpers includingaddRenderer,addMiddleware, andinjectTypes. - Register the integration in
astro.config.mjsby importing the factory and including its return value in theintegrationsarray. - Use the test fixture in
packages/astro/test/fixtures/custom-rendereras a reference implementation for custom renderers. - Leverage the scoped
loggerinstance provided in hook contexts to output integration-specific diagnostic information.
Frequently Asked Questions
What is the minimum required structure for an AstroIntegration object?
According to the Astro source code in packages/astro/src/types/public/integrations.ts, an AstroIntegration object must contain exactly two properties: a name string that uniquely identifies the integration, and a hooks object that maps lifecycle event names to callback functions. The hooks object can be empty, but the integration will not modify the build process without at least one hook implementation.
How do I add a custom renderer to handle a new UI framework?
Define an object satisfying the AstroRenderer interface with name, clientEntrypoint, and serverEntrypoint properties. Inside the astro:config:setup hook, call the addRenderer helper and pass your renderer object. The clientEntrypoint is bundled for browser execution, while the serverEntrypoint handles server-side rendering during the build, as demonstrated in the custom-renderer test fixture.
Can an integration modify the Vite configuration directly?
Yes. During the astro:config:setup hook, you can access the updateConfig helper to merge custom Vite configuration options into Astro's internal Vite setup. This allows integrations to register additional plugins, define aliases, or modify build behavior at the bundler level.
Where should I place my custom integration files in a project?
While Astro imposes no strict directory requirement, convention places custom integrations in a src/integrations/ or integrations/ directory at the project root. The entry point must be importable by your astro.config.mjs file, so ensure the file path in your import statement resolves correctly relative to the config file location.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →