/**
* The one place that knows how posts and photo sets combine into a single
* stream. The listing page, both feeds and the home page all read from here,
* so they can never disagree about what is published or in what order.
*/
import { getCollection, type CollectionEntry } from 'astro:content';
export type BlogEntry =
| ({ kind: 'post' } & CollectionEntry<'posts'>)
| ({ kind: 'photos' } & CollectionEntry<'photos'>);
/**
* Drafts and future-dated entries are visible while running `astro dev` and
* excluded from every production build. That means you can write a post, see
* it locally, commit it, and it stays invisible until you either flip
* `draft: false` or its pubDate arrives — without a second branch.
*/
function isPublished(data: { draft: boolean; pubDate: Date }): boolean {
if (import.meta.env.DEV) return true;
return !data.draft && data.pubDate.getTime() <= Date.now();
}
/** Every entry from both collections, newest first. */
export async function allEntries(): Promise<BlogEntry[]> {
const [posts, photos] = await Promise.all([getCollection('posts'), getCollection('photos')]);
const combined: BlogEntry[] = [
...posts.map((entry) => ({ kind: 'post' as const, ...entry })),
...photos.map((entry) => ({ kind: 'photos' as const, ...entry })),
];
return combined
.filter((entry) => isPublished(entry.data))
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
}
/** The URL path for an entry. Collection ids already carry the year folder. */
export function entryPath(entry: { id: string }): string {
return `/${entry.id}/`;
}
/**
* Map entries into the shape the feed builders want.
*
* `rendered.html` is what Astro's content loader produced when it processed
* the file. Plain Markdown lands here fully rendered, so subscribers get the
* whole piece. MDX compiles to a component rather than a string, so it has no
* usable html and the feed falls back to the summary — see lib/feeds.ts in
* @eratic/core for why we accept that instead of reaching for the
* experimental Container API.
*/
export async function feedItems() {
const entries = await allEntries();
return entries.map((entry) => ({
path: entryPath(entry),
title: entry.data.title,
summary: entry.data.summary,
pubDate: entry.data.pubDate,
updatedDate: entry.data.updatedDate,
tags: entry.data.tags,
content: (entry as { rendered?: { html?: string } }).rendered?.html,
}));
}
/** The little label that says what kind of thing this is. */
export function entryKicker(entry: BlogEntry): string {
if (entry.kind === 'photos') {
const count = entry.data.photos.length;
const place = entry.data.place ? `${entry.data.place}, ` : '';
return `${place}${count} photograph${count === 1 ? '' : 's'}`;
}
return 'Writing';
}