Content-Driven Architecture

Purpose and Scope

Astro’s content-driven architecture is the system that turns structured project content into queryable, renderable application data. In user-facing terms, this is the foundation behind content collections: named groups of entries such as blog posts, product records, recipes, documentation pages, or remote records with a shared shape. In source terms, the architecture is split across configuration primitives, a build-time content layer, a serialized data store, and runtime query helpers. Those parts let Astro validate content, track changes, render Markdown-like entries, and expose stable APIs through modules such as astro:content.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/content-layer.ts, packages/astro/src/content/data-store.ts, packages/astro/src/content/runtime.ts

The important distinction is that content is not treated as arbitrary files once it enters the system. A collection definition describes how entries are loaded and optionally validated. The content layer runs loaders and writes normalized entries into a store. The runtime reads that store and returns entries with collection metadata, transformed image references, and rendering support. This separation keeps authoring APIs high-level while allowing the implementation to handle file watching, hashing, markdown rendering, virtual module loading, and live collection validation behind the scenes.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/content-layer.ts, packages/astro/src/content/data-store.ts, packages/astro/src/content/runtime.ts

Relevant Source Files

  • packages/astro/src/content/config.ts - Defines collection configuration types such as DataEntry, DataStore, MetaStore, SchemaContext, CollectionConfig, LiveCollectionConfig, and defineLiveCollection().
  • packages/astro/src/content/content-layer.ts - Implements the ContentLayer class that coordinates syncing, loader context creation, file watching, digest generation, markdown rendering, and writes into a mutable data store.
  • packages/astro/src/content/data-store.ts - Defines the immutable runtime store, the DataEntry and RenderedContent shapes, and loading from the astro:data-layer-content virtual module.
  • packages/astro/src/content/runtime.ts - Implements runtime-facing helpers including live collection validation support and createGetCollection(), which reads entries from the global data store.

Core Primitives

The configuration layer defines the vocabulary used by content collections. A DataEntry has an id, parsed data, and optional filePath and body fields at the public configuration boundary. The runtime data-store version expands that shape with digest, rendered content, deferred rendering, and asset imports. A DataStore exposes collection-local operations such as get, set, values, entries, keys, has, delete, and clear, while MetaStore stores string metadata beside a collection. SchemaContext currently includes an image helper, allowing schemas to describe image metadata in a way that stays aligned with Astro’s image pipeline.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/data-store.ts

CollectionConfig models several collection modes rather than a single file convention. A content-layer collection has a loader and optional schema, and the loader can be an object loader or an async function returning either an array of entries or a record keyed by entry id. Data collections and content collections are represented separately for compatibility and type clarity: data collections are explicitly typed as data, while content collections do not accept a loader in the shown type. LiveCollectionConfig is separate again, with a live loader, optional schema, and a live content type.

Sources: packages/astro/src/content/config.ts

import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
 
const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: z.object({ title: z.string() }),
});
 
export const collections = { blog };

The official docs frame this as the recommended way to manage sets of structurally similar content. The source reflects that framing by making loaders and schemas first-class inputs rather than incidental implementation details. A loader answers where entries come from, while a schema answers what each entry should look like. Because those choices are captured in configuration, the rest of Astro can provide predictable query APIs, type generation, validation, and rendering behavior without every page needing to know whether the content came from Markdown files, JSON, a CMS, or a custom data source.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/runtime.ts

System-to-Code Mapping

The content layer is the build-time orchestration point. ContentLayer receives Astro settings, a logger, a mutable data store, and optionally a Vite file watcher. Internally it wraps the watcher, holds the last content configuration digest, subscribes to a content configuration observer, and uses a PQueue with concurrency one. That queue is a significant design choice: content refreshes are serialized, which avoids overlapping sync work while files or configuration are changing. The public loading getter reports whether queued or pending content work is still active.

Sources: packages/astro/src/content/content-layer.ts

When the content configuration changes, watchContentConfig subscribes to the global observer and calls sync only when a loaded configuration has a digest different from the previous one. Digest generation is delegated to xxhash-wasm and cached after first use. The hash is explicitly described in the source as a fast non-cryptographic content digest, suitable for detecting content changes rather than protecting secrets. Together, the observer, digest, and queue give Astro a repeatable refresh model for both development and build-like workflows.

Sources: packages/astro/src/content/content-layer.ts

The loader context created by ContentLayer is the bridge from user-defined loaders to Astro internals. It includes the collection name, a scoped store for the collection, a meta store, a forked integration logger, the active Astro config, a parseData function, and markdown rendering support. That shape explains why loader authors can interact with the content layer without owning the whole pipeline: they receive a controlled interface for writing entries, validating data, reporting messages, and rendering content when applicable.

Sources: packages/astro/src/content/content-layer.ts

Data Store and Runtime Flow

The data-store module defines ImmutableDataStore as the read-only runtime view of loaded content. It stores a map of collection names to maps of entry ids to entries. Query methods are intentionally simple: get returns a single entry, entries returns id-entry pairs, values returns entries, keys returns ids, has checks one entry, hasCollection checks the collection map, and collections exposes the underlying collection map. Runtime code reads from this immutable view; content loading and mutation belong to the mutable store used by ContentLayer.

