NE

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. 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. 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. 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. 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. 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. 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