← Semua pembelajaran / Astro Nol → Portal Berita
Fase 9 · Migrasi dari CodeIgniter 3

Peta URL lama & redirect 301

Ini bagian migrasi yang paling langsung berdampak ke trafik, dan yang kerusakannya tidak muncul sebagai error di mana pun — hanya sebagai grafik yang menurun.

Intisari

  • Kumpulkan URL dari empat sumber: Search Console, log akses, sitemap lama, dan database.
  • 301 untuk pindah permanen. 302 memberitahu Google URL lamanya masih yang utama.
  • Peta 40 ribu URL masuk ke database, dilayani rute rest — bukan ke konfigurasi.
  • Rantai redirect memboroskan anggaran perayapan. Petakan langsung ke tujuan akhir.
  • Verifikasi dengan menjalankan seluruh daftar URL lewat curl dan memeriksa status serta tujuannya.

Kumpulkan URL dari empat sumber

# 1. Log akses nginx/Apache CI3 — sumber paling lengkap
zcat /var/log/nginx/access.log-*.gz \
  | awk '$9 == 200 && $6 == "\"GET" {print $7}' \
  | sed 's/?.*//' | sort | uniq -c | sort -rn > /tmp/url-log.txt

wc -l /tmp/url-log.txt
# 2. Sitemap lama
curl -s https://portal.contoh.id/sitemap.xml \
  | grep -oP '(?<=<loc>)[^<]+' > /tmp/url-sitemap.txt

# 3. Search Console → Ekspor "Pages" (URL yang benar-benar terindeks)

# 4. Database — bentuk URL yang dihasilkan CI3
mysql -N -e "SELECT CONCAT('/news/detail/', id, '/', slug) FROM artikel WHERE status='terbit'" portal \
  > /tmp/url-db.txt

Log akses adalah sumber yang paling sering dilupakan dan paling berharga. Ia memuat URL yang benar-benar dikunjungi manusia — termasuk bentuk URL lama dari sistem sebelum CI3, tautan dari media sosial dengan parameter aneh, dan halaman yang tidak ada di sitemap mana pun. Kolom jumlah kunjungan juga memberitahumu mana yang benar-benar penting.

Pola URL CI3 dan padanannya

URL lamaURL baruCara memetakan
/news/detail/4471/rupiah-menguat/berita/rupiah-menguatID → slug, dari database
/news/detail/4471/berita/rupiah-menguatSama
/kategori/ekonomi/berita/ekonomiPola
/index.php/news/detail/4471SamaBuang /index.php dulu
/emiten.php?kode=BBCA/perusahaan/BBCAQuery → jalur
/berita.html?p=12/berita?hal=12Sistem sebelum CI3

Tabel peta

