20. modulÉles üzem: SEO, i18n, cache, hibakövetés
Nuxt for Devs · 20. modul

Éles üzem: SEO, i18n, cache, hibakövetés

Az app működik. Innentől négy olyan kérdés marad, amit a fejlesztés közben senki nem tesz fel — és mind a négyre a kiadás előtt kell válaszolni, mert utólag javítani mindegyiket drágább. Ez a modul végigmegy rajtuk, Workers-specifikus buktatókkal, és egy induláskori ellenőrzőlistával zár.

20.1A négy kérdés

SEO — „megtalálnak?” canonical · sitemap · robots · strukturált adat · OG-kép Workers-vetület: az OG-kép WASM-mal fut, és bundle-t eszik i18n — „értik?” útvonal-stratégia · hreflang · fordítások betöltése Workers-vetület: minden nyelv a bundle-ben van, ha nem lusta Cache — „gyors és olcsó?” routeRules · HTTP-fejlécek · Cache API Workers-vetület: a rossz cache adatszivárgás, nem lassulás Hibakövetés — „megtudod?” Sentry · Workers Logs · source map · riasztás Workers-vetület: a szerveroldal dev módban nem is figyel
Mind a négy „később is ráérünk” kategóriába szokott esni. Kettő közülük — a cache és a hibakövetés — az indulás napján a legdrágább hiányzó dolog.

20.2SEO: a minimum, ami nélkül nem indulsz

A Nuxt SSR-je azért fontos SEO-szempontból, mert a keresőrobot a kezdeti HTML-t nézi. Minden, ami csak hidratálás után kerül a fejlécbe, valószínűleg nem számít. Ebből az első és legfontosabb szabály:

Soha ne onMounted()-ben állíts fejléc-metaadatot. Az onMounted a kliensen fut, jóval azután, hogy a szerver elküldte a választ — a robot által látott HTML-ben nem lesz benne. A meta-adatokat a setup-ban add meg, useHead() vagy useSeoMeta() hívással.
// pages/termek/[slug].vue
const { data: termek } = await useFetch(`/api/termek/${slug}`)

// kliensen is elérhető, reaktív
useSeoMeta({
  title: () => termek.value?.nev,
  description: () => termek.value?.leiras,
  ogTitle: () => termek.value?.nev,
  ogImage: () => termek.value?.kep,
  twitterCard: 'summary_large_image',
})
És a Workers-specifikus finomítás: ha a metaadat nem változik kliensoldali navigáció során, használd a useServerSeoMeta()-t. Az azonos API, de a kódja nem kerül bele a kliens-bundle-be — a robot úgyis csak a szerver által renderelt HTML-t nézi. A 15. modul bundle-logikája szerint ez ingyen nyereség: kevesebb JS a felhasználónak, ugyanaz az SEO-eredmény.

A canonical, amit mindenki elront

Egy Nuxt-app ugyanazt a tartalmat elérhetővé teszi /termek/bogre, /termek/bogre/, /termek/bogre?utm_source=hirlevel és — ha elfelejtetted a workers_dev: false-t (17.3) — appnev.subdomain.workers.dev/termek/bogre címen is. A keresőnek meg kell mondani, melyik az igazi.

// nuxt.config.ts — a Nuxt SEO csomag ebből dolgozik
export default defineNuxtConfig({
  site: {
    url: 'https://bolt.hu',
    name: 'Bolt',
  },
})

20.3A Nuxt SEO csomag

A @nuxtjs/seo egy gyűjtőmodul, ami hat kisebbet húz be. Nem kell mind — érdemes tudni, melyik mit ad, hogy dönthess (13. modul szűrője szerint):

AlmodulMit adKell?
@nuxtjs/robotsrobots.txt, meta robots, X-Robots-Tag fejlécigen — a staging kizárása is ez
@nuxtjs/sitemapsitemap.xml az útvonalaidból, i18n-támogatássaligen, ha nyilvános tartalom van
nuxt-schema-orgJSON-LD strukturált adat értelmes alapokkalwebshop/cikk esetén erősen
nuxt-seo-utilsfavicon-ok, alap meta-tagek, breadcrumbkényelmi
nuxt-link-checkertörött link keresése build-időbenCI-ben hasznos
nuxt-og-imagedinamikus Open Graph képeklásd a következő szakaszt
A robots-modul egy gyakran elfelejtett szolgálata: a staging-környezeted kizárása a keresőkből. Ha a staging.bolt.hu indexelődik, duplikált tartalmad lesz, és néha a staging fog megjelenni a találatok között. A 12. modul $env mechanizmusával ez két sor: nem-produkciós környezetben tiltasz mindent.

