Files
andwati.com/apps/site/src/content.config.ts
T
andwati 9392d45f53 Build the design system: typography, color tokens, real Base layout
References: pixperk.tech (dark, warm off-white text, serif-italic +
mono mixed typography, personal dashboard modules) and mcyoung.xyz
(meta line with reading time, subtle accent underline on titles).

- src/styles/tokens.css: dark-first color palette with a warm-paper
  light mode (data-theme override + prefers-color-scheme, toggle wins
  either direction), fluid clamp() type scale, spacing/radius scale.
- Fonts: Fraunces (display/headings, italic), Newsreader (body,
  long-form readability), JetBrains Mono (nav/meta/code) — all
  self-hosted via @fontsource, continuing the mono choice already
  used in the legacy OG-image pipeline.
- Base.astro rewritten: real header/nav with active-link state, a
  theme toggle (persisted to localStorage, no-flash inline script),
  footer with social links (ported from zola.toml), replacing the
  scaffold placeholder.
- Reading time: toml-content-loader.ts computes wordCount/
  readingMinutes from the raw body at load time, surfaced in the
  writings list and detail meta line.
- Bug fix: the marked heading renderer was using raw token.text for
  both the visible heading and the TOC, so a heading like
  "**Registers**" rendered literal asterisks instead of bold — now
  renders inline tokens properly and strips markdown for the
  plain-text TOC label/slug.

Verified in-browser (light + dark) across home, a migrated writing
with TOC/headings, bookshelf, and 404.
2026-08-31 08:03:50 +03:00

80 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(),
wordCount: z.number().optional(),
readingMinutes: z.number().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 };