Skip to content

Commit e78e4a2

Browse files
committedSep 29, 2019
feat: add configuration for sitemap index and multiple sitemaps
fix #6 BREAKING CHANGE: Drop support for Nuxt.js 1.x
1 parent 3428a30 commit e78e4a2

12 files changed

+830
-199
lines changed
 

‎README.md

+194-51
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Sitemap Module
2+
23
[![npm (scoped with tag)](https://img.shields.io/npm/v/@nuxtjs/sitemap/latest.svg?style=flat-square)](https://npmjs.com/package/@nuxtjs/sitemap)
34
[![npm](https://img.shields.io/npm/dt/@nuxtjs/sitemap.svg?style=flat-square)](https://npmjs.com/package/@nuxtjs/sitemap)
45
[![CircleCI](https://img.shields.io/circleci/project/github/nuxt-community/sitemap-module.svg?style=flat-square)](https://circleci.com/gh/nuxt-community/sitemap-module)
@@ -13,26 +14,49 @@
1314
## Features
1415

1516
- Module based on the awesome **[sitemap.js](https://github.com/ekalinin/sitemap.js) package** ❤️
16-
- Automatically add the static routes to the sitemap
17+
- Create **sitemap** or **sitemap index**
18+
- Automatically add the static routes to each sitemap
1719
- Works with **all modes** (universal, spa, generate)
18-
- For **Nuxt 1.x** and higher
20+
- For **Nuxt 2.x** and higher
21+
22+
---
23+
24+
## Table of Contents
25+
26+
- [Installation](#installation)
27+
- [Usage](#usage)
28+
- [Sitemap Configuration](#sitemap-configuration)
29+
- [Sitemap Index Configuration](#sitemap-index-configuration)
30+
- [Routes Declaration](#routes-declaration)
31+
32+
## Installation
33+
34+
> npm install @nuxtjs/sitemap --save
35+
36+
or
1937

20-
## Setup
38+
> yarn add @nuxtjs/sitemap
2139
22-
- Add the `@nuxtjs/sitemap` dependency with `yarn` or `npm` to your project.
40+
## Usage
2341

24-
- Add `@nuxtjs/sitemap` to the `modules` section of `nuxt.config.js`:
42+
- Add `@nuxtjs/sitemap` to the `modules` section of your `nuxt.config.js` file:
2543

2644
```js
2745
modules: [
2846
'@nuxtjs/sitemap'
2947
]
3048
```
31-
> **notice:** If you use other modules (eg. `nuxt-i18n`), always declare the sitemap module at end of array (eg. `modules: ['nuxt-i18n', '@nuxtjs/sitemap']`)
3249

33-
- Configure it:
50+
> **notice:**
51+
> If you use other modules (eg. `nuxt-i18n`), always declare the sitemap module at end of array
52+
> eg. `modules: ['nuxt-i18n', '@nuxtjs/sitemap']`
53+
54+
- Add a custom configuration with the `sitemap` property.
55+
56+
You can set a single item of [sitemap](#sitemap-configuration) or [sitemap index](#sitemap-index-configuration) or an array of item:
3457

3558
```js
59+
// Setup a simple sitemap.xml
3660
{
3761
modules: [
3862
'@nuxtjs/sitemap'
@@ -56,41 +80,96 @@
5680
}
5781
```
5882
59-
## Configuration
83+
```js
84+
// Setup a sitemap index and its linked sitemaps
85+
{
86+
modules: [
87+
'@nuxtjs/sitemap'
88+
],
89+
sitemap: {
90+
path: '/sitemapindex.xml',
91+
hostname: 'https://example.com',
92+
lastmod: '2017-06-30',
93+
sitemaps: [
94+
{
95+
path: '/sitemap-foo.xml',
96+
routes: ['foo/1', 'foo/2'],
97+
gzip: true
98+
}, {
99+
path: '/folder/sitemap-bar.xml',
100+
routes: ['bar/1', 'bar/2'],
101+
exclude: ['/**']
102+
}
103+
]
104+
}
105+
```
106+
107+
```js
108+
// Setup several sitemaps
109+
{
110+
modules: [
111+
'@nuxtjs/sitemap'
112+
],
113+
sitemap: [
114+
{
115+
path: '/sitemap-products.xml',
116+
routes: [
117+
// array of URL
118+
]
119+
}, {
120+
path: '/sitemap-news.xml',
121+
routes: () => // promise or function
122+
}, {
123+
path: '/sitemapindex.xml',
124+
sitemaps: [{
125+
// array of Sitemap configuration
126+
}]
127+
}
128+
}
129+
}
130+
```
131+
132+
## Sitemap Options
133+
134+
### `routes` - array or promise function
60135
61-
### `routes`
62136
- Default: `[]` or [`generate.routes`](https://nuxtjs.org/api/configuration-generate#routes) value from your `nuxt.config.js`
63137
64138
The `routes` parameter follows the same way than the `generate` [configuration](https://nuxtjs.org/api/configuration-generate).
65-
66-
See as well the [routes](#routes-1) examples below.
67139
68-
### `path` (optional)
140+
See as well the [routes declaration](#routes-declaration) examples below.
141+
142+
### `path` (optional) - string
143+
69144
- Default: `/sitemap.xml`
70145
71146
The URL path of the generated sitemap.
72147
73-
### `hostname` (optional)
74-
- Default:
148+
### `hostname` (optional) - string
149+
150+
- Default:
75151
1. `sitemap.hostname` value from your `nuxt.config.js`
76152
2. [`build.publicPath`](https://nuxtjs.org/api/configuration-build/#publicpath) value from your `nuxt.config.js`
77153
3. [`os.hostname()`](https://nodejs.org/api/os.html#os_os_hostname) for **generate** or **spa** mode, or dynamically based on request URL (`headers.host`) for **universal** mode
78154
79155
This value is **mandatory** for generation sitemap file, and you should explicitly provide it for **generate** or **spa** mode.
80156
81-
### `cacheTime` (optional)
157+
### `cacheTime` (optional) - number
158+
82159
- Default: `1000 * 60 * 15` (15 Minutes)
83160
84-
Defines how frequently should sitemap **routes** being updated.
161+
Defines how frequently should sitemap **routes** being updated (value in milliseconds).
162+
163+
Please note that after each invalidation, `routes` will be evaluated again. (See [routes declaration](#routes-declaration) section)
85164
86-
Please note that after each invalidation, `routes` will be evaluated again. (See [routes](#routes-1) section)
165+
### `exclude` (optional) - string array
87166
88-
### `exclude` (optional)
89167
- Default: `[]`
90168
91169
The `exclude` parameter is an array of [glob patterns](https://github.com/isaacs/minimatch#features) to exclude static routes from the generated sitemap.
92170
93-
### `filter` (optional)
171+
### `filter` (optional) - function
172+
94173
- Default: `undefined`
95174
96175
If `filter` option is set as a function, all routes will be filtered through it.
@@ -125,12 +204,14 @@ Examples:
125204
}
126205
```
127206
128-
### `gzip` (optional)
207+
### `gzip` (optional) - boolean
208+
129209
- Default: `false`
130210
131211
Enable the creation of the `.xml.gz` sitemap compressed with gzip.
132212
133-
### `xmlNs` (optional)
213+
### `xmlNs` (optional) - string
214+
134215
- Default: `undefined`
135216
136217
Set the XML namespaces by override all default `xmlns` attributes in `<urlset>` element.
@@ -145,19 +226,22 @@ Set the XML namespaces by override all default `xmlns` attributes in `<urlset>`
145226
}
146227
```
147228
148-
### `xslUrl` (optional)
229+
### `xslUrl` (optional) - string
230+
149231
- Default: `undefined`
150232
151233
The URL path of the XSL file to style the sitemap.
152234
153-
### `trailingSlash` (optional)
235+
### `trailingSlash` (optional) - boolean
236+
154237
- Default: `false`
155238
156239
Add a trailing slash to each route URL (eg. `/page/1` => `/page/1/`)
157240
158241
> **notice:** To avoid [duplicate content](https://support.google.com/webmasters/answer/66359) detection from crawlers, you have to configure an HTTP 301 redirect between the 2 URLs (see [redirect-module](https://github.com/nuxt-community/redirect-module) or [nuxt-trailingslash-module](https://github.com/WilliamDASILVA/nuxt-trailingslash-module)).
159242
160-
### `defaults` (optional)
243+
### `defaults` (optional) - object
244+
161245
- Default: `{}`
162246
163247
The `defaults` parameter set the default options for all routes.
@@ -179,7 +263,87 @@ The `defaults` parameter set the default options for all routes.
179263
180264
See available options: https://github.com/ekalinin/sitemap.js#usage
181265
182-
## Routes
266+
## Sitemap Index Configuration
267+
268+
### `path` (optional) - string
269+
270+
- Default: `/sitemapindex.xml`
271+
272+
The URL path of the generated sitemap index.
273+
274+
### `hostname` (optional) - string
275+
276+
Set the `hostname` value to each sitemap linked to its sitemap index.
277+
278+
### `sitemaps` - array of object
279+
280+
- Default: `[]`
281+
282+
Array of [sitemap configuration](#sitemap-configuration]) linked to the sitemap index.
283+
284+
```js
285+
// nuxt.config.js
286+
287+
sitemap: {
288+
path: '/sitemapindex.xml',
289+
hostname: 'https://example.com',
290+
sitemaps: [
291+
{
292+
path: '/sitemap-foo.xml',
293+
// ...
294+
}, {
295+
path: '/folder/sitemap-bar.xml',
296+
// ...
297+
}
298+
]
299+
}
300+
```
301+
302+
```xml
303+
<!-- generated sitemapindex.xml -->
304+
305+
<?xml version="1.0" encoding="UTF-8"?>
306+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
307+
<sitemap>
308+
<loc>https://example.com/sitemap-foo.xml</loc>
309+
</sitemap>
310+
<sitemap>
311+
<loc>https://example.com/folder/sitemap-bar.xml</loc>
312+
</sitemap>
313+
</sitemapindex>
314+
```
315+
316+
See more [examples](#usage) above.
317+
318+
### `gzip` (optional) - boolean
319+
320+
- Default: `false`
321+
322+
Enable the creation of the `.xml.gz` sitemap index compressed with gzip.
323+
324+
### `xmlNs` (optional) - string
325+
326+
- Default: `undefined`
327+
328+
Set the XML namespaces by override all default `xmlns` attributes in `<sitemapindex>` element.
329+
330+
```js
331+
// nuxt.config.js
332+
333+
{
334+
sitemap: {
335+
xmlNs: 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"'
336+
}
337+
}
338+
```
339+
340+
### `xslUrl` (optional) - string
341+
342+
- Default: `undefined`
343+
344+
The URL path of the XSL file to style the sitemap index.
345+
346+
## Routes Declaration
183347
184348
By default, the dynamic routes are ignored by the sitemap module.
185349
Nuxt cannot automatically provide this type of complex routes.
@@ -198,6 +362,8 @@ If you want the module to add any route with dynamic parameters, you have to set
198362
199363
eg. add routes for `/users/:id` in the configuration:
200364
365+
### From a static list
366+
201367
```js
202368
// nuxt.config.js
203369

@@ -212,7 +378,7 @@ eg. add routes for `/users/:id` in the configuration:
212378
}
213379
```
214380
215-
### Function which returns a Promise
381+
### From a function which returns a Promise
216382
217383
```js
218384
// nuxt.config.js
@@ -227,35 +393,12 @@ const axios = require('axios')
227393
}
228394
}
229395
}
230-
```
231-
232-
### Function with a callback
233-
234-
**This feature is deprecated**. Use a promise-based approach instead.
235-
236-
```js
237-
// nuxt.config.js
238-
239-
const axios = require('axios')
240-
241-
{
242-
sitemap: {
243-
routes (callback) {
244-
axios.get('https://jsonplaceholder.typicode.com/users')
245-
.then(res => {
246-
let routes = res.data.map(user => '/users/' + user.username)
247-
callback(null, routes)
248-
})
249-
.catch(callback)
250-
}
251-
}
252-
}
253-
```
254396

255397
## License
256398

257399
[MIT License](./LICENSE)
258400

259401
### Contributors
260-
- [Nicolas PENNEC](https://github.com/NicoPennec)
402+
403+
- [Nicolas Pennec](https://github.com/NicoPennec)
261404
- [Pooya Parsa](https://github.com/pi0)

‎lib/builder.js

+62-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { hostname } = require('os')
2+
const { URL } = require('url')
23
const { promisify } = require('util')
34

45
const isHTTPS = require('is-https')
@@ -7,18 +8,27 @@ const sm = require('sitemap')
78
/**
89
* Initialize a fresh sitemap instance
910
*
10-
* @param {Object} options
11-
* @param {Array} routes
12-
* @param {Request} req
11+
* @param {Object} options
12+
* @param {Array} routes
13+
* @param {Request} req
1314
* @returns {Sitemap} sitemap builder
1415
*/
1516
function createSitemap(options, routes, req = null) {
1617
const sitemapConfig = {}
1718

19+
// Set cacheTime
20+
sitemapConfig.cacheTime = options.cacheTime || 0
21+
1822
// Set sitemap hostname
19-
sitemapConfig.hostname =
20-
options.hostname || (req && `${isHTTPS(req) ? 'https' : 'http'}://${req.headers.host}`) || `http://${hostname()}`
23+
sitemapConfig.hostname = getHostname(options, req)
24+
25+
// Set XML namespaces
26+
sitemapConfig.xmlNs = options.xmlNs
27+
28+
// Set XSL url
29+
sitemapConfig.xslUrl = options.xslUrl
2130

31+
// Set default values to each route
2232
routes = routes.map(route => ({ ...options.defaults, ...route }))
2333

2434
// Add a trailing slash to each route URL
@@ -32,28 +42,64 @@ function createSitemap(options, routes, req = null) {
3242
// Enable filter function for each declared route
3343
if (typeof options.filter === 'function') {
3444
routes = options.filter({
35-
routes,
36-
options: { ...options, ...sitemapConfig }
45+
options: { ...sitemapConfig },
46+
routes
3747
})
3848
}
3949

4050
// Set urls and ensure they are unique
4151
sitemapConfig.urls = [...new Set(routes)]
4252

43-
// Set cacheTime
44-
sitemapConfig.cacheTime = options.cacheTime || 0
53+
// Create promisified instance and return
54+
const sitemap = sm.createSitemap(sitemapConfig)
55+
sitemap.toXML = promisify(sitemap.toXML)
56+
57+
return sitemap
58+
}
59+
60+
/**
61+
* Initialize a fresh sitemapindex instance
62+
*
63+
* @param {Object} options
64+
* @param {Request} req
65+
* @returns {string}
66+
*/
67+
function createSitemapIndex(options, req = null) {
68+
const sitemapIndexConfig = {}
69+
70+
// Set urls
71+
const defaultHostname = getHostname(options, req)
72+
sitemapIndexConfig.urls = options.sitemaps.map(options => {
73+
const path = options.gzip ? `${options.path}.gz` : options.path
74+
const hostname = options.hostname || defaultHostname
75+
const url = new URL(path, hostname)
76+
return url.href
77+
})
78+
79+
// Set lastmod for each sitemap
80+
sitemapIndexConfig.lastmod = options.lastmod
4581

4682
// Set XML namespaces
47-
sitemapConfig.xmlNs = options.xmlNs
83+
sitemapIndexConfig.xmlNs = options.xmlNs
4884

4985
// Set XSL url
50-
sitemapConfig.xslUrl = options.xslUrl
86+
sitemapIndexConfig.xslUrl = options.xslUrl
5187

52-
// Create promisified instance and return
53-
const sitemap = sm.createSitemap(sitemapConfig)
54-
sitemap.toXML = promisify(sitemap.toXML)
88+
// Create a sitemapindex
89+
return sm.buildSitemapIndex(sitemapIndexConfig)
90+
}
5591

56-
return sitemap
92+
/**
93+
* Determine the current hostname
94+
*
95+
* @param {Object} options
96+
* @param {Request} req
97+
* @returns {string}
98+
*/
99+
function getHostname(options, req) {
100+
return (
101+
options.hostname || (req && `${isHTTPS(req) ? 'https' : 'http'}://${req.headers.host}`) || `http://${hostname()}`
102+
)
57103
}
58104

59-
module.exports = { createSitemap }
105+
module.exports = { createSitemap, createSitemapIndex }

‎lib/cache.js

+21-19
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ const AsyncCache = require('async-cache')
44
const unionBy = require('lodash.unionby')
55

66
/**
7-
* Initialize a AsyncCache instance for routes
7+
* Initialize a cache instance for sitemap routes
88
*
9-
* @param {string[]} staticRoutes
10-
* @param {Object} options
9+
* @param {Object} globalCache
10+
* @param {Object} options
1111
* @returns {AsyncCache.Cache<any>} Cache instance
1212
*/
13-
function createCache(staticRoutes, options) {
13+
function createRoutesCache(globalCache, options) {
1414
const cache = new AsyncCache({
1515
maxAge: options.cacheTime,
1616
load(_, callback) {
1717
promisifyRoute(options.routes)
18-
.then(routes => routesUnion(staticRoutes, routes))
18+
.then(routes => joinRoutes(globalCache.staticRoutes ? globalCache.staticRoutes() : [], routes))
1919
.then(routes => {
2020
callback(null, routes)
2121
})
@@ -31,6 +31,7 @@ function createCache(staticRoutes, options) {
3131
}
3232

3333
/* istanbul ignore next */
34+
/* eslint-disable */
3435

3536
/**
3637
* Promisify the `options.routes` option
@@ -62,30 +63,31 @@ function promisifyRoute(fn) {
6263
}
6364
return promise
6465
}
66+
/* eslint-enable */
6567

6668
/**
67-
* Join static and options-defined routes into single array
69+
* Join static and dynamic routes into a single list
6870
*
69-
* @param {string[]} staticRoutes
70-
* @param {Array} optionsRoutes
71+
* @param {Array} staticRoutes
72+
* @param {Array} dynamicRoutes
7173
* @returns {Array} List of routes
7274
*/
73-
function routesUnion(staticRoutes, optionsRoutes) {
74-
// Make sure any routes passed as strings are converted to objects with url properties
75-
staticRoutes = staticRoutes.map(ensureRouteIsObject)
76-
optionsRoutes = optionsRoutes.map(ensureRouteIsObject)
77-
// Add static routes to options routes, discarding any defined in options
78-
return unionBy(optionsRoutes, staticRoutes, 'url')
75+
function joinRoutes(staticRoutes, dynamicRoutes) {
76+
// Validate routes
77+
staticRoutes = staticRoutes.map(ensureIsValidRoute)
78+
dynamicRoutes = dynamicRoutes.map(ensureIsValidRoute)
79+
// Join sitemap routes by URL
80+
return unionBy(dynamicRoutes, staticRoutes, 'url')
7981
}
8082

8183
/**
82-
* Make sure a passed route is an object
84+
* Make sure a route is an object with "url" property
8385
*
84-
* @param {Object | string} route Route Object or String value
86+
* @param {Object | string} route Route Object or Payload Object or String value
8587
* @returns {Object} A valid route object
8688
*/
87-
function ensureRouteIsObject(route) {
88-
return typeof route === 'object' ? route : { url: route }
89+
function ensureIsValidRoute(route) {
90+
return typeof route === 'object' ? (route.route ? { url: route.route } : route) : { url: route }
8991
}
9092

91-
module.exports = { createCache }
93+
module.exports = { createRoutesCache }

‎lib/generator.js

+76-16
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,106 @@
11
const path = require('path')
2+
const { gzipSync } = require('zlib')
23

34
const consola = require('consola')
45
const fs = require('fs-extra')
56

6-
const { createSitemap } = require('./builder.js')
7+
const { createSitemap, createSitemapIndex } = require('./builder.js')
8+
const { createRoutesCache } = require('./cache.js')
9+
const { setDefaultSitemapOptions, setDefaultSitemapIndexOptions } = require('./options.js')
10+
const { excludeRoutes } = require('./routes.js')
711

812
/**
9-
* Generating sitemap
13+
* Generate a static file for each sitemap or sitemapindex
1014
*
1115
* @param {Object} options
12-
* @param {Object} cache
13-
* @param {Nuxt} nuxtInstance
16+
* @param {Object} globalCache
17+
* @param {Nuxt} nuxtInstance
1418
*/
15-
async function generateSitemap(options, cache, nuxtInstance) {
19+
async function generateSitemaps(options, globalCache, nuxtInstance) {
20+
const isSitemapIndex = options && options.sitemaps && Array.isArray(options.sitemaps) && options.sitemaps.length > 0
21+
22+
if (isSitemapIndex) {
23+
await generateSitemapIndex(options, globalCache, nuxtInstance)
24+
} else {
25+
await generateSitemap(options, globalCache, nuxtInstance)
26+
}
27+
}
28+
29+
/**
30+
* Generate a sitemap file
31+
*
32+
* @param {Object} options
33+
* @param {Object} globalCache
34+
* @param {Nuxt} nuxtInstance
35+
*/
36+
async function generateSitemap(options, globalCache, nuxtInstance) {
1637
consola.info('Generating sitemap')
1738

39+
// Init options
40+
options = setDefaultSitemapOptions(options, nuxtInstance)
41+
42+
// Init cache
43+
const cache = {}
44+
cache.staticRoutes = () => excludeRoutes(options.exclude, globalCache.staticRoutes)
45+
cache.routes = createRoutesCache(cache, options)
46+
1847
// Generate sitemap.xml
19-
const routes = await cache.sitemap.get('routes')
48+
const routes = await cache.routes.get('routes')
2049
const sitemap = await createSitemap(options, routes)
2150
const xml = await sitemap.toXML()
22-
const xmlGeneratePath = path.join(nuxtInstance.options.generate.dir, options.path)
23-
fs.outputFileSync(xmlGeneratePath, xml)
24-
consola.success('Generated', getPathname(nuxtInstance.options.generate.dir, xmlGeneratePath))
51+
const xmlFilePath = path.join(nuxtInstance.options.generate.dir, options.path)
52+
fs.outputFileSync(xmlFilePath, xml)
53+
consola.success('Generated', getPathname(nuxtInstance.options.generate.dir, xmlFilePath))
2554

2655
// Generate sitemap.xml.gz
2756
if (options.gzip) {
2857
const gzip = await sitemap.toGzip()
29-
const gzipGeneratePath = path.join(nuxtInstance.options.generate.dir, options.pathGzip)
30-
fs.outputFileSync(gzipGeneratePath, gzip)
31-
consola.success('Generated', getPathname(nuxtInstance.options.generate.dir, gzipGeneratePath))
58+
const gzipFilePath = path.join(nuxtInstance.options.generate.dir, options.pathGzip)
59+
fs.outputFileSync(gzipFilePath, gzip)
60+
consola.success('Generated', getPathname(nuxtInstance.options.generate.dir, gzipFilePath))
61+
}
62+
}
63+
64+
/**
65+
* Generate a sitemapindex file
66+
*
67+
* @param {Object} options
68+
* @param {Object} globalCache
69+
* @param {Nuxt} nuxtInstance
70+
*/
71+
async function generateSitemapIndex(options, globalCache, nuxtInstance) {
72+
consola.info('Generating sitemapindex')
73+
74+
// Init options
75+
options = setDefaultSitemapIndexOptions(options)
76+
77+
// Generate sitemapindex.xml
78+
const xml = createSitemapIndex(options)
79+
const xmlFilePath = path.join(nuxtInstance.options.generate.dir, options.path)
80+
fs.outputFileSync(xmlFilePath, xml)
81+
consola.success('Generated', getPathname(nuxtInstance.options.generate.dir, xmlFilePath))
82+
83+
// Generate sitemapindex.xml.gz
84+
if (options.gzip) {
85+
const gzip = gzipSync(xml)
86+
const gzipFilePath = path.join(nuxtInstance.options.generate.dir, options.pathGzip)
87+
fs.outputFileSync(gzipFilePath, gzip)
88+
consola.success('Generated', getPathname(nuxtInstance.options.generate.dir, gzipFilePath))
3289
}
90+
91+
// Generate linked sitemaps
92+
await Promise.all(options.sitemaps.map(sitemapOptions => generateSitemaps(sitemapOptions, globalCache, nuxtInstance)))
3393
}
3494

3595
/**
3696
* Convert a file path to a URL pathname
3797
*
38-
* @param {string} dirPath
39-
* @param {string} filePath
40-
* @returns {string} URL pathname
98+
* @param {string} dirPath
99+
* @param {string} filePath
100+
* @returns {string}
41101
*/
42102
function getPathname(dirPath, filePath) {
43103
return [, ...path.relative(dirPath, filePath).split(path.sep)].join('/')
44104
}
45105

46-
module.exports = { generateSitemap }
106+
module.exports = { generateSitemaps, generateSitemap, generateSitemapIndex }

‎lib/middleware.js

+95-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,67 @@
1-
const { createSitemap } = require('./builder.js')
1+
const { gzipSync } = require('zlib')
2+
3+
const consola = require('consola')
4+
5+
const { createSitemap, createSitemapIndex } = require('./builder.js')
6+
const { createRoutesCache } = require('./cache.js')
7+
const { setDefaultSitemapOptions, setDefaultSitemapIndexOptions } = require('./options.js')
8+
const { excludeRoutes } = require('./routes.js')
9+
10+
/**
11+
* Register a middleware for each sitemap or sitemapindex
12+
*
13+
* @param {Object} options
14+
* @param {Object} globalCache
15+
* @param {Nuxt} nuxtInstance
16+
* @param {number} depth
17+
*/
18+
function registerSitemaps(options, globalCache, nuxtInstance, depth = 0) {
19+
if (depth > 1) {
20+
// see https://webmasters.stackexchange.com/questions/18243/can-a-sitemap-index-contain-other-sitemap-indexes
21+
/* istanbul ignore next */
22+
consola.warn("A sitemap index file can't list other sitemap index files, but only sitemap files")
23+
}
24+
25+
const isSitemapIndex = options && options.sitemaps && Array.isArray(options.sitemaps) && options.sitemaps.length > 0
26+
27+
if (isSitemapIndex) {
28+
registerSitemapIndex(options, globalCache, nuxtInstance, depth)
29+
} else {
30+
registerSitemap(options, globalCache, nuxtInstance)
31+
}
32+
}
233

334
/**
435
* Register a middleware to serve a sitemap
536
*
637
* @param {Object} options
7-
* @param {Object} cache
8-
* @param {Nuxt} nuxtInstance
38+
* @param {Object} globalCache
39+
* @param {Nuxt} nuxtInstance
940
*/
10-
function registerSitemap(options, cache, nuxtInstance) {
41+
function registerSitemap(options, globalCache, nuxtInstance) {
42+
// Init options
43+
options = setDefaultSitemapOptions(options, nuxtInstance)
44+
45+
// Init cache
46+
const cache = {}
47+
cache.staticRoutes = () => excludeRoutes(options.exclude, globalCache.staticRoutes)
48+
cache.routes = createRoutesCache(cache, options)
49+
50+
// On run cmd "start" or "generate [--no-build]"
51+
if (globalCache.staticRoutes) {
52+
// On server ready
53+
nuxtInstance.nuxt.hook('listen', () => {
54+
// Hydrate cache
55+
cache.routes.get('routes')
56+
})
57+
}
58+
1159
if (options.gzip) {
1260
// Add server middleware for sitemap.xml.gz
1361
nuxtInstance.addServerMiddleware({
1462
path: options.pathGzip,
1563
handler(req, res, next) {
16-
cache.sitemap
64+
cache.routes
1765
.get('routes')
1866
.then(routes => createSitemap(options, routes, req))
1967
.then(sitemap => sitemap.toGzip())
@@ -34,7 +82,7 @@ function registerSitemap(options, cache, nuxtInstance) {
3482
nuxtInstance.addServerMiddleware({
3583
path: options.path,
3684
handler(req, res, next) {
37-
cache.sitemap
85+
cache.routes
3886
.get('routes')
3987
.then(routes => createSitemap(options, routes, req))
4088
.then(sitemap => sitemap.toXML())
@@ -50,4 +98,44 @@ function registerSitemap(options, cache, nuxtInstance) {
5098
})
5199
}
52100

53-
module.exports = { registerSitemap }
101+
/**
102+
* Register a middleware to serve a sitemapindex
103+
*
104+
* @param {Object} options
105+
* @param {Object} globalCache
106+
* @param {Nuxt} nuxtInstance
107+
* @param {number} depth
108+
*/
109+
function registerSitemapIndex(options, globalCache, nuxtInstance, depth = 0) {
110+
// Init options
111+
options = setDefaultSitemapIndexOptions(options)
112+
113+
if (options.gzip) {
114+
// Add server middleware for sitemapindex.xml.gz
115+
nuxtInstance.addServerMiddleware({
116+
path: options.pathGzip,
117+
handler(req, res, next) {
118+
const sitemapIndex = createSitemapIndex(options, req)
119+
const gzip = gzipSync(sitemapIndex)
120+
res.setHeader('Content-Type', 'application/x-gzip')
121+
res.setHeader('Content-Encoding', 'gzip')
122+
res.end(gzip)
123+
}
124+
})
125+
}
126+
127+
// Add server middleware for sitemapindex.xml
128+
nuxtInstance.addServerMiddleware({
129+
path: options.path,
130+
handler(req, res, next) {
131+
const sitemapIndex = createSitemapIndex(options, req)
132+
res.setHeader('Content-Type', 'application/xml')
133+
res.end(sitemapIndex)
134+
}
135+
})
136+
137+
// Register linked sitemaps
138+
options.sitemaps.forEach(sitemapOptions => registerSitemaps(sitemapOptions, globalCache, nuxtInstance, depth + 1))
139+
}
140+
141+
module.exports = { registerSitemaps, registerSitemap, registerSitemapIndex }

‎lib/module.js

+26-67
Original file line numberDiff line numberDiff line change
@@ -2,89 +2,48 @@ const path = require('path')
22

33
const consola = require('consola')
44
const fs = require('fs-extra')
5-
const { Minimatch } = require('minimatch')
65

7-
const { createCache } = require('./cache.js')
8-
const { generateSitemap } = require('./generator.js')
9-
const { registerSitemap } = require('./middleware.js')
6+
const { generateSitemaps } = require('./generator.js')
7+
const { registerSitemaps } = require('./middleware.js')
108
const { getStaticRoutes } = require('./routes.js')
119

12-
const defaultPublicPath = '/_nuxt/'
13-
1410
module.exports = function module(moduleOptions) {
15-
const defaults = {
16-
path: '/sitemap.xml',
17-
hostname: this.options.build.publicPath !== defaultPublicPath ? this.options.build.publicPath : undefined,
18-
exclude: [],
19-
routes: this.options.generate.routes || [],
20-
cacheTime: 1000 * 60 * 15,
21-
filter: undefined,
22-
gzip: false,
23-
xmlNs: undefined,
24-
xslUrl: undefined,
25-
trailingSlash: false,
26-
defaults: {}
27-
}
28-
29-
const options = {
30-
...defaults,
31-
...this.options.sitemap,
32-
...moduleOptions
33-
}
11+
const nuxtInstance = this
3412

35-
options.pathGzip = options.gzip ? `${options.path}.gz` : options.path
13+
// Init options
14+
let options = nuxtInstance.options.sitemap || moduleOptions
15+
options = Array.isArray(options) ? options : [options]
3616

37-
// sitemap-routes.json is written to dist dir on "build" mode
38-
const jsonStaticRoutesPath = !this.options.dev
39-
? path.resolve(this.options.buildDir, path.join('dist', 'sitemap-routes.json'))
17+
// Init cache
18+
// a file "sitemap-routes.json" is written to "dist" dir on "build" mode
19+
const jsonStaticRoutesPath = !nuxtInstance.options.dev
20+
? path.resolve(nuxtInstance.options.buildDir, path.join('dist', 'sitemap-routes.json'))
4021
: null
41-
4222
const staticRoutes = fs.readJsonSync(jsonStaticRoutesPath, { throws: false })
43-
const cache = {}
44-
45-
// On run cmd "start" or "generate [--no-build]"
46-
if (staticRoutes) {
47-
// Create a cache for routes
48-
cache.sitemap = createCache(staticRoutes, options)
49-
50-
// On server ready, hydrate cache
51-
this.nuxt.hook('listen', () => {
52-
cache.sitemap.get('routes')
53-
})
54-
}
23+
const globalCache = { staticRoutes }
5524

56-
// On extend routes
57-
this.extendRoutes(routes => {
58-
let staticRoutes = getStaticRoutes(routes)
25+
// Init static routes
26+
nuxtInstance.extendRoutes(routes => {
27+
// Create a cache for static routes
28+
globalCache.staticRoutes = getStaticRoutes(routes)
5929

60-
// Exclude routes
61-
options.exclude.forEach(pattern => {
62-
const minimatch = new Minimatch(pattern)
63-
minimatch.negate = true
64-
staticRoutes = staticRoutes.filter(route => minimatch.match(route))
65-
})
66-
67-
// Create a cache for routes
68-
cache.sitemap = createCache(staticRoutes, options)
69-
70-
if (!this.options.dev) {
30+
// On run cmd "build"
31+
if (!nuxtInstance.options.dev) {
7132
// Save static routes
72-
fs.outputJsonSync(jsonStaticRoutesPath, staticRoutes)
33+
fs.outputJsonSync(jsonStaticRoutesPath, globalCache.staticRoutes)
7334
}
7435
})
7536

76-
/* istanbul ignore if */
77-
if (options.generate) {
78-
consola.warn("The option `sitemap.generate` isn't needed anymore")
79-
}
80-
81-
// On "generate" mode, generate a sitemap file in dist dir
82-
this.nuxt.hook('generate:done', async () => {
83-
await generateSitemap(options, cache, this)
37+
// On "generate" mode, generate static files for each sitemap or sitemapindex
38+
nuxtInstance.nuxt.hook('generate:done', async () => {
39+
consola.info('Generating sitemaps')
40+
await Promise.all(options.map(options => generateSitemaps(options, globalCache, nuxtInstance)))
8441
})
8542

86-
// On "universal" mode, register a middleware for each sitemap path
87-
registerSitemap(options, cache, this)
43+
// On "universal" mode, register middlewares for each sitemap or sitemapindex
44+
options.forEach(options => {
45+
registerSitemaps(options, globalCache, nuxtInstance)
46+
})
8847
}
8948

9049
module.exports.meta = require('../package.json')

‎lib/options.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const consola = require('consola')
2+
3+
const DEFAULT_NUXT_PUBLIC_PATH = '/_nuxt/'
4+
5+
/**
6+
* Set default options for a sitemap config
7+
*
8+
* @param {Object} options
9+
* @param {Nuxt} nuxtInstance
10+
* @returns {Object}
11+
*/
12+
function setDefaultSitemapOptions(options, nuxtInstance) {
13+
const defaults = {
14+
path: '/sitemap.xml',
15+
hostname:
16+
nuxtInstance.options.build.publicPath !== DEFAULT_NUXT_PUBLIC_PATH
17+
? nuxtInstance.options.build.publicPath
18+
: undefined,
19+
exclude: [],
20+
routes: nuxtInstance.options.generate.routes || [],
21+
cacheTime: 1000 * 60 * 15,
22+
filter: undefined,
23+
gzip: false,
24+
xmlNs: undefined,
25+
xslUrl: undefined,
26+
trailingSlash: false,
27+
defaults: {}
28+
}
29+
30+
options = {
31+
...defaults,
32+
...options
33+
}
34+
35+
/* istanbul ignore if */
36+
if (options.generate) {
37+
consola.warn('The "generate" option isn\'t needed anymore')
38+
}
39+
40+
/* istanbul ignore if */
41+
if (!options.path) {
42+
consola.warn('The "path" option is either empty or missing for a sitemap')
43+
}
44+
45+
options.pathGzip = options.gzip ? `${options.path}.gz` : options.path
46+
47+
return options
48+
}
49+
50+
/**
51+
* Set default options for a sitemapindex config
52+
*
53+
* @param {Object} options
54+
* @returns {Object}
55+
*/
56+
function setDefaultSitemapIndexOptions(options) {
57+
const defaults = {
58+
path: '/sitemapindex.xml',
59+
hostname: undefined,
60+
sitemaps: [],
61+
gzip: false,
62+
xmlNs: undefined,
63+
xslUrl: undefined
64+
}
65+
66+
options = {
67+
...defaults,
68+
...options
69+
}
70+
71+
/* istanbul ignore if */
72+
if (options.generate) {
73+
consola.warn("The option `generate` isn't needed anymore")
74+
}
75+
76+
/* istanbul ignore if */
77+
if (!options.path) {
78+
consola.warn('The "path" option is either empty or missing for a sitemap index')
79+
}
80+
81+
// set cascading hostname
82+
options.sitemaps.forEach(sitemapOptions => {
83+
sitemapOptions.hostname = sitemapOptions.hostname || options.hostname
84+
})
85+
86+
options.pathGzip = options.gzip ? `${options.path}.gz` : options.path
87+
88+
return options
89+
}
90+
91+
module.exports = { setDefaultSitemapOptions, setDefaultSitemapIndexOptions }

‎lib/routes.js

+24-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
1+
const { Minimatch } = require('minimatch')
2+
3+
/**
4+
* Exclude routes by glob patterns
5+
*
6+
* @param {string[]} patterns
7+
* @param {string[]} routes
8+
* @returns {string[]}
9+
*/
10+
function excludeRoutes(patterns, routes) {
11+
patterns.forEach(pattern => {
12+
const minimatch = new Minimatch(pattern)
13+
minimatch.negate = true
14+
routes = routes.filter(route => minimatch.match(route))
15+
})
16+
return routes
17+
}
18+
119
/**
2-
* Get list of static routes from Nuxt router
20+
* Get static routes from Nuxt router and ignore dynamic routes
321
*
4-
* @param {Object} router
22+
* @param {Object} router
523
* @returns {string[]}
624
*/
725
function getStaticRoutes(router) {
@@ -12,9 +30,9 @@ function getStaticRoutes(router) {
1230
/**
1331
* Recursively flatten all routes and their child-routes
1432
*
15-
* @param {Object} router
16-
* @param {string} path
17-
* @param {string[]} routes
33+
* @param {Object} router
34+
* @param {string} path
35+
* @param {string[]} routes
1836
* @returns {string[]}
1937
*/
2038
function flattenRoutes(router, path = '', routes = []) {
@@ -29,4 +47,4 @@ function flattenRoutes(router, path = '', routes = []) {
2947
return routes
3048
}
3149

32-
module.exports = { getStaticRoutes }
50+
module.exports = { excludeRoutes, getStaticRoutes }

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"standard-version": "latest"
7575
},
7676
"engines": {
77-
"node": ">=8.0.0",
77+
"node": ">=8.9.0",
7878
"npm": ">=5.0.0"
7979
},
8080
"publishConfig": {
+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`sitemap - generate mode sitemap.xml 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:news=\\"http://www.google.com/schemas/sitemap-news/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\"><url><loc>https://example.com/child</loc></url><url><loc>https://example.com/exclude</loc></url><url><loc>https://example.com/filtered</loc></url><url><loc>https://example.com/parent/child/subchild</loc></url><url><loc>https://example.com/parent/child</loc></url><url><loc>https://example.com/parent</loc></url><url><loc>https://example.com/sub</loc></url><url><loc>https://example.com/sub/sub</loc></url><url><loc>https://example.com/</loc></url></urlset>"`;
3+
exports[`sitemap - generate mode sitemap.xml 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:news=\\"http://www.google.com/schemas/sitemap-news/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\"><url><loc>https://example.com/child</loc></url><url><loc>https://example.com/filtered</loc></url><url><loc>https://example.com/parent/child/subchild</loc></url><url><loc>https://example.com/parent/child</loc></url><url><loc>https://example.com/parent</loc></url><url><loc>https://example.com/sub</loc></url><url><loc>https://example.com/sub/sub</loc></url><url><loc>https://example.com/</loc></url></urlset>"`;
44
55
exports[`sitemap - minimal configuration sitemap.xml 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:news=\\"http://www.google.com/schemas/sitemap-news/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\"><url><loc>https://example.com/child</loc></url><url><loc>https://example.com/exclude</loc></url><url><loc>https://example.com/filtered</loc></url><url><loc>https://example.com/parent/child/subchild</loc></url><url><loc>https://example.com/parent/child</loc></url><url><loc>https://example.com/parent</loc></url><url><loc>https://example.com/sub</loc></url><url><loc>https://example.com/sub/sub</loc></url><url><loc>https://example.com/</loc></url></urlset>"`;
6+
7+
exports[`sitemap - multiple configuration sitemap-bar.xml 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:news=\\"http://www.google.com/schemas/sitemap-news/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\"><url><loc>https://example.org/child</loc></url><url><loc>https://example.org/exclude</loc></url><url><loc>https://example.org/filtered</loc></url><url><loc>https://example.org/parent/child/subchild</loc></url><url><loc>https://example.org/parent/child</loc></url><url><loc>https://example.org/parent</loc></url><url><loc>https://example.org/sub</loc></url><url><loc>https://example.org/sub/sub</loc></url><url><loc>https://example.org/</loc></url></urlset>"`;
8+
9+
exports[`sitemap - multiple configuration sitemap-foo.xml 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:news=\\"http://www.google.com/schemas/sitemap-news/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\"><url><loc>https://example.com/child</loc></url><url><loc>https://example.com/exclude</loc></url><url><loc>https://example.com/filtered</loc></url><url><loc>https://example.com/parent/child/subchild</loc></url><url><loc>https://example.com/parent/child</loc></url><url><loc>https://example.com/parent</loc></url><url><loc>https://example.com/sub</loc></url><url><loc>https://example.com/sub/sub</loc></url><url><loc>https://example.com/</loc></url></urlset>"`;
10+
11+
exports[`sitemapindex - generate mode sitemap-bar.xml 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:news=\\"http://www.google.com/schemas/sitemap-news/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\"><url><loc>https://example.fr/bar/1</loc></url><url><loc>https://example.fr/bar/2</loc></url><url><loc>https://example.fr/child</loc></url><url><loc>https://example.fr/exclude</loc></url><url><loc>https://example.fr/filtered</loc></url><url><loc>https://example.fr/parent/child/subchild</loc></url><url><loc>https://example.fr/parent/child</loc></url><url><loc>https://example.fr/parent</loc></url><url><loc>https://example.fr/sub</loc></url><url><loc>https://example.fr/sub/sub</loc></url><url><loc>https://example.fr/</loc></url></urlset>"`;
12+
13+
exports[`sitemapindex - generate mode sitemap-foo.xml 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><urlset xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:news=\\"http://www.google.com/schemas/sitemap-news/0.9\\" xmlns:xhtml=\\"http://www.w3.org/1999/xhtml\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\"><url><loc>https://example.com/foo/1</loc></url><url><loc>https://example.com/foo/2</loc></url><url><loc>https://example.com/child</loc></url><url><loc>https://example.com/exclude</loc></url><url><loc>https://example.com/filtered</loc></url><url><loc>https://example.com/parent/child/subchild</loc></url><url><loc>https://example.com/parent/child</loc></url><url><loc>https://example.com/parent</loc></url><url><loc>https://example.com/sub</loc></url><url><loc>https://example.com/sub/sub</loc></url><url><loc>https://example.com/</loc></url></urlset>"`;
14+
15+
exports[`sitemapindex - generate mode sitemapindex.xml 1`] = `
16+
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
17+
<sitemapindex xmlns=\\"http://www.sitemaps.org/schemas/sitemap/0.9\\" xmlns:mobile=\\"http://www.google.com/schemas/sitemap-mobile/1.0\\" xmlns:image=\\"http://www.google.com/schemas/sitemap-image/1.1\\" xmlns:video=\\"http://www.google.com/schemas/sitemap-video/1.1\\">
18+
<sitemap>
19+
<loc>https://example.com/sitemap-foo.xml</loc>
20+
</sitemap>
21+
<sitemap>
22+
<loc>https://example.fr/sitemap-bar.xml</loc>
23+
</sitemap>
24+
</sitemapindex>"
25+
`;

‎test/fixture/nuxt.config.js

+31-14
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,36 @@ module.exports = {
44
resourceHints: false
55
},
66
modules: [require('../..')],
7-
sitemap: {
8-
path: '/sitemap.xml',
9-
exclude: ['/exclude'],
10-
gzip: true,
11-
hostname: 'http://localhost:3000/',
12-
routes: ['1/', 'child/1', { url: 'test' }],
13-
filter: ({ routes }) => routes.filter(route => route.url !== '/filtered'),
14-
xmlNs: 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
15-
// xslUrl: 'sitemap.xsl',
16-
trailingSlash: true,
17-
defaults: {
18-
changefreq: 'daily',
19-
priority: 1
7+
sitemap: [
8+
{
9+
path: '/sitemap.xml',
10+
exclude: ['/exclude'],
11+
gzip: true,
12+
hostname: 'http://localhost:3000/',
13+
routes: ['1/', 'child/1', { url: 'test' }],
14+
filter: ({ routes }) => routes.filter(route => route.url !== '/filtered/'),
15+
xmlNs: 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
16+
// xslUrl: 'sitemap.xsl',
17+
trailingSlash: true,
18+
defaults: {
19+
changefreq: 'daily',
20+
priority: 1
21+
}
22+
},
23+
{
24+
hostname: 'https://example.com/',
25+
lastmod: new Date().toISOString(),
26+
sitemaps: [
27+
{
28+
path: '/sitemap-foo.xml',
29+
routes: ['foo/1', 'foo/2']
30+
},
31+
{
32+
hostname: 'https://yolo.com/',
33+
path: '/sitemap-bar.xml',
34+
routes: ['bar/1', 'bar/2']
35+
}
36+
]
2037
}
21-
}
38+
]
2239
}

‎test/module.test.js

+188-1
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,154 @@ describe('sitemap - advanced configuration', () => {
236236
})
237237
})
238238

239+
describe('sitemap - multiple configuration', () => {
240+
let nuxt = null
241+
242+
beforeAll(async () => {
243+
nuxt = await startServer({
244+
...config,
245+
sitemap: [
246+
{
247+
path: 'sitemap-foo.xml',
248+
hostname: 'https://example.com/'
249+
},
250+
{
251+
path: 'sitemap-bar.xml',
252+
hostname: 'https://example.org/'
253+
}
254+
]
255+
})
256+
})
257+
258+
test('sitemap-foo.xml', async () => {
259+
const xml = await get('/sitemap-foo.xml')
260+
expect(xml).toMatchSnapshot()
261+
})
262+
263+
test('sitemap-bar.xml', async () => {
264+
const xml = await get('/sitemap-bar.xml')
265+
expect(xml).toMatchSnapshot()
266+
})
267+
268+
afterAll(async () => {
269+
await nuxt.close()
270+
})
271+
})
272+
273+
describe('sitemapindex - minimal configuration', () => {
274+
let nuxt = null
275+
276+
beforeAll(async () => {
277+
nuxt = await startServer({
278+
...config,
279+
sitemap: {
280+
hostname: 'https://example.com/',
281+
sitemaps: [
282+
{
283+
path: '/sitemap-foo.xml',
284+
routes: ['foo/1', 'foo/2']
285+
},
286+
{
287+
path: '/sitemap-bar.xml',
288+
routes: ['bar/1', 'bar/2']
289+
}
290+
]
291+
}
292+
})
293+
})
294+
295+
test('sitemapindex.xml', async () => {
296+
const xml = await get('/sitemapindex.xml')
297+
expect(xml).toContain('<loc>https://example.com/sitemap-foo.xml</loc>')
298+
expect(xml).toContain('<loc>https://example.com/sitemap-bar.xml</loc>')
299+
})
300+
301+
test('sitemap-foo.xml', async () => {
302+
const xml = await get('/sitemap-foo.xml')
303+
expect(xml).toContain('<loc>https://example.com/foo/1</loc>')
304+
expect(xml).toContain('<loc>https://example.com/foo/2</loc>')
305+
})
306+
307+
test('sitemap-bar.xml', async () => {
308+
const xml = await get('/sitemap-bar.xml')
309+
expect(xml).toContain('<loc>https://example.com/bar/1</loc>')
310+
expect(xml).toContain('<loc>https://example.com/bar/2</loc>')
311+
})
312+
313+
afterAll(async () => {
314+
await nuxt.close()
315+
})
316+
})
317+
318+
describe('sitemapindex - advanced configuration', () => {
319+
let nuxt = null
320+
let xml = null
321+
const lastmod = new Date().toISOString()
322+
323+
beforeAll(async () => {
324+
nuxt = await startServer({
325+
...config,
326+
sitemap: {
327+
path: '/sitemapindex.xml',
328+
hostname: 'https://example.com/',
329+
sitemaps: [
330+
{
331+
path: '/sitemap-foo.xml',
332+
routes: ['foo/1', 'foo/2']
333+
},
334+
{
335+
hostname: 'https://example.fr/',
336+
path: '/sitemap-bar.xml',
337+
routes: ['bar/1', 'bar/2']
338+
}
339+
],
340+
gzip: true,
341+
lastmod,
342+
xmlNs: 'xmlns="https://example.com/schemas/sitemap/0.9"',
343+
xslUrl: 'sitemapindex.xsl'
344+
}
345+
})
346+
347+
xml = await get('/sitemapindex.xml')
348+
})
349+
350+
test('cascading hostname', () => {
351+
expect(xml).toContain('<loc>https://example.com/sitemap-foo.xml</loc>')
352+
expect(xml).toContain('<loc>https://example.fr/sitemap-bar.xml</loc>')
353+
})
354+
355+
test('gzip enabled', async () => {
356+
const gz = await getGzip('/sitemapindex.xml.gz')
357+
const sitemap = gunzipSync(gz).toString()
358+
expect(xml).toEqual(sitemap)
359+
})
360+
361+
test('custom lastmod', () => {
362+
expect(xml).toContain(`<lastmod>${lastmod}</lastmod>`)
363+
})
364+
365+
test('custom XML namespaces', () => {
366+
expect(xml).toContain('<sitemapindex xmlns="https://example.com/schemas/sitemap/0.9">')
367+
})
368+
369+
test('custom XSL', () => {
370+
expect(xml).toContain('<?xml-stylesheet type="text/xsl" href="sitemapindex.xsl"?>')
371+
})
372+
373+
afterAll(async () => {
374+
await nuxt.close()
375+
})
376+
})
377+
378+
// TODO: describe('sitemapindex - multiple configuration', () => { ... })
379+
239380
describe('sitemap - generate mode', () => {
240381
test('sitemap.xml', async () => {
241382
await runGenerate({
242383
...config,
243384
sitemap: {
244-
hostname: 'https://example.com/'
385+
hostname: 'https://example.com/',
386+
exclude: ['/exclude']
245387
}
246388
})
247389

@@ -264,3 +406,48 @@ describe('sitemap - generate mode', () => {
264406
expect(xml).toEqual(sitemap)
265407
})
266408
})
409+
410+
describe('sitemapindex - generate mode', () => {
411+
beforeAll(async () => {
412+
await runGenerate({
413+
...config,
414+
sitemap: {
415+
hostname: 'https://example.com/',
416+
sitemaps: [
417+
{
418+
path: '/sitemap-foo.xml',
419+
routes: ['foo/1', 'foo/2']
420+
},
421+
{
422+
hostname: 'https://example.fr/',
423+
path: '/sitemap-bar.xml',
424+
routes: ['bar/1', 'bar/2']
425+
}
426+
],
427+
gzip: true
428+
}
429+
})
430+
})
431+
432+
test('sitemapindex.xml', () => {
433+
const xml = readFileSync(resolve(__dirname, '../dist/sitemapindex.xml'), 'utf8')
434+
expect(xml).toMatchSnapshot()
435+
})
436+
437+
test('sitemapindex.xml.gz', () => {
438+
const xml = readFileSync(resolve(__dirname, '../dist/sitemapindex.xml'), 'utf8')
439+
const gz = readFileSync(resolve(__dirname, '../dist/sitemapindex.xml.gz'))
440+
const sitemapindex = gunzipSync(gz).toString()
441+
expect(xml).toEqual(sitemapindex)
442+
})
443+
444+
test('sitemap-foo.xml', () => {
445+
const xml = readFileSync(resolve(__dirname, '../dist/sitemap-foo.xml'), 'utf8')
446+
expect(xml).toMatchSnapshot()
447+
})
448+
449+
test('sitemap-bar.xml', () => {
450+
const xml = readFileSync(resolve(__dirname, '../dist/sitemap-bar.xml'), 'utf8')
451+
expect(xml).toMatchSnapshot()
452+
})
453+
})

1 commit comments

Comments
 (1)

alatella87 commented on Feb 8, 2020

@alatella87

Hi @NicoPennec, if I'd like to generate custom tags such as <news:title>, (as per https://support.google.com/news/publisher-center/answer/9606710) what would be the approach?

I tried to add entries to the default object but without success.

Please sign in to comment.