Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/content/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readdirSync } from 'node:fs';
import { defineCollection, reference, z } from 'astro:content';
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

// Topic ids come from the data files themselves, so post frontmatter is
Expand Down Expand Up @@ -31,7 +31,9 @@ const posts = defineCollection({
z.object({
title: z.string(),
summary: z.string(),
authors: z.array(reference('authors')).min(1),
// Plain author handles, resolved defensively at render (see lib/posts.ts):
// a missing profile falls back to Guest rather than failing the build.
authors: z.array(z.string()).min(1),
date: z.coerce.date(),
readMin: z.number().int().positive(),
// Display name is derived from topicId via topicName(); kept optional for
Expand Down Expand Up @@ -62,7 +64,7 @@ const tools = defineCollection({
tag: z.enum(['Live', 'Beta', 'Experimental', 'Soon']),
icon: z.string().optional(),
thumbnail: z.union([image(), z.string().url()]).optional(),
authors: z.array(reference('authors')).optional(),
authors: z.array(z.string()).optional(),
topics: z.array(z.string()).optional(),
tags: z.array(z.string()).optional(),
repo: z.string().url().optional(),
Expand Down
33 changes: 30 additions & 3 deletions src/lib/posts.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
// Server-only post helpers (astro:content cannot be bundled into client scripts —
// keep this module out of anything imported by <script> code; taxonomy lives in data.ts).
import { getEntries, type CollectionEntry } from 'astro:content';
import { getEntry, type CollectionEntry } from 'astro:content';

type AuthorData = CollectionEntry<'authors'>['data'];

// A resolved author is shaped like an authors-collection entry, but is produced
// defensively: a handle with no profile never throws, it degrades to Guest.
export type ResolvedAuthor = { id: string; data: AuthorData };

export type PostWithAuthors = CollectionEntry<'posts'> & {
authors: CollectionEntry<'authors'>[];
authors: ResolvedAuthor[];
};

const SYNTHETIC_GUEST: ResolvedAuthor = {
id: 'guest',
data: { name: 'Guest Contributor', role: 'contributor' } as AuthorData,
};

// Resolve one handle to an author. Missing profile → Guest, with a build-time
// warning so a broken reference is visible in logs without breaking the build.
async function resolveAuthor(handle: string): Promise<ResolvedAuthor> {
const entry = await getEntry('authors', handle);
if (entry) return { id: entry.id, data: entry.data };
console.warn(`[authors] no profile for "${handle}" — falling back to Guest.`);
const guest = await getEntry('authors', 'guest');
return guest ? { id: guest.id, data: guest.data } : SYNTHETIC_GUEST;
}

export async function resolveAuthors(handles: string[] = []): Promise<ResolvedAuthor[]> {
return Promise.all(handles.map(resolveAuthor));
}

export async function resolvePostAuthors(
posts: CollectionEntry<'posts'>[],
): Promise<PostWithAuthors[]> {
return Promise.all(posts.map(async (p) => ({ ...p, authors: await getEntries(p.data.authors) })));
return Promise.all(
posts.map(async (p) => ({ ...p, authors: await resolveAuthors(p.data.authors) })),
);
}

export function authorNames(post: PostWithAuthors): string {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/authors/[handle]/articles/[...page].astro
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const getStaticPaths = (async ({ paginate }) => {
const all = await getCollection('posts', ({ data }) => !data.draft);
const paths = [];
for (const author of authors) {
const matching = all.filter((p) => p.data.authors.some((ref) => ref.id === author.id));
const matching = all.filter((p) => p.data.authors.some((h) => h === author.id));
const posts = (await resolvePostAuthors(matching)).sort(sortPostsByDate);
paths.push(
...paginate(posts, { params: { handle: author.id }, pageSize: 20, props: { author } }),
Expand Down
2 changes: 1 addition & 1 deletion src/pages/authors/[handle]/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const { author } = Astro.props;
const RECENT_LIMIT = 5;

const allRaw = await getCollection('posts', ({ data }) => !data.draft);
const matching = allRaw.filter((p) => p.data.authors.some((ref) => ref.id === author.id));
const matching = allRaw.filter((p) => p.data.authors.some((h) => h === author.id));
const posts = (await resolvePostAuthors(matching)).sort(sortPostsByDate);
const recent = posts.slice(0, RECENT_LIMIT);

Expand Down
2 changes: 1 addition & 1 deletion src/pages/authors/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const posts = await getCollection('posts', ({ data }) => !data.draft);

const contributors = authors
.map((author) => {
const authorPosts = posts.filter((p) => p.data.authors.some((ref) => ref.id === author.id));
const authorPosts = posts.filter((p) => p.data.authors.some((h) => h === author.id));
const lastDate = authorPosts.reduce<Date | null>((acc, p) => {
const d = p.data.date;
return !acc || d > acc ? d : acc;
Expand Down
5 changes: 3 additions & 2 deletions src/pages/blog/[slug].astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { getCollection, getEntries, render } from 'astro:content';
import { getCollection, render } from 'astro:content';
import { resolveAuthors } from '@/lib/posts';
import type { CollectionEntry } from 'astro:content';
import { Image } from 'astro:assets';
import BaseLayout from '@/layouts/BaseLayout.astro';
Expand Down Expand Up @@ -31,7 +32,7 @@ const { Content } = await render(post);
// Match remark-math's triggers ($$…$$ or $…$) so a stray '$' (prices, shell) doesn't pull in KaTeX CSS.
const hasMath = /\$\$[\s\S]+?\$\$|\$[^\n$]+?\$/.test(post.body ?? '');

const authors = await getEntries(post.data.authors);
const authors = await resolveAuthors(post.data.authors);
const authorNames = authors.map((a) => a.data.name).join(', ');

const all = await getCollection('posts', ({ data }) => !data.draft);
Expand Down
5 changes: 3 additions & 2 deletions src/pages/og/post/[slug].png.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { APIRoute } from 'astro';
import { getCollection, getEntries } from 'astro:content';
import { getCollection } from 'astro:content';
import { resolveAuthors } from '@/lib/posts';
import type { CollectionEntry } from 'astro:content';
import { readFileSync } from 'fs';
import { join } from 'path';
Expand Down Expand Up @@ -76,7 +77,7 @@ async function coverDataUrl(post: CollectionEntry<'posts'>): Promise<string | un
export const GET: APIRoute = async ({ props }) => {
const { post } = props as { post: CollectionEntry<'posts'> };
return pngResponseWithFallback(async () => {
const authors = await getEntries(post.data.authors);
const authors = await resolveAuthors(post.data.authors);
const cover = await coverDataUrl(post);
return generateOgPng(
{
Expand Down
5 changes: 3 additions & 2 deletions src/pages/playground/[tool].astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
import { getCollection, getEntries, render } from 'astro:content';
import { getCollection, render } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
import BaseLayout from '@/layouts/BaseLayout.astro';
import { mdxComponents } from '@/components/MDXComponents.tsx';
import { resolveAuthors } from '@/lib/posts';
import { SITE } from '@/lib/site';

export async function getStaticPaths() {
Expand All @@ -20,7 +21,7 @@ interface Props {
const { tool } = Astro.props;
const { Content } = await render(tool);

const authors = tool.data.authors ? await getEntries(tool.data.authors) : [];
const authors = await resolveAuthors(tool.data.authors);
const canonical = `${SITE.url}/playground/${tool.id}`;
const ogImage = `/og/tool/${tool.id}.png`;

Expand Down
Loading