/**
* Formatting helpers.
*
* Dates are formatted in IDENTITY.timeZone, never in the build machine's local
* zone. Without that, the same post renders with a different date depending on
* whether it was built on your laptop or in CI.
*/
import { IDENTITY } from '../config.js';
const long = new Intl.DateTimeFormat(IDENTITY.locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: IDENTITY.timeZone,
});
const short = new Intl.DateTimeFormat(IDENTITY.locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: IDENTITY.timeZone,
});
const monthYear = new Intl.DateTimeFormat(IDENTITY.locale, {
year: 'numeric',
month: 'long',
timeZone: IDENTITY.timeZone,
});
/** "5 August 2026" */
export const formatDate = (date: Date): string => long.format(date);
/** "5 Aug 2026" */
export const formatDateShort = (date: Date): string => short.format(date);
/** "August 2026" — listing group headers. */
export const formatMonth = (date: Date): string => monthYear.format(date);
/** The machine-readable form for <time datetime>. */
export const isoDate = (date: Date): string => date.toISOString();
/** Group entries under a month heading, newest month first. */
export function groupByMonth<T extends { pubDate: Date }>(entries: T[]): Array<[string, T[]]> {
const groups = new Map<string, T[]>();
for (const entry of entries) {
const key = formatMonth(entry.pubDate);
const bucket = groups.get(key);
if (bucket) bucket.push(entry);
else groups.set(key, [entry]);
}
return [...groups.entries()];
}
/**
* Rough reading time. Deliberately vague — "6 min" is useful, "6.4 min" is
* false precision about someone else's reading speed.
*/
export function readingTime(text: string): string {
const words = text.trim().split(/\s+/).length;
return `${Math.max(1, Math.round(words / 220))} min`;
}
/** Strip a URL down to something worth showing as a link label. */
export function prettyHost(url: string): string {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch {
return url;
}
}