Batik Soehadi · Developer Docs

Content API v1 — Integration Guide

Panduan integrasi untuk developer website ekosistem. API memberikan metadata artikel ringan (thumbnail, judul, deskripsi, kategori, tanggal, slug, URL asli) — bukan konten penuh. Setiap kartu artikel yang Anda tampilkan link langsung ke artikel asli di batiksoehadi.com, yang tetap menjadi sumber kanonik dan tujuan traffic utama.

Base URL
https://batiksoehadi.com/api/v1
Auth
Authorization: Bearer <API_KEY> · server-to-server
Format
JSON murni — API selalu mengembalikan application/json; tidak pernah HTML siap-tampil. Website ekosistem merender sendiri kartunya dari data JSON.
Versi teks
content-api-guide.md (markdown)

1. Prinsip Utama

  1. Jangan tampilkan konten penuh di website ekosistem — hanya kartu ringkasan.
  2. Jangan hardcode URL artikel sendiri. Selalu pakai field url dari response.
  3. Panggil API dari server Anda (server-to-server). API key tidak boleh ada di browser/frontend.
  4. Cache response di server Anda minimal 10 menit. Jangan fetch per page-view.
  5. Satu key = satu website. Key dipakai bersama beberapa website = bisa kena rate limit.

2. Authentication

Kirim API key Anda di header Authorization pada setiap request. Simpan key sebagai environment variable di server (mis. BATIKSOEHADI_API_KEY) — jangan pernah commit ke Git atau tampilkan di frontend.

Authorization: Bearer <API_KEY_ANDA>

Tanpa key atau key salah → 401:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "API authentication required. Pass a valid API key via the Authorization: Bearer header."
  }
}

3. Endpoint

GET https://batiksoehadi.com/api/v1/articles — daftar kartu artikel tanpa body konten (endpoint utama).

GET https://batiksoehadi.com/api/v1/articles/{slug} — detail satu artikel, menambahkan body dan english_url. Untuk kebutuhan khusus saja.

GET https://batiksoehadi.com/api/v1/categories — daftar kategori + jumlah artikel.

GET https://batiksoehadi.com/api/v1/tags — daftar tag + jumlah artikel.

4. Query Parameters

ParamTipeDefaultKeterangan
pageinteger ≥ 11Nomor halaman (offset pagination)
per_pageinteger 1–10020Item per halaman (max 100)
cursorstring opaque—Keyset cursor dari meta.next_cursor — lihat §7. Tidak boleh digabung dengan page.
categorystring—Slug kategori, mis. motif, seragam (lihat /categories)
tagstring—Filter tag (substring match)
featuredtrue / false—Hanya artikel unggulan (kurasi editorial)
searchstring 2–80—Cari di judul + deskripsi + focus keyword
sortlatest / oldestlatestUrut berdasarkan tanggal publikasi

Parameter invalid mengembalikan 400 dengan pesan yang menjelaskan param mana yang salah.

5. Response