20.4OG-kép Workers-en — a WASM-realitás

A dinamikus Open Graph kép (a link megosztásakor megjelenő előnézet) látványos, és a nuxt-og-image Workers-en is működik — de nem ingyen, és érdemes tudatosan dönteni róla.

A képet nem böngésző rajzolja, hanem egy WASM-ra fordított renderelő: a Satori vagy az újabb Takumi. Ezek a bináris modulok bekerülnek a Workered bundle-jébe, ami a 15. modul limitjeit terheli. Emellett futásidőben betűtípusra van szükségük, amit az asset-kötésen keresztül olvasnak be:

// wrangler.jsonc — a fontok betöltéséhez kell az ASSETS binding
{
  "assets": {
    "directory": ".output/public",
    "binding": "ASSETS"
  }
}
// nuxt.config.ts — a WASM-változat kikényszerítése
defineNuxtConfig({
  ogImage: {
    compatibility: { runtime: { resvg: 'wasm' } },
  },
})
Itt kapott értelmet a 17.3-as binding: "ASSETS" sor, amit ott még „csak ha kódból is olvasnál assetet” megjegyzéssel írtam oda. Ez az egyik konkrét eset, amikor kell.
A józan döntés a legtöbb projektben: ha a megosztott oldalaid halmaza véges és ismert (blogcikkek, terméklapok), prerendereld az OG-képeket build-időben, és futásidőben ne generálj semmit. Így a WASM-renderelő nem kerül bele a Worker-bundle-be, nincs futásidejű CPU-költség, és a kép mint statikus asset ingyenes (17.1). Futásidejű generálásra csak akkor van szükség, ha a kép felhasználónként vagy valós idejű adatból változik — és akkor mérd meg, mit tesz a bundle-mérettel.

20.5i18n: a stratégia, ami később nem cserélhető

A @nuxtjs/i18n négy útvonal-stratégiát kínál, és ez az egyik olyan döntés, amit utólag változtatni fájdalmas: az URL-jeid megváltoznak, ami átirányítási táblát és elveszett keresőrangsort jelent.

StratégiaURL-ekSEOMikor
prefix/hu/rolunk, /en/abouta legtisztább nyelvjelzésha egyik nyelv sem „fő”
prefix_except_default/rolunk, /en/aboutjó, de gondos canonical kellha van egyértelmű fő piac
prefix_and_defaultmindkettő működikduplikált tartalom kockázataritkán indokolt
no_prefix/rolunk mindenkineka nyelv láthatatlan a keresőnekcsak belső appnál
// nuxt.config.ts
i18n: {
  locales: [
    { code: 'hu', language: 'hu-HU', file: 'hu.json' },
    { code: 'en', language: 'en-GB', file: 'en.json' },
  ],
  defaultLocale: 'hu',
  strategy: 'prefix_except_default',
  baseUrl: 'https://bolt.hu',      // a hreflang abszolút URL-ekhez kell
  lazy: true,                       // nyelvenként külön chunk
}

Két szabály a hreflangról

A modul automatikusan generálja a hreflang-tageket, ha a fenti konfiguráció megvan — de két dolgot érdemes érteni, mert ezeken bukik el a legtöbb többnyelvű oldal:

A Workers-vetület: a lazy: true nem opcionális kényelem, hanem bundle-kérdés. Enélkül minden nyelv minden fordítása benne van abban, amit a felhasználó letölt — öt nyelvnél ez már megérződik a 15. modul mérőszámain. Lusta betöltéssel nyelvenként külön chunk keletkezik, és a látogató csak a sajátját tölti le. A szerveroldalon ugyanez a Worker-bundle méretét érinti.

20.6Cache-stratégia három rétegben

A cache-elésnek három egymásra épülő rétege van egy Nuxt-appban Workers-en, és a leggyakoribb hiba az, hogy valaki egy rétegben gondolkodik, miközben egy másik dolgozik ellene.

RétegHol dől elHatóköre
routeRulesNuxt-konfig, build-időbenprerender, SWR/ISR, fejlécek — útvonalanként (7. modul)
② HTTP-fejléceka válaszon, futásidőbenböngésző-cache és a Cloudflare edge-cache
③ Cache APIkódban, explicitadatközpontonként külön (19.6)

