Next.js Blog

Connect Next.js Blog to Citeca and publish articles automatically.

Render your Citeca articles as a blog inside your own Next.js app (App Router). There is nothing to install: two pages and a small helper fetch your published articles from the API with your key, and refresh every hour.

Setup time

About 10 minutes

Access needed

Admin on the site or store

What we touch

Blog posts only

Note

This connection can only read and publish blog posts. Your theme, products and settings are never modified.

What you need#

  • A Next.js 15 or later app using the App Router
  • An API key, generated in Settings → Integrations → Next.js Blog

Connecting Next.js Blog#

Open Settings → Publishing, find Next.js Blog and press Connect. The wizard walks you through the steps below.

  1. 1

    Get your API key

    Your Next.js app reads your published articles with it (read-only, never drafts).

    • Generate a key and copy it. It’s shown once.
    • Add it to your app’s environment, and to your host’s environment variables (e.g. Vercel → Settings → Environment Variables): .env.local is under The code below.
  2. 2

    Add a small API helper

    Nothing to install: it’s a plain fetch against our API, cached for an hour.

    • Create this file. It fetches with your key on the server, so the key never reaches the browser: lib/citeca.ts is under The code below.
    • Base URL and endpoints, if you want to call them yourself:
  3. 3

    Add the blog pages

    Two App Router pages: the list and one page per article.

    • The blog index at /blog: app/blog/page.tsx is under The code below.
    • Each article at /blog/{slug}, with its title, description and FAQ schema: app/blog/[slug]/page.tsx is under The code below.
    • Style them with your own layout and typography. The article HTML is plain semantic markup (headings, paragraphs, lists, links, images).

Next.js blog ready

Deploy, open /blog and your published articles are there. New ones appear within the hour, no rebuild needed.

Check it worked#

Verify it’s working

3 checks

  • /blog lists your published articles.
  • Each article opens at /blog/{slug} with its own title and description.
  • A new article appears within the hour without a rebuild.

To test without waiting for a scheduled article, open any finished article in Blog content and press Publish. You get a result per destination immediately, including the exact error if something is wrong.

Managing the connection#

Once connected, the Next.js Blog card shows a green Connected dot and a Manage button. Everything about the connection is changed from there — there are three controls.

  • Test connection — checks the credentials against Next.js Blog right now. If they fail you get the platform’s own response, not a generic message.
  • Reconnect — walks through the setup again. This is how you paste a rotated credential or change where we publish. It replaces what is stored rather than adding a second connection.
  • Disconnect — deletes the stored credentials and stops publishing. Articles already published stay on your site untouched, and your drafts and schedule in Citeca are kept.

Note

When something fails you get a plain explanation and the steps to fix it. The platform’s own response — status code, endpoint and response body — is folded away under Details for support; “Copy error report” puts all of it in one paste for a support message.

Troubleshooting

Questions#

New articles are not showing on my deployed site.

Pages revalidate every hour (revalidate = 3600), so a new article can take up to an hour. Lower the number for faster updates, and check the key is set in your host’s environment variables, not only in .env.local.

Can I restyle the articles?

Yes. The article HTML is plain semantic markup (headings, paragraphs, lists, links, images) inside your own layout, so your typography and components apply.

I rotated the API key.

Set the new key in your environment and redeploy. The old one stops working immediately, so requests with it get 401.

The code#

.env.local

# .env.local (server-only: never prefix it with NEXT_PUBLIC_)
CITECA_API_KEY=citeca_your_key_here

lib/citeca.ts

const BASE = "https://www.citeca.ai/api/integrations/v1";

export type CitecaArticle = {
  id: string;
  title: string;
  slug: string;
  metaDescription: string | null;
  content_html: string; // starts with the article's <h1>
  heroImageUrl: string | null;
  heroImageAlt: string | null;
  faqJsonLd: Record<string, unknown> | null;
  readingTime: number;
  publishedAt: string;
};

async function citeca<T>(path: string): Promise<T | null> {
  const res = await fetch(BASE + path, {
    headers: { "x-api-key": process.env.CITECA_API_KEY ?? "" },
    next: { revalidate: 3600 }, // new articles appear within the hour
  });
  if (res.status === 404) return null;
  if (!res.ok) throw new Error("Citeca API " + res.status);
  return res.json() as Promise<T>;
}

export async function getArticles(): Promise<CitecaArticle[]> {
  const data = await citeca<{ articles: CitecaArticle[] }>("/articles?limit=100");
  return data?.articles ?? [];
}

export function getArticle(slug: string) {
  return citeca<CitecaArticle>("/articles/" + encodeURIComponent(slug));
}

app/blog/page.tsx

import Link from "next/link";
import { getArticles } from "@/lib/citeca";

export const revalidate = 3600;

export default async function BlogPage() {
  const articles = await getArticles();
  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {articles.map((a) => (
          <li key={a.id}>
            <Link href={`/blog/${a.slug}`}>{a.title}</Link>
            {a.metaDescription ? <p>{a.metaDescription}</p> : null}
          </li>
        ))}
      </ul>
    </main>
  );
}

app/blog/[slug]/page.tsx

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getArticle } from "@/lib/citeca";

export const revalidate = 3600;

type Props = { params: Promise<{ slug: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const article = await getArticle((await params).slug);
  if (!article) return {};
  return {
    title: article.title,
    description: article.metaDescription ?? undefined,
    openGraph: article.heroImageUrl ? { images: [article.heroImageUrl] } : undefined,
  };
}

export default async function ArticlePage({ params }: Props) {
  const article = await getArticle((await params).slug);
  if (!article) notFound();
  return (
    <main>
      <article dangerouslySetInnerHTML={{ __html: article.content_html }} />
      {article.faqJsonLd ? (
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify(article.faqJsonLd).replace(/</g, "\\u003c"),
          }}
        />
      ) : null}
    </main>
  );
}

Heads up

Keep the key server-side. lib/citeca.ts only runs in server components, so the key never reaches the browser; never name it NEXT_PUBLIC_….

Something missing or wrong here? Tell us and we will fix it.

Still stuck?

Can’t find the answer? Ask a person.

Tell us what you were trying to do and where it stopped. If the answer belongs in these docs, we add it here too.

Already a customer? Open your dashboard