{
  "data": [
    {
      "id": "866976b5-4f9e-4a2e-9e46-1b8d5b6f8a01",
      "title": "Mengenal Filosofi Batik Parang",
      "slug": "motif-batik-parang",
      "description": "Mengenal sejarah, filosofi, dan makna dari motif Batik Parang.",
      "thumbnail": "https://s3.nevaobjects.id/batiksoehadi-bucket/articles/batik-fabric/batik-parang-pattern.jpg",
      "category": { "name": "motif", "slug": "motif" },
      "tags": ["parang batik", "motif klasik"],
      "featured": false,
      "published_at": "2026-08-13T00:00:00.000Z",
      "updated_at": "2026-08-13T00:00:00.000Z",
      "url": "https://batiksoehadi.com/artikel/motif-batik-parang/"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 2,
    "total": 432,
    "total_pages": 216,
    "next_cursor": "eyJ0IjoiMjAyNi0wOC0xM1QwMDowMDowMC4wMDBaIiwiaWQiOiI4NjY5NzYiLCJkIjoibGF0ZXN0In0"
  }
}
FieldArti
urlURL artikel asli di batiksoehadi.com. Selalu pakai ini untuk link "Baca Selengkapnya". Selalu absolut dan canonical.
thumbnailURL gambar absolut. Boleh langsung dipakai sebagai <img src>. Bisa null.
descriptionExcerpt singkat untuk kartu. Bisa null.
categoryObjek {name, slug} atau null.
published_at / updated_atISO 8601 UTC.
meta.next_cursorAda hanya saat masih ada data berikutnya. Hilang = data habis.

6. Pagination Offset

Untuk browsing / UI dengan nomor halaman. Pakai meta.total_pages untuk render tombol halaman.

GET https://batiksoehadi.com/api/v1/articles?page=1&per_page=12
GET https://batiksoehadi.com/api/v1/articles?page=2&per_page=12

7. Pagination Cursor

cursor adalah keyset pagination: stabil saat artikel baru terbit di tengah walk, tanpa duplikat atau baris terlewat. Cocok untuk sinkronisasi awal atau job berkala yang menarik seluruh dataset.

GET https://batiksoehadi.com/api/v1/articles?per_page=100              → halaman 1 + meta.next_cursor
GET https://batiksoehadi.com/api/v1/articles?per_page=100&cursor=...   → halaman berikutnya

Walk lengkap (Node.js):

let url = "https://batiksoehadi.com/api/v1/articles?per_page=100";
while (url) {
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.BATIKSOEHADI_API_KEY}` },
  });
  if (res.status === 429) {
    await new Promise((r) => setTimeout(r, 60_000)); // tunggu window reset
    continue;
  }
  const { data, meta } = await res.json();
  for (const card of data) {
    // simpan/proses kartu...
  }
  url = meta.next_cursor
    ? `https://batiksoehadi.com/api/v1/articles?per_page=100&cursor=${encodeURIComponent(meta.next_cursor)}`
    : null;
}
  • cursor dan page mutually exclusive (gabungan = 400).
  • Cursor invalid/garbled → 400. Selalu pakai nilai next_cursor apa adanya; jangan karang sendiri.
  • Pertahankan sort yang sama selama satu walk (cursor terikat ke arah sort).

8. Rate Limit

Limit60 request / menit per API key (sliding window)
HeaderX-RateLimit-Limit, X-RateLimit-Remaining
Saat kena limitHTTP 429 {"error":{"code":"RATE_LIMITED",...}} + header Retry-After (detik)

Wajib cache di sisi Anda. Dengan cache 10 menit, satu website butuh ≤ 6 request/menit untuk melayani semua visitor — jauh di bawah limit.

9. Caching & Freshness

  • Response bersifat per-identitas (auth per key), sehingga tidak di-cache CDN — cache di server Anda per key Anda sendiri.
  • Data sisi kami di-cache 10 menit (in-process), jadi artikel baru muncul dalam ≤ ~10 menit.
  • Cache 10–60 menit di sisi Anda memberikan freshness wajar tanpa membakar rate limit.

10. Menampilkan Kartu Artikel

<a href="{card.url}" rel="noopener">
  <img src="{card.thumbnail}" alt="{card.title}" loading="lazy" />
  <h3>{card.title}</h3>
  <p>{card.description}</p>
  <span>Baca Selengkapnya di Batik Soehadi →</span>
</a>
  • Link = card.url — jangan bentuk URL sendiri, jangan proxy/relink.
  • Thumbnail langsung dari card.thumbnail (sudah absolut).
  • Next.js dengan next/image: daftarkan remote pattern { protocol: "https", hostname: "batiksoehadi.com" } di images.remotePatterns — atau pakai <img> biasa / URL optimizer kami (/_next/image?url=<encoded>&w=640&q=75).

11. Contoh Lengkap — Next.js (App Router)

// app/artikel-terkini/page.tsx (di website ekosistem)
export const revalidate = 600; // cache 10 menit

