withastro/astro · error · AstroError

ServerOnlyModule

ServerOnlyModule

Error message

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

What it means

The astro:content virtual module — getCollection, getEntry, render, and config types — is generated for server bundles only. When the module is requested from a client environment build, such as a hydrated island importing it, the plugin throws ServerOnlyModule to stop the dependency on server-only infrastructure from reaching the browser bundle.

Solutions

  1. Fetch entries in an .astro page or component and pass plain serializable data as props into the island.
  2. Remove astro:content imports from any file reachable from a client: directive or an inline <script>.
  3. For type-only usage, use import type so the import is erased at compile time.
  4. Split shared modules into server-only and client-safe variants so data access stays on the server.

Example fix

// before: Island.jsx hydrated with client:load
import { getEntry } from 'astro:content';
export default function Island({ slug }) { /* uses getEntry */ }

// after: page.astro fetches server-side and passes data
---
import { getEntry } from 'astro:content';
import Island from '../components/Island.jsx';
const post = await getEntry('blog', Astro.params.slug);
---
<Island client:load post={post.data} />
Defensive patterns

Strategy: validation

Validate before calling

// Static check: no file under a client-hydrated component dir may import astro:content
import { readdir, readFile } from 'node:fs/promises';
for (const file of await readdir('src/components/islands', { recursive: true })) {
  if (!/\.(jsx|tsx|svelte|vue)$/.test(String(file))) continue;
  const src = await readFile(`src/components/islands/${file}`, 'utf8');
  if (src.includes('astro:content')) console.error(`astro:content imported in island ${file}`);
}

Prevention

When it happens

Trigger: import { getEntry } from 'astro:content' inside a component hydrated with client:load/client:visible (React, Svelte, Vue, Solid); a <script> processed for the browser importing a module that imports astro:content; framework client entrypoints re-exporting a file that pulls it in.

Common situations: Moving data fetching into a shared UI component that later gets hydrated; shared constants files re-exporting from astro:content; refactoring islands so previously server-only code lands in the client graph.

Related errors


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

Appendix: source

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

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 157c500c38)