Mantap. Karena R2 kamu sudah aktif, sekarang langkah yang paling benar adalah:

Pages untuk frontend, R2 untuk media.
Itu memang pola yang dianjurkan Cloudflare untuk asset storage saat file di Pages mulai banyak atau besar. R2 bisa diakses lewat endpoint S3-compatible, dan bucket bisa dibuat public lewat custom domain atau managed subdomain. (Cloudflare Docs)

Arsitektur yang kita pakai

Flow barunya begini:

media lokal
→ upload ke R2
→ simpan URL public R2 di data.json
→ push hanya index.html / app.js / style.css / data.json ke GitHub
→ Pages deploy

Dengan model ini, repo kamu tetap ringan, deploy Pages lebih cepat, dan video tidak numpuk di GitHub. R2 bucket default-nya private, jadi kamu memang perlu public access lewat custom domain atau Cloudflare-managed subdomain supaya file video bisa diputar langsung dari browser. (Cloudflare Docs)

Langkah 1 — siapkan bucket dan URL public

Di dashboard Cloudflare, masuk ke R2 lalu buat bucket kalau belum ada. Setelah itu, di bucket tersebut buka Settings dan tambahkan Custom Domain atau pakai public access yang disediakan. Di dokumentasi resmi, custom domain diatur dari halaman bucket > Settings > Custom Domains. (Cloudflare Docs)

Contoh hasil akhirnya nanti kamu punya base URL seperti ini:

https://cdn.domainkamu.com

atau semacam managed public URL dari Cloudflare.

Coba tes satu file nanti dengan format:

https://cdn.domainkamu.com/video1.mp4

Kalau URL itu bisa dibuka langsung di browser, berarti bucket public kamu sudah benar. Pengaturan public bucket memang perlu dilakukan secara eksplisit karena bucket R2 tidak public secara default. (Cloudflare Docs)

Langkah 2 — buat Access Key untuk upload

Kalau kamu mau upload dari script Node lokal, cara paling praktis adalah pakai API S3-compatible R2 dengan Access Key ID dan Secret Access Key. Cloudflare menjelaskan key ini dibuat dari dashboard R2 melalui API Tokens / Manage API Tokens, lalu pilih permission Object Read & Write untuk bucket yang kamu pakai. Endpoint S3-nya berbentuk:

https://<ACCOUNT_ID>.r2.cloudflarestorage.com

(Cloudflare Docs)

Langkah 3 — install package yang dibutuhkan

Di project kamu, install AWS SDK v3 untuk akses S3-compatible R2:

npm install @aws-sdk/client-s3

Cloudflare memang menyediakan contoh resmi penggunaan @aws-sdk/client-s3 untuk R2. (Cloudflare Docs)

Langkah 4 — tambahkan .env

Buat file .env di root project:

R2_ACCOUNT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
R2_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx
R2_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
R2_BUCKET=nama-bucket-kamu
R2_PUBLIC_BASE_URL=https://cdn.domainkamu.com

R2_PUBLIC_BASE_URL ini penting karena nanti data.json akan memakai URL public itu, bukan path lokal media/....

Tambahkan .env ke .gitignore supaya key tidak ikut ke GitHub.

Langkah 5 — rapikan package.json

Isi minimal yang aman:

{
  "name": "video-site",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "node build.js",
    "generate": "node generate.js",
    "thumb": "node generate-thumb.js",
    "r2": "node upload-r2.js"
  },
  "dependencies": {
    "@aws-sdk/client-s3": "^3.0.0"
  }
}

Langkah 6 — buat upload-r2.js

Ini versi yang sederhana dan cocok buat workflow kamu. Script ini akan upload file dari folder media/ ke bucket R2 dengan key yang sama seperti nama file lokal.

import fs from "fs";
import path from "path";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const mediaDir = "./media";

const {
  R2_ACCOUNT_ID,
  R2_ACCESS_KEY_ID,
  R2_SECRET_ACCESS_KEY,
  R2_BUCKET
} = process.env;

if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !R2_BUCKET) {
  console.error("Env R2 belum lengkap.");
  process.exit(1);
}

const s3 = new S3Client({
  region: "auto",
  endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: R2_ACCESS_KEY_ID,
    secretAccessKey: R2_SECRET_ACCESS_KEY
  }
});

