MADEBYARIS

Cara membuat halaman jadwal dan hasil pertandingan yang ramah SEO dengan Next.js

By Aris Setiawan8 min read
Cara membuat halaman jadwal dan hasil pertandingan yang ramah SEO dengan Next.js

Borneo FC memainkan laga debutnya di AFC Challenge League (ACGL) melawan Yangon United di Stadion Segiri, Samarinda, pada Minggu, 18 Oktober 2026 pukul 20.00 WITA atau 19.00 WIB, sesuai jadwal di website resmi klub. Kalau klub atau turnamen kamu membagikan jadwal hanya sebagai gambar di grup WhatsApp atau sebagai satu file PDF, Google tidak punya halaman untuk setiap pertandingan yang bisa diberi data terstruktur dan diperbarui dengan mudah.

Jawaban singkat

Struktur yang ramah SEO cukup terdiri dari satu halaman daftar dan satu halaman untuk setiap pertandingan, dengan data yang diambil dari satu file supaya tidak ada dua versi jadwal. Setiap halaman pertandingan diberi JSON-LD SportsEvent, yaitu data terstruktur yang menjelaskan siapa bertanding, kapan, dan di mana. Setelah halaman live, hasilnya dicek di Rich Results Test dan Search Console.

Halaman daftar jadwal dan hasil Liga Contoh di desktop, dikelompokkan per pekan, dengan skor untuk laga yang sudah selesai

Hasil akhir: halaman jadwal yang bisa dibaca Google

Hasilnya berupa satu halaman daftar jadwal dan hasil, ditambah satu URL untuk setiap pertandingan dengan judul, tempat, dan waktu dalam bentuk teks biasa. Data yang dipakai adalah data contoh (fiktif), yaitu liga “Liga Contoh” dengan tim “Tim A FC” sampai “Tim H FC”. Saya membangun demo ini dengan bantuan AI coding agent, lalu hasilnya saya cek sendiri.

Di tutorial ini kita tidak membuat skor live yang berubah tanpa reload. Untuk kebutuhan itu, saya sudah menulis scoreboard sepak bola live dengan Next.js, dan halaman jadwal di sini bisa menjadi pasangannya.

Ilustrasi daftar jadwal dan skor pertandingan yang rapi di ponsel dan browser, cocok untuk halaman ramah SEO

Yang kamu perlukan

Demo ini dibuat dengan Next.js 16.4.2 (App Router), React 19.3.0, dan Tailwind CSS v4, jadi kamu perlu Node.js yang bisa menjalankan versi Next.js itu, editor kode, dan sedikit pengalaman dengan App Router. Kalau App Router masih terasa baru, baca dulu App Router dalam praktik.

Langkah 1: simpan data pertandingan di satu tempat

Semua halaman membaca data dari satu file JSON berisi daftar pertandingan, lewat fungsi kecil seperti getAllMatches() dan getMatchBySlug(), sehingga perubahan jadwal cukup ditulis sekali dan muncul di halaman daftar maupun di halaman pertandingan setelah build berikutnya. Halaman-halaman itu tidak memanggil API atau database.

Cuplikan data pertandingan (JSON), 2 dari 8 pertandingan:

[
  {
    "slug": "tim-a-fc-vs-tim-b-fc-pekan-1",
    "matchday": 1,
    "homeTeam": "Tim A FC",
    "awayTeam": "Tim B FC",
    "status": "finished",
    "homeScore": 2,
    "awayScore": 1,
    "kickoff": "2026-10-12T15:30:00+07:00",
    "venue": "Stadion Contoh Utara, Jakarta"
  },
  …
  {
    "slug": "tim-a-fc-vs-tim-c-fc-pekan-2",
    "matchday": 2,
    "homeTeam": "Tim A FC",
    "awayTeam": "Tim C FC",
    "status": "scheduled",
    "kickoff": "2026-10-25T19:00:00+07:00",
    "venue": "Stadion Contoh Utara, Jakarta"
  },
]

Laga yang sudah selesai punya homeScore dan awayScore, sedangkan laga yang belum dimainkan tidak punya kolom skor, dan kickoff selalu membawa zona waktu (+07:00).

Langkah 2: buat halaman daftar jadwal dan hasil

Halaman daftar ada di /jadwal dan mengelompokkan pertandingan per pekan, dengan judul “Pekan 1”, “Pekan 2”, dan seterusnya, lalu setiap kartu menampilkan “Selesai” beserta skornya atau “Akan main” beserta jam kick-off WIB. Semua isi ditulis sebagai teks HTML, sehingga setiap baris jadwal bisa dibaca, disalin, dan diperbarui tanpa membuat gambar baru. Di halaman yang sama, demo juga menaruh satu tag script berisi JSON-LD untuk kedelapan pertandingan. Google sendiri menyarankan markup event dipasang di halaman setiap event daripada di halaman yang berisi daftar jadwal, jadi markup yang penting ada di halaman pertandingan pada langkah berikutnya.