A fejlécek: kinek szól melyik?

// böngészőnek 5 perc, edge-nek 1 óra
setResponseHeader(event, 'Cache-Control', 'public, max-age=300, s-maxage=3600')
DirektívaKinek szól
max-agea böngészőnek
s-maxagea megosztott cache-nek, vagyis a Cloudflare edge-nek
stale-while-revalidateszolgálj ki régit, közben frissíts a háttérben
stale-if-errorhiba esetén szolgálj ki régit
private, no-storesenki ne tárolja — bejelentkezett oldalnál ez a helyes
Egy nem nyilvánvaló kölcsönhatás: az s-maxage kikapcsolja a stale-while-revalidate-et, mert a szabvány szerint magában hordozza a „ne szolgálj ki elavultat” jelentést. Ha SWR-viselkedést szeretnél az edge-en, ne tedd mellé az s-maxage-et.

A legdrágább hiba az egész tanfolyamban

Bejelentkezett felhasználónak szánt SSR-oldalt soha ne cache-elj megosztott cache-ben. Ha egy hitelesített oldalra rákerül a public, s-maxage=…, a Cloudflare eltárolja az egyik felhasználó által látott HTML-t, és kiszolgálja a következőnek. Ez nem teljesítményhiba, hanem adatszivárgás — és az egyik legrosszabb, ami egy multitenant alkalmazással történhet. A helyes alapállás: minden hitelesített útvonalon Cache-Control: private, no-store, és a cache-elés az a kivétel, amit útvonalanként, tudatosan engedélyezel.

Van egy csendes védőháló is: a Cloudflare Cache API soha nem cache-el Set-Cookie fejlécet tartalmazó választ (19.6). Ez sok esetben megvéd — de ne erre építs: ha a session-süti már korábban ki lett adva, az adott válaszban nincs Set-Cookie, és a védőháló nem lép működésbe.

// server/middleware/no-cache-auth.ts — öv és nadrágtartó
export default defineEventHandler((event) => {
  const ut = getRequestURL(event).pathname
  if (ut.startsWith('/app') || ut.startsWith('/api/')) {
    setResponseHeader(event, 'Cache-Control', 'private, no-store')
  }
})
Emlékeztető a 17.2-ből: ha egy útvonal előrenderelt, akkor assetként szolgálódik ki, és ez a middleware nem fut le rajta. Vagyis a védelem és a prerender kizárják egymást — ami helyes is, hiszen előrenderelni csak azt szabad, ami mindenkinek ugyanaz.

20.7Hibakövetés: Sentry Workers-en

A 16. modul hibakeresésről szólt — arról, hogy te hogyan találod meg a hibát. Ez a szakasz arról szól, hogy hogyan tudod meg, hogy van. A Workers Logs (16.8) ehhez kevés: az kereshető napló, nem riasztó rendszer. Egy éles alkalmazáshoz kell valami, ami szól.

npm install @sentry/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@sentry/nuxt/module'],
  runtimeConfig: {
    public: { sentry: { dsn: process.env.NUXT_PUBLIC_SENTRY_DSN } },
  },
  sentry: {
    org: 'sajat-org',
    project: 'bolt',
    authToken: process.env.SENTRY_AUTH_TOKEN,
  },
  sourcemap: { client: 'hidden' },
})
// server/plugins/sentry.ts — a Cloudflare-specifikus rész
import { sentryCloudflareNitroPlugin } from '@sentry/nuxt/module/plugins'

export default defineNitroPlugin(sentryCloudflareNitroPlugin({
  dsn: process.env.NUXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
}))
// wrangler.jsonc — két dolog kell hozzá
{
  "compatibility_flags": ["nodejs_compat"],   // AsyncLocalStorage miatt
  "version_metadata": { "binding": "CF_VERSION_METADATA" }
}
Három csapda, amit érdemes előre tudni. (1) Ha van sentry.server.config.ts-ed egy korábbi (Node-os) beállításból, töröld — ütközik a Cloudflare-es felállással. (2) A szerveroldali monitorozás dev módban nem működik: build kell hozzá, hogy tesztelni tudd — vagyis a 18. modul dev:worker foka az a hely, ahol ellenőrizheted. (3) A version_metadata binding az, amitől a Sentry tudja, melyik verzióból jött a hiba — ez a 17.5-ös fokozatos kigördítésnél aranyat ér, mert azonnal látszik, hogy az új verzió termeli-e a hibákat.

A source map-lánc