function getContentType(file) {
  const ext = path.extname(file).toLowerCase();

  if (ext === ".mp4") return "video/mp4";
  if (ext === ".m3u8") return "application/vnd.apple.mpegurl";
  if (ext === ".ts") return "video/mp2t";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".png") return "image/png";
  if (ext === ".webp") return "image/webp";
  if (ext === ".gif") return "image/gif";

  return "application/octet-stream";
}

async function uploadFile(file) {
  const fullPath = path.join(mediaDir, file);
  const body = fs.readFileSync(fullPath);

  const command = new PutObjectCommand({
    Bucket: R2_BUCKET,
    Key: file,
    Body: body,
    ContentType: getContentType(file)
  });

  await s3.send(command);
  console.log(`Uploaded: ${file}`);
}

async function main() {
  const files = fs.readdirSync(mediaDir);

  for (const file of files) {
    const fullPath = path.join(mediaDir, file);
    if (fs.statSync(fullPath).isFile()) {
      await uploadFile(file);
    }
  }

  console.log("Upload selesai.");
}

main().catch(err => {
  console.error("Upload gagal:", err);
  process.exit(1);
});

Script ini memakai pola yang sesuai dengan contoh resmi Cloudflare untuk AWS SDK v3, yaitu client S3-compatible dengan endpoint R2 dan credentials Access Key/Secret Key. (Cloudflare Docs)

Langkah 7 — update generate.js agar pakai URL R2

Sekarang data.json kamu masih mengarah ke lokal:

"src": "media/ade_manisss02_2022-05-10_10-01-23.mp4"

Itu harus diubah menjadi URL public R2. Cara paling enak: generate.js baca R2_PUBLIC_BASE_URL.

Tambahkan ini di atas:

const publicBase = process.env.R2_PUBLIC_BASE_URL || "";

Lalu bagian src dan thumb ubah dari:

src: `media/${file}`

menjadi:

src: publicBase ? `${publicBase}/${file}` : `media/${file}`

Dan thumbnail dari:

thumb: `media/${thumb}`

menjadi:

thumb: publicBase ? `${publicBase}/${thumb}` : `media/${thumb}`

Jadi kalau env ada, otomatis pakai R2. Kalau env belum ada, tetap fallback ke lokal. Itu bikin workflow kamu aman saat testing lokal.

Langkah 8 — workflow harian kamu nanti

Nanti urutannya jadi begini:

node generate-thumb.js
node upload-r2.js
node generate.js
git add .
git commit -m "update content"
git push origin main

Atau nanti kita satukan jadi satu command publish.js.

Langkah 9 — HLS perlu perhatian khusus

Kalau kamu upload .m3u8, kamu juga wajib upload semua .ts yang dirujuk playlist itu, dan path-nya harus tetap cocok. Selain itu, content type untuk .m3u8 dan .ts harus benar supaya browser/player tidak aneh. Cloudflare memang mendukung upload object ke R2 dengan berbagai tool, dan aksesnya lewat object URL/public bucket setelah di-publish. (Cloudflare Docs)

Langkah 10 — saran paling aman buat kamu sekarang

Mulai dari MP4 + thumbnail dulu ke R2, jangan langsung HLS semua.
Kenapa? Karena MP4 lebih gampang divalidasi:

  1. upload 1 file mp4
  2. upload 1 jpg
  3. buka URL public keduanya
  4. generate data.json
  5. test di site

Kalau itu sudah mulus, baru kita naik ke HLS.

Ringkasnya

Sekarang fokus kamu adalah:

  • buat bucket public
  • buat access key
  • install AWS SDK
  • upload file ke R2
  • generate data.json dengan URL R2

Kalau kamu mau, next saya bantu bikin versi final generate.js yang langsung cocok dengan R2, jadi kamu tinggal copy-paste tanpa mikir lagi.

Mantap. Karena R2 kamu sudah aktif, sekarang langkah yang paling benar adalah:

Pages untuk frontend, R2 untuk media.
Itu memang pola yang dianjurkan Cloudflare untuk asset storage saat file di Pages mulai banyak atau besar. R2 bisa diakses lewat endpoint S3-compatible, dan bucket bisa dibuat public lewat custom domain atau managed subdomain. (Cloudflare Docs)

