[{"data":1,"prerenderedAt":654},["ShallowReactive",2],{"blog-nuxt-i18n-multilingual-seo-hreflang-en":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":54,"title":57,"description":58,"coverAlt":59,"metaTitle":60,"takeaways":61,"faq":67,"toc":83,"blocks":114,"others":448},"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","Multilingual SEO with Nuxt i18n: URL strategy, hreflang, canonicals and sitemaps","How to set up Nuxt i18n so search engines index every language: routing strategy, useLocaleHead, hreflang rules, partial translations and sitemaps.","Three language versions of one page, English, German and Hungarian, linked to each other with hreflang arrows and an x-default fallback.","Nuxt i18n SEO: hreflang, canonicals, sitemaps · Balázs Csorba",[62,63,64,65,66],"Give every language its own URL; Google recommends this over cookies or browser settings, and Googlebot sends no Accept-Language header.","In @nuxtjs\u002Fi18n, set language on every locale and a production baseUrl, then call useLocaleHead to get lang, hreflang, og:locale and canonical tags.","hreflang sets must be identical on every version, include the page itself, use fully-qualified URLs and valid ISO language codes.","For pages that exist in fewer languages, list only the real translations and point untranslated copies' canonical at the original.","Do not redirect by browser language; offer a language switcher with real links and let hreflang send searchers to the right version.",[68,71,74,77,80],{"q":69,"a":70},"Which Nuxt i18n routing strategy is best for SEO?","prefix_except_default and prefix both give every language its own URL, which is what Google recommends. prefix_except_default keeps the default language unprefixed. Avoid no_prefix for public content, because all languages share one URL and search engines can only index one of them.",{"q":72,"a":73},"Does useLocaleHead generate hreflang automatically?","Yes. It returns the html lang attribute, an alternate link with hreflang for every configured locale, og:locale tags and a canonical link. It needs a language value on each locale and a baseUrl so that the URLs are fully qualified.",{"q":75,"a":76},"Do I need x-default?","It is recommended. Google uses x-default for users whose language matches none of your versions. On most sites it points at the default-language version or at a language selector page.",{"q":78,"a":79},"Should hreflang go in the HTML head or in the sitemap?","Either. Google treats HTML tags, HTTP headers and sitemap entries as equivalent and says using several brings no extra benefit. Pick the one you can keep correct; for static sites generated from one data source, both can come from the same code.",{"q":81,"a":82},"Does Google use hreflang to detect the page language?","No. Google says it detects the language from the content with its own algorithms; hreflang only tells it which URLs are localized versions of the same page. A German page full of English text is still English to Google.",[84,87,90,93,96,99,102,105,108,111],{"id":85,"title":86},"url-strategy","Which URL strategy should a multilingual Nuxt site use?",{"id":88,"title":89},"configure-nuxt-i18n","How do you configure @nuxtjs\u002Fi18n for SEO?",{"id":91,"title":92},"use-locale-head","What does useLocaleHead put into the page head?",{"id":94,"title":95},"hreflang-rules","Which hreflang rules do sites get wrong?",{"id":97,"title":98},"partial-translations","What about pages that exist in only some languages?",{"id":100,"title":101},"sitemaps","Should hreflang also go into the sitemap?",{"id":103,"title":104},"no-automatic-redirects","Why not redirect visitors by browser language?",{"id":106,"title":107},"beyond-the-head","What else needs translating for multilingual SEO?",{"id":109,"title":110},"test-it","How do you verify the setup?",{"id":112,"title":113},"sources","Sources",[115,119,122,125,137,183,186,187,209,211,212,245,247,277,278,287,294,335,341,342,354,356,359,360,363,365,366,373,380,381,415,423,424,427,429,430],{"type":116,"content":117},"paragraph",[118],"A multilingual Nuxt site can look perfect to visitors and still be half invisible to search engines: the German pages are not indexed, Google shows the English page to Hungarian searchers, or two language versions compete for the same query. Almost always the cause is in the head of the page: missing or inconsistent hreflang links, a canonical that points at the wrong language, or a URL strategy that hides languages behind a cookie.",{"type":116,"content":120},[121],"This article walks through a setup with @nuxtjs\u002Fi18n that search engines understand, using this site as the example: it is generated statically in English, German and Hungarian. It covers the routing strategy, useLocaleHead, the hreflang rules from Google's documentation, pages that exist in only some languages, sitemaps, and why automatic language redirects hurt.",{"type":123,"level":124,"id":85,"text":86},"heading",2,{"type":116,"content":126},[127,128,132,133,136],"Google's guidance on ",{"tag":129,"href":46,"children":130},"a",[131],"multilingual sites"," is direct: use different URLs for each language version rather than cookies or browser settings. If content changes by language on one URL, Google might not find all variants, because Googlebot usually crawls from the USA and sends requests without an Accept-Language header. Nuxt i18n offers four ",{"tag":129,"href":40,"children":134},[135],"routing strategies",":",{"type":138,"head":139,"rows":146},"table",[140,142,144],[141],"Strategy",[143],"URLs",[145],"SEO verdict",[147,156,165,174],[148,152,154],[149],{"tag":150,"children":151},"code",[20],[153],"\u002Fblog, \u002Fde\u002Fblog, \u002Fhu\u002Fblog",[155],"Good default; the main language keeps clean URLs",[157,161,163],[158],{"tag":150,"children":159},[160],"prefix",[162],"\u002Fen\u002Fblog, \u002Fde\u002Fblog, \u002Fhu\u002Fblog",[164],"Good; symmetric, needs a root redirect or language selector",[166,170,172],[167],{"tag":150,"children":168},[169],"prefix_and_default",[171],"\u002Fblog and \u002Fen\u002Fblog, \u002Fde\u002Fblog",[173],"Works, but creates duplicates; the canonical goes to the unprefixed URL",[175,179,181],[176],{"tag":150,"children":177},[178],"no_prefix",[180],"\u002Fblog for all",[182],"Avoid for public content; one URL can only be indexed in one language",{"type":116,"content":184},[185],"Subdirectories on one domain are also the cheapest option to run. Separate country domains make sense when the business is genuinely separate per country; they are not needed just to serve a second language.",{"type":123,"level":124,"id":88,"text":89},{"type":116,"content":188},[189,190,193,194,197,198,200,201,204,205,208],"Two settings are required for the ",{"tag":129,"href":37,"children":191},[192],"SEO features"," to produce correct output: each locale needs a ",{"tag":150,"children":195},[196],"language"," tag, which becomes the ",{"tag":150,"children":199},[12]," value and the ",{"tag":150,"children":202},[203],"\u003Chtml lang>",", and ",{"tag":150,"children":206},[207],"baseUrl"," must be the production origin, because hreflang and canonical URLs have to be fully qualified. This is the configuration of this site:",{"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()"," returns head metadata that you pass to ",{"tag":150,"children":219},[220],"useHead",": the ",{"tag":150,"children":223},[224],"lang"," attribute for ",{"tag":150,"children":227},[228],"\u003Chtml>",", an alternate link with ",{"tag":150,"children":231},[12]," for every configured locale, ",{"tag":150,"children":234},[235],"og:locale"," and ",{"tag":150,"children":238},[239],"og:locale:alternate"," meta tags, and a canonical link. Call it once in ",{"tag":150,"children":242},[243],"app.vue"," so every page gets it.",{"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,236,265,268,269,272,273,276],"For a page at ",{"tag":150,"children":251},[252],"\u002Fde\u002Fblog\u002Fpost",", the rendered head then contains ",{"tag":150,"children":255},[256],"\u003Chtml lang=\"de-AT\">",", a canonical link to the German URL, and alternate links for ",{"tag":150,"children":259},[260],"en-US",", ",{"tag":150,"children":263},[264],"de-AT",{"tag":150,"children":266},[267],"hu-HU",". The module also adds a catch-all link per language group, such as ",{"tag":150,"children":270},[271],"hreflang=\"en\"",", and an ",{"tag":150,"children":274},[275],"x-default",". Check the generated HTML once per page type; it is easy to assume the head is right because the module is installed.",{"type":123,"level":124,"id":94,"text":95},{"type":279,"attrs":280,"inner":284,"caption":285},"diagram",{"viewBox":281,"role":282,"aria-labelledby":283},"0 0 720 310","img","nhl-t nhl-d","\u003Ctitle id=\"nhl-t\">An hreflang cluster\u003C\u002Ftitle>\u003Cdesc id=\"nhl-d\">Three language versions of one page link to each other in both directions, each also listing itself, and an x-default link points at the English version for unmatched languages.\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\">Every version lists all three plus x-default, including itself.\u003C\u002Ftext>",[286],"hreflang only works as a closed set: if the German page does not link back, Google may ignore the pair.",{"type":116,"content":288},[289,290,293],"Google's ",{"tag":129,"href":43,"children":291},[292],"hreflang documentation"," is short, and most of the bugs I find in audits break one of these rules:",{"type":295,"items":296},"list",[297,303,311,323,330],[298,302],{"tag":299,"children":300},"strong",[301],"Include the page itself."," Each version lists one link per variant, including itself, and the set is identical on every version.",[304,307,308,310],{"tag":299,"children":305},[306],"Use fully-qualified URLs,"," with protocol and host. Relative hrefs are the usual result of a missing ",{"tag":150,"children":309},[207],".",[312,315,316,318,319,322],{"tag":299,"children":313},[314],"Use valid codes:"," an ISO 639-1 language, optionally followed by an ISO 3166-1 region (",{"tag":150,"children":317},[264],"). A region alone does not work, and codes like ",{"tag":150,"children":320},[321],"es-419"," are not supported.",[324,329],{"tag":299,"children":325},[326,327],"Add ",{"tag":150,"children":328},[275]," for users whose language matches none of your versions, usually the default-language page.",[331,334],{"tag":299,"children":332},[333],"Make the canonical self-referencing"," on each language version. A German page whose canonical points at the English page tells Google to drop the German one.",{"type":116,"content":336},[337,338,340],"One more fact worth knowing: Google says it does not use hreflang or the ",{"tag":150,"children":339},[224]," attribute to detect a page's language; it detects language from the content. A translated template with untranslated body text is still an English page.",{"type":123,"level":124,"id":97,"text":98},{"type":116,"content":343},[344,345,347,348,350,351,353],"With ",{"tag":150,"children":346},[20],", Nuxt generates a route for every locale, so an English-only article still renders at ",{"tag":150,"children":349},[252]," with German navigation and an English body. Left alone, useLocaleHead advertises it as a German translation, and the canonical of the German copy points at itself. This site fixes both in ",{"tag":150,"children":352},[243],": a page that exists in fewer languages registers them, hreflang then lists only those, and untranslated copies point their canonical at the English original.",{"type":150,"code":355},"\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":357},[358],"The alternative is not to generate untranslated routes at all and to return a 404 or a redirect to the original. That is cleaner for search engines but worse for a visitor who switched the interface to German and wants to keep it. Either works, as long as the head tells the truth about which translations exist.",{"type":123,"level":124,"id":100,"text":101},{"type":116,"content":361},[362],"Google accepts hreflang in three places, HTML tags, HTTP headers and the sitemap, treats them as equivalent, and says using several brings no benefit in Search but makes maintenance harder. HTTP headers are mainly useful for non-HTML files such as PDFs. This site emits both head tags and sitemap entries because both are generated from the same post data at build time, so they cannot drift apart. If yours come from different code paths, pick one.",{"type":150,"code":364},"\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":367},[368,369,372],"Google advises against redirecting users to a language version based on what you think their language is, because it can stop users and search engines from reaching all versions. Nuxt i18n's ",{"tag":150,"children":370},[371],"detectBrowserLanguage"," can redirect on the first visit; for a public site I turn it off and rely on two things instead: hreflang, which sends searchers to the right version directly from the results page, and a language switcher made of real links, so crawlers can follow it too.",{"type":374,"variant":375,"title":376,"body":377},"callout","tip","Remember an explicit choice, not a guess",[378],[379],"Storing the language a visitor picked in the switcher in a cookie is fine; it reflects a decision. Redirecting on the browser's Accept-Language header is a guess, and Googlebot does not send that header at all.",{"type":123,"level":124,"id":106,"text":107},{"type":295,"items":382},[383,388,401,406],[384,387],{"tag":299,"children":385},[386],"Titles and meta descriptions per locale."," They are what searchers read in results; a German page with an English title loses clicks.",[389,395,396,310],{"tag":299,"children":390},[391,392],"Structured data with ",{"tag":150,"children":393},[394],"inLanguage"," that matches the page, so AI search and rich results quote the right version. I cover this in the ",{"tag":397,"to":398,"children":399},"link","\u002Fblog\u002Fgenerative-engine-optimization-audit",[400],"GEO audit of this site",[402,405],{"tag":299,"children":403},[404],"Image alt text and Open Graph images"," with localized text, if the image contains words.",[407,410,411,414],{"tag":299,"children":408},[409],"Internal links that stay in the language."," Use ",{"tag":150,"children":412},[413],"localePath()"," everywhere, so a German article links to German pages and does not leak visitors back to English.",{"type":116,"content":416},[417,418,422],"Performance matters as much in every language; the same rules as in ",{"tag":397,"to":419,"children":420},"\u002Fblog\u002Fnuxt-core-web-vitals-performance",[421],"Core Web Vitals for Nuxt"," apply, and lazy-loading the message files per locale keeps the bundle small.",{"type":123,"level":124,"id":109,"text":110},{"type":116,"content":425},[426],"Fetch the generated HTML of one page per type and language and compare the link sets; a short script catches the common mistakes before Search Console does, weeks later.",{"type":150,"code":428},"\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":295,"ordered":431,"items":432},true,[433,436,439,442,445],[434],{"tag":129,"href":37,"children":435},[36],[437],{"tag":129,"href":40,"children":438},[39],[440],{"tag":129,"href":43,"children":441},[42],[443],{"tag":129,"href":46,"children":444},[45],[446],{"tag":129,"href":49,"children":447},[48],[449,517,569,608],{"slug":450,"published":5,"minutes":451,"category":7,"tags":452,"keywords":457,"about":470,"sources":477,"cover":511,"og":512,"expertise":52,"locales":513,"lang":54,"title":514,"description":515,"coverAlt":516},"llm-endpoint-abuse-denial-of-wallet",14,[9,453,454,455,456],"LLM security","Rate limiting","OWASP","Cost control",[458,459,460,461,462,463,464,465,466,467,468,469],"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",[471,473,476],{"name":454,"url":472},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRate_limiting",{"name":474,"url":475},"Denial-of-service attack","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FDenial-of-service_attack",{"name":9,"url":27},[478,481,484,487,490,493,496,499,502,505,508],{"title":479,"url":480},"OWASP GenAI Security Project: LLM10:2025 Unbounded Consumption","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm102025-unbounded-consumption\u002F",{"title":482,"url":483},"Nuxt Security: Rate Limiter middleware","https:\u002F\u002Fnuxt-security.vercel.app\u002Fmiddleware\u002Frate-limiter",{"title":485,"url":486},"Nuxt Security: Request Size Limiter middleware","https:\u002F\u002Fnuxt-security.vercel.app\u002Fmiddleware\u002Frequest-size-limiter",{"title":488,"url":489},"nuxt-auth-utils: README (GitHub)","https:\u002F\u002Fgithub.com\u002Fatinux\u002Fnuxt-auth-utils",{"title":491,"url":492},"h3: request utilities (getRequestIP, readValidatedBody)","https:\u002F\u002Fh3.dev\u002Futils\u002Frequest",{"title":494,"url":495},"Upstash: Ratelimiting algorithms","https:\u002F\u002Fupstash.com\u002Fdocs\u002Fredis\u002Fsdks\u002Fratelimit-ts\u002Falgorithms",{"title":497,"url":498},"Cloudflare Turnstile: Validate the token","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fturnstile\u002Fget-started\u002Fserver-side-validation\u002F",{"title":500,"url":501},"Claude Platform Docs: Rate limits and spend limits","https:\u002F\u002Fplatform.claude.com\u002Fdocs\u002Fen\u002Fapi\u002Frate-limits",{"title":503,"url":504},"Claude Platform Docs: Token counting","https:\u002F\u002Fplatform.claude.com\u002Fdocs\u002Fen\u002Fbuild-with-claude\u002Ftoken-counting",{"title":506,"url":507},"OpenAI API Docs: Spend limits","https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fguides\u002Fspend-limits",{"title":509,"url":510},"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: protecting public LLM endpoints in a Nuxt app","How to stop abuse and runaway cost on a public LLM route in Nuxt: auth, per-user rate limits, prompt caps, token budgets, Turnstile and spend limits.","Requests to a Nuxt chat endpoint pass through bot checks, sessions, rate limits, prompt caps and a token budget before reaching the model API.",{"slug":518,"published":519,"minutes":520,"category":7,"tags":521,"keywords":525,"about":533,"sources":540,"cover":562,"og":563,"expertise":564,"locales":565,"lang":54,"title":566,"description":567,"coverAlt":568},"llms-txt-vs-markdown-content-negotiation","2026-10-07",9,[522,523,524,9],"llms.txt","Content negotiation","AI agents",[522,526,527,528,529,530,531,532],"markdown for agents","content negotiation","AI crawlers","generative engine optimization","Accept text\u002Fmarkdown header","llms-full.txt","agent-ready website",[534,537],{"name":535,"url":536},"Markdown","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FMarkdown",{"name":538,"url":539},"Hypertext Transfer Protocol","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHypertext_Transfer_Protocol",[541,544,547,550,553,556,559],{"title":542,"url":543},"llmstxt.org: The \u002Fllms.txt file (proposal, version 2)","https:\u002F\u002Fllmstxt.org\u002F",{"title":545,"url":546},"llmstxt.org: Changes from v1 to v2","https:\u002F\u002Fllmstxt.org\u002Fchanges.html",{"title":548,"url":549},"Cloudflare changelog: Markdown for Agents (12 February 2026)","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fchangelog\u002Fpost\u002F2026-02-12-markdown-for-agents\u002F",{"title":551,"url":552},"Cloudflare docs: Markdown for Agents","https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Freference\u002Fmarkdown-for-agents\u002F",{"title":554,"url":555},"Ahrefs: 137K sites analyzed, 97% of llms.txt files never get read (June 2026)","https:\u002F\u002Fahrefs.com\u002Fblog\u002Fllmstxt-study\u002F",{"title":557,"url":558},"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":560,"url":561},"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 vs Markdown content negotiation: what agents actually fetch","llms.txt is a proposal, Markdown content negotiation is a header. What AI agents fetch, what the logs show, and how to serve both from Nuxt and nginx.","Four stacked layers that serve AI agents: declared WebMCP tools, a Markdown copy of every page, a negotiated Markdown response, and an llms.txt index.",{"slug":570,"published":571,"minutes":6,"category":7,"tags":572,"keywords":578,"about":586,"sources":595,"cover":602,"og":603,"expertise":52,"locales":604,"lang":54,"title":605,"description":606,"coverAlt":607},"multiplayer-sailing-game-threejs","2026-10-05",[573,574,575,576,577],"three.js","WebGL","GLSL","WebSockets","Game development",[579,580,581,582,583,584,585,575],"three.js game","three.js multiplayer","Gerstner waves","WebGL ocean shader","day night cycle three.js","WebSocket relay","browser game development",[587,590,592],{"name":588,"url":589},"Three.js","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FThree.js",{"name":574,"url":591},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebGL",{"name":593,"url":594},"WebSocket","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebSocket",[596,599],{"title":597,"url":598},"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":600,"url":601},"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],"Building a multiplayer 3D sailing game with plain three.js","Gerstner waves shared by GPU and CPU, one sky function for sky, water and fog, a five-minute day\u002Fnight cycle and a tiny WebSocket relay – the tech behind the game on my portfolio.","A dragon-prowed junk with glowing lanterns sailing across a choppy bay at dusk, forested hills on both sides.",{"slug":609,"published":610,"minutes":520,"category":7,"tags":611,"keywords":617,"about":626,"sources":635,"cover":648,"og":649,"expertise":52,"locales":650,"lang":54,"title":651,"description":652,"coverAlt":653},"home-assistant-ev-charging-energy-manager","2026-09-16",[612,613,614,615,616],"Home Assistant","EPEX Spot","Energy prices","Tesla","Energy management",[618,619,620,621,622,623,624,625],"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",[627,630,632],{"name":628,"url":629},"EPEX SPOT","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FEPEX_SPOT",{"name":612,"url":631},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHome_Assistant",{"name":633,"url":634},"Load management","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLoad_management",[636,639,642,645],{"title":637,"url":638},"aWATTar Austria: market data API","https:\u002F\u002Fwww.awattar.at\u002Fservices\u002Fapi",{"title":640,"url":641},"EPEX SPOT: market results","https:\u002F\u002Fwww.epexspot.com\u002Fen\u002Fmarket-results",{"title":643,"url":644},"Home Assistant developer docs: apps","https:\u002F\u002Fdevelopers.home-assistant.io\u002Fdocs\u002Fapps\u002F",{"title":646,"url":647},"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],"Charging on EPEX Austria prices: what my Home Assistant app saves","A year of hourly EPEX prices for Austria, replayed for a Tesla and a water boiler: cheapest-hour charging costs 6.5 instead of 18.9 ct\u002FkWh, close to €590 a year, without blowing a 20 A fuse.","Cover: three bars comparing 18.9, 13.1 and 6.5 ct\u002FkWh for charging at 18:00, overnight and in the cheapest hours.",1792177152177]