Get started
Build your blog frontend with the framework you already use.
ContioReach manages your blog content and exposes published posts through a simple Content API. Your frontend requests structured content from ContioReach and decides exactly how that content should be rendered.
The fastest way in is one command. It creates a complete blog for Next.js, Nuxt, Astro, SvelteKit, Remix or React that runs on demo content straight away, with no account needed:
npx create-contioreach-blog my-blogOr pick the path that fits what you are doing:
The rest of this page is the API itself: endpoints, parameters, and the response shape every guide relies on.
If your application can make an HTTP request and work with JSON, it can use ContioReach.
With the ContioReach Content API, you can:
All Content API requests use:
https://cms-api.contioreach.comEvery request is authenticated with your workspace API key, sent as an X-API-Key header:
Content-Type: application/json
X-API-Key: YOUR_API_KEYThe Content API exposes four read endpoints.
GET https://cms-api.contioreach.com/v1/blogsGET https://cms-api.contioreach.com/v1/categoriesGET https://cms-api.contioreach.com/v1/authorsGET https://cms-api.contioreach.com/v1/tagsUse minimal=true wherever you are rendering a list. It returns the same structured metadata without the article content, which keeps listing responses small.
Every endpoint returns the same envelope: a success flag, data, and — for lists — pagination meta. A lookup by slug or id returns no meta.
{
"success": true,
"data": [
{
"id": "18c366b1-cd1f-45c3-b7cf-6ebb44172177",
"title": "How to turn off AI Overviews in Google Search?",
"slug": "turn-off-ai-overviews-google-search",
"excerpt": "Google won't let you disable AI Overviews outright…",
"description": "Google won't let you disable AI Overviews outright…",
"metaTitle": "How to Turn Off AI Overviews in Google Search (2026)",
"coverImage": "https://contiocdn.com/…/cover.webp",
"coverImageAlt": "Google search results page with an AI Overview panel",
"publishedAt": "2026-08-21T06:23:29.009Z",
"createdAt": "2026-08-21T06:19:01.152Z",
"updatedAt": "2026-08-21T06:26:48.423Z",
"primaryKeyword": "turn off AI Overviews in Google",
"secondaryKeywords": ["disable AI Overviews", "Google AI Overview settings"],
"readingTime": null,
"authors": [
{
"id": "ffb69c16-be70-47f1-b206-d7464c0bdbd5",
"name": "Team Contioreach",
"slug": "team-contioreach",
"bio": "Team Contioreach publishes expert content on SEO…",
"image": "https://contiocdn.com/…/author.webp",
"twitter": null,
"website": null
}
],
"tags": [],
"categories": [
{
"id": "8766bbbf-5abb-4e5d-9a76-68ce7f868669",
"name": "Headless CMS",
"slug": "headless-cms"
}
]
}
],
"meta": {
"page": 1,
"limit": 1,
"total": 64,
"totalPages": 64,
"hasNextPage": true,
"hasPrevPage": false
}
}content is left out by default. Pass minimal=false to receive the full article body as HTML, along with a calculated readingTime.
Check success before reading data. A response can arrive with HTTP 200 and success: false when the request was understood but could not be fulfilled.
A blog built on ContioReach makes the same small set of calls.
Every call shares the same base URL, headers, and error handling, so it is worth writing once:
const BASE_URL = "https://cms-api.contioreach.com";
async function apiRequest(endpoint, tags = []) {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.CMS_API_KEY
},
next: { revalidate: 3600, tags }
});
if (!response.ok) {
throw new Error(
`API request failed: ${response.status}`
);
}
const data = await response.json();
if (!data.success) {
throw new Error(
data.error?.message || "API request failed"
);
}
return data;
}Request a page of posts with minimal=true, then read data for the posts and meta for pagination:
export async function getBlogs({
page = 1,
limit = 12,
category,
tags,
search
} = {}) {
const params = new URLSearchParams({
page,
limit,
minimal: "true"
});
if (category) params.append("category", category);
if (tags) params.append("tags", tags);
if (search) params.append("search", search);
return apiRequest(`/v1/blogs?${params}`, ["blogs"]);
}Query by slug with minimal=false to receive the full article body. A slug lookup returns data as a single object, not an array, and responds 404 when the post does not exist:
export async function getBlogBySlug(slug) {
const params = new URLSearchParams({
slug,
minimal: "false"
});
const result = await apiRequest(
`/v1/blogs?${params}`,
["blogs", `blog-${slug}`]
);
return result.data;
}Categories are just a filter on the posts endpoint. Fetch the category itself for its name and description, then request its posts:
export async function getCategory(slug) {
const result = await apiRequest(
`/v1/categories?slug=${slug}`,
["categories"]
);
return result.data[0] || null;
}
export async function getBlogsByCategory(slug, params = {}) {
return getBlogs({ ...params, category: slug });
}To pre-render every article route, walk every page of posts. limit is capped at 100, so a single request with a higher limit silently stops at 100:
export async function generateStaticParams() {
const slugs = [];
let page = 1;
let hasNextPage = true;
while (hasNextPage) {
const result = await getBlogs({ page, limit: 100 });
slugs.push(...result.data.map((blog) => ({ slug: blog.slug })));
hasNextPage = result.meta.hasNextPage;
page += 1;
}
return slugs;
}Cache tags pair with webhooks. Tagging requests "blogs" and "blog-<slug>" lets a publishing event revalidate exactly the affected content instead of the whole site.
Was this page helpful?