Reputation: 1736
I have a collection of blog posts stored as markdown files. Each file has some yaml frontmatter, and most contain a date
timestamp in YYYY-MM-DD
format, but not all. For the files
that don't have a date
key, I want to set the date to a value extracted from the markdown filename, which starts with a YYYY-MM-DD
timestamp.
It seems like this should be relatively straightforward to do, but I haven't found any way to either modify the frontmatter before it's parsed by zod, or to access the filename from within a .transform()
schema method.
There are certainly workarounds, but none of them feel great. I can call getCollection()
, map over the array, and return new entries with the missing dates filled in. But then the date
type will still be Date|undefined
, since it had to be made optional in the schema so that every file is parsed. I could also just modify the frontmatter in all the markdown files directly, but that may not always be possible in the general case.
There may be a way to create a custom loader with the new content collections API in Astro v5, and thereby modify the entries before they're processed, but the docs are pretty thin and I haven't found any examples in the wild yet.
Is there a better approach to doing this?
Upvotes: 1
Views: 280
Reputation: 1736
Not sure if it's the best approach, but this is where I've ended up. The globWithParser()
function below creates a glob()
loader and then injects a custom parser()
function that can modify the entry before it's passed to the loader's parseData()
function.
// globWithParser.ts
import { glob, type ParseDataOptions } from "astro/loaders";
type Parser = <TData extends Record<string, unknown>>(
options: ParseDataOptions<TData>
) => Promise<ParseDataOptions<TData>>;
type GlobWithParserOptions = Parameters<typeof glob>[0] & {
parser: Parser,
};
export function globWithParser({
parser,
...globOptions }: GlobWithParserOptions)
{
const loader = glob(globOptions);
const originalLoad = loader.load;
loader.load = async ({ parseData, ...rest }) => originalLoad({
parseData: async (entry) => parseData(await parser(entry)),
...rest
});
return loader;
}
That function can then be used to create a collection of blog posts that intercepts each entry and adds the date
key if it's missing:
// content.config.ts
const blog = defineCollection({
loader: globWithParser({
pattern: "**/*.md",
base: "./src/content/blog",
parser: async (entry) => {
const { id, data } = entry;
if (!data.date) {
// cast the data object to keep TypeScript happy
(data as { date?: string }).date = id.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
}
return entry;
}
}),
schema: ...
});
Having to cast the data inside the parser function is not ideal, but this approach does enable the Zod schema to define the date
key as required instead of optional, since all the blog posts will have one before the schema is checked.
Upvotes: 1