Két, egymástól független source map-igényed van, és mindkettőt külön kell megoldani:

OldalKinek kellBeállítás
Kliensa Sentrynek, a böngészőben dobott hibákhozsourcemap: { client: 'hidden' } + Sentry-feltöltés
Szervera Cloudflare-nek, a Worker stack trace-eiheznitro.sourceMap: true + upload_source_maps (16.8)

A 'hidden' érték azt jelenti, hogy a source map elkészül és feltöltődik a Sentrybe, de a kiszolgált JS-fájlokban nincs rá hivatkozás — vagyis a látogató nem tudja visszafejteni a forrásodat, a hibakövetőd viszont igen.

20.8Megfigyelés: mit nézel, és milyen gyakran

Az eszközök megvannak; a kérdés, hogy mit kezdesz velük. Egy működő ritmus:

MikorMitMire figyelsz
Deploy után 15 percSentry hibaráta, Workers Logs --status errorúj hibatípus jelent-e meg
NapontaSentry új problémák, kérésszám és CPU-időtrendváltás, költségugrás
HetenteCore Web Vitals (RUM), lassú útvonalaklassú kúszás, amit egy deploy sem magyaráz
Havontabundle-méret és indulási idő trendjeközeledsz-e a 15. modul limitjeihez
Incidensbenwrangler tail --status error, verzió-összehasonlításmelyik verzió, melyik útvonal, mióta
Riasztásnál a hibakódra riasszunk, ne a szövegre. A 16.9-ből: a Nuxt 4.5 stabil hibakódjai (NUXT_E1001) verzióról verzióra ugyanazok maradnak, a bőbeszédű magyarázat viszont kikerül a production buildből. Egy szövegre illesztett riasztás a következő Nuxt-frissítésnél csendben elnémul — a kódra illesztett nem.

20.9Élesindulási ellenőrzőlista

Ez a lista a teljes tanfolyam zárókövei egy helyen. Nem mind vonatkozik minden projektre, de mindegyikre döntést kell hozni — nem elfelejteni.

Biztonság és adat

  1. Minden hitelesített útvonalon Cache-Control: private, no-store (20.6).
  2. Nincs modul-szintű mutable állapot; a felhasználói adat kérésenként keletkezik (6. modul).
  3. A tenant-szűrés kikényszerített, nem konvención alapul (11.7).
  4. A session-titok legalább 32 karakter, és minden környezetben más (10. modul).
  5. A runtimeConfig publikus ágában nincs olyan, aminek nem szabad kikerülnie (12. modul).
  6. Az API-hibák message-e nem szivárog a kliensre (16.9).

Deploy és üzemeltetés

  1. A build és a deploy egyetlen scriptben, hogy ne lehessen szétcsúsztatni (17.4).
  2. Minden binding minden környezetben szerepel — a bindingok nem öröklődnek (17.3).
  3. workers_dev: false, preview_urls: true (17.3).
  4. A migrációk bővítés-szűkítés mintát követnek, hogy a rollback működjön (17.10).
  5. Füstteszt fut a preview URL ellen a forgalomra állítás előtt (16.6).

Teljesítmény és költség

  1. A bundle mérete és az indulási idő mérve, CI-kapuval a limitek 80%-ánál (15. modul).
  2. Ami statikussá tehető, statikus — az ingyenes (17.1).
  3. A soha nem interaktív komponenseken hydrate-never (15.2).
  4. Egy SSR-oldal nem hív 6-nál több külső forrást párhuzamosan (19.5).

Találhatóság

  1. site.url beállítva; canonical minden oldalon önmagára mutat (20.2).
  2. robots.txt és sitemap a helyén; a staging kizárva az indexelésből (20.3).
  3. Többnyelvűségnél az útvonal-stratégia véglegesítve, hreflang oda-vissza mutat (20.5).
  4. A meta-adatok a setup-ban készülnek, nem onMounted-ben (20.2).

Megfigyelhetőség

  1. observability.enabled bekapcsolva, ésszerű head_sampling_rate-tel (16.8).
  2. Mindkét source map-lánc kész: kliens a Sentrynek, szerver a Cloudflare-nek (20.7).
  3. Sentry (vagy egyenértékű) beállítva, version_metadata bindinggel (20.7).
  4. Van legalább egy riasztás, ami valakinek tényleg szól — és a hibakódra illeszkedik (20.8).
  5. A logolás strukturált JSON, nem összefűzött szöveg (16.8).