Arsitektur yang kita pakai

Flow barunya begini:

media lokal
→ upload ke R2
→ simpan URL public R2 di data.json
→ push hanya index.html / app.js / style.css / data.json ke GitHub
→ Pages deploy

Dengan model ini, repo kamu tetap ringan, deploy Pages lebih cepat, dan video tidak numpuk di GitHub. R2 bucket default-nya private, jadi kamu memang perlu public access lewat custom domain atau Cloudflare-managed subdomain supaya file video bisa diputar langsung dari browser. (Cloudflare Docs)

Langkah 1 — siapkan bucket dan URL public

Di dashboard Cloudflare, masuk ke R2 lalu buat bucket kalau belum ada. Setelah itu, di bucket tersebut buka Settings dan tambahkan Custom Domain atau pakai public access yang disediakan. Di dokumentasi resmi, custom domain diatur dari halaman bucket > Settings > Custom Domains. (Cloudflare Docs)

Contoh hasil akhirnya nanti kamu punya base URL seperti ini:

https://cdn.domainkamu.com

atau semacam managed public URL dari Cloudflare.

Coba tes satu file nanti dengan format:

https://cdn.domainkamu.com/video1.mp4

Kalau URL itu bisa dibuka langsung di browser, berarti bucket public kamu sudah benar. Pengaturan public bucket memang perlu dilakukan secara eksplisit karena bucket R2 tidak public secara default. (Cloudflare Docs)

Langkah 2 — buat Access Key untuk upload

Kalau kamu mau upload dari script Node lokal, cara paling praktis adalah pakai API S3-compatible R2 dengan Access Key ID dan Secret Access Key. Cloudflare menjelaskan key ini dibuat dari dashboard R2 melalui API Tokens / Manage API Tokens, lalu pilih permission Object Read & Write untuk bucket yang kamu pakai. Endpoint S3-nya berbentuk:

https://<ACCOUNT_ID>.r2.cloudflarestorage.com

(Cloudflare Docs)

Langkah 3 — install package yang dibutuhkan

Di project kamu, install AWS SDK v3 untuk akses S3-compatible R2:

npm install @aws-sdk/client-s3

Cloudflare memang menyediakan contoh resmi penggunaan @aws-sdk/client-s3 untuk R2. (Cloudflare Docs)

Langkah 4 — tambahkan .env

Buat file .env di root project:

R2_ACCOUNT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
R2_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx
R2_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
R2_BUCKET=nama-bucket-kamu
R2_PUBLIC_BASE_URL=https://cdn.domainkamu.com

R2_PUBLIC_BASE_URL ini penting karena nanti data.json akan memakai URL public itu, bukan path lokal media/....

Tambahkan .env ke .gitignore supaya key tidak ikut ke GitHub.

Langkah 5 — rapikan package.json

Isi minimal yang aman:

{
  "name": "video-site",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "node build.js",
    "generate": "node generate.js",
    "thumb": "node generate-thumb.js",
    "r2": "node upload-r2.js"
  },
  "dependencies": {
    "@aws-sdk/client-s3": "^3.0.0"
  }
}

Langkah 6 — buat upload-r2.js

Ini versi yang sederhana dan cocok buat workflow kamu. Script ini akan upload file dari folder media/ ke bucket R2 dengan key yang sama seperti nama file lokal.

import fs from "fs";
import path from "path";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const mediaDir = "./media";

const {
  R2_ACCOUNT_ID,
  R2_ACCESS_KEY_ID,
  R2_SECRET_ACCESS_KEY,
  R2_BUCKET
} = process.env;

if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !R2_BUCKET) {
  console.error("Env R2 belum lengkap.");
  process.exit(1);
}

const s3 = new S3Client({
  region: "auto",
  endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: R2_ACCESS_KEY_ID,
    secretAccessKey: R2_SECRET_ACCESS_KEY
  }
});

function getContentType(file) {
  const ext = path.extname(file).toLowerCase();

  if (ext === ".mp4") return "video/mp4";
  if (ext === ".m3u8") return "application/vnd.apple.mpegurl";
  if (ext === ".ts") return "video/mp2t";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".png") return "image/png";
  if (ext === ".webp") return "image/webp";
  if (ext === ".gif") return "image/gif";

  return "application/octet-stream";
}

