Blog

Build post indexes, article pages, archives, search, and related-content navigation.

Render 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.blog is true.
  • 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

ts
// src/data/blog.ts
import { 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.

ts
const 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.blog is false.
  • List responses never assume html is present.
  • An old post slug permanently redirects when redirected and newSlug are 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 be null.
  • 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.