Skip to content
Codeloom
Astro

Building and Using Astro Integrations

How Astro integrations work under the hood: using official integrations, configuring MDX and Tailwind, and building your own custom integration with lifecycle hooks.

·7 min read · By Codeloom
Advanced 14 min read

What you'll learn

  • How the Astro integration API works with lifecycle hooks
  • How to configure official integrations: MDX, Tailwind, Sitemap
  • How to build a custom integration from scratch
  • How to modify the Vite config and inject scripts through integrations
  • Best practices for publishing and sharing integrations

Prerequisites

  • Astro project setup
  • Basic understanding of build tools and Vite

What Integrations Actually Do

An Astro integration is a JavaScript object with a name and a set of lifecycle hooks. When Astro builds your site, it calls these hooks at specific points: before the build starts, when the Vite config is being assembled, after pages are rendered, when the dev server starts. Integrations use these hooks to add functionality: MDX support, CSS frameworks, image optimization, analytics scripts.

astro:config:setup → astro:config:done → astro:server:setup → astro:build:start → astro:build:done
   (modify config)    (config finalized)   (dev server)         (build begins)      (build complete)
Integration lifecycle

Using Official Integrations

Adding MDX Support

npx astro add mdx

This installs @astrojs/mdx and adds it to your config. MDX lets you use components inside Markdown:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';

export default defineConfig({
  integrations: [
    mdx({
      // Customize remark/rehype plugins
      remarkPlugins: [],
      rehypePlugins: [],
      // Enable GitHub Flavored Markdown
      gfm: true,
    }),
  ],
});

Now .mdx files in your content collections can import and use components:

---
title: "My Post"
---

import Callout from '@/components/Callout.astro';

## Introduction

Here is a callout:

<Callout type="warning">
  This is important information.
</Callout>

Adding Tailwind CSS

npx astro add tailwind
// astro.config.mjs
import tailwind from '@astrojs/tailwind';

export default defineConfig({
  integrations: [
    tailwind({
      // Apply Tailwind's base styles
      applyBaseStyles: true,
      // Custom config path
      configFile: './tailwind.config.mjs',
    }),
  ],
});
// tailwind.config.mjs
export default {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  theme: {
    extend: {
      colors: {
        brand: {
          50: '#eff6ff',
          500: '#3b82f6',
          900: '#1e3a5f',
        },
      },
    },
  },
};

Adding a Sitemap

npx astro add sitemap
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://example.com',
  integrations: [
    sitemap({
      filter: (page) => !page.includes('/admin/'),
      changefreq: 'weekly',
      lastmod: new Date(),
    }),
  ],
});

Adding React (or Vue, Svelte, Solid)

npx astro add react
import react from '@astrojs/react';

export default defineConfig({
  integrations: [
    react({
      // Include specific file patterns
      include: ['**/react/*'],
    }),
  ],
});

Building a Custom Integration

The Integration Shape

Every integration is an object with a name and hooks:

// src/integrations/my-integration.ts
import type { AstroIntegration } from 'astro';

export default function myIntegration(options?: { debug?: boolean }): AstroIntegration {
  return {
    name: 'my-integration',
    hooks: {
      'astro:config:setup': ({ config, updateConfig, addMiddleware, logger }) => {
        logger.info('Setting up my integration');
      },
      'astro:config:done': ({ config }) => {
        // Config is finalized, read-only
      },
      'astro:build:done': ({ dir, pages, logger }) => {
        logger.info(`Built ${pages.length} pages to ${dir}`);
      },
    },
  };
}

Use it in your config:

import myIntegration from './src/integrations/my-integration';

export default defineConfig({
  integrations: [myIntegration({ debug: true })],
});

Example: Auto-Import Global Styles

// src/integrations/global-styles.ts
import type { AstroIntegration } from 'astro';

export default function globalStyles(stylePaths: string[]): AstroIntegration {
  return {
    name: 'global-styles',
    hooks: {
      'astro:config:setup': ({ injectScript }) => {
        const imports = stylePaths
          .map((path) => `import '${path}';`)
          .join('\n');

        injectScript('page-ssr', imports);
      },
    },
  };
}
// astro.config.mjs
import globalStyles from './src/integrations/global-styles';

export default defineConfig({
  integrations: [
    globalStyles([
      './src/styles/reset.css',
      './src/styles/typography.css',
    ]),
  ],
});

Example: Build-Time Analytics

Generate a build report showing page counts, sizes, and build time:

// src/integrations/build-report.ts
import type { AstroIntegration } from 'astro';
import fs from 'node:fs';
import path from 'node:path';

