astro-loader-quarto
v1.0.0
Published
Astro Loader for Quarto content collections with full type safety and field mapping
Maintainers
Readme
astro-loader-quarto
THIS PROJECT IS WORK IN PROGRESS. DO NOT USE.
Astro Loader for Quarto content collections with full type safety and field mapping
Enable your Astro projects to use Quarto Listings as Content Collections through a custom Astro Loader. Author content in Quarto (.qmd files) and consume it seamlessly in Astro with full type safety and schema validation.
Features
- 🚀 Zero-config setup - Works out of the box with sensible defaults
- 🎯 Type-safe - Full TypeScript support with auto-generated Zod schemas
- 🔄 Field mapping - Seamlessly map Quarto field names to Astro conventions
- 🎨 Flexible - Support for multiple listings, custom filters, and transforms
- ⚡ Fast - Parallel parsing with intelligent caching
- 🔥 Hot reload - File watching for development
- 📦 Multiple listings - Load different content types from one Quarto project
Requirements
- Astro: 4.0 or higher (required for Loader API)
- Quarto: 1.3 or higher
- Node.js: 18 or higher
Installation
npm install astro-loader-quartoQuick Start
1. Set up your Quarto project
Create a Quarto project with a listing in _quarto.yml:
# quarto/_quarto.yml
project:
type: website
output-dir: _site
format: gfm # REQUIRED: GitHub Flavored Markdown for Astro compatibility
listing:
- id: blog-posts
contents: posts/*.qmd
sort: "date desc"
type: gridCreate some .qmd files:
---
# quarto/posts/my-first-post.qmd
title: "My First Post"
description: "An introduction to my blog"
author: "Jane Doe"
date: "2025-11-24"
image: "featured.jpg"
categories: ["Technology"]
tags: ["astro", "quarto"]
---
# My First Post
Your content here...2. Configure Astro content collection
// src/content/config.ts
import { defineCollection } from "astro:content";
import { quartoLoader } from "astro-loader-quarto";
const blog = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "blog-posts",
}),
});
export const collections = { blog };3. Render Quarto content
cd quarto && quarto render4. Use in your Astro pages
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post }
}));
}
const { post } = Astro.props;
const { Content } = await post.render(); // Render markdown content
---
<article>
{post.data.heroImage && (
<img src={post.data.heroImage} alt={post.data.title} />
)}
<h1>{post.data.title}</h1>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString()}
</time>
<Content /> <!-- Rendered Quarto markdown content -->
</article>Default Field Mappings
The loader applies these mappings by default to match Astro's blog template conventions:
| Quarto Field | Astro Field | Type |
| --------------- | ------------- | ----------------------------- |
| date | pubDate | Date |
| date-modified | updatedDate | Date (optional) |
| image | heroImage | string (optional) |
| title | title | string |
| description | description | string (optional) |
| author | author | string | string[] (optional) |
| categories | categories | string[] (optional) |
| tags | tags | string[] (optional) |
| draft | draft | boolean |
All other fields pass through with their original Quarto names.
Custom Field Mappings
You can customize field mappings to match your needs:
const blog = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "blog-posts",
fieldMappings: {
date: "publishedAt", // Override default
"date-modified": "updatedAt", // Override default
image: "coverImage", // Override default
"reading-time": "readingMinutes", // Custom field
},
}),
});Advanced Usage
Multiple Listings
Load different content types from multiple listings:
const blog = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "blog-posts",
}),
});
const docs = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "documentation",
fieldMappings: {
date: "lastModified",
version: "version",
},
}),
});
export const collections = { blog, docs };Filtering Entries
Filter entries based on custom logic:
const blog = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "blog-posts",
filter: (entry) => {
// Only published posts, not in the future
return !entry.draft && new Date(entry.pubDate) <= new Date();
},
}),
});Transforming Entries
Add computed fields or modify entries:
const blog = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "blog-posts",
transform: (entry) => ({
...entry,
year: new Date(entry.pubDate).getFullYear(),
excerpt: entry.description || generateExcerpt(entry.content),
}),
}),
});Custom Schema
Extend or override the auto-generated schema:
import { z } from "zod";
const blog = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "blog-posts",
schema: {
extend: z.object({
readingTime: z.number().optional(),
featured: z.boolean().default(false),
}),
},
}),
});Development Workflow
Option 1: Manual Render (Recommended)
Run Quarto and Astro separately:
# Terminal 1: Quarto preview (auto-renders on save)
cd quarto && quarto preview
# Terminal 2: Astro dev server
npm run devOption 2: Auto-Render (Good for CI/CD)
Let the loader automatically render Quarto content:
// src/content/config.ts
const blog = defineCollection({
loader: quartoLoader({
quartoRoot: "./quarto",
listings: "blog-posts",
autoRender: true, // Automatically runs 'quarto render'
}),
});Then just run:
npm run devOption 3: Parallel with npm-run-all
{
"scripts": {
"dev:quarto": "cd quarto && quarto preview",
"dev:astro": "astro dev",
"dev": "npm-run-all --parallel dev:quarto dev:astro"
}
}Production Build
# 1. Render Quarto content
cd quarto && quarto render && cd ..
# 2. Build Astro site
npm run buildConfiguration Options
See docs/api.md for complete API documentation.
interface QuartoLoaderConfig {
quartoRoot: string; // Required: Path to Quarto project
outputDir?: string; // Output directory (default: _site)
listings?: string | string[] | "all"; // Which listing(s) to load
fieldMappings?: FieldMappings; // Custom field name mappings
filter?: (entry) => boolean; // Filter entries
transform?: (entry) => any; // Transform entries
schema?: SchemaConfig; // Schema customization
cache?: boolean; // Enable caching (default: true)
parallel?: boolean; // Parallel processing (default: true)
}Documentation
- API Documentation - Complete configuration reference
- Field Mappings Guide - Detailed field mapping examples
- Examples - More usage examples
Example Project
See examples/basic-blog for a complete working example.
Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests.
License
GPL-3.0 - See LICENSE for details.
Acknowledgments
- Built for Astro and Quarto
- Inspired by the need to combine scientific publishing with modern web development
