SEO-First Next.js: Checklist Lengkap untuk 2026

Foto oleh Unsplash

Foto oleh Unsplash
Saya sudah mengaudit banyak situs Next.js dengan Lighthouse dan kesalahan SEO yang sama muncul di 80% dari mereka: canonical URL yang hilang, tidak ada OG image, sitemap yang menunjuk ke localhost, dan gambar LCP yang tidak di-preload. Checklist ini adalah semua yang saya periksa sebelum menganggap situs App Router siap SEO. Ini bukan teoritis — ini yang saya terapkan pada situs portfolio saya sendiri untuk mendapatkan skor Lighthouse 90+ yang konsisten.
Pikirkan SEO Next.js sebagai lima layer yang ditumpuk: (1) Metadata API — judul, deskripsi, canonical, dan tag sosial dasar. (2) Structured Data (JSON-LD) — konten yang dapat dibaca mesin untuk rich result. (3) Core Web Vitals — sinyal performa yang digunakan Google sebagai faktor peringkat. (4) Sitemap dan robots.txt — panduan crawl. (5) Optimasi gambar — baik untuk performa maupun berbagi sosial.
Next.js App Router memiliki tiga cara untuk mengatur metadata. Yang paling sederhana adalah mengekspor objek metadata dari halaman — bagus untuk halaman statis seperti About atau Contact. Untuk halaman dinamis (posting blog, halaman produk), gunakan generateMetadata() yang menerima parameter route dan dapat mengambil data. Untuk root layout, atur metadata default yang diwarisi semua halaman anak.
Jika situsmu multibahasa (milik saya adalah Inggris dan Indonesia), kamu memerlukan canonical dan alternates.languages di metadata setiap halaman. Canonical menunjuk ke URL locale saat ini. Objek alternates.languages memetakan kode locale ke URL lengkap. Ini memberi tahu Google versi bahasa mana yang ditunjukkan kepada pengguna mana, mencegah penalti konten duplikat.
Next.js SEO Checklist — Signal Flow
┌─────────────────────────────────────────────┐
│ 1. Metadata API (app/layout.tsx) │
│ title · description · openGraph │
│ twitter · robots · canonical │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 2. Structured Data (JSON-LD) │
│ Article · BreadcrumbList │
│ Person · WebSite · FAQPage │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 3. Core Web Vitals │
│ LCP < 2.5s · FID < 100ms · CLS < 0.1 │
│ TTFB < 600ms · INP < 200ms │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 4. Sitemap + robots.txt │
│ /sitemap.xml (dynamic) · /robots.txt │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 5. Image Optimization │
│ next/image · WebP/AVIF · alt text │
│ priority on LCP image │
└─────────────────────────────────────────────┘Gunakan generateStaticParams() untuk semua route posting blog untuk memaksa static generation. Halaman yang dihasilkan secara statis memiliki TTFB hampir nol dari edge cache Vercel, yang merupakan keuntungan terbesar untuk Core Web Vitals dan SEO. Halaman posting blog yang dirender secara dinamis pada 800ms TTFB versus yang statis pada 40ms adalah perbedaan yang bermakna dalam LCP.
JSON-LD structured data mengubah kontenmu menjadi sinyal yang dapat dibaca mesin yang digunakan Google untuk rich result — byline artikel, dropdown FAQ, dan breadcrumb yang kamu lihat di hasil pencarian. Untuk blog, implementasikan skema Article di setiap posting dan BreadcrumbList di halaman listing. Render JSON-LD sebagai tag script di komponen atau layout halaman-mu.
File app/sitemap.ts di Next.js App Router mengembalikan array MetadataRoute.Sitemap yang disajikan Next.js sebagai /sitemap.xml. Untuk situs bilingual, sertakan kedua varian locale dari setiap URL. Atur lastModified dari tanggal updatedAt konten aktual, bukan nilai yang dikodekan keras. Atur changeFrequency dan priority dengan tepat.
// app/[locale]/blog/[slug]/page.tsx
import type { Metadata } from "next"
import { getTranslations } from "next-intl/server"
type Props = { params: { slug: string; locale: string } }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug, locale } = params
const post = await getBlogPost(slug)
return {
title: post.title,
description: post.excerpt,
alternates: {
canonical: `https://www.matthewswong.com/${locale}/blog/${slug}`,
languages: {
"en": `https://www.matthewswong.com/en/blog/${slug}`,
"id": `https://www.matthewswong.com/id/blog/${slug}`,
},
},
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://www.matthewswong.com/${locale}/blog/${slug}`,
siteName: "Matthews Wong",
images: [{ url: post.image, width: 1200, height: 630 }],
type: "article",
publishedTime: post.datePublished,
authors: ["Matthews Wong"],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.image],
},
}
}
// JSON-LD structured data component
export function ArticleJsonLd({ post }: { post: BlogPost }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.image,
author: {
"@type": "Person",
name: "Matthews Wong",
url: "https://www.matthewswong.com",
},
publisher: {
"@type": "Person",
name: "Matthews Wong",
},
datePublished: post.datePublished,
dateModified: post.dateModified,
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://www.matthewswong.com/blog/${post.slug}`,
},
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
// app/sitemap.ts — dynamic sitemap
import { MetadataRoute } from "next"
import { blogPosts } from "@/app/lib/blogData"
export default function sitemap(): MetadataRoute.Sitemap {
const blogRoutes = blogPosts.flatMap((post) =>
["en", "id"].map((locale) => ({
url: `https://www.matthewswong.com/${locale}/blog/${post.slug}`,
lastModified: new Date(post.datePublished),
changeFrequency: "monthly" as const,
priority: 0.7,
}))
)
return [
{ url: "https://www.matthewswong.com", priority: 1.0 },
{ url: "https://www.matthewswong.com/en/blog", priority: 0.8 },
...blogRoutes,
]
}LCP (Largest Contentful Paint) hampir selalu adalah gambar hero-mu. Tambahkan priority ke komponen next/image untuk gambar di atas lipatan di setiap halaman — ini memicu tag preload link. Untuk CLS (Cumulative Layout Shift), selalu tentukan width dan height pada gambar dan gunakan CSS aspect-ratio untuk elemen berukuran dinamis. Untuk INP, hindari task JavaScript yang lama di main thread.
Mengatur document.title atau menyuntikkan meta tag dari useEffect tidak terlihat oleh crawler. Metadata API Next.js menghasilkan tag ini di sisi server dalam HTML head, yang dibaca oleh bot mesin pencari. Jika kamu melihat judul yang hilang di Search Console untuk halaman yang terlihat benar di browser, periksa apakah metadata diatur melalui Next.js API atau JavaScript sisi klien.
Setiap gambar memerlukan atribut alt yang deskriptif — bukan 'gambar' atau nama file. Gunakan komponen next/image untuk semua gambar konten untuk mendapatkan penyajian WebP/AVIF otomatis dan lazy loading. Untuk OG image (gambar pratinjau saat berbagi di LinkedIn atau X), gunakan library next/og Next.js untuk membuatnya secara dinamis. OG image harus berukuran 1200x630px.
Jalankan Lighthouse dalam mode Incognito (untuk menghindari ekstensi yang mempengaruhi hasil). Gunakan Rich Results Test Google untuk memverifikasi JSON-LD. Kirimkan sitemap di Search Console dan tunggu laporan cakupan. Gunakan alat URL Inspection di Search Console untuk melihat halaman seperti yang dilihat Googlebot — ini mengungkap masalah rendering JS.