Dokumentasi Resmi
|tulis.org

Arsitektur Dokumentasi

Peta jalan panduan, SDK integration, dan API reference lengkap yang disiapkan secara terstruktur.

TULIS CMS/DEVELOPER HUB
/sdks-nextjs-example

API Publik Tulis — Contoh Next.js (App Router)

Integrasi Next.js App Router minimal untuk API publik Tulis headless CMS.

Peringatan keamanan: Kunci API harus tetap di server. Bacalah dari variabel lingkungan (TULIS_API_KEY) dan jangan pernah diawali dengan NEXT_PUBLIC_. Next.js hanya mengirim variabel NEXT_PUBLIC_* ke browser; yang lainnya tetap di sisi server. Server Component dan Route Handler berjalan di server, sehingga kunci tidak pernah terekspos ke klien.

Pengaturan lingkungan

Buat .env.local di direktori root proyek Anda:

TULIS_API_KEY=your_api_key_here
TULIS_WORKSPACE_ID=your_workspace_id_here
TULIS_HOST=https://your-host.example.com

⚠️ Jangan awali ini dengan NEXT_PUBLIC_. Simpan hanya di sisi server.

Halaman beranda — ambil posting di sisi server

app/page.tsx adalah Server Component yang mengambil posting di server menggunakan fetch. Kunci API tidak pernah mencapai browser.

import { TULIS_API_KEY, TULIS_WORKSPACE_ID, TULIS_HOST } from "@/lib/env";

async function getPosts() {
  const res = await fetch(
    `${TULIS_HOST}/v1/posts?page=1&per_page=10`,
    {
      headers: {
        Authorization: `Bearer ${TULIS_API_KEY}`,
        "X-Workspace-ID": TULIS_WORKSPACE_ID,
      },
      // Revalidasi setiap 60 detik (ISR). Sesuaikan sesuai kebutuhan.
      next: { revalidate: 60 },
    }
  );

  if (!res.ok) {
    throw new Error(`Gagal memuat posting: ${res.status}`);
  }

  const json = await res.json();
  return json.data;
}

export default async function HomePage() {
  const posts = await getPosts();

  return (
    <main>
      <h1>Posting Terbaru</h1>
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>
            <a href={`/posts/${post.slug}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </main>
  );
}

lib/env.ts (modul khusus server untuk memusatkan akses lingkungan):

export const TULIS_API_KEY = process.env.TULIS_API_KEY!;
export const TULIS_WORKSPACE_ID = process.env.TULIS_WORKSPACE_ID!;
export const TULIS_HOST = process.env.TULIS_HOST!;

Detail posting — rute dinamis

app/posts/[slug]/page.tsx mengambil satu posting dan menyuntikkan json_ld untuk SEO. Ini juga menggunakan generateMetadata untuk mengatur judul dan deskripsi halaman dari seo_title / seo_desc.

import { notFound } from "next/navigation";
import { TULIS_API_KEY, TULIS_WORKSPACE_ID, TULIS_HOST } from "@/lib/env";

type Params = { slug: string };

async function getPost(slug: string) {
  const res = await fetch(
    `${TULIS_HOST}/v1/posts/${encodeURIComponent(slug)}`,
    {
      headers: {
        Authorization: `Bearer ${TULIS_API_KEY}`,
        "X-Workspace-ID": TULIS_WORKSPACE_ID,
      },
      next: { revalidate: 60 },
    }
  );

  if (!res.ok) {
    return null;
  }

  const json = await res.json();
  return json; // { status, message, data: Post, json_ld: {...} }
}

export async function generateMetadata({ params }: { params: Params }) {
  const json = await getPost(params.slug);
  if (!json) return {};

  const post = json.data;
  return {
    title: post.seo_title || post.title,
    description: post.seo_desc || post.excerpt,
  };
}

export default async function PostPage({ params }: { params: Params }) {
  const json = await getPost(params.slug);
  if (!json) notFound();

  const post = json.data;
  const jsonLd = json.json_ld;

  return (
    <main>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <h1>{post.title}</h1>
      {post.feature_image ? (
        <img src={post.feature_image} alt={post.title} />
      ) : null}
      <article dangerouslySetInnerHTML={{ __html: post.content }} />
    </main>
  );
}

Bidang feature_image dan content berasal dari respons satu posting (data). Objek json_ld di-render ke dalam blok <script type="application/ld+json"> untuk data terstruktur mesin pencari. Karena getPost dipanggil baik di generateMetadata maupun di halaman, pertimbangkan untuk menyimpannya dalam cache melalui React cache() untuk menghindari permintaan duplikat.

Lini Masa Peluncuran Fitur

Fase 1: Setup Awal & KonfigurasiSELESAI

Docker Compose, pengaturan file environment, dan panduan setup lokal telah lengkap dibuat.

Fase 2: Referensi RESTful APISELESAI

Dokumentasi route endpoint publik, otentikasi JWT, dan kontrol hak akses telah selesai disusun.

Fase 3: Integrasi Frontend & Fitur LanjutSEDANG BERJALAN

Mempersiapkan boilerplate untuk Next.js client, manajemen revisi konten, dan optimasi query.