Cara Membuat Fitur Followers dengan Next.js dan Cloudflare D1

PE
Pena Edukasi
Ilustrasi antarmuka fitur followers pada portal edukasi yang dibangun menggunakan Next.js, Google Login, dan database 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.

Antarmuka Tombol Fitur Follow Pena Edukasi

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.

Baca Juga: Adab kepada Orang Tua dan Guru: Dua Pintu Keberkahan dalam Hidup

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.

1. Klik Follow (Belum Login) 2. localStorage: "true" 3. Otomatis Ter-Follow!

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:

  1. Buka Dasbor Cloudflare, navigasikan ke Workers & Pages > D1 SQL Database.
  2. Buat database baru bernama Contoh: follower-penaedukasi-db.
  3. 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.

route.ts export const dynamic = 'force-dynamic' ⚡ EDGE API D1 Database 200 OK

Ilustrasi 4: Edge Runtime API mengeksekusi D1 secara force-dynamic

Baca Juga: Cara Generate dan Verifikasi API Key IndexNow di Bing Webmaster Tools

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>
 );
}

Baca Juga: Cara Otomatisasi IndexNow Bing di Next.js dan Cloudflare Pages CMS

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 add .
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.

Artikel Terkait