Guide

Media

Media store bawaan berbasis git, dan backend media handler buat external storage.

Default: berbasis git

Tanpa konfigurasi tambahan, file yang diupload lewat field image langsung di-commit ke repo, di bawah media.tina di tina/config.ts:

media: {
  tina: {
    mediaRoot: 'uploads',
    publicFolder: 'public',
  },
}

Opsi paling simpel, nggak butuh infrastruktur tambahan, tapi setiap upload jadi commit Git.

Backend media handler

Buat external storage, nuxt-tina nyambungin backend media handler satu-satu. Cloudinary yang udah beneran diimplementasikan sekarang. Yang lain masih di roadmap, belum tersedia:

BackendStatus
CloudinaryTersedia
AWS S3 BucketSegera hadir
DigitalOcean SpacesSegera hadir

media.provider sekarang cuma nerima 'tina' \| 'cloudinary': coba isi yang lain langsung jadi type error, bukan opsi yang diam-diam diabaikan.

Cloudinary

nuxt-tina udah nyediain adapter Cloudinary siap pakai lewat next-tinacms-cloudinary (bukan hard dependency: cuma di-import secara dinamis pas kamu beneran pakai).

1. Aktifkan server route-nya

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-tina'],
  tina: {
    media: {
      provider: 'cloudinary', // default: 'tina'
    },
  },
})

Ini ngedaftarin /api/cloudinary/** (adapter Nitro buat createMediaHandler dari next-tinacms-cloudinary), cuma kalau diaktifkan, jadi kalau kamu nggak pakai Cloudinary, package itu nggak pernah ke-bundle sama sekali.

2. Setup credentials-nya

Cloudinary butuh tiga set env var yang berbeda. Ini bukan duplikasi yang bisa dipangkas. Masing-masing dibaca sama consumer yang beda:

.env
# Dibaca sama Nitro runtime (handler /api/cloudinary/**)
NUXT_TINA_CLOUDINARY_CLOUD_NAME=<cloud-name>
NUXT_TINA_CLOUDINARY_API_KEY=<api-key>
NUXT_TINA_CLOUDINARY_API_SECRET=<api-secret>

# Dibaca sama CLI tinacms (tinacms build / tinacms dev)
TINA_TOKEN=<sama-dengan-NUXT_TINA_TOKEN>
TINA_PUBLIC_CLIENT_ID=<sama-dengan-NUXT_TINA_CLIENT_ID>

# Dibaca sama next-tinacms-cloudinary (package ini dari ekosistem Next.js)
NEXT_PUBLIC_TINA_CLIENT_ID=<sama-dengan-NUXT_TINA_CLIENT_ID>
VarDibaca samaKenapa format ini
NUXT_TINA_*Nitro server runtimeKonvensi prefix env Nuxt sendiri
TINA_TOKEN + TINA_PUBLIC_CLIENT_IDCLI tinacmsCLI-nya punya konvensi naming sendiri
NEXT_PUBLIC_TINA_CLIENT_IDinternal next-tinacms-cloudinaryPackage ini hardcode baca env var dengan prefix NEXT_PUBLIC_

3. Install dependency-nya

pnpm add next-tinacms-cloudinary @tinacms/auth

4. Arahkan tina/config.ts ke Cloudinary

import { tinaCloudinaryMediaStore } from 'nuxt-tina/media'

export default defineConfig({
  media: {
    loadCustomStore: tinaCloudinaryMediaStore,
  },
})

⚠️ Gotcha: jangan pakai process.env di sini buat toggle on/off

Bagian media di tina/config.ts juga ke-bundle ke JavaScript yang jalan di browser admin (nggak cuma dieksekusi di server): @tinacms/cli nyalin source mentah file ini buat bagian yang mesti jalan di client-side (loadCustomStore, ui.component custom, dll), bukan hasil evaluasi Node-nya.

Artinya kalau kamu nulis:

// ❌ SALAH: process.env selalu undefined di browser
const useCloudinary = !!process.env.NUXT_TINA_CLOUDINARY_CLOUD_NAME
media: useCloudinary ? { loadCustomStore: tinaCloudinaryMediaStore } : { tina: {...} }

...toggle-nya selalu balik ke default di admin (Media Manager browsing filesystem project, bukan Cloudinary), meskipun credentials-nya valid dan server route-nya jalan normal pas ditest langsung pakai curl. Cek tina/__generated__/config.prebuild.jsx kalau mau lihat buktinya: ekspresi process.env.X-nya ada di situ apa adanya, nggak pernah di-inline jadi true/false.

Fix-nya: kalau kamu mau nyalain/matiin Cloudinary, ubah langsung nilai literal-nya di source:

// ✅ BENAR: literal yang fixed, bukan ekspresi env
const useCloudinary = true // ubah manual, jangan baca env var

Toggle di sisi server (media.provider di nuxt.config.ts) nggak kena masalah ini: itu murni Node/build-time dan jalan normal pakai env var biasa.

Copyright © 2026