Skip to content

Commit 7346834

Browse files
committed
feat: add support for localizations in sitemap generator
1 parent 195d1f1 commit 7346834

File tree

6 files changed

+146
-2
lines changed

6 files changed

+146
-2
lines changed

docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,99 @@ Output:
8787
</urlset>
8888
```
8989

90+
### Generate a localized Sitemap
91+
92+
```ts filename="app/sitemap.ts" switcher
93+
import { MetadataRoute } from 'next'
94+
95+
export default function sitemap(): MetadataRoute.Sitemap {
96+
return [
97+
{
98+
url: 'https://acme.com',
99+
lastModified: new Date(),
100+
alternates: {
101+
languages: {
102+
es: 'https://acme.com/es',
103+
de: 'https://acme.com/de',
104+
},
105+
},
106+
},
107+
{
108+
url: 'https://acme.com/about',
109+
lastModified: new Date(),
110+
alternates: {
111+
languages: {
112+
es: 'https://acme.com/es/about',
113+
de: 'https://acme.com/de/about',
114+
},
115+
},
116+
},
117+
{
118+
url: 'https://acme.com/blog',
119+
lastModified: new Date(),
120+
alternates: {
121+
languages: {
122+
es: 'https://acme.com/es/blog',
123+
de: 'https://acme.com/de/blog',
124+
},
125+
},
126+
},
127+
]
128+
}
129+
```
130+
131+
Output:
132+
133+
```xml filename="acme.com/sitemap.xml"
134+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9 xmlns:xhtml="http://www.w3.org/1999/xhtml"">
135+
<url>
136+
<loc>https://acme.com</loc>
137+
<xhtml:link
138+
rel="alternate"
139+
hreflang="es"
140+
href="https://acme.com/es"/>
141+
<xhtml:link
142+
rel="alternate"
143+
hreflang="de"
144+
href="https://acme.com/de"/>
145+
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
146+
</url>
147+
<url>
148+
<loc>https://acme.com/about</loc>
149+
<xhtml:link
150+
rel="alternate"
151+
hreflang="es"
152+
href="https://acme.com/es/about"/>
153+
<xhtml:link
154+
rel="alternate"
155+
hreflang="de"
156+
href="https://acme.com/de/about"/>
157+
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
158+
</url>
159+
<url>
160+
<loc>https://acme.com/blog</loc>
161+
<xhtml:link
162+
rel="alternate"
163+
hreflang="es"
164+
href="https://acme.com/es/blog"/>
165+
<xhtml:link
166+
rel="alternate"
167+
hreflang="de"
168+
href="https://acme.com/de/blog"/>
169+
<lastmod>2023-04-06T15:02:24.021Z</lastmod>
170+
</url>
171+
</urlset>
172+
```
173+
90174
### Sitemap Return Type
91175

92176
```tsx
93177
type Sitemap = Array<{
94178
url: string
95179
lastModified?: string | Date
180+
alternates?: {
181+
languages?: Languages<string>
182+
}
96183
}>
97184
```
98185

packages/next/src/build/webpack/loaders/metadata/resolve-route-data.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,32 @@ describe('resolveRouteData', () => {
9595
"
9696
`)
9797
})
98+
it('should resolve sitemap.xml with alternates', () => {
99+
expect(
100+
resolveSitemap([
101+
{
102+
url: 'https://example.com',
103+
lastModified: '2021-01-01',
104+
alternates: {
105+
languages: {
106+
es: 'https://example.com/es',
107+
de: 'https://example.com/de',
108+
},
109+
},
110+
},
111+
])
112+
).toMatchInlineSnapshot(`
113+
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
114+
<urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\">
115+
<url>
116+
<loc>https://example.com</loc>
117+
<xhtml:link rel=\\"alternate\\" hreflang=\\"es\\" href=\\"https://example.com/es\\" />
118+
<xhtml:link rel=\\"alternate\\" hreflang=\\"de\\" href=\\"https://example.com/de\\" />
119+
<lastmod>2021-01-01</lastmod>
120+
</url>
121+
</urlset>
122+
"
123+
`)
124+
})
98125
})
99126
})

packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { MetadataRoute } from '../../../../lib/metadata/types/metadata-interface'
22
import { resolveArray } from '../../../../lib/metadata/generate/utils'
3+
import { Languages } from '../../../../lib/metadata/types/alternative-urls-types'
34

45
// convert robots data to txt string
56
export function resolveRobots(data: MetadataRoute.Robots): string {
@@ -44,12 +45,31 @@ export function resolveRobots(data: MetadataRoute.Robots): string {
4445
// TODO-METADATA: support multi sitemap files
4546
// convert sitemap data to xml string
4647
export function resolveSitemap(data: MetadataRoute.Sitemap): string {
48+
const hasAlternates = data.some(
49+
(item) => Object.keys(item.alternates ?? {}).length > 0
50+
)
51+
4752
let content = ''
4853
content += '<?xml version="1.0" encoding="UTF-8"?>\n'
49-
content += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
54+
content += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"'
55+
if (hasAlternates) {
56+
content += ' xmlns:xhtml="http://www.w3.org/1999/xhtml">\n'
57+
} else {
58+
content += '>\n'
59+
}
5060
for (const item of data) {
5161
content += '<url>\n'
5262
content += `<loc>${item.url}</loc>\n`
63+
if (
64+
item.alternates?.languages &&
65+
Object.keys(item.alternates.languages).length
66+
) {
67+
for (const language in item.alternates.languages) {
68+
content += `<xhtml:link rel="alternate" hreflang="${language}" href="${
69+
item.alternates.languages[language as keyof Languages<string>]
70+
}" />\n`
71+
}
72+
}
5373
if (item.lastModified) {
5474
content += `<lastmod>${
5575
item.lastModified instanceof Date

packages/next/src/lib/metadata/types/alternative-urls-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ type UnmatchedLang = 'x-default'
426426

427427
type HrefLang = LangCode | UnmatchedLang
428428

429-
type Languages<T> = {
429+
export type Languages<T> = {
430430
[s in HrefLang]?: T
431431
}
432432

packages/next/src/lib/metadata/types/metadata-interface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
AlternateURLs,
3+
Languages,
34
ResolvedAlternateURLs,
45
} from './alternative-urls-types'
56
import type {
@@ -558,6 +559,9 @@ type RobotsFile = {
558559
type SitemapFile = Array<{
559560
url: string
560561
lastModified?: string | Date
562+
alternates?: {
563+
languages?: Languages<string>
564+
}
561565
}>
562566

563567
type ResolvingMetadata = Promise<ResolvedMetadata>

test/e2e/app-dir/metadata-dynamic-routes/app/sitemap.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
1010
{
1111
url: 'https://example.com/about',
1212
lastModified: '2021-01-01',
13+
alternates: {
14+
languages: {
15+
es: 'https://example.com/es/about',
16+
de: 'https://example.com/de/about',
17+
},
18+
},
1319
},
1420
]
1521
}

0 commit comments

Comments
 (0)