Cookbook
Package readmes tell you what each function does — this page shows how the packages work together. Every recipe is a complete, runnable fragment of a build script.
Blog with pagination and tag pages
@sphido/collections covers what nearly every blog re-implements
by hand: sorting by date, paginated index pages, tag pages from page.tags and
previous/next links.
import {allPages, getPages, writeFile} from '@sphido/core';
import {frontmatter} from '@sphido/frontmatter';
import {groupByTag, paginate, siblings, sortBy} from '@sphido/collections';
const pages = await getPages({path: 'content'}, frontmatter, (page) => {
page.slug = `${page.name}.html`;
});
// newest first; posts date their pages in the YAML front matter
const posts = sortBy([...allPages(pages)], (post) => post.date, 'desc');
// paginated index: /index.html, /page/2.html, ...
for (const {items, page, prev, next} of paginate(posts, 10)) {
const file = page === 1 ? 'public/index.html' : `public/page/${page}.html`;
await writeFile(file, renderIndex(items, {page, prev, next}));
}
// one page per tag: /tag/javascript.html, ...
for (const [tag, tagged] of groupByTag(posts)) {
await writeFile(`public/tag/${tag}.html`, renderIndex(tagged, {tag}));
}
// article pages with previous / next navigation
for (const post of posts) {
const {prev, next} = siblings(posts, post);
await writeFile(`public/${post.slug}`, renderPost(post, {prev, next}));
}
renderIndex and renderPost are your template functions — a template literal is
all Sphido expects.
RSS feed from front matter
@sphido/feed renders a valid RSS 2.0 feed from plain objects. Reuse
the sorted posts from the previous recipe:
import {renderFeed, writeFeed} from '@sphido/feed';
const items = posts.slice(0, 20).map((post) => ({
title: post.title,
url: new URL(post.slug, 'https://example.com').href,
date: new Date(post.date),
description: post.description,
}));
const xml = renderFeed({
title: 'My Blog',
link: 'https://example.com',
description: 'Notes about everything',
feedUrl: 'https://example.com/rss.xml',
}, items);
await writeFeed('public/rss.xml', xml);
Dates come out as RFC 822, lastBuildDate is taken from the newest item and
feedUrl adds the atom:link rel="self" element that feed validators expect.
Write your own extender
An extender is just a function that gets each page during getPages(). No plugin
API, no registration — reading time in five lines:
const readingTime = (page, dirent) => {
if (dirent.isFile() && page.content) {
page.minutes = Math.ceil(page.content.split(/\s+/).length / 200);
}
};
const pages = await getPages({path: 'content'}, frontmatter, readingTime);
Order matters: frontmatter loads page.content from disk, so readingTime runs
after it and gets the content for free. Extenders may be sync or async.
Dev server with live reload
@sphido/dev wraps any build function with a watcher, a static server
and browser reload. Export your build as a function and add a dev.js:
import {serve} from '@sphido/dev';
import {build} from './build.js';
await serve({watch: ['content'], output: 'public', build});
Every change in content/ rebuilds the site and reloads the browser. New projects
scaffolded with npm create sphido ship this setup out of the box.
Typed pages in TypeScript
Extender packages export the types they contribute, so a fully typed page is one intersection away:
import {getPages, type Page} from '@sphido/core';
import {frontmatter, type WithFrontmatter} from '@sphido/frontmatter';
import {hashtags, type WithHashtags} from '@sphido/hashtags';
type BlogPage = Page & WithFrontmatter & WithHashtags & {slug: string};
const pages = await getPages<BlogPage>({path: 'content'}, frontmatter, hashtags, (page) => {
page.slug = `${page.name}.html`;
});