Blog
Build post indexes, article pages, archives, search, and related-content navigation.
A
JRender Cavuno’s published editorial content in your own layout. The list APIs return summaries; only board.blog.posts.retrieve returns the article html and full SEO fields.
Prerequisites
board.context().features.blogistrue.- Routes for
/blog,/blog/:postSlug, and any tag or author archives you expose. - A deliberate policy for rendering stored HTML in your framework.
Load an index and article
12345678910111213141516171819202122232425262728293031323334353637// src/data/blog.tsimport { isNotFound } from '@cavuno/board';import { blogPostPath } from '@cavuno/board/paths';import { board } from '../lib/board';export async function loadBlogIndex(cursor?: string) {const page = await board.blog.posts.list({ limit: 12, cursor });return {posts: page.data.map((post) => ({...post,path: blogPostPath(post.slug),})),nextCursor: page.nextCursor,};}export async function loadBlogPost(postSlug: string) {try {const post = await board.blog.posts.retrieve(postSlug);if (post.redirected && post.newSlug) {return { redirectTo: blogPostPath(post.newSlug) } as const;}const [adjacent, similar] = await Promise.all([board.blog.posts.adjacent(post.slug),board.blog.posts.similar(post.slug, { limit: 4 }),]);return { post, adjacent, similar: similar.data } as const;} catch (error) {if (isNotFound(error)) return null;throw error;}}
Use seoTitle ?? title for the page title and seoDescription ?? customExcerpt for the description. If canonicalUrl is present, prefer it over a locally constructed canonical.
Add archives and search
1234567891011121314const tagged = await board.blog.posts.list({tagSlug: 'engineering',limit: 12,});const authored = await board.blog.posts.list({authorSlug: 'jane-doe',limit: 12,});const results = await board.blog.search({query: 'salary transparency',limit: 20,});
Tag and author list methods return the archive entities themselves. Build archive paths with blogTagPath and blogAuthorPath from @cavuno/board/paths.
Expected result
The index contains summary cards without article HTML. A detail load returns the article, its optional adjacent posts, and related summaries, or a permanent redirect for an old slug.
Verify the result
- The blog route is absent when
features.blogisfalse. - List responses never assume
htmlis present. - An old post slug permanently redirects when
redirectedandnewSlugare returned. - Previous, next, and similar posts remain optional when no match exists.
Errors and edge cases
- Search limits results to 1–50; post lists support 1–100.
html, images, excerpts, reading time, and publication dates can benull.- Do not generate archive pages for empty tags or authors solely to fill a sitemap.
- If your rendering layer applies an additional HTML policy, test that it does not break valid article markup.
Production checklist
- Cache published content, but refresh it on the cadence your editorial team expects.
- Emit one canonical URL per post and honor externally configured
canonicalUrl. - Give feature images their returned alt text when present.
- Include published posts, used tags, and used authors in the sitemap.
Next, build data-led landing pages with Salaries.