Skip to main content

Overview

This guide uses:
  • Next.js 16 (App Router)
  • Server Components
  • Static list generation with revalidation
  • Dynamic blog/[slug] pages

1. Create environment variables

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