Sources: packages/astro/src/content/data-store.ts, packages/astro/src/content/content-layer.ts

ImmutableDataStore.fromModule shows how the build-time result crosses into runtime. It attempts to import the astro:data-layer-content virtual module, then accepts either a Map directly or a devalue-unflattened serialized representation. If loading fails, it returns an empty store. The module-level globalDataStore singleton memoizes that async loading operation and also allows a store to be injected. This design supports normal Vite-backed runtime imports as well as tests or programmatic rendering paths that need to provide their own store.

Sources: packages/astro/src/content/data-store.ts

RenderedContent captures the rendering side of content entries. It stores rendered HTML and optional metadata such as image paths, headings, raw frontmatter, and other renderer-provided fields. DataEntry can also mark deferredRender, meaning rendering is delegated to a virtual module during the runtime phase when renderEntry is called. The same entry can carry assetImports for images or other transformed assets. This makes the store more than a database of frontmatter: it is also the handoff point for content rendering metadata.

Sources: packages/astro/src/content/data-store.ts

Runtime APIs and Live Collections

The runtime module turns stored entries into public query behavior. createGetCollection returns an async getCollection function. It first checks whether the requested collection is actually a live collection and, if so, throws an AstroError telling the caller to use getLiveCollection() instead. For build-time collections, it reads the global data store, imports astro:asset-imports, updates image references in entry data, attaches the collection name to each entry, applies a function filter when provided, and returns the result array.

Sources: packages/astro/src/content/runtime.ts, packages/astro/src/content/data-store.ts

Live collections use a different runtime contract because they are request-time data sources rather than preloaded build-time entries. defineLiveCollection enforces that live collections are defined from a src/live.config.ts file, defaults the type to the live content type, and rejects non-live types with a LiveContentConfigError. At runtime, parseLiveEntry validates a live entry with a Zod schema and also validates cacheHint metadata. Valid cache hints may include tags and lastModified; invalid entry data or cache hints are returned as live collection errors.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/runtime.ts

That split is central to Astro’s content architecture. Build-time collections optimize for loading, validating, serializing, and querying a known content graph. Live collections optimize for fresh remote data and explicit error handling at access time. The public docs describe both as content collections, but the implementation keeps their configuration files, loader types, runtime APIs, and validation paths distinct. Developers should choose build-time collections for content that can be known during sync or build, and live collections for data that must be current at request time.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/runtime.ts

Implementation Details and Design Constraints

The architecture uses virtual modules as stable boundaries between phases. astro:data-layer-content carries serialized collection data into runtime, while astro:asset-imports is loaded when getCollection prepares entries with image-aware data. These imports are intentionally hidden behind runtime helpers, so application code can keep using content APIs rather than importing generated files directly. The same pattern appears in deferred rendering: an entry can record that rendering work should happen later through a virtual module instead of being fully materialized in the store immediately.

Sources: packages/astro/src/content/data-store.ts, packages/astro/src/content/runtime.ts

Error boundaries are also explicit. Configuration errors use AstroError and AstroErrorData, including live content configuration errors when defineLiveCollection is called from the wrong config file or with the wrong type. Runtime live errors are specialized as LiveCollectionError, LiveCollectionCacheHintError, LiveEntryNotFoundError, and LiveCollectionValidationError. For build-time getCollection calls, an empty or missing collection logs a warning and returns an empty array. These different responses reflect different failure modes: invalid configuration should stop the developer, while an absent collection query can be reported without crashing immediately.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/runtime.ts

Compact Reference

ComponentSource contractRole in the architecture
DataEntryid, data, optional filePath, body, digest, rendered, deferredRender, assetImportsNormalized representation of one collection entry.
RenderedContenthtml plus metadata such as imagePaths, headings, frontmatterStores render output and renderer metadata for content entries.
DataStore / ImmutableDataStoreget, entries, values, keys, has, hasCollectionRuntime read API over collection maps.
MetaStoreget, set, delete, hasCollection-local string metadata used by loaders.
CollectionConfigcontent, data, or content_layer collection configurationDescribes build-time collection behavior and schemas.
LiveCollectionConfiglive loader, optional schema, live typeDescribes request-time live content collections.
ContentLayersync orchestration, watcher integration, loader context, digestingRuns loaders and writes normalized content into the store.
createGetCollection()returns async getCollection(collection, filter?)Runtime query factory for build-time collections.

Sources: packages/astro/src/content/config.ts, packages/astro/src/content/content-layer.ts, packages/astro/src/content/data-store.ts, packages/astro/src/content/runtime.ts

Next Steps

Read Content Collections next if you want the author-facing workflow for defining collections, schemas, and queries. Read Content Loaders if you need to bring content from local files, remote APIs, or a custom source into the content layer. For runtime module details, continue to Astro Modules Reference, especially astro:content and related virtual modules. If you are building a content-heavy site, start by choosing build-time versus live collections, then design schemas around the fields your pages, feeds, and endpoints actually consume.