Skip to content

Commit

Permalink
fix(i18n): filter out alternate paths based on include, exclude
Browse files Browse the repository at this point in the history
Fixes #273
  • Loading branch information
harlan-zw committed May 6, 2024
1 parent 0949698 commit f275c7a
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 50 deletions.
4 changes: 4 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,10 @@ declare module 'vue-router' {
strategy: nuxtI18nConfig.strategy || 'no_prefix',
routesNameSeparator: nuxtI18nConfig.routesNameSeparator,
normalisedLocales,
filter: {
include: normalizeFilters(config.include),
exclude: normalizeFilters(config.exclude),
},
})
if (!resolvedConfigUrls) {
config.urls && userGlobalSources.push({
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/nitro/sitemap/builder/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export async function buildSitemap(sitemap: SitemapDefinition, resolvers: NitroU
let resolvedSources = await resolveSitemapSources(sources)
// normalise the sources for i18n
if (autoI18n)
resolvedSources = normaliseI18nSources(resolvedSources, { autoI18n, isI18nMapped })
resolvedSources = normaliseI18nSources(resolvedSources, { autoI18n, isI18nMapped, ...sitemap })
// 1. normalise
const normalisedUrls = normaliseSitemapUrls(resolvedSources.map(e => e.urls).flat(), resolvers)

Expand Down Expand Up @@ -110,7 +110,7 @@ export async function buildSitemap(sitemap: SitemapDefinition, resolvers: NitroU
.filter(Boolean) as ResolvedSitemapUrl[]
// TODO enable
if (autoI18n?.locales)
enhancedUrls = applyI18nEnhancements(enhancedUrls, { isI18nMapped, autoI18n, sitemapName: sitemap.sitemapName })
enhancedUrls = applyI18nEnhancements(enhancedUrls, { isI18nMapped, autoI18n, ...sitemap })
// 3. filtered urls
// TODO make sure include and exclude start with baseURL?
const filteredUrls = filterSitemapUrls(enhancedUrls, { event: resolvers.event, isMultiSitemap, autoI18n, ...sitemap })
Expand Down
41 changes: 1 addition & 40 deletions src/runtime/nitro/sitemap/urlset/filter.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,9 @@
import { parseURL } from 'ufo'
import { createRouter, toRouteMatcher } from 'radix3'
import type { H3Event } from 'h3'
import type { ModuleRuntimeConfig, ResolvedSitemapUrl } from '../../../types'
import { createFilter } from '../../../utils-pure'
import { getPathRobotConfig } from '#imports'

interface CreateFilterOptions {
include?: (string | RegExp)[]
exclude?: (string | RegExp)[]
}

function createFilter(options: CreateFilterOptions = {}): (path: string) => boolean {
const include = options.include || []
const exclude = options.exclude || []
if (include.length === 0 && exclude.length === 0)
return () => true

return function (path: string): boolean {
for (const v of [{ rules: exclude, result: false }, { rules: include, result: true }]) {
const regexRules = v.rules.filter(r => r instanceof RegExp) as RegExp[]

if (regexRules.some(r => r.test(path)))
return v.result

const stringRules = v.rules.filter(r => typeof r === 'string') as string[]
if (stringRules.length > 0) {
const routes = {}
for (const r of stringRules) {
// quick scan of literal string matches
if (r === path)
return v.result

// need to flip the array data for radix3 format, true value is arbitrary
// @ts-expect-error untyped
routes[r] = true
}
const routeRulesMatcher = toRouteMatcher(createRouter({ routes, strictTrailingSlash: false }))
if (routeRulesMatcher.matchAll(path).length > 0)
return Boolean(v.result)
}
}
return include.length === 0
}
}

export function filterSitemapUrls(_urls: ResolvedSitemapUrl[], options: Pick<ModuleRuntimeConfig, 'autoI18n' | 'isMultiSitemap'> & Pick<ModuleRuntimeConfig['sitemaps'][string], 'sitemapName' | 'include' | 'exclude'> & { event: H3Event }) {
// base may be wrong here
const urlFilter = createFilter({
Expand Down
24 changes: 19 additions & 5 deletions src/runtime/nitro/sitemap/urlset/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import type {
SitemapSourceResolved,
SitemapUrl,
} from '../../../types'
import { splitForLocales } from '../../../utils-pure'
import { createPathFilter, splitForLocales } from '../../../utils-pure'

export function normaliseI18nSources(sources: SitemapSourceResolved[], { autoI18n, isI18nMapped }: { autoI18n: ModuleRuntimeConfig['autoI18n'], isI18nMapped: boolean }) {
export function normaliseI18nSources(sources: SitemapSourceResolved[], { autoI18n, isI18nMapped, include, exclude }: { autoI18n: ModuleRuntimeConfig['autoI18n'], isI18nMapped: boolean } & Pick<ModuleRuntimeConfig['sitemaps'][string], 'sitemapName' | 'include' | 'exclude'>) {
// base may be wrong here
const filterPath = createPathFilter({
include,
exclude,
})
if (autoI18n && isI18nMapped) {
return sources.map((s) => {
const urls = (s.urls || []).map((_url) => {
Expand Down Expand Up @@ -38,6 +43,8 @@ export function normaliseI18nSources(sources: SitemapSourceResolved[], { autoI18
if (u._sitemap || u._i18nTransform)
return false
if (u?.loc) {
if (!filterPath(u.loc))
return false
const [_localeCode, _pathWithoutPrefix] = splitForLocales(u.loc, autoI18n.locales.map(l => l.code))
if (pathWithoutPrefix === _pathWithoutPrefix) {
const entries: AlternativeEntry[] = []
Expand Down Expand Up @@ -75,8 +82,13 @@ export function normaliseI18nSources(sources: SitemapSourceResolved[], { autoI18
return sources
}

export function applyI18nEnhancements(_urls: ResolvedSitemapUrl[], options: Pick<Required<ModuleRuntimeConfig>, 'autoI18n' | 'isI18nMapped'> & { sitemapName: string }): ResolvedSitemapUrl[] {
const { autoI18n } = options
export function applyI18nEnhancements(_urls: ResolvedSitemapUrl[], options: Pick<Required<ModuleRuntimeConfig>, 'autoI18n' | 'isI18nMapped'> & Pick<ModuleRuntimeConfig['sitemaps'][string], 'sitemapName' | 'include' | 'exclude'>): ResolvedSitemapUrl[] {
// base may be wrong here
const { autoI18n, include, exclude } = options
const filterPath = createPathFilter({
include,
exclude,
})
// we won't remove any urls, only add and modify
// for example an API returns ['/foo', '/bar'] but we want i18n integration
return _urls
Expand Down Expand Up @@ -148,11 +160,13 @@ export function applyI18nEnhancements(_urls: ResolvedSitemapUrl[], options: Pick
}
}
const hreflang = locale.iso || locale.code
if (!filterPath(href))
return false
return {
hreflang,
href,
}
}),
}).filter(Boolean),
}
})
})
Expand Down
58 changes: 57 additions & 1 deletion src/runtime/utils-pure.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createDefu } from 'defu'
import { withLeadingSlash } from 'ufo'
import { parseURL, withLeadingSlash } from 'ufo'
import { createRouter, toRouteMatcher } from 'radix3'
import type { FilterInput } from './types'

const merger = createDefu((obj, key, value) => {
Expand Down Expand Up @@ -45,3 +46,58 @@ export function normalizeRuntimeFilters(input?: FilterInput[]): (RegExp | string
return false
}).filter(Boolean) as (RegExp | string)[]
}

export interface CreateFilterOptions {
include?: (FilterInput | string | RegExp)[]
exclude?: (FilterInput | string | RegExp)[]
}

export function createPathFilter(options: CreateFilterOptions = {}) {
const urlFilter = createFilter(options)
return (loc: string) => {
let path = loc
try {
// e.loc is absolute here
path = parseURL(loc).pathname
}
catch {
// invalid URL
return false
}
return urlFilter(path)
}
}

export function createFilter(options: CreateFilterOptions = {}): (path: string) => boolean {
const include = options.include || []
const exclude = options.exclude || []
if (include.length === 0 && exclude.length === 0)
return () => true

return function (path: string): boolean {
for (const v of [{ rules: exclude, result: false }, { rules: include, result: true }]) {
const regexRules = v.rules.filter(r => r instanceof RegExp) as RegExp[]

if (regexRules.some(r => r.test(path)))
return v.result

const stringRules = v.rules.filter(r => typeof r === 'string') as string[]
if (stringRules.length > 0) {
const routes = {}
for (const r of stringRules) {
// quick scan of literal string matches
if (r === path)
return v.result

// need to flip the array data for radix3 format, true value is arbitrary
// @ts-expect-error untyped
routes[r] = true
}
const routeRulesMatcher = toRouteMatcher(createRouter({ routes, strictTrailingSlash: false }))
if (routeRulesMatcher.matchAll(path).length > 0)
return Boolean(v.result)
}
}
return include.length === 0
}
}
7 changes: 6 additions & 1 deletion src/util/nuxtSitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNuxt } from '@nuxt/kit'
import { extname } from 'pathe'
import { defu } from 'defu'
import type { SitemapDefinition, SitemapUrl, SitemapUrlInput } from '../runtime/types'
import { type CreateFilterOptions, createPathFilter } from '../runtime/utils-pure'

export async function resolveUrls(urls: Required<SitemapDefinition>['urls']): Promise<SitemapUrlInput[]> {
if (typeof urls === 'function')
Expand All @@ -21,6 +22,7 @@ export interface NuxtPagesToSitemapEntriesOptions {
defaultLocale: string
strategy: 'no_prefix' | 'prefix_except_default' | 'prefix' | 'prefix_and_default'
isI18nMapped: boolean
filter: CreateFilterOptions
}

interface PageEntry extends SitemapUrl {
Expand Down Expand Up @@ -48,6 +50,7 @@ function deepForEachPage(
}

export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: NuxtPagesToSitemapEntriesOptions) {
const pathFilter = createPathFilter(config.filter)
const routesNameSeparator = config.routesNameSeparator || '___'
let flattenedPages: PageEntry[] = []
deepForEachPage(
Expand Down Expand Up @@ -124,11 +127,13 @@ export function convertNuxtPagesToSitemapEntries(pages: NuxtPage[], config: Nuxt
const alternatives = entries.map((entry) => {
// check if the locale has a iso code
const hreflang = config.normalisedLocales.find(l => l.code === entry.locale)?.iso || entry.locale
if (!pathFilter(entry.loc))
return false
return {
hreflang,
href: entry.loc,
}
})
}).filter(Boolean)
const xDefault = entries.find(a => a.locale === config.defaultLocale)
if (xDefault && alternatives.length) {
alternatives.push({
Expand Down
3 changes: 2 additions & 1 deletion test/integration/i18n/filtering-regexp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ await setup({
/.no-i18n/,
'/en/__sitemap/**',
'/__sitemap/**',
// exclude fr
'/fr',
],
},
},
Expand All @@ -31,7 +33,6 @@ describe('i18n filtering with regexp', () => {
<loc>https://nuxtseo.com/en</loc>
<xhtml:link rel="alternate" hreflang="en-US" href="https://nuxtseo.com/en" />
<xhtml:link rel="alternate" hreflang="es-ES" href="https://nuxtseo.com/es" />
<xhtml:link rel="alternate" hreflang="fr-FR" href="https://nuxtseo.com/fr" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://nuxtseo.com/en" />
</url>
</urlset>"
Expand Down

0 comments on commit f275c7a

Please sign in to comment.