Autodiscovery alone is invisible since browsers dropped feed-detection UI — add a small RSS pill in the subheader (new feedHref prop on BaseLayout) linking to the page's scoped feed. Claude-Session: https://claude.ai/code/session_01WZaczDJjL3xZ3u5spsN5AL
164 lines
10 KiB
Markdown
164 lines
10 KiB
Markdown
# 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 only via the bundles that reference them, the `/sources` cross-reference, or direct URL — they don't crowd the home feed, tag pages, 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*)
|
||
- Auto-generated `/categories/<name>/` and `/tags/<name>/` listing pages — finds excluded by design
|
||
- **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
|
||

|
||
|
||
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 5–15× 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()`.
|