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
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.localis under The code below.
- 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.tsis under The code below. - Base URL and endpoints, if you want to call them yourself:
- Create this file. It fetches with your key on the server, so the key never reaches the browser:
- 3
Add the blog pages
Two App Router pages: the list and one page per article.
- The blog index at /blog:
app/blog/page.tsxis under The code below. - Each article at /blog/{slug}, with its title, description and FAQ schema:
app/blog/[slug]/page.tsxis under The code below. - Style them with your own layout and typography. The article HTML is plain semantic markup (headings, paragraphs, lists, links, images).
- The blog index at /blog:
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
/bloglists 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.
TroubleshootingQuestions#
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 (server-only: never prefix it with NEXT_PUBLIC_)
CITECA_API_KEY=citeca_your_key_hereconst 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));
}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>
);
}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.
