[{"data":1,"prerenderedAt":655},["ShallowReactive",2],{"blog-nuxt-i18n-multilingual-seo-hreflang-de":3},{"slug":4,"published":5,"minutes":6,"category":7,"tags":8,"keywords":14,"about":25,"sources":34,"cover":50,"og":51,"expertise":52,"locales":53,"lang":55,"title":57,"description":58,"coverAlt":59,"metaTitle":60,"takeaways":61,"faq":67,"toc":83,"blocks":114,"others":449},"nuxt-i18n-multilingual-seo-hreflang","2026-10-16",8,"web",[9,10,11,12,13],"Nuxt","i18n","SEO","hreflang","Multilingual",[15,16,17,18,19,20,21,22,23,24],"nuxt i18n seo","nuxt hreflang","useLocaleHead","nuxt i18n canonical","hreflang x-default","prefix_except_default","multilingual seo nuxt","nuxt i18n sitemap hreflang","hreflang self-referencing","nuxt i18n baseUrl",[26,28,31],{"name":9,"url":27},"https:\u002F\u002Fnuxt.com\u002F",{"name":29,"url":30},"Hreflang","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHreflang",{"name":32,"url":33},"Internationalization and localization","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FInternationalization_and_localization",[35,38,41,44,47],{"title":36,"url":37},"Nuxt i18n docs: SEO (useLocaleHead)","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide\u002Fseo",{"title":39,"url":40},"Nuxt i18n docs: Routing strategies","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide",{"title":42,"url":43},"Google Search Central: Tell Google about localized versions of your page","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Flocalized-versions",{"title":45,"url":46},"Google Search Central: Managing multi-regional and multilingual sites","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Fmanaging-multi-regional-sites",{"title":48,"url":49},"Google Search Central: How to specify a canonical URL","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fconsolidate-duplicate-urls","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fog.jpg","vue-nuxt-developer",[54,55,56],"en","de","hu","Mehrsprachiges SEO mit Nuxt i18n: URL-Strategie, hreflang, Canonicals und Sitemaps","So richtet man Nuxt i18n ein, damit Suchmaschinen jede Sprache indexieren: Routing-Strategie, useLocaleHead, hreflang-Regeln, Teilübersetzungen, Sitemaps.","Drei Sprachversionen einer Seite, Englisch, Deutsch und Ungarisch, mit hreflang-Pfeilen untereinander verknüpft und einem x-default-Fallback.","Nuxt-i18n-SEO: hreflang, Canonicals, Sitemaps · Balázs Csorba",[62,63,64,65,66],"Jeder Sprache eine eigene URL geben; Google empfiehlt das statt Cookies oder Browsereinstellungen, und der Googlebot sendet keinen Accept-Language-Header.","In @nuxtjs\u002Fi18n bei jeder Locale language und eine produktive baseUrl setzen, dann liefert useLocaleHead lang-, hreflang-, og:locale- und Canonical-Tags.","hreflang-Sätze müssen auf jeder Version identisch sein, die Seite selbst enthalten, vollständige URLs und gültige ISO-Sprachcodes nutzen.","Bei Seiten in weniger Sprachen nur die echten Übersetzungen listen und den Canonical unübersetzter Kopien aufs Original zeigen lassen.","Nicht nach Browsersprache umleiten; einen Sprachumschalter mit echten Links anbieten und hreflang die Suchenden zur richtigen Version schicken lassen.",[68,71,74,77,80],{"q":69,"a":70},"Welche Routing-Strategie von Nuxt i18n ist die beste fürs SEO?","prefix_except_default und prefix geben jeder Sprache eine eigene URL, wie Google es empfiehlt. prefix_except_default lässt die Standardsprache ohne Präfix. no_prefix für öffentliche Inhalte meiden, denn alle Sprachen teilen sich eine URL und Suchmaschinen können nur eine davon indexieren.",{"q":72,"a":73},"Erzeugt useLocaleHead hreflang automatisch?","Ja. Es liefert das lang-Attribut des html-Elements, einen Alternate-Link mit hreflang für jede konfigurierte Locale, og:locale-Tags und einen Canonical-Link. Dafür braucht jede Locale einen language-Wert und das Modul eine baseUrl, damit die URLs vollständig sind.",{"q":75,"a":76},"Brauche ich x-default?","Es wird empfohlen. Google nutzt x-default für Nutzer, deren Sprache zu keiner Version passt. Meist zeigt es auf die Version in der Standardsprache oder auf eine Sprachauswahlseite.",{"q":78,"a":79},"Gehört hreflang in den HTML-Head oder in die Sitemap?","Beides geht. Google behandelt HTML-Tags, HTTP-Header und Sitemap-Einträge als gleichwertig und sagt, dass mehrere davon keinen Zusatznutzen bringen. Die Variante wählen, die man korrekt halten kann; bei statischen Seiten aus einer Datenquelle können beide aus demselben Code kommen.",{"q":81,"a":82},"Nutzt Google hreflang, um die Sprache einer Seite zu erkennen?","Nein. Laut Google wird die Sprache mit eigenen Algorithmen aus dem Inhalt erkannt; hreflang sagt nur, welche URLs lokalisierte Versionen derselben Seite sind. Eine deutsche Seite voller englischem Text ist für Google trotzdem englisch.",[84,87,90,93,96,99,102,105,108,111],{"id":85,"title":86},"url-strategy","Welche URL-Strategie sollte eine mehrsprachige Nuxt-Seite nutzen?",{"id":88,"title":89},"configure-nuxt-i18n","Wie konfiguriert man @nuxtjs\u002Fi18n fürs SEO?",{"id":91,"title":92},"use-locale-head","Was schreibt useLocaleHead in den Head der Seite?",{"id":94,"title":95},"hreflang-rules","Welche hreflang-Regeln werden oft falsch umgesetzt?",{"id":97,"title":98},"partial-translations","Was ist mit Seiten, die nur in manchen Sprachen existieren?",{"id":100,"title":101},"sitemaps","Gehört hreflang auch in die Sitemap?",{"id":103,"title":104},"no-automatic-redirects","Warum nicht nach Browsersprache weiterleiten?",{"id":106,"title":107},"beyond-the-head","Was muss für mehrsprachiges SEO noch übersetzt werden?",{"id":109,"title":110},"test-it","Wie prüft man das Setup?",{"id":112,"title":113},"sources","Quellen",[115,119,122,125,137,183,186,187,209,211,212,245,247,278,279,288,295,336,342,343,355,357,360,361,364,366,367,374,381,382,416,424,425,428,430,431],{"type":116,"content":117},"paragraph",[118],"Eine mehrsprachige Nuxt-Seite kann für Besucher perfekt aussehen und für Suchmaschinen trotzdem halb unsichtbar sein: Die deutschen Seiten werden nicht indexiert, Google zeigt ungarischen Suchenden die englische Seite, oder zwei Sprachversionen konkurrieren um dieselbe Suchanfrage. Fast immer liegt die Ursache im Head der Seite: fehlende oder widersprüchliche hreflang-Links, ein Canonical auf die falsche Sprache oder eine URL-Strategie, die Sprachen hinter einem Cookie versteckt.",{"type":116,"content":120},[121],"Dieser Artikel zeigt ein Setup mit @nuxtjs\u002Fi18n, das Suchmaschinen verstehen, am Beispiel dieser Website, die statisch auf Englisch, Deutsch und Ungarisch generiert wird. Es geht um die Routing-Strategie, useLocaleHead, die hreflang-Regeln aus Googles Dokumentation, Seiten, die nur in manchen Sprachen existieren, Sitemaps und warum automatische Sprachweiterleitungen schaden.",{"type":123,"level":124,"id":85,"text":86},"heading",2,{"type":116,"content":126},[127,128,132,133,136],"Googles Empfehlung für ",{"tag":129,"href":46,"children":130},"a",[131],"mehrsprachige Websites"," ist eindeutig: für jede Sprachversion eigene URLs statt Cookies oder Browsereinstellungen. Wechselt der Inhalt auf einer URL je nach Sprache, findet Google womöglich nicht alle Varianten, denn der Googlebot crawlt meist aus den USA und sendet Anfragen ohne Accept-Language-Header. Nuxt i18n bietet vier ",{"tag":129,"href":40,"children":134},[135],"Routing-Strategien",":",{"type":138,"head":139,"rows":146},"table",[140,142,144],[141],"Strategie",[143],"URLs",[145],"SEO-Urteil",[147,156,165,174],[148,152,154],[149],{"tag":150,"children":151},"code",[20],[153],"\u002Fblog, \u002Fde\u002Fblog, \u002Fhu\u002Fblog",[155],"Guter Standard; die Hauptsprache behält saubere URLs",[157,161,163],[158],{"tag":150,"children":159},[160],"prefix",[162],"\u002Fen\u002Fblog, \u002Fde\u002Fblog, \u002Fhu\u002Fblog",[164],"Gut; symmetrisch, braucht eine Weiterleitung oder Sprachauswahl an der Wurzel",[166,170,172],[167],{"tag":150,"children":168},[169],"prefix_and_default",[171],"\u002Fblog and \u002Fen\u002Fblog, \u002Fde\u002Fblog",[173],"Funktioniert, erzeugt aber Duplikate; der Canonical zeigt auf die URL ohne Präfix",[175,179,181],[176],{"tag":150,"children":177},[178],"no_prefix",[180],"\u002Fblog for all",[182],"Für öffentliche Inhalte meiden; eine URL wird nur in einer Sprache indexiert",{"type":116,"content":184},[185],"Unterverzeichnisse auf einer Domain sind auch im Betrieb die günstigste Option. Eigene Länderdomains lohnen sich, wenn das Geschäft pro Land wirklich getrennt ist; für eine zweite Sprache braucht man sie nicht.",{"type":123,"level":124,"id":88,"text":89},{"type":116,"content":188},[189,190,193,194,197,198,200,201,204,205,208],"Zwei Einstellungen sind nötig, damit die ",{"tag":129,"href":37,"children":191},[192],"SEO-Funktionen"," korrekte Ausgaben liefern: Jede Locale braucht ein ",{"tag":150,"children":195},[196],"language","-Tag, das zum ",{"tag":150,"children":199},[12],"-Wert und zum ",{"tag":150,"children":202},[203],"\u003Chtml lang>"," wird, und ",{"tag":150,"children":206},[207],"baseUrl"," muss der produktive Origin sein, denn hreflang- und Canonical-URLs müssen vollständig sein. So sieht die Konfiguration dieser Website aus:",{"type":150,"code":210},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@nuxtjs\u002Fi18n'],\n  i18n: {\n    baseUrl: 'https:\u002F\u002Fbalazscsorba.com',\n    strategy: 'prefix_except_default',\n    defaultLocale: 'en',\n    locales: [\n      { code: 'en', language: 'en-US', name: 'English', file: 'en.json' },\n      { code: 'de', language: 'de-AT', name: 'Deutsch', file: 'de.json' },\n      { code: 'hu', language: 'hu-HU', name: 'Magyar', file: 'hu.json' },\n    ],\n    \u002F\u002F No automatic redirect by browser language; the switcher links to every version.\n    detectBrowserLanguage: false,\n  },\n})\n",{"type":123,"level":124,"id":91,"text":92},{"type":116,"content":213},[214,217,218,221,222,225,226,229,230,232,233,236,237,240,241,244],{"tag":150,"children":215},[216],"useLocaleHead()"," liefert Head-Metadaten, die man an ",{"tag":150,"children":219},[220],"useHead"," übergibt: das ",{"tag":150,"children":223},[224],"lang","-Attribut für ",{"tag":150,"children":227},[228],"\u003Chtml>",", einen Alternate-Link mit ",{"tag":150,"children":231},[12]," für jede konfigurierte Locale, ",{"tag":150,"children":234},[235],"og:locale","- und ",{"tag":150,"children":238},[239],"og:locale:alternate","-Meta-Tags und einen Canonical-Link. Einmal in ",{"tag":150,"children":242},[243],"app.vue"," aufrufen, dann bekommt es jede Seite.",{"type":150,"code":246},"\u003C!-- app.vue -->\n\u003Cscript setup lang=\"ts\">\nconst i18nHead = useLocaleHead({ dir: true, seo: true })\n\nuseHead(() => ({\n  htmlAttrs: { lang: i18nHead.value.htmlAttrs?.lang },\n  link: [...(i18nHead.value.link ?? [])],\n  meta: [...(i18nHead.value.meta ?? [])],\n}))\n\u003C\u002Fscript>\n",{"type":116,"content":248},[249,250,253,254,257,258,261,262,265,266,269,270,273,274,277],"Für eine Seite unter ",{"tag":150,"children":251},[252],"\u002Fde\u002Fblog\u002Fpost"," enthält der gerenderte Head dann ",{"tag":150,"children":255},[256],"\u003Chtml lang=\"de-AT\">",", einen Canonical-Link auf die deutsche URL und Alternate-Links für ",{"tag":150,"children":259},[260],"en-US",", ",{"tag":150,"children":263},[264],"de-AT"," und ",{"tag":150,"children":267},[268],"hu-HU",". Das Modul ergänzt außerdem einen Catch-all-Link pro Sprachgruppe, etwa ",{"tag":150,"children":271},[272],"hreflang=\"en\"",", und ein ",{"tag":150,"children":275},[276],"x-default",". Das generierte HTML einmal pro Seitentyp prüfen; man nimmt leicht an, der Head stimme, nur weil das Modul installiert ist.",{"type":123,"level":124,"id":94,"text":95},{"type":280,"attrs":281,"inner":285,"caption":286},"diagram",{"viewBox":282,"role":283,"aria-labelledby":284},"0 0 720 310","img","nhl-t nhl-d","\u003Ctitle id=\"nhl-t\">Ein hreflang-Cluster\u003C\u002Ftitle>\u003Cdesc id=\"nhl-d\">Drei Sprachversionen einer Seite verlinken sich gegenseitig in beide Richtungen, jede listet auch sich selbst, und ein x-default-Link zeigt für nicht passende Sprachen auf die englische Version.\u003C\u002Fdesc>\u003Cdefs>\u003Cmarker id=\"nhl-ah\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\u003Cpath d=\"M0 0L10 5L0 10z\" class=\"d-head\" \u002F>\u003C\u002Fmarker>\u003C\u002Fdefs>\u003Crect x=\"270\" y=\"20\" width=\"180\" height=\"64\" rx=\"10\" class=\"d-accent\" \u002F>\u003Ctext x=\"360\" y=\"48\" text-anchor=\"middle\" class=\"d-text\">\u002Fblog\u002Fpost\u003C\u002Ftext>\u003Ctext x=\"360\" y=\"70\" text-anchor=\"middle\" class=\"d-small\">hreflang=\"en\"\u003C\u002Ftext>\u003Crect x=\"60\" y=\"190\" width=\"180\" height=\"64\" rx=\"10\" class=\"d-mint\" \u002F>\u003Ctext x=\"150\" y=\"218\" text-anchor=\"middle\" class=\"d-text\">\u002Fde\u002Fblog\u002Fpost\u003C\u002Ftext>\u003Ctext x=\"150\" y=\"240\" text-anchor=\"middle\" class=\"d-small\">hreflang=\"de\"\u003C\u002Ftext>\u003Crect x=\"480\" y=\"190\" width=\"180\" height=\"64\" rx=\"10\" class=\"d-sky\" \u002F>\u003Ctext x=\"570\" y=\"218\" text-anchor=\"middle\" class=\"d-text\">\u002Fhu\u002Fblog\u002Fpost\u003C\u002Ftext>\u003Ctext x=\"570\" y=\"240\" text-anchor=\"middle\" class=\"d-small\">hreflang=\"hu\"\u003C\u002Ftext>\u003Cpath d=\"M300 86L190 186\" class=\"d-line\" marker-start=\"url(#nhl-ah)\" marker-end=\"url(#nhl-ah)\" \u002F>\u003Cpath d=\"M420 86L530 186\" class=\"d-line\" marker-start=\"url(#nhl-ah)\" marker-end=\"url(#nhl-ah)\" \u002F>\u003Cpath d=\"M244 222H476\" class=\"d-line\" marker-start=\"url(#nhl-ah)\" marker-end=\"url(#nhl-ah)\" \u002F>\u003Crect x=\"560\" y=\"28\" width=\"140\" height=\"48\" rx=\"10\" class=\"d-gold\" \u002F>\u003Ctext x=\"630\" y=\"57\" text-anchor=\"middle\" class=\"d-small\">x-default\u003C\u002Ftext>\u003Cpath d=\"M558 52H454\" class=\"d-line-accent\" marker-end=\"url(#nhl-ah)\" \u002F>\u003Ctext x=\"360\" y=\"296\" text-anchor=\"middle\" class=\"d-label\">Jede Version listet alle drei plus x-default, auch sich selbst.\u003C\u002Ftext>",[287],"hreflang funktioniert nur als geschlossener Satz: Verlinkt die deutsche Seite nicht zurück, ignoriert Google das Paar womöglich.",{"type":116,"content":289},[290,291,294],"Googles ",{"tag":129,"href":43,"children":292},[293],"hreflang-Dokumentation"," ist kurz, und die meisten Fehler, die ich in Audits finde, verletzen eine dieser Regeln:",{"type":296,"items":297},"list",[298,304,312,324,331],[299,303],{"tag":300,"children":301},"strong",[302],"Die Seite selbst einschließen."," Jede Version listet einen Link pro Variante, sich selbst eingeschlossen, und der Satz ist auf jeder Version identisch.",[305,308,309,311],{"tag":300,"children":306},[307],"Vollständige URLs verwenden,"," mit Protokoll und Host. Relative hrefs sind meist die Folge einer fehlenden ",{"tag":150,"children":310},[207],".",[313,316,317,319,320,323],{"tag":300,"children":314},[315],"Gültige Codes nutzen:"," eine Sprache nach ISO 639-1, optional gefolgt von einer Region nach ISO 3166-1 (",{"tag":150,"children":318},[264],"). Eine Region allein funktioniert nicht, und Codes wie ",{"tag":150,"children":321},[322],"es-419"," werden nicht unterstützt.",[325,330],{"tag":300,"children":326},[327,329],{"tag":150,"children":328},[276]," ergänzen"," für Nutzer, deren Sprache zu keiner Version passt, meist die Seite in der Standardsprache.",[332,335],{"tag":300,"children":333},[334],"Den Canonical selbstreferenzierend setzen"," auf jeder Sprachversion. Eine deutsche Seite, deren Canonical auf die englische zeigt, sagt Google, die deutsche fallen zu lassen.",{"type":116,"content":337},[338,339,341],"Noch ein wichtiger Punkt: Google sagt, dass es weder hreflang noch das ",{"tag":150,"children":340},[224],"-Attribut zur Spracherkennung nutzt, sondern die Sprache aus dem Inhalt erkennt. Ein übersetztes Template mit unübersetztem Fließtext bleibt eine englische Seite.",{"type":123,"level":124,"id":97,"text":98},{"type":116,"content":344},[345,346,348,349,351,352,354],"Mit ",{"tag":150,"children":347},[20]," erzeugt Nuxt für jede Locale eine Route, ein rein englischer Artikel wird also auch unter ",{"tag":150,"children":350},[252]," mit deutscher Navigation und englischem Text gerendert. Ohne Eingriff bewirbt useLocaleHead ihn als deutsche Übersetzung, und der Canonical der deutschen Kopie zeigt auf sich selbst. Diese Website behebt beides in ",{"tag":150,"children":353},[243],": Eine Seite, die in weniger Sprachen existiert, meldet diese an, hreflang listet dann nur sie, und unübersetzte Kopien setzen ihren Canonical aufs englische Original.",{"type":150,"code":356},"\u002F\u002F app.vue (excerpt): pageLocales is set by pages that exist in fewer languages\nconst headLinks = computed(() => {\n  const links = i18nHead.value.link ?? []\n  const only = pageLocales.value\n  if (!only || only.path !== route.path) return links\n  return links\n    .filter((l) => l.rel !== 'alternate' || !l.hreflang || l.hreflang === 'x-default'\n      || only.locales.some((c) => l.hreflang.startsWith(c)))\n    .map((l) => (l.rel === 'canonical' && !only.translated\n      ? { ...l, href: `${siteUrl}${switchLocalePath('en')}` }\n      : l))\n})\n",{"type":116,"content":358},[359],"Die Alternative ist, unübersetzte Routen gar nicht zu erzeugen und einen 404 oder eine Weiterleitung aufs Original zu liefern. Das ist für Suchmaschinen sauberer, aber schlechter für Besucher, die die Oberfläche auf Deutsch gestellt haben und dabei bleiben wollen. Beides funktioniert, solange der Head die Wahrheit darüber sagt, welche Übersetzungen existieren.",{"type":123,"level":124,"id":100,"text":101},{"type":116,"content":362},[363],"Google akzeptiert hreflang an drei Stellen, in HTML-Tags, HTTP-Headern und der Sitemap, behandelt sie als gleichwertig und sagt, mehrere davon brächten in der Suche keinen Vorteil, erschwerten aber die Pflege. HTTP-Header sind vor allem für Nicht-HTML-Dateien wie PDFs nützlich. Diese Website gibt Head-Tags und Sitemap-Einträge aus, weil beide beim Build aus denselben Beitragsdaten erzeugt werden und so nicht auseinanderlaufen können. Kommen sie bei dir aus verschiedenen Codepfaden, eine Variante wählen.",{"type":150,"code":365},"\u003Curl>\n  \u003Cloc>https:\u002F\u002Fbalazscsorba.com\u002Fde\u002Fblog\u002Fagent-loop-explained\u003C\u002Floc>\n  \u003Cxhtml:link rel=\"alternate\" hreflang=\"en\" href=\"https:\u002F\u002Fbalazscsorba.com\u002Fblog\u002Fagent-loop-explained\"\u002F>\n  \u003Cxhtml:link rel=\"alternate\" hreflang=\"de\" href=\"https:\u002F\u002Fbalazscsorba.com\u002Fde\u002Fblog\u002Fagent-loop-explained\"\u002F>\n  \u003Cxhtml:link rel=\"alternate\" hreflang=\"hu\" href=\"https:\u002F\u002Fbalazscsorba.com\u002Fhu\u002Fblog\u002Fagent-loop-explained\"\u002F>\n  \u003Cxhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"https:\u002F\u002Fbalazscsorba.com\u002Fblog\u002Fagent-loop-explained\"\u002F>\n\u003C\u002Furl>\n",{"type":123,"level":124,"id":103,"text":104},{"type":116,"content":368},[369,370,373],"Google rät davon ab, Nutzer anhand der vermuteten Sprache auf eine Sprachversion umzuleiten, weil das Nutzer und Suchmaschinen daran hindern kann, alle Versionen zu erreichen. ",{"tag":150,"children":371},[372],"detectBrowserLanguage"," von Nuxt i18n kann beim ersten Besuch umleiten; bei einer öffentlichen Website schalte ich es ab und setze stattdessen auf zwei Dinge: hreflang, das Suchende direkt aus der Ergebnisseite zur richtigen Version schickt, und einen Sprachumschalter aus echten Links, dem auch Crawler folgen können.",{"type":375,"variant":376,"title":377,"body":378},"callout","tip","Eine bewusste Wahl merken, keine Vermutung",[379],[380],"Die im Umschalter gewählte Sprache in einem Cookie zu speichern ist in Ordnung; das spiegelt eine Entscheidung. Eine Weiterleitung anhand des Accept-Language-Headers ist eine Vermutung, und der Googlebot sendet diesen Header gar nicht.",{"type":123,"level":124,"id":106,"text":107},{"type":296,"items":383},[384,389,402,407],[385,388],{"tag":300,"children":386},[387],"Titel und Meta-Descriptions pro Locale."," Sie sind das, was Suchende in den Ergebnissen lesen; eine deutsche Seite mit englischem Titel verliert Klicks.",[390,396,397,311],{"tag":300,"children":391},[392,393],"Strukturierte Daten mit ",{"tag":150,"children":394},[395],"inLanguage",", passend zur Seite, damit KI-Suche und Rich Results die richtige Version zitieren. Mehr dazu im ",{"tag":398,"to":399,"children":400},"link","\u002Fblog\u002Fgenerative-engine-optimization-audit",[401],"GEO-Audit dieser Website",[403,406],{"tag":300,"children":404},[405],"Alt-Texte und Open-Graph-Bilder"," mit lokalisiertem Text, wenn das Bild Wörter enthält.",[408,411,412,415],{"tag":300,"children":409},[410],"Interne Links, die in der Sprache bleiben."," Überall ",{"tag":150,"children":413},[414],"localePath()"," verwenden, damit ein deutscher Artikel auf deutsche Seiten verlinkt und Besucher nicht ins Englische abfließen.",{"type":116,"content":417},[418,419,423],"Performance zählt in jeder Sprache gleich; es gelten dieselben Regeln wie in ",{"tag":398,"to":420,"children":421},"\u002Fblog\u002Fnuxt-core-web-vitals-performance",[422],"Core Web Vitals für Nuxt",", und Lazy Loading der Nachrichtendateien pro Locale hält das Bundle klein.",{"type":123,"level":124,"id":109,"text":110},{"type":116,"content":426},[427],"Das generierte HTML einer Seite pro Typ und Sprache abrufen und die Link-Sätze vergleichen; ein kurzes Skript findet die üblichen Fehler, bevor die Search Console es Wochen später tut.",{"type":150,"code":429},"\u002F\u002F check-hreflang.mjs: every version must list the same set, including itself\nconst pages = ['\u002Fblog\u002Fagent-loop-explained', '\u002Fde\u002Fblog\u002Fagent-loop-explained', '\u002Fhu\u002Fblog\u002Fagent-loop-explained']\nconst base = 'https:\u002F\u002Fbalazscsorba.com'\nconst sets = await Promise.all(pages.map(async (p) => {\n  const html = await (await fetch(base + p)).text()\n  const attr = (tag, name) => tag.match(new RegExp(` ${name}=\"([^\"]+)\"`))?.[1]\n  const tags = html.match(\u002F\u003Clink[^>]*>\u002Fg) ?? []\n  const links = tags.filter((t) => attr(t, 'rel') === 'alternate' && attr(t, 'hreflang'))\n    .map((t) => [attr(t, 'hreflang'), attr(t, 'href')])\n  const canonical = attr(tags.find((t) => attr(t, 'rel') === 'canonical') ?? '', 'href')\n  if (canonical !== base + p) console.warn(`${p}: canonical is ${canonical}`)\n  if (!links.some(([, href]) => href === base + p)) console.warn(`${p}: does not list itself`)\n  return links.map(([lang, href]) => `${lang} ${href}`).sort().join('\\n')\n}))\nconsole.log(new Set(sets).size === 1 ? 'hreflang sets match' : 'hreflang sets differ')\n",{"type":123,"level":124,"id":112,"text":113},{"type":296,"ordered":432,"items":433},true,[434,437,440,443,446],[435],{"tag":129,"href":37,"children":436},[36],[438],{"tag":129,"href":40,"children":439},[39],[441],{"tag":129,"href":43,"children":442},[42],[444],{"tag":129,"href":46,"children":445},[45],[447],{"tag":129,"href":49,"children":448},[48],[450,518,570,609],{"slug":451,"published":5,"minutes":452,"category":7,"tags":453,"keywords":458,"about":471,"sources":478,"cover":512,"og":513,"expertise":52,"locales":514,"lang":55,"title":515,"description":516,"coverAlt":517},"llm-endpoint-abuse-denial-of-wallet",14,[9,454,455,456,457],"LLM security","Rate limiting","OWASP","Cost control",[459,460,461,462,463,464,465,466,467,468,469,470],"denial of wallet llm","owasp llm10 unbounded consumption","rate limit llm api nuxt","nuxt security rate limiter","protect openai api key frontend","llm token budget per user","limit llm cost per user","nitro api rate limiting","cloudflare turnstile nuxt","anthropic spend limit","openai hard spend limit","llm prompt size limit",[472,474,477],{"name":455,"url":473},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRate_limiting",{"name":475,"url":476},"Denial-of-service attack","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FDenial-of-service_attack",{"name":9,"url":27},[479,482,485,488,491,494,497,500,503,506,509],{"title":480,"url":481},"OWASP GenAI Security Project: LLM10:2025 Unbounded Consumption","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm102025-unbounded-consumption\u002F",{"title":483,"url":484},"Nuxt Security: Rate Limiter middleware","https:\u002F\u002Fnuxt-security.vercel.app\u002Fmiddleware\u002Frate-limiter",{"title":486,"url":487},"Nuxt Security: Request Size Limiter middleware","https:\u002F\u002Fnuxt-security.vercel.app\u002Fmiddleware\u002Frequest-size-limiter",{"title":489,"url":490},"nuxt-auth-utils: README (GitHub)","https:\u002F\u002Fgithub.com\u002Fatinux\u002Fnuxt-auth-utils",{"title":492,"url":493},"h3: request utilities (getRequestIP, readValidatedBody)","https:\u002F\u002Fh3.dev\u002Futils\u002Frequest",{"title":495,"url":496},"Upstash: Ratelimiting algorithms","https:\u002F\u002Fupstash.com\u002Fdocs\u002Fredis\u002Fsdks\u002Fratelimit-ts\u002Falgorithms",{"title":498,"url":499},"Cloudflare Turnstile: Validate the token","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fturnstile\u002Fget-started\u002Fserver-side-validation\u002F",{"title":501,"url":502},"Claude Platform Docs: Rate limits and spend limits","https:\u002F\u002Fplatform.claude.com\u002Fdocs\u002Fen\u002Fapi\u002Frate-limits",{"title":504,"url":505},"Claude Platform Docs: Token counting","https:\u002F\u002Fplatform.claude.com\u002Fdocs\u002Fen\u002Fbuild-with-claude\u002Ftoken-counting",{"title":507,"url":508},"OpenAI API Docs: Spend limits","https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fguides\u002Fspend-limits",{"title":510,"url":511},"OpenAI API Docs: Rate limits","https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Frate-limits","\u002Fimages\u002Fblog\u002Fllm-endpoint-abuse-denial-of-wallet\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllm-endpoint-abuse-denial-of-wallet\u002Fog.jpg",[54,55,56],"Denial of Wallet: öffentliche LLM-Endpunkte in einer Nuxt-App absichern","Missbrauch und ausufernde Kosten auf einer öffentlichen LLM-Route in Nuxt stoppen: Auth, Rate Limits, Prompt-Limits, Token-Budgets, Turnstile, Spend Limits.","Anfragen an einen Nuxt-Chat-Endpunkt durchlaufen Bot-Prüfung, Session, Rate Limit, Prompt-Limit und Token-Budget, bevor sie die Modell-API erreichen.",{"slug":519,"published":520,"minutes":521,"category":7,"tags":522,"keywords":526,"about":534,"sources":541,"cover":563,"og":564,"expertise":565,"locales":566,"lang":55,"title":567,"description":568,"coverAlt":569},"llms-txt-vs-markdown-content-negotiation","2026-10-07",9,[523,524,525,9],"llms.txt","Content negotiation","AI agents",[523,527,528,529,530,531,532,533],"markdown for agents","content negotiation","AI crawlers","generative engine optimization","Accept text\u002Fmarkdown header","llms-full.txt","agent-ready website",[535,538],{"name":536,"url":537},"Markdown","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMarkdown",{"name":539,"url":540},"Hypertext Transfer Protocol","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHypertext_Transfer_Protocol",[542,545,548,551,554,557,560],{"title":543,"url":544},"llmstxt.org: The \u002Fllms.txt file (proposal, version 2)","https:\u002F\u002Fllmstxt.org\u002F",{"title":546,"url":547},"llmstxt.org: Changes from v1 to v2","https:\u002F\u002Fllmstxt.org\u002Fchanges.html",{"title":549,"url":550},"Cloudflare changelog: Markdown for Agents (12 February 2026)","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fchangelog\u002Fpost\u002F2026-02-12-markdown-for-agents\u002F",{"title":552,"url":553},"Cloudflare docs: Markdown for Agents","https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fmarkdown-for-agents\u002F",{"title":555,"url":556},"Ahrefs: 137K sites analyzed, 97% of llms.txt files never get read (June 2026)","https:\u002F\u002Fahrefs.com\u002Fblog\u002Fllmstxt-study\u002F",{"title":558,"url":559},"Checkly: The current state of content negotiation for AI agents (February 2026)","https:\u002F\u002Fwww.checklyhq.com\u002Fblog\u002Fstate-of-ai-agent-content-negotation\u002F",{"title":561,"url":562},"Suganthan: tracking Cloudflare Markdown for Agents on one site","https:\u002F\u002Fsuganthan.com\u002Fblog\u002Fcloudflare-markdown-for-agents\u002F","\u002Fimages\u002Fblog\u002Fllms-txt-vs-markdown-content-negotiation\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllms-txt-vs-markdown-content-negotiation\u002Fog.jpg","ai-engineer",[54,55,56],"llms.txt versus Markdown Content Negotiation: was Agenten wirklich abrufen","llms.txt ist ein Vorschlag, Markdown Content Negotiation ein Header. Was KI-Agenten abrufen, was die Logs zeigen und wie man beides aus Nuxt und nginx ausliefert.","Vier gestapelte Schichten, die KI-Agenten bedienen: deklarierte WebMCP-Tools, eine Markdown-Kopie jeder Seite, eine per Content Negotiation ausgelieferte Markdown-Antwort und ein llms.txt-Index",{"slug":571,"published":572,"minutes":6,"category":7,"tags":573,"keywords":579,"about":587,"sources":596,"cover":603,"og":604,"expertise":52,"locales":605,"lang":55,"title":606,"description":607,"coverAlt":608},"multiplayer-sailing-game-threejs","2026-10-05",[574,575,576,577,578],"three.js","WebGL","GLSL","WebSockets","Game development",[580,581,582,583,584,585,586,576],"three.js game","three.js multiplayer","Gerstner waves","WebGL ocean shader","day night cycle three.js","WebSocket relay","browser game development",[588,591,593],{"name":589,"url":590},"Three.js","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FThree.js",{"name":575,"url":592},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebGL",{"name":594,"url":595},"WebSocket","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebSocket",[597,600],{"title":598,"url":599},"GPU Gems, Chapter 1: Effective Water Simulation from Physical Models","https:\u002F\u002Fdeveloper.nvidia.com\u002Fgpugems\u002Fgpugems\u002Fpart-i-natural-effects\u002Fchapter-1-effective-water-simulation-physical-models",{"title":601,"url":602},"RFC 6455: The WebSocket Protocol","https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6455","\u002Fimages\u002Fblog\u002Fmultiplayer-sailing-game-threejs\u002Fdusk.webp","\u002Fimages\u002Fblog\u002Fmultiplayer-sailing-game-threejs\u002Fog.jpg",[54,55,56],"Ein 3D-Segelspiel mit Multiplayer in reinem three.js","Gerstner-Wellen, die GPU und CPU gleich berechnen, eine Himmelsfunktion für Himmel, Wasser und Nebel, ein Tag-Nacht-Wechsel in fünf Minuten und ein winziger WebSocket-Relay – die Technik hinter dem Spiel auf meiner Website.","Eine Dschunke mit Drachenbug und leuchtenden Laternen segelt in der Abenddämmerung über eine bewegte Bucht, bewaldete Hügel auf beiden Seiten.",{"slug":610,"published":611,"minutes":521,"category":7,"tags":612,"keywords":618,"about":627,"sources":636,"cover":649,"og":650,"expertise":52,"locales":651,"lang":55,"title":652,"description":653,"coverAlt":654},"home-assistant-ev-charging-energy-manager","2026-09-16",[613,614,615,616,617],"Home Assistant","EPEX Spot","Energy prices","Tesla","Energy management",[619,620,621,622,623,624,625,626],"EPEX Austria prices","spot tariff Austria EV charging","Home Assistant EV charging","cheapest hours charging","day-ahead price Austria","negative electricity prices Austria","Tesla charging spot prices","load management 3 phase",[628,631,633],{"name":629,"url":630},"EPEX SPOT","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FEPEX_SPOT",{"name":613,"url":632},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHome_Assistant",{"name":634,"url":635},"Load management","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLoad_management",[637,640,643,646],{"title":638,"url":639},"aWATTar Austria: market data API","https:\u002F\u002Fwww.awattar.at\u002Fservices\u002Fapi",{"title":641,"url":642},"EPEX SPOT: market results","https:\u002F\u002Fwww.epexspot.com\u002Fen\u002Fmarket-results",{"title":644,"url":645},"Home Assistant developer docs: apps","https:\u002F\u002Fdevelopers.home-assistant.io\u002Fdocs\u002Fapps\u002F",{"title":647,"url":648},"Home Assistant: Tesla Fleet integration","https:\u002F\u002Fwww.home-assistant.io\u002Fintegrations\u002Ftesla_fleet\u002F","\u002Fimages\u002Fblog\u002Fhome-assistant-ev-charging-energy-manager\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fhome-assistant-ev-charging-energy-manager\u002Fog.jpg",[54,55,56],"Laden nach EPEX-Preisen in Österreich: was meine Home-Assistant-App spart","Ein Jahr stündlicher EPEX-Preise für Österreich, nachgespielt für einen Tesla und einen Boiler: Laden in den günstigsten Stunden kostet 6,5 statt 18,9 ct\u002FkWh, knapp 590 € im Jahr, ohne 20-A-Sicherung zu sprengen.","Titelbild: drei Balken vergleichen 18,9, 13,1 und 6,5 ct\u002FkWh für Laden um 18:00, über Nacht und in den günstigsten Stunden.",1792177149336]