initial: four-collection content pipeline for unique.rzen.dev

A small Astro + MDX blog for delightful, daily-use finds. The architecture
separates capture from publication and supports reuse:

- find/    captured items, hidden from indexes
- finds/   editorial superposts referencing find slugs in a grid
- reviews/ full-length writeups; can graduate from a find via fromFind
- posts/   regular essays

Three project-local skills under .claude/skills/ drive the pipeline:
daily-finds (capture-only sweep across sources.json), build-finds (draft
a thematic superpost), build-review (graduate a find).

sources.json carries 47 curated feeds with per-source fetch strategy
(webfetch / curl / skip) so Cloudflare-blocked sites and Reddit JSON
endpoints are handled deterministically. /sources renders a 3-column
card stream with a "Recent finds" cross-reference per source.

Seeds: 17 reviews and one superpost referencing 5 finds.
This commit is contained in:
2026-05-03 11:48:03 -04:00
commit 9858abedc2
95 changed files with 10230 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
---
name: build-finds
description: Draft a 'finds' superpost (editorial roundup) for unique.rzen.dev. Selects find items from src/content/find/, groups them under a theme, and writes a new file at src/content/finds/. Use when the user runs /build-finds, asks to "draft today's finds post", "build a roundup", or "make a catch".
disable-model-invocation: true
---
# build-finds
Author one editorial superpost (collection: `finds`) that references existing find items. Each superpost has a theme, a short blurb, and an `items` array listing find slugs. The grid layout is rendered by the page template — this skill only writes the MDX.
Invoke from inside the `unique.rzen.dev` repo.
## Inputs
- `src/content/find/*.mdx` — the available pool of finds.
- `src/content/finds/*.mdx` — existing superposts (used to detect already-referenced finds).
- Optional argument: a theme phrase (e.g. "single-virtue tools", "things-disguised-as-other-things").
- Optional argument: an explicit list of find slugs the user wants in the post.
## Procedure
### 1. Determine candidate pool
- If the user provided explicit slugs: use those.
- If the user provided a theme phrase only: scan all finds, surface the 812 that best fit the theme.
- If neither: scan recent (last ~14 days) finds that are **not yet referenced** by any existing superpost (read every `items[]` array; subtract). From the unreferenced pool, propose 13 thematic clusters of 510 finds each, with a candidate theme phrase per cluster.
Re-use is allowed — a find can appear in multiple superposts under different themes. But by default, prefer surfacing **unreferenced** finds first; only reach for re-uses when the theme genuinely calls for them.
### 2. Confirm the cluster
If proposing clusters, show the user:
```
Cluster A — "<theme phrase>"
- <slug>: <name>
- <slug>: <name>
- ...
Cluster B — "<theme phrase>"
...
```
Wait for the user to pick a cluster (or refine). Don't write anything until confirmed.
### 3. Write the superpost
File: `src/content/finds/<YYYY-MM-DD>-<theme-slug>.mdx` (theme-slug = kebab-case of the theme phrase, lowercase, ~30 chars max).
Frontmatter:
```yaml
---
title: "<Theme phrased as a noun phrase, max ~60 chars>"
date: YYYY-MM-DD
tags: [daily-finds, <topic-tag-1>, <topic-tag-2>]
description: "<One-line description for meta tags. Reuse the title if nothing better.>"
blurb: "<One short editorial sentence; appears as the post's secondary line in the home feed.>"
items:
- <slug>
- <slug>
- ...
---
```
Body: a brief audience-facing intro paragraph (12 sentences max) — open with "Today's catch" or a close variant; name what links these finds; vary the wording day to day. **No** numbered candidates, no headings inside the body, no list items — the grid renders from `items[]`.
Example body:
```mdx
> Today's catch from a morning sweep across the daily feeds. Five small things, each one built around exactly one virtue — a single sound, a single diagnostic, a single disguise, a single object made, a single quiet joke.
```
### 4. Verify
- Confirm every slug in `items[]` exists at `src/content/find/<slug>.mdx`. If any is missing, abort and report — never reference a non-existent find.
- Run `npm run build` to confirm the new superpost compiles.
### 5. Update the log
Append to `daily-finds.log.md`:
```
## YYYY-MM-DD — superpost: <theme>
Slug: <date>-<theme-slug>
Items referenced:
- <slug>
- <slug>
...
```
## Notes
- Default to 5 finds per superpost. Use 3 for tighter themes, up to 8 if the theme really supports it. Beyond 8 the grid stops being scannable.
- The body is for the audience, not the user. Never write "pick one", "candidates", "today's pick" — those are internal terms.
- Never edit existing find files when building a superpost. The find body and metadata are stable; the superpost only references them.
- If a find that's referenced has been graduated to a review (its frontmatter has `promotedTo`), it can still appear in the superpost — the grid card simply links to the find page, which itself links forward to the review.
+97
View File
@@ -0,0 +1,97 @@
---
name: build-review
description: Graduate a find item to a full review for unique.rzen.dev. Reads src/content/find/<slug>.mdx, expands the body into a full review, writes src/content/reviews/<slug>.mdx, and links the two via fromFind / promotedTo. Use when the user runs /build-review <slug>, says "review this find", or "graduate <slug>".
disable-model-invocation: true
---
# build-review
Convert one captured `find` into a full `review`. Both records persist — the find stays put as the historical capture, the review becomes the canonical long-form treatment, and the two link to each other.
Invoke from inside the `unique.rzen.dev` repo with a find slug as the argument.
## Inputs
- `src/content/find/<slug>.mdx` — the source find (must exist; abort otherwise).
- `src/content/reviews/<slug>.mdx` — must NOT exist (abort if a review with the same slug is already there; ask the user to disambiguate).
## Procedure
### 1. Read the find
Parse frontmatter. Note: `name`, `subtitle`, `link`, `linkText`, `source`, `topics`, `tags`, plus the body.
If `promotedTo` is already set, abort: "This find has already been promoted to <slug>."
### 2. Suggest a hero asset
Look at the external link and propose a hero asset — a screenshot, demo gif, or product photo from the maker's site.
- Mac apps: an App Store screenshot or hero image from the developer's site.
- Physical products: the maker's product photo.
- Travel destinations: a Wikipedia or Wikimedia Commons photo (free-to-use only).
Tell the user what you'd download and where to place it (`src/assets/reviews/<slug>/hero.<ext>` or similar). Pause for approval before downloading.
### 3. Expand the body
Take the find's 2-3 sentence body and expand it to a full review:
- Lead with a `>` blockquote summary (1-2 sentences) — this renders as the accent-bordered lead per the existing review style.
- 3-5 paragraphs covering: what it is, the specific problem it solves, the concrete details that make it earn its place, and any honest limitations.
- Reference the hero asset inline if downloaded (`![alt](../../assets/reviews/<slug>/hero.png)`).
- Don't pad. If the find body is already complete, the review is just it + a blockquote summary + maybe one paragraph on context.
- Match the voice of existing reviews under `src/content/reviews/` — informative, concise, no sales tone.
### 4. Write the review
File: `src/content/reviews/<slug>.mdx`.
Frontmatter:
```yaml
---
name: "<from the find>"
subtitle: "<from the find — required for reviews>"
date: YYYY-MM-DD # today (the review date), not the find's date
category: <pick from existing categories or propose a new one>
tags: <merge the find's tags with any review-specific additions>
link: "<from the find>"
linkText: "<from the find, if set>"
description: "<short meta description>"
source: "<from the find>"
fromFind: "<find slug>"
---
```
If the find had no `subtitle`, generate one — it's required on reviews.
### 5. Update the find
Edit `src/content/find/<slug>.mdx` frontmatter: add `promotedTo: <review-slug>`.
(Slug usually matches the find's slug, so `promotedTo` and the find's filename match.)
### 6. Verify
- Run `npm run build`. Confirm:
- `/reviews/<slug>/` exists, shows the source badge and "← First surfaced…" backlink to the find page.
- `/find/<slug>/` shows the "→ Read the full review" callout pointing to the review.
- Any superpost that references the find still renders fine; the find card on the grid remains.
### 7. Update the log
Append to `daily-finds.log.md`:
```
## YYYY-MM-DD — review: <slug>
Graduated from find captured on <find-date>, source: <source>.
```
## Notes
- The find's slug and the review's slug should match. Same slug = same item across the pipeline; cross-references stay simple.
- Don't move or delete the find. Both records persist. The find is the historical capture; the review is the long-form treatment.
- Never auto-write a review without showing the user the proposed body first. This is editorial work; the skill's job is to draft, not to publish unilaterally.
- If multiple finds describe the same underlying thing, the user should consolidate them manually before running this skill.
+106
View File
@@ -0,0 +1,106 @@
---
name: daily-finds
description: Capture-only daily sweep for unique.rzen.dev. Scans every source in sources.json, writes one find file per qualifying item to src/content/find/, and nominates 1-2 new sources to consider. Does NOT pick a winner, write a superpost, or address the user/audience. Picking happens via /build-finds and /build-review. Use when the user asks for "today's finds", "morning sweep", "capture", or runs /daily-finds.
disable-model-invocation: true
---
# daily-finds
Pure capture: every item that passes the bar becomes a `find` file. No daily cap, no theme detection, no audience-facing post. Whether any of these finds appears in a `finds` superpost or graduates to a review is a separate editorial decision.
Invoke from inside the `unique.rzen.dev` repo.
## Inputs
- `sources.json` at the project root — curated source list with per-source `fetch` strategy (`webfetch` / `curl` / `skip`) and optional `fetchUrl`.
- `daily-finds.log.md` — running log of every find slug ever written (used to skip duplicates).
- `src/content/find/*.mdx` — already-captured finds (every existing slug is a "do not re-suggest" item).
- `src/content/reviews/*.mdx` — already-reviewed items (do not re-suggest).
- `candidate-sources.md` — newly nominated sources awaiting user vetting.
## The bar
A find qualifies if and only if it is **(a)** delightful, unusual, or distinctive in a way the audience would care about *and* **(b)** usable daily (relax to "memorable for a visitor" for travel). Drop generic mass-market items, ad-copy listicles, speculative / not-yet-shipping products, self-promo, and anything not traceable to a real currently-buyable / currently-visitable thing.
Crucially: do **not** filter by "is it the best of the day." Capture broadly. Editorial selection happens later.
## Procedure
### 1. Scan every source
For each source in `sources.json`:
- **Default (`fetch: "webfetch"` or no field)**: WebFetch against `fetchUrl` if present, else `url`. Extract recently-featured items (name, link, one-line description).
- **`fetch: "curl"`**: shell out to `curl -sL -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15" --max-time 25 "<fetchUrl|url>"` and parse the HTML body. For Reddit JSON endpoints, pipe through `python3 -c '…'` to extract titles, links, and selftext. Treat HTTP 403 with a non-empty HTML body as a content response (Atlas Obscura quirk) — only treat empty bodies or Cloudflare challenge markers as blocked.
- **`fetch: "skip"`**: log the source as skipped (with `skipReason`) and move on. Do not retry.
Parallelize WebFetch in batches of ~8 concurrent requests; finish all sources before proceeding.
For each source record: items extracted, or `empty` / `blocked` / `skipped`.
### 2. Filter
For each extracted item, drop:
- Anything whose slug already exists in `src/content/find/`.
- Anything whose name appears anywhere in `daily-finds.log.md`.
- Anything reviewed in `src/content/reviews/` (compare by `name` field).
- Items that fail the bar above.
### 3. Write find files
For every survivor, write one file at `src/content/find/<slug>.mdx`:
- Slug: kebab-case form of the name (lowercase, ASCII, no leading numbers).
- Frontmatter: `name`, `subtitle` (optional, one-liner), `date` (today), `link` (external URL — required), `linkText` (optional), `source` (the source name from sources.json — required, exact match), `topics` (copied from the source's `topics`), `tags` (free editorial tags inferred from the item — keep to 2-4), `description` (optional meta override).
- Body: 2-3 sentence note. Concrete, name a *specific* virtue. Bad: "great design". Good: "the silicone bumper means it survives drops onto concrete; the lanyard hole is positioned where your thumb naturally rests".
- No daily cap — write every survivor.
If `link` for a Reddit-discovered item points at a v.redd.it / i.redd.it URL, fetch the post's selftext or comments to find the actual product URL before writing the file.
### 4. Nominate 12 new sources
Spend a small slice of the run on source discovery:
- During scans, when another publication / blog / shop / newsletter is referenced in a way that suggests it'd fit the brief, capture it.
- Or run one targeted `WebSearch` for `"best <topic> newsletter"` or `"<topic> curator blog"`.
- Append nominees to `candidate-sources.md` (create if missing) under today's date heading. For each: name, URL, a one-line case for adding, and which existing source surfaced it (or "WebSearch").
- Don't auto-add to `sources.json` — that's a user decision.
### 5. Update the log
Append to `daily-finds.log.md`:
```
## YYYY-MM-DD — capture run
Sources scanned: <N> total — <hits> hits, <empty> empty, <blocked> blocked, <skipped> skipped.
Productive sources: <comma-separated list of sources that produced at least one written find>.
Captured finds:
- <slug> — <name> — source: <source>
- <slug> — <name> — source: <source>
- ...
New source nominees: <count> (see candidate-sources.md).
```
### 6. Report to the user
Short summary in chat:
```
Captured <N> finds across <M> productive sources today.
Blocked: <K> sources. New source nominees: <count>.
Run /build-finds to draft a superpost from the recent pool, or /build-review <slug> to graduate one to a review.
```
That's it. No theme, no candidates list, no "pick a winner."
## Notes
- "Why it fits" / find descriptions should name a *specific* virtue. Bad: "great design". Good: "magnetic connector means it never strains the cable when a foot catches it".
- Travel destinations relax the daily-use criterion — a place qualifies if a visitor would remember it years later.
- If a source's homepage is too noisy, prefer adding a `fetchUrl` to `sources.json` rather than working around it inside the skill.
- Never invent slugs that collide with existing finds or reviews. If a name would produce a colliding slug, append a year or distinguishing word.
- Source attribution must be exact — the `source` field in each find file must match a `name` in `sources.json` so the `/sources` page cross-reference works.
+26
View File
@@ -0,0 +1,26 @@
# build output
dist/
.output/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS
.DS_Store
# editors
.vscode/
.idea/
+43
View File
@@ -0,0 +1,43 @@
# Changelog
## May 2026
- Restructure into a four-collection content pipeline: `find` (captured items, hidden from indexes), `finds` (editorial superposts that reference find slugs in a Pinterest-style grid), `reviews` (existing — gains optional `source` and `fromFind` fields for graduations), and `posts` (existing, generic essays)
- New routes `/find/<slug>/` (direct URL only — finds don't appear on home, tag, or category pages) and `/finds/<slug>/` (superpost detail with intro + grid)
- New components: `FindCard`, `FindsGrid`, `Backlinks`, `SourceBadge`
- `/sources` cards gain a "Recent finds" sub-list per source — the one cross-cutting surface where individual finds are visible
- Find pages show "Featured in:" backlinks computed at build time from any superpost referencing them; review pages show "← First surfaced…" when promoted from a find
- New project-local skills under `.claude/skills/`: `daily-finds` (capture-only — no theme detection, no candidate post, no winner pick), `build-finds` (editorial superpost author), `build-review` (find → review graduation)
- `sources.json` gains per-source `fetch` strategy (`webfetch` / `curl` / `skip`) with optional `fetchUrl` override; 11 sources switch to curl with a Safari User-Agent (Cool Material, MoMA Design Store, StyleForum, Atlas Obscura, etc.); 9 marked `skip` after Cloudflare JS challenges and active anti-bot defeats both WebFetch and curl
- New `/sources` page (3-column card stream listing every source with its type, cadence, topics, and notes)
- Migrate the seed daily-finds post: extract 5 candidates into individual find files, slim the superpost to just the intro paragraph, move from `posts/` to `finds/`
- Initial `sources.json` curated across macOS apps, useful tools, gifts, clothing, travel, and general-delight feeds (47 sources)
- Daily-finds capture log at `daily-finds.log.md`; new-source nominations queued at `candidate-sources.md`
- Initial blog scaffold with Astro 5 (content collections, no client JS)
- Base layout with light/dark theme via `prefers-color-scheme`
- Index page listing posts by date (newest first)
- Per-post page with title, date, category, optional external link, tags
- Auto-generated category and tag listing pages
- Seed content: 12 posts on delightful Mac apps (Cotypist, Folder Peek, DockDoor, TypeWhisper, Hyperkey, AntiNote, FluxMarkdown, Monocle, Alcove, Shottr, MiddleDrag, LinearMouse)
- Switch to MDX (`@astrojs/mdx`) for post bodies
- Restructure into two content types: `reviews` (name + subtitle + body) and `posts` (title + body, empty for now)
- Unified entry helper (`src/lib/entries.ts`) and shared `EntryList` component so the home, category, and tag pages render a mixed chronological feed across collections
- Reviews route moved to `/reviews/<slug>/`; review detail pages show name as `<h1>` with subtitle as a tagline beneath
- Index entries now show two lines for reviews (bold name + muted subtitle), single line for posts
- Migrate the 12 seed entries from `src/content/posts/*.md` to `src/content/reviews/*.mdx` with the new schema
- Add 5 new reviews: Ice (interface), AltTab (interface), Mac Mouse Fix (input), Homerow (input), Tot (writing)
- Establish media convention at `src/assets/reviews/<slug>/` for real screenshots/clips downloaded from each app's site
- Add visuals to 15 of 17 reviews (mp4 demos, animated gifs, or App Store screenshots), with one-line italic captions naming what's shown
- Style images and videos in review pages: max-width 100%, centered, soft border-radius; italic-only paragraphs auto-render as captions
- Use `import …?url` for animated GIFs (cotypist, flux-markdown) to bypass Sharp and preserve animation
- linearmouse and middledrag intentionally have no visual: linearmouse.app is JS-only with no scrapeable demo and the repo carries no screenshots; the `artginzburg/MiddleDrag` repo cited in the review no longer exists
- Rewrite all 17 reviews — informative, concise, no sales tone; longer reviews lead with a pre-review blockquote summary
- Style `.body blockquote` as a confident lead (accent left-border, no italics, foreground color) on review and post detail pages
- Add optional `linkText` field to reviews: overrides the external-link label (e.g. "Visit in App Store" or "Visit on GitHub"); defaults to the link's hostname with `www.` stripped
- Sample a Pinterest-like 3-column tile stream at `/grid/` (CSS `column-count` masonry, no JS); responsive to 2 cols / 1 col on narrow viewports
- `src/lib/hero.ts` maps slug → hero asset (image or video) so the grid can render visuals; reviews without media (linearmouse, middledrag) become text-only tiles with an accent gradient
- Add `wide` prop to `BaseLayout` for pages that need a wider max-width than the default reading column
- Floating "grid view →" link from the home page; the main index keeps its plain list layout
- Add a CSS-only scroll-snap carousel for reviews with multiple visuals: each `<figure>` slide carries its own `<figcaption>`; pure CSS, no client JS; thin scrollbar visible on desktop, swipe on touch
- Expand 8 reviews into multi-slide carousels with downloaded media — antinote (5), typewhisper (6), dockdoor (3), shottr (3), folder-peek (3), homerow (3), ice (3), tot (2); 28 slides total across the site
- Limit the home page index to the 12 most recent entries across all collections; older entries remain reachable via `/grid/`, categories, and tags
+111
View File
@@ -0,0 +1,111 @@
# 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
- **Unified chronological feed** on the home page across reviews + finds (superposts) + posts
- 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
- 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
Create an MDX file under `src/content/reviews/<slug>.mdx`:
```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 slug becomes the URL at `/reviews/<slug>/`.
## Media
Visuals for a review live at `src/assets/reviews/<slug>/`, mirroring the slug. Reference them from MDX with relative paths so Astro's image pipeline picks them up (responsive variants, lazy loading, modern formats):
```mdx
![alt text](../../assets/reviews/<slug>/feature.png)
```
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.
## Adding a post (essay)
Create an MDX file under `src/content/posts/<slug>.mdx`:
```mdx
---
title: "Some essay title"
date: 2026-05-10
category: writing
tags: [meta, opinion]
description: "Optional <meta> tag override."
---
Body in markdown / MDX.
```
The slug becomes the URL at `/posts/<slug>/`.
## Project layout
```
src/
content.config.ts # collection schemas (reviews, posts)
assets/
reviews/<slug>/ # hero images and screenshots per review
content/
reviews/*.mdx # product reviews
posts/*.mdx # essays (empty for now)
lib/
entries.ts # getAllEntries() — unified feed across collections
components/
EntryList.astro # shared two-shape entry list
layouts/
BaseLayout.astro
pages/
index.astro # unified feed
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()`.
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
export default defineConfig({
site: 'https://unique.rzen.dev',
integrations: [mdx()],
});
+7
View File
@@ -0,0 +1,7 @@
# Candidate sources
Newly nominated sources awaiting review before promotion to `sources.json`. Each entry says where it came from and the case for adding it. Vet manually, then move to the main list.
## 2026-05-03
- **Robin Good's "Best Curated Newsletters"** — https://robingood.substack.com/ — surfaced via WebSearch. Annual list of curated newsletters; less a daily-find source than a yearly source-of-sources to scan once and harvest. Worth checking the most recent edition for newsletters in our brief that we don't yet track.
+28
View File
@@ -0,0 +1,28 @@
# Daily finds — log
Running record of every candidate surfaced by `/daily-finds`. The chosen pick of the day is prepended with `[chosen]`. Source attribution per candidate lets us track which sources produce winners over time.
## 2026-05-03 — theme: single-virtue tools
Sources scanned: 46 total — 23 hits, 5 empty, 17 blocked (Cloudflare 403 / 404 / timeout / dead domain), 1 skipped (paywalled).
Productive sources: r/macapps, Hacker News — Show HN, Tools and Toys, Kikkerland.
Unproductive but live: MacStories (gaming-heavy week), Six Colors, Daring Fireball, One Thing Well, Outlier, Snow Peak, Put This On, A Continuous Lean, Roadtrippers, AFAR, Wallpaper*, Boutique Homes, Things Magazine, Open Culture, Messy Nessy Chic, The Outbound, Uncommon Goods, Werd.com, Cool Tools, Carryology, Kottke.org, Recomendo (Substack archive empty).
Blocked or broken: The Sweet Setup (403), Cool Material (500), Gear Patrol (403), The Awesomer (403), MoMA Design Store (no scraped content), Permanent Style (403), Heddels (403), Standard & Strange (404 — URL needs fixing), Mr Porter Journal (timeout), StyleForum (403), Atlas Obscura (403), Spotted by Locals (403), Unusual Hotels of the World (uhotw.com is now a gambling site — domain lost), This Is Why I'm Broke (404), Indie App Sunday (Mastodon hashtag URL 404), Web Curios (only newsletter index), Kevin Kelly kk.org (homepage doesn't list posts).
Skipped: The Browser (paid).
### Today's candidates
- **Klack 2** — https://tryklack.com/ — source: r/macapps
- **WhatCable** — https://github.com/darrylmorley/whatcable — source: Hacker News — Show HN
- **CoBak "Composition Notebook" Case** — https://www.amazon.com/dp/B0CNX5587H — source: Tools and Toys
- **Bridge City Chopstick Master Gen 2** — https://bridgecitytools.com/products/csmv2-chopstick-master-gen-2 — source: Tools and Toys
- **Juicy Jingles Bike Bell** — https://kikkerland.com/products/juicy-jingles-bike-bell — source: Kikkerland
New source nominees: 1 (see candidate-sources.md).
### Source maintenance for next pruning pass
- **uhotw.com**: domain expired and is now a gambling site. Update to `unusualhotelsoftheworld.com` or remove.
- **Standard & Strange Journal**: `/blogs/journal` returned 404 — find current journal URL.
- **Indie App Sunday**: Mastodon hashtag URL needs the right format (`https://mastodon.social/tags/IndieAppSunday` 404'd; try `.../tags/indieappsunday`).
- **17 sources block WebFetch with 403** — likely Cloudflare. Many of these would work via `curl` with a browser User-Agent. Consider adding `fetch: curl` mode for a `--with-headers=browser` strategy, or accept that ~⅓ of sources need a different fetch path.
+7076
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "unique",
"type": "module",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.3.14",
"astro": "^5.1.1",
"typescript": "^5.7.2"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#b34700"/><text x="16" y="22" font-family="ui-sans-serif,system-ui,sans-serif" font-size="20" font-weight="700" fill="#fafaf7" text-anchor="middle">u</text></svg>

After

Width:  |  Height:  |  Size: 265 B

+424
View File
@@ -0,0 +1,424 @@
{
"updated": "2026-05-03",
"topics": [
"macos-apps",
"tools",
"gifts",
"clothing",
"travel",
"general-delight"
],
"sources": [
{
"name": "MacStories",
"url": "https://www.macstories.net",
"type": "blog",
"topics": ["macos-apps"],
"cadence": "daily",
"notes": "Federico Viticci's site. Long-form, considered reviews of Apple-platform apps. Pair with their Club MacStories newsletter."
},
{
"name": "The Sweet Setup",
"url": "https://thesweetsetup.com",
"type": "blog",
"topics": ["macos-apps", "tools"],
"cadence": "weekly",
"fetch": "skip",
"skipReason": "Cloudflare JS challenge — both WebFetch and curl with Safari UA are blocked. Only headless-browser scraping would work.",
"notes": "App-of-the-week recommendations and workflow guides — opinionated picks rather than exhaustive roundups."
},
{
"name": "Tools and Toys",
"url": "https://toolsandtoys.net",
"type": "blog",
"topics": ["macos-apps", "tools", "gifts"],
"cadence": "weekly",
"notes": "Shawn Blanc's curated picks across software and physical objects. Strong taste, light volume."
},
{
"name": "One Thing Well",
"url": "https://onethingwell.org",
"type": "blog",
"topics": ["macos-apps", "tools"],
"cadence": "weekly",
"notes": "Tiny posts about software that does one thing well — heavy on indie utilities."
},
{
"name": "Six Colors",
"url": "https://sixcolors.com",
"type": "blog",
"topics": ["macos-apps"],
"cadence": "daily",
"notes": "Jason Snell + Dan Moren on Apple. Reviews mixed with industry coverage; scan for app posts."
},
{
"name": "Daring Fireball — Linked List",
"url": "https://daringfireball.net",
"type": "blog",
"topics": ["macos-apps", "general-delight"],
"cadence": "daily",
"notes": "Gruber's link blog often surfaces small Mac utilities and design-y oddities."
},
{
"name": "r/macapps",
"url": "https://www.reddit.com/r/macapps/",
"type": "subreddit",
"topics": ["macos-apps"],
"cadence": "daily",
"fetch": "curl",
"fetchUrl": "https://www.reddit.com/r/macapps/top/.json?t=week&limit=25",
"notes": "Noisy but high-recall — sort by Top of the week to filter for signal. Reddit blocks WebFetch; the skill scans the JSON endpoint via curl with a User-Agent header."
},
{
"name": "Indie App Sunday",
"url": "https://mastodon.social/tags/IndieAppSunday",
"type": "hashtag",
"topics": ["macos-apps"],
"cadence": "weekly",
"fetch": "skip",
"skipReason": "mastodon.social returns 404 for hashtag pages and the .rss endpoint to non-authenticated requests. Need a logged-in Mastodon account or a different instance.",
"notes": "Mastodon hashtag where indie devs surface their apps each Sunday — good for pre-launch / unknown finds."
},
{
"name": "Cool Tools",
"url": "https://kk.org/cooltools/",
"type": "blog",
"topics": ["tools", "gifts"],
"cadence": "daily",
"notes": "Kevin Kelly's long-running catalog of physical and digital tools. The original source for this kind of taste."
},
{
"name": "Recomendo",
"url": "https://recomendo.com",
"type": "newsletter",
"topics": ["tools", "gifts", "travel", "general-delight"],
"cadence": "weekly",
"fetch": "curl",
"fetchUrl": "https://recomendo.substack.com/feed",
"notes": "Six recommendations a week from Kevin Kelly, Mark Frauenfelder, Claudia Dawson. Almost always includes one delightful object or app. Use the Substack RSS feed — full issue content is in the feed, the public site is locked down."
},
{
"name": "Uses This",
"url": "https://usesthis.com",
"type": "interviews",
"topics": ["tools", "macos-apps"],
"cadence": "weekly",
"notes": "Interviews about the hardware and software people use. Good for triangulating what genuinely useful people pick."
},
{
"name": "r/BuyItForLife",
"url": "https://www.reddit.com/r/BuyItForLife/",
"type": "subreddit",
"topics": ["tools", "clothing", "gifts"],
"cadence": "daily",
"fetch": "curl",
"fetchUrl": "https://www.reddit.com/r/BuyItForLife/top/.json?t=week&limit=25",
"notes": "Durable goods recommendations. Filter for posts with provenance and long-term reports. Reddit blocks WebFetch; the skill scans the JSON endpoint via curl with a User-Agent header."
},
{
"name": "Hacker News — Show HN",
"url": "https://news.ycombinator.com/show",
"type": "aggregator",
"topics": ["macos-apps", "tools"],
"cadence": "daily",
"notes": "Devs surfacing what they built. High volume — scan top of week."
},
{
"name": "Product Hunt",
"url": "https://www.producthunt.com",
"type": "aggregator",
"topics": ["macos-apps", "tools"],
"cadence": "daily",
"notes": "Marketing-heavy, but the daily top-5 often includes a Mac app or utility worth a closer look."
},
{
"name": "Carryology",
"url": "https://www.carryology.com",
"type": "blog",
"topics": ["tools", "clothing", "gifts"],
"cadence": "weekly",
"notes": "Bags, EDC, and well-made carry gear — overlaps clothing and tools."
},
{
"name": "Uncommon Goods",
"url": "https://www.uncommongoods.com",
"type": "shop",
"topics": ["gifts"],
"cadence": "ongoing",
"notes": "Curated marketplace heavy on small-batch / handmade gifts. Use the New Arrivals feed."
},
{
"name": "This Is Why I'm Broke",
"url": "https://www.thisiswhyimbroke.com",
"type": "aggregator",
"topics": ["gifts"],
"cadence": "daily",
"fetch": "curl",
"notes": "Internet-flotsam product aggregator. Quality varies wildly but the genuinely-novel finds are hard to get elsewhere. WebFetch hits 404; curl with Safari UA returns the homepage."
},
{
"name": "Cool Material",
"url": "https://coolmaterial.com",
"type": "blog",
"topics": ["gifts", "clothing", "tools"],
"cadence": "daily",
"fetch": "curl",
"notes": "Men's-leaning lifestyle finds: gear, gifts, apparel. WebFetch hits a 500 — curl with Safari UA returns the homepage cleanly."
},
{
"name": "Werd.com",
"url": "https://werd.com",
"type": "blog",
"topics": ["gifts", "clothing", "tools"],
"cadence": "daily",
"notes": "Compact daily roundup of gear and gifts in a Tools-and-Toys vein."
},
{
"name": "Gear Patrol",
"url": "https://www.gearpatrol.com",
"type": "magazine",
"topics": ["gifts", "clothing", "tools"],
"cadence": "daily",
"fetch": "skip",
"skipReason": "Cloudflare JS challenge — neither WebFetch nor curl with Safari UA gets past it.",
"notes": "Editorial gift guides and product features. Skim seasonal guides for gift candidates."
},
{
"name": "The Awesomer",
"url": "https://theawesomer.com",
"type": "aggregator",
"topics": ["gifts", "general-delight"],
"cadence": "daily",
"fetch": "skip",
"skipReason": "Cloudflare JS challenge — both WebFetch and curl with Safari UA are blocked.",
"notes": "Quirky finds across products, art, and video — hits the 'delight' brief directly."
},
{
"name": "MoMA Design Store",
"url": "https://store.moma.org",
"type": "shop",
"topics": ["gifts"],
"cadence": "ongoing",
"fetch": "curl",
"fetchUrl": "https://store.moma.org/collections/new",
"notes": "Museum-curated objects. New Arrivals reliably includes one beautifully-designed daily-use thing. WebFetch returns nav-only — curl with Safari UA returns the full product grid."
},
{
"name": "Kikkerland",
"url": "https://www.kikkerland.com",
"type": "shop",
"topics": ["gifts"],
"cadence": "ongoing",
"notes": "Affordable design gifts; small clever objects."
},
{
"name": "A Continuous Lean",
"url": "https://www.acontinuouslean.com",
"type": "blog",
"topics": ["clothing"],
"cadence": "weekly",
"notes": "Michael Williams on Americana, heritage menswear, and the makers behind it."
},
{
"name": "Put This On",
"url": "https://putthison.com",
"type": "blog",
"topics": ["clothing"],
"cadence": "weekly",
"notes": "Long-running menswear blog. Strong on classic tailoring + rotating sale roundups."
},
{
"name": "Permanent Style",
"url": "https://www.permanentstyle.com",
"type": "blog",
"topics": ["clothing"],
"cadence": "daily",
"fetch": "skip",
"skipReason": "Cloudflare JS challenge — body returns 152kb but body is the challenge page, not content.",
"notes": "Simon Crompton on craft tailoring and permanent-wardrobe pieces. Deep, evidence-led reviews."
},
{
"name": "Heddels",
"url": "https://www.heddels.com",
"type": "blog",
"topics": ["clothing"],
"cadence": "daily",
"fetch": "skip",
"skipReason": "Cloudflare JS challenge — both WebFetch and curl with Safari UA are blocked.",
"notes": "Raw denim, workwear, and 'fades' culture. Strong on construction details."
},
{
"name": "Standard & Strange Journal",
"url": "https://www.standardandstrange.com/blogs/standard-strange-stories",
"type": "shop-blog",
"topics": ["clothing"],
"cadence": "weekly",
"notes": "Bay Area shop's editorial — small-batch workwear and Japanese makers, with substantive notes per piece. The journal lives at /blogs/standard-strange-stories."
},
{
"name": "Outlier",
"url": "https://outlier.nyc",
"type": "shop",
"topics": ["clothing"],
"cadence": "ongoing",
"notes": "Technical fabric clothing for daily use — the brand itself is a source. Watch product drops + the Out of Office blog."
},
{
"name": "Snow Peak",
"url": "https://www.snowpeak.com",
"type": "shop",
"topics": ["clothing", "tools"],
"cadence": "ongoing",
"notes": "Japanese outdoors brand whose apparel and titanium kit reliably qualify as 'delightful daily-use.'"
},
{
"name": "Mr Porter Journal",
"url": "https://www.mrporter.com/en-us/journal",
"type": "magazine",
"topics": ["clothing", "gifts"],
"cadence": "weekly",
"fetch": "skip",
"skipReason": "Active anti-bot — both WebFetch and curl time out indefinitely (status 000) even with browser UA.",
"notes": "Glossy but often surfaces small makers and well-photographed individual pieces."
},
{
"name": "StyleForum",
"url": "https://www.styleforum.net",
"type": "forum",
"topics": ["clothing"],
"cadence": "daily",
"fetch": "curl",
"notes": "Long threads (some 1000+ pages) where subject-matter obsessives compare specific brands — best for deep dives, not browsing. WebFetch hits 403; curl with Safari UA returns the full forum index."
},
{
"name": "Atlas Obscura",
"url": "https://www.atlasobscura.com",
"type": "site",
"topics": ["travel", "general-delight"],
"cadence": "daily",
"fetch": "curl",
"notes": "The definitive catalog of unusual places. Use the 'Places' filter for destinations; the editorial covers oddities globally. Quirk: returns HTTP 403 status with real HTML body — the skill should ignore the status code when the body parses as content."
},
{
"name": "Roadtrippers Magazine",
"url": "https://roadtrippers.com/magazine",
"type": "magazine",
"topics": ["travel"],
"cadence": "weekly",
"notes": "North-America-leaning road trip routes and stops, often hitting roadside oddities."
},
{
"name": "Spotted by Locals",
"url": "https://www.spottedbylocals.com",
"type": "blog-network",
"topics": ["travel"],
"cadence": "weekly",
"fetch": "skip",
"skipReason": "Cloudflare JS challenge — both WebFetch and curl with Safari UA are blocked.",
"notes": "Resident-written tips for ~80 cities — better signal than algorithmic 'top 10' lists."
},
{
"name": "Messy Nessy Chic",
"url": "https://www.messynessychic.com",
"type": "blog",
"topics": ["travel", "general-delight"],
"cadence": "weekly",
"notes": "'13 Things I Found on the Internet' weekly roundup; lots of obscure travel + cultural curiosities."
},
{
"name": "The Outbound Collective",
"url": "https://www.theoutbound.com",
"type": "site",
"topics": ["travel"],
"cadence": "daily",
"notes": "User-submitted outdoor adventures with photos — useful for trail/landscape destinations specifically."
},
{
"name": "Unusual Hotels of the World",
"url": "https://www.unusualhotelsoftheworld.com",
"type": "site",
"topics": ["travel"],
"cadence": "ongoing",
"fetch": "curl",
"notes": "Catalog of architecturally unusual stays — treehouses, ice hotels, repurposed structures. Earlier sources.json had wrong domain (uhotw.com — now a gambling site)."
},
{
"name": "AFAR",
"url": "https://www.afar.com",
"type": "magazine",
"topics": ["travel"],
"cadence": "daily",
"notes": "Editorial travel mag; 'experiential' coverage that surfaces more idiosyncratic destinations than typical glossies."
},
{
"name": "Wallpaper* Travel",
"url": "https://www.wallpaper.com/travel",
"type": "magazine",
"topics": ["travel"],
"cadence": "weekly",
"notes": "Design-forward stays and destinations — overlaps with architectural delight."
},
{
"name": "Boutique Homes",
"url": "https://www.boutique-homes.com",
"type": "shop",
"topics": ["travel"],
"cadence": "ongoing",
"notes": "Hand-picked architectural rentals worldwide — useful as a 'destination via the building' source."
},
{
"name": "Kottke.org",
"url": "https://kottke.org",
"type": "blog",
"topics": ["general-delight"],
"cadence": "daily",
"notes": "Jason Kottke's link blog. The single best low-noise feed for cross-disciplinary delight."
},
{
"name": "The Browser",
"url": "https://thebrowser.com",
"type": "newsletter",
"topics": ["general-delight"],
"cadence": "daily",
"fetch": "skip",
"skipReason": "Paid newsletter, no public daily picks page. Manual review only.",
"notes": "Five hand-picked articles per day. Paid; the picks are unusually consistent."
},
{
"name": "Web Curios",
"url": "https://webcurios.co.uk",
"type": "newsletter",
"topics": ["general-delight"],
"cadence": "weekly",
"fetch": "curl",
"fetchUrl": "https://webcurios.co.uk/all-curios/",
"notes": "Matt Muir's enormous Friday roundup of internet curios. High volume, idiosyncratic taste. Use /all-curios/ — the homepage only lists newsletter titles."
},
{
"name": "Things Magazine",
"url": "https://www.thingsmagazine.net",
"type": "blog",
"topics": ["general-delight", "gifts"],
"cadence": "weekly",
"notes": "Quiet, image-heavy notes on objects, architecture, and obscure design corners."
},
{
"name": "Open Culture",
"url": "https://www.openculture.com",
"type": "blog",
"topics": ["general-delight"],
"cadence": "daily",
"notes": "Free cultural & educational media — adjacent to the brief but a steady source of unusual-thing stories."
},
{
"name": "Kevin Kelly — kk.org",
"url": "https://kk.org",
"type": "blog",
"topics": ["tools", "general-delight"],
"cadence": "weekly",
"fetch": "curl",
"fetchUrl": "https://kk.org/thetechnium/",
"notes": "Kelly's main site — the '68 Bits of Advice' / Excellent Advice posts often link out to delightful tools and books. Posts live at /thetechnium/; the homepage is a static about page."
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

+42
View File
@@ -0,0 +1,42 @@
---
import type { FindsPost } from '../lib/backlinks';
interface Props {
posts: FindsPost[];
label?: string;
}
const { posts, label = 'Featured in' } = Astro.props;
---
{
posts.length > 0 && (
<p class="backlinks">
<span class="label">{label}:</span>
{posts.map((p, i) => (
<>
{i > 0 && <span class="sep">·</span>}
<a href={`/finds/${p.id}/`}>{p.data.title}</a>
</>
))}
</p>
)
}
<style>
.backlinks {
margin: 1.5rem 0 0;
padding: 0.85rem 1rem;
background: var(--rule);
border-radius: 6px;
font-size: 0.9rem;
color: var(--muted);
}
.backlinks .label {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 600;
color: var(--fg);
margin-right: 0.4rem;
}
.backlinks .sep { margin: 0 0.45rem; color: var(--muted); }
.backlinks a { color: var(--accent); }
</style>
+80
View File
@@ -0,0 +1,80 @@
---
import type { Entry } from '../lib/entries';
interface Props {
entries: Entry[];
}
const { entries } = Astro.props;
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
---
<ul class="entry-list">
{
entries.map((entry) => (
<li>
<a href={entry.href} class="row">
<div class="text">
<span class="primary">{entry.primary}</span>
{entry.secondary && <span class="secondary">{entry.secondary}</span>}
</div>
<time datetime={entry.date.toISOString()}>{fmt.format(entry.date)}</time>
</a>
</li>
))
}
</ul>
<style>
.entry-list {
list-style: none;
margin: 0;
padding: 0;
}
.entry-list li {
border-bottom: 1px solid var(--rule);
}
.entry-list .row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
padding: 0.85rem 0;
color: inherit;
}
.entry-list .row:hover { text-decoration: none; }
.entry-list .row:hover .primary { color: var(--accent); }
.text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.primary {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1.05rem;
color: var(--fg);
letter-spacing: -0.01em;
}
.secondary {
color: var(--muted);
font-size: 0.95rem;
line-height: 1.35;
}
time {
color: var(--muted);
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
flex-shrink: 0;
align-self: flex-start;
padding-top: 0.15rem;
}
</style>
+80
View File
@@ -0,0 +1,80 @@
---
import type { FindItem } from '../lib/find-items';
import SourceBadge from './SourceBadge.astro';
interface Props {
item: FindItem;
}
const { item } = Astro.props;
function host(url: string) {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch {
return url;
}
}
---
<a href={`/find/${item.id}/`} class="find-card">
<div class="head">
<span class="name">{item.data.name}</span>
<SourceBadge source={item.data.source} />
</div>
{item.data.subtitle && <p class="subtitle">{item.data.subtitle}</p>}
<p class="external">
<span class="ext-label">→</span>
<span class="host">{host(item.data.link)}</span>
</p>
</a>
<style>
.find-card {
display: block;
border-radius: 8px;
border: 1px solid var(--rule);
background: var(--bg);
padding: 0.95rem 1.05rem 1rem;
color: inherit;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.find-card:hover {
text-decoration: none;
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.18);
}
.find-card:hover .name { color: var(--accent); }
.head {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.name {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1.05rem;
color: var(--fg);
letter-spacing: -0.01em;
transition: color 0.18s ease;
}
.subtitle {
margin: 0 0 0.6rem;
color: var(--muted);
font-size: 0.92rem;
line-height: 1.4;
font-style: italic;
}
.external {
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.78rem;
color: var(--muted);
display: flex;
gap: 0.35rem;
align-items: baseline;
}
.ext-label { color: var(--accent); font-family: ui-sans-serif, system-ui, sans-serif; }
</style>
+35
View File
@@ -0,0 +1,35 @@
---
import type { FindItem } from '../lib/find-items';
import FindCard from './FindCard.astro';
interface Props {
items: FindItem[];
}
const { items } = Astro.props;
---
<ul class="finds-grid">
{items.map((item) => (
<li>
<FindCard item={item} />
</li>
))}
</ul>
<style>
.finds-grid {
column-count: 3;
column-gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
@media (max-width: 880px) { .finds-grid { column-count: 2; } }
@media (max-width: 520px) { .finds-grid { column-count: 1; } }
.finds-grid > li {
break-inside: avoid;
display: block;
margin: 0 0 1rem;
}
</style>
+28
View File
@@ -0,0 +1,28 @@
---
interface Props {
source: string;
prefix?: string;
}
const { source, prefix = 'via' } = Astro.props;
---
<span class="source-badge">{prefix} <span class="name">{source}</span></span>
<style>
.source-badge {
display: inline-flex;
align-items: baseline;
gap: 0.3rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.source-badge .name {
color: var(--accent);
text-transform: none;
letter-spacing: 0;
font-weight: 600;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const reviews = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/reviews' }),
schema: z.object({
name: z.string(),
subtitle: z.string(),
date: z.coerce.date(),
category: z.string(),
tags: z.array(z.string()).default([]),
link: z.string().url().optional(),
linkText: z.string().optional(),
description: z.string().optional(),
source: z.string().optional(),
fromFind: z.string().optional(),
}),
});
const posts = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
category: z.string(),
tags: z.array(z.string()).default([]),
description: z.string().optional(),
}),
});
const find = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/find' }),
schema: z.object({
name: z.string(),
subtitle: z.string().optional(),
date: z.coerce.date(),
link: z.string().url(),
linkText: z.string().optional(),
source: z.string(),
topics: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]),
description: z.string().optional(),
promotedTo: z.string().optional(),
}),
});
const finds = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/finds' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
category: z.string().default('finds'),
tags: z.array(z.string()).default([]),
description: z.string().optional(),
blurb: z.string().optional(),
items: z.array(z.string()).min(1),
}),
});
export const collections = { reviews, posts, find, finds };
@@ -0,0 +1,11 @@
---
name: "Bridge City Chopstick Master Gen 2"
subtitle: "Woodworking jig for making your own chopsticks"
date: 2026-05-03
link: https://bridgecitytools.com/products/csmv2-chopstick-master-gen-2
source: "Tools and Toys"
topics: [tools, gifts]
tags: [woodworking, craft]
---
A purpose-built woodworking jig that lets a beginner whittle a clean pair of chopsticks from any scrap of hardwood. The whole tool exists to make exactly one object, and the precision of the jig means your first pair is usable rather than a souvenir of the attempt.
@@ -0,0 +1,12 @@
---
name: 'CoBak "Composition Notebook" Case'
subtitle: "reMarkable 2 disguised as a school comp book"
date: 2026-05-03
link: https://www.amazon.com/dp/B0CNX5587H
linkText: "Buy on Amazon"
source: "Tools and Toys"
topics: [tools, gifts]
tags: [accessories, stationery, disguise]
---
A hard case for the reMarkable 2 paper tablet styled exactly like a marble-cover school composition notebook. The disguise is the point — you carry an expensive piece of e-paper hardware in something that reads as cheap stationery, which is both funny and quietly useful in any setting where flashing tech feels wrong.
@@ -0,0 +1,11 @@
---
name: "Juicy Jingles Bike Bell"
subtitle: "Fruit-shaped bicycle bell"
date: 2026-05-03
link: https://kikkerland.com/products/juicy-jingles-bike-bell
source: Kikkerland
topics: [gifts]
tags: [bicycle, accessories, whimsy]
---
A bicycle bell shaped like a piece of fruit. It rings normally; it reads as a small joke every time you reach for it. Useful daily, mildly funny daily, costs about as much as the screw it mounts on.
+11
View File
@@ -0,0 +1,11 @@
---
name: "Klack 2"
subtitle: "Mechanical-keyboard sound effects for Mac typing"
date: 2026-05-03
link: https://tryklack.com/
source: r/macapps
topics: [macos-apps]
tags: [macos, audio, typing]
---
A Mac app that adds the sound of a mechanical keyboard to your typing. The original Klack went viral in 2023 because the MacBook speaker grille happens to sit directly under the keys — the illusion is uncanny if you've never heard it. Version 2 is a complete rewrite with new sound profiles and lower CPU.
+11
View File
@@ -0,0 +1,11 @@
---
name: "WhatCable"
subtitle: "Menu bar utility that diagnoses USB-C cable specs"
date: 2026-05-03
link: https://github.com/darrylmorley/whatcable
source: "Hacker News — Show HN"
topics: [macos-apps, tools]
tags: [macos, usb, hardware]
---
A menu bar utility that interrogates a USB-C cable plugged into your Mac and tells you exactly what it can do — power delivery rating, data speed, Thunderbolt support. Solves the genuinely terrible problem of cables that look identical but aren't, without making you guess from the box you threw away.
@@ -0,0 +1,15 @@
---
title: "Single-virtue tools"
date: 2026-05-03
tags: [daily-finds, tools, macos-apps]
description: "Today's catch — five small tools, each built around exactly one specific virtue."
blurb: "Five small things, each built around exactly one virtue."
items:
- klack-2
- whatcable
- cobak-composition-notebook-case
- bridge-city-chopstick-master
- juicy-jingles-bike-bell
---
> Today's catch from a morning sweep across the daily feeds. Five small things, each one built around exactly one virtue — a single sound, a single diagnostic, a single disguise, a single object made, a single quiet joke.
View File
+19
View File
@@ -0,0 +1,19 @@
---
name: Alcove
subtitle: The Dynamic Island, but for your Mac
date: 2026-04-15
category: interface
tags: [macos, notch, menu-bar, music]
description: "Reclaims the MacBook notch as a tiny ambient widget for music, AirDrop, AirPods, charging — and more."
link: https://tryalcove.com/
---
![The Alcove widget unfurling from the MacBook notch with album art and playback controls](../../assets/reviews/alcove/hero.png)
*Music, AirDrop, AirPods, and charging events bloom from the notch and fade back to invisible.*
> A small UI surface that uses the MacBook notch as a Dynamic Island for music, AirPods, AirDrop, and charging. The notch is already a black strip Apple asks you to ignore — Alcove makes the pixels useful.
Plug in headphones and a connection indicator unfurls from the notch. Start playing music and album art appears with scrubbable controls. AirDrop incoming, MacBook charging, AirPods nearby with low battery — each event blooms briefly and recedes. The widget is small, contextual, and never blocks active windows.
The package also includes file-drop targets at the notch edges, a clipboard-history strip, a "battery for connected devices" view, and a tunable volume/brightness HUD. Notched MacBooks only, macOS 13+. Subscription with a lifetime option.
+19
View File
@@ -0,0 +1,19 @@
---
name: AltTab
subtitle: The window switcher Apple should have built
date: 2026-05-03
category: interface
tags: [macos, window-management, open-source, productivity]
description: "An open-source app switcher that shows full live window thumbnails — finally, ⌘-Tab between windows, not just apps."
link: https://alt-tab-macos.netlify.app/
---
![AltTab's switcher showing live thumbnails of multiple windows across spaces](../../assets/reviews/alt-tab/hero.jpg)
*AltTab shows every window across every space in one switcher — minimized, fullscreen, off-display.*
> An open-source ⌥-Tab window switcher for macOS that shows live thumbnails of every window across every space. Apple's ⌘-Tab switches between *apps*; AltTab fills the obvious gap by switching between *windows*.
The default ⌥-Tab binding is unused on macOS, so AltTab slips in without conflict. Hold the modifier, scrub through full thumbnails with the keys or trackpad, release on the one you want. Defaults are correct for most people — minimized windows, multiple displays, fullscreen apps — but every behavior is a preference if you want it.
Sits in the menu bar at near-zero idle CPU and only wakes when invoked. Free, GPL-licensed, macOS 11+.
+52
View File
@@ -0,0 +1,52 @@
---
name: AntiNote
subtitle: Notes that fight against organizing themselves
date: 2026-04-07
category: writing
tags: [macos, notes, scratchpad, productivity]
description: "A frictionless scratchpad for the moments where Notion or Apple Notes is too heavy to bother."
link: https://antinote.io/
---
import slide1 from '../../assets/reviews/antinote/swipe.mp4';
import slide2 from '../../assets/reviews/antinote/plaintext.mp4';
import slide3 from '../../assets/reviews/antinote/links.mp4';
import slide4 from '../../assets/reviews/antinote/math.mp4';
import slide5 from '../../assets/reviews/antinote/conversions.mp4';
<div class="carousel">
<figure id="s1">
<video src={slide1} autoplay loop muted playsinline />
<figcaption>Tap a hotkey, type, tap again — the floating sticky stays exactly where you left it.</figcaption>
</figure>
<figure id="s2">
<video src={slide2} autoplay loop muted playsinline />
<figcaption>Markdown formatting renders inline as you write.</figcaption>
</figure>
<figure id="s3">
<video src={slide3} autoplay loop muted playsinline />
<figcaption>Pasted links collapse to readable titles, expandable on click.</figcaption>
</figure>
<figure id="s4">
<video src={slide4} autoplay loop muted playsinline />
<figcaption>Inline math: type an expression, see the result alongside.</figcaption>
</figure>
<figure id="s5">
<video src={slide5} autoplay loop muted playsinline />
<figcaption>Unit conversions resolve as you type.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
<a href="#s3">3</a>
<a href="#s4">4</a>
<a href="#s5">5</a>
</nav>
> A floating-sticky scratchpad for thoughts that don't deserve a database row — phone numbers, half-finished sentences, a URL you need for ninety seconds. Hit a hotkey, type, hit it again. The note stays exactly where you left it; no naming or filing.
Multiple notes can float at once, snap to screen edges, and stack neatly. There's enough markdown to keep text readable (bold, italic, code blocks, headers), and code-fence detection kicks in automatically when you paste in JSON or a stack trace. No syncing, no tags, no folders — when something matters enough to keep, you copy it into Notion or Bear yourself.
macOS only, single-device, one-time purchase.
+19
View File
@@ -0,0 +1,19 @@
---
name: Cotypist
subtitle: Autocomplete that lives everywhere on your Mac
date: 2026-05-03
category: writing
tags: [macos, typing, productivity, autocomplete]
description: "A system-wide inline autocompletion engine for macOS that quietly speeds up everything you type."
link: https://cotypist.app/
---
import demo from '../../assets/reviews/cotypist/hero.gif?url';
<img src={demo} alt="Cotypist showing inline ghost-text autocomplete" />
*Suggestions appear in ghost text as you type; press tab to accept.*
System-wide inline autocompletion for macOS. Type in any app — Mail, Notes, Slack, the address bar — and Cotypist offers the rest of your sentence in ghost text. It learns from what you actually write, so completions echo your own phrasing rather than a generic prediction.
The accept rate climbs over the first few days as it picks up names, project codes, and recurring sentence openings. Runs entirely on-device — no cloud calls, no telemetry. Free with a paid tier for advanced dictionaries and per-app shortcuts. macOS 12+.
+38
View File
@@ -0,0 +1,38 @@
---
name: DockDoor
subtitle: Hover the Dock, see your windows
date: 2026-04-19
category: interface
tags: [macos, dock, windows, open-source]
description: "Brings Windows-style live window previews to the macOS Dock, done with surprising restraint."
link: https://dockdoor.net/
---
import slide1 from '../../assets/reviews/dockdoor/alttab.mp4';
import slide2 from '../../assets/reviews/dockdoor/aeroShake.mp4';
import slide3 from '../../assets/reviews/dockdoor/clickToMinimize.mp4';
<div class="carousel">
<figure id="s1">
<video src={slide1} autoplay loop muted playsinline />
<figcaption>Hover the Dock to see live thumbnails of every window an app has open.</figcaption>
</figure>
<figure id="s2">
<video src={slide2} autoplay loop muted playsinline />
<figcaption>Shake-to-minimize: jiggle one window, the rest minimize out of the way.</figcaption>
</figure>
<figure id="s3">
<video src={slide3} autoplay loop muted playsinline />
<figcaption>Click an active Dock icon to minimize the front window.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
<a href="#s3">3</a>
</nav>
Hover a Dock icon. A floating panel slides up showing live thumbnails of every window that app has open, with titles and a close button on each. Click a thumbnail to jump to that window — no Mission Control, no clicking-then-guessing.
Also includes a window-level ⌘-Tab replacement (Apple's stops at app level), keyboard navigation through the previews, and configurable hover delays. Open source, MIT-licensed, free. The thumbnails update in real time and the animations are tuned to match macOS's own — a rare port that doesn't read as a port. macOS 13+.
+22
View File
@@ -0,0 +1,22 @@
---
name: Flux Markdown
subtitle: A markdown editor that flows
date: 2026-03-30
category: writing
tags: [macos, markdown, editor, open-source]
description: "An open-source markdown editor that renders inline as you type, with a quiet, distraction-free aesthetic."
link: https://github.com/xykong/flux-markdown
linkText: Visit on GitHub
---
import demo from '../../assets/reviews/flux-markdown/demo.gif?url';
<img src={demo} alt="Flux Markdown editor with inline rendering" />
*Markdown syntax shows up only on the line you're editing, then dissolves into rendered formatting.*
> A free, open-source markdown editor that renders syntax inline as you type — no preview pane, no WYSIWYG mode, no split screen. Asterisks and hashes vanish into formatting on lines you've left, and reappear when you cursor back in.
Headings get larger and bolder as you write them; lists indent into bullets the moment you hit space after a dash; links collapse to their visible text and expand when clicked. The mental cost of switching between "writing" and "previewing" never comes up because both happen in the same view.
Electron-based — about 200 MB on disk and modestly heavy on memory — but the editor itself is responsive and the codebase is small enough to fork. MIT license.
+40
View File
@@ -0,0 +1,40 @@
---
name: Folder Peek
subtitle: The Dock folder behavior macOS forgot to ship
date: 2026-03-26
category: interface
tags: [macos, dock, files, finder]
description: "Pin folders to the menu bar with rich previews, fuzzy search, and quick actions — a small fix for a daily annoyance."
link: https://apps.apple.com/us/app/folder-peek/id6450453695
linkText: Visit in App Store
---
import { Image } from 'astro:assets';
import slide1 from '../../assets/reviews/folder-peek/screenshot.png';
import slide2 from '../../assets/reviews/folder-peek/screenshot-2.png';
import slide3 from '../../assets/reviews/folder-peek/screenshot-3.png';
<div class="carousel">
<figure id="s1">
<Image src={slide1} alt="Pinned folders open as menu-bar panels with thumbnails and search." />
<figcaption>Pinned folders open as menu-bar panels with thumbnails and search.</figcaption>
</figure>
<figure id="s2">
<Image src={slide2} alt="Quick Look-style preview without leaving the menu." />
<figcaption>Quick Look-style preview without leaving the menu.</figcaption>
</figure>
<figure id="s3">
<Image src={slide3} alt="Configurable display options." />
<figcaption>Configurable display options.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
<a href="#s3">3</a>
</nav>
Pin folders to the macOS menu bar with the niceties Dock stacks have always lacked: file thumbnails, Quick Look on hover, fuzzy search across the directory, and quick actions (reveal in Finder, copy path, move, share) on each row. Multiple pinned folders means the menu bar doubles as a bookmark bar for *places on disk* — Downloads, the active project, the screenshots folder, that one git repo you keep poking at.
Not a Finder replacement, just a 200-pixel-wide window into a folder you check often. macOS only, one-time purchase via the App Store.
+40
View File
@@ -0,0 +1,40 @@
---
name: Homerow
subtitle: Click anywhere on the screen with your keyboard
date: 2026-05-03
category: input
tags: [macos, keyboard, accessibility, productivity]
description: "A keyboard navigation overlay that puts a two-letter label on every clickable element — type the letters to click."
link: https://homerow.app/
---
import slide1 from '../../assets/reviews/homerow/overlay.mp4';
import slide2 from '../../assets/reviews/homerow/scroll.mp4';
import slide3 from '../../assets/reviews/homerow/search.mp4';
<div class="carousel">
<figure id="s1">
<video src={slide1} autoplay loop muted playsinline />
<figcaption>Every clickable element sprouts a two-letter label; type it to click.</figcaption>
</figure>
<figure id="s2">
<video src={slide2} autoplay loop muted playsinline />
<figcaption>Scroll mode: navigate long pages with j and k, like Vim.</figcaption>
</figure>
<figure id="s3">
<video src={slide3} autoplay loop muted playsinline />
<figcaption>Search overlay finds elements by visible text, not just by label.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
<a href="#s3">3</a>
</nav>
> A keyboard overlay that lets you click anywhere on screen by typing two letters. Press a shortcut, every clickable element sprouts a label, type the letters, the click happens at native speed. A Vim trick ported to the entire OS.
The overlay covers buttons, menu items, links, tab bars, and most native controls through the macOS accessibility framework. Modes for double-click, right-click, and scroll (j/k for up/down) cover the rest. The few apps that don't expose accessibility info can be configured manually or skipped.
Useful even when you already have shortcuts and a tiled launcher in place — there's always *that button right there* that has no shortcut, and Homerow covers it. macOS 12+, subscription with a one-time-purchase option.
+19
View File
@@ -0,0 +1,19 @@
---
name: Hyperkey
subtitle: Turning Caps Lock into a fifth modifier
date: 2026-04-23
category: input
tags: [macos, keyboard, shortcuts, productivity]
description: "A free menu-bar app that promotes Caps Lock to a hyper-modifier for conflict-free shortcuts."
link: https://hyperkey.app/
---
![Hyperkey's preference window with the Caps-Lock-to-Hyper toggle](../../assets/reviews/hyperkey/hero.png)
*One toggle to enable, one optional secondary mapping for tap-vs-hold — the entire UI.*
> A free menu-bar app that rebinds Caps Lock to a *hyper-modifier* — when held, it fires ⌘⌥⌃⇧ together. The result is an entire shortcut namespace with zero conflicts against any app's existing keybindings.
The hyper-key trick has lived inside Karabiner configurations for a decade. Hyperkey skips the JSON. Pair it with Raycast, BetterTouchTool, or the built-in Shortcuts app to bind Hyper+T → Terminal, Hyper+B → Browser, Hyper+anything to anything you do often.
Free, ~4 MB, runs at zero CPU. macOS 12+.
+41
View File
@@ -0,0 +1,41 @@
---
name: Ice
subtitle: The free, open-source successor to Bartender
date: 2026-05-03
category: interface
tags: [macos, menu-bar, open-source, utilities]
description: "An open-source menu bar manager that hides, organizes, and reveals icons on demand — a genuine replacement for Bartender."
link: https://icemenubar.app/
---
import { Image } from 'astro:assets';
import slide1 from '../../assets/reviews/ice/banner.png';
import slide2 from '../../assets/reviews/ice/ice-bar.png';
import slide3 from '../../assets/reviews/ice/menu-bar-layout.png';
<div class="carousel">
<figure id="s1">
<Image src={slide1} alt="Ice in action: a clean menu bar with hidden icons accessible on demand." />
<figcaption>Ice in action: a clean menu bar with hidden icons accessible on demand.</figcaption>
</figure>
<figure id="s2">
<Image src={slide2} alt="Hidden section collapses behind a chevron and reveals on click." />
<figcaption>Hidden section collapses behind a chevron and reveals on click.</figcaption>
</figure>
<figure id="s3">
<Image src={slide3} alt="Three sections: always visible, hidden, always hidden." />
<figcaption>Three sections: always visible, hidden, always hidden.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
<a href="#s3">3</a>
</nav>
> A free, open-source menu bar manager: hide icons, organize them into "always visible," "hidden," and "always hidden" sections, reveal hidden ones on click. The community's chosen successor to Bartender after Bartender was sold to a new owner without telling its users.
Ice covers the same ground: a chevron in the bar collapses what you don't need to see, a search field jumps to a buried icon by name, hotkeys reveal sections directly. It tracks each macOS update without breakage — a quiet but meaningful achievement for a category that has historically struggled at every annual release.
The bar feels native rather than ported: icons don't shuffle when sections collapse, the chevron lands where the eye expects, the preferences window is built in SwiftUI. macOS 14+, MIT license.
+15
View File
@@ -0,0 +1,15 @@
---
name: LinearMouse
subtitle: Making external mice feel like they belong on a Mac
date: 2026-04-27
category: input
tags: [macos, mouse, input, open-source]
description: "Per-device mouse acceleration, scroll direction, and sensitivity tuning for macOS — finally."
link: https://linearmouse.app/
---
> Per-device pointer and scroll tuning for macOS, free and open source. Each external mouse gets its own acceleration curve, scroll direction, and sensitivity — independent of the trackpad and any other connected mouse.
macOS treats every input device with one set of global pointer settings, which is why a $9 office mouse and a high-DPI sensor never quite feel right on the same machine. LinearMouse splits the controls per device. You can pick a *linear* curve (no acceleration), a custom curve, or one of the presets, and override scroll direction independently of the natural-scroll trackpad setting.
Per-app rules can adjust horizontal scroll speed in Final Cut, disable acceleration in a game, or invert the wheel inside a specific browser. Lives in the menu bar with near-zero footprint. macOS 13+, MIT-licensed.
+19
View File
@@ -0,0 +1,19 @@
---
name: Mac Mouse Fix
subtitle: Trackpad gestures, mapped onto a third-party mouse
date: 2026-05-03
category: input
tags: [macos, mouse, input, open-source, gestures]
description: "An open-source mouse customization tool that brings trackpad-style gestures, smooth scroll, and configurable buttons to any external mouse."
link: https://macmousefix.com/
---
![Mac Mouse Fix's preferences mapping mouse buttons to trackpad gestures](../../assets/reviews/mac-mouse-fix/studio-display.png)
*Buttons and the wheel become configurable triggers for Mission Control, desktop switching, and smart zoom.*
> Open-source mouse customization that maps trackpad-style gestures (Mission Control, desktop switching, smart zoom) onto a third-party mouse's buttons and wheel. Pairs with LinearMouse — that one tunes the *cursor*, this one tunes the *gestures*.
The middle button can summon Mission Control, side buttons can switch desktops, modifier-plus-wheel can zoom or page-navigate. Smooth scrolling is reimplemented to match the trackpad's momentum, so wheel scrolling stops feeling jagged compared to two-finger swipes.
A single menu-bar app, no kernel extension, no helper services. Free, donation-supported. macOS 11+.
+14
View File
@@ -0,0 +1,14 @@
---
name: MiddleDrag
subtitle: A single gesture you'll use a hundred times a day
date: 2026-03-22
category: input
tags: [macos, mouse, gestures, open-source]
description: "Click-drag from any pixel of a window to move it. A tiny, free utility that erases a quiet daily friction."
link: https://github.com/artginzburg/MiddleDrag
linkText: Visit on GitHub
---
Hold a modifier (or click the literal middle mouse button) and drag from anywhere inside any window to move it. A second modifier resizes. That's the entire app — a Linux/X11 trick that macOS has never shipped, ported in a few hundred lines of Swift.
Most useful when a window's title bar is hidden, off-screen, or buried under another window — the cases where macOS leaves you stranded. Open source, MIT-licensed, runs at zero CPU when idle. macOS 12+.
+21
View File
@@ -0,0 +1,21 @@
---
name: Monocle
subtitle: A focus tool that points instead of nags
date: 2026-04-11
category: interface
tags: [macos, focus, productivity, accessibility]
description: "A pointer-driven spotlight overlay that dims everything except where you're looking — quietly useful."
link: https://www.heyiam.dk/monocle
---
import spotlight from '../../assets/reviews/monocle/spotlight.mp4';
<video src={spotlight} autoplay loop muted playsinline />
*A soft circle of brightness follows the cursor; the rest of the screen dims.*
> A pointer-driven spotlight overlay: the screen dims everywhere except a soft circle around your cursor. Dim level, radius, and edge softness are tunable; a hotkey toggles the effect.
Most "focus" apps work by removing things — blocking sites, hiding the dock, locking apps. Monocle works by quietly biasing your eye toward where you're already looking. Distractions remain on screen but require small intentional effort to look at, and most of the time you don't.
Also useful for screen recording and presentations: the audience's eye follows the cursor automatically because there's nowhere else to go. macOS only, one-time purchase.
+40
View File
@@ -0,0 +1,40 @@
---
name: Shottr
subtitle: The screenshot tool that takes its job seriously
date: 2026-04-30
category: screenshots
tags: [macos, screenshots, ocr, productivity]
description: "A native, ridiculously fast macOS screenshot utility with scroll capture, OCR, pixel ruler, and annotation."
link: https://shottr.cc/
---
import slide1 from '../../assets/reviews/shottr/text-erase.mp4';
import slide2 from '../../assets/reviews/shottr/backdrop.mp4';
import slide3 from '../../assets/reviews/shottr/crop-paddings.mp4';
<div class="carousel">
<figure id="s1">
<video src={slide1} autoplay loop muted playsinline />
<figcaption>OCR + erase: select text inside any screenshot and remove it in place.</figcaption>
</figure>
<figure id="s2">
<video src={slide2} autoplay loop muted playsinline />
<figcaption>Backdrop softens the surroundings around your selection for context.</figcaption>
</figure>
<figure id="s3">
<video src={slide3} autoplay loop muted playsinline />
<figcaption>Adjust padding around the crop while the original stays visible behind.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
<a href="#s3">3</a>
</nav>
> A native macOS screenshot utility that fills in what Apple's built-in tool leaves out: scrolling capture, on-image OCR, pixel ruler, color picker, and proper annotation. Free for personal use; one-time purchase for commercial.
Scrolling capture stitches long pages into a single image with clean seams. OCR pulls text from any region you select — including blurry video frames — and can erase it in place against the surrounding background. There's a pixel ruler, an element-sizing mode, blur-for-redaction, numbered callouts, and arrows that snap to common angles. The capture cursor shows live pixel coordinates while you drag.
Pinned screenshots float above all windows, so copying from one and pasting into another is a single motion. Native Cocoa, no Electron — opens in well under a second. macOS 12+.
+35
View File
@@ -0,0 +1,35 @@
---
name: Tot
subtitle: A scratchpad with exactly seven slots, on purpose
date: 2026-05-03
category: writing
tags: [macos, ios, notes, scratchpad]
description: "Seven color-coded notes from Panic — never an eighth — synced quietly between Mac and iPhone."
link: https://tot.rocks/
---
import { Image } from 'astro:assets';
import slide1 from '../../assets/reviews/tot/seven-dots.png';
import slide2 from '../../assets/reviews/tot/sync.png';
<div class="carousel">
<figure id="s1">
<Image src={slide1} alt="Seven dots are tabs across the top of the editor — and the same seven in the menu bar." />
<figcaption>Seven dots are tabs across the top of the editor — and the same seven in the menu bar.</figcaption>
</figure>
<figure id="s2">
<Image src={slide2} alt="Notes sync across iOS, macOS, and watchOS via iCloud." />
<figcaption>Notes sync across iOS, macOS, and watchOS via iCloud.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
</nav>
> Seven color-coded notes from Panic, on Mac and iPhone. There are exactly seven slots — never an eighth — synced through iCloud. The constraint is the entire product.
The dots span red to violet and live across the top of the editor as well as in the menu bar. You assign your own meaning to each — one for the day's todos, one for whatever you're drafting, one for clipboard staging — and the mapping lives in your head, not in a settings panel. When a slot fills up, you triage; the constraint forces it.
Markdown support is intentionally minimal: bold, italic, code, lists, headers. The editor comes from the studio behind Nova, Coda, and the Playdate — taste shows up in cursor placement, keyboard shortcuts, and how the app handles iCloud conflicts. Mac one-time purchase; iOS free.
+59
View File
@@ -0,0 +1,59 @@
---
name: TypeWhisper
subtitle: Dictation that doesn't sound like a robot took it
date: 2026-04-03
category: writing
tags: [macos, dictation, voice, ai]
description: "A push-to-talk Whisper-powered dictation tool that turns spoken thoughts into clean, formatted text in any app."
link: https://typewhisper.com/
---
import { Image } from 'astro:assets';
import slide1 from '../../assets/reviews/typewhisper/demo.mp4';
import slide2 from '../../assets/reviews/typewhisper/home.png';
import slide3 from '../../assets/reviews/typewhisper/recording.png';
import slide4 from '../../assets/reviews/typewhisper/workflows.png';
import slide5 from '../../assets/reviews/typewhisper/dictionary.png';
import slide6 from '../../assets/reviews/typewhisper/file-transcription.png';
<div class="carousel">
<figure id="s1">
<video src={slide1} autoplay loop muted playsinline />
<figcaption>Hold a key, talk, release — cleaned-up text lands wherever the cursor is.</figcaption>
</figure>
<figure id="s2">
<Image src={slide2} alt="Main window: pick a model, set a hotkey." />
<figcaption>Main window: pick a model, set a hotkey.</figcaption>
</figure>
<figure id="s3">
<Image src={slide3} alt="Active recording with a live waveform indicator." />
<figcaption>Active recording with a live waveform indicator.</figcaption>
</figure>
<figure id="s4">
<Image src={slide4} alt="Per-app workflows tune the cleanup pass for each context." />
<figcaption>Per-app workflows tune the cleanup pass for each context.</figcaption>
</figure>
<figure id="s5">
<Image src={slide5} alt="Custom dictionary handles proper nouns and project codes." />
<figcaption>Custom dictionary handles proper nouns and project codes.</figcaption>
</figure>
<figure id="s6">
<Image src={slide6} alt="Transcribe an existing audio file straight to text." />
<figcaption>Transcribe an existing audio file straight to text.</figcaption>
</figure>
</div>
<nav class="carousel-nav" aria-label="Carousel navigation">
<a href="#s1">1</a>
<a href="#s2">2</a>
<a href="#s3">3</a>
<a href="#s4">4</a>
<a href="#s5">5</a>
<a href="#s6">6</a>
</nav>
> Push-to-talk dictation that runs OpenAI's Whisper locally and applies a light cleanup pass — the result reads like written prose, not a transcript. Hold a key, talk, release; the cleaned text lands wherever your cursor was.
Cleanup trims filler words, applies punctuation, capitalizes properly, and resolves the "uh, wait, scratch that" pattern into the corrected sentence. Aggressiveness is configurable per app — useful when you want raw fidelity in one place and polished prose in another.
Transcription is fully on-device, so it works on a plane and never sends audio to a server. The cleanup pass uses an LLM call (your own API key) and can be disabled for fully offline use. One-time purchase, macOS 13+.
+1
View File
@@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />
+126
View File
@@ -0,0 +1,126 @@
---
interface Props {
title: string;
description?: string;
wide?: boolean;
}
const { title, description = 'Unique — a blog about delightful things.', wide = false } = Astro.props;
const siteName = 'Unique';
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>{title === siteName ? siteName : `${title} — ${siteName}`}</title>
</head>
<body class:list={[{ wide }]}>
<header>
<a href="/" class="brand">{siteName}</a>
<p class="tagline">A small log of delightful things.</p>
</header>
<main>
<slot />
</main>
<footer>
<p>© {new Date().getFullYear()} Unique</p>
</footer>
<style is:global>
:root {
--fg: #1a1a1a;
--bg: #fafaf7;
--muted: #6b6b6b;
--accent: #b34700;
--rule: #e6e3dc;
--max: 42rem;
}
@media (prefers-color-scheme: dark) {
:root {
--fg: #ececea;
--bg: #161613;
--muted: #8d8d88;
--accent: #ff9c5b;
--rule: #2a2a26;
}
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
font-family: ui-serif, Georgia, 'Iowan Old Style', 'Apple Garamond', 'Palatino Linotype', serif;
font-size: 18px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
body {
max-width: var(--max);
margin: 0 auto;
padding: 2.5rem 1.25rem 4rem;
}
body.wide { max-width: 76rem; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
header {
margin-bottom: 2.5rem;
border-bottom: 1px solid var(--rule);
padding-bottom: 1.25rem;
}
header .brand {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1.5rem;
color: var(--fg);
letter-spacing: -0.01em;
}
header .tagline {
margin: 0.25rem 0 0;
color: var(--muted);
font-size: 0.95rem;
font-style: italic;
}
main { min-height: 60vh; }
footer {
margin-top: 4rem;
padding-top: 1.25rem;
border-top: 1px solid var(--rule);
color: var(--muted);
font-size: 0.85rem;
}
footer p { margin: 0; }
h1, h2, h3 {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
letter-spacing: -0.015em;
line-height: 1.2;
}
h1 { font-size: 1.85rem; margin: 0 0 0.5rem; }
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.9em;
background: var(--rule);
padding: 0.1em 0.35em;
border-radius: 3px;
}
pre {
background: var(--rule);
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
}
pre code { background: none; padding: 0; }
blockquote {
margin: 1.25rem 0;
padding-left: 1rem;
border-left: 3px solid var(--rule);
color: var(--muted);
font-style: italic;
}
hr { border: 0; border-top: 1px solid var(--rule); margin: 2rem 0; }
</style>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import { getCollection, type CollectionEntry } from 'astro:content';
export type FindsPost = CollectionEntry<'finds'>;
export async function findsReferencing(slug: string): Promise<FindsPost[]> {
const all = await getCollection('finds');
return all
.filter((p) => p.data.items.includes(slug))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
}
+52
View File
@@ -0,0 +1,52 @@
import { getCollection } from 'astro:content';
export type Entry = {
type: 'review' | 'post' | 'finds';
id: string;
href: string;
date: Date;
category: string;
tags: string[];
primary: string;
secondary?: string;
};
export async function getAllEntries(): Promise<Entry[]> {
const reviews = await getCollection('reviews');
const posts = await getCollection('posts');
const findsPosts = await getCollection('finds');
const r: Entry[] = reviews.map((e) => ({
type: 'review',
id: e.id,
href: `/reviews/${e.id}/`,
date: e.data.date,
category: e.data.category,
tags: e.data.tags,
primary: e.data.name,
secondary: e.data.subtitle,
}));
const p: Entry[] = posts.map((e) => ({
type: 'post',
id: e.id,
href: `/posts/${e.id}/`,
date: e.data.date,
category: e.data.category,
tags: e.data.tags,
primary: e.data.title,
}));
const f: Entry[] = findsPosts.map((e) => ({
type: 'finds',
id: e.id,
href: `/finds/${e.id}/`,
date: e.data.date,
category: e.data.category,
tags: e.data.tags,
primary: e.data.title,
secondary: e.data.blurb,
}));
return [...r, ...p, ...f].sort((a, b) => b.date.valueOf() - a.date.valueOf());
}
+24
View File
@@ -0,0 +1,24 @@
import { getCollection, type CollectionEntry } from 'astro:content';
export type FindItem = CollectionEntry<'find'>;
export async function getAllFinds(): Promise<FindItem[]> {
const items = await getCollection('find');
return items.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
}
export async function resolveFinds(slugs: string[]): Promise<FindItem[]> {
const all = await getCollection('find');
const bySlug = new Map(all.map((f) => [f.id, f]));
return slugs
.map((s) => bySlug.get(s))
.filter((f): f is FindItem => Boolean(f));
}
export async function recentFindsBySource(
sourceName: string,
limit = 5
): Promise<FindItem[]> {
const all = await getAllFinds();
return all.filter((f) => f.data.source === sourceName).slice(0, limit);
}
+40
View File
@@ -0,0 +1,40 @@
import type { ImageMetadata } from 'astro';
import alcove from '../assets/reviews/alcove/hero.png';
import altTab from '../assets/reviews/alt-tab/hero.jpg';
import cotypist from '../assets/reviews/cotypist/hero.gif';
import fluxMarkdown from '../assets/reviews/flux-markdown/demo.gif';
import folderPeek from '../assets/reviews/folder-peek/screenshot.png';
import hyperkey from '../assets/reviews/hyperkey/hero.png';
import ice from '../assets/reviews/ice/ice-bar.png';
import macMouseFix from '../assets/reviews/mac-mouse-fix/studio-display.png';
import tot from '../assets/reviews/tot/seven-dots.png';
import antinote from '../assets/reviews/antinote/swipe.mp4';
import dockdoor from '../assets/reviews/dockdoor/alttab.mp4';
import homerow from '../assets/reviews/homerow/overlay.mp4';
import monocle from '../assets/reviews/monocle/spotlight.mp4';
import shottr from '../assets/reviews/shottr/text-erase.mp4';
import typewhisper from '../assets/reviews/typewhisper/demo.mp4';
export type Hero =
| { kind: 'image'; src: ImageMetadata }
| { kind: 'video'; src: string };
export const heroes: Record<string, Hero | undefined> = {
alcove: { kind: 'image', src: alcove },
'alt-tab': { kind: 'image', src: altTab },
cotypist: { kind: 'image', src: cotypist },
'flux-markdown': { kind: 'image', src: fluxMarkdown },
'folder-peek': { kind: 'image', src: folderPeek },
hyperkey: { kind: 'image', src: hyperkey },
ice: { kind: 'image', src: ice },
'mac-mouse-fix': { kind: 'image', src: macMouseFix },
tot: { kind: 'image', src: tot },
antinote: { kind: 'video', src: antinote },
dockdoor: { kind: 'video', src: dockdoor },
homerow: { kind: 'video', src: homerow },
monocle: { kind: 'video', src: monocle },
shottr: { kind: 'video', src: shottr },
typewhisper: { kind: 'video', src: typewhisper },
};
+30
View File
@@ -0,0 +1,30 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import EntryList from '../../components/EntryList.astro';
import { getAllEntries } from '../../lib/entries';
export async function getStaticPaths() {
const entries = await getAllEntries();
const categories = [...new Set(entries.map((e) => e.category))];
return categories.map((category) => ({
params: { category },
props: {
category,
entries: entries.filter((e) => e.category === category),
},
}));
}
const { category, entries } = Astro.props;
---
<BaseLayout title={`Category: ${category}`}>
<h1>Category: {category}</h1>
<EntryList entries={entries} />
<p class="back"><a href="/">← back</a></p>
<style>
h1 { margin-bottom: 1rem; }
.back { margin-top: 2rem; font-size: 0.9rem; }
</style>
</BaseLayout>
+135
View File
@@ -0,0 +1,135 @@
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import SourceBadge from '../../components/SourceBadge.astro';
import Backlinks from '../../components/Backlinks.astro';
import { findsReferencing } from '../../lib/backlinks';
export async function getStaticPaths() {
const items = await getCollection('find');
return items.map((item) => ({
params: { slug: item.id },
props: { item },
}));
}
const { item } = Astro.props;
const { Content } = await render(item);
const featuringPosts = await findsReferencing(item.id);
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const linkLabel =
item.data.linkText ?? new URL(item.data.link).hostname.replace(/^www\./, '');
---
<BaseLayout
title={item.data.name}
description={item.data.description ?? item.data.subtitle ?? item.data.name}
>
<article>
<p class="source-line"><SourceBadge source={item.data.source} /></p>
<header class="find-header">
<h1>{item.data.name}</h1>
{item.data.subtitle && <p class="subtitle">{item.data.subtitle}</p>}
<p class="meta">
<time datetime={item.data.date.toISOString()}>
{fmt.format(item.data.date)}
</time>
</p>
</header>
<div class="body">
<Content />
</div>
<p class="external">
<a href={item.data.link} target="_blank" rel="noopener noreferrer">
{linkLabel} ↗
</a>
</p>
{
item.data.promotedTo && (
<p class="promoted">
→ Read the full review:
<a href={`/reviews/${item.data.promotedTo}/`}>{item.data.promotedTo}</a>
</p>
)
}
<Backlinks posts={featuringPosts} />
{
item.data.tags.length > 0 && (
<footer class="tags">
{item.data.tags.map((tag) => (
<a href={`/tags/${tag}/`} class="tag">#{tag}</a>
))}
</footer>
)
}
<p class="back"><a href="/">← back</a></p>
</article>
<style>
.source-line { margin: 0 0 0.5rem; }
.find-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--rule);
}
h1 { margin: 0 0 0.35rem; }
.subtitle {
margin: 0 0 0.75rem;
color: var(--muted);
font-size: 1.1rem;
font-style: italic;
}
.meta {
margin: 0;
color: var(--muted);
font-size: 0.9rem;
}
.body :global(p) { margin: 0 0 1rem; }
.external {
margin: 1.5rem 0 0;
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.external a {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--accent);
color: var(--bg);
border-radius: 6px;
font-weight: 600;
font-size: 0.95rem;
}
.external a:hover { text-decoration: none; opacity: 0.9; }
.promoted {
margin: 1.5rem 0 0;
padding: 0.85rem 1rem;
background: color-mix(in srgb, var(--accent) 12%, transparent);
border-radius: 6px;
font-size: 0.95rem;
}
.promoted a { font-weight: 600; }
.tags {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--rule);
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.tag { font-size: 0.85rem; color: var(--muted); }
.tag:hover { color: var(--accent); }
.back { margin-top: 2rem; font-size: 0.9rem; }
</style>
</BaseLayout>
+95
View File
@@ -0,0 +1,95 @@
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import FindsGrid from '../../components/FindsGrid.astro';
import { resolveFinds } from '../../lib/find-items';
export async function getStaticPaths() {
const finds = await getCollection('finds');
return finds.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
const items = await resolveFinds(post.data.items);
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
---
<BaseLayout
title={post.data.title}
description={post.data.description ?? post.data.blurb}
wide
>
<article>
<header class="finds-header">
<h1>{post.data.title}</h1>
<p class="meta">
<time datetime={post.data.date.toISOString()}>
{fmt.format(post.data.date)}
</time>
</p>
</header>
<div class="body">
<Content />
</div>
<FindsGrid items={items} />
{
post.data.tags.length > 0 && (
<footer class="tags">
{post.data.tags.map((tag) => (
<a href={`/tags/${tag}/`} class="tag">#{tag}</a>
))}
</footer>
)
}
<p class="back"><a href="/">← back</a></p>
</article>
<style>
.finds-header {
margin-bottom: 1.5rem;
}
h1 { margin: 0 0 0.35rem; font-size: 1.85rem; }
.meta {
margin: 0;
color: var(--muted);
font-size: 0.9rem;
}
.body {
margin-bottom: 1.75rem;
max-width: 42rem;
}
.body :global(p) { margin: 0 0 1rem; }
.body :global(blockquote) {
margin: 1.25rem 0;
padding: 0.15rem 0 0.15rem 1rem;
border-left: 3px solid var(--accent);
color: var(--fg);
font-style: normal;
}
.body :global(blockquote p) { margin: 0; }
.tags {
margin-top: 2.5rem;
padding-top: 1rem;
border-top: 1px solid var(--rule);
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.tag { font-size: 0.85rem; color: var(--muted); }
.tag:hover { color: var(--accent); }
.back { margin-top: 2rem; font-size: 0.9rem; }
</style>
</BaseLayout>
+150
View File
@@ -0,0 +1,150 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro';
import { heroes } from '../lib/hero';
const reviews = (await getCollection('reviews')).sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
);
const tiles = reviews.map((review) => ({
href: `/reviews/${review.id}/`,
name: review.data.name,
subtitle: review.data.subtitle,
hero: heroes[review.id],
}));
---
<BaseLayout title="Grid" wide>
<div class="intro">
<h1>Grid view</h1>
<p><a href="/">← list view</a></p>
</div>
<ul class="grid">
{
tiles.map((tile) => (
<li>
<a href={tile.href} class:list={['tile', { 'text-only': !tile.hero }]}>
{tile.hero?.kind === 'image' && (
<img
class="media"
src={tile.hero.src.src}
width={tile.hero.src.width}
height={tile.hero.src.height}
loading="lazy"
decoding="async"
alt=""
/>
)}
{tile.hero?.kind === 'video' && (
<video
class="media"
src={tile.hero.src}
autoplay
loop
muted
playsinline
preload="metadata"
/>
)}
<div class="text">
<span class="name">{tile.name}</span>
<span class="subtitle">{tile.subtitle}</span>
</div>
</a>
</li>
))
}
</ul>
<style>
.intro {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
.intro h1 { margin: 0; font-size: 1.5rem; }
.intro p { margin: 0; font-size: 0.9rem; color: var(--muted); }
.grid {
column-count: 3;
column-gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
@media (max-width: 880px) {
.grid { column-count: 2; }
}
@media (max-width: 520px) {
.grid { column-count: 1; }
}
.grid > li {
break-inside: avoid;
display: block;
margin: 0 0 1rem;
}
.tile {
display: block;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--rule);
background: var(--bg);
color: inherit;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.tile:hover {
text-decoration: none;
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.18);
}
.tile:hover .name { color: var(--accent); }
.media {
display: block;
width: 100%;
height: auto;
background: var(--rule);
}
.text {
padding: 0.85rem 1rem 1rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.name {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1rem;
letter-spacing: -0.01em;
color: var(--fg);
transition: color 0.18s ease;
}
.subtitle {
color: var(--muted);
font-size: 0.92rem;
line-height: 1.4;
}
.tile.text-only {
background: linear-gradient(155deg, var(--rule), transparent);
}
.tile.text-only .text {
padding: 1.5rem 1.25rem;
}
.tile.text-only .name {
font-size: 1.25rem;
margin-bottom: 0.3rem;
}
.tile.text-only .subtitle {
font-size: 1rem;
}
</style>
</BaseLayout>
+28
View File
@@ -0,0 +1,28 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import EntryList from '../components/EntryList.astro';
import { getAllEntries } from '../lib/entries';
const entries = (await getAllEntries()).slice(0, 12);
---
<BaseLayout title="Unique">
<EntryList entries={entries} />
<p class="view-toggle">
<a href="/sources/">sources</a>
<span aria-hidden="true">·</span>
<a href="/grid/">grid view →</a>
</p>
<style>
.view-toggle {
margin-top: 1.75rem;
text-align: right;
font-size: 0.85rem;
color: var(--muted);
}
.view-toggle a { color: var(--muted); }
.view-toggle a:hover { color: var(--accent); }
.view-toggle span { margin: 0 0.4rem; color: var(--muted); }
</style>
</BaseLayout>
+93
View File
@@ -0,0 +1,93 @@
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
---
<BaseLayout title={post.data.title} description={post.data.description}>
<article>
<header class="post-header">
<h1>{post.data.title}</h1>
<p class="meta">
<time datetime={post.data.date.toISOString()}>
{fmt.format(post.data.date)}
</time>
<span class="dot">·</span>
<a href={`/categories/${post.data.category}/`} class="category">
{post.data.category}
</a>
</p>
</header>
<div class="body">
<Content />
</div>
{
post.data.tags.length > 0 && (
<footer class="tags">
{post.data.tags.map((tag) => (
<a href={`/tags/${tag}/`} class="tag">#{tag}</a>
))}
</footer>
)
}
<p class="back"><a href="/">← back</a></p>
</article>
<style>
.post-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--rule);
}
h1 { margin: 0 0 0.5rem; }
.meta { margin: 0; color: var(--muted); font-size: 0.9rem; }
.meta .dot { margin: 0 0.4rem; }
.meta .category { color: var(--muted); }
.meta .category:hover { color: var(--accent); }
.body :global(p) { margin: 0 0 1rem; }
.body :global(h2) { font-size: 1.3rem; margin: 2rem 0 0.75rem; }
.body :global(h3) { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; }
.body :global(ul), .body :global(ol) { padding-left: 1.4rem; }
.body :global(li) { margin: 0.25rem 0; }
.body :global(blockquote) {
font-size: 1.1rem;
line-height: 1.5;
font-style: normal;
border-left: 3px solid var(--accent);
padding: 0.15rem 0 0.15rem 1rem;
color: var(--fg);
margin: 0 0 1.75rem;
}
.body :global(blockquote p) { margin: 0; }
.tags {
margin-top: 2.5rem;
padding-top: 1rem;
border-top: 1px solid var(--rule);
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.tag { font-size: 0.85rem; color: var(--muted); }
.tag:hover { color: var(--accent); }
.back { margin-top: 2rem; font-size: 0.9rem; }
</style>
</BaseLayout>
+234
View File
@@ -0,0 +1,234 @@
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import SourceBadge from '../../components/SourceBadge.astro';
export async function getStaticPaths() {
const reviews = await getCollection('reviews');
return reviews.map((review) => ({
params: { slug: review.id },
props: { review },
}));
}
const { review } = Astro.props;
const { Content } = await render(review);
const fmt = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const linkLabel =
review.data.link &&
(review.data.linkText ?? new URL(review.data.link).hostname.replace(/^www\./, ''));
const fromFindItem = review.data.fromFind
? (await getCollection('find')).find((f) => f.id === review.data.fromFind)
: undefined;
---
<BaseLayout
title={review.data.name}
description={review.data.description ?? review.data.subtitle}
>
<article>
{review.data.source && (
<p class="source-line"><SourceBadge source={review.data.source} /></p>
)}
<header class="review-header">
<h1>{review.data.name}</h1>
<p class="subtitle">{review.data.subtitle}</p>
<p class="meta">
<time datetime={review.data.date.toISOString()}>
{fmt.format(review.data.date)}
</time>
<span class="dot">·</span>
<a href={`/categories/${review.data.category}/`} class="category">
{review.data.category}
</a>
{
review.data.link && (
<>
<span class="dot">·</span>
<a href={review.data.link} target="_blank" rel="noopener noreferrer">
{linkLabel} ↗
</a>
</>
)
}
</p>
</header>
{fromFindItem && (
<p class="from-find">
← First surfaced on{' '}
<time datetime={fromFindItem.data.date.toISOString()}>
{fmt.format(fromFindItem.data.date)}
</time>{' '}
as <a href={`/find/${fromFindItem.id}/`}>a find</a>
</p>
)}
<div class="body">
<Content />
</div>
{
review.data.tags.length > 0 && (
<footer class="tags">
{review.data.tags.map((tag) => (
<a href={`/tags/${tag}/`} class="tag">#{tag}</a>
))}
</footer>
)
}
<p class="back"><a href="/">← back</a></p>
</article>
<style>
.source-line { margin: 0 0 0.5rem; }
.from-find {
margin: 0 0 1.5rem;
padding: 0.6rem 0.85rem;
background: var(--rule);
border-radius: 6px;
font-size: 0.85rem;
color: var(--muted);
}
.from-find time { color: var(--muted); }
.review-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--rule);
}
h1 { margin: 0 0 0.35rem; }
.subtitle {
margin: 0 0 0.75rem;
color: var(--muted);
font-size: 1.1rem;
font-style: italic;
}
.meta {
margin: 0;
color: var(--muted);
font-size: 0.9rem;
}
.meta .dot { margin: 0 0.4rem; }
.meta .category { color: var(--muted); }
.meta .category:hover { color: var(--accent); }
.body :global(p) { margin: 0 0 1rem; }
.body :global(h2) { font-size: 1.3rem; margin: 2rem 0 0.75rem; }
.body :global(h3) { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; }
.body :global(ul), .body :global(ol) { padding-left: 1.4rem; }
.body :global(li) { margin: 0.25rem 0; }
.body :global(img),
.body :global(video) {
display: block;
max-width: 100%;
height: auto;
margin: 1.5rem auto 0.5rem;
border-radius: 6px;
}
.body :global(p:has(> em:only-child)) {
text-align: center;
font-size: 0.9rem;
color: var(--muted);
margin-bottom: 1.75rem;
}
.body :global(.carousel) {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
gap: 0.75rem;
margin: 1.5rem 0 0.5rem;
padding-bottom: 0.5rem;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.body :global(.carousel > figure) {
flex: 0 0 calc(100% - 2.5rem);
scroll-snap-align: start;
margin: 0;
min-width: 0;
}
.body :global(.carousel img),
.body :global(.carousel video) {
width: 100%;
height: auto;
border-radius: 6px;
display: block;
margin: 0;
}
.body :global(.carousel figcaption) {
font-size: 0.85rem;
color: var(--muted);
text-align: center;
margin-top: 0.5rem;
font-style: italic;
}
.body :global(.carousel)::-webkit-scrollbar {
height: 8px;
}
.body :global(.carousel)::-webkit-scrollbar-track {
background: var(--rule);
border-radius: 4px;
}
.body :global(.carousel)::-webkit-scrollbar-thumb {
background: var(--muted);
border-radius: 4px;
}
.body :global(.carousel-nav) {
display: flex;
justify-content: center;
gap: 0.35rem;
margin: 0 0 1.75rem;
flex-wrap: wrap;
}
.body :global(.carousel-nav a) {
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
font-size: 0.8rem;
color: var(--muted);
background: var(--rule);
padding: 0.3rem 0.65rem;
border-radius: 999px;
text-decoration: none;
min-width: 1.6rem;
text-align: center;
line-height: 1;
transition: color 0.15s, background 0.15s;
}
.body :global(.carousel-nav a:hover) {
color: var(--bg);
background: var(--accent);
}
.body :global(.carousel:has(figure:target)) {
scroll-snap-type: none;
}
.body :global(blockquote) {
font-size: 1.1rem;
line-height: 1.5;
font-style: normal;
border-left: 3px solid var(--accent);
padding: 0.15rem 0 0.15rem 1rem;
color: var(--fg);
margin: 0 0 1.75rem;
}
.body :global(blockquote p) { margin: 0; }
.tags {
margin-top: 2.5rem;
padding-top: 1rem;
border-top: 1px solid var(--rule);
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.tag { font-size: 0.85rem; color: var(--muted); }
.tag:hover { color: var(--accent); }
.back { margin-top: 2rem; font-size: 0.9rem; }
</style>
</BaseLayout>
+226
View File
@@ -0,0 +1,226 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import sourcesData from '../../sources.json';
import { getAllFinds } from '../lib/find-items';
type Source = {
name: string;
url: string;
type: string;
topics: string[];
cadence: string;
notes: string;
};
const sources: Source[] = [...sourcesData.sources].sort((a, b) =>
a.name.localeCompare(b.name)
);
const topicLabels: Record<string, string> = {
'macos-apps': 'macOS apps',
'tools': 'tools',
'gifts': 'gifts',
'clothing': 'clothing',
'travel': 'travel',
'general-delight': 'general delight',
};
function host(url: string) {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch {
return url;
}
}
const allFinds = await getAllFinds();
const findsBySource = new Map<string, typeof allFinds>();
for (const f of allFinds) {
const arr = findsBySource.get(f.data.source) ?? [];
arr.push(f);
findsBySource.set(f.data.source, arr);
}
---
<BaseLayout title="Sources" wide>
<div class="intro">
<h1>Sources</h1>
<p>Where the daily picks come from. {sources.length} feeds, shops, and forums.</p>
</div>
<ul class="grid">
{
sources.map((source) => {
const recent = (findsBySource.get(source.name) ?? []).slice(0, 5);
return (
<li>
<div class="card">
<a href={source.url} class="head-link" target="_blank" rel="noopener">
<div class="head">
<span class="name">{source.name}</span>
<span class="host">{host(source.url)}</span>
</div>
</a>
<p class="notes">{source.notes}</p>
<div class="meta">
<span class="type">{source.type}</span>
<span class="cadence">{source.cadence}</span>
</div>
<ul class="topics">
{source.topics.map((t) => (
<li class="topic">{topicLabels[t] ?? t}</li>
))}
</ul>
{recent.length > 0 && (
<div class="recent">
<p class="recent-label">Recent finds</p>
<ul class="recent-list">
{recent.map((f) => (
<li>
<a href={`/find/${f.id}/`}>{f.data.name}</a>
</li>
))}
</ul>
</div>
)}
</div>
</li>
);
})
}
</ul>
<p class="back"><a href="/">← back</a></p>
<style>
.intro {
margin-bottom: 1.5rem;
}
.intro h1 { margin: 0 0 0.25rem; font-size: 1.85rem; }
.intro p { margin: 0; color: var(--muted); }
.grid {
column-count: 3;
column-gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
@media (max-width: 880px) { .grid { column-count: 2; } }
@media (max-width: 520px) { .grid { column-count: 1; } }
.grid > li {
break-inside: avoid;
display: block;
margin: 0 0 1rem;
}
.card {
display: block;
border-radius: 8px;
border: 1px solid var(--rule);
background: var(--bg);
padding: 0.95rem 1.05rem 1rem;
color: inherit;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--accent) 40%, var(--rule));
box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.18);
}
.head-link { color: inherit; display: block; }
.head-link:hover { text-decoration: none; }
.head-link:hover .name { color: var(--accent); }
.head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.6rem;
margin-bottom: 0.45rem;
}
.name {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 1rem;
letter-spacing: -0.01em;
color: var(--fg);
transition: color 0.18s ease;
}
.host {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.72rem;
color: var(--muted);
flex-shrink: 0;
}
.notes {
margin: 0 0 0.7rem;
font-size: 0.92rem;
line-height: 1.4;
color: var(--fg);
}
.meta {
display: flex;
gap: 0.4rem;
margin-bottom: 0.5rem;
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.meta .type { color: var(--accent); }
.meta .cadence::before { content: '· '; color: var(--muted); }
.topics {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.topic {
font-family: ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
font-size: 0.72rem;
padding: 0.1rem 0.45rem;
border-radius: 999px;
background: var(--rule);
color: var(--muted);
}
.recent {
margin-top: 0.7rem;
padding-top: 0.6rem;
border-top: 1px dashed var(--rule);
}
.recent-label {
margin: 0 0 0.3rem;
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.recent-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.recent-list li { font-size: 0.85rem; line-height: 1.35; }
.recent-list a { color: var(--fg); }
.recent-list a:hover { color: var(--accent); }
.back {
margin-top: 2rem;
font-size: 0.9rem;
color: var(--muted);
}
</style>
</BaseLayout>
+30
View File
@@ -0,0 +1,30 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import EntryList from '../../components/EntryList.astro';
import { getAllEntries } from '../../lib/entries';
export async function getStaticPaths() {
const entries = await getAllEntries();
const tags = [...new Set(entries.flatMap((e) => e.tags))];
return tags.map((tag) => ({
params: { tag },
props: {
tag,
entries: entries.filter((e) => e.tags.includes(tag)),
},
}));
}
const { tag, entries } = Astro.props;
---
<BaseLayout title={`Tag: ${tag}`}>
<h1>#{tag}</h1>
<EntryList entries={entries} />
<p class="back"><a href="/">← back</a></p>
<style>
h1 { margin-bottom: 1rem; }
.back { margin-top: 2rem; font-size: 0.9rem; }
</style>
</BaseLayout>
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}