@contedra/astro-loader-firestore
v0.5.0
Published
Astro Content Layer loader for Firestore collections backed by contedra content models
Maintainers
Readme
@contedra/astro-loader-firestore
Astro Content Layer loader for Firestore collections backed by contedra content models. Loads content and exposes it through Astro's Content Layer API (v5+).
Installation
pnpm add @contedra/astro-loader-firestorePeer dependency: astro >= 5.0.0
Usage
// src/content.config.ts
import { defineCollection } from "astro:content";
import { contedraLoader } from "@contedra/astro-loader-firestore";
const blogPosts = defineCollection({
loader: contedraLoader({
modelFile: "./models/blog_posts.json",
firebaseConfig: {
projectId: "your-project-id",
credential: "./service-account.json",
},
}),
});
export const collections = { blogPosts };Options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| modelFile | string | Yes | Path to the content model JSON file (single ModelDefinition or ModelManifest) |
| modelName | string | Conditional | Required when modelFile is a ModelManifest containing multiple models |
| firebaseConfig.projectId | string | Yes | Firebase project ID |
| firebaseConfig.credential | string | No | Path to service account JSON (uses ADC if omitted) |
| firebaseConfig.storageBucket | string | No | Firebase Storage bucket name (required for assets) |
| collection | string | No | Firestore collection name (defaults to modelName) |
| bodyField | string | No | Field to map to Astro's body (auto-detects element: "markdown" fields) |
| assets | AssetOptions | No | Asset resolution options for asset:// URIs (see below) |
Loading from a multi-model manifest file
When the modelFile is a ModelManifest ({ "models": [...] }) carrying several models in one file, pass modelName to pick the entry to load:
const blogPosts = defineCollection({
loader: contedraLoader({
modelFile: "./models/site-models.json",
modelName: "blog_posts",
firebaseConfig: { projectId: "your-project-id" },
}),
});For single-model files ({ id, modelName, properties }), modelName is optional. See @contedra/core for the full selection matrix.
Asset Resolution
The loader can resolve asset:// URIs (created by @contedra/md-importer) to either public Firebase Storage URLs or locally downloaded files.
URL Mode (default)
Converts asset:// URIs to Firebase Storage public URLs. No files are downloaded.
const blogPosts = defineCollection({
loader: contedraLoader({
modelFile: "./models/blog_posts.json",
firebaseConfig: {
projectId: "your-project-id",
credential: "./service-account.json",
storageBucket: "your-project.firebasestorage.app",
},
assets: {}, // mode defaults to "url"
}),
});Download Mode
Downloads assets from Firebase Storage at build time and serves them as local files.
const blogPosts = defineCollection({
loader: contedraLoader({
modelFile: "./models/blog_posts.json",
firebaseConfig: {
projectId: "your-project-id",
credential: "./service-account.json",
storageBucket: "your-project.firebasestorage.app",
},
assets: {
mode: "download",
// All options below are optional:
cacheDir: "./.asset-cache", // default
outputDir: "./public/assets", // default
publicPath: "/assets", // default
},
}),
});Asset Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| mode | "url" \| "download" | "url" | Resolution mode |
| cacheDir | string | "./.asset-cache" | Local cache directory (download mode) |
| outputDir | string | "./public/assets" | Output directory for serving (download mode) |
| publicPath | string | "/assets" | URL prefix for resolved assets (download mode) |
CI Cache (GitHub Actions)
Cache the .asset-cache directory to speed up builds:
- uses: actions/cache@v4
with:
path: .asset-cache
key: assets-${{ hashFiles('src/content.config.ts') }}
restore-keys: assets-Migrating from resolveAssetUrl()
If you previously resolved asset:// URIs at render time with a custom helper, you can now remove it:
Before:
// src/lib/assets.ts
export function resolveAssetUrl(uri: string): string {
return uri.replace("asset://", "https://firebasestorage.googleapis.com/...");
}
// In templates:
<img src={resolveAssetUrl(post.data.thumbnail)} />After:
// src/content.config.ts — add assets option
assets: { mode: "url" }
// In templates — URIs are already resolved:
<img src={post.data.thumbnail} />Data Type Mapping
| dataType | Zod Schema | Description |
|------------|------------|-------------|
| string | z.string() | Plain string |
| datetime | z.coerce.date() | Firestore Timestamp converted to Date |
| relatedOne | z.string() | Referenced document ID |
| relatedMany | z.array(z.string()) | Array of referenced document IDs |
| asset | z.string() | Resolved asset URL string (see Asset Resolution). MVP supports mediaType: "image"; future schema versions will add video / audio / file. |
asset dataType vs. legacy string + --image-fields
Two ways exist to carry image references through the pipeline. Both end up resolved by the loader transparently — getCollection().data.cover is a plain URL string in templates regardless of which path was used.
| Source | Model declaration | md-importer side | Loader side |
|--------|-------------------|------------------|-------------|
| Recommended | dataType: "asset", mediaType: "image" | Frontmatter value is auto-uploaded — no --image-fields needed | asset:// URI in Firestore is auto-resolved per assets config |
| Legacy / backward-compat | dataType: "string" | Pass --image-fields cover,… to upload | Same — the loader resolves any asset:// URI it encounters in string values |
The loader does not branch on dataType for asset resolution — it walks every string value in the document and replaces asset:// URIs. So both columns behave identically downstream once the data lands in Firestore. The only practical difference is whether md-importer auto-uploads (asset dataType) or requires opt-in (--image-fields).
Resolving References
The loader maps relatedOne and relatedMany fields to document ID strings. Resolve them with getEntry():
---
import { getCollection, getEntry } from "astro:content";
const posts = await getCollection("blogPosts");
for (const post of posts) {
const tags = [];
for (const tagId of post.data.tags ?? []) {
const tag = await getEntry("tags", tagId);
if (tag) tags.push(tag);
}
}
---Note: Astro's
reference()helper cannot be used in custom loader schemas because the Loaderschemafunction does not receive a schema context that includesreference(). Use manual resolution withgetEntry()instead.
Authentication
With credential (local development):
firebaseConfig: {
projectId: "your-project-id",
credential: "./service-account.json",
}Without credential (Application Default Credentials):
firebaseConfig: {
projectId: "your-project-id",
}When credential is omitted, the loader uses ADC. Set the GOOGLE_APPLICATION_CREDENTIALS environment variable or use Workload Identity Federation in CI/CD.
License
MIT