app/jadwal/page.tsx:

export default function JadwalPage() {
  const matches = getAllMatches();
  const byDay = groupMatchesByMatchday(matches);
  const jsonLd = matches.map((m) => sportsEventJsonLd(m));

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <div className="mx-auto max-w-5xl px-4 py-8 sm:px-6 sm:py-10">
        {/* … header … */}
        <div className="space-y-10">
          {[...byDay.entries()].map(([matchday, dayMatches]) => (
            <section key={matchday} aria-labelledby={`pekan-${matchday}`}>
              <h2
                id={`pekan-${matchday}`}
                className="mb-4 text-xl font-bold text-zinc-800"
              >
                Pekan {matchday}
              </h2>
              <ul className="grid gap-4 sm:grid-cols-2">
                {dayMatches.map((match) => (
                  <li key={match.slug}>
                    <MatchCard match={match} />
                  </li>
                ))}
              </ul>
            </section>
          ))}
        </div>
        {/* … */}

Langkah 3: buat satu halaman untuk setiap pertandingan

Halaman pertandingan memakai route dinamis /jadwal/[slug], misalnya /jadwal/tim-a-fc-vs-tim-b-fc-pekan-1, dan semua halamannya dibuat lebih dulu lewat generateStaticParams. Fungsi generateMetadata mengisi judul serta deskripsi dari data pertandingan itu sendiri, sehingga setiap URL punya judul yang berbeda. Cara mengatur metadata ini saya bahas lebih lengkap di metadata SEO di Next.js App Router.

app/jadwal/[slug]/page.tsx:

type PageProps = { params: Promise<{ slug: string }> };

export async function generateStaticParams() {
  return getAllSlugs().map((slug) => ({ slug }));
}

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const match = getMatchBySlug(slug);
  if (!match) {
    return { title: "Pertandingan tidak ditemukan" };
  }
  const title = matchTitle(match);
  const description =
    match.status === "finished"
      ? `Hasil ${title}: ${match.homeScore}-${match.awayScore}. Pekan ${match.matchday}, ${match.venue}.`
      : `${title} pekan ${match.matchday}. Kick-off ${formatKickoffWIB(match.kickoff)} WIB di ${match.venue}.`;

  return {
    title,
    description,
    openGraph: {
      title,
      description,
      locale: "id_ID",
      type: "website",
    },
  };
}
Halaman satu pertandingan Tim A FC vs Tim B FC dengan skor akhir 2:1, nama stadion, jam kick-off WIB, dan slug URL-nya

Langkah 4: tambahkan JSON-LD SportsEvent

JSON-LD dibuat oleh fungsi sportsEventJsonLd(match) lalu ditaruh di halaman sebagai tag script, mengikuti pola dari dokumentasi Next.js tentang JSON-LD, dan isinya memakai properti Event dari schema.org seperti nama, waktu mulai, lokasi, dan status.

lib/json-ld.ts:

import { siteUrl } from "./site-url";
// …

export function sportsEventJsonLd(match: Match) {
  const name = `${match.homeTeam} vs ${match.awayTeam}`;
  const url = siteUrl(`/jadwal/${match.slug}`);

  const payload: Record<string, unknown> = {
    "@context": "https://schema.org",
    "@type": "SportsEvent",
    name,
    description: `Pertandingan Liga Contoh pekan ${match.matchday}: ${name}`,
    startDate: match.kickoff,
    eventStatus: eventStatus(match),
    eventAttendanceMode: eventAttendanceMode(),
    location: {
      "@type": "Place",
      name: match.venue,
      address: {
        "@type": "PostalAddress",
        addressCountry: "ID",
      },
    },
    // … homeTeam, awayTeam, competitor …
    url,
  };

  return payload;
}

Cara halaman pertandingan menampilkannya, di app/jadwal/[slug]/page.tsx:

  const jsonLd = sportsEventJsonLd(match);
  // …
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />

Fungsi siteUrl membaca NEXT_PUBLIC_SITE_URL dan memakai https://demo.example.com kalau variabel itu kosong, jadi di website yang sudah live, isi variabel itu dengan domain website kamu sendiri.

Data terstruktur membantu Google memahami halaman, tetapi Google sendiri menulis bahwa mereka tidak menjamin data terstruktur akan muncul di hasil pencarian, dan tampilan khusus untuk event juga belum tercantum untuk Indonesia. Anggap JSON-LD sebagai cara membuat halaman lebih jelas bagi mesin pencari.