async function getArticles() {
  const res = await fetch(
    "https://batiksoehadi.com/api/v1/articles?page=1&per_page=12",
    {
      headers: { Authorization: `Bearer ${process.env.BATIKSOEHADI_API_KEY}` },
      next: { revalidate: 600 },
    }
  );
  if (!res.ok) return [];
  const { data } = await res.json();
  return data;
}

export default async function Page() {
  const articles = await getArticles();
  return (
    <section>
      {articles.map((card) => (
        <a key={card.id} href={card.url}>
          {card.thumbnail && <img src={card.thumbnail} alt={card.title} loading="lazy" />}
          <h3>{card.title}</h3>
          <p>{card.description}</p>
          <span>Baca Selengkapnya →</span>
        </a>
      ))}
    </section>
  );
}

12. Error Reference

Semua error punya bentuk konsisten: { "error": { "code": "<KODE>", "message": "..." } }

HTTPCodePenyebabYang harus dilakukan
400INVALID_PARAMETERParam query tidak valid (mis. per_page=101, cursor rusak)Perbaiki param sesuai pesan error
401UNAUTHORIZEDKey hilang/salahCek header Authorization: Bearer
404NOT_FOUNDSlug detail tidak adaSlug salah / artikel dihapus
429RATE_LIMITED>60 req/menitHormati Retry-After, tambah caching
500INTERNAL_ERRORKesalahan sisi kamiRetry dengan backoff; kontak tim kalau menetap

13. Checklist Go-Live

  • API key disimpan sebagai env var di server, tidak pernah di frontend/Git
  • Semua request server-to-server (tidak ada panggilan dari browser)
  • Cache ≥ 10 menit di server Anda
  • Semua link artikel memakai card.url apa adanya
  • Handling 429 dengan Retry-After
  • Handling 401 diperlakukan sebagai config error (key salah/expired) — alert ke tim
  • Tidak ada konten penuh ditampilkan ulang — hanya kartu + link ke batiksoehadi.com

14. FAQ

Bisakah saya ambil full artikel dan tampilkan di website saya?

Tidak. API list sengaja tidak menyediakan body, dan menampilkan ulang konten penuh akan menduplikasi konten SEO yang merugikan kedua website. Tampilkan kartu, link ke sumber.

Berapa lama artikel baru muncul di API?

≤ ~10 menit setelah terbit (karena data cache di sisi kami).

Apakah bisa request tanpa API key?

Tidak — production menegakkan auth. Hubungi tim Batik Soehadi untuk key per website Anda.

Bagaimana kalau saya butuh lebih dari 100 item per halaman?

Tidak bisa via per_page (max 100) — gunakan cursor pagination (§7) untuk menarik dataset penuh secara bertahap.

Apakah URL gambar bisa berubah?

Path gambar bisa berubah saat artikel diedit; selalu render dari thumbnail terbaru yang Anda fetch, jangan simpan URL gambar secara permanen tanpa refresh berkala.

15. Pola SEO-Friendly Halaman Detail Ekosistem

Bagi website ekosistem yang ingin menyediakan rute halaman sendiri (misalnya /artikel/[slug]/), gunakan pola berikut agar halaman terindeks Google dengan judul dan ringkasan yang kaya keyword, namun aman 100% dari penalty duplicate content dan mengalirkan traffic ke batiksoehadi.com:

// app/artikel/[slug]/page.tsx (di website ekosistem)
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const article = await fetchArticleBySlug(slug);
  if (!article) return {};

  return {
    title: `${article.title} — ${siteConfig.name}`,
    description: article.description,
    alternates: {
      canonical: article.url, // Mengarahkan crawler ke https://batiksoehadi.com/artikel/{slug}/
    },
  };
}
  • Tag Kanonik Wajib: Selalu set alternates.canonical = article.url agar Googlebot mengakui Batik Soehadi sebagai pemilik sah artikel.
  • Ringkasan Terbaca: Render <h1>{article.title}</h1>, cover image, dan teks ringkasan <p>{article.description}</p> di halaman agar bot Google mengindeks kata kuncinya.
  • CTA Baca Lengkap: Sediakan tombol menonjol "Baca Selengkapnya di Batik Soehadi" dengan tautan ke article.url (membuka website utama).