~james/core

core/blog/src/lib/posts.ts -rw-r--r-- 3.7 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
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
80
81
82
83
84
85
86
87
88
89
import { existsSync } from "node:fs";
import { getCollection, type CollectionEntry } from "astro:content";

export type Post = CollectionEntry<"posts">;

const BASE = import.meta.env.BASE_URL.replace(/\/$/, "");

export const withBase = (path: string): string => `${BASE}${path}`;

// drafts show up in `astro dev` and nowhere else
export const getPosts = async (): Promise<Post[]> => {
	const posts = await getCollection("posts", (post) => import.meta.env.DEV || !post.data.draft);
	return posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
};

// "5 October 2026", set in capitals by .dateline
const dateFormat = new Intl.DateTimeFormat("en-GB", {
	year: "numeric",
	month: "long",
	day: "numeric",
	timeZone: "UTC",
});

export const formatDate = (date: Date): string => dateFormat.format(date);

export const profileUrl = (handle: string): string => `https://tangled.org/${handle}`;

// fetched once from bluesky's public api into public/avatars: the build is
// offline and tangled's avatar service needs signed urls
export const avatarSrc = (handle: string): string | undefined =>
	existsSync(`public/avatars/${handle}.jpg`) ? withBase(`/avatars/${handle}.jpg`) : undefined;

export const byline = ({ data }: Post): string => data.authors.map((author) => author.name).join(", ");

// pages build to <slug>/index.html, and cloudflare redirects /<slug> to /<slug>/, so
// links and canonical urls take the slash (the feed keeps its old ids, without it)
export const postPath = ({ data }: Post): string => withBase(`/${data.slug}/`);

// a post as plain markdown, for /<slug>.md and llms-full.txt: the source body, with
// root-relative links made absolute so they still resolve once the text is lifted
// out of the site. an .mdx post's imports and component lines (its diagrams) have
// no markdown form, so they're left out; an inline component that only dresses up
// some text (<Ripple text="…" />) is left as that text
const MDX_ONLY = /^(import|export) .*$|^<[A-Z][\w.]*\b[^>]*\/>$/gm;
const MDX_TEXT = /<[A-Z][\w.]*\s+text="([^"]*)"\s*\/>/g;
export const postMarkdown = (post: Post, site: URL): string => {
	const { title, subtitle, date } = post.data;
	const body = (post.body ?? "")
		.replace(MDX_ONLY, "")
		.replace(MDX_TEXT, "$1")
		.replace(/\n{3,}/g, "\n\n")
		.replace(/\]\(\//g, `](${new URL("/", site).href}`);
	const lede = subtitle ? `\n> ${subtitle}\n` : "";
	return `# ${title}\n${lede}\n${byline(post)} · ${formatDate(date)} · ${new URL(postPath(post), site).href}\n\n${body.trim()}\n`;
};

// the standard.site records `blog publish` (cmd/blog, blog/standard.go) keeps on
// the tangled.org pds; the pages link to them for clients that verify the site
const REPO_DID = "did:plc:wshs7t2adsemcrrd4snkeqli";
export const PUBLICATION_URI = `at://${REPO_DID}/site.standard.publication/3myiuoozs2222`;
export const documentUri = ({ data }: Post): string | undefined =>
	data.rkey && `at://${REPO_DID}/site.standard.document/${data.rkey}`;

const publisher = {
	"@type": "Organization",
	name: "Tangled",
	url: "https://tangled.org",
	logo: "https://tangled.org/blog/logos/dolly.png",
};

export const postAuthors = ({ data }: Post) =>
	data.authors.map((author) => ({ name: author.name, url: profileUrl(author.handle) }));

// schema.org BlogPosting, for the post page's json-ld and the index's blogPost list
export const postLd = (post: Post, site: URL) => {
	const { title, subtitle, date, image } = post.data;
	const url = new URL(postPath(post), site).href;
	return {
		"@type": "BlogPosting",
		headline: title,
		description: subtitle || undefined,
		datePublished: date.toISOString(),
		url,
		mainEntityOfPage: url,
		image,
		author: postAuthors(post).map((author) => ({ "@type": "Person", ...author })),
		publisher,
	};
};