Examples
Copy, paste, swap in your server, blog and key.
A plain HTML site with your own 404
Serve this page for every address under /blog (most hosts call it a rewrite or a single page fallback). The post or list renders in the browser; a missing post sends readers to your own /404.html.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Blog</title>
<link rel="stylesheet" href="/site.css">
</head>
<body>
<header><a href="/">My site</a> <a href="/blog">Blog</a></header>
<main id="blog" aria-busy="true">Loading…</main>
<script type="module">
import { createClient, mountBlog } from "https://drive.example.com/sdk/stavo-blog.js";
const blog = createClient({ url: "https://drive.example.com", blog: "weeknight-kitchen", key: "stc_YOUR_CONTENT_KEY" });
mountBlog(blog, { root: document.getElementById("blog"), base: "/blog", notFound: () => location.replace("/404.html") });
</script>
</body>
</html>Good for small sites. Remember that search engines and link previews may not see posts rendered in the browser; the next two examples render on the server.
Next.js
Fetch on the server and let Next.js cache the page. With revalidate, a new post shows up within five minutes and readers always get real HTML. Keep the key in an environment variable even though it's public: it keeps it out of your repository.
// app/blog/page.tsx (Next.js App Router): rendered on the server, refreshed every 5 minutes
import { createClient } from "@/lib/stavo-blog"; // stavo-blog.js copied into your project
const blog = createClient({ url: process.env.STAVO_URL!, blog: "weeknight-kitchen", key: process.env.STAVO_KEY! });
export const revalidate = 300;
export default async function Blog() {
const { posts } = await blog.posts.list({ limit: 20 });
return (
<ul>
{posts.map((p) => <li key={p.id}><a href={`/blog/${p.slug}`}>{p.title}</a></li>)}
</ul>
);
}// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { createClient } from "@/lib/stavo-blog";
const blog = createClient({ url: process.env.STAVO_URL!, blog: "weeknight-kitchen", key: process.env.STAVO_KEY! });
export const revalidate = 300;
export async function generateMetadata({ params }) {
const post = await blog.posts.get((await params).slug);
if (!post) return {};
return {
title: post.meta_title ?? post.title,
description: post.meta_description ?? post.excerpt,
alternates: { canonical: post.canonical_url ?? post.url },
openGraph: { title: post.title, images: post.og_image ?? post.feature_image ?? undefined },
};
}
export default async function Post({ params }) {
const post = await blog.posts.get((await params).slug);
if (!post) notFound(); // your site's own 404
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(post.json_ld).replace(/</g, "\\u003c") }} />
</article>
);
}Astro
Build one static page per post. Rebuild when you publish (a scheduled build every hour is often enough).
---
// src/pages/blog/[slug].astro (static build: one page per post)
import { createClient } from "../../lib/stavo-blog.js";
const blog = createClient({ url: import.meta.env.STAVO_URL, blog: "weeknight-kitchen", key: import.meta.env.STAVO_KEY });
export async function getStaticPaths() {
const { posts } = await blog.posts.list({ limit: "all", fields: "slug" });
return posts.map((p) => ({ params: { slug: p.slug } }));
}
const post = await blog.posts.get(Astro.params.slug);
if (!post) return Astro.redirect("/404");
---
<html lang="en">
<head>
<title>{post.meta_title ?? post.title}</title>
<meta name="description" content={post.meta_description ?? post.excerpt} />
<link rel="canonical" href={post.canonical_url ?? post.url} />
<script type="application/ld+json" set:html={JSON.stringify(post.json_ld)} />
</head>
<body>
<h1>{post.title}</h1>
<article set:html={post.html} />
</body>
</html>Getting SEO and link previews right
- Render posts on the server or at build time, so crawlers get the title, text and image in the HTML.
- Use the post's
meta_title,meta_description,og_image(orfeature_image) andcanonical_urlin your head tags. - Embed
json_ldas ascript type="application/ld+json". Recipe cards come with Recipe data, so food posts can show up as recipes in search. - If the blog also has its own pages on Stavo, point canonical URLs at the version you want indexed (a post's canonical URL setting does that) so search engines don't see two copies.