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:
@@ -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());
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 },
|
||||
};
|
||||
Reference in New Issue
Block a user