CREATE TABLE peta_url (
  jalur_lama  VARCHAR(500) NOT NULL,
  jalur_baru  VARCHAR(500) NOT NULL,
  status      SMALLINT     NOT NULL DEFAULT 301,
  hit         INT UNSIGNED NOT NULL DEFAULT 0,
  terakhir    DATETIME     NULL,
  PRIMARY KEY (jalur_lama(255))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Isi dari database untuk pola yang bisa diturunkan
INSERT IGNORE INTO peta_url (jalur_lama, jalur_baru)
SELECT CONCAT('/news/detail/', id, '/', slug), CONCAT('/berita/', slug)
FROM artikel WHERE status = 'terbit';

INSERT IGNORE INTO peta_url (jalur_lama, jalur_baru)
SELECT CONCAT('/news/detail/', id), CONCAT('/berita/', slug)
FROM artikel WHERE status = 'terbit';

Melayaninya

---
// src/pages/[...jalur].astro — penangkap terakhir
export const prerender = false;

import { cariPetaUrl } from "@/lib/peta-url";

const jalur = "/" + (Astro.params.jalur ?? "");

// 1. Normalisasi bentuk yang umum
const dinormalkan = jalur
  .replace(/^\/index\.php/, "")
  .replace(/\.html$/, "")
  .replace(/\/+$/, "") || "/";

// 2. Cari di peta
const tujuan = await cariPetaUrl(dinormalkan);

if (tujuan) {
  return Astro.redirect(tujuan.jalurBaru, tujuan.status);
}

// 3. Coba pola yang bisa dihitung, tanpa kueri peta
const cocok = dinormalkan.match(/^\/news\/detail\/(\d+)/);
if (cocok) {
  const slug = await slugArtikelById(Number(cocok[1]));
  if (slug) return Astro.redirect(`/berita/${slug}`, 301);
}

// 4. Benar-benar tidak ada
Astro.response.status = 404;
Astro.response.headers.set("Cache-Control", "public, max-age=0, s-maxage=300");
---
<Halaman404 jalurDiminta={jalur} />

Rute rest ini otomatis kalah dari semua rute lain (Fase 1). Kamu tidak perlu mengatur urutan apa pun — /berita/[slug].astro selalu menang, dan penangkap ini hanya menerima yang tidak cocok dengan rute mana pun. Ini kenapa aturan prioritas routing Astro sangat cocok untuk pekerjaan ini.

Cache-nya, karena crawler akan datang berulang

// src/lib/peta-url.ts
import { cache } from "./cache-memori";

export function cariPetaUrl(jalur: string) {
  return cache(`peta:${jalur}`, 3600, async () => {
    const b = await dbBaca
      .selectFrom("peta_url")
      .select(["jalur_baru", "status"])
      .where("jalur_lama", "=", jalur)
      .executeTakeFirst();

    // Catat pemakaian, tanpa ditunggu
    if (b) {
      void dbTulis.updateTable("peta_url")
        .set((eb) => ({ hit: eb("hit", "+", 1), terakhir: new Date() }))
        .where("jalur_lama", "=", jalur).execute();
    }
    return b ? { jalurBaru: b.jalur_baru, status: b.status } : null;
  });
}

Redirect-nya sendiri juga layak di-cache di Cloudflare:

Astro.response.headers.set("Cache-Control", "public, max-age=0, s-maxage=86400");

Redirect 301 tidak berubah. Meng-cache-nya sehari di edge berarti crawler yang merayapi 40 ribu URL lama tidak memukul origin-mu 40 ribu kali.

Jangan bikin rantai

BURUK:
  /news/detail/4471  →  301  →  /berita/detail/4471  →  301  →  /berita/rupiah-menguat

BAIK:
  /news/detail/4471  →  301  →  /berita/rupiah-menguat

Setiap lompatan memakan anggaran perayapan dan menambah latensi bagi pembaca. Kalau kamu pernah mengubah bentuk URL sebelumnya, ratakan seluruh rantai lama langsung ke tujuan akhir sekarang.

301 vs 302 vs 410

StatusArtinya bagi GooglePakai untuk
301Pindah permanen; indeks & sinyal peringkat dipindahkanMigrasi URL
302 / 307Sementara; URL lama tetap yang utamaLogin, A/B test
404Tidak ada; mungkin kembaliSalah ketik URL
410Sengaja dihapus, tidak akan kembaliArtikel yang ditarik permanen

Memakai 302 untuk migrasi adalah kesalahan yang merugikan berbulan-bulan. Google akan mempertahankan URL lama di indeks dan tidak memindahkan sinyal peringkat ke URL baru. Trafiknya tidak hilang seketika — ia perlahan menurun, dan penyebabnya sulit ditemukan karena semuanya "bekerja".

Verifikasi seluruh daftar

#!/usr/bin/env bash
# uji-redirect.sh — jalankan terhadap SELURUH daftar URL lama
U=https://portal.contoh.id
gagal=0

while read -r jalur; do
  hasil=$(curl -sI -o /dev/null -w '%{http_code} %{redirect_url}' "$U$jalur")
  kode=${hasil%% *}
  tujuan=${hasil#* }

  case "$kode" in
    301) ;;                                             # baik
    200) echo "PERINGATAN 200 (tidak dialihkan): $jalur" ;;
    404) echo "GAGAL 404: $jalur"; gagal=$((gagal+1)) ;;
    *)   echo "GAGAL $kode: $jalur"; gagal=$((gagal+1)) ;;
  esac

  # Deteksi rantai
  if [ -n "$tujuan" ]; then
    k2=$(curl -sI -o /dev/null -w '%{http_code}' "$tujuan")
    [ "$k2" = "301" ] && echo "RANTAI: $jalur → $tujuan → …"
  fi
done < /tmp/url-semua.txt

echo "Gagal: $gagal"
exit $((gagal > 0))

Setelah peluncuran

WaktuYang dilakukan
Hari 0Kirim sitemap baru di Search Console; pertahankan yang lama sementara
Minggu 1Pantau laporan Pages harian; perbaiki 404 yang muncul
Minggu 2–4Trafik biasanya turun sementara lalu pulih — ini normal
Bulan 2Kueri peta_url untuk baris ber-hit nol; itu URL yang tidak pernah dipakai
Bulan 6Redirect tetap dipertahankan — jangan dihapus

Jangan pernah menghapus redirect. Biayanya satu baris database dan satu kueri ter-cache; keuntungannya adalah tautan dari artikel lain, dari media sosial, dan dari arsip yang dibuat bertahun-tahun lalu tetap hidup. Portal berita hidup dari tautan masuk, dan tautan yang mati tidak pernah kembali.

Latihan: kumpulkan URL dari keempat sumber untuk portalmu dan gabungkan jadi satu daftar unik. Hitung jumlahnya — angka itu menentukan apakah peta di database benar-benar perlu atau redirects di konfigurasi sudah cukup. Bangun tabel peta_url, isi dari database, dan jalankan skrip verifikasi terhadap 500 URL paling sering dikunjungi dari log-mu.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.