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,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user