export default function buildReport(): AstroIntegration {
  let buildStart: number;

  return {
    name: 'build-report',
    hooks: {
      'astro:build:start': ({ logger }) => {
        buildStart = performance.now();
        logger.info('Build started');
      },
      'astro:build:done': async ({ dir, pages, logger }) => {
        const duration = Math.round(performance.now() - buildStart);
        const outputDir = dir.pathname;

        // Calculate total output size
        let totalSize = 0;
        const walkDir = (dirPath: string) => {
          const entries = fs.readdirSync(dirPath, { withFileTypes: true });
          for (const entry of entries) {
            const fullPath = path.join(dirPath, entry.name);
            if (entry.isDirectory()) {
              walkDir(fullPath);
            } else {
              totalSize += fs.statSync(fullPath).size;
            }
          }
        };
        walkDir(outputDir);

        const report = {
          pages: pages.length,
          totalSizeKB: Math.round(totalSize / 1024),
          buildTimeMs: duration,
          timestamp: new Date().toISOString(),
        };

        // Write report to dist
        fs.writeFileSync(
          path.join(outputDir, 'build-report.json'),
          JSON.stringify(report, null, 2),
        );

        logger.info(`Build complete: ${report.pages} pages, ${report.totalSizeKB}KB, ${duration}ms`);
      },
    },
  };
}

Example: Inject Analytics Script

// src/integrations/analytics.ts
import type { AstroIntegration } from 'astro';

export default function analytics(trackingId: string): AstroIntegration {
  return {
    name: 'analytics',
    hooks: {
      'astro:config:setup': ({ injectScript }) => {
        injectScript(
          'page',
          `
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', '${trackingId}');
          `,
        );
      },
    },
  };
}

Example: Modify Vite Config

// src/integrations/custom-aliases.ts
import type { AstroIntegration } from 'astro';
import path from 'node:path';

export default function customAliases(): AstroIntegration {
  return {
    name: 'custom-aliases',
    hooks: {
      'astro:config:setup': ({ updateConfig }) => {
        updateConfig({
          vite: {
            resolve: {
              alias: {
                '@components': path.resolve('./src/components'),
                '@layouts': path.resolve('./src/layouts'),
                '@utils': path.resolve('./src/lib'),
              },
            },
          },
        });
      },
    },
  };
}

Example: Add Middleware from an Integration

// src/integrations/security-headers.ts
import type { AstroIntegration } from 'astro';

export default function securityHeaders(): AstroIntegration {
  return {
    name: 'security-headers',
    hooks: {
      'astro:config:setup': ({ addMiddleware }) => {
        addMiddleware({
          entrypoint: './src/integrations/security-headers-middleware.ts',
          order: 'pre',
        });
      },
    },
  };
}
// src/integrations/security-headers-middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next();

  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()',
  );

  return response;
});

The Complete Hook Reference

HookWhenCommon uses
astro:config:setupConfig is being assembledModify config, add renderers, inject scripts, add middleware
astro:config:doneConfig is finalizedRead final config, validate settings
astro:server:setupDev server is createdAdd custom middleware to the dev server
astro:server:startDev server has startedLog the dev URL, run setup tasks
astro:server:doneDev server is closingCleanup resources
astro:build:startBuild beginsStart timers, validate pre-conditions
astro:build:setupBuild is being configuredModify the Vite build config
astro:build:generatedStatic routes are generatedPost-process static HTML
astro:build:doneBuild is completeGenerate sitemaps, reports, clean up

Integration Ordering

Integrations run in the order they appear in the integrations array. This matters when one integration depends on another:

export default defineConfig({
  integrations: [
    // Tailwind first — it modifies PostCSS config
    tailwind(),
    // MDX second — it processes .mdx files
    mdx(),
    // React third — it adds the React renderer
    react(),
    // Sitemap last — it reads the final page list
    sitemap(),
    // Custom integrations after official ones
    buildReport(),
  ],
});

Publishing an Integration

If you want to share your integration as an npm package:

{
  "name": "astro-my-integration",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "keywords": ["astro-integration", "withastro"],
  "peerDependencies": {
    "astro": "^5.0.0"
  }
}

The astro-integration and withastro keywords make your package discoverable on the Astro integrations page.

Summary

Integrations are Astro’s extension mechanism. Official integrations add MDX, Tailwind, React, sitemaps, and more with a single command. Custom integrations use lifecycle hooks to modify the build pipeline: inject scripts, add middleware, modify Vite config, generate reports, and post-process output. The API is small — a name and a handful of hooks — but it covers every point where you might want to customize how Astro builds your site.