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.
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:
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',
})
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.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',
},
})
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):
| Almodul | Mit ad | Kell? |
|---|---|---|
@nuxtjs/robots | robots.txt, meta robots, X-Robots-Tag fejléc | igen — a staging kizárása is ez |
@nuxtjs/sitemap | sitemap.xml az útvonalaidból, i18n-támogatással | igen, ha nyilvános tartalom van |
nuxt-schema-org | JSON-LD strukturált adat értelmes alapokkal | webshop/cikk esetén erősen |
nuxt-seo-utils | favicon-ok, alap meta-tagek, breadcrumb | kényelmi |
nuxt-link-checker | törött link keresése build-időben | CI-ben hasznos |
nuxt-og-image | dinamikus Open Graph képek | lásd a következő szakaszt |
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.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' } },
},
})
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 @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égia | URL-ek | SEO | Mikor |
|---|---|---|---|
prefix | /hu/rolunk, /en/about | a legtisztább nyelvjelzés | ha egyik nyelv sem „fő” |
prefix_except_default | /rolunk, /en/about | jó, de gondos canonical kell | ha van egyértelmű fő piac |
prefix_and_default | mindkettő működik | duplikált tartalom kockázata | ritkán indokolt |
no_prefix | /rolunk mindenkinek | a nyelv láthatatlan a keresőnek | csak 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
}
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:
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.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éteg | Hol dől el | Hatóköre |
|---|---|---|
① routeRules | Nuxt-konfig, build-időben | prerender, SWR/ISR, fejlécek — útvonalanként (7. modul) |
| ② HTTP-fejlécek | a válaszon, futásidőben | böngésző-cache és a Cloudflare edge-cache |
| ③ Cache API | kódban, explicit | adatközpontonként külön (19.6) |
// böngészőnek 5 perc, edge-nek 1 óra
setResponseHeader(event, 'Cache-Control', 'public, max-age=300, s-maxage=3600')
| Direktíva | Kinek szól |
|---|---|
max-age | a böngészőnek |
s-maxage | a megosztott cache-nek, vagyis a Cloudflare edge-nek |
stale-while-revalidate | szolgálj ki régit, közben frissíts a háttérben |
stale-if-error | hiba esetén szolgálj ki régit |
private, no-store | senki ne tárolja — bejelentkezett oldalnál ez a helyes |
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.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')
}
})
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" }
}
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.Két, egymástól független source map-igényed van, és mindkettőt külön kell megoldani:
| Oldal | Kinek kell | Beállítás |
|---|---|---|
| Kliens | a Sentrynek, a böngészőben dobott hibákhoz | sourcemap: { client: 'hidden' } + Sentry-feltöltés |
| Szerver | a Cloudflare-nek, a Worker stack trace-eihez | nitro.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.
Az eszközök megvannak; a kérdés, hogy mit kezdesz velük. Egy működő ritmus:
| Mikor | Mit | Mire figyelsz |
|---|---|---|
| Deploy után 15 perc | Sentry hibaráta, Workers Logs --status error | új hibatípus jelent-e meg |
| Naponta | Sentry új problémák, kérésszám és CPU-idő | trendváltás, költségugrás |
| Hetente | Core Web Vitals (RUM), lassú útvonalak | lassú kúszás, amit egy deploy sem magyaráz |
| Havonta | bundle-méret és indulási idő trendje | közeledsz-e a 15. modul limitjeihez |
| Incidensben | wrangler tail --status error, verzió-összehasonlítás | melyik verzió, melyik útvonal, mióta |
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.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.
Cache-Control: private, no-store (20.6).runtimeConfig publikus ágában nincs olyan, aminek nem szabad kikerülnie (12. modul).message-e nem szivárog a kliensre (16.9).workers_dev: false, preview_urls: true (17.3).hydrate-never (15.2).site.url beállítva; canonical minden oldalon önmagára mutat (20.2).robots.txt és sitemap a helyén; a staging kizárva az indexelésből (20.3).setup-ban készülnek, nem onMounted-ben (20.2).observability.enabled bekapcsolva, ésszerű head_sampling_rate-tel (16.8).version_metadata bindinggel (20.7).onMounted()-ben meta-tageket beállítani?
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.
useServerSeoMeta() a useSeoMeta() helyett?
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.
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.
lazy: true az i18n-konfigban egy Workers-appban?
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.
max-age és az s-maxage között, és mi a nem nyilvánvaló kölcsönhatásuk?
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.
public, s-maxage=600. Mi a következmény?
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.
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.
version_metadata binding a Sentry-beállításban?
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.
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á.
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.