Files
andwati.com/content
andwati 1b7c4039e3 Migrate legacy Zola posts into content/writings/ (additive)
scripts/migrate-posts-to-writings.mjs converts all 20 posts under
content/posts/ into the new writings schema — normalizes dates to
bare TOML local-dates, converts the one unambiguous Zola shortcode in
use (youtube embeds) to a plain <iframe>, and strips the now-meaningless
{% raw %}/{% endraw %} escape tags. content/posts/ itself is untouched;
this only adds content/writings/*.

Also generates 301 redirects from the old /posts/<slug>/ URLs to the
new /writings/<slug>/ ones into sws.toml, inside marker comments so
re-running the script regenerates that block idempotently instead of
duplicating it.

Two posts (from-c-to-machine-code, self-hosting-gitea-and-mirroring-github)
use note/tip/warning/danger callout shortcodes that have no Astro
equivalent yet — flagged by the script rather than guessed at, since
that needs a real component once the design system exists.

Verified: astro check clean, full site builds (80 pages, all real
migrated posts render), redirect generation confirmed idempotent
across repeated runs.
2026-08-30 19:10:53 +03:00
..
2026-04-02 13:14:18 +03:00
2026-04-02 13:14:18 +03:00
2026-07-16 16:09:20 +03:00

content/

This directory is the canonical source of truth for all site content. It is shared between apps/site (Astro reads it directly at build time) and apps/cms (Strapi's custom provider reads/writes these same files — its SQLite database is only a disposable cache/index, never the source of record). The site must always be buildable straight from this directory with Strapi entirely absent.

Frontmatter is TOML (+++ ... +++), matching the legacy Zola posts under content/posts/ to ease migration. Each content type below is one file (or a directory with index.md for entries with co-located assets), one frontmatter schema.

writings/ (blog / essays)

+++
title = "..."
description = "..."
author = "andwati"
date = 2026-04-08
updated = 2026-04-10       # optional, only when the post has been revised
draft = false
canonical_url = ""         # optional, only if first published elsewhere
[taxonomies]
tags = ["..."]
[extra]
series = "..."             # optional
series_index = 1           # required if `series` is set
llms_description = "..."   # optional per-post override of the site-wide one
+++

portfolio/ (projects / professional work)

+++
title = "..."
description = "..."
role = "..."
date_start = 2025-01-01
date_end = 2025-06-01      # optional, omit if ongoing
outcome = "one-line result"
draft = false
[[links]]
label = "Live"
url = "https://..."
[[links]]
label = "Source"
url = "https://..."
[taxonomies]
tags = ["..."]             # stack / tech tags
+++

Body is a free-form Markdown case study.

bookshelf/ (books and papers)

kind selects which identifier fields apply — books use isbn, papers use doi/arxiv_id. Book covers are fetched from Open Library by ISBN and cached into apps/site/public/covers/ (served at /covers/...), never fetched at page-render time. Papers have no reliable cover source, so cover_image is set manually for them, if at all; Semantic Scholar is only used to backfill a missing title/authors by DOI/arXiv id.

Note: the field is named read_status, not status — 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 breaks writes. Don't rename it back.

+++
title = "..."
kind = "book"               # "book" | "paper"
authors = ["..."]
isbn = "9780000000000"      # books only
doi = "10.1000/xyz123"      # papers only
arxiv_id = "2101.00001"     # papers only, optional
url = "https://..."         # papers only: landing page / source link
cover_image = "/covers/slug.jpg"
rating = 5                  # optional, 1-5
read_status = "read"        # "reading" | "read" | "abandoned"
date_started = 2026-01-01
date_finished = 2026-01-20  # optional, omit if still in progress
[taxonomies]
tags = ["..."]              # genre / topic
+++

Body is a free-form personal review/notes.

blogs/ (curated external blogs)

+++
title = "..."     # the blog/site's name
url = "https://..."
feed_url = "https://.../rss.xml"
+++

Body is a one-to-two sentence blurb on why you follow it. No live feed fetching — this list is hand-curated and static.