import { defineCollection, reference } from 'astro:content'; import { file, glob } from 'astro/loaders'; import { z } from 'astro/zod'; import { ComponentReferenceSchema } from './types/component-reference'; import { SUPPORTED_FRAMEWORKS } from './types/docs'; import { FeatureReferenceSchema } from './types/feature-reference'; import { MediaReferenceSchema } from './types/media-reference'; import { PresetReferenceSchema } from './types/preset-reference'; import { UtilReferenceSchema } from './types/util-reference'; import { defaultGitService } from './utils/gitService'; import { globWithParser } from './utils/globWithParser'; /** * Extract date from filename in format: YYYY-MM-DD-slug.mdx * Throws an error if the filename doesn't match the expected pattern */ export function extractDateFromFilename(id: string): Date { const match = id.match(/^(\d{4})-(\d{2})-(\d{2})-/); if (!match) { throw new Error(`Filename "${id}" must follow format: YYYY-MM-DD-slug.mdx`); } const [, year, month, day] = match; return new Date(`${year}-${month}-${day}`); } const blog = defineCollection({ // Load MDX files in the `src/content/blog/` directory. loader: globWithParser({ base: './src/content/blog', pattern: '**/*.mdx', generateId: ({ entry }) => { // Remove date prefix and extension from slug (e.g., "2022-07-08-first-post.mdx" -> "first-post") return entry.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.mdx$/, ''); }, parser: async (entry, originalEntry) => { // Extract pubDate from original filename (before date prefix was removed) const pubDate = extractDateFromFilename(originalEntry); // Get updatedDate from git history (last modification date) const filePath = `site/src/content/blog/${originalEntry}`; const updatedDate = await defaultGitService.getLastModifiedDate(filePath); // Return transformed entry with added fields return { ...entry, data: { ...entry.data, pubDate, ...(updatedDate && updatedDate.getTime() !== pubDate.getTime() ? { updatedDate } : {}), }, }; }, }), // Type-check frontmatter using a schema schema: ({ image }) => z.object({ title: z.string(), description: z.string(), pubDate: z.date(), updatedDate: z.coerce.date().optional(), authors: z.array(reference('authors')), canonical: z.url().optional(), devOnly: z.boolean().optional(), // only visible in development mode ogTitle: z.string().optional(), ogImage: image().or(z.url()).optional(), twitterImage: image().or(z.url()).optional(), }), }); const docs = defineCollection({ loader: globWithParser({ base: './src/content/docs', pattern: '**/*.mdx', parser: async (entry, originalEntry) => { // Get updatedDate from git history const filePath = `site/src/content/docs/${originalEntry}`; const updatedDate = await defaultGitService.getLastModifiedDate(filePath); // Return transformed entry with added field if updatedDate exists return { ...entry, data: { ...entry.data, ...(updatedDate ? { updatedDate } : {}), }, }; }, }), schema: z.object({ title: z.string(), description: z.string(), updatedDate: z.coerce.date().optional(), ogTitle: z.string().optional(), frameworkTitle: z.partialRecord(z.enum(SUPPORTED_FRAMEWORKS as [string, ...string[]]), z.string()).optional(), }), }); // Release notes generated by the release workflow (raw git-cliff bullets), // then rewritten into prose by the changelog-prose workflow. MDX lets the // changelog use the same typography component mapping as the blog. const changelog = defineCollection({ loader: glob({ base: './src/content/changelog', pattern: '*.mdx', generateId: ({ entry }) => entry.replace(/\.mdx$/, ''), }), schema: z.object({ description: z.string(), date: z.coerce.date(), version: z.string(), prerelease: z.boolean(), breaking: z.boolean(), compareUrl: z.url(), }), }); const authors = defineCollection({ loader: file('./src/content/authors.json'), schema: z.object({ name: z.string(), shortName: z.string(), bio: z.string().optional(), avatar: z.string().optional(), socialLinks: z .object({ x: z.string().optional(), bluesky: z.string().optional(), mastodon: z.string().optional(), github: z.string().optional(), linkedin: z.string().optional(), website: z.string().optional(), }) .optional(), }), }); const componentReference = defineCollection({ loader: glob({ pattern: '*.json', base: './src/content/generated-component-reference', }), schema: ComponentReferenceSchema, }); const utilReference = defineCollection({ loader: glob({ pattern: '*.json', base: './src/content/generated-util-reference', }), schema: UtilReferenceSchema, }); const featureReference = defineCollection({ loader: glob({ pattern: '*.json', base: './src/content/generated-feature-reference', }), schema: FeatureReferenceSchema, }); const mediaReference = defineCollection({ loader: glob({ pattern: '*.json', base: './src/content/generated-media-reference', }), schema: MediaReferenceSchema, }); const presetReference = defineCollection({ loader: glob({ pattern: '*.json', base: './src/content/generated-preset-reference', }), schema: PresetReferenceSchema, }); const ejectedSkins = defineCollection({ loader: file('./src/content/ejected-skins.json'), schema: z.object({ id: z.string(), name: z.string(), platform: z.enum(['html', 'react']), style: z.enum(['css', 'tailwind']), html: z.string().optional(), tsx: z.record(z.string(), z.string()).optional(), jsx: z.record(z.string(), z.string()).optional(), css: z.string().optional(), }), }); // Media subpaths that ship a CDN build, generated by scripts/build-cdn-manifest.ts. // Each entry's id is a media subpath (e.g. `hlsjs-video`). Used by the installation // guide to hide the CDN install option for renderers with no CDN bundle. const cdnMedia = defineCollection({ loader: file('./src/content/cdn-media.json'), schema: z.object({ id: z.string(), }), }); export const collections = { blog, docs, changelog, authors, componentReference, utilReference, featureReference, mediaReference, presetReference, ejectedSkins, cdnMedia, };