claude
This commit is contained in:
parent
fcb0e25ebd
commit
3441a7e4af
108 changed files with 641 additions and 869 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { defineCollection, reference } from 'astro:content';
|
||||
import { defineCollection } from 'astro:content';
|
||||
import type { SchemaContext } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
import { z } from 'astro/zod';
|
||||
|
|
@ -41,6 +41,17 @@ function isIframeUrl(url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export const TAGS = [
|
||||
'ai',
|
||||
'systems',
|
||||
'graphics',
|
||||
'simulation',
|
||||
'embedded',
|
||||
'web',
|
||||
'tools',
|
||||
'games',
|
||||
] as const;
|
||||
|
||||
const linkSchema = z.object({
|
||||
label: z.string(),
|
||||
url: linkUrl,
|
||||
|
|
@ -53,6 +64,15 @@ const thumbnailSchema = ({ image }: SchemaContext) =>
|
|||
alt: z.string().min(1, 'Thumbnail alt text must not be empty.'),
|
||||
});
|
||||
|
||||
// Partial thumbnail used by the project card to override just the image, just
|
||||
// the alt text, or both. Anything omitted falls back to the entry's top-level
|
||||
// thumbnail, so a card that only rewords its alt doesn't repeat the `src`.
|
||||
const thumbnailOverrideSchema = ({ image }: SchemaContext) =>
|
||||
z.object({
|
||||
src: image().optional(),
|
||||
alt: z.string().min(1, 'Thumbnail alt text must not be empty.').optional(),
|
||||
});
|
||||
|
||||
const mediaSchema = ({ image }: SchemaContext) =>
|
||||
z
|
||||
.discriminatedUnion('type', [
|
||||
|
|
@ -97,73 +117,72 @@ const mediaSchema = ({ image }: SchemaContext) =>
|
|||
}
|
||||
);
|
||||
|
||||
const posts = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/posts' }),
|
||||
// A single collection where each entry can carry an `article` facet (a written
|
||||
// page under /articles/<slug>), a `project` facet (a card in the /projects/
|
||||
// index), or both. Fields shared by both facets live at the top level and are
|
||||
// written once; the few where the project card deliberately differs from the
|
||||
// article are overridden inside `project`. The markdown body is the article's
|
||||
// content.
|
||||
const articleFacet = ({ image }: SchemaContext) =>
|
||||
z.object({
|
||||
updated: z.coerce.date().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
iframeThumbnail: z.boolean().default(false),
|
||||
featuredOrder: z.number().optional(),
|
||||
tags: z.array(z.enum(TAGS)).default([]),
|
||||
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'),
|
||||
media: z.array(mediaSchema({ image })).default([]),
|
||||
});
|
||||
|
||||
const projectFacet = ({ image }: SchemaContext) =>
|
||||
z.object({
|
||||
selected: z.boolean().default(false),
|
||||
// Card chips. When omitted, the card falls back to the article's `stack`.
|
||||
technologies: z.array(z.string()).optional(),
|
||||
// Card overrides. When omitted, the card uses the top-level value.
|
||||
title: z.string().optional(),
|
||||
description: z.string().max(160).optional(),
|
||||
thumbnail: thumbnailOverrideSchema({ image }).optional(),
|
||||
});
|
||||
|
||||
const work = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/work' }),
|
||||
schema: ({ image }) =>
|
||||
z
|
||||
.object({
|
||||
// Shared identity (single source of truth).
|
||||
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 }),
|
||||
iframeThumbnail: z.boolean().default(false),
|
||||
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'),
|
||||
period: z.string().optional(),
|
||||
date: z.coerce.date(),
|
||||
links: z.array(linkSchema).default([]),
|
||||
media: z.array(mediaSchema({ image })).default([]),
|
||||
|
||||
article: articleFacet({ image }).optional(),
|
||||
project: projectFacet({ image }).optional(),
|
||||
})
|
||||
.refine((entry) => Boolean(entry.article) || Boolean(entry.project), {
|
||||
message: 'An entry needs at least an `article` or a `project` facet.',
|
||||
})
|
||||
.refine(
|
||||
(post) =>
|
||||
!post.iframeThumbnail ||
|
||||
post.links.some(
|
||||
(entry) =>
|
||||
!entry.article?.iframeThumbnail ||
|
||||
entry.links.some(
|
||||
(link) =>
|
||||
!link.download &&
|
||||
link.label.trim().toLowerCase() === 'demo' &&
|
||||
isIframeUrl(link.url)
|
||||
),
|
||||
{
|
||||
path: ['iframeThumbnail'],
|
||||
path: ['article', 'iframeThumbnail'],
|
||||
message:
|
||||
'iframeThumbnail requires a non-download Demo link with an https or root-relative URL.',
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
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 };
|
||||
export const collections = { work };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue