Convert projects to content collections
This commit is contained in:
parent
c7e8edfbf4
commit
b20139cb60
35 changed files with 402 additions and 472 deletions
143
src/content.config.ts
Normal file
143
src/content.config.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { defineCollection, reference } from 'astro:content';
|
||||
import type { SchemaContext } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
import { z } from 'astro/zod';
|
||||
|
||||
function isRootRelativeUrl(url: string) {
|
||||
return url.startsWith('/') && !url.startsWith('//');
|
||||
}
|
||||
|
||||
const linkUrl = z.string().refine(
|
||||
(url) => {
|
||||
if (isRootRelativeUrl(url)) return true;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return ['https:', 'mailto:'].includes(parsed.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: 'URL must be an absolute https/mailto URL or a root-relative path.' }
|
||||
);
|
||||
|
||||
const mediaUrl = z.string().refine(
|
||||
(url) => {
|
||||
if (isRootRelativeUrl(url)) return true;
|
||||
try {
|
||||
return new URL(url).protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: 'Media URL must be an absolute https URL or a root-relative path.' }
|
||||
);
|
||||
|
||||
const linkSchema = z.object({
|
||||
label: z.string(),
|
||||
url: linkUrl,
|
||||
download: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const thumbnailSchema = ({ image }: SchemaContext) =>
|
||||
z.object({
|
||||
src: image(),
|
||||
alt: z.string(),
|
||||
});
|
||||
|
||||
const mediaSchema = ({ image }: SchemaContext) =>
|
||||
z
|
||||
.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.enum(['image', 'diagram']),
|
||||
src: image(),
|
||||
alt: z.string().optional(),
|
||||
decorative: z.boolean().optional(),
|
||||
caption: z.string().optional(),
|
||||
transcript: z.string().optional(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
type: z.literal('video'),
|
||||
poster: image().optional(),
|
||||
mp4: mediaUrl.optional(),
|
||||
webm: mediaUrl.optional(),
|
||||
captions: mediaUrl.optional(),
|
||||
captionsLabel: z.string().default('English captions'),
|
||||
alt: z.string().optional(),
|
||||
decorative: z.boolean().optional(),
|
||||
caption: z.string().optional(),
|
||||
transcript: z.string().optional(),
|
||||
})
|
||||
.refine((item) => Boolean(item.mp4) || Boolean(item.webm), {
|
||||
message: 'Video media needs at least one mp4 or webm source.',
|
||||
}),
|
||||
])
|
||||
.refine((item) => item.decorative || (Boolean(item.alt) && Boolean(item.caption)), {
|
||||
message: 'Meaningful media needs both alt text and a caption.',
|
||||
})
|
||||
.refine(
|
||||
(item) => item.type !== 'video' || item.decorative || Boolean(item.captions),
|
||||
{
|
||||
message: 'Meaningful video needs captions.',
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(item) => item.type !== 'video' || item.decorative || Boolean(item.transcript),
|
||||
{
|
||||
message: 'Meaningful video needs a transcript.',
|
||||
}
|
||||
);
|
||||
|
||||
const posts = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string().max(160),
|
||||
date: z.coerce.date(),
|
||||
updated: z.coerce.date().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
thumbnail: thumbnailSchema({ image }),
|
||||
tags: z.array(
|
||||
z.enum([
|
||||
'ai',
|
||||
'systems',
|
||||
'graphics',
|
||||
'simulation',
|
||||
'embedded',
|
||||
'web',
|
||||
'tools',
|
||||
'games',
|
||||
])
|
||||
),
|
||||
featuredOrder: z.number().optional(),
|
||||
projectPeriod: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
stack: z.array(z.string()).optional(),
|
||||
scale: z.string().optional(),
|
||||
outcome: z.string().optional(),
|
||||
audience: z
|
||||
.enum(['general', 'technical', 'recruiter-relevant'])
|
||||
.default('technical'),
|
||||
links: z.array(linkSchema).default([]),
|
||||
media: z.array(mediaSchema({ image })).default([]),
|
||||
}),
|
||||
});
|
||||
|
||||
const projects = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/projects' }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string().max(160),
|
||||
thumbnail: thumbnailSchema({ image }),
|
||||
period: z.string(),
|
||||
sortDate: z.coerce.date(),
|
||||
technologies: z.array(z.string()).default([]),
|
||||
selected: z.boolean().default(false),
|
||||
essay: reference('posts').optional(),
|
||||
links: z.array(linkSchema).default([]),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { posts, projects };
|
||||
Loading…
Add table
Add a link
Reference in a new issue