> ## Documentation Index
> Fetch the complete documentation index at: https://docs.skayle.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Integrate Skayle CMS with Next.js 16 App Router using server components and static generation patterns.

## Overview

This guide uses:

* Next.js 16 (App Router)
* Server Components
* Static list generation with revalidation
* Dynamic `blog/[slug]` pages

## 1. Create environment variables

```bash theme={null}
# .env.local
SKAYLE_API_BASE_URL=https://api.skayle.ai
SKAYLE_ORG_ID=your_organization_id
```

## 2. Add a CMS fetch helper

Create `src/lib/skayle-cms.ts`:

```ts theme={null}
const API_BASE = process.env.SKAYLE_API_BASE_URL || "https://api.skayle.ai";
const ORG_ID = process.env.SKAYLE_ORG_ID;

if (!ORG_ID) {
	throw new Error("Missing SKAYLE_ORG_ID env var");
}

type JsonApiResource<T = Record<string, unknown>> = {
	id: string;
	type: string;
	attributes: T;
	relationships?: Record<string, unknown>;
};

type JsonApiListResponse<T> = {
	data: Array<JsonApiResource<T>>;
	included?: Array<JsonApiResource>;
	meta?: { total?: number; page?: number; per_page?: number };
};

type JsonApiSingleResponse<T> = {
	data: JsonApiResource<T> | null;
	included?: Array<JsonApiResource>;
};

export type CmsPost = {
	title: string;
	slug: string;
	excerpt: string | null;
	content_html: string | null;
	created_at: string;
	updated_at: string;
};

async function fetchJson<T>(path: string): Promise<T> {
	const url = `${API_BASE}/v1/${ORG_ID}${path}`;
	const res = await fetch(url, {
		next: { revalidate: 300 },
		headers: { "Content-Type": "application/json" },
	});

	if (!res.ok) {
		throw new Error(`Skayle CMS request failed: ${res.status}`);
	}

	return res.json() as Promise<T>;
}

export async function getPosts() {
	const response = await fetchJson<JsonApiListResponse<CmsPost>>(
		"/articles?status=published&orderby=date&order=desc&page=1&per_page=50",
	);
	return response.data;
}

export async function getPostBySlug(slug: string) {
	const response = await fetchJson<JsonApiSingleResponse<CmsPost>>(
		`/articles/${encodeURIComponent(slug)}?include=categories,tags,authors`,
	);
	return response.data;
}
```

## 3. Build static blog listing

Create `src/app/blog/page.tsx`:

```tsx theme={null}
import Link from "next/link";
import { getPosts } from "@/lib/skayle-cms";

export const revalidate = 300;

export default async function BlogPage() {
	const posts = await getPosts();

	return (
		<main>
			<h1>Blog</h1>
			<ul>
				{posts.map((post) => (
					<li key={post.id}>
						<Link href={`/blog/${post.attributes.slug}`}>
							{post.attributes.title}
						</Link>
					</li>
				))}
			</ul>
		</main>
	);
}
```

## 4. Build dynamic `blog/[slug]` page

Create `src/app/blog/[slug]/page.tsx`:

```tsx theme={null}
import { notFound } from "next/navigation";
import { getPostBySlug, getPosts } from "@/lib/skayle-cms";

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

export async function generateStaticParams() {
	const posts = await getPosts();
	return posts.map((post) => ({ slug: post.attributes.slug }));
}

export default async function BlogPostPage({ params }: PageProps) {
	const { slug } = await params;
	const post = await getPostBySlug(slug);

	if (!post) notFound();

	return (
		<article>
			<h1>{post.attributes.title}</h1>
			{post.attributes.excerpt ? <p>{post.attributes.excerpt}</p> : null}
			<div
				dangerouslySetInnerHTML={{
					__html: post.attributes.content_html || "",
				}}
			/>
		</article>
	);
}
```

## 5. Production-ready patterns

* Keep fetches server-side in App Router for stable SEO output.
* Use `revalidate` for incremental freshness.
* Use `notFound()` for missing slugs.
* Add a local error boundary (`error.tsx`) and loading states (`loading.tsx`) for resilience.
* If you need immediate freshness for preview-like behavior, use `cache: "no-store"` selectively.

## 6. Use non-article collections (optional)

If your organization uses additional collections/content types, fetch from collection items.
Non-article collections are available on the `Authority` plan.

```ts theme={null}
export async function getAnswerItems() {
	const response = await fetchJson<JsonApiListResponse<CmsPost>>(
		"/collections/answers/items?status=published&orderby=date&order=desc&page=1&per_page=50",
	);
	return response.data;
}
```

`/posts` is still supported as an alias, but `/articles` is the primary endpoint.
