Files
rzen 9e5fce1eb0 site: topics axis + per-source indexes
Topics — the curated subscription layer: 18-value controlled vocabulary
in src/lib/topics.ts, enum-enforced by the content schema on finds and
reviews so unattended runs can't drift it. /topics/ index +
/topics/<t>/ pages with finds first-class alongside reviews, per-topic
RSS feeds that include finds (unlike category/tag feeds), "By topic"
section on /feeds/, Topics footer link and llms.txt section, dev-shim
coverage for the new feed URLs. daily-finds and build-review skills now
classify at capture/graduation time; "giftable" is a tag, never a
topic.

Per-source indexes: /sources/<slug>/ lists every review and find from a
source (catalog ∪ names in content, so retired sources resolve);
find/review colophons link "(more from this source)"; /sources/ cards
link "everything from this source →"; sub-3-item pages noindexed.

Claude-Session: https://claude.ai/code/session_01WZaczDJjL3xZ3u5spsN5AL
2026-07-12 19:39:11 -04:00

166 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Unique
A small blog about delightful, unique things — products, apps, phenomena, oddities.
Built with [Astro](https://astro.build) + MDX.
## Key features
- **Four-collection pipeline**: `find` (captured items, hidden from indexes), `bundles` (editorial roundups that reference find slugs in a grid), `reviews` (full-length, sometimes graduated from a find), `posts` (generic essays)
- **Autonomous daily pipeline** under `.claude/skills/`: `daily-pipeline` (the unattended chain: capture → bundle → review graduation → pick of the day → build → commit → push → digest); `daily-finds` (sweeps every source in `sources.json`, writes one find file per qualifying item, marks a ranked shortlist of up to 12, adds Amazon links for matched physical products, and self-maintains `sources.json`); `build-bundle` (thematic bundle from the find pool; `auto` and `weekly` catch modes); `cluster-bundles` (batch-clusters the find pool into multiple bundles); `build-review` (graduates a find to a full review with a mandatory fact-check in auto mode, linking the two via `fromFind` / `promotedTo`)
- **Pick of the day** driven by `src/data/pick.json` (falls back to the latest review) — the homepage hero is a one-line content edit, also settable from the dev-mode button on any review page
- **Sources catalog** at `sources.json` with per-source fetch strategy (`feed` / `webfetch` / `curl` / `skip`, feeds preferred) and the `/sources` page rendering a 3-column card stream with a "Recent finds" sub-list per card
- **Strict find visibility**: find items are reachable via the bundles that reference them, the `/sources` cross-reference, tag pages (a secondary "Finds" list below the entry grid), or direct URL — they don't crowd the home feed, grids, or category pages
- **Backlinks** computed at build time: each find page lists every bundle that references it; promoted finds show a forward link to the review; reviews show a "← First surfaced…" backlink to their originating find
- **Newspaper-style masthead** on every page: live entry-count + last-updated stats on the left, large serif "Unique" wordmark centered between thin rules, About/Blog nav on the right; per-page italic subheader ("A small log of delightful things.") with last-updated date on listing pages
- **Pinterest-style home page**: hero card for the latest review plus a 3-column masonry grid of remaining reviews with a "Load more" reveal; old plain-list home preserved at `/index3/`
- **Unified chronological feed** at `/index3/` across reviews + bundles + posts
- **SEO-friendly site footer** with Browse / Collections (live counts) / About link columns and a centered legal line; consistent 76rem page width across the entire site (article prose constrained to a comfortable reading measure inside the wider layout)
- Reviews render as two-line index entries: bold name + muted subtitle (`Cotypist` / *Autocomplete that lives everywhere on your Mac*)
- **Topics — the curated subscription axis**: an 18-value controlled vocabulary in `src/lib/topics.ts` (schema-enum-enforced on finds and reviews, so the unattended pipeline can't drift it); `/topics/` index + `/topics/<t>/` pages where finds are first-class alongside reviews, and per-topic RSS feeds that include finds — unlike category/tag feeds. Facets (`open-source`, `bifl`, `giftable`…) stay tags by design
- **Per-source indexes**: `/sources/<slug>/` lists every review and find surfaced via a source (generated for the catalog all source names in content); find/review colophons link "(more from this source)" next to the source name, and `/sources/` cards link through; thin pages (<3 items) noindexed
- Auto-generated `/categories/<name>/` and `/tags/<name>/` listing pages; tag pages are built from the union of entry and find tags (so every tag linked from a find resolves) and list tagged finds in a secondary section — find-only tag pages with fewer than 3 finds are noindexed until they accumulate content; category pages stay entries-only (finds have no category)
- **Amazon affiliate handling** in `src/lib/affiliate.ts`: outbound links to Amazon (`amazon.com`, `amzn.to`, `a.co`, `smile.amazon.com`) get the configured tracking ID appended automatically; the link gets `rel="sponsored"`, an "Affiliate" badge appears next to it, and a one-line disclosure is shown beneath. Finds and reviews also take an optional `amazonLink` frontmatter field (canonical `/dp/<ASIN>` form) rendered as an "Also on Amazon" link — the capture pipeline populates it when a shortlisted physical product has an exact Amazon match. Add additional marketplace IDs (e.g. `amazon.co.uk`) to the `AMAZON_TAGS` map when needed
- **SEO surface**: single `<SEO>` component owning canonical, Open Graph, Twitter Card, feed autodiscovery, and JSON-LD; per-page structured data (`Review`, `BlogPosting`, `Product`, `Article`, `CollectionPage`, `ItemList`, `BreadcrumbList`, `WebSite` with `SearchAction`, `Organization`); auto-generated `sitemap-index.xml` (excludes `/grid/` and `/index3/`); `robots.txt`; static OG image fallbacks under `/public/og/`; promoted finds canonical to their review
- **Feeds everywhere**: site-wide RSS 2.0 at `/rss.xml` and JSON Feed 1.1 at `/feed.json`, plus per-category (`/categories/<name>/rss.xml`) and per-tag (`/tags/<name>/rss.xml`) RSS feeds, all derived from a single item source in `src/lib/feed.ts`; category/tag pages advertise their scoped feed via `<link rel="alternate">` and show a visible RSS pill in the subheader; a human-readable directory at `/feeds/` (footer-linked) lists every feed with counts; RSS renders as a readable page in browsers via `public/feed.xsl`
- **Programmatic surface**: build-generated `/llms.txt` (llmstxt.org format — site map with every review/bundle/post and one-line descriptions, plus feed and endpoint pointers) and the full-site JSON index at `/search.json`, both documented in the "For robots" section of `/feeds/`
- Custom 404 page with recent-reviews and navigation fallback
- Light/dark mode via `prefers-color-scheme`
- **Cloudflare Web Analytics** beacon in `BaseLayout.astro` for cookieless, privacy-preserving page-view + referrer + Core Web Vitals tracking scoped to `unique.rzen.dev`
- Otherwise no client-side framework — only the inline theme bootstrap and the analytics beacon
- Static build, deployable anywhere
## Local development
```bash
npm install
npm run dev # local dev server
npm run build # type-check + static build to ./dist
npm run preview # serve the built site
```
## Adding a review
Each review is a folder with `index.mdx` plus its media. The folder name is the slug:
```
src/content/reviews/cotypist/
├── index.mdx # the article + frontmatter
├── hero.mp4 # web-ready variants live here, alongside the article
├── screenshot.png
└── source/ # heavy originals (archival; never bundled)
└── hero.gif
```
`index.mdx` frontmatter:
```mdx
---
name: "Cotypist"
subtitle: "Autocomplete that lives everywhere on your Mac"
date: 2026-05-03
category: writing
tags: [macos, productivity]
link: https://cotypist.app/ # optional product URL
linkText: "Visit in App Store" # optional; defaults to the link's hostname (sans `www.`)
description: "Optional <meta> override; defaults to subtitle."
---
Body in markdown / MDX. You can import and use components here.
```
The folder name becomes the URL at `/reviews/<slug>/`. Media is referenced relatively:
```mdx
![alt](./screenshot.png)
import demo from './hero.mp4';
<video src={demo} autoplay loop muted playsinline preload="metadata" />
```
Review heroes are auto-discovered: name the file `hero.<ext>` (any of png/jpg/jpeg/gif/webp/avif, or `hero.mp4` for video) and the home/grid tiles pick it up — no registration. Only assets with non-`hero` filenames need an explicit entry in the `overrides` map in `src/lib/hero.ts`.
## Media
Prefer real screenshots, GIFs, or short clips from the app's own site (downloaded locally — don't hotlink). Pair with a short italic caption that names what's being shown. Visuals should illustrate the virtue of the product, not decorate the page.
**Source vs published media.** Heavy originals live in `<slug>/source/`; web-ready variants live at `<slug>/<file>` and are what the site actually ships. Files inside `source/` aren't imported anywhere, so Astro doesn't bundle them into `dist/`. To add or replace a video:
```bash
# 1. drop the original into source/
mv ~/Downloads/demo.mp4 src/content/reviews/<slug>/source/
# 2. encode a smaller web variant alongside (h264 CRF 28, ≤1280px, no audio)
ffmpeg -i src/content/reviews/<slug>/source/demo.mp4 \
-c:v libx264 -preset slow -crf 28 \
-movflags +faststart -pix_fmt yuv420p \
-vf "scale='min(1280,iw)':-2,crop=trunc(iw/2)*2:trunc(ih/2)*2" \
-an src/content/reviews/<slug>/demo.mp4
# 3. reference ./demo.mp4 from index.mdx or src/lib/hero.ts
```
For animated GIFs, use the same recipe with `crf 26` and source `.gif` — the resulting MP4 is typically 515× smaller.
## Adding a post (essay)
Create a folder + `index.mdx` under `src/content/posts/<slug>/`:
```mdx
---
title: "Some essay title"
date: 2026-05-10
category: writing
tags: [meta, opinion]
description: "Optional <meta> tag override."
---
Body in markdown / MDX.
```
The folder name becomes the URL at `/posts/<slug>/`. Drop any media (images, screenshots) next to `index.mdx` and reference them as `./image.png`. Same convention applies to the `find/` and `bundles/` collections — each entry is a folder with `index.mdx` and any sibling media.
## Project layout
```
src/
content.config.ts # collection schemas (reviews, posts, find, bundles)
content/
reviews/<slug>/
index.mdx # article + frontmatter
<media files> # web-ready variants (hero, screenshots, demos)
source/ # archival originals (not bundled)
find/<year>/<month>/<slug>/
index.mdx # find folders are bucketed by capture date
hero.{jpg,png,gif,webp} # auto-resolved by src/lib/find-hero.ts; URLs stay flat at /find/<slug>/
bundles/<slug>/index.mdx # editorial roundups (text intro + grid)
posts/<slug>/index.mdx # essays
lib/
entries.ts # getAllEntries() — unified feed across collections
components/
EntryList.astro # shared two-shape entry list
layouts/
BaseLayout.astro
pages/
index.astro # Pinterest-style home (hero + masonry)
index3.astro # plain chronological list view
about.astro # About page
blog.astro # /posts index (essays)
grid.astro # full grid of every review
sources.astro # source catalog
reviews/[...slug].astro
posts/[...slug].astro
categories/[category].astro
tags/[tag].astro
public/
favicon.svg
```
## Adding a new content type
1. Define the collection in `src/content.config.ts`.
2. Extend the `Entry` union and `getAllEntries()` in `src/lib/entries.ts` so the new type joins the merged feed.
3. Add a detail route at `src/pages/<type>/[...slug].astro`.
Listing pages (index, categories, tags) need no changes — they consume `getAllEntries()`.