eratic / sites / io / src / lib / entries.ts
317 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function escapeAttr(text: string): string {
	return escapeHtml(text).replace(/"/g, '&quot;');
}

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/eratic.git