SEO portal berita saat migrasi
Portal berita punya aturan SEO sendiri yang tidak berlaku di situs lain — dan migrasi adalah momen paling rawan untuk melanggarnya.
Intisari
- Sitemap Google News hanya memuat artikel 2 hari terakhir — maksimum 1.000 URL.
- URL kanonik absolut di tiap halaman. Ini yang mencegah parameter pelacakan jadi duplikat.
NewsArticleJSON-LD denganisAccessibleForFreeuntuk artikel berbayar (Fase 5).datePublisheddandateModifiedharus akurat dan ber-zona waktu.- Jangan mengubah bentuk URL dan platform sekaligus kalau bisa dihindari — kalau harus, lakukan bersamaan sekali, bukan dua kali.
Kanonik yang benar
---
// src/layouts/Dasar.astro
const kanonik = new URL(Astro.url.pathname, Astro.site).href;
---
<link rel="canonical" href={kanonik} />
| Yang dicegah kanonik | Contoh |
|---|---|
| Parameter pelacakan | ?utm_source=facebook |
| Variasi trailing slash | /berita/halo/ vs /berita/halo |
| Beda protokol atau host | www. vs tanpa www. |
| Halaman yang bisa dicapai lewat dua jalur | Artikel di dua kategori |
Perhatikan bahwa kanonik dihitung dari pathname, bukan dari Astro.url
utuh. Kalau kamu memakai URL lengkap, parameter pelacakan ikut masuk ke kanonik — dan tiap tautan
yang dibagikan di media sosial jadi URL kanonik tersendiri, yang justru kebalikan dari tujuannya.
Ingat juga peringatan dari Fase 1: di dalam server island, Astro.url bernilai
/_server-islands/…. Layout yang menghitung kanonik tidak boleh dipakai di dalam island.
Sitemap berita
// src/pages/sitemap-berita.xml.ts
export const prerender = false;
export const GET: APIRoute = async ({ site }) => {
// Google News: hanya 2 hari terakhir, maksimum 1.000 URL.
const artikel = await dbBaca
.selectFrom("artikel")
.select(["slug", "judul", "terbit_pada"])
.where("status", "=", "terbit")
.where("terbit_pada", ">=", new Date(Date.now() - 2 * 86400000))
.where("terbit_pada", "<=", new Date())
.orderBy("terbit_pada", "desc")
.limit(1000)
.execute();
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
${artikel.map((a) => ` <url>
<loc>${new URL(`/berita/${a.slug}`, site).href}</loc>
<news:news>
<news:publication>
<news:name>Portal Contoh</news:name>
<news:language>id</news:language>
</news:publication>
<news:publication_date>${a.terbit_pada!.toISOString()}</news:publication_date>
<news:title>${escXml(a.judul)}</news:title>
</news:news>
</url>`).join("\n")}
</urlset>`;
return new Response(xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "public, max-age=0, s-maxage=300",
},
});
};
Batas dua hari dan seribu URL bukan saran. Sitemap berita yang memuat artikel lebih lama akan diabaikan atau memicu peringatan di Search Console. Arsip lengkap masuk ke sitemap biasa yang terpisah, dipecah maksimal 50.000 URL per berkas dengan indeks sitemap di atasnya.
Sitemap arsip
// src/pages/sitemap-[hal].xml.ts — 50.000 URL per berkas
const PER_HAL = 50_000;
export const GET: APIRoute = async ({ params, site }) => {
const hal = Number(params.hal);
if (!Number.isInteger(hal) || hal < 1) return new Response(null, { status: 404 });
const artikel = await dbBaca
.selectFrom("artikel")
.select(["slug", "diubah_pada", "terbit_pada"])
.where("status", "=", "terbit")
.orderBy("id", "asc")
.limit(PER_HAL)
.offset((hal - 1) * PER_HAL)
.execute();
if (artikel.length === 0) return new Response(null, { status: 404 });
// … susun XML
};
Data terstruktur
const jsonLd = {
"@context": "https://schema.org",
"@type": "NewsArticle",
mainEntityOfPage: { "@type": "WebPage", "@id": kanonik },
headline: artikel.judul.slice(0, 110), // Google memotong di sekitar 110
description: artikel.ringkasan ?? undefined,
image: artikel.gambarSampul ? [artikel.gambarSampul] : undefined,
datePublished: artikel.terbitPada.toISOString(),
dateModified: (artikel.diubahPada ?? artikel.terbitPada).toISOString(),
author: [{ "@type": "Person", name: artikel.penulis, url: `${situs}/penulis/${artikel.penulisSlug}` }],
publisher: {
"@type": "Organization",
name: "Portal Contoh",
logo: { "@type": "ImageObject", url: `${situs}/logo-600x60.png`, width: 600, height: 60 },
},
isAccessibleForFree: !artikel.premium,
...(artikel.premium && {
hasPart: {
"@type": "WebPageElement",
isAccessibleForFree: false,
cssSelector: ".badan-berbayar",
},
}),
};
| Kesalahan umum | Akibatnya |
|---|---|
dateModified berubah tiap render | Google mengira artikel terus disunting; kepercayaannya turun |
| Tanggal tanpa zona waktu | Ditafsirkan salah; artikel terlihat terbit di masa depan |
headline lebih dari 110 karakter | Dianggap tidak valid |
cssSelector menunjuk elemen yang tidak ada | Paywall tidak dikenali; risiko dianggap cloaking |
| Logo publisher bukan rasio yang didukung | Peringatan di Search Console |
Baris pertama itu jebakan yang halus. Kalau kamu menuliskan dateModified dari
new Date() — atau dari kolom yang ikut berubah saat penghitung hits di-update — maka setiap
perayapan melihat tanggal yang berbeda. Ambil dari kolom yang hanya berubah saat editor benar-benar
menyunting isinya.
Tiga hal khusus Google News
| Hal | Aturannya |
|---|---|
| Kesegaran | Artikel harus muncul di sitemap berita dalam hitungan menit setelah terbit |
| URL stabil | Slug tidak boleh berubah setelah terbit — kalau harus, 301 dan pertahankan selamanya |
| Isi lengkap untuk crawler | Isi berbayar tetap boleh diindeks asal ditandai dengan benar (Fase 5) |
Yang tidak boleh diindeks
---
// Halaman hasil pencarian, paginasi dalam, filter
const noindex = Astro.url.searchParams.has("q") || hal > 20;
---
{noindex && <meta name="robots" content="noindex, follow" />}
follow-nya penting: kamu tidak ingin halaman itu terindeks, tapi kamu ingin Google mengikuti
tautan di dalamnya untuk menemukan artikelmu. noindex, nofollow memutus jalur penemuan itu.
Pantau selama migrasi
| Metrik | Di mana | Ekspektasi normal |
|---|---|---|
| Halaman terindeks | Search Console → Pages | Turun sedikit lalu pulih dalam 2–4 minggu |
| Error perayapan | Search Console | Tidak boleh naik |
| Klik & tayangan | Search Console → Performance | Turun 5–15% sementara, lalu pulih |
| Core Web Vitals | Search Console | Harus membaik — kalau tidak, ada yang salah |
| Item Google News | Publisher Center | Tidak boleh turun |
Penurunan sementara adalah normal; penurunan yang tidak pulih setelah enam minggu adalah masalah. Kalau itu terjadi, tersangka pertama selalu sama: redirect yang terlewat, kanonik yang salah, atau bentuk URL yang berubah tanpa peta lengkap. Semuanya bisa diperbaiki — tapi lebih cepat ditemukan lebih baik.
Latihan: bangun sitemap berita dan sitemap arsip berindeks. Validasi keduanya dengan Search
Console. Lalu jalankan Rich Results Test pada satu artikel gratis dan satu artikel premium, dan
pastikan isAccessibleForFree serta hasPart terbaca dengan benar. Terakhir,
periksa bahwa cssSelector yang kamu tulis benar-benar cocok dengan elemen di HTML yang
dilihat pembaca anonim.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.