Langkah 5: ubah status saat pertandingan ditunda atau dibatalkan

Di demo, status pertandingan hanya scheduled dan finished, dan keduanya tetap menjadi EventScheduled di JSON-LD, seperti terlihat di fungsi berikut.

lib/json-ld.ts:

function eventStatus(match: Match): string {
  if (match.status === "finished") {
    return "https://schema.org/EventScheduled";
  }
  return "https://schema.org/EventScheduled";
}

Untuk laga yang ditunda atau dibatalkan, tambahkan status itu di data, lalu petakan ke nilai yang dijelaskan di panduan Event dari Google berikut:

  • Ditunda tanpa tanggal baru: EventPostponed, dengan startDate lama tetap dipakai sampai tanggal barunya diketahui.
  • Ditunda dengan tanggal baru: EventRescheduled, dengan startDate diganti ke tanggal baru dan previousStartDate berisi tanggal lama (opsional).
  • Dibatalkan: EventCancelled, dengan startDate dan lokasi tetap seperti sebelumnya.

Langkah 6: cek hasilnya di Rich Results Test dan Search Console

Demo ini belum online, jadi screenshot berikut menunjukkan JSON-LD yang dihasilkan satu halaman pertandingan lewat view-source di browser. Setelah halaman kamu live, masukkan URL-nya ke Rich Results Test, lalu pantau di Search Console apakah halaman pertandingan sudah terindeks.

JSON-LD SportsEvent dari satu halaman pertandingan, terlihat lewat view-source di browser

Pertanyaan yang sering ditanyakan

Apakah schema SportsEvent membuat jadwal tampil khusus di Google?

Tidak ada jaminan, seperti dijelaskan di Langkah 4. Yang bisa kamu cek sendiri adalah apakah Google membaca markup-nya, yaitu dengan memasukkan URL halaman pertandingan ke Rich Results Test setelah halaman live, lalu memantau apakah halaman itu terindeks di Search Console. Kalau Rich Results Test menampilkan error, perbaiki dulu kolom yang diminta, karena halaman dengan markup yang rusak tidak membantu Google memahami pertandingan kamu.

Apa bedanya halaman jadwal dan halaman scoreboard live?

Daftar jadwal berisi pertandingan dan hasil akhirnya, yang di demo ini dibuat lebih dulu saat build, sedangkan scoreboard live memperbarui skor selama laga berlangsung tanpa reload. Karena itu, halaman jadwal cocok sebagai halaman yang dicari lewat Google, sementara scoreboard cocok dibuka saat laga sedang berjalan. Kamu bisa menautkan scoreboard dari halaman pertandingan yang sedang berlangsung, sehingga pengunjung dari Google langsung menemukan skor terbaru.

Bagaimana cara menandai pertandingan yang ditunda?

Pakai EventPostponed kalau tanggal barunya belum ada, lalu ganti ke EventRescheduled setelah tanggalnya diketahui, sesuai daftar di Langkah 5. Status itu belum ada di demo, jadi kamu perlu menambahkannya sendiri di data dan di fungsi eventStatus. Tulis juga keterangan “ditunda” di kartu jadwal dan di halaman pertandingan, karena pengunjung membaca teks halaman, dan informasi di teks sebaiknya sama dengan isi markup.

Apakah halaman jadwal perlu satu URL untuk setiap pertandingan?

Perlu, kalau kamu memakai data terstruktur Event, karena panduan Google meminta setiap event punya halaman sendiri dengan markup di URL itu. Di demo, URL itu dibentuk dari slug yang memuat nama tim dan pekan, misalnya /jadwal/tim-a-fc-vs-tim-b-fc-pekan-1, sehingga alamatnya bisa dibaca orang. Pertahankan slug yang sama setelah laga selesai, supaya link yang sudah dibagikan tetap mengarah ke halaman hasil pertandingan.

Penutup

Jadwal yang bisa dibaca Google dimulai dari data teks di satu sumber dan satu URL untuk setiap pertandingan. Buka jadwal klub atau turnamen kamu dan cek apakah jadwal itu masih berupa gambar. Kalau kamu ingin halaman seperti ini dibuatkan, kirim link jadwal yang sekarang lewat jasa pembuatan website Next.js, dan saya balas dalam 24 jam. Kamu juga bisa melihat dulu layanan Next.js development saya.

Terakhir dicek: 18 Oktober 2026.

More onNext.js

Need this built instead of read about?

Send me your project. I reply within 24 hours and tell you plainly whether I’m the right fit.

Send me your project

More on Next.js