Connect to Your App
Copy-paste integration examples for the most popular frameworks. Pick the one you're using below.
Not sure which to pick? Use Next.js for new projects (it's the most full-featured), React if you're using Vite/CRA, Astro for static blogs, or Vanilla JS for any plain HTML site.
Next.js (App Router & React Server Components)
Integrate headless blog posts using Next.js Server Components with native fetch revalidation (ISR / SSG) and pagination navigation.
// app/blog/page.tsx
import Link from "next/link";
interface BlogPost {
_id: string;
title: string;
slug: string;
excerpt: string;
createdAt: string;
authorData?: { firstName: string; lastName: string };
categoryData?: { name: string; slug: string };
}
interface PaginationDTO {
total: number;
page: number;
limit: number;
itemsPerPage: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
nextPage: number | null;
prevPage: number | null;
}
async function getBlogPosts(page = 1, category = ""): Promise<{ data: BlogPost[]; pagination: PaginationDTO }> {
const url = new URL(`http://localhost:3000/api/v1/projects/${process.env.NEXT_PUBLIC_BLOG_PROJECT_ID}/posts`);
url.searchParams.set("status", "published");
url.searchParams.set("page", String(page));
url.searchParams.set("limit", "10");
if (category) url.searchParams.set("category", category);
const res = await fetch(url.toString(), {
headers: {
"x-api-key": process.env.BLOG_API_KEY || "",
},
// Revalidate every 60 seconds (Incremental Static Regeneration)
next: { revalidate: 60, tags: ["blog-posts"] },
});
if (!res.ok) throw new Error("Failed to fetch blog posts");
const json = await res.json();
return { data: json.data, pagination: json.pagination };
}
export default async function BlogIndexPage({ searchParams }: { searchParams: Promise<{ page?: string; category?: string }> }) {
const params = await searchParams;
const page = parseInt(params.page || "1");
const { data: posts, pagination } = await getBlogPosts(page, params.category || "");
return (
<main className="max-w-4xl mx-auto py-12 px-4 space-y-8">
<h1 className="text-3xl font-bold">Latest Articles (Page {pagination.page} of {pagination.totalPages})</h1>
<div className="grid gap-6">
{posts.map((post) => (
<article key={post._id} className="p-6 rounded-xl border border-gray-200">
<span className="text-xs font-semibold text-amber-600">{post.categoryData?.name}</span>
<h2 className="text-xl font-bold mt-1">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
<p className="text-gray-600 text-sm mt-2">{post.excerpt}</p>
</article>
))}
</div>
{/* Pagination Controls using DTO flags */}
<div className="flex justify-between items-center pt-6 border-t border-gray-200">
{pagination.hasPrevPage ? (
<Link href={`/blog?page=${pagination.prevPage}`} className="px-4 py-2 bg-gray-100 rounded-lg">← Previous Page</Link>
) : <span />}
<span className="text-xs text-gray-500">Total {pagination.total} Posts</span>
{pagination.hasNextPage ? (
<Link href={`/blog?page=${pagination.nextPage}`} className="px-4 py-2 bg-gray-900 text-white rounded-lg">Next Page →</Link>
) : <span />}
</div>
</main>
);
}React (Vite / Client Hook)
Fetch blog posts dynamically on the client side using custom React hooks with full pagination state and filtering.
// hooks/useBlogPosts.ts
import { useEffect, useState } from "react";
export function useBlogPosts(projectId: string, apiKey: string, page = 1, category = "") {
const [posts, setPosts] = useState([]);
const [pagination, setPagination] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchPosts() {
setLoading(true);
try {
const url = new URL(`http://localhost:3000/api/v1/projects/${projectId}/posts`);
url.searchParams.set("status", "published");
url.searchParams.set("page", String(page));
if (category) url.searchParams.set("category", category);
const res = await fetch(url.toString(), {
headers: { "x-api-key": apiKey }
});
const json = await res.json();
if (json.success) {
setPosts(json.data);
setPagination(json.pagination);
} else {
setError(json.message);
}
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchPosts();
}, [projectId, apiKey, page, category]);
return { posts, pagination, loading, error };
}Astro SSG Integration
Generate super-fast static blog routes dynamically during Astro build time.
---
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const res = await fetch(`http://localhost:3000/api/v1/projects/${import.meta.env.BLOG_PROJECT_ID}/posts?status=published&limit=100`, {
headers: { "x-api-key": import.meta.env.BLOG_API_KEY }
});
const json = await res.json();
return json.data.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
---
<html lang="en">
<head>
<title>{post.title}</title>
</head>
<body className="container mx-auto py-10 px-4">
<h1 className="text-4xl font-bold">{post.title}</h1>
<div className="prose mt-6" set:html={post.content} />
</body>
</html>Vanilla HTML & JavaScript Feed
Fetch and render posts directly inside static HTML websites using standard async/await.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Blog Feed</title>
</head>
<body>
<div id="blog-container">Loading articles...</div>
<script>
async function loadBlogFeed(page = 1) {
const container = document.getElementById("blog-container");
try {
const response = await fetch(`http://localhost:3000/api/v1/projects/YOUR_PROJECT_ID/posts?status=published&page=${page}&limit=10`, {
headers: { "x-api-key": "sk_live_YOUR_API_KEY" }
});
const json = await response.json();
container.innerHTML = json.data.map(post => `
<div class="post-card">
<h3>${post.title}</h3>
<p>${post.excerpt}</p>
</div>
`).join("");
} catch (err) {
container.innerHTML = "<p>Failed to load posts.</p>";
}
}
loadBlogFeed();
</script>
</body>
</html>HTML & Headless Form Submissions
Submit forms directly from pure HTML or JavaScript using the required project API key in the action URL.
// .env.local
// NEXT_PUBLIC_BLOG_CMS_URL="http://localhost:3000/api/v1"
// NEXT_PUBLIC_BLOG_FORM_ID="6a83f200f18109585cde0f7e"
// NEXT_PUBLIC_BLOG_API_KEY="sk_live_YOUR_PROJECT_API_KEY"
<!-- Pure HTML Form (using action URL with key) -->
<form action="http://localhost:3000/api/v1/f/YOUR_FORM_ID?key=sk_live_YOUR_API_KEY" method="POST">
<!-- Optional submission type tag -->
<input type="hidden" name="type" value="INQUIRY" />
<label>Full Name</label>
<input type="text" name="fullName" required />
<label>Email Address</label>
<input type="email" name="email" required />
<label>Message</label>
<textarea name="message" required></textarea>
<button type="submit">Submit Form</button>
</form>
<!-- Or via JavaScript fetch() using environment variables -->
<script>
async function submitContactForm(formData) {
const url = `${process.env.NEXT_PUBLIC_BLOG_CMS_URL}/f/${process.env.NEXT_PUBLIC_BLOG_FORM_ID}?key=${process.env.NEXT_PUBLIC_BLOG_API_KEY}`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "INQUIRY",
fullName: formData.fullName,
email: formData.email,
message: formData.message
})
});
const result = await response.json();
console.log(result);
}
</script>How External Apps Style HTML Content Without CSS Collisions
Our Headless CMS outputs standard semantic HTML5 tags (h1, p, ul, table, blockquote, pre). We deliberately avoid hardcoding restrictive inline styles so that your external website maintains 100% control over its design system and theme.
Option 1: Tailwind CSS Typography Plugin (Recommended)
If your target site uses Tailwind CSS, install @tailwindcss/typography and apply the prose utility class to your post wrapper:
<!-- React / Next.js / Astro -->
<article className="prose prose-lg dark:prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>Option 2: Copy-Paste Universal `cms-article.css` Stylesheet
For Vanilla HTML, React, Vue, or Svelte apps without Tailwind, copy this standalone CSS snippet into your stylesheet:
/* ===== cms-article.css ===== */
.cms-article { font-family: system-ui, -apple-system, sans-serif; line-height: 1.8; color: #1e293b; }
.cms-article h1 { font-size: 2.25rem; font-weight: 800; margin: 2rem 0 1rem; color: #0f172a; }
.cms-article h2 { font-size: 1.75rem; font-weight: 700; margin: 1.75rem 0 0.875rem; color: #0f172a; }
.cms-article h3 { font-size: 1.35rem; font-weight: 600; margin: 1.5rem 0 0.75rem; color: #0f172a; }
.cms-article p { margin-bottom: 1.25rem; }
.cms-article ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1.25rem; }
.cms-article ol { list-style-type: decimal; padding-left: 1.5rem; margin-bottom: 1.25rem; }
.cms-article blockquote { border-left: 4px solid #6366f1; padding-left: 1rem; font-style: italic; color: #475569; margin: 1.5rem 0; }
.cms-article pre { background-color: #090d16; color: #f8fafc; padding: 1.25rem; border-radius: 0.875rem; font-family: monospace; font-size: 0.875rem; overflow-x: auto; margin: 1.5rem 0; }
.cms-article code { background-color: #f1f5f9; color: #6366f1; padding: 0.2rem 0.4rem; border-radius: 0.375rem; font-size: 0.875em; }
.cms-article img { max-width: 100%; height: auto; border-radius: 1rem; margin: 1.5rem 0; shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
.cms-article img.float-left { float: left; margin-right: 1.5rem; margin-bottom: 1rem; width: 50%; }
.cms-article img.float-right { float: right; margin-left: 1.5rem; margin-bottom: 1rem; width: 50%; }
.cms-article table { border-collapse: collapse; width: 100%; margin: 1.5rem 0; font-size: 0.9rem; }
.cms-article th, .cms-article td { border: 1px solid #cbd5e1; padding: 0.75rem 1rem; text-align: left; }
.cms-article th { background-color: #f8fafc; font-weight: 700; }Looking for API Endpoint Schema Details?
Explore complete parameter tables, payload examples, and status codes in our REST API reference.