# Batik Soehadi — Content API v1: Integration Guide

> **Panduan integrasi untuk developer website ekosistem.**
> API ini 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`
- **Format:** **JSON murni** — semua endpoint selalu mengembalikan `application/json`. API **tidak pernah** mengirim HTML siap-tampil; website ekosistem yang merender sendiri kartunya dari data JSON. (Halaman `/content-api-guide/` dan file `.md` ini hanyalah dokumentasi — bukan bagian dari API.)
- **Auth:** API key per website (Bearer token, server-to-server)
- **Kontak untuk API key:** tim Batik Soehadi

---

## 1. Prinsip Utama (baca dulu, 30 detik)

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:

```
Authorization: Bearer <API_KEY_ANDA>
```

Simpan key sebagai **environment variable** di server Anda (contoh: `BATIKSOEHADI_API_KEY`), jangan pernah commit ke Git atau tampilkan di frontend.

### Contoh response error auth

```json
// 401 — tanpa key atau key salah
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "API authentication required. Pass a valid API key via the Authorization: Bearer header."
  }
}
```

---

## 3. Endpoint

### `GET /articles` — daftar artikel (endpoint utama)

Mengembalikan kartu artikel **tanpa body konten**.

### `GET /articles/{slug}` — detail satu artikel

Menambahkan field `body` (konten penuh) dan `english_url`. **Untuk kebutuhan khusus saja** — integrasi standar cukup memakai list + URL kanonik.

### `GET /categories` — daftar kategori + jumlah artikel

```json
{
  "data": [
    { "slug": "seragam", "name": "seragam", "article_count": 48 },
    { "slug": "motif", "name": "motif", "article_count": 30 }
  ]
}
```

### `GET /tags` — daftar tag + jumlah artikel

Format sama dengan categories.

---

## 4. Query Parameters (`GET /articles`)

| Param | Tipe | Default | Keterangan |
|---|---|---|---|
| `page` | integer ≥ 1 | `1` | Nomor halaman (offset pagination) |
| `per_page` | integer 1–100 | `20` | Item per halaman (**max 100**) |
| `cursor` | string opaque | — | Keyset cursor dari `meta.next_cursor` (lihat §7). Tidak boleh digabung dengan `page`. |
| `category` | string | — | Slug kategori, mis. `motif`, `seragam` (lihat `/categories`) |
| `tag` | string | — | Filter tag (substring match) |
| `featured` | `true`/`false` | — | Hanya artikel unggulan (kurasi editorial) |
| `search` | string 2–80 | — | Cari di judul + deskripsi + focus keyword |
| `sort` | `latest` / `oldest` | `latest` | Urut berdasarkan tanggal publikasi |

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

---

## 5. Response

### Contoh `GET /articles?page=1&per_page=2`

```json
{
  "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"
  }
}
```

### Arti field penting

| Field | Arti |
|---|---|
| `url` | **URL artikel asli di batiksoehadi.com.** Selalu pakai ini untuk link "Baca Selengkapnya". Selalu absolut, selalu canonical. |
| `thumbnail` | URL gambar absolut di batiksoehadi.com. Boleh langsung dipakai sebagai `<img src>`. Bisa `null`. |
| `description` | Excerpt singkat untuk kartu. Bisa `null`. |
| `category` | Objek `{name, slug}` atau `null`. |
| `published_at` / `updated_at` | ISO 8601 UTC. |
| `meta.next_cursor` | Ada hanya saat masih ada data berikutnya. Hilang (absent) = data habis. |

---

## 6. Pagination Offset (untuk browsing / UI halaman)

```
GET /articles?page=1&per_page=12
GET /articles?page=2&per_page=12
```

Pakai `meta.total_pages` untuk render tombol halaman. Cocok untuk grid artikel dengan nomor halaman.

---

## 7. Pagination Cursor (untuk sync besar / download seluruh dataset)

`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 /articles?per_page=100              → halaman 1 + meta.next_cursor
GET /articles?per_page=100&cursor=...   → halaman berikutnya
```

### Walk lengkap (Node.js)

```javascript
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;
}
```

Aturan cursor:
- `cursor` dan `page` **mutually exclusive** (gabungan = 400).
- Cursor invalid/garbled → 400 (bukan crash). 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

| Aturan | Nilai |
|---|---|
| Limit | **60 request / menit** per API key (sliding window) |
| Header | `X-RateLimit-Limit`, `X-RateLimit-Remaining` |
| Saat kena limit | **HTTP 429** `{"error":{"code":"RATE_LIMITED",...}}` + header `Retry-After` (detik) |

**Wajib cache di sisi Anda.** Rekomendasi:
- UI: cache 10–60 menit per query (artikel tidak berubah per menit).
- Sync: jalankan walk berkala (harian cukup), bukan per page-view.

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

---

## 9. Caching & Freshness

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

---

## 10. Menampilkan Kartu Artikel

Pola minimal yang benar:

```html
<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>
```

Aturan rendering:
- **Link = `card.url`** — jangan bentuk URL sendiri, jangan proxy/relink.
- **Thumbnail langsung dari `card.thumbnail`** (sudah absolut).
- Next.js: jika pakai `next/image` dengan domain kita, daftarkan remote pattern:

```javascript
// next.config.ts (di website ekosistem)
images: {
  remotePatterns: [{ protocol: "https", hostname: "batiksoehadi.com" }],
}
```

  atau pakai `<img>` biasa / URL optimizer kami (`https://batiksoehadi.com/_next/image?url=<encoded>&w=640&q=75`).

