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 => { 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 /index.html, and cloudflare redirects / to //, 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 /.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 () 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, }; };