Files
andwati.com/apps/site/src/content.config.ts
T
andwati 62db772dad Add bookshelf ISBN/paper enrichment, fix status field name collision
apps/cms/src/utils/bookshelf-covers.ts fetches book covers from Open
Library by ISBN (real image, cached into apps/site/public/covers/)
and backfills missing title/authors for papers from Semantic Scholar
by DOI/arXiv id. Wired into the sync middleware in content-sync.ts:
runs once per entry (cover_image already set means "done"), and only
on live create/update through Strapi, not during the boot-time
import (so a restart doesn't hammer external APIs).

Also renames the bookshelf `status` field to `read_status`
everywhere (schema, content/README.md, seed content, Astro zod
schema, bookshelf page, sync mapping). Strapi's content-manager API
treats a top-level `status` key as its own reserved draft/publish
parameter even with draftAndPublish disabled, so an attribute
literally named `status` silently rejected every write — caught this
while testing the enrichment hook.

Verified end-to-end: created a book by ISBN with no cover_image,
confirmed a real cover image was downloaded and cached, cover_image
was set on the entry and written to the file, and the cover/file were
both removed on delete.
2026-08-30 18:52:39 +03:00

78 lines
2.2 KiB
TypeScript

import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { tomlContentLoader } from "./lib/toml-content-loader";
const taxonomies = z
.object({
tags: z.array(z.string()).default([]),
})
.optional();
const writings = defineCollection({
loader: tomlContentLoader("../../content/writings/"),
schema: z.object({
title: z.string(),
description: z.string().optional(),
author: z.string().optional(),
date: z.coerce.date(),
updated: z.coerce.date().optional(),
draft: z.boolean().default(false),
canonical_url: z.string().optional(),
taxonomies,
extra: z
.object({
series: z.string().optional(),
series_index: z.number().optional(),
llms_description: z.string().optional(),
})
.optional(),
}),
});
const portfolio = defineCollection({
loader: tomlContentLoader("../../content/portfolio/"),
schema: z.object({
title: z.string(),
description: z.string().optional(),
role: z.string().optional(),
date_start: z.coerce.date().optional(),
date_end: z.coerce.date().optional(),
outcome: z.string().optional(),
draft: z.boolean().default(false),
links: z
.array(z.object({ label: z.string(), url: z.string() }))
.default([]),
taxonomies,
}),
});
const bookshelf = defineCollection({
loader: tomlContentLoader("../../content/bookshelf/"),
schema: z.object({
title: z.string(),
kind: z.enum(["book", "paper"]),
authors: z.array(z.string()).default([]),
isbn: z.string().optional(),
doi: z.string().optional(),
arxiv_id: z.string().optional(),
url: z.string().optional(),
cover_image: z.string().optional(),
rating: z.number().optional(),
read_status: z.enum(["reading", "read", "abandoned"]).optional(),
date_started: z.coerce.date().optional(),
date_finished: z.coerce.date().optional(),
taxonomies,
}),
});
const blogs = defineCollection({
loader: tomlContentLoader("../../content/blogs/"),
schema: z.object({
title: z.string(),
url: z.string(),
feed_url: z.string().optional(),
}),
});
export const collections = { writings, portfolio, bookshelf, blogs };