Next.js SEO: kako da aplikaciju učinite prohodnom i indeksabilnom
Napravite React aplikaciju, deplojujete je i gledate kako analitika stoji u
mestu. Google-ovi crawleri dođu do vaših stranica, vide prazan
<div id="root"></div> i idu dalje. Vaš sadržaj nikad ne bude indeksiran.
Next.js to rešava tako što renderuje kompletan HTML na serveru pre nego što išta stigne do pregledača. Ali samo renderovanje na serveru nije dovoljno - trebaju vam ispravni metapodaci, strukturirani podaci i brzina da biste stvarno rangirali.
Prvo: proverite šta crawler dobije
Pre nego što išta menjate, pogledajte odgovor servera, a ne renderovan DOM:
curl -s https://vas-sajt.rs/proizvod/x | grep -c "opis proizvoda"
Ako vašeg sadržaja nema u tom ispisu, vaše indeksiranje zavisi od toga da li Google renderuje vaš JavaScript - a renderuje, ali sa odlaganjem i bez garancije. To je jedina dijagnoza koja se računa, i većina je preskoči.
Strategije renderovanja
SSR - renderuje se na serveru pri svakom zahtevu. Za stranice koje izgledaju drugačije po korisniku ili po minutu: rezultati pretrage, korpa, stanje naloga.
SSG - napravljeno u trenutku build-a. Najbrže moguće, i tačan izbor za sve što se retko menja: tekstovi na blogu, landing stranice, dokumentacija.
ISR - napravljeno statički, pa se u pozadini obnavlja po isteku. Kompromis za velike kataloge proizvoda koji se menjaju dnevno, ali ne po zahtevu.
Na klijentu - za SEO mrtvo. Koristite iza login-a, gde indeksiranje ionako nije poželjno.
Pravilo: statički gde može, ISR gde statički ne stiže, SSR samo kad je sadržaj stvarno drugačiji pri svakom zahtevu.
Metadata API
U App Router-u vam ne treba dodatni paket. Metapodaci se izvoze po ruti:
export const metadata: Metadata = {
metadataBase: new URL('https://vas-sajt.rs'),
title: 'Naslov stranice',
description: 'Opis koji poziva na klik.',
alternates: { canonical: '/proizvod/x' },
openGraph: {
type: 'website',
url: 'https://vas-sajt.rs/proizvod/x',
images: [{ url: '/og/proizvod-x.png', width: 1200, height: 630 }],
},
}
Za dinamičke rute generateMetadata, koji sme da povuče iste podatke kao i
stranica - Next će poziv deduplicirati:
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params
const proizvod = await getProizvod(slug)
if (!proizvod) return {}
return {
title: `Kupite ${proizvod.naziv}`,
description: proizvod.kratakOpis,
alternates: { canonical: `/proizvod/${slug}` },
}
}
Obratite pažnju: u Next-u 15 i novijem params je Promise i mora da se
sačeka.
Da li vam još treba next-seo?
Ukratko: u App Router-u ne. next-seo je pravljen za Pages Router, gde su
metapodaci išli kroz next/head. Metadata API nativno pokriva title,
description, canonical, OpenGraph, Twitter kartice i alternates.languages za
hreflang.
Ako ste još na Pages Router-u, next-seo i dalje ima smisla. Pri migraciji ga
izbacujete zajedno sa next/head.
Sitemap i robots
Oba kao konvencije fajlova, ne kao API rute:
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const proizvodi = await getSveProizvode()
return proizvodi.map((p) => ({
url: `https://vas-sajt.rs/proizvod/${p.slug}`,
lastModified: p.izmenjeno,
}))
}
Napomena koja se tiče više sajtova nego što se misli: nemojte stavljati
lastModified: new Date(). Tada svaki URL pri svakom pozivu tvrdi da je
promenjen danas, pa Google nauči da vam lastmod uopšte ignoriše. Izostavite
polje ako nemate pravi datum.
Strukturirani podaci
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
JSON.stringify je ovde bitan. Ručno sklopljen JSON string puca na prvom
apostrofu u opisu, a ako slučajno ubacite React element, ispisaće
[object Object] - tiho, i zato ostane pokvareno.
Core Web Vitals
LCP - next/image sa priority za sliku iznad pregiba, fontovi preko
next/font na sopstvenom serveru umesto sa tuđih.
CLS - uvek zadajte širinu i visinu, ostavite mesto za banere, bez pomeranja layouta zbog fontova koji stižu kasnije.
INP - držite klijentske bundle-ove malim. Proverite koliko vaših komponenti
nosi 'use client' a da im ne treba; u App Router projektima to je najčešći
razlog za previše JavaScript-a.
Kratka čeklista
- Sadržaj je u odgovoru servera, provereno sa
curl - Canonical na svakoj ruti
metadataBasepostavljen, inače su OG URL-ovi relativni- Sitemap bez izmišljenog
lastmod - JSON-LD kroz
JSON.stringify, provereno Rich Results testom hreflangsamo za jezike koje stranica stvarno ima- Slike kroz
next/image, fontovi kroznext/font
Ako imate Next.js sajt koji se obilazi ali se ne indeksira - to je dijagnoza koju radim za agencije i produkt timove, zajedno sa izvedbom posle nje.