Ha csak öt sorra van időd a huszonnégyből, ezek legyenek azok: az 1-es (a cache-szivárgás a legdrágább hiba), a 8-as (a hiányzó binding a leggyakoribb éles hiba), a 11-es (a füstteszt az egyetlen teszt, ami a valóságról szól), a 17-es (az indexelt staging hetekig mérgezi a rangsorodat), és a 23-as (riasztás nélkül a felhasználóid lesznek a monitorozásod).

20.10Ellenőrizd magad

  1. Miért nem szabad onMounted()-ben meta-tageket beállítani?
    ▸ Válasz

    Mert az onMounted a kliensen fut, azután, hogy a szerver már elküldte a választ — a keresőrobot által látott kezdeti HTML-ben nem lesz benne. A setup-ban kell, useHead() vagy useSeoMeta() hívással.

  2. Mikor éri meg a useServerSeoMeta() a useSeoMeta() helyett?
    ▸ Válasz

    Ha a metaadat nem változik kliensoldali navigáció során. Az API azonos, de a kódja nem kerül a kliens-bundle-be — kevesebb JS a felhasználónak, ugyanaz az SEO-eredmény.

  3. A magyar oldal canonicalje az angol változatra mutat, mert „az az eredeti”. Mi történik?
    ▸ Válasz

    Azt üzened a keresőnek, hogy a magyar oldal duplikátum, és ki fogja hagyni az indexelésből. Minden nyelvi változat önmagára mutasson canonicallel; a nyelvi kapcsolatot a hreflang jelenti be.

  4. Miért nem opcionális kényelem a lazy: true az i18n-konfigban egy Workers-appban?
    ▸ Válasz

    Mert enélkül minden nyelv minden fordítása benne van abban, amit a felhasználó letölt, és a Worker-bundle-ben is. Öt nyelvnél ez már megérződik a 15. modul mérőszámain. Lustán nyelvenként külön chunk keletkezik.

  5. Mi a különbség a max-age és az s-maxage között, és mi a nem nyilvánvaló kölcsönhatásuk?
    ▸ Válasz

    A max-age a böngészőnek szól, az s-maxage a megosztott cache-nek (a Cloudflare edge-nek). A csapda: az s-maxage kikapcsolja a stale-while-revalidate-et, mert a szabvány szerint magában hordozza a „ne szolgálj ki elavultat” jelentést.

  6. Egy bejelentkezett felhasználóknak szánt oldalra rákerül a public, s-maxage=600. Mi a következmény?
    ▸ Válasz

    Adatszivárgás. A Cloudflare eltárolja az egyik felhasználó HTML-jét, és kiszolgálja a következőnek. Ez nem teljesítményhiba. A helyes alapállás minden hitelesített útvonalon private, no-store, és a cache-elés a tudatosan engedélyezett kivétel.

  7. Van egy védőháló, ami néha megment ettől. Mi az, és miért ne építs rá?
    ▸ Válasz

    A Cloudflare Cache API soha nem cache-el Set-Cookie fejlécű választ. De ha a session-süti már korábban ki lett adva, az adott válaszban nincs Set-Cookie — és a védőháló nem lép működésbe.

  8. Mit ad a version_metadata binding a Sentry-beállításban?
    ▸ Válasz

    Ettől tudja a Sentry, melyik verzióból jött a hiba. Fokozatos kigördítésnél (17.5) ez azonnal megmutatja, hogy az új verzió termeli-e a hibákat — enélkül a két verzió hibái összemosódnak.

  9. Hány source map-láncot kell beállítanod, és mik ezek?
    ▸ Válasz

    Kettőt, egymástól függetlenül: kliensoldal a Sentrynek (sourcemap: { client: 'hidden' } + Sentry-feltöltés) és szerveroldal a Cloudflare-nek (nitro.sourceMap: true + upload_source_maps). A 'hidden' azt jelenti, hogy a map feltöltődik, de a kiszolgált JS nem hivatkozik rá.

  10. Miért érdemes a Nuxt hibakódra riasztani, nem a hibaszövegre?
    ▸ Válasz

    Mert a stabil kódok (NUXT_E1001) verzióról verzióra ugyanazok, a bőbeszédű magyarázat viszont kikerül a production buildből, és változhat. Egy szövegre illesztett riasztás a következő frissítésnél csendben elnémul.

Előző19. modul — Ami Workers-en máshogy megy Következő 21. modul — Migrációs recept: Vue SPA + Express → Nuxt