diff --git a/client/app.vue b/client/app.vue
index 2870c478..057b147d 100644
--- a/client/app.vue
+++ b/client/app.vue
@@ -286,7 +286,7 @@ const userSources = computed(() => (data.value?.globalSources || []).filter(s =>
{{ sitemap.sitemapName }}
diff --git a/client/composables/rpc.ts b/client/composables/rpc.ts
index 0964d6e9..88dd4294 100644
--- a/client/composables/rpc.ts
+++ b/client/composables/rpc.ts
@@ -1,5 +1,5 @@
import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
-import type { $Fetch } from 'nitropack'
+import type { $Fetch } from 'ofetch'
import { ref, watchEffect } from 'vue'
import type { NuxtDevtoolsClient } from '@nuxt/devtools-kit/types'
import { refreshSources } from './state'
diff --git a/package.json b/package.json
index 759b857c..51db44ce 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,7 @@
"test": "vitest run && pnpm run test:attw",
"test:unit": "vitest --project=unit",
"test:attw": "attw --pack",
- "typecheck": "vue-tsc --noEmit"
+ "typecheck": "nuxt typecheck"
},
"dependencies": {
"@nuxt/devtools-kit": "^3.1.1",
@@ -93,6 +93,7 @@
}
},
"devDependencies": {
+ "vue-i18n-routing": "^1.2.0",
"@arethetypeswrong/cli": "^0.18.2",
"@nuxt/content": "^3.9.0",
"@nuxt/eslint-config": "^1.11.0",
diff --git a/playground/tsconfig.json b/playground/tsconfig.json
new file mode 100644
index 00000000..aca7bba9
--- /dev/null
+++ b/playground/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "./.nuxt/tsconfig.app.json"
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f54f63a9..bc315821 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -122,6 +122,9 @@ importers:
vitest:
specifier: ^3
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.8.1)(happy-dom@20.0.11)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
+ vue-i18n-routing:
+ specifier: ^1.2.0
+ version: 1.2.0(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
vue-tsc:
specifier: ^3.1.6
version: 3.1.6(typescript@5.9.3)
@@ -1016,6 +1019,10 @@ packages:
resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==}
engines: {node: '>= 16'}
+ '@intlify/shared@9.14.5':
+ resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==}
+ engines: {node: '>= 16'}
+
'@intlify/unplugin-vue-i18n@11.0.1':
resolution: {integrity: sha512-nH5NJdNjy/lO6Ne8LDtZzv4SbpVsMhPE+LbvBDmMeIeJDiino8sOJN2QB3MXzTliYTnqe3aB9Fw5+LJ/XVaXCg==}
engines: {node: '>= 20'}
@@ -1033,6 +1040,22 @@ packages:
resolution: {integrity: sha512-8i3uRdAxCGzuHwfmHcVjeLQBtysQB2aXl/ojoagDut5/gY5lvWCQ2+cnl2TiqE/fXj/D8EhWG/SLKA7qz4a3QA==}
engines: {node: '>= 18'}
+ '@intlify/vue-i18n-bridge@1.1.0':
+ resolution: {integrity: sha512-yBwGpr70Rc56pjsPdtvNRi/ju0P9h3670EkCOuxAzKKR5OH61uF9LprLUGmph/Uy2TXBO2DKqpnJBFXyXJQKeg==}
+ engines: {node: '>= 12'}
+ hasBin: true
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue-i18n: ^8.26.1 || >=9.2.0
+ vue-i18n-bridge: '>=9.2.0'
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+ vue-i18n:
+ optional: true
+ vue-i18n-bridge:
+ optional: true
+
'@intlify/vue-i18n-extensions@8.0.0':
resolution: {integrity: sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==}
engines: {node: '>= 18'}
@@ -1051,6 +1074,19 @@ packages:
vue-i18n:
optional: true
+ '@intlify/vue-router-bridge@1.1.0':
+ resolution: {integrity: sha512-EX+KndT9VS3muMdZWFmc99D8nUaWTOXr322a8zNf5HnMCbpbogdifWYW8hat+nVE73St/gcDbPz6u5smVUPoQg==}
+ engines: {node: '>= 12'}
+ hasBin: true
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue-router: ^4.0.0-0 || ^3.0.0
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+ vue-router:
+ optional: true
+
'@ioredis/commands@1.4.0':
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
@@ -7253,6 +7289,27 @@ packages:
vue-flow-layout@0.2.0:
resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==}
+ vue-i18n-routing@1.2.0:
+ resolution: {integrity: sha512-pn+bIFRMX5BN1BVQJ5rn05dYVnBhU/QnkxhjEJAe9HnYtJhDubetvoY+yfgDNWwesNWfHbbvsilsgSGL6DJyeA==}
+ engines: {node: '>= 14.6'}
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue: ^2.6.14 || ^2.7.0 || ^3.2.0
+ vue-i18n: ^8.26.1 || >=9.2.0
+ vue-i18n-bridge: '>=9.2.0'
+ vue-router: ^3.5.3 || ^3.6.0 || ^4.0.0
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+ vue:
+ optional: true
+ vue-i18n:
+ optional: true
+ vue-i18n-bridge:
+ optional: true
+ vue-router:
+ optional: true
+
vue-i18n@11.1.12:
resolution: {integrity: sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==}
engines: {node: '>= 16'}
@@ -8222,6 +8279,8 @@ snapshots:
'@intlify/shared@11.1.12': {}
+ '@intlify/shared@9.14.5': {}
+
'@intlify/unplugin-vue-i18n@11.0.1(@vue/compiler-dom@3.5.25)(eslint@9.39.1(jiti@2.6.1))(rollup@4.53.3)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
@@ -8248,6 +8307,10 @@ snapshots:
'@intlify/utils@0.13.0': {}
+ '@intlify/vue-i18n-bridge@1.1.0(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))':
+ optionalDependencies:
+ vue-i18n: 11.1.12(vue@3.5.25(typescript@5.9.3))
+
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.25)(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@babel/parser': 7.28.4
@@ -8257,6 +8320,14 @@ snapshots:
vue: 3.5.25(typescript@5.9.3)
vue-i18n: 11.1.12(vue@3.5.25(typescript@5.9.3))
+ '@intlify/vue-router-bridge@1.1.0(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))':
+ dependencies:
+ vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3))
+ optionalDependencies:
+ vue-router: 4.6.3(vue@3.5.25(typescript@5.9.3))
+ transitivePeerDependencies:
+ - vue
+
'@ioredis/commands@1.4.0': {}
'@isaacs/balanced-match@4.0.1': {}
@@ -16306,6 +16377,18 @@ snapshots:
vue-flow-layout@0.2.0: {}
+ vue-i18n-routing@1.2.0(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
+ dependencies:
+ '@intlify/shared': 9.14.5
+ '@intlify/vue-i18n-bridge': 1.1.0(vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)))
+ '@intlify/vue-router-bridge': 1.1.0(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
+ ufo: 1.6.1
+ vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3))
+ optionalDependencies:
+ vue: 3.5.25(typescript@5.9.3)
+ vue-i18n: 11.1.12(vue@3.5.25(typescript@5.9.3))
+ vue-router: 4.6.3(vue@3.5.25(typescript@5.9.3))
+
vue-i18n@11.1.12(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@intlify/core-base': 11.1.12
diff --git a/src/devtools.ts b/src/devtools.ts
index 04c543a8..5c37a872 100644
--- a/src/devtools.ts
+++ b/src/devtools.ts
@@ -24,8 +24,9 @@ export function setupDevToolsUI(options: ModuleOptions, resolve: Resolver['resol
// In local development, start a separate Nuxt Server and proxy to serve the client
else {
nuxt.hook('vite:extendConfig', (config) => {
- config.server = config.server || {}
- config.server.proxy = config.server.proxy || {}
+ // @ts-expect-error vite config server is readonly but we need to mutate it
+ if (!config.server) config.server = {}
+ if (!config.server.proxy) config.server.proxy = {}
config.server.proxy[DEVTOOLS_UI_ROUTE] = {
target: `http://localhost:${DEVTOOLS_UI_LOCAL_PORT}${DEVTOOLS_UI_ROUTE}`,
changeOrigin: true,
diff --git a/src/prerender.ts b/src/prerender.ts
index 95039156..4440f281 100644
--- a/src/prerender.ts
+++ b/src/prerender.ts
@@ -153,8 +153,9 @@ async function prerenderRoute(nitro: Nitro, route: string) {
const _route: PrerenderRoute = { route, fileName: route }
// Fetch the route
const encodedRoute = encodeURI(route)
+ const fetchUrl = withBase(encodedRoute, nitro.options.baseURL)
const res = await globalThis.$fetch.raw(
- withBase(encodedRoute, nitro.options.baseURL),
+ fetchUrl,
{
headers: { 'x-nitro-prerender': encodedRoute },
retry: nitro.options.prerender.retry,
@@ -171,10 +172,13 @@ async function prerenderRoute(nitro: Nitro, route: string) {
const filePath = join(nitro.options.output.publicDir, _route.fileName!)
await mkdir(dirname(filePath), { recursive: true })
const data = res._data
+ if (data === undefined) {
+ throw new Error(`No data returned from '${fetchUrl}'`)
+ }
if (filePath.endsWith('json') || typeof data === 'object')
await writeFile(filePath, JSON.stringify(data), 'utf8')
else
- await writeFile(filePath, data, 'utf8')
+ await writeFile(filePath, data as string, 'utf8')
_route.generateTimeMS = Date.now() - start
nitro._prerenderedRoutes!.push(_route)
nitro.logger.log(formatPrerenderRoute(_route))
diff --git a/src/runtime/server/plugins/nuxt-content-v2.ts b/src/runtime/server/plugins/nuxt-content-v2.ts
index f9b0ced2..ab2bc437 100644
--- a/src/runtime/server/plugins/nuxt-content-v2.ts
+++ b/src/runtime/server/plugins/nuxt-content-v2.ts
@@ -1,3 +1,5 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck this is for v2, consider it was stable and the types will not match when running type tests
import { defu } from 'defu'
import type { ParsedContentv2 } from '@nuxt/content'
import type { NitroApp } from 'nitropack/types'
diff --git a/src/runtime/server/routes/__sitemap__/debug.ts b/src/runtime/server/routes/__sitemap__/debug.ts
index d9e88466..a9e5d09e 100644
--- a/src/runtime/server/routes/__sitemap__/debug.ts
+++ b/src/runtime/server/routes/__sitemap__/debug.ts
@@ -1,5 +1,4 @@
import { defineEventHandler } from 'h3'
-import type { SitemapDefinition } from '../../../types'
import { useSitemapRuntimeConfig } from '../../utils'
import {
childSitemapSources,
@@ -17,8 +16,11 @@ export default defineEventHandler(async (e) => {
delete runtimeConfig.sitemaps
const globalSources = await globalSitemapSources()
const nitroOrigin = getNitroOrigin(e)
- const sitemaps: Record = {}
+ const sitemaps: Record> }> = {}
for (const s of Object.keys(_sitemaps)) {
+ if (!_sitemaps[s]) {
+ throw new Error('Could not resolve matching key in _sitemaps')
+ }
// resolve the sources
sitemaps[s] = {
..._sitemaps[s],
diff --git a/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v2.ts b/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v2.ts
index 6e948065..f7e004ff 100644
--- a/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v2.ts
+++ b/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v2.ts
@@ -1,4 +1,5 @@
import { defineEventHandler } from 'h3'
+// @ts-expect-error for nuxt v2 - type checking for nuxt v3
import type { ParsedContent } from '@nuxt/content'
// @ts-expect-error alias module
diff --git a/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.ts b/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.ts
index 41216aea..89800198 100644
--- a/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.ts
+++ b/src/runtime/server/routes/__sitemap__/nuxt-content-urls-v3.ts
@@ -2,13 +2,14 @@ import { defineEventHandler } from 'h3'
import { queryCollection } from '@nuxt/content/server'
// @ts-expect-error alias
import manifest from '#content/manifest'
+import type { Collections } from '@nuxt/content'
export default defineEventHandler(async (e) => {
- const collections = []
+ const collections: (keyof Collections)[] = []
// each collection in the manifest has a key => with fields which has a `sitemap`, we want to get all those
for (const collection in manifest) {
- if (manifest[collection].fields.sitemap) {
- collections.push(collection)
+ if (manifest[collection]?.fields?.sitemap) {
+ collections.push(collection as keyof Collections)
}
}
// now we need to handle multiple queries here, we want to run the requests in parralel
@@ -31,6 +32,7 @@ export default defineEventHandler(async (e) => {
.filter(c => c.sitemap !== false && c.path)
.flatMap(c => ({
loc: c.path,
+ // @ts-expect-error cannot figure out how to make sure this is resolvable when no Collections are in manifest
...(c.sitemap || {}),
}))
})
diff --git a/src/runtime/server/routes/sitemap.xml.ts b/src/runtime/server/routes/sitemap.xml.ts
index 217cf566..376ec2ba 100644
--- a/src/runtime/server/routes/sitemap.xml.ts
+++ b/src/runtime/server/routes/sitemap.xml.ts
@@ -13,5 +13,9 @@ export default defineEventHandler(async (e) => {
return sendRedirect(e, withBase('/sitemap_index.xml', useRuntimeConfig().app.baseURL), import.meta.dev ? 302 : 301)
}
- return createSitemap(e, Object.values(sitemaps)[0], runtimeConfig)
+ const sitemap = Object.values(sitemaps)
+ // if we had an index, we would have returned above. as we do not
+ // this is compatible with SitemapDefinition expected
+ const sm = sitemap[0] as typeof sitemaps['any_key_except_index']
+ return createSitemap(e, sm, runtimeConfig)
})
diff --git a/src/runtime/server/routes/sitemap/[sitemap].xml.ts b/src/runtime/server/routes/sitemap/[sitemap].xml.ts
index f255994c..3253bf6b 100644
--- a/src/runtime/server/routes/sitemap/[sitemap].xml.ts
+++ b/src/runtime/server/routes/sitemap/[sitemap].xml.ts
@@ -66,7 +66,7 @@ export default defineEventHandler(async (e) => {
}
// Get the appropriate sitemap configuration
- const sitemapConfig = getSitemapConfig(sitemapName, sitemaps, runtimeConfig.defaultSitemapsChunkSize)
+ const sitemapConfig = getSitemapConfig(sitemapName, sitemaps, runtimeConfig.defaultSitemapsChunkSize || 1000)
return createSitemap(e, sitemapConfig, runtimeConfig)
})
diff --git a/src/runtime/server/sitemap/builder/sitemap.ts b/src/runtime/server/sitemap/builder/sitemap.ts
index 1fe53493..a69e5179 100644
--- a/src/runtime/server/sitemap/builder/sitemap.ts
+++ b/src/runtime/server/sitemap/builder/sitemap.ts
@@ -70,10 +70,11 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, urls: SitemapU
for (const e of validI18nUrlsForTransform) {
// let's try and find other urls that we can use for alternatives
if (!e._i18nTransform && !e.alternatives?.length) {
- const alternatives = withoutPrefixPaths[e._pathWithoutPrefix]
- .map((u) => {
+ function processAlternatives(alts: undefined | NormalizedI18n[]) {
+ if (!alts) return []
+ return alts?.map((u) => {
const entries: AlternativeEntry[] = []
- if (u._locale.code === autoI18n.defaultLocale) {
+ if (u._locale.code === autoI18n?.defaultLocale) {
entries.push({
href: u.loc,
hreflang: 'x-default',
@@ -81,35 +82,41 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, urls: SitemapU
}
entries.push({
href: u.loc,
- hreflang: u._locale._hreflang || autoI18n.defaultLocale,
+ hreflang: u._locale._hreflang || autoI18n?.defaultLocale,
})
return entries
})
- .flat()
- .filter(Boolean) as AlternativeEntry[]
+ .flat()
+ }
+ const alternatives = processAlternatives(withoutPrefixPaths[e._pathWithoutPrefix])
+
if (alternatives.length)
e.alternatives = alternatives
}
else if (e._i18nTransform) {
delete e._i18nTransform
+ // @ts-expect-error looks to be checking an old possible value for strategy, no longer typed as valid
if (autoI18n.strategy === 'no_prefix') {
warnIncorrectI18nTransformUsage = true
}
- // keep single entry, just add alternatvies
+ // keep single entry, just add alternatives
if (autoI18n.differentDomains) {
- e.alternatives = [
- {
+ const alternatives: AutoI18nConfig['locales'] = []
+ const defaultLocale = autoI18n.locales.find(l => [l.code, l.language].includes(autoI18n.defaultLocale))
+ if (defaultLocale) {
+ alternatives.push({
// apply default locale domain
- ...autoI18n.locales.find(l => [l.code, l.language].includes(autoI18n.defaultLocale)),
+ ...defaultLocale,
code: 'x-default',
- },
- ...autoI18n.locales
- .filter(l => !!l.domain),
- ]
+ })
+ }
+ const localesForDomain = autoI18n.locales.filter(l => !!l.domain)
+ alternatives.push(...localesForDomain)
+ e.alternatives = alternatives
.map((locale) => {
return {
- hreflang: locale._hreflang,
- href: joinURL(withHttps(locale.domain!), e._pathWithoutPrefix),
+ hreflang: locale.hreflang,
+ href: joinURL(withHttps(String(locale.href)!), e._pathWithoutPrefix),
}
})
}
@@ -145,14 +152,12 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, urls: SitemapU
}
const _sitemap = isI18nMapped ? l._sitemap : undefined
- const newEntry: NormalizedI18n = preNormalizeEntry({
- _sitemap,
- ...e,
- _index: undefined,
- _key: `${_sitemap || ''}${loc || '/'}${e._path.search}`,
- _locale: l,
- loc,
- alternatives: [{ code: 'x-default', _hreflang: 'x-default' }, ...autoI18n.locales].map((locale) => {
+
+ const alternatives: AlternativeEntry[] = [
+ { code: 'x-default', _hreflang: 'x-default' },
+ ...autoI18n.locales,
+ ]
+ .map((locale): AlternativeEntry | false => {
const code = locale.code === 'x-default' ? autoI18n.defaultLocale : locale.code
const isDefault = locale.code === 'x-default' || locale.code === autoI18n.defaultLocale
let href = e._pathWithoutPrefix
@@ -192,13 +197,24 @@ export function resolveSitemapEntries(sitemap: SitemapDefinition, urls: SitemapU
if (!filterPath(href))
return false
+
return {
hreflang: locale._hreflang,
href,
}
- }).filter(Boolean),
+ })
+ .filter(l => l !== false)
+
+ const newEntry: ResolvedSitemapUrl = preNormalizeEntry({
+ _sitemap,
+ ...e,
+ _index: undefined,
+ _locale: l,
+ loc,
+ alternatives,
}, resolvers)
- if (e._locale.code === newEntry._locale.code) {
+
+ if (e._index !== undefined && e._locale.code === newEntry._locale?.code) {
// replace
_urls[e._index] = newEntry
// avoid getting re-replaced
@@ -253,7 +269,7 @@ export async function buildSitemapUrls(sitemap: SitemapDefinition, resolvers: Ni
}
function maybeSlice(urls: T): T {
- return sliceUrlsForChunk(urls, sitemap.sitemapName, sitemaps, defaultSitemapsChunkSize) as T
+ return sliceUrlsForChunk(urls, sitemap.sitemapName, sitemaps, defaultSitemapsChunkSize !== false ? defaultSitemapsChunkSize : undefined) as T
}
if (autoI18n?.differentDomains) {
const domain = autoI18n.locales.find(e => [e.language, e.code].includes(sitemap.sitemapName))?.domain
diff --git a/src/runtime/server/sitemap/builder/xml.ts b/src/runtime/server/sitemap/builder/xml.ts
index 905c8753..8d462ee4 100644
--- a/src/runtime/server/sitemap/builder/xml.ts
+++ b/src/runtime/server/sitemap/builder/xml.ts
@@ -1,5 +1,13 @@
import { withQuery } from 'ufo'
-import type { ModuleRuntimeConfig, NitroUrlResolvers, ResolvedSitemapUrl } from '../../../types'
+import type {
+ AlternativeEntry,
+ GoogleNewsEntry,
+ ImageEntry,
+ ModuleRuntimeConfig,
+ NitroUrlResolvers,
+ ResolvedSitemapUrl,
+ VideoEntry,
+} from '../../../types'
import { xmlEscape } from '../../utils'
// Optimized XML escaping using string replace (faster than character loop)
@@ -52,7 +60,7 @@ function buildUrlXml(url: ResolvedSitemapUrl): string {
switch (key) {
case 'alternatives':
if (Array.isArray(value) && value.length > 0) {
- for (const alt of value) {
+ for (const alt of value as AlternativeEntry[]) {
const attrs = Object.entries(alt)
.map(([k, v]) => `${k}="${escapeValueForXml(v)}"`)
.join(' ')
@@ -63,13 +71,13 @@ function buildUrlXml(url: ResolvedSitemapUrl): string {
case 'images':
if (Array.isArray(value) && value.length > 0) {
- for (const img of value) {
+ for (const img of value as ImageEntry[]) {
parts[partIndex++] = ' '
- parts[partIndex++] = ` ${escapeValueForXml(img.loc)}`
+ parts[partIndex++] = ` ${escapeValueForXml(String(img.loc))}`
if (img.title) parts[partIndex++] = ` ${escapeValueForXml(img.title)}`
if (img.caption) parts[partIndex++] = ` ${escapeValueForXml(img.caption)}`
- if (img.geo_location) parts[partIndex++] = ` ${escapeValueForXml(img.geo_location)}`
- if (img.license) parts[partIndex++] = ` ${escapeValueForXml(img.license)}`
+ if (img.geoLocation) parts[partIndex++] = ` ${escapeValueForXml(img.geoLocation)}`
+ if (img.license) parts[partIndex++] = ` ${escapeValueForXml(String(img.license))}`
parts[partIndex++] = ' '
}
}
@@ -77,22 +85,17 @@ function buildUrlXml(url: ResolvedSitemapUrl): string {
case 'videos':
if (Array.isArray(value) && value.length > 0) {
- for (const video of value) {
+ for (const video of value as VideoEntry[]) {
parts[partIndex++] = ' '
parts[partIndex++] = ` ${escapeValueForXml(video.title)}`
if (video.thumbnail_loc) {
- parts[partIndex++] = ` ${escapeValueForXml(video.thumbnail_loc)}`
+ parts[partIndex++] = ` ${escapeValueForXml(String(video.thumbnail_loc))}`
}
parts[partIndex++] = ` ${escapeValueForXml(video.description)}`
if (video.content_loc) {
- parts[partIndex++] = ` ${escapeValueForXml(video.content_loc)}`
- }
- if (video.player_loc) {
- const attrs = video.player_loc.allow_embed ? ' allow_embed="yes"' : ''
- const autoplay = video.player_loc.autoplay ? ' autoplay="yes"' : ''
- parts[partIndex++] = ` ${escapeValueForXml(video.player_loc)}`
+ parts[partIndex++] = ` ${escapeValueForXml(String(video.content_loc))}`
}
if (video.duration !== undefined) {
parts[partIndex++] = ` ${video.duration}`
@@ -126,6 +129,7 @@ function buildUrlXml(url: ResolvedSitemapUrl): string {
if (video.price) {
const prices = Array.isArray(video.price) ? video.price : [video.price]
for (const price of prices) {
+ if (!price.price) continue
const attrs: string[] = []
if (price.currency) attrs.push(`currency="${price.currency}"`)
if (price.type) attrs.push(`type="${price.type}"`)
@@ -134,7 +138,7 @@ function buildUrlXml(url: ResolvedSitemapUrl): string {
}
}
if (video.uploader) {
- const info = video.uploader.info ? ` info="${escapeValueForXml(video.uploader.info)}"` : ''
+ const info = video.uploader.info ? ` info="${escapeValueForXml(String(video.uploader.info))}"` : ''
parts[partIndex++] = ` ${escapeValueForXml(video.uploader.uploader)}`
}
if (video.live !== undefined) {
@@ -150,8 +154,7 @@ function buildUrlXml(url: ResolvedSitemapUrl): string {
parts[partIndex++] = ` ${escapeValueForXml(video.category)}`
}
if (video.gallery_loc) {
- const title = video.gallery_loc.title ? ` title="${escapeValueForXml(video.gallery_loc.title)}"` : ''
- parts[partIndex++] = ` ${escapeValueForXml(video.gallery_loc)}`
+ parts[partIndex++] = ` ${escapeValueForXml(String(video.gallery_loc))}`
}
parts[partIndex++] = ' '
}
@@ -160,29 +163,18 @@ function buildUrlXml(url: ResolvedSitemapUrl): string {
case 'news':
if (value) {
+ const newsValue = value as GoogleNewsEntry
parts[partIndex++] = ' '
parts[partIndex++] = ' '
- parts[partIndex++] = ` ${escapeValueForXml(value.publication.name)}`
- parts[partIndex++] = ` ${escapeValueForXml(value.publication.language)}`
+ parts[partIndex++] = ` ${escapeValueForXml(newsValue.publication.name)}`
+ parts[partIndex++] = ` ${escapeValueForXml(newsValue.publication.language)}`
parts[partIndex++] = ' '
- if (value.title) {
- parts[partIndex++] = ` ${escapeValueForXml(value.title)}`
- }
- if (value.publication_date) {
- parts[partIndex++] = ` ${value.publication_date}`
- }
- if (value.access) {
- parts[partIndex++] = ` ${value.access}`
- }
- if (value.genres) {
- parts[partIndex++] = ` ${escapeValueForXml(value.genres)}`
- }
- if (value.keywords) {
- parts[partIndex++] = ` ${escapeValueForXml(value.keywords)}`
+ if (newsValue.title) {
+ parts[partIndex++] = ` ${escapeValueForXml(newsValue.title)}`
}
- if (value.stock_tickers) {
- parts[partIndex++] = ` ${escapeValueForXml(value.stock_tickers)}`
+ if (newsValue.publication_date) {
+ parts[partIndex++] = ` ${newsValue.publication_date}`
}
parts[partIndex++] = ' '
}
diff --git a/src/runtime/server/sitemap/nitro.ts b/src/runtime/server/sitemap/nitro.ts
index e00fe033..56841ce3 100644
--- a/src/runtime/server/sitemap/nitro.ts
+++ b/src/runtime/server/sitemap/nitro.ts
@@ -17,13 +17,13 @@ import { normaliseEntry, preNormalizeEntry } from './urlset/normalise'
import { sortInPlace } from './urlset/sort'
// @ts-expect-error virtual
import { getPathRobotConfig } from '#internal/nuxt-robots/getPathRobotConfig' // can't solve this
-import { useSiteConfig } from '#site-config/server/composables/useSiteConfig'
import { createSitePathResolver } from '#site-config/server/composables/utils'
+import { getSiteConfig } from '#site-config/server/composables'
export function useNitroUrlResolvers(e: H3Event): NitroUrlResolvers {
const canonicalQuery = getQuery(e).canonical
const isShowingCanonical = typeof canonicalQuery !== 'undefined' && canonicalQuery !== 'false'
- const siteConfig = useSiteConfig(e)
+ const siteConfig = getSiteConfig(e)
return {
event: e,
fixSlashes: (path: string) => fixSlashes(siteConfig.trailingSlash, path),
@@ -42,12 +42,12 @@ async function buildSitemapXml(event: H3Event, definition: SitemapDefinition, re
const { sitemapName } = definition
const nitro = useNitroApp()
if (import.meta.prerender) {
- const config = useSiteConfig(event)
+ const config = getSiteConfig(event)
if (!config.url && !nitro._sitemapWarned) {
nitro._sitemapWarned = true
logger.error('Sitemap Site URL missing!')
logger.info('To fix this please add `{ site: { url: \'site.com\' } }` to your Nuxt config or a `NUXT_PUBLIC_SITE_URL=site.com` to your .env. Learn more at https://nuxtseo.com/site-config/getting-started/how-it-works')
- throw new createError({
+ throw new (createError as any)({
statusMessage: 'You must provide a site URL to prerender a sitemap.',
statusCode: 500,
})
@@ -60,8 +60,7 @@ async function buildSitemapXml(event: H3Event, definition: SitemapDefinition, re
// Process in place to avoid creating intermediate arrays
let validCount = 0
- for (let i = 0; i < sitemapUrls.length; i++) {
- const u = sitemapUrls[i]
+ for (const u of sitemapUrls) {
const path = u._path?.pathname || u.loc
// Early continue for robots blocked paths
@@ -81,7 +80,7 @@ async function buildSitemapXml(event: H3Event, definition: SitemapDefinition, re
// Skip invalid entries
if (routeRules.sitemap === false)
continue
- // @ts-expect-error runtime types
+
if (typeof routeRules.robots !== 'undefined' && !routeRules.robots)
continue
diff --git a/src/runtime/server/sitemap/urlset/normalise.ts b/src/runtime/server/sitemap/urlset/normalise.ts
index f8e1fda1..7997c358 100644
--- a/src/runtime/server/sitemap/urlset/normalise.ts
+++ b/src/runtime/server/sitemap/urlset/normalise.ts
@@ -17,7 +17,7 @@ import type {
import { mergeOnKey } from '../../../utils-pure'
function resolve(s: string | URL, resolvers?: NitroUrlResolvers): string
-function resolve(s: string | undefined | URL, resolvers?: NitroUrlResolvers): string | undefined {
+function resolve(s: string | undefined | URL, resolvers?: NitroUrlResolvers): string | URL | undefined {
if (typeof s === 'undefined' || !resolvers)
return s
// convert url to string
@@ -37,8 +37,11 @@ function removeTrailingSlash(s: string) {
export function preNormalizeEntry(_e: SitemapUrl | string, resolvers?: NitroUrlResolvers): ResolvedSitemapUrl {
const e = (typeof _e === 'string' ? { loc: _e } : { ..._e }) as ResolvedSitemapUrl
+ // @ts-expect-error detecting older property, url, to update it
if (e.url && !e.loc) {
+ // @ts-expect-error moving old url property to new loc
e.loc = e.url
+ // @ts-expect-error url would exist here but still not typed
delete e.url
}
if (typeof e.loc !== 'string') {
@@ -50,7 +53,7 @@ export function preNormalizeEntry(_e: SitemapUrl | string, resolvers?: NitroUrlR
try {
e._path = e._abs ? parseURL(e.loc) : parsePath(e.loc)
}
- catch (e) {
+ catch (e: any) {
e._path = null
}
if (e._path) {
@@ -104,8 +107,7 @@ export function normaliseEntry(_e: ResolvedSitemapUrl, defaults: Omit ({ ...a }))
- for (let i = 0; i < alternatives.length; i++) {
- const alt = alternatives[i]
+ for (const alt of alternatives) {
// Modify in place
if (typeof alt.href === 'string') {
alt.href = resolve(alt.href, resolvers)
@@ -120,20 +122,23 @@ export function normaliseEntry(_e: ResolvedSitemapUrl, defaults: Omit ({ ...i }))
- for (let i = 0; i < images.length; i++) {
- images[i].loc = resolve(images[i].loc, resolvers)
- }
+ images.forEach((image, i) => {
+ if (!images[i]) return
+ images[i].loc = resolve(image.loc, resolvers)
+ })
e.images = mergeOnKey(images, 'loc')
}
if (e.videos) {
// Process videos in place
const videos = e.videos.map(v => ({ ...v }))
- for (let i = 0; i < videos.length; i++) {
- if (videos[i].content_loc) {
- videos[i].content_loc = resolve(videos[i].content_loc, resolvers)
+ videos.forEach((v, i) => {
+ const contentLoc = v.content_loc
+ // current ts says videos[i] can be undefined. It cannot, but check added
+ if (contentLoc && videos[i]) {
+ videos[i].content_loc = resolve(contentLoc, resolvers)
}
- }
+ })
e.videos = mergeOnKey(videos, 'content_loc')
}
return e
@@ -156,7 +161,7 @@ export function normaliseDate(d: Date | string) {
// correct a time component without a timezone
if (d.includes('T')) {
const t = d.split('T')[1]
- if (!t.includes('+') && !t.includes('-') && !t.includes('Z')) {
+ if (!t || (t && !t.includes('+') && !t.includes('-') && !t.includes('Z'))) {
// add UTC timezone
d += 'Z'
}
diff --git a/src/runtime/server/sitemap/utils/chunk.ts b/src/runtime/server/sitemap/utils/chunk.ts
index 9e1bbb47..9cac7cb3 100644
--- a/src/runtime/server/sitemap/utils/chunk.ts
+++ b/src/runtime/server/sitemap/utils/chunk.ts
@@ -86,6 +86,10 @@ export function getSitemapConfig(
}
}
+ if (!sitemaps[sitemapName]) {
+ throw new Error(`Cannot find sitemap with name: '${sitemapName}'`)
+ }
+
// Regular sitemap
return sitemaps[sitemapName]
}
diff --git a/src/runtime/server/utils.ts b/src/runtime/server/utils.ts
index e0e6729a..44f2553e 100644
--- a/src/runtime/server/utils.ts
+++ b/src/runtime/server/utils.ts
@@ -21,6 +21,7 @@ export function useSitemapRuntimeConfig(e?: H3Event): ModuleRuntimeConfig {
// normalize the filters for runtime
for (const k in clone.sitemaps) {
const sitemap = clone.sitemaps[k]
+ if (!sitemap) continue
sitemap.include = normalizeRuntimeFilters(sitemap.include)
sitemap.exclude = normalizeRuntimeFilters(sitemap.exclude)
clone.sitemaps[k] = sitemap
diff --git a/src/runtime/types.ts b/src/runtime/types.ts
index ea40d5aa..958ded6c 100644
--- a/src/runtime/types.ts
+++ b/src/runtime/types.ts
@@ -2,6 +2,23 @@ import type { FetchOptions } from 'ofetch'
import type { H3Event } from 'h3'
import type { ParsedURL } from 'ufo'
import type { NuxtI18nOptions } from '@nuxtjs/i18n'
+import type { MaybeArray } from 'unhead/types'
+
+declare module 'nitropack/types' {
+ interface NitroApp {
+ _sitemapWarned?: boolean
+ }
+
+ interface NitroRouteConfig {
+ robots?: boolean
+ }
+}
+
+declare module 'nitropack' {
+ interface NitroRouteRules {
+ robots?: boolean
+ }
+}
// we need to have the module options within the runtime entry
// as we don't want to depend on the module entry as it can cause
@@ -208,6 +225,8 @@ interface LocaleObject extends Record {
cache?: boolean
}[]
isCatchallLocale?: boolean
+ _sitemap?: string
+ _hreflang?: string
/**
* @deprecated in v9, use `language` instead
*/
@@ -217,7 +236,7 @@ interface LocaleObject extends Record {
export interface AutoI18nConfig {
differentDomains?: boolean
- locales: (LocaleObject & { _sitemap: string, _hreflang: string })[]
+ locales: LocaleObject[]
defaultLocale: string
strategy: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix'
pages?: Record>
@@ -226,7 +245,13 @@ export interface AutoI18nConfig {
export interface ModuleRuntimeConfig extends Pick {
version: string
isNuxtContentDocumentDriven: boolean
- sitemaps: { index?: Pick & { sitemaps: SitemapIndexEntry[] } } & Record & { _hasSourceChunk?: boolean }>
+ sitemaps: {
+ index?: Pick & { sitemaps: SitemapIndexEntry[] }
+ }
+ & Record<
+ string,
+ Omit & { _hasSourceChunk?: boolean }
+ >
autoI18n?: AutoI18nConfig
isMultiSitemap: boolean
isI18nMapped: boolean
@@ -294,7 +319,7 @@ export interface SitemapDefinition {
/**
* Default options for all URLs in the sitemap.
*/
- defaults?: Omit
+ defaults: Omit
/**
* Additional sources of URLs to include in the sitemap.
*/
@@ -392,12 +417,19 @@ export interface SitemapUrl {
videos?: Array
_i18nTransform?: boolean
_sitemap?: string | false
+ /**
+ * Added these for sitemap.ts on or around line 199
+ * const newEntry: ResolvedSitemapUrl = preNormalizeEntry({})
+ */
+ _index?: number
+ _key?: string
+ _locale?: LocaleObject
}
export type SitemapStrict = Required
export interface AlternativeEntry {
- hreflang: string
+ hreflang?: string
href: string | URL
}
@@ -436,6 +468,12 @@ export interface ImageEntry {
license?: string | URL
}
+export interface VideoEntryPrice {
+ price?: number | string
+ currency?: string
+ type?: 'rent' | 'purchase' | 'package' | 'subscription'
+}
+
export interface VideoEntry {
title: string
thumbnail_loc: string | URL
@@ -450,11 +488,7 @@ export interface VideoEntry {
family_friendly?: 'yes' | 'no' | boolean
restriction?: Restriction
platform?: Platform
- price?: ({
- price?: number | string
- currency?: string
- type?: 'rent' | 'purchase' | 'package' | 'subscription'
- })[]
+ price?: MaybeArray
requires_subscription?: 'yes' | 'no' | boolean
uploader?: {
uploader: string
@@ -462,6 +496,8 @@ export interface VideoEntry {
}
live?: 'yes' | 'no' | boolean
tag?: string | string[]
+ category?: string
+ gallery_loc?: string | URL
}
export interface Restriction {
diff --git a/src/runtime/utils-pure.ts b/src/runtime/utils-pure.ts
index ae93423d..bc834a8a 100644
--- a/src/runtime/utils-pure.ts
+++ b/src/runtime/utils-pure.ts
@@ -46,7 +46,7 @@ export function splitForLocales(path: string, locales: string[]): [string | null
// we only want to use the first path segment otherwise we can end up turning "/ending" into "/en/ding"
const prefix = withLeadingSlash(path).split('/')[1]
// make sure prefix is a valid locale
- if (locales.includes(prefix))
+ if (prefix && locales.includes(prefix))
return [prefix, path.replace(`/${prefix}`, '')]
return [null, path]
}
@@ -62,7 +62,7 @@ export function normalizeRuntimeFilters(input?: FilterInput[]): (RegExp | string
return rule
// regex is already validated
const match = rule.regex.match(StringifiedRegExpPattern)
- if (match)
+ if (match && match[1] && match[2])
return new RegExp(match[1], match[2])
return false
}).filter(Boolean) as (RegExp | string)[]
diff --git a/src/utils-internal/i18n.ts b/src/utils-internal/i18n.ts
index 37a7d179..db016488 100644
--- a/src/utils-internal/i18n.ts
+++ b/src/utils-internal/i18n.ts
@@ -44,10 +44,10 @@ export function generatePathForI18nPages(ctx: StrategyProps): string {
}
export function normalizeLocales(nuxtI18nConfig: NuxtI18nOptions): AutoI18nConfig['locales'] {
- let locales = nuxtI18nConfig.locales || []
+ const _locales: NonNullable = nuxtI18nConfig.locales || []
let onlyLocales = nuxtI18nConfig?.bundle?.onlyLocales || []
onlyLocales = typeof onlyLocales === 'string' ? [onlyLocales] : onlyLocales
- locales = mergeOnKey(locales.map((locale: any) => typeof locale === 'string' ? { code: locale } : locale), 'code')
+ let locales: AutoI18nConfig['locales'] = mergeOnKey(_locales.map((locale: any) => typeof locale === 'string' ? { code: locale } : locale), 'code')
if (onlyLocales.length) {
locales = locales.filter((locale: LocaleObject) => onlyLocales.includes(locale.code))
}
diff --git a/src/utils-internal/nuxtSitemap.ts b/src/utils-internal/nuxtSitemap.ts
index 3dd5032d..6d44c57a 100644
--- a/src/utils-internal/nuxtSitemap.ts
+++ b/src/utils-internal/nuxtSitemap.ts
@@ -10,12 +10,12 @@ import type { AutoI18nConfig, SitemapDefinition, SitemapUrl, SitemapUrlInput } f
import { createPathFilter } from '../runtime/utils-pure'
import type { CreateFilterOptions } from '../runtime/utils-pure'
-export async function resolveUrls(urls: Required['urls'], ctx: { logger: ConsolaInstance, path: string }): Promise {
+export async function resolveUrls(_urls: Required['urls'], ctx: { logger: ConsolaInstance, path: string }): Promise {
+ let urls: SitemapUrlInput[] = []
try {
- if (typeof urls === 'function')
- urls = urls()
- // resolve promise
- urls = await urls
+ if (typeof _urls === 'function') {
+ urls = await _urls()
+ }
}
catch (e) {
ctx.logger.error(`Failed to resolve ${typeof urls} urls.`)
@@ -71,7 +71,7 @@ function deepForEachPage(
if (opts.isI18nMicro) {
const localePattern = /\/:locale\(([^)]+)\)/
const match = localePattern.exec(currentPath || '')
- if (match) {
+ if (match && match[1]) {
const locales = match[1].split('|')
locales.forEach((locale) => {
const subPage = { ...page }
@@ -151,6 +151,9 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
pagesWithMeta.reduce((acc: Record, e) => {
if (e.page!.name?.includes(routesNameSeparator)) {
const [name, locale] = e.page!.name.split(routesNameSeparator)
+ if (!name) {
+ return acc
+ }
if (!acc[name])
acc[name] = []
const { _sitemap } = config.normalisedLocales.find(l => l.code === locale) || { _sitemap: locale }
@@ -170,6 +173,7 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
// we add pages without a prefix, they may have disabled i18n
return entries.map((e) => {
const [name] = (e.page?.name || '').split(routesNameSeparator)
+ if (!name) return false
// we need to check if the same page with a prefix exists within the default locale
// for example this will fix the `/` if the configuration is set to `prefix`
if (localeGroups[name]?.some(a => a.locale === config.defaultLocale))
@@ -222,7 +226,7 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
}
export function generateExtraRoutesFromNuxtConfig(nuxt: Nuxt = useNuxt()) {
- const filterForValidPage = p => p && !extname(p) && !p.startsWith('/api/') && !p.startsWith('/_')
+ const filterForValidPage = (p: string | undefined) => p && !extname(p) && !p.startsWith('/api/') && !p.startsWith('/_')
const routeRules = Object.entries(nuxt.options.routeRules || {})
.filter(([k, v]) => {
// make sure key doesn't use a wildcard and its not for a file
diff --git a/src/utils/parseSitemapXml.ts b/src/utils/parseSitemapXml.ts
index 9e1f49c1..4d47801d 100644
--- a/src/utils/parseSitemapXml.ts
+++ b/src/utils/parseSitemapXml.ts
@@ -1,4 +1,12 @@
-import type { SitemapUrlInput, VideoEntry, ImageEntry, AlternativeEntry, GoogleNewsEntry, SitemapStrict } from '../runtime/types'
+import type {
+ SitemapUrlInput,
+ VideoEntry,
+ ImageEntry,
+ AlternativeEntry,
+ GoogleNewsEntry,
+ SitemapStrict,
+ VideoEntryPrice
+} from '../runtime/types'
interface ParsedUrl {
loc?: string
@@ -384,7 +392,7 @@ function extractUrlFromParsedElement(
return {
price: String(priceValue),
currency: price.currency,
- type: price.type as VideoEntry['price'][0]['type'],
+ type: price.type as VideoEntryPrice['type'],
}
})
.filter((p): p is NonNullable => p !== null)
@@ -487,7 +495,7 @@ function extractUrlFromParsedElement(
}
// Filter out undefined values and empty arrays
- const filteredUrlObj = Object.fromEntries(
+ const filteredUrlObj: typeof urlObj = Object.fromEntries(
Object.entries(urlObj).filter(([_, value]) =>
value != null && (!Array.isArray(value) || value.length > 0),
),
diff --git a/tsconfig.json b/tsconfig.json
index c5c22aab..00bff21f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,7 @@
"extends": "./.nuxt/tsconfig.json",
"exclude": [
"test/**",
- "playground"
+ "playground",
+ "dist/"
]
}