Cara Membuat Fitur Followers dengan Next.js dan Cloudflare D1

Ilustrasi: Antarmuka widget Followers terintegrasi dengan ekosistem Next.js dan Cloudflare D1.
Pengembangan antarmuka website modern menuntut pengalaman pengguna (User Experience / UX) yang mulus, interaktif, dan terintegrasi secara mendalam dengan sistem autentikasi. Pada platform portal edukasi yang dibangun menggunakan arsitektur mutakhir seperti Next.js, Tailwind CSS, NextAuth (Google Login), dan ekosistem Cloudflare Pages, salah satu fitur komunitas yang paling krusial adalah widget pengikut (Followers).
Artikel ini mendokumentasikan langkah demi langkah, dari awal hingga sukses 100% tanpa error, dalam membangun fitur Followers di sidebar kanan website. Kita akan membedah cara merancang komponen UI yang mendukung mode gelap (Dark Mode), menerapkan logika auto-follow cerdas menggunakan localStorage, mengatasi masalah caching agresif pada Next.js, dan menyimpan data secara permanen di database SQL serverless Cloudflare D1.
Ilustrasi 1: Antarmuka premium widget Followers yang responsif
Langkah 1: Merancang Antarmuka Premium dan Responsif
Langkah pertama adalah membuat tata letak visual yang elegan. Widget ini ditempatkan di sidebar kanan, memanfaatkan ruang secara optimal dengan desain grid 5 kolom. Untuk memastikan widget ini menyatu dengan tema website yang dinamis, kita menggunakan variabel warna Tailwind CSS bawaan sistem (seperti bg-card, border-border, dan text-primary). Hal ini menjamin widget akan otomatis menyesuaikan diri dengan Mode Terang maupun Mode Gelap.
Selain estetika, performa visual juga diperhatikan dengan membatasi jumlah avatar yang tampil sebanyak 15 item per halaman. Sistem paginasi (Prev/Next) disematkan untuk mengakomodasi pertumbuhan jumlah pengikut tanpa merusak rasio tinggi sidebar.
Langkah 2: Menerapkan Logika Cerdas "Auto-Follow"
Pengalaman pengguna yang buruk sering terjadi ketika pengunjung yang belum login menekan sebuah tombol, lalu dipaksa login, dan setelah login mereka harus menekan tombol yang sama lagi. Kita memecahkan masalah ini dengan logika localStorage.
Ketika pengunjung anonim menekan tombol "Ikuti Pena Edukasi", sistem akan memunculkan dialog konfirmasi. Jika disetujui, sistem mencatat perintah pending_follow = true di penyimpanan lokal peramban sebelum mengarahkan pengguna ke halaman login Google (NextAuth). Setelah autentikasi berhasil dan pengguna dikembalikan ke halaman semula, sebuah useEffect mendeteksi jejak ini, mengeksekusi fungsi penyimpanan follower, dan menghapus jejak tersebut secara otomatis.
Ilustrasi 2: Alur cerdas Auto-Follow memanfaatkan localStorage
Langkah 3: Konfigurasi Database Cloudflare D1
Sebagai platform yang di-hosting pada Cloudflare Pages, menggunakan Cloudflare D1 adalah keputusan paling optimal. D1 adalah database SQLite serverless yang memberikan latensi sangat rendah dan batas penggunaan gratis yang masif (hingga 100.000 operasi tulis dan 5.000.000 operasi baca per hari).
Cara Pengaturan:
- Buka Dasbor Cloudflare, navigasikan ke Workers & Pages > D1 SQL Database.
- Buat database baru bernama
Contoh: follower-penaedukasi-db. - Buka tab Console pada database tersebut dan jalankan kueri SQL berikut untuk membuat tabel:
CREATE TABLE followers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
avatar TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Pengikatan (Binding) Mutlak Diperlukan: Agar kode Next.js dapat mengenali database ini, Anda wajib menghubungkannya di pengaturan Cloudflare Pages. Masuk ke Settings > Bindings proyek Anda, tambahkan D1 database binding dengan nama variabel persis DB dan pilih database follower-penaedukasi-db. Kegagalan melakukan ini akan memicu Error Status 500.
Ilustrasi 4: Edge Runtime API mengeksekusi D1 secara force-dynamic
Langkah 4: Membangun Backend API yang Tahan Banting
Sistem ini membutuhkan jalur komunikasi (API) antara antarmuka pengguna dengan database. Di sinilah sering terjadi masalah di mana data pengikut muncul saat tombol ditekan, namun hilang kembali saat halaman di-reload. Penyebab utamanya adalah fitur caching agresif bawaan Next.js yang menyimpan hasil tanggapan usang.
Untuk mengatasinya, kita menggunakan perintah export const dynamic = 'force-dynamic'; agar API selalu mengambil data segar secara real-time dari database. Selain itu, fungsi API harus dijalankan di Edge Runtime Cloudflare menggunakan modul @cloudflare/next-on-pages. Penanganan error yang mendetail juga ditambahkan untuk memudahkan pelacakan (debugging).
Berikut adalah kode final untuk file app/api/follow/route.ts:
import { NextResponse } from 'next/server';
import { getRequestContext } from '@cloudflare/next-on-pages';
// WAJIB: Menjalankan API di Edge Runtime Cloudflare
export const runtime = 'edge';
// WAJIB: Mematikan cache Next.js agar data selalu tampil terbaru saat direload
export const dynamic = 'force-dynamic';
export async function GET() {
try {
const env = getRequestContext().env as any;
// Validasi apakah Binding DB berhasil dideteksi
if (!env || !env.DB) {
return NextResponse.json({ error: "Database binding 'DB' tidak ditemukan di Cloudflare!" }, { status: 500 });
}
// Mengambil data dan mengurutkannya dari yang terbaru
const { results } = await env.DB.prepare(
"SELECT * FROM followers ORDER BY created_at DESC"
).all();
return NextResponse.json(results || [], { status: 200 });
} catch (error: any) {
return NextResponse.json({ error: "DB Error (GET): " + error.message }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
const { email, name, avatar } = body;
const env = getRequestContext().env as any;
if (!env || !env.DB) {
return NextResponse.json({ error: "Database binding 'DB' tidak ditemukan di Cloudflare!" }, { status: 500 });
}
if (!email || !name) {
return NextResponse.json({ error: "Email dan nama wajib diisi" }, { status: 400 });
}
// Menyimpan data. 'ON CONFLICT DO NOTHING' mencegah duplikasi jika tombol ditekan berkali-kali
await env.DB.prepare(
"INSERT INTO followers (email, name, avatar) VALUES (?, ?, ?) ON CONFLICT(email) DO NOTHING"
).bind(email, name, avatar).run();
return NextResponse.json({ success: true, message: "Berhasil follow" }, { status: 200 });
} catch (error: any) {
return NextResponse.json({ error: "DB Error (POST): " + error.message }, { status: 500 });
}
}
Langkah 5: Kode Final Komponen Front-End (Bebas Error)
Dengan backend yang sudah siap sedia, kita menyatukan seluruh logika ke dalam komponen React. Kode ini menerapkan Optimistic UI, di mana profil pengguna langsung dimunculkan di layar tanpa perlu menunggu balasan lambat dari server, memberikan ilusi kecepatan yang luar biasa. Deklarasi FollowerType juga disematkan agar pemeriksaan tipe data (TypeScript) lolos 100% tanpa peringatan bergaris merah di editor VS Code.
Berikut adalah kode final untuk file components/FollowersWidget.tsx yang terbukti berjalan sempurna:
'use client';
import { useState, useEffect } from 'react';
import { useSession, signIn } from 'next-auth/react';
// Mendefinisikan tipe data agar TypeScript (TS) aman 100%
type FollowerType = {
id?: number | string;
email: string;
name: string;
avatar: string;
created_at?: string;
};
// 15 item per halaman (3 baris x 5 kolom)
const ITEMS_PER_PAGE = 15;
export default function FollowersWidget() {
const { data: session, status } = useSession();
const isLoggedIn = status === 'authenticated';
const [followers, setFollowers] = useState<FollowerType[]>([]);
const [currentPage, setCurrentPage] = useState(0);
const [isFollowing, setIsFollowing] = useState(false);
// Mencegah totalPages menjadi 0 agar tombol prev/next tidak error
const totalPages = Math.max(1, Math.ceil(followers.length / ITEMS_PER_PAGE));
const currentFollowers = followers.slice(
currentPage * ITEMS_PER_PAGE,
(currentPage + 1) * ITEMS_PER_PAGE
);
// 1. AMBIL DATA DARI DATABASE SAAT HALAMAN DIBUKA
useEffect(() => {
async function fetchFollowers() {
try {
const res = await fetch('/api/follow');
const data = await res.json();
if (Array.isArray(data)) {
setFollowers(data);
// Cek apakah pengunjung yang sedang login sudah pernah follow sebelumnya
if (session?.user?.email) {
const hasFollowed = data.some((f: FollowerType) => f.email === session.user?.email);
if (hasFollowed) {
setIsFollowing(true);
}
}
}
} catch (error) {
console.error("Gagal memuat pengikut:", error);
}
}
fetchFollowers();
}, [session]);
// 2. CEK AUTO-FOLLOW SETELAH LOGIN GOOGLE
useEffect(() => {
if (isLoggedIn && session?.user) {
const pendingFollow = localStorage.getItem("pending_follow");
if (pendingFollow === "true" && !isFollowing) {
eksekusiFollow(session.user);
localStorage.removeItem("pending_follow");
}
}
}, [isLoggedIn, session, isFollowing]);
// 3. SIMPAN KE DATABASE KETIKA TOMBOL FOLLOW DITEKAN
const eksekusiFollow = async (user: any) => {
if (!user?.email) return;
setIsFollowing(true);
const newFollower: FollowerType = {
id: Date.now(),
email: user.email,
name: user.name || "Pengguna",
avatar: user.image || `https://ui-avatars.com/api/?name=${user.name || 'P'}&background=0D8ABC&color=fff`,
};
// Optimistic UI: Tampilkan langsung di layar
setFollowers((prev) => [newFollower, ...prev]);
// Kirim data ke API (Database) secara diam-diam di background
try {
await fetch('/api/follow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newFollower)
});
} catch (error) {
console.error("Gagal menyimpan ke database:", error);
}
};
const handleFollowClick = () => {
if (!isLoggedIn) {
const confirmLogin = window.confirm(
"Anda belum bisa follow sebelum login.\n\nKlik OK untuk melanjutkan login dengan Google."
);
if (confirmLogin) {
localStorage.setItem("pending_follow", "true");
signIn('google');
}
return;
}
if (!isFollowing) {
eksekusiFollow(session?.user);
}
};
return (
<div className="w-full rounded-xl border border-border bg-card p-5 shadow-sm font-sans">
{/* Header Widget */}
<div className="flex items-center justify-between mb-4 border-b-2 border-primary pb-2">
<h2 className="text-xs font-bold uppercase tracking-wider text-primary">
Pengikut ({followers.length})
</h2>
{/* Pagination Prev / Next */}
<div className="flex gap-2 text-xs font-medium">
<button
onClick={() => setCurrentPage(prev => Math.max(0, prev - 1))}
disabled={currentPage === 0}
className={`transition-colors ${currentPage === 0 ? 'text-muted-foreground opacity-50 cursor-not-allowed' : 'text-primary hover:opacity-80'}`}
>
Prev
</button>
<span className="text-muted-foreground opacity-50">|</span>
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages - 1, prev + 1))}
disabled={currentPage === totalPages - 1}
className={`transition-colors ${currentPage === totalPages - 1 ? 'text-muted-foreground opacity-50 cursor-not-allowed' : 'text-primary hover:opacity-80'}`}
>
Next
</button>
</div>
</div>
{/* Area Grid Avatar (Kosong atau Terisi) */}
{followers.length === 0 ? (
<div className="text-center py-6 text-xs text-muted-foreground">
Belum ada pengikut. Jadilah yang pertama!
</div>
) : (
<div className="grid grid-cols-5 gap-2 mb-5">
{currentFollowers.map((follower) => (
<div key={follower.id || follower.email} className="relative group aspect-square">
<img
src={follower.avatar}
alt={follower.name}
className="w-full h-full object-cover rounded-md shadow-sm border border-border group-hover:scale-105 transition-transform duration-200"
/>
<div className="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-foreground text-background text-[10px] px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10 shadow-lg">
{follower.name}
</div>
</div>
))}
</div>
)}
{/* Tombol Follow */}
<button
onClick={handleFollowClick}
disabled={isFollowing}
className={`w-full py-2.5 rounded-lg text-sm font-semibold transition-all duration-300 flex items-center justify-center gap-2 ${
isFollowing
? 'bg-secondary text-secondary-foreground cursor-default'
: 'bg-[#1a388b] text-white hover:bg-[#122866] hover:shadow-md active:scale-[0.98]'
}`}
>
{isFollowing ? (
<>
<svg className="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
Mengikuti
</>
) : (
'Ikuti Pena Edukasi'
)}
</button>
</div>
);
}
Langkah 6: Eksekusi dan Deployment
Proses integrasi di atas ditutup dengan melakukan pengiriman kode sumber ke repositori GitHub. Anda hanya perlu membuka terminal di direktori proyek dan mengeksekusi tiga perintah standar:
git commit -m "feat: merilis fitur followers terintegrasi dengan database D1"
git push
Berkat infrastruktur CI/CD Cloudflare Pages, repositori yang diperbarui akan memicu proses deployment ulang secara otomatis. Hanya dalam hitungan menit, sistem basis data Cloudflare D1 beserta kaitan variabel DB terjalin secara utuh dalam siklus aplikasi live.
Kesimpulan
Keberhasilan mengimplementasikan sistem Follower interaktif ini membuktikan kekuatan kombinasi ekosistem Next.js dan infrastruktur Edge Cloudflare. Dengan fondasi database dan API yang kini telah terbangun kokoh serta terbukti bebas dari gangguan caching agresif, pengembang dapat dengan mudah menduplikasi arsitektur yang persis sama untuk memperluas fitur aplikasi lainnya—seperti membangun modul Komentar yang tangguh.


