buildBlog
uses node:fs
Build tcm's blog into out, in the docs site's look: a page for each post (#fp3gzlcf), with who wrote it, when,
and the posts before and after it; the list of them (index.html); and its RSS feed (rss.xml, blogFeed),
which every page names for feed readers to find. Each post's page names its standard.site record
(#yxevay4p). Each page says what it is for search engines and link previews.
tcm's server has it at /blog (pnpm server:deploy). A post's link to a docs page that isn't one is a problem.
import { BLOG_DESCRIPTION, BLOG_URL, blogFeed, esc, finishSite, highlighter, pageList, postDay, POSTS, renderPost, SOURCES, STANDARD_SITE, STYLE, } from "#codebase/tcmts/site"; import type { Post } from "#codebase/tcmts/site"; import fs from "node:fs"; import path from "node:path"; export async function buildBlog(out: string): Promise<{ posts: number; problems: string[] }> { const hl = await highlighter(); const docs = new Set(pageList(SOURCES["docs/README.md"]!).map((p) => p.file)); const problems: string[] = []; const page = ( meta: { title: string; name: string; description: string; url: string; post?: Post }, body: string, ) => { const { title, name, description, url, post } = meta; const head = [ `<title>${esc(title)}</title>`, `<meta name="description" content="${esc(description)}">`, `<link rel="canonical" href="${url}">`, `<link rel="alternate" type="application/rss+xml" title="tcm blog" href="rss.xml">`, `<meta property="og:site_name" content="tcm">`, `<meta property="og:type" content="${post ? "article" : "website"}">`, `<meta property="og:title" content="${esc(name)}">`, `<meta property="og:description" content="${esc(description)}">`, `<meta property="og:url" content="${url}">`, ...(post ? [ `<meta property="article:published_time" content="${post.date}">`, `<meta name="author" content="${esc(post.author)}">`, // (its standard.site record, which readers on the AT Protocol check it's the page of) STANDARD_SITE.link(post.slug), ] : []), `<meta name="twitter:card" content="summary">`, ].join("\n"); return `<!doctype html>\n<html lang="en"><head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n${head}\n<link rel="stylesheet" href="style.css">\n</head><body>\n<header class="top"><div class="bar"><a class="logo" href="/"><span class="mark"></span>tcm</a><nav class="links" aria-label="site"><a href="/docs/">Docs</a><a href="/docs/examples.html">Examples</a><a class="on" href="./">Blog</a><a href="/forge/">Forge</a><a href="/registry/">Registry</a></nav></div></header>\n<div class="site single blog">\n<main>\n${body}\n</main>\n</div>\n</body></html>\n`; }; fs.rmSync(out, { recursive: true, force: true }); fs.mkdirSync(out, { recursive: true }); fs.writeFileSync(path.join(out, "style.css"), STYLE); const posts = POSTS.map((post) => ({ post, rendered: renderPost(post, hl, docs) })); const day = (post: Post) => `<time datetime="${post.date}">${postDay(post.date)}</time>`; posts.forEach(({ post, rendered: r }, i) => { problems.push(...r.problems.map((p) => `blog/${post.slug}: ${p}`)); // (newest first: the one before it in the list is newer) const newer = posts[i - 1]; const older = posts[i + 1]; const pager = newer || older ? `<nav class="pager" aria-label="newer and older posts">${ older ? `<a class="prev" href="${older.post.slug}.html"><small>Older</small>${esc(older.rendered.title)}</a>` : "<span></span>" }${ newer ? `<a class="next" href="${newer.post.slug}.html"><small>Newer</small>${esc(newer.rendered.title)}</a>` : "" }</nav>\n` : ""; fs.writeFileSync( path.join(out, `${post.slug}.html`), page( { title: `${r.title} — tcm`, name: r.title, description: r.description, url: `${BLOG_URL}${post.slug}.html`, post, }, `<article>\n<h1>${r.titleHtml}</h1>\n<p class="date">${day(post)} · ${esc(post.author)}</p>\n${r.body}</article>\n${pager}<p class="back"><a href="./">All posts</a> · <a href="rss.xml">RSS feed</a></p>`, ), ); }); const list = posts.map( ({ post, rendered: r }) => `<article><p class="date">${day(post)}</p><h2><a href="${post.slug}.html">${esc(r.title)}</a></h2><p>${r.summary}</p></article>`, ); fs.writeFileSync( path.join(out, "index.html"), page( { title: "Blog — tcm", name: "tcm blog", description: BLOG_DESCRIPTION, url: BLOG_URL }, `<h1>Blog</h1>\n<p class="lede">${esc(BLOG_DESCRIPTION)} <a href="rss.xml">RSS feed</a></p>\n<div class="posts">\n${list.join("\n")}\n</div>`, ), ); fs.writeFileSync(path.join(out, "rss.xml"), blogFeed(posts)); finishSite(out); return { posts: POSTS.length, problems }; }
uses
- tcmts.site.BLOG_DESCRIPTION
- tcmts.site.BLOG_URL
- tcmts.site.POSTS
- tcmts.site.Post
- tcmts.site.SOURCES
- tcmts.site.STANDARD_SITE
- tcmts.site.STYLE
- tcmts.site.blogFeed
- tcmts.site.esc
- tcmts.site.finishSite
- tcmts.site.highlighter
- tcmts.site.pageList
- tcmts.site.postDay
- tcmts.site.renderPost