async function uploadFile(file) {
  const fullPath = path.join(mediaDir, file);
  const body = fs.readFileSync(fullPath);

  const command = new PutObjectCommand({
    Bucket: R2_BUCKET,
    Key: file,
    Body: body,
    ContentType: getContentType(file)
  });

  await s3.send(command);
  console.log(`Uploaded: ${file}`);
}

async function main() {
  const files = fs.readdirSync(mediaDir);

  for (const file of files) {
    const fullPath = path.join(mediaDir, file);
    if (fs.statSync(fullPath).isFile()) {
      await uploadFile(file);
    }
  }

  console.log("Upload selesai.");
}

main().catch(err => {
  console.error("Upload gagal:", err);
  process.exit(1);
});

Script ini memakai pola yang sesuai dengan contoh resmi Cloudflare untuk AWS SDK v3, yaitu client S3-compatible dengan endpoint R2 dan credentials Access Key/Secret Key. (Cloudflare Docs)

Langkah 7 — update generate.js agar pakai URL R2

Sekarang data.json kamu masih mengarah ke lokal:

"src": "media/ade_manisss02_2022-05-10_10-01-23.mp4"

Itu harus diubah menjadi URL public R2. Cara paling enak: generate.js baca R2_PUBLIC_BASE_URL.

Tambahkan ini di atas:

const publicBase = process.env.R2_PUBLIC_BASE_URL || "";

Lalu bagian src dan thumb ubah dari:

src: `media/${file}`

menjadi:

src: publicBase ? `${publicBase}/${file}` : `media/${file}`

Dan thumbnail dari:

thumb: `media/${thumb}`

menjadi:

thumb: publicBase ? `${publicBase}/${thumb}` : `media/${thumb}`

Jadi kalau env ada, otomatis pakai R2. Kalau env belum ada, tetap fallback ke lokal. Itu bikin workflow kamu aman saat testing lokal.

Langkah 8 — workflow harian kamu nanti

Nanti urutannya jadi begini:

node generate-thumb.js
node upload-r2.js
node generate.js
git add .
git commit -m "update content"
git push origin main

Atau nanti kita satukan jadi satu command publish.js.

Langkah 9 — HLS perlu perhatian khusus

Kalau kamu upload .m3u8, kamu juga wajib upload semua .ts yang dirujuk playlist itu, dan path-nya harus tetap cocok. Selain itu, content type untuk .m3u8 dan .ts harus benar supaya browser/player tidak aneh. Cloudflare memang mendukung upload object ke R2 dengan berbagai tool, dan aksesnya lewat object URL/public bucket setelah di-publish. (Cloudflare Docs)

Langkah 10 — saran paling aman buat kamu sekarang

Mulai dari MP4 + thumbnail dulu ke R2, jangan langsung HLS semua.
Kenapa? Karena MP4 lebih gampang divalidasi:

  1. upload 1 file mp4
  2. upload 1 jpg
  3. buka URL public keduanya
  4. generate data.json
  5. test di site

Kalau itu sudah mulus, baru kita naik ke HLS.

Ringkasnya

Sekarang fokus kamu adalah:

  • buat bucket public
  • buat access key
  • install AWS SDK
  • upload file ke R2
  • generate data.json dengan URL R2

Kalau kamu mau, next saya bantu bikin versi final generate.js yang langsung cocok dengan R2, jadi kamu tinggal copy-paste tanpa mikir lagi.

alattempur sudah siap

Mantap. Tapi ada satu hal penting dulu:

Key R2 kamu sudah kelihatan di screenshot.
Anggap itu sudah terekspos. Habis ini, rotate/ganti R2_ACCESS_KEY_ID dan R2_SECRET_ACCESS_KEY di Cloudflare dulu. Jangan dipakai lama-lama.

Kalau itu sudah siap, kita lanjut ke script upload.

1. Install paket yang dibutuhkan

Di folder project:

npm install @aws-sdk/client-s3 dotenv

2. Update package.json

Pakai isi yang rapi seperti ini:

{
  "name": "video-site",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "node build.js",
    "generate": "node generate.js",
    "thumb": "node generate-thumb.js",
    "r2": "node upload-r2.js",
    "publish": "node publish.js"
  },
  "dependencies": {
    "@aws-sdk/client-s3": "^3.0.0",
    "dotenv": "^16.4.0"
  }
}

