import { site } from "@/lib/site";
import { authors, blog, categories } from "./config";
import {
BLOG_PATH,
categoryPath,
FEED_PATH,
postImagePath,
postMarkdownPath,
postPath,
} from "./routes";
import type { Post, PostSummary } from "./schema";
export const absolute = (path: string) => new URL(path, site.url).toString();
export const authorOf = (slug: string) =>
authors.find((a) => a.slug === slug) ?? authors[0];
export const categoryOf = (slug: string) =>
categories.find((c) => c.slug === slug);
export const lastChanged = (p: PostSummary) => p.updatedAt ?? p.publishedAt;
const xml = (s: string) =>
s
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
export function atomFeed(posts: PostSummary[]): string {
const updated =
posts.map(lastChanged).sort().at(-1) ?? new Date(0).toISOString();
const entries = posts.map((p) => {
const url = absolute(postPath(p.slug));
return `
${xml(p.title)}
${url}
${p.publishedAt}
${lastChanged(p)}
${xml(authorOf(p.author).name)}
${xml(p.description)}
`;
});
return `
${xml(blog.name)}
${xml(blog.description)}
${absolute(BLOG_PATH)}
${updated}
${entries.join("\n")}
`;
}
export function llmsTxt(posts: PostSummary[]): string {
const lines = [
`# ${blog.name}`,
"",
`> ${blog.description}`,
"",
`Every post has a plain Markdown copy at its URL plus \`.md\`. Site: ${site.url}`,
];
for (const c of categories) {
const inCat = posts.filter((p) => p.category === c.slug);
if (!inCat.length) continue;
lines.push("", `## ${c.name}`, "", c.description, "");
for (const p of inCat)
lines.push(
`- [${p.title}](${absolute(postMarkdownPath(p.slug))}): ${p.description}`,
);
}
return `${lines.join("\n")}\n`;
}
export function postMarkdown(p: Post): string {
const author = authorOf(p.author);
const meta = [
`# ${p.title}`,
"",
`- Author: ${author.name} (${author.url})`,
`- Published: ${p.publishedAt.slice(0, 10)}`,
...(p.updatedAt ? [`- Updated: ${p.updatedAt.slice(0, 10)}`] : []),
`- Category: ${categoryOf(p.category)?.name ?? p.category}`,
`- Canonical: ${absolute(postPath(p.slug))}`,
"",
`> ${p.description}`,
"",
];
return `${meta.join("\n")}${p.body}\n`;
}
type Crumb = { name: string; path: string };
export function breadcrumbLd(crumbs: Crumb[]) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: crumbs.map((c, i) => ({
"@type": "ListItem",
position: i + 1,
name: c.name,
item: absolute(c.path),
})),
};
}
export function blogPostingLd(p: Post) {
const author = authorOf(p.author);
return {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: p.title,
description: p.description,
datePublished: p.publishedAt,
dateModified: lastChanged(p),
image: absolute(p.image ?? postImagePath(p.slug)),
mainEntityOfPage: absolute(postPath(p.slug)),
keywords: [p.primaryKeyword, ...p.secondaryKeywords].join(", "),
articleSection: categoryOf(p.category)?.name,
author: {
"@type": author.kind === "person" ? "Person" : "Organization",
name: author.name,
url: author.url,
sameAs: author.sameAs,
},
publisher: {
"@type": "Organization",
name: site.name,
url: site.url,
logo: absolute("/logo.svg"),
},
};
}
export const crumbs = {
index: { name: "Blog", path: BLOG_PATH },
category: (slug: string) => ({
name: categoryOf(slug)?.name ?? slug,
path: categoryPath(slug),
}),
};