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