Nikola Arsić

Next.js SEO: kako da aplikaciju učinite prohodnom i indeksabilnom

Nikola ArsićNext.js
Next.js SEO

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
  • metadataBase postavljen, inače su OG URL-ovi relativni
  • Sitemap bez izmišljenog lastmod
  • JSON-LD kroz JSON.stringify, provereno Rich Results testom
  • hreflang samo za jezike koje stranica stvarno ima
  • Slike kroz next/image, fontovi kroz next/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.