withastro/astro · error · AstroError

ServerOnlyModule

ServerOnlyModule

Error message

The "astro:content" module is only available server-side.

What it means

Thrown when the `astro:content` virtual module is imported in a client-side (browser) context. The virtual module plugin's `load` hook checks the `isClient` flag; since content collections rely on filesystem access and server-only APIs, importing `astro:content` in client JavaScript is not supported and the `ServerOnlyModule` error is thrown.

Source

Thrown at packages/astro/src/content/vite-plugin-content-virtual-mod.ts:373

async function generateContentEntryFile({
	settings,
	isClient,
}: {
	settings: AstroSettings;
	fs: typeof nodeFs;
	isClient: boolean;
}) {
	const contentPaths = getContentPaths(
		settings.config,
		undefined,
		settings.config.legacy?.collectionsBackwardsCompat,
	);
	const relContentDir = rootRelativePath(settings.config.root, contentPaths.contentDir);

	let virtualModContents: string;
	if (isClient) {
		throw new AstroError({
			...AstroErrorData.ServerOnlyModule,
			message: AstroErrorData.ServerOnlyModule.message('astro:content'),
		});
	} else {
		virtualModContents = nodeFs
			.readFileSync(contentPaths.virtualModTemplate, 'utf-8')
			.replace('@@CONTENT_DIR@@', relContentDir)
			.replace(
				'/* @@LIVE_CONTENT_CONFIG@@ */',
				contentPaths.liveConfig.exists
					? // Dynamic import so it extracts the chunk and avoids a circular import
						`const liveCollections = (await import(${JSON.stringify(fileURLToPath(contentPaths.liveConfig.url))})).collections;`
					: 'const liveCollections = {};',
			);
	}

	return virtualModContents;
}

View on GitHub (pinned to d081033d5f)

Solutions

  1. Move all `astro:content` imports to server-only code (`.astro` frontmatter, endpoints, or `server` islands).
  2. Pass collection data as props from the server to client components instead of importing `astro:content` in the client.
  3. If using `client:` directives, ensure the component does not directly import `astro:content`.
  4. Split shared utilities into a server-only module and a client-safe module.

Example fix

// before — src/components/BlogList.jsx (client component)
import { getCollection } from 'astro:content';
export default function BlogList() {
  const posts = await getCollection('blog'); // crashes in browser
  return <ul>{posts.map(p => <li>{p.id}</li>)}</ul>;
}

// after — src/components/BlogList.jsx
export default function BlogList({ posts }) {
  return <ul>{posts.map(p => <li>{p.id}</li>)}</ul>;
}
// parent .astro file fetches on server and passes as prop
Defensive patterns

Strategy: type-guard

Validate before calling

// Ensure no client-side file imports astro:content
// In vite.config or astro.config, you can add a build-time check:
function assertNoClientContentImport(filePath: string, isClient: boolean) {
  if (isClient) {
    const content = readFileSync(filePath, 'utf-8');
    if (content.includes("astro:content")) {
      throw new Error(`${filePath} imports astro:content but runs client-side`);
    }
  }
}

Type guard

// Server-only context check
function isServerSide(): boolean {
  return typeof window === 'undefined';
}

Prevention

When it happens

Trigger: The virtual module plugin's `load` hook is invoked with `isClient: true`. Code in a `.astro` component's client-side script, a `.js`/`.ts` file imported by client script, or a framework component (React/Vue/Svelte) running in the browser imports from `astro:content`.

Common situations: Trying to query content collections inside a React `useEffect` or Vue `onMounted`. Importing `getCollection` in a file that's bundled for the client. Accidentally adding `client:` directives to a component that imports `astro:content`. Moving server-side logic into a shared module that also gets imported client-side.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/3ee4412df2fd358e. Report an issue: GitHub.