import { getCollection, type CollectionEntry } from 'astro:content';
import { LINK_KINDS, type LinkKind } from '../content.config';
export type Issue = CollectionEntry<'issues'>;
function isPublished(data: { draft: boolean; pubDate: Date }): boolean {
if (import.meta.env.DEV) return true;
return !data.draft && data.pubDate.getTime() <= Date.now();
}
export async function allIssues(): Promise<Issue[]> {
const issues = await getCollection('issues');
return issues
.filter((entry) => isPublished(entry.data))
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
}
export function issuePath(entry: { id: string }): string {
return `/${entry.id}/`;
}
export function issueTitle(entry: Issue): string {
return entry.data.title ?? `Issue ${entry.data.issue}`;
}
/** Group an issue's links by kind, in a fixed order. */
export function groupLinks(entry: Issue): Array<[LinkKind, Issue['data']['links']]> {
return LINK_KINDS.map(
(kind) => [kind, entry.data.links.filter((link) => link.kind === kind)] as const,
).filter(([, links]) => links.length > 0) as Array<[LinkKind, Issue['data']['links']]>;
}
/**
* Feed items for io.
*
* The links are rendered into the feed content directly rather than left to
* `rendered.html`, because the links *are* the issue — a subscriber who only
* ever reads in their feed reader should get the whole thing, not a teaser
* plus a link back. This is the one section where the feed is the primary
* surface and the web page is the copy.
*/
export async function feedItems() {
const issues = await allIssues();
return issues.map((entry) => {
const body = (entry as { rendered?: { html?: string } }).rendered?.html ?? '';
const list = groupLinks(entry)
.map(([kind, links]) => {
const items = links
.map((link) => {
const via = link.via ? ` <em>via ${escapeHtml(link.via)}</em>` : '';
const note = link.note ? `<br />${escapeHtml(link.note)}` : '';
return `<li><a href="${escapeAttr(link.url)}">${escapeHtml(link.title)}</a>${via}${note}</li>`;
})
.join('');
return `<h3>${kind.charAt(0).toUpperCase()}${kind.slice(1)}</h3><ul>${items}</ul>`;
})
.join('');
return {
path: issuePath(entry),
title: issueTitle(entry),
summary: entry.data.summary,
pubDate: entry.data.pubDate,
updatedDate: entry.data.updatedDate,
tags: entry.data.tags,
content: body + list,
};
});
}
function escapeHtml(text: string): string {
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function escapeAttr(text: string): string {
return escapeHtml(text).replace(/"/g, '"');
}