The Container API is experimental and subject to breaking changes. Consult the Astro CHANGELOG for updates.
The Container API allows you to render Astro components in isolation outside of a full Astro application. This is useful for testing components, rendering components in non-Astro environments, or server-side rendering individual components.
When to use this API
Use the Container API when you need to:
- Test Astro components in isolation with Vitest or other test runners
- Render Astro components in non-Vite environments (PHP, Elixir, Ruby, etc.)
- Generate HTML from components for emails or documentation
- Server-side render components independently of page routing
- Create component previews or style guides
Creating a container
Import and create a container instance:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
const container = await AstroContainer.create();
Configuration options
Enables HTML streaming for component rendering.const container = await AstroContainer.create({
streaming: true
});
renderers
AddServerRenderer[]
default:"[]"
List of client renderers for UI framework components (React, Vue, Svelte, etc.).import { getContainerRenderer } from '@astrojs/react';
import { loadRenderers } from 'astro:container';
const renderers = await loadRenderers([getContainerRenderer()]);
const container = await AstroContainer.create({ renderers });
Rendering components
renderToString()
Renders a component and returns HTML as a string:
renderToString(
component: AstroComponentFactory,
options?: ContainerRenderOptions
): Promise<string>
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import Card from '../src/components/Card.astro';
const container = await AstroContainer.create();
const html = await container.renderToString(Card);
renderToResponse()
Renders a component and returns a Response object:
renderToResponse(
component: AstroComponentFactory,
options?: ContainerRenderOptions
): Promise<Response>
const container = await AstroContainer.create();
const response = await container.renderToResponse(Card);
const html = await response.text();
Rendering options
Both rendering methods accept an optional configuration object:
Component properties to pass to the component.const html = await container.renderToString(Card, {
props: { title: 'Hello World', count: 42 }
});
---
interface Props {
title: string;
count: number;
}
const { title, count } = Astro.props;
---
<div>{title}: {count}</div>
Content to render in component slots.// Single default slot
const html = await container.renderToString(Card, {
slots: { default: 'Card content' }
});
// Named slots
const html = await container.renderToString(Card, {
slots: {
header: 'Header content',
footer: 'Footer content'
}
});
Render components in slots:const header = await container.renderToString(CardHeader);
const html = await container.renderToString(Card, {
slots: { header }
});
Request object with URL and headers information.const html = await container.renderToString(Card, {
request: new Request('https://example.com/blog', {
headers: { 'x-custom-header': 'value' }
})
});
params
Record<string, string | undefined>
Route parameters for dynamic routes.const html = await container.renderToString(BlogPost, {
params: {
locale: 'en',
slug: 'getting-started'
}
});
---
const { locale, slug } = Astro.params;
---
Context locals for middleware and request lifecycle.const html = await container.renderToString(Card, {
locals: {
user: { id: 1, name: 'Alice' },
isAuthenticated: true
}
});
---
const { user } = Astro.locals;
---
<p>Welcome, {user.name}!</p>
Specify that you’re rendering an endpoint.import * as Endpoint from '../src/pages/api/data.js';
const response = await container.renderToResponse(Endpoint, {
routeType: 'endpoint'
});
const json = await response.json();
Test different HTTP methods:const response = await container.renderToResponse(Endpoint, {
routeType: 'endpoint',
request: new Request('https://example.com/api', {
method: 'POST',
body: JSON.stringify({ data: 'value' })
})
});
Whether to render as a page partial (without <!DOCTYPE html>).// Render full page with DOCTYPE
const html = await container.renderToString(Page, {
partial: false
});
Adding renderers
For components using UI frameworks, you need to add renderers.
In Vite environments
Use the loadRenderers() helper with getContainerRenderer():
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { getContainerRenderer as getReactRenderer } from '@astrojs/react';
import { getContainerRenderer as getSvelteRenderer } from '@astrojs/svelte';
import { loadRenderers } from 'astro:container';
import ReactComponent from '../src/components/ReactComponent.jsx';
const renderers = await loadRenderers([
getReactRenderer(),
getSvelteRenderer()
]);
const container = await AstroContainer.create({ renderers });
const html = await container.renderToString(ReactComponent);
In non-Vite environments
Manually import and add server and client renderers:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import reactRenderer from '@astrojs/react/server.js';
import vueRenderer from '@astrojs/vue/server.js';
const container = await AstroContainer.create();
// Add server renderers for static components
container.addServerRenderer({ renderer: vueRenderer });
// Add server + client renderers for interactive components
container.addServerRenderer({ renderer: reactRenderer });
container.addClientRenderer({
name: '@astrojs/react',
entrypoint: '@astrojs/react/client.js'
});
Server renderers must be added before client renderers. This ensures proper rendering order and hydration.
Testing example
Use the Container API with Vitest to test components:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';
test('Card renders with title', async () => {
const container = await AstroContainer.create();
const html = await container.renderToString(Card, {
props: { title: 'Test Card' }
});
expect(html).toContain('Test Card');
});
test('Card renders slot content', async () => {
const container = await AstroContainer.create();
const html = await container.renderToString(Card, {
slots: { default: 'Card body content' }
});
expect(html).toContain('Card body content');
});
Advanced usage
Rendering in PHP
<?php
// Load Node.js script that uses Container API
exec('node render-component.js', $output);
echo $output[0];
?>
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import reactRenderer from '@astrojs/react/server.js';
import Component from './src/components/Component.astro';
const container = await AstroContainer.create();
container.addServerRenderer({ renderer: reactRenderer });
const html = await container.renderToString(Component);
console.log(html);
Email generation
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import EmailTemplate from '../src/emails/Welcome.astro';
const container = await AstroContainer.create();
const emailHtml = await container.renderToString(EmailTemplate, {
props: {
userName: 'Alice',
verificationLink: 'https://example.com/verify/abc123'
}
});
// Send email with emailHtml
TypeScript support
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
import type { ContainerRenderOptions } from 'astro/container';
const options: ContainerRenderOptions = {
props: { title: 'Hello' },
slots: { default: 'Content' },
request: new Request('https://example.com')
};