3. Update .gitignore

Pastikan ada ini:

node_modules
.env
.DS_Store
Thumbs.db

4. Buat upload-r2.js

Replace atau buat file baru upload-r2.js:

import "dotenv/config";
import fs from "fs";
import path from "path";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const mediaDir = "./media";

const {
  R2_ACCOUNT_ID,
  R2_ACCESS_KEY_ID,
  R2_SECRET_ACCESS_KEY,
  R2_BUCKET
} = process.env;

if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !R2_BUCKET) {
  console.error("Env R2 belum lengkap. Cek .env");
  process.exit(1);
}

const client = new S3Client({
  region: "auto",
  endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: R2_ACCESS_KEY_ID,
    secretAccessKey: R2_SECRET_ACCESS_KEY
  }
});

function getContentType(fileName) {
  const ext = path.extname(fileName).toLowerCase();

  if (ext === ".mp4") return "video/mp4";
  if (ext === ".m3u8") return "application/vnd.apple.mpegurl";
  if (ext === ".ts") return "video/mp2t";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".png") return "image/png";
  if (ext === ".webp") return "image/webp";
  if (ext === ".gif") return "image/gif";
  if (ext === ".json") return "application/json";

  return "application/octet-stream";
}

async function uploadFile(fileName) {
  const fullPath = path.join(mediaDir, fileName);

  if (!fs.statSync(fullPath).isFile()) return;

  const body = fs.readFileSync(fullPath);

  const command = new PutObjectCommand({
    Bucket: R2_BUCKET,
    Key: fileName,
    Body: body,
    ContentType: getContentType(fileName)
  });

  await client.send(command);
  console.log(`Uploaded: ${fileName}`);
}

async function main() {
  const files = fs.readdirSync(mediaDir);

  for (const file of files) {
    await uploadFile(file);
  }

  console.log("R2 upload selesai.");
}

main().catch(err => {
  console.error("Upload gagal:", err);
  process.exit(1);
});

5. Update generate.js

Karena kamu pakai .env, file generate.js juga harus membaca env.

Tambahkan paling atas:

import "dotenv/config";

Jadi bagian awalnya jadi begini:

import "dotenv/config";
import fs from "fs";
import path from "path";
import { execSync } from "child_process";

Lalu pakai versi generate.js yang terakhir tadi, yang sudah ada helper makeMediaUrl().


6. Buat publish.js

Ini biar nanti sekali jalan:

  • upload ke R2
  • generate data.json

Buat file publish.js:

import { execSync } from "child_process";

function run(cmd) {
  console.log(`\n▶ ${cmd}`);
  execSync(cmd, { stdio: "inherit" });
}

try {
  console.log("🚀 START PUBLISH");

  run("node upload-r2.js");
  run("node generate.js");

  console.log("\n✅ PUBLISH DONE");
} catch (err) {
  console.error("\n❌ PUBLISH FAILED");
  process.exit(1);
}

7. Test satu-satu dulu

Jalankan ini dulu, jangan langsung semua:

test upload R2

npm run r2

Kalau sukses, file di folder media harus naik ke bucket sosmedviral.

test generate data

npm run generate

Cek data.json, harusnya src dan thumb sudah jadi URL https://cdn.yadisini.com/...


8. Kalau dua-duanya sukses, baru jalankan:

npm run publish

9. Setelah itu push ke GitHub

git add .
git commit -m "add R2 media pipeline"
git push origin main

10. Yang harus kamu cek setelah publish

Di data.json, item seperti ini:

{
  "title": "ade_manisss02_2022-05-10_10-01-23",
  "type": "mp4",
  "kind": "video",
  "src": "https://cdn.yadisini.com/ade_manisss02_2022-05-10_10-01-23.mp4",
  "thumb": "https://cdn.yadisini.com/ade_manisss02_2022-05-10_10-01-23.jpg",
  "duration": 11,
  "size": 2544791
}

Kalau masih media/..., berarti dotenv belum kebaca atau generate.js belum diupdate.


Urutan aman sekarang

Lakukan begini:

npm install
npm run r2
npm run generate

Kalau dua itu sukses, kirim errornya kalau ada.
Kalau bersih, baru lanjut npm run publish dan push.