Next.js Blog integration
Connect Auto SEO to Next.js Blog
About 10 minutes. Your Next.js site fetches your published articles from Auto SEO with one server-side key — no CMS, no webhook to host, and nothing writing into your repo. Works the same in Astro, Remix, SvelteKit or anything else that can make an authenticated fetch on the server.
- 1
Create a blog API key
In Auto SEO: Integrations → Next.js Blog → Create key. It's scoped to one website and read-only, and the secret is shown exactly once — we store only a hash, so there is no way to reveal it again. Lost it? Revoke and make a new one.
- 2
Put it in your environment
Add AUTOSEO_BLOG_API_KEY=asb_live_… to .env.local and to your host's environment variables. Never NEXT_PUBLIC_ it: that prefix ships the value to the browser, and the key would be readable in view-source.
- 3
Drop in the client
Copy lib/autoseo.ts below into your project. It's about 40 lines and has no dependencies — you own it, so you can change caching or add fields without waiting on a package release.
- 4
Add your blog routes
app/blog/page.tsx for the index and app/blog/[slug]/page.tsx for the article, both Server Components. Style them with your own components — we return the data, your site stays your site.
- 5
Wire the sitemap
app/blog/sitemap.ts composes your URLs from the slugs we return. We deliberately don't emit XML: your blog might live at /blog, /insights or the root, and a sitemap built on a guessed base URL is a sitemap of broken links.
- 6
Publish from Auto SEO
Set the site's publish target to Next.js Blog. Articles appear on your site as soon as your cache revalidates — an hour with the settings above, instantly if you call revalidatePath('/blog') from a route of your own.
Example payload / snippet
1. lib/autoseo.ts
// lib/autoseo.ts — the whole client. No dependency to install.
const API = "https://autoseo.it.com/api/content-api/v1";
// Server-side only. Never prefix this with NEXT_PUBLIC_ — that would ship your
// key in the browser bundle, and anyone could read it from view-source.
const KEY = process.env.AUTOSEO_BLOG_API_KEY;
export type Article = {
slug: string;
title: string;
metaTitle: string | null;
metaDescription: string | null;
excerpt: string;
image: string | null;
tags: string[];
wordCount: number;
readingTimeMinutes: number;
publishedAt: string | null;
updatedAt: string | null;
html?: string;
};
async function get<T>(path: string, revalidate = 3600): Promise<T> {
if (!KEY) throw new Error("AUTOSEO_BLOG_API_KEY is not set");
const res = await fetch(`${API}${path}`, {
headers: { Authorization: `Bearer ${KEY}` },
// Next caches this on your server. An hour keeps your blog fast and picks
// new articles up on its own; drop it to 0 while you're building.
next: { revalidate },
});
if (res.status === 404) return null as T;
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `Auto SEO API ${res.status}`);
}
return res.json() as Promise<T>;
}
export const listArticles = (page = 1, tag?: string) =>
get<{ articles: Article[]; page: number; totalPages: number; total: number }>(
`/articles?page=${page}${tag ? `&tag=${encodeURIComponent(tag)}` : ""}`,
);
export const getArticle = (slug: string) =>
get<{ article: Article } | null>(`/articles/${encodeURIComponent(slug)}`);
export const listTags = () => get<{ tags: { tag: string; slug: string; count: number }[] }>("/tags");
export const listSitemap = () =>
get<{ entries: { slug: string; updatedAt: string | null }[] }>("/sitemap");2. Your blog routes
// app/blog/page.tsx — the index
import Link from "next/link";
import { listArticles } from "@/lib/autoseo";
export const metadata = { title: "Blog" };
export default async function BlogIndex({
searchParams,
}: { searchParams: Promise<{ page?: string }> }) {
const { page } = await searchParams;
const { articles, totalPages, page: current } = await listArticles(Number(page) || 1);
return (
<main>
<h1>Blog</h1>
{articles.map((a) => (
<article key={a.slug}>
{a.image && <img src={a.image} alt="" />}
<h2><Link href={`/blog/${a.slug}`}>{a.title}</Link></h2>
<p>{a.excerpt}</p>
<small>{a.readingTimeMinutes} min read</small>
</article>
))}
{current < totalPages && <Link href={`/blog?page=${current + 1}`}>Next page</Link>}
</main>
);
}
// app/blog/[slug]/page.tsx — the article
import { notFound } from "next/navigation";
import { getArticle } from "@/lib/autoseo";
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const data = await getArticle(slug);
if (!data) return {};
const a = data.article;
return {
title: a.metaTitle || a.title,
description: a.metaDescription || a.excerpt,
alternates: { canonical: `/blog/${a.slug}` },
openGraph: {
title: a.metaTitle || a.title,
description: a.metaDescription || a.excerpt,
images: a.image ? [a.image] : [],
type: "article",
publishedTime: a.publishedAt || undefined,
},
};
}
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const data = await getArticle(slug);
if (!data) notFound();
const a = data.article;
return (
<article>
<h1>{a.title}</h1>
{a.publishedAt && <time dateTime={a.publishedAt}>{new Date(a.publishedAt).toLocaleDateString()}</time>}
{/* The body is HTML we generated and you approved. */}
<div dangerouslySetInnerHTML={{ __html: a.html! }} />
</article>
);
}3. Sitemap
// app/blog/sitemap.ts — your domain, our slugs
import type { MetadataRoute } from "next";
import { listSitemap } from "@/lib/autoseo";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const { entries } = await listSitemap();
return entries.map((e) => ({
url: `https://yoursite.com/blog/${e.slug}`,
lastModified: e.updatedAt ? new Date(e.updatedAt) : undefined,
}));
}What gets published
- title, slug, metaTitle, metaDescription and a derived excerpt
- The article body as HTML (contentFormat: "html") — only on the single-article endpoint, so your index stays light
- Featured image as an absolute URL, tags, word count and reading time
- publishedAt / updatedAt timestamps for your <time> elements and sitemap
Troubleshooting
401 means the key is wrong or revoked; 403 means you used an account API key instead of a blog key (make one under Integrations → Next.js Blog). A 404 from /articles/{slug} means no PUBLISHED article has that slug — drafts and scheduled posts are never returned, which is deliberate. If a new article isn't showing, it's almost always your own cache: the client above holds responses for an hour. Calling the API from a Client Component will fail — it's server-to-server only, and a browser preflight is answered with 405 on purpose so a leaked key can't be used from a page.
Slug: nextjs-blog