Slik ser KI siden din: innhold crawleren ikke finner
En produktside i en nettbutikk: navn og pris over bildet, under det tre faner – «Beskrivelse», «Spesifikasjoner», «Levering» – og lenger ned en seksjon med anmeldelser som lastes når brukeren scroller dit. Beskrivelsen er på to tusen ord, spesifikasjonene har tretti parametere, og det finnes to hundre anmeldelser. Alt som trengs for å svare på spørsmålet «passer denne lampen for et lite kontor?».
Noen stiller akkurat dette spørsmålet til en KI-assistent med nettsøk. Svaret siterer siden din – og bygger bare på to setninger fra meta description og prisen. Ikke et ord om mål, lysstyrke eller anmeldelser, selv om de står på siden.
Den første mistanken faller som regel på CSS: «fanene har display: none, så boten ser dem ikke». Det er nesten aldri årsaken. For å forstå hvor innholdet egentlig forsvinner, må du vite hva en crawler faktisk gjør med en side.
En crawler ser ikke på siden, den leser HTML
En nettleser laster ned HTML, så CSS og JavaScript, bygger DOM-treet, kjører skript, beregner layout og tegner piksler. Brukeren ser resultatet av hele denne prosessen. En crawler gjør som regel bare det første steget: den laster ned HTML-en og henter ut teksten.
Googlebot er unntaket – den har i årevis rendret sider i en nettleser, JavaScript inkludert, om enn med forsinkelse. Men crawlerne som mater KI-assistenter og søket deres, gjør det for det meste ikke. Analyser av trafikk fra slike boter, publisert blant annet av Vercel, viste at de laster ned JavaScript-filer, men ikke kjører dem. I praksis betyr det én enkel regel som alt annet avhenger av: en KI-crawler ser det som står i HTML-en serveren sender – og ingenting annet.
To ting følger av denne regelen samtidig. Innhold som er skjult med stiler, men finnes i HTML-en, er like synlig for crawleren som alt annet. Og innhold som først oppstår i nettleseren – etter et klikk, etter scrolling, etter at et skript har kjørt – finnes ikke for crawleren.
Fellen: display: none er ikke problemet
En crawler som ikke beregner layout, vet ikke engang at en CSS-regel skjuler et element. For den er en fane med display: none, hidden eller visibility: hidden bare enda en tekstbit i HTML-en. Søkemotorer vekter kanskje slikt innhold annerledes, men de ser det.
Det egentlige problemet ligger et steg tidligere – i om det skjulte innholdet i det hele tatt havnet i HTML-en. Og her er det ikke CSS som avgjør, men hvordan komponenten skjuler det. Sammenlign to implementasjoner av de samme fanene i React:
tsx
// «Betingede» faner: bare det aktive panelet er i HTML-enfunction ProductTabs({ tabs }: { tabs: Tab[] }) {const [active, setActive] = useState(tabs[0].id)const current = tabs.find((t) => t.id === active)!return (<div><TabList tabs={tabs} active={active} onChange={setActive} /><div role="tabpanel">{current.content}</div></div>)}
Det ser uskyldig ut og fungerer fint for brukeren. Men serveren rendrer bare den første fanen. Spesifikasjoner og leveringsinformasjon lages i nettleseren først etter et klikk – og en crawler klikker aldri. For den har produktsiden en beskrivelse og ingenting mer.
tsx
// Faner skjult med attributt: alle paneler er i HTML-enfunction ProductTabs({ tabs }: { tabs: Tab[] }) {const [active, setActive] = useState(tabs[0].id)return (<div><TabList tabs={tabs} active={active} onChange={setActive} />{tabs.map((t) => (<divkey={t.id}role="tabpanel"id={"panel-" + t.id}aria-labelledby={"tab-" + t.id}hidden={t.id !== active}>{t.content}</div>))}</div>)}
De samme fanene for brukeren, samme tilstand, samme klikkhåndtering. Det er bare én forskjell: alle paneler rendres, og de inaktive skjules av attributtet hidden. Serveren sender hele beskrivelsen, alle spesifikasjonene og leveringsinformasjonen i HTML-en. Crawleren leser alt.
Det samme gjelder trekkspill i FAQ, seksjoner som «Mer informasjon» og alle komponenter som viser innhold på forespørsel. Regelen er én: skjul med stil eller attributt, ikke med en betingelse. {open && <Panel />} fjerner innholdet fra HTML-en, hidden={!open} bare skjuler det.
hidden="until-found": skjult, men mulig å finne
Det finnes en variant som går et steg lenger. I artikkelen om content-visibility beskrev jeg hidden="until-found" – et attributt som skjuler et element slik hidden gjør, men som lar nettleseren avsløre det når brukeren søker etter teksten med Ctrl+F eller kommer dit via en lenke med tekstfragment.
html
<div role="tabpanel" id="panel-spec" hidden="until-found"><table>…</table></div>
For en crawler er det nøyaktig det samme som vanlig hidden – teksten er i HTML-en. For brukeren løser det et problem faner alltid har hatt: søket i nettleseren finner ikke ordet «vekt», fordi spesifikasjonstabellen ligger i en fane ingen har åpnet. Med until-found avslører nettleseren panelet selv og sender hendelsen beforematch på elementet, der du kan bytte aktiv fane så grensesnittet henger sammen. En nettleser som ikke kjenner denne verdien, behandler den som vanlig hidden – så du taper ingenting.
Den samme effekten får du gratis med nativ <details>: innholdet i et lukket element står i HTML-en, og nyere nettlesere avslører det ved søk.
Tekst som ikke finnes i HTML-en: CSS-content og bilder
Den andre kategorien er innhold som er synlig på siden, men som slett ikke er tekst i dokumentet. Det mest lumske tilfellet er content i pseudoelementer:
css
.price::after {content: " inkl. mva.";}.product-card--new::before {content: "Nyhet";}.shipping-info::before {content: "Gratis frakt over 500 kr";}
Brukeren ser «Gratis frakt over 500 kr». Crawleren gjør det ikke, fordi teksten ikke står i HTML-en – den finnes bare i stilarket, og crawleren bruker ikke CSS. Den finnes heller ikke i textContent, kan ikke kopieres, og et utdrag av siden limt inn i en assistent inneholder den ikke. Pseudoelementer er fine til dekorasjon – ikoner, piler, anførselstegn. All informasjon som noen kunne sitere, bør være ekte tekst i HTML-en.
Den samme regelen gjelder tekst i bilder. Et banner med «−30 % på hele kolleksjonen til søndag» eksportert som én JPG er for en crawler bare alt-teksten – hvis den finnes. Det er bedre å gjøre det samme med ekte tekst lagt over bildet med CSS: position: absolute på teksten, object-fit: cover på bildet og en gradient under for kontrast. Det ser likt ut, skalerer med skjermen, kan oversettes – og kan leses.
Innhold som lastes senere: anmeldelser, «vis mer», uendelig scrolling
Den tredje kategorien er innhold som først kommer på siden etter en handling fra brukeren eller etter scrolling. I netthandel er det oftest anmeldelser – og det er et dobbelt tap, for anmeldelser er nettopp det folk spør KI-assistenter om: «er dette produktet bra?», «hva klager kjøperne på?».
tsx
// Anmeldelser som hentes når seksjonen kommer inn på skjermenfunction Reviews({ productId }: { productId: string }) {const [reviews, setReviews] = useState<Review[]>([])const ref = useInView(() => {fetch("/api/reviews/" + productId).then((r) => r.json()).then(setReviews)})return <section ref={ref}>{reviews.map(renderReview)}</section>}
For ytelsen er det fornuftig: du henter ikke to hundre anmeldelser før noen scroller dit. Men en crawler scroller ikke, så for den er anmeldelsesseksjonen tom. Kompromisset som som regel fungerer best: de første anmeldelsene og et sammendrag (gjennomsnittlig vurdering, antall anmeldelser, vanligste fordeler og ulemper) i HTML-en fra serveren, resten lastet ved scrolling eller etter klikk på «Vis mer». For brukeren er forskjellen umerkelig, for crawleren er det forskjellen mellom «ingen anmeldelser» og konkrete argumenter.
Det samme gjelder forkorting av tekst. En beskrivelse forkortet med stiler – line-clamp, max-height med gradient og en «Les mer»-knapp – står i sin helhet i HTML-en. En beskrivelse forkortet i koden – description.slice(0, 300) med resten hentet ved klikk – har tre hundre tegn i HTML-en.
css
/* Hele beskrivelsen er i HTML-en, de første 4 linjene er synlige */.description:not(.is-expanded) {display: -webkit-box;-webkit-line-clamp: 4;-webkit-box-orient: vertical;overflow: hidden;}
Next.js: hva som havner i HTML-en, og hva som ikke gjør det
I Next.js med App Router får du mye av dette gratis, men ikke alt. Fire tilfeller er verdt å kjenne:
- Server Components rendres på serveren, og resultatet havner i HTML-en. Data hentet i en Server Component – produktbeskrivelse, spesifikasjoner, de første anmeldelsene – er synlige for alle crawlere.
- Client Components (
"use client") rendres også på serveren ved første forespørsel. Det er en vanlig misforståelse:"use client"betyr ikke «bare i nettleseren». Fanene fra eksempelet over kan være en klientkomponent og likevel sende alle panelene i HTML-en. - Data hentet i
useEffect, eller av et bibliotek som bare kjører i nettleseren, havner ikke i HTML-en – effekter kjører ikke på serveren. Det samme gjelder komponenter lastet meddynamic(..., { ssr: false }). - Streaming med Suspense – beskrevet i artikkelen om
use()og Suspense – sender innhold i den samme HTTP-responsen, bare senere. En crawler som laster ned hele responsen, får også det strømmede innholdet, selv om det står på slutten av dokumentet og flyttes på plass av et lite skript. Teksten er i HTML-en, så crawleren leser den.
Det tryggeste mønsteret for en produktside: data hentet i Server Components, interaktivitet i små klientkomponenter som får innholdet som props eller children i stedet for å hente det selv.
Slik sjekker du hva crawleren ser
Du trenger ikke gjette. Den enkleste testen er å laste ned siden uten nettleser og søke etter en bestemt frase fra det skjulte innholdet:
bash
# Står en frase fra spesifikasjonene i HTML-en serveren sender?curl -s https://example.com/produkt/kontorlampe | grep -c "Lysstrøm"# Det samme med User-Agent-headeren til en av botenecurl -s -A "GPTBot" https://example.com/produkt/kontorlampe | grep -c "Lysstrøm"
0 betyr at crawleren ikke vil se frasen. Den andre måten krever ingen terminal: slå av JavaScript i devtools (i Chrome: kommandopaletten, «Disable JavaScript») og last siden på nytt. Det du da ser – eller ikke ser – er svært nær det crawleren får. Den tredje: «Vis sidekilde» i stedet for fanen Elements. Elements viser DOM-en etter at skriptene har kjørt, kilden viser HTML-en serveren sendte.
Feller og god praksis
- Skjuling med stiler skader ikke, fravær i HTML-en gjør det.
display: noneoghiddenbeholder teksten i dokumentet.{open && <Panel />}fjerner den fra HTML-en serveren sender. - Rendre faner og trekkspill i sin helhet. Skjul inaktive paneler med attributtet
hidden, eller medhidden="until-found", som i tillegg lar brukere finne innholdet med Ctrl+F. - Informasjon er tekst, ikke CSS-
contenteller et bilde. La pseudoelementer være til dekorasjon, og legg bannertekst over bildet med CSS. - De viktigste anmeldelsene og et sammendrag i HTML-en fra serveren. Lasting ved scrolling er fint for resten, men en crawler scroller aldri.
- Forkort tekst med stiler, ikke i koden.
line-clampbeholder hele beskrivelsen i HTML-en,slice()gjør det ikke. "use client"utelukker ikke SSR, menuseEffectogssr: falsegjør det. Hent innhold i Server Components og send det videre til interaktive komponenter.- Test med
curleller med JavaScript slått av. Fanen Elements i devtools viser siden etter skriptene, crawleren ser den før dem.
Oppsummering
Crawlerne som mater KI-assistenter, kjører for det meste ikke JavaScript og bruker ikke CSS – de leser HTML-en serveren sender, og henter ut teksten. Derfor er skjuling med stiler, som vi bekymrer oss så mye for, nesten aldri problemet: en fane med display: none er like lesbar for en crawler som resten av siden. Problemet er innhold som ikke finnes i denne HTML-en – paneler rendret betinget, anmeldelser hentet ved scrolling, beskrivelser forkortet i koden, informasjon satt inn med CSS-content eller brent inn i et bilde. Løsningene er enkle og forbedrer som regel også tilgjengelighet og søk i nettleseren: rendre alt og skjul med et attributt, hent det viktigste innholdet på serveren, skriv informasjon som tekst. Og når du er usikker, forteller curl og én enkelt grep deg på et sekund hva en crawler ser av siden din.