‏API محتوا

سایت شما پست‌های منتشرشده‌اش را با یک درخواست GET احراز هویت‌شده از Contivo می‌خواند. چیزی داخل کد شما نوشته نمی‌شود، افزونه‌ای در کار نیست، و با هر تکنولوژی‌ای که بتواند یک درخواست HTTP بفرستد کار می‌کند.

نشانی پایه https://www.contivo.app · فقط خواندنی · JSON

چطور کار می‌کند

‏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

پست‌های منتشرشدهٔ فضای کاری صاحب کلید را برمی‌گرداند، از تازه‌ترین.

پارامترنوعمعنی
limit1–100, default 20چند پست برگردانده شود. مقدارهای بیرون از بازه به همان بازه محدود می‌شوند، نه اینکه رد شوند.
cursorstringمقدار nextCursor از پاسخ قبلی. برای صفحهٔ اول آن را نفرستید.
channeldefault "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 یک‌بار هنگام انتشار تعیین می‌شود و دیگر عوض نمی‌شود، و همین است که نشانی‌های شما را پایدار نگه می‌دارد.

شیء پست

فیلدنوعتوضیح
idstringشناسهٔ داخلی و پایدار. برای key در React خوب است، برای نشانی‌ها نه.
slugstringبرای نشانی امن است، در هر فضای کاری یکتاست و از لحظهٔ انتشار دائمی می‌ماند.
titlestringموضوع پست.
contentstringمتن کامل، به شکل Markdown. با همان ابزاری که الان دارید رندرش کنید.
excerptstringحدود ۲۰۰ نویسهٔ اول، بدون نشانه‌های Markdown. برای کارت‌ها، فهرست‌ها و توضیحات متا.
channelstringمعمولاً مقدار blog است. مقدار دیگری فقط وقتی می‌آید که channel=all خواسته باشید.
publishedAtstring | null‏ISO ۸۶۰۱ به وقت UTC.
updatedAtstring‏ISO ۸۶۰۱ به وقت UTC. برای کلید کش، نقشهٔ سایت و lastmod به کار می‌آید.

خطاها

وضعیتخطای بدنهیعنی چه
401unauthorizedکلید نیست، بدشکل است، ناشناس است یا باطل شده؛ یا اتصال سایت فعال نیست. هر چهار حالت عمداً یک پاسخ می‌گیرند تا کسی نتواند از این نقطهٔ پایانی بفهمد چه کلیدهایی وجود دارند.
404not_foundفقط برای تک‌پست. در این فضای کاری پست منتشرشده‌ای با آن slug وجود ندارد. مقدار slug متعلق به مشتری دیگر هم ۴۰۴ می‌گیرد، نه ۴۰۳.

آرایهٔ خالی posts خطا نیست. یعنی کلید معتبر است و هنوز چیزی روی آن کانال منتشر نشده؛ تا اتوپایلت چیزی منتشر نکند، اینجا چیزی دیده نمی‌شود.

کش و تازگی داده

پاسخ‌ها با هدر Cache-Control: no-store فرستاده می‌شوند، پس API هیچ‌وقت از CDN یا مرورگر نمی‌خواهد آن‌ها را نگه دارد. تصمیم دربارهٔ کش کاملاً با شماست و همین درست است: فریم‌ورک شما ترافیک شما را می‌شناسد و می‌داند تا کجا می‌شود داده را قدیمی نگه داشت.

‏Next.js؛ بازخوانی هر پنج دقیقه
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 درست بعد از هر انتشار صدایش بزند و کش شما به‌جای بازهٔ بعدی، همان لحظه پاک شود.

چیزی که 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" }
‏Next.js؛ app/api/revalidate/route.ts
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;
}

تولید سایت ایستا

‏Next.js؛ generateStaticParams
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 آن را چاپ کنید.

‏API محتوا · Contivo برای توسعه‌دهندگان