چطور کار میکند
Contivo طبق زمانبندی خودتان مینویسد و منتشر میکند. وقتی پستی منتشر میشود، یک slug دائمی میگیرد و منتشرشده علامت میخورد؛ کار Contivo همینجا تمام است. سایت شما هر وقت خواست، پستها را میخواهد.
جهت این رابطه مهم است: Contivo هیچوقت دسترسی نوشتن به کد شما، خط استقرار یا CMS شما ندارد. اگر Contivo را خاموش کنید، سایت شما همان چیزی را که آخرین بار گرفته است نشان میدهد.
احراز هویت
هر درخواست یک کلید سایت را به شکل bearer token همراه دارد. کلید را از اتصالها ← وبسایتها بسازید. کلید فقط یکبار و هنگام ساختهشدن نمایش داده میشود و بعد از آن بازیابی نمیشود؛ اگر گمش کردید، یک اتصال سایت تازه بسازید.
Authorization: Bearer ctv_your_site_keyکلید را سمت سرور نگه دارید. این کلید میتواند هر چیزی را که این فضای کاری تا امروز منتشر کرده بخواند. نقطههای پایانی هدرهای CORS سختگیرانهای نمیفرستند، پس مرورگر هم میتواند صدایشان بزند؛ اما این کار کلید شما را به دست هر بازدیدکننده میرساند. درخواست را از یک کامپوننت سروری، یک route handler، هنگام بیلد یا از بکاند خودتان بفرستید.
فهرست پستها
GET https://www.contivo.app/api/v1/postsپستهای منتشرشدهٔ فضای کاری صاحب کلید را برمیگرداند، از تازهترین.
| پارامتر | نوع | معنی |
|---|---|---|
| limit | 1–100, default 20 | چند پست برگردانده شود. مقدارهای بیرون از بازه به همان بازه محدود میشوند، نه اینکه رد شوند. |
| cursor | string | مقدار nextCursor از پاسخ قبلی. برای صفحهٔ اول آن را نفرستید. |
| channel | default "blog" | کدام کانال برگردانده شود. برای اینکه پستهای شبکههای اجتماعی هم بیایند، مقدار all را بفرستید؛ معمولاً اینها را روی یک وبسایت نمیخواهید. |
{
"posts": [
{
"id": "cmtj8m9cw0004669budmzpagu",
"slug": "eliminating-the-triage-bottleneck",
"title": "Eliminating the Triage Bottleneck",
"content": "Manual bug triage remains a major source of friction…",
"excerpt": "Manual bug triage remains a major source of friction for engineering teams…",
"channel": "blog",
"publishedAt": "2026-09-02T02:00:00.000Z",
"updatedAt": "2026-09-02T02:00:04.118Z"
}
],
"nextCursor": "cmtj8m9cw0004669budmzpagu"
}وقتی صفحهٔ دیگری نمانده باشد، nextCursor برابر null است. تنها نشانهٔ قابلاتکا برای پایان فهرست همین است؛ با رسیدن به یک صفحهٔ کوتاه متوقف نشوید.
گرفتن یک پست
GET https://www.contivo.app/api/v1/posts/{slug}{ "post": { … } } را برمیگرداند، با همان شیئی که بالا آمد. مسیر صفحههایتان را روی slug بسازید، نه روی id: مقدار slug یکبار هنگام انتشار تعیین میشود و دیگر عوض نمیشود، و همین است که نشانیهای شما را پایدار نگه میدارد.
شیء پست
| فیلد | نوع | توضیح |
|---|---|---|
| id | string | شناسهٔ داخلی و پایدار. برای key در React خوب است، برای نشانیها نه. |
| slug | string | برای نشانی امن است، در هر فضای کاری یکتاست و از لحظهٔ انتشار دائمی میماند. |
| title | string | موضوع پست. |
| content | string | متن کامل، به شکل Markdown. با همان ابزاری که الان دارید رندرش کنید. |
| excerpt | string | حدود ۲۰۰ نویسهٔ اول، بدون نشانههای Markdown. برای کارتها، فهرستها و توضیحات متا. |
| channel | string | معمولاً مقدار blog است. مقدار دیگری فقط وقتی میآید که channel=all خواسته باشید. |
| publishedAt | string | null | ISO ۸۶۰۱ به وقت UTC. |
| updatedAt | string | ISO ۸۶۰۱ به وقت UTC. برای کلید کش، نقشهٔ سایت و lastmod به کار میآید. |
خطاها
| وضعیت | خطای بدنه | یعنی چه |
|---|---|---|
| 401 | unauthorized | کلید نیست، بدشکل است، ناشناس است یا باطل شده؛ یا اتصال سایت فعال نیست. هر چهار حالت عمداً یک پاسخ میگیرند تا کسی نتواند از این نقطهٔ پایانی بفهمد چه کلیدهایی وجود دارند. |
| 404 | not_found | فقط برای تکپست. در این فضای کاری پست منتشرشدهای با آن slug وجود ندارد. مقدار slug متعلق به مشتری دیگر هم ۴۰۴ میگیرد، نه ۴۰۳. |
آرایهٔ خالی posts خطا نیست. یعنی کلید معتبر است و هنوز چیزی روی آن کانال منتشر نشده؛ تا اتوپایلت چیزی منتشر نکند، اینجا چیزی دیده نمیشود.
کش و تازگی داده
پاسخها با هدر Cache-Control: no-store فرستاده میشوند، پس API هیچوقت از CDN یا مرورگر نمیخواهد آنها را نگه دارد. تصمیم دربارهٔ کش کاملاً با شماست و همین درست است: فریمورک شما ترافیک شما را میشناسد و میداند تا کجا میشود داده را قدیمی نگه داشت.
const res = await fetch("https://www.contivo.app/api/v1/posts", {
headers: { Authorization: `Bearer ${process.env.CONTIVO_SITE_KEY}` },
next: { revalidate: 300 },
});پنج دقیقه پیشفرض معقولی است. اگر بهروزرسانی آنی میخواهید، بهجای سرکشی تندتر، یک بازهٔ revalidate طولانی را با وبهوک پایین ترکیب کنید.
وبهوک revalidate
اختیاری است. روی اتصال سایت یک نشانی revalidate بگذارید تا Contivo درست بعد از هر انتشار صدایش بزند و کش شما بهجای بازهٔ بعدی، همان لحظه پاک شود.
POST <your revalidate URL>
Content-Type: application/json
Authorization: Bearer <your revalidate secret> // only if you set one
{ "slug": "eliminating-the-triage-bottleneck", "event": "post.published" }import { revalidatePath } from "next/cache";
export async function POST(req: Request) {
if (req.headers.get("authorization") !== `Bearer ${process.env.CONTIVO_REVALIDATE_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
const { slug } = await req.json();
revalidatePath("/blog");
revalidatePath(`/blog/${slug}`);
return Response.json({ revalidated: true });
}این فراخوانی بعد از ۸ ثانیه منقضی میشود و وضعیتش روی اتصال سایت ثبت میشود. شکستش ثبت میشود و هیچوقت جلوی انتشار را نمیگیرد؛ پست از قبل منتشر شده و سایت شما در بازخوانی بعدیاش آن را برمیدارد.
نمونههای آماده
آزمودن کلید از ترمینال
curl -sS -H "Authorization: Bearer $CONTIVO_SITE_KEY" \
"https://www.contivo.app/api/v1/posts?limit=1" | jqگرفتن همهٔ پستها، صفحهبهصفحه
async function allPosts(key) {
const out = [];
let cursor = null;
do {
const url = new URL("https://www.contivo.app/api/v1/posts");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
if (!res.ok) throw new Error(`Contivo returned ${res.status}`);
const page = await res.json();
out.push(...page.posts);
cursor = page.nextCursor; // null ends the loop
} while (cursor);
return out;
}تولید سایت ایستا
export async function generateStaticParams() {
const res = await fetch("https://www.contivo.app/api/v1/posts?limit=100", {
headers: { Authorization: `Bearer ${process.env.CONTIVO_SITE_KEY}` },
});
const { posts } = await res.json();
return posts.map((p) => ({ slug: p.slug }));
}Python
import os, requests
r = requests.get(
"https://www.contivo.app/api/v1/posts",
headers={"Authorization": f"Bearer {os.environ['CONTIVO_SITE_KEY']}"},
params={"limit": 20},
timeout=10,
)
r.raise_for_status()
for post in r.json()["posts"]:
print(post["slug"], "-", post["title"])PHP / WordPress
$response = wp_remote_get(
'https://www.contivo.app/api/v1/posts?limit=20',
['headers' => ['Authorization' => 'Bearer ' . getenv('CONTIVO_SITE_KEY')]]
);
$posts = json_decode(wp_remote_retrieve_body($response), true)['posts'];محدودیتها و تضمینها
| ویژگی | مقدار | توضیح |
|---|---|---|
| متدها | GET, OPTIONS | فقط خواندنی. از راه این API نمیشود محتوا نوشت. |
| اندازهٔ صفحه | حداکثر ۱۰۰ | مقدارهای بزرگتر محدود میشوند، نه اینکه رد شوند. |
| دامنه | یک فضای کاری | هر کلید فقط همان فضای کاریای را میخواند که در آن ساخته شده است. |
| Slugs | دائمی | هنگام انتشار تعیین میشوند و دیگر بازنویسی نمیشوند. استفاده از آنها در نشانیها و نقشهٔ سایت امن است. |
| باطلکردن | بیدرنگ | با حذف یا غیرفعالکردن یک اتصال سایت، کلیدش در درخواست بعدی ۴۰۱ میگیرد. |
فعلاً محدودیت نرخ اعلامشدهای وجود ندارد. با این حال پاسخها را کش کنید؛ سایتی که در هر بازدید صفحه یک درخواست میفرستد، فارغ از ما هم شکننده است.
چیزی اینجا اشتباه است یا جا افتاده؟ این صفحه از همان کدی ساخته میشود که API را اجرا میکند، پس هر ناهماهنگی یک باگ است و ارزش گزارش دادن دارد. برای ذخیرهٔ این صفحه به شکل PDF آن را چاپ کنید.