site: feeds everywhere + programmatic surface
Feeds: JSON Feed 1.1 at /feed.json, per-category and per-tag RSS at /categories/<name>/rss.xml and /tags/<name>/rss.xml alongside the main /rss.xml — all from a shared src/lib/feed.ts item source. Category/tag pages advertise their scoped feed via autodiscovery; new /feeds/ directory page (footer-linked) lists every feed with counts; RSS feeds gain an atom:link self reference and a feed.xsl stylesheet so they render readably in browsers. Programmatic readers: build-generated /llms.txt (site map with every review/bundle/post), SearchAction on the WebSite JSON-LD wired to /search/?q=, and a "For robots" section on /feeds/ documenting search.json, llms.txt, the sitemap, and JSON-LD coverage. Claude-Session: https://claude.ai/code/session_01WZaczDJjL3xZ3u5spsN5AL
This commit is contained in:
@@ -19,6 +19,8 @@ interface Props {
|
||||
modifiedTime?: Date;
|
||||
articleTags?: string[];
|
||||
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
||||
/** Page-scoped feeds (category/tag) advertised alongside the site-wide ones. */
|
||||
feeds?: Array<{ title: string; href: string }>;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -33,6 +35,7 @@ const {
|
||||
modifiedTime,
|
||||
articleTags = [],
|
||||
jsonLd,
|
||||
feeds = [],
|
||||
} = Astro.props;
|
||||
|
||||
const SITE_NAME = 'Unique';
|
||||
@@ -59,6 +62,10 @@ const jsonLdBlocks = jsonLd
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<meta name="theme-color" content="#fafaf7" />
|
||||
<link rel="alternate" type="application/rss+xml" title={`${SITE_NAME} — RSS`} href="/rss.xml" />
|
||||
<link rel="alternate" type="application/feed+json" title={`${SITE_NAME} — JSON Feed`} href="/feed.json" />
|
||||
{feeds.map((f) => (
|
||||
<link rel="alternate" type="application/rss+xml" title={f.title} href={f.href} />
|
||||
))}
|
||||
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:title" content={title} />
|
||||
|
||||
@@ -20,6 +20,8 @@ interface Props {
|
||||
modifiedTime?: Date;
|
||||
articleTags?: string[];
|
||||
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
||||
/** Page-scoped feeds (category/tag) advertised alongside the site-wide ones. */
|
||||
feeds?: Array<{ title: string; href: string }>;
|
||||
/** Opt into the wider content tier on roomy viewports (grid/listing pages). */
|
||||
wide?: boolean;
|
||||
}
|
||||
@@ -40,6 +42,7 @@ const {
|
||||
modifiedTime,
|
||||
articleTags,
|
||||
jsonLd,
|
||||
feeds,
|
||||
wide,
|
||||
} = Astro.props;
|
||||
const siteName = 'Unique';
|
||||
@@ -77,6 +80,7 @@ const year = new Date().getFullYear();
|
||||
modifiedTime={modifiedTime}
|
||||
articleTags={articleTags}
|
||||
jsonLd={jsonLd}
|
||||
feeds={feeds}
|
||||
/>
|
||||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "fc978456945c49ae90a0b7e0b891ffd2"}'></script>
|
||||
</head>
|
||||
@@ -123,6 +127,7 @@ const year = new Date().getFullYear();
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/grid/">Grid</a></li>
|
||||
<li><a href="/sources/">Sources</a></li>
|
||||
<li><a href="/feeds/">Feeds</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
/**
|
||||
* One source of truth for everything syndicated: the main /rss.xml and
|
||||
* /feed.json feeds plus every per-category and per-tag feed derive their
|
||||
* items from here. Mirrors getAllEntries() (reviews + posts + bundles —
|
||||
* finds stay bundle-only) but keeps the description field feeds need.
|
||||
*/
|
||||
export type FeedItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
pubDate: Date;
|
||||
/** Site-relative path, e.g. /reviews/foo/ */
|
||||
link: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export const FEED_TITLE = 'Unique';
|
||||
export const FEED_DESCRIPTION =
|
||||
'A small log of delightful, unique things — products, apps, phenomena, oddities.';
|
||||
|
||||
let itemsCache: Promise<FeedItem[]> | null = null;
|
||||
|
||||
export function getFeedItems(): Promise<FeedItem[]> {
|
||||
if (!itemsCache) itemsCache = buildFeedItems();
|
||||
return itemsCache;
|
||||
}
|
||||
|
||||
async function buildFeedItems(): Promise<FeedItem[]> {
|
||||
const [reviews, posts, bundles] = await Promise.all([
|
||||
getCollection('reviews'),
|
||||
getCollection('posts'),
|
||||
getCollection('bundles'),
|
||||
]);
|
||||
|
||||
const items: FeedItem[] = [
|
||||
...reviews.map((r) => ({
|
||||
title: r.data.name,
|
||||
description: r.data.description ?? r.data.subtitle ?? '',
|
||||
pubDate: r.data.date,
|
||||
link: `/reviews/${r.id}/`,
|
||||
category: r.data.category,
|
||||
tags: r.data.tags,
|
||||
})),
|
||||
...posts.map((p) => ({
|
||||
title: p.data.title,
|
||||
description: p.data.description ?? '',
|
||||
pubDate: p.data.date,
|
||||
link: `/posts/${p.id}/`,
|
||||
category: p.data.category,
|
||||
tags: p.data.tags,
|
||||
})),
|
||||
...bundles.map((b) => ({
|
||||
title: b.data.title,
|
||||
description: b.data.description ?? b.data.blurb ?? '',
|
||||
pubDate: b.data.date,
|
||||
link: `/bundles/${b.id}/`,
|
||||
category: b.data.category,
|
||||
tags: b.data.tags,
|
||||
})),
|
||||
];
|
||||
|
||||
return items.sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf());
|
||||
}
|
||||
|
||||
/** Shape @astrojs/rss expects. */
|
||||
export function toRssItems(items: FeedItem[]) {
|
||||
return items.map((i) => ({
|
||||
title: i.title,
|
||||
description: i.description,
|
||||
pubDate: i.pubDate,
|
||||
link: i.link,
|
||||
categories: [i.category, ...i.tags],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an RSS 2.0 response with the trimmings every feed on the site
|
||||
* shares: language, an atom:link self reference, and the browser-facing
|
||||
* XSL stylesheet.
|
||||
*/
|
||||
export function renderRss(opts: {
|
||||
site: URL;
|
||||
/** Site-relative path of the feed itself, e.g. /rss.xml */
|
||||
path: string;
|
||||
title: string;
|
||||
description: string;
|
||||
items: FeedItem[];
|
||||
}) {
|
||||
const selfUrl = new URL(opts.path, opts.site).href;
|
||||
return rss({
|
||||
title: opts.title,
|
||||
description: opts.description,
|
||||
site: opts.site,
|
||||
items: toRssItems(opts.items),
|
||||
stylesheet: '/feed.xsl',
|
||||
xmlns: { atom: 'http://www.w3.org/2005/Atom' },
|
||||
customData: [
|
||||
'<language>en-us</language>',
|
||||
`<atom:link href="${selfUrl}" rel="self" type="application/rss+xml"/>`,
|
||||
].join(''),
|
||||
});
|
||||
}
|
||||
|
||||
export type FeedScope = { name: string; count: number };
|
||||
|
||||
export async function getFeedCategories(): Promise<FeedScope[]> {
|
||||
return scopeCounts((i) => [i.category]);
|
||||
}
|
||||
|
||||
export async function getFeedTags(): Promise<FeedScope[]> {
|
||||
return scopeCounts((i) => i.tags);
|
||||
}
|
||||
|
||||
async function scopeCounts(
|
||||
pick: (i: FeedItem) => string[]
|
||||
): Promise<FeedScope[]> {
|
||||
const items = await getFeedItems();
|
||||
const counts = new Map<string, number>();
|
||||
for (const item of items) {
|
||||
for (const name of pick(item)) {
|
||||
counts.set(name, (counts.get(name) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return [...counts.entries()]
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
@@ -106,6 +106,14 @@ export const WEBSITE = {
|
||||
'A small log of delightful, unique things — products, apps, phenomena, oddities.',
|
||||
publisher: { '@id': `${SITE_URL}/#org` },
|
||||
inLanguage: 'en-US',
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: `${SITE_URL}/search/?q={search_term_string}`,
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
};
|
||||
|
||||
const SOFTWARE_CATEGORIES = new Set([
|
||||
|
||||
@@ -60,6 +60,7 @@ const breadcrumbs = buildBreadcrumbs([
|
||||
lastUpdated={lastUpdated}
|
||||
ogImage={ogImage}
|
||||
jsonLd={[collectionPage, itemList, breadcrumbs]}
|
||||
feeds={[{ title: `Unique — ${category} (RSS)`, href: `/categories/${category}/rss.xml` }]}
|
||||
wide
|
||||
>
|
||||
<EntryGrid entries={entries} />
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getFeedItems, getFeedCategories, renderRss, FEED_TITLE } from '../../../lib/feed';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = await getFeedCategories();
|
||||
return categories.map(({ name }) => ({ params: { category: name } }));
|
||||
}
|
||||
|
||||
export async function GET(context) {
|
||||
const { category } = context.params;
|
||||
const items = (await getFeedItems()).filter((i) => i.category === category);
|
||||
return renderRss({
|
||||
site: context.site,
|
||||
path: `/categories/${category}/rss.xml`,
|
||||
title: `${FEED_TITLE} — ${category}`,
|
||||
description: `Entries in ${category} on ${FEED_TITLE}.`,
|
||||
items,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getFeedItems, FEED_TITLE, FEED_DESCRIPTION } from '../lib/feed';
|
||||
|
||||
// JSON Feed 1.1 — https://jsonfeed.org/version/1.1
|
||||
export async function GET(context) {
|
||||
const items = await getFeedItems();
|
||||
|
||||
const feed = {
|
||||
version: 'https://jsonfeed.org/version/1.1',
|
||||
title: FEED_TITLE,
|
||||
home_page_url: context.site.href,
|
||||
feed_url: new URL('/feed.json', context.site).href,
|
||||
description: FEED_DESCRIPTION,
|
||||
language: 'en-US',
|
||||
authors: [{ name: 'rzen', url: new URL('/about/', context.site).href }],
|
||||
items: items.map((i) => ({
|
||||
id: new URL(i.link, context.site).href,
|
||||
url: new URL(i.link, context.site).href,
|
||||
title: i.title,
|
||||
content_text: i.description,
|
||||
date_published: i.pubDate.toISOString(),
|
||||
tags: [i.category, ...i.tags],
|
||||
})),
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(feed, null, 2), {
|
||||
headers: { 'Content-Type': 'application/feed+json' },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getFeedCategories, getFeedTags } from '../lib/feed';
|
||||
import { buildOgImage, buildBreadcrumbs, absoluteUrl } from '../lib/seo';
|
||||
|
||||
const categories = await getFeedCategories();
|
||||
const tags = await getFeedTags();
|
||||
|
||||
const description =
|
||||
'Subscribe to Unique: RSS and JSON feeds for everything, plus per-category and per-tag feeds.';
|
||||
|
||||
const ogImage = buildOgImage(undefined, 'site', 'Feeds on Unique');
|
||||
|
||||
const collectionPage = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
'@id': absoluteUrl('/feeds/#collection'),
|
||||
name: 'Feeds — Unique',
|
||||
description,
|
||||
url: absoluteUrl('/feeds/'),
|
||||
inLanguage: 'en-US',
|
||||
};
|
||||
|
||||
const breadcrumbs = buildBreadcrumbs([
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Feeds', url: '/feeds/' },
|
||||
]);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Feeds"
|
||||
description={description}
|
||||
subheader="Take Unique with you."
|
||||
jsonLd={[collectionPage, breadcrumbs]}
|
||||
>
|
||||
<h1>Feeds</h1>
|
||||
<p>
|
||||
Everything published here — reviews, bundles, posts — is available as a
|
||||
feed. Paste any of these into your feed reader.
|
||||
</p>
|
||||
|
||||
<h2>Everything</h2>
|
||||
<ul class="feed-list">
|
||||
<li>
|
||||
<a href="/rss.xml">/rss.xml</a> — all entries, RSS 2.0
|
||||
</li>
|
||||
<li>
|
||||
<a href="/feed.json">/feed.json</a> — all entries,
|
||||
<a href="https://jsonfeed.org/">JSON Feed 1.1</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>By category</h2>
|
||||
<p class="hint">Each category page also advertises its own feed for autodiscovery.</p>
|
||||
<ul class="feed-list scopes">
|
||||
{categories.map(({ name, count }) => (
|
||||
<li>
|
||||
<a href={`/categories/${name}/`}>{name}</a>
|
||||
<span class="count">({count})</span>
|
||||
<a class="rss" href={`/categories/${name}/rss.xml`}>RSS</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h2>By tag</h2>
|
||||
<ul class="feed-list scopes">
|
||||
{tags.map(({ name, count }) => (
|
||||
<li>
|
||||
<a href={`/tags/${name}/`}>#{name}</a>
|
||||
<span class="count">({count})</span>
|
||||
<a class="rss" href={`/tags/${name}/rss.xml`}>RSS</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h2>For robots</h2>
|
||||
<p>Reading the site programmatically? You may also want:</p>
|
||||
<ul class="feed-list">
|
||||
<li>
|
||||
<a href="/search.json">/search.json</a> — the full index of everything on
|
||||
the site as JSON, including individual finds (which don't appear in the
|
||||
feeds — they surface through bundles)
|
||||
</li>
|
||||
<li>
|
||||
<a href="/llms.txt">/llms.txt</a> — a plain-text map of the site for
|
||||
language models and other tools
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sitemap-index.xml">/sitemap-index.xml</a> — every page, for crawlers
|
||||
</li>
|
||||
<li>
|
||||
Every page embeds schema.org JSON-LD (<code>Review</code>,
|
||||
<code>Product</code>, <code>Article</code>, <code>CollectionPage</code>…)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
margin-top: 2.25rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
.hint {
|
||||
margin: 0.25rem 0 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.feed-list {
|
||||
margin: 0.5rem 0 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
.feed-list li {
|
||||
margin: 0.3rem 0;
|
||||
}
|
||||
.feed-list.scopes {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem 1.4rem;
|
||||
}
|
||||
.feed-list.scopes li {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.count {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.rss {
|
||||
margin-left: 0.45rem;
|
||||
padding: 0.05rem 0.4rem;
|
||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
background: var(--rule);
|
||||
color: var(--muted);
|
||||
border-radius: 999px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.rss:hover {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { APIContext } from 'astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { FEED_DESCRIPTION } from '../lib/feed';
|
||||
|
||||
// llms.txt — https://llmstxt.org/ — a plain-Markdown map of the site for
|
||||
// language models and other programmatic readers. Regenerated on every
|
||||
// build from the content collections, so it never goes stale.
|
||||
export async function GET(context: APIContext) {
|
||||
const site = context.site!;
|
||||
const abs = (path: string) => new URL(path, site).href;
|
||||
|
||||
const [reviews, bundles, posts, finds] = await Promise.all([
|
||||
getCollection('reviews'),
|
||||
getCollection('bundles'),
|
||||
getCollection('posts'),
|
||||
getCollection('find'),
|
||||
]);
|
||||
|
||||
const byDateDesc = <T extends { data: { date: Date } }>(a: T, b: T) =>
|
||||
b.data.date.valueOf() - a.data.date.valueOf();
|
||||
|
||||
const reviewLines = reviews
|
||||
.sort(byDateDesc)
|
||||
.map(
|
||||
(r) =>
|
||||
`- [${r.data.name}](${abs(`/reviews/${r.id}/`)}): ${r.data.description ?? r.data.subtitle}`
|
||||
);
|
||||
|
||||
const bundleLines = bundles
|
||||
.sort(byDateDesc)
|
||||
.map(
|
||||
(b) =>
|
||||
`- [${b.data.title}](${abs(`/bundles/${b.id}/`)}): ${b.data.description ?? b.data.blurb ?? `${b.data.items.length} finds`}`
|
||||
);
|
||||
|
||||
const postLines = posts
|
||||
.sort(byDateDesc)
|
||||
.map(
|
||||
(p) => `- [${p.data.title}](${abs(`/posts/${p.id}/`)}): ${p.data.description ?? ''}`
|
||||
);
|
||||
|
||||
const body = `# Unique
|
||||
|
||||
> ${FEED_DESCRIPTION} One person's editorial log, published daily at ${site.href} — every entry is something genuinely delightful, hand-picked and written up.
|
||||
|
||||
Content lives in four collections: **reviews** (full write-ups), **bundles** (themed roundups), **posts** (essays), and **finds** (short captures — ${finds.length} so far — that surface only through the bundles referencing them, at /find/<slug>/).
|
||||
|
||||
## Feeds
|
||||
|
||||
- [Everything (RSS 2.0)](${abs('/rss.xml')})
|
||||
- [Everything (JSON Feed 1.1)](${abs('/feed.json')})
|
||||
- Per-category RSS at /categories/<category>/rss.xml and per-tag RSS at /tags/<tag>/rss.xml — all listed at [/feeds/](${abs('/feeds/')})
|
||||
|
||||
## Machine-readable
|
||||
|
||||
- [Full site index as JSON](${abs('/search.json')}): every review, bundle, post, and find with title, URL, date, tags, and blurb
|
||||
- [Sitemap](${abs('/sitemap-index.xml')})
|
||||
- Every page embeds schema.org JSON-LD (Review, Product, SoftwareApplication, Article, CollectionPage, BreadcrumbList)
|
||||
|
||||
## Reviews
|
||||
|
||||
${reviewLines.join('\n')}
|
||||
|
||||
## Bundles
|
||||
|
||||
${bundleLines.join('\n')}
|
||||
|
||||
## Posts
|
||||
|
||||
${postLines.join('\n')}
|
||||
|
||||
## About
|
||||
|
||||
- [About the site](${abs('/about/')})
|
||||
- [Legal & disclosures](${abs('/legal/')}): affiliate-link policy, privacy
|
||||
`;
|
||||
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
+6
-38
@@ -1,43 +1,11 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getFeedItems, renderRss, FEED_TITLE, FEED_DESCRIPTION } from '../lib/feed';
|
||||
|
||||
export async function GET(context) {
|
||||
const [reviews, posts, bundles] = await Promise.all([
|
||||
getCollection('reviews'),
|
||||
getCollection('posts'),
|
||||
getCollection('bundles'),
|
||||
]);
|
||||
|
||||
const items = [
|
||||
...reviews.map((r) => ({
|
||||
title: r.data.name,
|
||||
description: r.data.description ?? r.data.subtitle ?? '',
|
||||
pubDate: r.data.date,
|
||||
link: `/reviews/${r.id}/`,
|
||||
categories: [r.data.category, ...r.data.tags],
|
||||
})),
|
||||
...posts.map((p) => ({
|
||||
title: p.data.title,
|
||||
description: p.data.description ?? '',
|
||||
pubDate: p.data.date,
|
||||
link: `/posts/${p.id}/`,
|
||||
categories: [p.data.category, ...p.data.tags],
|
||||
})),
|
||||
...bundles.map((b) => ({
|
||||
title: b.data.title,
|
||||
description: b.data.description ?? b.data.blurb ?? '',
|
||||
pubDate: b.data.date,
|
||||
link: `/bundles/${b.id}/`,
|
||||
categories: [b.data.category, ...b.data.tags],
|
||||
})),
|
||||
].sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf());
|
||||
|
||||
return rss({
|
||||
title: 'Unique',
|
||||
description:
|
||||
'A small log of delightful, unique things — products, apps, phenomena, oddities.',
|
||||
return renderRss({
|
||||
site: context.site,
|
||||
items,
|
||||
customData: '<language>en-us</language>',
|
||||
path: '/rss.xml',
|
||||
title: FEED_TITLE,
|
||||
description: FEED_DESCRIPTION,
|
||||
items: await getFeedItems(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ const breadcrumbs = buildBreadcrumbs([
|
||||
lastUpdated={lastUpdated}
|
||||
ogImage={ogImage}
|
||||
jsonLd={[collectionPage, itemList, breadcrumbs]}
|
||||
feeds={[{ title: `Unique — #${tag} (RSS)`, href: `/tags/${tag}/rss.xml` }]}
|
||||
wide
|
||||
>
|
||||
<EntryGrid entries={entries} />
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getFeedItems, getFeedTags, renderRss, FEED_TITLE } from '../../../lib/feed';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const tags = await getFeedTags();
|
||||
return tags.map(({ name }) => ({ params: { tag: name } }));
|
||||
}
|
||||
|
||||
export async function GET(context) {
|
||||
const { tag } = context.params;
|
||||
const items = (await getFeedItems()).filter((i) => i.tags.includes(tag));
|
||||
return renderRss({
|
||||
site: context.site,
|
||||
path: `/tags/${tag}/rss.xml`,
|
||||
title: `${FEED_TITLE} — #${tag}`,
|
||||
description: `Entries tagged #${tag} on ${FEED_TITLE}.`,
|
||||
items,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user