---

## 11. Contoh Lengkap — Next.js (App Router, server-side)

```javascript
// 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>
  );
}
```

### Contoh fetch biasa (Node.js / framework lain)

```javascript
const res = await fetch(
  "https://batiksoehadi.com/api/v1/articles?page=1&per_page=10",
  { headers: { Authorization: `Bearer ${process.env.BATIKSOEHADI_API_KEY}` } }
);
const result = await res.json();
// result.data → array kartu; result.meta → pagination
```

---

## 12. Error Reference

Semua error punya bentuk konsisten:

```json
{ "error": { "code": "<KODE>", "message": "<penjelasan>" } }
```

| HTTP | Code | Penyebab | Yang harus dilakukan |
|---|---|---|---|
| 400 | `INVALID_PARAMETER` | Param query tidak valid (mis. `per_page=101`, cursor rusak) | Perbaiki param sesuai pesan error |
| 401 | `UNAUTHORIZED` | Key hilang/salah | Cek header `Authorization: Bearer` |
| 404 | `NOT_FOUND` | Slug detail tidak ada | Slug salah / artikel dihapus |
| 429 | `RATE_LIMITED` | >60 req/menit | Hormati `Retry-After`, tambah caching |
| 500 | `INTERNAL_ERROR` | Kesalahan sisi kami | Retry 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

**Q: 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.

**Q: Berapa lama artikel baru muncul di API?**
≤ ~10 menit setelah terbit (karena data cache di sisi kami).

**Q: Apakah bisa request tanpa API key?**
Tidak — production menegakkan auth. Hubungi tim Batik Soehadi untuk key per website Anda.

**Q: 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.

**Q: 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 (`/artikel/[slug]`)

Bagi website ekosistem yang ingin menyediakan rute halaman sendiri (misalnya `/artikel/[slug]/` di `batik-seo-ecosystem`), 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`:

### 1. Tag Kanonik Merujuk ke Sumber Utama
Di fungsi `generateMetadata`:
```tsx
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}/
    },
  };
}
```

### 2. Tampilkan Ringkasan & Gambar untuk Googlebot & Pengunjung
Di body halaman:
* Render tag `<h1>{article.title}</h1>`
* Tampilkan cover `article.thumbnail`
* Tampilkan teks deskripsi/ringkasan `<p>{article.description}</p>`

### 3. Sediakan Box CTA "Baca Selengkapnya di Batik Soehadi"
Arahkan pengunjung yang ingin membaca versi lengkap, foto proses produksi, maupun melihat katalog produk ke artikel sumber:
```tsx
<div className="cta-box">
  <h3>Baca Artikel Lengkap di Batik Soehadi</h3>
  <a href={article.url} target="_blank" rel="noopener noreferrer" className="btn-primary">
    Baca Selengkapnya ↗
  </a>
</div>
```

---

*Batik Soehadi — Premium Indonesian Batik Manufacturer & Textile Supplier.*
*API ini didesain untuk content distribution, bukan content duplication.*

