What Causes This Error?
This error occurs when a remark or rehype plugin modifies the frontmatter in an invalid way, such as setting non-serializable values or invalid data types that conflict with your schema.
The Problem
// A remark plugin injecting invalid data
function remarkPlugin() {
return (tree, file) => {
// ❌ Injecting a function (non-serializable)
file.data.astro.frontmatter.compute = () => {};
// ❌ Injecting circular reference
const obj = {};
obj.self = obj;
file.data.astro.frontmatter.data = obj;
};
}
The Fix
Inject Serializable Data Only
// ✅ Valid frontmatter injection
function remarkPlugin() {
return (tree, file) => {
// Strings, numbers, booleans, arrays, plain objects
file.data.astro.frontmatter.readingTime = '5 min';
file.data.astro.frontmatter.wordCount = 1234;
file.data.astro.frontmatter.tags = ['astro', 'markdown'];
};
}
Common Scenarios
Reading Time Plugin
// remark-reading-time.js
import getReadingTime from 'reading-time';
import { toString } from 'mdast-util-to-string';
export function remarkReadingTime() {
return function (tree, file) {
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
// ✅ Inject serializable values
file.data.astro.frontmatter.minutesRead = readingTime.text;
};
}
// astro.config.mjs
import { remarkReadingTime } from './remark-reading-time.js';
export default defineConfig({
markdown: {
remarkPlugins: [remarkReadingTime],
},
});
Update Schema for Injected Fields
// src/content/config.ts
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
// ✅ Add fields that plugins inject
minutesRead: z.string().optional(),
wordCount: z.number().optional(),
}),
});
Auto-Generate Fields
// remark-auto-meta.js
export function remarkAutoMeta() {
return function (tree, file) {
const frontmatter = file.data.astro.frontmatter;
// ✅ Only add if not already set
if (!frontmatter.lastModified) {
frontmatter.lastModified = new Date().toISOString();
}
};
}
Table of Contents Injection
// remark-toc-data.js
import { toc } from 'mdast-util-toc';
export function remarkTocData() {
return function (tree, file) {
const result = toc(tree);
// ✅ Convert to serializable format
const headings = extractHeadings(result.map);
file.data.astro.frontmatter.headings = headings;
};
}
function extractHeadings(node) {
// Return plain objects/arrays only
if (!node) return [];
// ... extraction logic returning strings/objects
}
Debugging Plugin Output
export function remarkDebug() {
return function (tree, file) {
console.log('Current frontmatter:', file.data.astro.frontmatter);
// Test serialization
try {
JSON.stringify(file.data.astro.frontmatter);
console.log('✅ Frontmatter is serializable');
} catch (e) {
console.error('❌ Frontmatter has non-serializable data:', e);
}
};
}
Valid Injection Types
// ✅ Valid types for frontmatter
file.data.astro.frontmatter.string = 'text';
file.data.astro.frontmatter.number = 42;
file.data.astro.frontmatter.boolean = true;
file.data.astro.frontmatter.array = ['a', 'b', 'c'];
file.data.astro.frontmatter.object = { key: 'value' };
file.data.astro.frontmatter.null = null;
file.data.astro.frontmatter.date = '2024-01-15'; // String, not Date object
// ❌ Invalid types
file.data.astro.frontmatter.func = () => {};
file.data.astro.frontmatter.symbol = Symbol('x');
file.data.astro.frontmatter.undefined = undefined;
file.data.astro.frontmatter.map = new Map();
file.data.astro.frontmatter.set = new Set();
Third-Party Plugin Issues
// If a third-party plugin causes issues:
// 1. Check plugin documentation
// 2. Update to latest version
// 3. Wrap with sanitization
import problematicPlugin from 'some-plugin';
function safePlugin() {
const plugin = problematicPlugin();
return function (tree, file) {
plugin(tree, file);
// Sanitize after
file.data.astro.frontmatter = JSON.parse(
JSON.stringify(file.data.astro.frontmatter)
);
};
}
Quick Checklist
- Only inject JSON-serializable data
- No functions, Symbols, Maps, Sets
- Update schema for injected fields
- Test with
JSON.stringify() - Use strings for dates, not Date objects