# 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), `finds` (editorial superposts that reference find slugs in a grid), `reviews` (full-length, sometimes graduated from a find), `posts` (generic essays)
- **Daily capture skills** under `.claude/skills/`: `daily-finds` (sweeps every source in `sources.json`, writes one find file per qualifying item — capture-only, no editorial selection); `build-finds` (drafts a thematic superpost from the find pool); `build-review` (graduates a find to a full review, linking the two via `fromFind` / `promotedTo`)
- **Sources catalog** at `sources.json` with per-source fetch strategy (`webfetch` / `curl` / `skip`) 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 superposts 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 superpost 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 + finds (superposts) + 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. 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, and JSON-LD; per-page structured data (`Review`, `BlogPosting`, `Product`, `Article`, `CollectionPage`, `ItemList`, `BreadcrumbList`, `WebSite`, `Organization`); auto-generated `sitemap-index.xml` (excludes `/grid/` and `/index3/`); RSS feed at `/rss.xml`; `robots.txt`; static OG image fallbacks under `/public/og/`; promoted finds canonical to their review
- Custom 404 page with recent-reviews and navigation fallback
- Light/dark mode via `prefers-color-scheme`
- No JavaScript shipped to the client
- 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" />
```

If the review needs a hero on the home/grid pages, add it to `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 `finds/` collections — each entry is a folder with `index.mdx` and any sibling media.

## Project layout

```
src/
  content.config.ts          # collection schemas (reviews, posts, find, finds)
  content/
    reviews/<slug>/
      index.mdx              # article + frontmatter
      <media files>          # web-ready variants (hero, screenshots, demos)
      source/                # archival originals (not bundled)
    find/<slug>/
      index.mdx
      hero.{jpg,png,gif,webp}  # auto-resolved by src/lib/find-hero.ts
    finds/<slug>/index.mdx   # editorial superposts (text-only today)
    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()`.
