├── .all-contributorsrc
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── example
├── pages__test
│ ├── .index.tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── admin
│ │ ├── page1.tsx
│ │ ├── page2.tsx
│ │ ├── page3.tsx
│ │ └── superadmins
│ │ │ ├── page1.tsx
│ │ │ └── page2.tsx
│ ├── index.old.tsx
│ ├── index.tsx
│ ├── login.tsx
│ ├── product-discount.tsx
│ ├── set-user.tsx
│ ├── store
│ │ ├── page1.tsx
│ │ ├── page2.tsx
│ │ └── product
│ │ │ ├── page1.tsx
│ │ │ └── page2.tsx
│ └── user
│ │ ├── page1.tsx
│ │ └── page2.tsx
└── static
│ └── sitemap.xml
├── jest.config.js
├── lib
├── InterfaceConfig.d.ts
├── InterfaceConfig.js
├── core.d.ts
├── core.js
├── index.d.ts
└── index.js
├── package.json
├── src
├── InterfaceConfig.ts
├── __snapshots__
│ └── core.test.ts.snap
├── core.test.ts
├── core.ts
└── index.ts
├── tsconfig.json
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "getriot",
10 | "name": "Daniele Simeone",
11 | "avatar_url": "https://avatars3.githubusercontent.com/u/2164596?v=4",
12 | "profile": "https://github.com/getriot",
13 | "contributions": [
14 | "code"
15 | ]
16 | },
17 | {
18 | "login": "illiteratewriter",
19 | "name": "illiteratewriter",
20 | "avatar_url": "https://avatars1.githubusercontent.com/u/5787110?v=4",
21 | "profile": "https://github.com/illiteratewriter",
22 | "contributions": [
23 | "doc"
24 | ]
25 | },
26 | {
27 | "login": "goran-zdjelar",
28 | "name": "Goran Zdjelar",
29 | "avatar_url": "https://avatars2.githubusercontent.com/u/45183713?v=4",
30 | "profile": "https://github.com/goran-zdjelar",
31 | "contributions": [
32 | "code"
33 | ]
34 | },
35 | {
36 | "login": "jlaramie",
37 | "name": "jlaramie",
38 | "avatar_url": "https://avatars0.githubusercontent.com/u/755748?v=4",
39 | "profile": "https://github.com/jlaramie",
40 | "contributions": [
41 | "code"
42 | ]
43 | },
44 | {
45 | "login": "stewartmcgown",
46 | "name": "Stewart McGown",
47 | "avatar_url": "https://avatars2.githubusercontent.com/u/1136276?v=4",
48 | "profile": "https://ecoeats.uk",
49 | "contributions": [
50 | "doc"
51 | ]
52 | },
53 | {
54 | "login": "jordanandree",
55 | "name": "Jordan Andree",
56 | "avatar_url": "https://avatars0.githubusercontent.com/u/235503?v=4",
57 | "profile": "https://jordanandree.com",
58 | "contributions": [
59 | "code"
60 | ]
61 | },
62 | {
63 | "login": "sakamossan",
64 | "name": "sakamossan",
65 | "avatar_url": "https://avatars3.githubusercontent.com/u/5309672?v=4",
66 | "profile": "https://github.com/sakamossan",
67 | "contributions": [
68 | "code"
69 | ]
70 | }
71 | ],
72 | "contributorsPerLine": 7,
73 | "projectName": "nextjs-sitemap-generator",
74 | "projectOwner": "IlusionDev",
75 | "repoType": "github",
76 | "repoHost": "https://github.com",
77 | "skipCi": true
78 | }
79 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "extends": [
7 | "standard",
8 | "plugin:import/typescript",
9 | "plugin:import/warnings",
10 | "plugin:import/errors"
11 | ],
12 | "globals": {
13 | "Atomics": "readonly",
14 | "SharedArrayBuffer": "readonly"
15 | },
16 | "parser": "@typescript-eslint/parser",
17 | "parserOptions": {
18 | "ecmaVersion": 2018,
19 | "sourceType": "module"
20 | },
21 | "settings": {
22 | "import/resolver": {
23 | "node": {
24 | "paths": [
25 | "src"
26 | ]
27 | }
28 | }
29 | },
30 | "plugins": [
31 | "@typescript-eslint"
32 | ],
33 | "rules": {}
34 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | /node_modules
3 | package-lock.json
4 | example/static/main.xml
5 | /lib
6 | src
7 | example
8 | tsconfig.json
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Adrián Alonso Vergara
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | [](#contributors)
5 |
6 | ## DEPRECATION
7 | **Due to new emerging libraries doing the same functionability this library is marked as deprecated and consider using [Next-Sitemap](https://github.com/iamvishnusankar/next-sitemap) library.**
8 |
9 | We are looking for maintainers because I don't have enough time to maintain the package.
10 |
11 | Please consider to make a donation for the maintenance of the project.
12 |
13 | [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YFXG8SLXPEVXN&source=url)
14 |
15 |
16 |
17 | Simple `sitemap.xml` mapper for Next.js projects.
18 |
19 | ## Installation
20 |
21 | To install the package execute this in your terminal if you are using yarn:
22 |
23 | ```
24 |
25 | yarn add nextjs-sitemap-generator
26 |
27 | ```
28 |
29 | And this if you are using npm:
30 |
31 | ```
32 |
33 | npm i --save-dev nextjs-sitemap-generator
34 |
35 | ```
36 |
37 | NextJs starts it's own server to serve all created files. But there are another option called [Custom server](https://nextjs.org/docs/advanced-features/custom-server) that uses a file to start a next server.
38 |
39 | If you want use this package you must create the sever file. You can find how to do it here [NextJs custom server](https://nextjs.org/docs/advanced-features/custom-server)
40 |
41 |
42 |
43 |
44 |
45 | This module have been created to be used at node [custom server](https://nextjs.org/docs/advanced-features/custom-server) side of NextJs.
46 |
47 | It is meant to be used in index.js/server.js so that when the server is initialized it will only run once.
48 |
49 | If you place it in any of the request handler of the node server performance may be affected.
50 |
51 |
52 |
53 | For those people who deploy in Vercel:
54 |
55 | > A custom server can not be deployed on Vercel, the platform Next.js was made for.
56 |
57 |
58 |
59 | For example:
60 |
61 | If you have this example server file
62 |
63 | ```js
64 |
65 | // server.js
66 |
67 | const sitemap = require('nextjs-sitemap-generator'); // Import the package
68 |
69 | const { createServer } = require('http')
70 |
71 | const { parse } = require('url')
72 |
73 | const next = require('next')
74 |
75 |
76 |
77 | const dev = process.env.NODE_ENV !== 'production'
78 |
79 | const app = next({ dev })
80 |
81 | const handle = app.getRequestHandler()
82 |
83 |
84 |
85 | /*
86 |
87 | Here you is you have to use the sitemap function.
88 |
89 | Using it here you are allowing to generate the sitemap file
90 |
91 | only once, just when the server starts.
92 |
93 | */
94 |
95 | sitemap({
96 | alternateUrls: {
97 | en: 'https://example.en',
98 | es: 'https://example.es',
99 | ja: 'https://example.jp',
100 | fr: 'https://example.fr',
101 | },
102 | baseUrl: 'https://example.com',
103 | ignoredPaths: ['admin'],
104 | extraPaths: ['/extraPath'],
105 | pagesDirectory: __dirname + "\\pages",
106 | targetDirectory : 'static/',
107 | sitemapFilename: 'sitemap.xml',
108 | nextConfigPath: __dirname + "\\next.config.js"
109 | });
110 |
111 |
112 |
113 | app.prepare().then(() => {
114 | createServer((req, res) => {
115 | const parsedUrl = parse(req.url, true)
116 | const { pathname, query } = parsedUrl
117 | if (pathname === '/a') {
118 | app.render(req, res, '/a', query)
119 | }
120 | else if (pathname === '/b') {
121 | app.render(req, res, '/b', query)
122 | } else {
123 | handle(req, res, parsedUrl)
124 | }}).listen(3000, (err) => {
125 | if (err) throw err
126 | console.log('> Ready on http://localhost:3000')
127 | })
128 | })
129 |
130 | ```
131 |
132 |
133 |
134 | #### Usage for static HTML apps
135 |
136 |
137 | If you are exporting the next project as a static HTML app, create a next-sitemap-generator script file in the base directory.
138 |
139 | The option `pagesDirectory` should point to the static files output folder.
140 |
141 | After generating the output files, run `node your_nextjs_sitemap_generator.js` to generate the sitemap.
142 |
143 |
144 |
145 | If your pages are statically served then you will need to set the `allowFileExtensions` option as `true` so that the pages contain the extension, most cases being `.html`.
146 |
147 | #### Usage with `getStaticPaths`
148 |
149 | If you are using `next@^9.4.0`, you may have your site configured with getStaticPaths to pregenerate pages on dynamic routes. To add those to your sitemap, you need to load the BUILD_ID file into your config to reach the generated build directory with statics pages inside, whilst excluding everything that isn't static pages:
150 |
151 |
152 |
153 | ```js
154 |
155 | const sitemap = require("nextjs-sitemap-generator");
156 |
157 | const fs = require("fs");
158 |
159 |
160 |
161 | const BUILD_ID = fs.readFileSync(".next/BUILD_ID").toString();
162 |
163 |
164 |
165 | sitemap({
166 | baseUrl: "https://example.com",
167 | // If you are using Vercel platform to deploy change the route to /.next/serverless/pages
168 | pagesDirectory: __dirname + "/.next/server/static/" + BUILD_ID + "/pages",
169 | targetDirectory: "public/",
170 | ignoredExtensions: ["js", "map"],
171 | ignoredPaths: ["assets"], // Exclude everything that isn't static page
172 | });
173 |
174 | ```
175 |
176 |
177 |
178 | ## OPTIONS
179 |
180 |
181 |
182 | ```javascript
183 | // your_nextjs_sitemap_generator.js
184 |
185 | const sitemap = require("nextjs-sitemap-generator");
186 |
187 | sitemap({
188 | alternateUrls: {
189 | en: "https://example.en",
190 | es: "https://example.es",
191 | ja: "https://example.jp",
192 | fr: "https://example.fr",
193 | },
194 | baseUrl: "https://example.com",
195 | ignoredPaths: ["admin"],
196 | extraPaths: ["/extraPath"],
197 | pagesDirectory: __dirname + "\\pages",
198 | targetDirectory: "static/",
199 | sitemapFilename: "sitemap.xml",
200 | nextConfigPath: __dirname + "\\next.config.js",
201 | ignoredExtensions: ["png", "jpg"],
202 | pagesConfig: {
203 | "/login": {
204 | priority: "0.5",
205 | changefreq: "daily",
206 | },
207 | },
208 | sitemapStylesheet: [
209 | {
210 | type: "text/css",
211 | styleFile: "/test/styles.css",
212 | },
213 | {
214 | type: "text/xsl",
215 | styleFile: "test/test/styles.xls",
216 | },
217 | ],
218 | });
219 |
220 | console.log(`✅ sitemap.xml generated!`);
221 |
222 |
223 | ```
224 | ## OPTIONS description
225 | - **alternateUrls**: You can add the alternate domains corresponding to the available language. (OPTIONAL)
226 |
227 | - **baseUrl**: The url that it's going to be used at the beginning of each page.
228 |
229 | - **ignoreIndexFiles**: Whether index file should be in URL or just directory ending with the slash (OPTIONAL)
230 |
231 | - **ignoredPaths**: File or directory to not map (like admin routes).(OPTIONAL)
232 |
233 | - **extraPaths**: Array of extra paths to include in the sitemap (even if not present in pagesDirectory) (OPTIONAL)
234 |
235 | - **ignoredExtensions**: Ignore files by extension.(OPTIONAL)
236 |
237 | - **pagesDirectory**: The directory where Nextjs pages live. You can use another directory while they are nextjs pages. **It must to be an absolute path**.
238 |
239 | - **targetDirectory**: The directory where sitemap.xml going to be written.
240 |
241 | - **sitemapFilename**: The filename for the sitemap. Defaults to `sitemap.xml`. (OPTIONAL)
242 |
243 | - **pagesConfig**: Object configuration of priority and changefreq per route. Accepts regex patterns(OPTIONAL) **Path keys must be lowercase**
244 |
245 | - **sitemapStylesheet**: Array of style objects that will be applied to sitemap.(OPTIONAL)
246 |
247 | - **nextConfigPath**(Used for dynamic routes): Calls `exportPathMap` if exported from `nextConfigPath` js file.
248 |
249 | See this to understand how to do it (https://nextjs.org/docs/api-reference/next.config.js/exportPathMap) (OPTIONAL)
250 |
251 | - **allowFileExtensions**(Used for static applications): Ensures the file extension is displayed with the path in the sitemap. If you are using nextConfigPath with exportTrailingSlash in next config, allowFileExtensions will be ignored. (OPTIONAL)
252 |
253 |
254 | ## Considerations
255 | For now the **ignoredPaths** matches whatever cointaning the thing you put, ignoring if there are files or directories.
256 | In the next versions this going to be fixed.
257 |
--------------------------------------------------------------------------------
/example/pages__test/.index.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/.index.tsx
--------------------------------------------------------------------------------
/example/pages__test/_app.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/_app.tsx
--------------------------------------------------------------------------------
/example/pages__test/_document.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/_document.tsx
--------------------------------------------------------------------------------
/example/pages__test/admin/page1.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/admin/page1.tsx
--------------------------------------------------------------------------------
/example/pages__test/admin/page2.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/admin/page2.tsx
--------------------------------------------------------------------------------
/example/pages__test/admin/page3.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/admin/page3.tsx
--------------------------------------------------------------------------------
/example/pages__test/admin/superadmins/page1.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/admin/superadmins/page1.tsx
--------------------------------------------------------------------------------
/example/pages__test/admin/superadmins/page2.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/admin/superadmins/page2.tsx
--------------------------------------------------------------------------------
/example/pages__test/index.old.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/index.old.tsx
--------------------------------------------------------------------------------
/example/pages__test/index.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/index.tsx
--------------------------------------------------------------------------------
/example/pages__test/login.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/login.tsx
--------------------------------------------------------------------------------
/example/pages__test/product-discount.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/product-discount.tsx
--------------------------------------------------------------------------------
/example/pages__test/set-user.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/set-user.tsx
--------------------------------------------------------------------------------
/example/pages__test/store/page1.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/store/page1.tsx
--------------------------------------------------------------------------------
/example/pages__test/store/page2.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/store/page2.tsx
--------------------------------------------------------------------------------
/example/pages__test/store/product/page1.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/store/product/page1.tsx
--------------------------------------------------------------------------------
/example/pages__test/store/product/page2.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/store/product/page2.tsx
--------------------------------------------------------------------------------
/example/pages__test/user/page1.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/user/page1.tsx
--------------------------------------------------------------------------------
/example/pages__test/user/page2.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IlusionDev/nextjs-sitemap-generator/cbac882083593b13b8cfe1f363ec6ad8773aad34/example/pages__test/user/page2.tsx
--------------------------------------------------------------------------------
/example/static/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | https://example.com.ru/exportPathMapURL/
12 |
13 | 2020-01-01
14 |
15 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node'
4 | }
5 |
--------------------------------------------------------------------------------
/lib/InterfaceConfig.d.ts:
--------------------------------------------------------------------------------
1 | export interface SitemapStyleFile {
2 | type: string;
3 | styleFile: string;
4 | }
5 | export default interface Config {
6 | alternateUrls?: object;
7 | baseUrl: string;
8 | ignoredPaths?: Array;
9 | extraPaths?: Array;
10 | ignoreIndexFiles?: Array | boolean;
11 | ignoredExtensions?: Array;
12 | pagesDirectory: string;
13 | nextConfigPath?: string;
14 | targetDirectory: string;
15 | sitemapFilename?: string;
16 | pagesConfig?: object;
17 | sitemapStylesheet?: Array;
18 | allowFileExtensions?: boolean;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/InterfaceConfig.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | ;
4 |
--------------------------------------------------------------------------------
/lib/core.d.ts:
--------------------------------------------------------------------------------
1 | import Config, { SitemapStyleFile } from './InterfaceConfig';
2 | declare class SiteMapper {
3 | pagesConfig?: object;
4 | alternatesUrls?: object;
5 | baseUrl: string;
6 | ignoredPaths?: Array;
7 | extraPaths?: Array;
8 | ignoreIndexFiles?: Array | boolean;
9 | ignoredExtensions?: Array;
10 | pagesdirectory: string;
11 | sitemapPath: string;
12 | nextConfigPath?: string;
13 | sitemapTag: string;
14 | sitemapUrlSet: string;
15 | nextConfig: any;
16 | targetDirectory: string;
17 | sitemapFilename?: string;
18 | sitemapStylesheet?: Array;
19 | allowFileExtensions?: boolean;
20 | constructor({ alternateUrls, baseUrl, extraPaths, ignoreIndexFiles, ignoredPaths, pagesDirectory, targetDirectory, sitemapFilename, nextConfigPath, ignoredExtensions, pagesConfig, sitemapStylesheet, allowFileExtensions }: Config);
21 | preLaunch(): void;
22 | finish(): void;
23 | isReservedPage(site: string): boolean;
24 | isIgnoredPath(site: string): boolean;
25 | isIgnoredExtension(fileExtension: string): boolean;
26 | mergePath(basePath: string, currentPage: string): string;
27 | buildPathMap(dir: any): object;
28 | checkTrailingSlash(): boolean;
29 | getSitemapURLs(dir: any): Promise<{
30 | pagePath: string;
31 | outputPath: string;
32 | priority: string;
33 | changefreq: string;
34 | }[]>;
35 | sitemapMapper(dir: any): Promise;
36 | }
37 | export default SiteMapper;
38 |
--------------------------------------------------------------------------------
/lib/core.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const fs_1 = __importDefault(require("fs"));
7 | const date_fns_1 = require("date-fns");
8 | const path_1 = __importDefault(require("path"));
9 | class SiteMapper {
10 | constructor({ alternateUrls, baseUrl, extraPaths, ignoreIndexFiles, ignoredPaths, pagesDirectory, targetDirectory, sitemapFilename, nextConfigPath, ignoredExtensions, pagesConfig, sitemapStylesheet, allowFileExtensions }) {
11 | this.pagesConfig = pagesConfig || {};
12 | this.alternatesUrls = alternateUrls || {};
13 | this.baseUrl = baseUrl;
14 | this.ignoredPaths = ignoredPaths || [];
15 | this.extraPaths = extraPaths || [];
16 | this.ignoreIndexFiles = ignoreIndexFiles || false;
17 | this.ignoredExtensions = ignoredExtensions || [];
18 | this.pagesdirectory = pagesDirectory;
19 | this.targetDirectory = targetDirectory;
20 | this.sitemapFilename = sitemapFilename || 'sitemap.xml';
21 | this.nextConfigPath = nextConfigPath;
22 | this.sitemapStylesheet = sitemapStylesheet || [];
23 | this.allowFileExtensions = allowFileExtensions || false;
24 | this.sitemapTag = '';
25 | this.sitemapUrlSet = `
26 |
31 | `;
32 | if (this.nextConfigPath) {
33 | this.nextConfig = require(nextConfigPath);
34 | if (typeof this.nextConfig === 'function') {
35 | this.nextConfig = this.nextConfig([], {});
36 | }
37 | }
38 | }
39 | preLaunch() {
40 | let xmlStyle = '';
41 | if (this.sitemapStylesheet) {
42 | this.sitemapStylesheet.forEach(({ type, styleFile }) => {
43 | xmlStyle += `\n`;
44 | });
45 | }
46 | fs_1.default.writeFileSync(path_1.default.resolve(this.targetDirectory, './', this.sitemapFilename), this.sitemapTag + xmlStyle + this.sitemapUrlSet, {
47 | flag: 'w'
48 | });
49 | }
50 | finish() {
51 | fs_1.default.writeFileSync(path_1.default.resolve(this.targetDirectory, './', this.sitemapFilename), '', {
52 | flag: 'as'
53 | });
54 | }
55 | isReservedPage(site) {
56 | let isReserved = false;
57 | if (site.charAt(0) === '_' || site.charAt(0) === '.')
58 | isReserved = true;
59 | return isReserved;
60 | }
61 | isIgnoredPath(site) {
62 | let toIgnore = false;
63 | for (const ignoredPath of this.ignoredPaths) {
64 | if (ignoredPath instanceof RegExp) {
65 | if (ignoredPath.test(site))
66 | toIgnore = true;
67 | }
68 | else {
69 | if (site.includes(ignoredPath))
70 | toIgnore = true;
71 | }
72 | }
73 | return toIgnore;
74 | }
75 | isIgnoredExtension(fileExtension) {
76 | let toIgnoreExtension = false;
77 | for (const extensionToIgnore of this.ignoredExtensions) {
78 | if (extensionToIgnore === fileExtension)
79 | toIgnoreExtension = true;
80 | }
81 | return toIgnoreExtension;
82 | }
83 | mergePath(basePath, currentPage) {
84 | let newBasePath = basePath;
85 | if (!basePath && !currentPage)
86 | return '';
87 | if (!newBasePath) {
88 | newBasePath = '/';
89 | }
90 | else if (currentPage) {
91 | newBasePath += '/';
92 | }
93 | return newBasePath + currentPage;
94 | }
95 | buildPathMap(dir) {
96 | let pathMap = {};
97 | const data = fs_1.default.readdirSync(dir);
98 | for (const site of data) {
99 | if (this.isReservedPage(site))
100 | continue;
101 | // Filter directories
102 | const nextPath = dir + path_1.default.sep + site;
103 | if (fs_1.default.lstatSync(nextPath).isDirectory()) {
104 | pathMap = {
105 | ...pathMap,
106 | ...this.buildPathMap(dir + path_1.default.sep + site)
107 | };
108 | continue;
109 | }
110 | const fileExtension = site.split('.').pop();
111 | if (this.isIgnoredExtension(fileExtension))
112 | continue;
113 | let fileNameWithoutExtension = site.substring(0, site.length - (fileExtension.length + 1));
114 | fileNameWithoutExtension =
115 | this.ignoreIndexFiles && fileNameWithoutExtension === 'index'
116 | ? ''
117 | : fileNameWithoutExtension;
118 | let newDir = dir.replace(this.pagesdirectory, '').replace(/\\/g, '/');
119 | if (newDir === '/index')
120 | newDir = '';
121 | const pagePath = this.mergePath(newDir, this.allowFileExtensions ? site : fileNameWithoutExtension);
122 | pathMap[pagePath] = {
123 | page: pagePath
124 | };
125 | }
126 | return pathMap;
127 | }
128 | checkTrailingSlash() {
129 | if (!this.nextConfig)
130 | return false;
131 | const { exportTrailingSlash, trailingSlash } = this.nextConfig;
132 | const next9OrlowerVersion = typeof exportTrailingSlash !== 'undefined';
133 | const next10Version = typeof trailingSlash !== 'undefined';
134 | if ((next9OrlowerVersion || next10Version) &&
135 | (exportTrailingSlash || trailingSlash)) {
136 | return true;
137 | }
138 | return false;
139 | }
140 | async getSitemapURLs(dir) {
141 | let pathMap = this.buildPathMap(dir);
142 | const exportTrailingSlash = this.checkTrailingSlash();
143 | const exportPathMap = this.nextConfig && this.nextConfig.exportPathMap;
144 | if (exportPathMap) {
145 | try {
146 | pathMap = await exportPathMap(pathMap, {});
147 | }
148 | catch (err) {
149 | console.log(err);
150 | }
151 | }
152 | const paths = Object.keys(pathMap).concat(this.extraPaths);
153 | return paths.map((pagePath) => {
154 | let outputPath = pagePath;
155 | if (exportTrailingSlash && !this.allowFileExtensions && outputPath.slice(-1) !== '/') {
156 | outputPath += '/';
157 | }
158 | let priority = '';
159 | let changefreq = '';
160 | if (!this.pagesConfig) {
161 | return {
162 | pagePath,
163 | outputPath,
164 | priority,
165 | changefreq
166 | };
167 | }
168 | Object.entries(this.pagesConfig).forEach(([key, val]) => {
169 | if (key.includes('*')) {
170 | const regex = new RegExp(key, 'i');
171 | if (regex.test(pagePath)) {
172 | priority = val.priority;
173 | changefreq = val.changefreq;
174 | }
175 | }
176 | });
177 | if (this.pagesConfig[pagePath.toLowerCase()]) {
178 | const pageConfig = this.pagesConfig[pagePath.toLowerCase()];
179 | priority = pageConfig.priority;
180 | changefreq = pageConfig.changefreq;
181 | }
182 | return {
183 | pagePath,
184 | outputPath,
185 | priority,
186 | changefreq
187 | };
188 | });
189 | }
190 | async sitemapMapper(dir) {
191 | const urls = await this.getSitemapURLs(dir);
192 | const filteredURLs = urls.filter((url) => !this.isIgnoredPath(url.pagePath));
193 | const date = date_fns_1.format(new Date(), 'yyyy-MM-dd');
194 | filteredURLs.forEach((url) => {
195 | let xmlObject = '\n\t';
196 | const location = `${this.baseUrl}${url.outputPath}`;
197 | xmlObject += `\n\t\t${location}`;
198 | let alternates = '';
199 | for (const langSite in this.alternatesUrls) {
200 | alternates += ``;
201 | }
202 | if (alternates !== '') {
203 | xmlObject += `\n\t\t${alternates}`;
204 | }
205 | if (url.priority) {
206 | const priority = `${url.priority}`;
207 | xmlObject += `\n\t\t${priority}`;
208 | }
209 | if (url.changefreq) {
210 | const changefreq = `${url.changefreq}`;
211 | xmlObject += `\n\t\t${changefreq}`;
212 | }
213 | const lastmod = `${date}`;
214 | xmlObject += `\n\t\t${lastmod}\n\t\n`;
215 | fs_1.default.writeFileSync(path_1.default.resolve(this.targetDirectory, './', this.sitemapFilename), xmlObject, {
216 | flag: 'as'
217 | });
218 | });
219 | }
220 | }
221 | exports.default = SiteMapper;
222 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | import InterfaceConfig from './InterfaceConfig';
2 | declare const _default: (config: InterfaceConfig) => Promise;
3 | export = _default;
4 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | const core_1 = __importDefault(require("./core"));
6 | module.exports = async function (config) {
7 | if (!config) {
8 | throw new Error('Config is mandatory');
9 | }
10 | const coreMapper = new core_1.default(config);
11 | coreMapper.preLaunch();
12 | await coreMapper.sitemapMapper(config.pagesDirectory);
13 | coreMapper.finish();
14 | };
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-sitemap-generator",
3 | "version": "1.3.0",
4 | "description": "Generate sitemap.xml from nextjs pages",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "scripts": {
8 | "test": "yarn jest && tsc",
9 | "tsc": "tsc"
10 | },
11 | "keywords": [
12 | "nextjs",
13 | "sitemap.xml",
14 | "pages",
15 | "node",
16 | "sitemap"
17 | ],
18 | "author": "Adrián Alonso Vergara",
19 | "license": "MIT",
20 | "dependencies": {
21 | "date-fns": "^2.9.0"
22 | },
23 | "husky": {
24 | "hooks": {
25 | "pre-push": "yarn jest"
26 | }
27 | },
28 | "homepage": "https://github.com/IlusionDev/nextjs-sitemap-generator",
29 | "devDependencies": {
30 | "@types/jest": "^24.0.25",
31 | "@types/node": "^13.1.6",
32 | "@typescript-eslint/eslint-plugin": "^2.15.0",
33 | "@typescript-eslint/parser": "^2.15.0",
34 | "eslint": "^6.8.0",
35 | "eslint-config-airbnb-base": "^14.0.0",
36 | "eslint-config-standard": "^14.1.0",
37 | "eslint-plugin-import": "^2.20.0",
38 | "eslint-plugin-jest": "^23.4.0",
39 | "eslint-plugin-node": "^11.0.0",
40 | "eslint-plugin-promise": "^4.2.1",
41 | "eslint-plugin-standard": "^4.0.1",
42 | "husky": "^4.0.6",
43 | "jest": "^24.9.0",
44 | "mockdate": "^2.0.5",
45 | "prettier": "^1.19.1",
46 | "ts-jest": "^24.3.0",
47 | "typescript": "^3.7.4"
48 | },
49 | "files": [
50 | "lib/**/*"
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/src/InterfaceConfig.ts:
--------------------------------------------------------------------------------
1 | export interface SitemapStyleFile {
2 | type: string;
3 | styleFile: string;
4 | }
5 | export default interface Config {
6 | alternateUrls?: object;
7 | baseUrl: string;
8 | ignoredPaths?: Array;
9 | extraPaths?: Array;
10 | ignoreIndexFiles?: Array | boolean;
11 | ignoredExtensions?: Array;
12 | pagesDirectory: string;
13 | nextConfigPath?: string;
14 | targetDirectory: string;
15 | sitemapFilename?: string;
16 | pagesConfig?: object;
17 | sitemapStylesheet?: Array
18 | allowFileExtensions?: boolean;
19 | };
20 |
--------------------------------------------------------------------------------
/src/__snapshots__/core.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Core testing Should generate valid sitemap.xml 1`] = `
4 | "
5 |
6 |
7 |
12 |
13 |
14 | https://example.com.ru/index.old
15 |
16 | 2020-01-01
17 |
18 |
19 |
20 | https://example.com.ru
21 |
22 | 2020-01-01
23 |
24 |
25 |
26 | https://example.com.ru/login
27 |
28 | 2020-01-01
29 |
30 |
31 |
32 | https://example.com.ru/product-discount
33 |
34 | 2020-01-01
35 |
36 |
37 |
38 | https://example.com.ru/set-user
39 |
40 | 2020-01-01
41 |
42 |
43 |
44 | https://example.com.ru/store/page1
45 |
46 | 2020-01-01
47 |
48 |
49 |
50 | https://example.com.ru/store/page2
51 |
52 | 2020-01-01
53 |
54 |
55 |
56 | https://example.com.ru/store/product/page1
57 |
58 | 2020-01-01
59 |
60 |
61 |
62 | https://example.com.ru/store/product/page2
63 |
64 | 2020-01-01
65 |
66 |
67 |
68 | https://example.com.ru/user/page1
69 |
70 | 2020-01-01
71 |
72 |
73 |
74 | https://example.com.ru/user/page2
75 |
76 | 2020-01-01
77 |
78 | "
79 | `;
80 |
81 | exports[`Core testing Should make map of sites 1`] = `
82 | Object {
83 | "": Object {
84 | "page": "",
85 | },
86 | "/admin/page1": Object {
87 | "page": "/admin/page1",
88 | },
89 | "/admin/page2": Object {
90 | "page": "/admin/page2",
91 | },
92 | "/admin/page3": Object {
93 | "page": "/admin/page3",
94 | },
95 | "/admin/superadmins/page1": Object {
96 | "page": "/admin/superadmins/page1",
97 | },
98 | "/admin/superadmins/page2": Object {
99 | "page": "/admin/superadmins/page2",
100 | },
101 | "/index.old": Object {
102 | "page": "/index.old",
103 | },
104 | "/login": Object {
105 | "page": "/login",
106 | },
107 | "/product-discount": Object {
108 | "page": "/product-discount",
109 | },
110 | "/set-user": Object {
111 | "page": "/set-user",
112 | },
113 | "/store/page1": Object {
114 | "page": "/store/page1",
115 | },
116 | "/store/page2": Object {
117 | "page": "/store/page2",
118 | },
119 | "/store/product/page1": Object {
120 | "page": "/store/product/page1",
121 | },
122 | "/store/product/page2": Object {
123 | "page": "/store/product/page2",
124 | },
125 | "/user/page1": Object {
126 | "page": "/user/page1",
127 | },
128 | "/user/page2": Object {
129 | "page": "/user/page2",
130 | },
131 | }
132 | `;
133 |
134 | exports[`Core testing Should match the snapshot if allowFileExtensions 1`] = `
135 | "
136 |
137 |
138 |
143 |
144 |
145 | https://example.com.ru/index.old.tsx
146 |
147 | 2020-01-01
148 |
149 |
150 |
151 | https://example.com.ru/index.tsx
152 |
153 | 2020-01-01
154 |
155 |
156 |
157 | https://example.com.ru/login.tsx
158 |
159 | 2020-01-01
160 |
161 |
162 |
163 | https://example.com.ru/product-discount.tsx
164 |
165 | 2020-01-01
166 |
167 |
168 |
169 | https://example.com.ru/set-user.tsx
170 |
171 | 2020-01-01
172 |
173 |
174 |
175 | https://example.com.ru/store/page1.tsx
176 |
177 | 2020-01-01
178 |
179 |
180 |
181 | https://example.com.ru/store/page2.tsx
182 |
183 | 2020-01-01
184 |
185 |
186 |
187 | https://example.com.ru/store/product/page1.tsx
188 |
189 | 2020-01-01
190 |
191 |
192 |
193 | https://example.com.ru/store/product/page2.tsx
194 |
195 | 2020-01-01
196 |
197 |
198 |
199 | https://example.com.ru/user/page1.tsx
200 |
201 | 2020-01-01
202 |
203 |
204 |
205 | https://example.com.ru/user/page2.tsx
206 |
207 | 2020-01-01
208 |
209 | "
210 | `;
211 |
212 | exports[`Core testing Should use regex in pagesConfig 1`] = `
213 | "
214 |
215 |
216 |
221 |
222 |
223 | https://example.com.ru/index.old.tsx
224 |
225 | 2020-01-01
226 |
227 |
228 |
229 | https://example.com.ru/index.tsx
230 |
231 | 2020-01-01
232 |
233 |
234 |
235 | https://example.com.ru/login.tsx
236 |
237 | 2020-01-01
238 |
239 |
240 |
241 | https://example.com.ru/product-discount.tsx
242 |
243 | 2020-01-01
244 |
245 |
246 |
247 | https://example.com.ru/set-user.tsx
248 |
249 | 2020-01-01
250 |
251 |
252 |
253 | https://example.com.ru/store/page1.tsx
254 |
255 | 2020-01-01
256 |
257 |
258 |
259 | https://example.com.ru/store/page2.tsx
260 |
261 | 2020-01-01
262 |
263 |
264 |
265 | https://example.com.ru/store/product/page1.tsx
266 |
267 | 2020-01-01
268 |
269 |
270 |
271 | https://example.com.ru/store/product/page2.tsx
272 |
273 | 2020-01-01
274 |
275 |
276 |
277 | https://example.com.ru/user/page1.tsx
278 |
279 | 2020-01-01
280 |
281 |
282 |
283 | https://example.com.ru/user/page2.tsx
284 |
285 | 2020-01-01
286 |
287 | "
288 | `;
289 |
290 | exports[`TestCore with nextConfig should exclude ignoredPaths returned by exportPathMap 1`] = `
291 | "
292 |
293 |
294 |
299 | "
300 | `;
301 |
302 | exports[`TestCore with nextConfig should generate valid sitemap 1`] = `
303 | "
304 |
305 |
306 |
311 |
312 |
313 | https://example.com.ru/exportPathMapURL/
314 |
315 | 2020-01-01
316 |
317 | "
318 | `;
319 |
320 | exports[`TestCore with nextConfig should respect exportTrailingSlash from Next config 1`] = `
321 | Array [
322 | Object {
323 | "changefreq": "",
324 | "outputPath": "/admin/page1/",
325 | "pagePath": "/admin/page1",
326 | "priority": "",
327 | },
328 | Object {
329 | "changefreq": "",
330 | "outputPath": "/admin/page2/",
331 | "pagePath": "/admin/page2",
332 | "priority": "",
333 | },
334 | Object {
335 | "changefreq": "",
336 | "outputPath": "/admin/page3/",
337 | "pagePath": "/admin/page3",
338 | "priority": "",
339 | },
340 | Object {
341 | "changefreq": "",
342 | "outputPath": "/admin/superadmins/page1/",
343 | "pagePath": "/admin/superadmins/page1",
344 | "priority": "",
345 | },
346 | Object {
347 | "changefreq": "",
348 | "outputPath": "/admin/superadmins/page2/",
349 | "pagePath": "/admin/superadmins/page2",
350 | "priority": "",
351 | },
352 | Object {
353 | "changefreq": "",
354 | "outputPath": "/index.old/",
355 | "pagePath": "/index.old",
356 | "priority": "",
357 | },
358 | Object {
359 | "changefreq": "",
360 | "outputPath": "/",
361 | "pagePath": "",
362 | "priority": "",
363 | },
364 | Object {
365 | "changefreq": "",
366 | "outputPath": "/login/",
367 | "pagePath": "/login",
368 | "priority": "",
369 | },
370 | Object {
371 | "changefreq": "",
372 | "outputPath": "/product-discount/",
373 | "pagePath": "/product-discount",
374 | "priority": "",
375 | },
376 | Object {
377 | "changefreq": "",
378 | "outputPath": "/set-user/",
379 | "pagePath": "/set-user",
380 | "priority": "",
381 | },
382 | Object {
383 | "changefreq": "",
384 | "outputPath": "/store/page1/",
385 | "pagePath": "/store/page1",
386 | "priority": "",
387 | },
388 | Object {
389 | "changefreq": "",
390 | "outputPath": "/store/page2/",
391 | "pagePath": "/store/page2",
392 | "priority": "",
393 | },
394 | Object {
395 | "changefreq": "weekly",
396 | "outputPath": "/store/product/page1/",
397 | "pagePath": "/store/product/page1",
398 | "priority": "0.6",
399 | },
400 | Object {
401 | "changefreq": "weekly",
402 | "outputPath": "/store/product/page2/",
403 | "pagePath": "/store/product/page2",
404 | "priority": "0.6",
405 | },
406 | Object {
407 | "changefreq": "",
408 | "outputPath": "/user/page1/",
409 | "pagePath": "/user/page1",
410 | "priority": "",
411 | },
412 | Object {
413 | "changefreq": "",
414 | "outputPath": "/user/page2/",
415 | "pagePath": "/user/page2",
416 | "priority": "",
417 | },
418 | ]
419 | `;
420 |
--------------------------------------------------------------------------------
/src/core.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import Core from "./core";
4 | import Config from "./InterfaceConfig";
5 | import path from "path";
6 | import fs from "fs";
7 | import MockDate from "mockdate";
8 |
9 | const rootPath = path.resolve("./");
10 |
11 | const config: Config = {
12 | alternateUrls: {
13 | en: "https://example.en",
14 | es: "https://example.es",
15 | ja: "https://example.jp",
16 | fr: "https://example.fr"
17 | },
18 | baseUrl: "https://example.com.ru",
19 | ignoredPaths: ["admin", /^\/like\//],
20 | pagesDirectory: path.resolve(rootPath, "example", "pages__test"),
21 | targetDirectory: path.resolve(rootPath, "example", "static"),
22 | ignoreIndexFiles: true,
23 | ignoredExtensions: ["yml"],
24 | sitemapStylesheet: [
25 | {
26 | type: "text/css",
27 | styleFile: "/test/styles.css"
28 | },
29 | {
30 | type: "text/xsl",
31 | styleFile: "test/test/styles.xls"
32 | }
33 | ]
34 | };
35 | const coreMapper = new Core(config);
36 | describe("Core testing", () => {
37 | beforeEach(() => {
38 | MockDate.set("2020-01-01T12:00:00Z");
39 | });
40 |
41 | afterAll(() => {
42 | MockDate.reset();
43 | });
44 |
45 | it("Should detect reserved sites", () => {
46 | const underscoredSite = coreMapper.isReservedPage("_admin");
47 | const dotedSite = coreMapper.isReservedPage(".admin");
48 |
49 | expect(underscoredSite).toBe(true);
50 | expect(dotedSite).toBe(true);
51 | });
52 |
53 | it("Should skip non reserved sites", () => {
54 | const site = coreMapper.isReservedPage("admin");
55 |
56 | expect(site).toBe(false);
57 | });
58 |
59 | it("Should ignore expecified site's path ", () => {
60 | const ignoredPath = coreMapper.isIgnoredPath("admin");
61 |
62 | expect(ignoredPath).toBe(true);
63 | });
64 |
65 | it("Should ignore expecified site's path with regexp", () => {
66 | const ignoredPath = coreMapper.isIgnoredPath("/like/product");
67 |
68 | expect(ignoredPath).toBe(true);
69 | });
70 |
71 | it("Should not ignore expecified site's path with regexp", () => {
72 | const ignoredPath = coreMapper.isIgnoredPath("/store/product/like-a-vergin");
73 |
74 | expect(ignoredPath).toBe(false);
75 | });
76 |
77 | it("Should skip non expecified sites's path", () => {
78 | const ignoredPath = coreMapper.isReservedPage("admin");
79 |
80 | expect(ignoredPath).toBe(false);
81 | });
82 |
83 | it("Should ignore expecified extensions", () => {
84 | const ignoredExtension = coreMapper.isIgnoredExtension("yml");
85 |
86 | expect(ignoredExtension).toBe(true);
87 | });
88 |
89 | it("Should skip non expecified extensions", () => {
90 | const ignoredExtension = coreMapper.isReservedPage("jsx");
91 |
92 | expect(ignoredExtension).toBe(false);
93 | });
94 |
95 | it("Should merge path", () => {
96 | const mergedPath = coreMapper.mergePath("/admin", "list");
97 |
98 | expect(mergedPath).toEqual("/admin/list");
99 | });
100 |
101 | it("Should merge path from empty base path", () => {
102 | const mergedPath = coreMapper.mergePath("", "list");
103 |
104 | expect(mergedPath).toEqual("/list");
105 | });
106 |
107 | it("Should merge path from ignored path", () => {
108 | const mergedPath = coreMapper.mergePath("/admin", "");
109 |
110 | expect(mergedPath).toEqual("/admin");
111 | });
112 |
113 | it("Should merge empty path", () => {
114 | const mergedPath = coreMapper.mergePath("", "");
115 |
116 | expect(mergedPath).toEqual("");
117 | });
118 |
119 | it("Should generate sitemap.xml", async () => {
120 | coreMapper.preLaunch();
121 | await coreMapper.sitemapMapper(config.pagesDirectory);
122 | coreMapper.finish();
123 | const sitemap = fs.statSync(
124 | path.resolve(config.targetDirectory, "./sitemap.xml")
125 | );
126 |
127 | expect(sitemap.size).toBeGreaterThan(0);
128 | });
129 |
130 | it("should add extraPaths to output", async () => {
131 | const core = new Core({
132 | ...config,
133 | extraPaths: ["/extraPath"]
134 | });
135 |
136 | const urls = await core.getSitemapURLs(config.pagesDirectory);
137 |
138 | expect(urls).toContainEqual({
139 | pagePath: "/extraPath",
140 | outputPath: "/extraPath",
141 | priority: "",
142 | changefreq: ""
143 | });
144 | });
145 |
146 | it("Should generate a sitemap with a custom file name", async () => {
147 | const coreMapper = new Core({
148 | ...config,
149 | sitemapFilename: "main.xml",
150 | });
151 | coreMapper.preLaunch();
152 | await coreMapper.sitemapMapper(config.pagesDirectory);
153 | coreMapper.finish();
154 | const sitemap = fs.statSync(
155 | path.resolve(config.targetDirectory, "./main.xml")
156 | );
157 |
158 | expect(sitemap.size).toBeGreaterThan(0);
159 | });
160 |
161 | it("Should generate valid sitemap.xml", async () => {
162 | coreMapper.preLaunch();
163 | await coreMapper.sitemapMapper(config.pagesDirectory);
164 | coreMapper.finish();
165 | const sitemap = fs.readFileSync(
166 | path.resolve(config.targetDirectory, "./sitemap.xml"),
167 | { encoding: "UTF-8" }
168 | );
169 | expect(sitemap.includes("xml-stylesheet"));
170 | expect(sitemap).toMatchSnapshot()
171 | });
172 |
173 | it("Should generate styles xml links", async () => {
174 | coreMapper.preLaunch();
175 | await coreMapper.sitemapMapper(config.pagesDirectory);
176 | coreMapper.finish();
177 | const sitemap = fs.readFileSync(
178 | path.resolve(config.targetDirectory, "./sitemap.xml"),
179 | { encoding: "UTF-8" }
180 | );
181 |
182 | expect(
183 | sitemap.includes(
184 | ''
185 | )
186 | ).toBe(true);
187 | expect(
188 | sitemap.includes(
189 | ''
190 | )
191 | ).toBe(true);
192 | });
193 |
194 | it("Should make map of sites", () => {
195 | const result = coreMapper.buildPathMap(config.pagesDirectory);
196 |
197 | expect(result).toMatchSnapshot()
198 | });
199 |
200 | it('Should contain a list of pages with their extension if allowFileExtensions', () => {
201 | const coreMapper = new Core({
202 | ...config,
203 | allowFileExtensions: true,
204 | });
205 | const result = coreMapper.buildPathMap(config.pagesDirectory);
206 |
207 | expect(result['/admin/page1.tsx']).toMatchObject({"page": "/admin/page1.tsx"});
208 | });
209 |
210 | it('Should match the snapshot if allowFileExtensions', async () => {
211 | const core = new Core({
212 | ...config,
213 | allowFileExtensions: true,
214 | });
215 | core.preLaunch();
216 | await core.sitemapMapper(config.pagesDirectory);
217 | core.finish();
218 |
219 | const sitemap = fs.readFileSync(
220 | path.resolve(config.targetDirectory, "./sitemap.xml"),
221 | { encoding: "UTF-8" }
222 | );
223 |
224 | expect(sitemap).toMatchSnapshot();
225 | });
226 |
227 | it('Should use regex in pagesConfig', async () => {
228 | const core = new Core({
229 | ...config,
230 | allowFileExtensions: true,
231 | });
232 | config.pagesConfig = {
233 | "/store/product/*": {
234 | priority: "0.6",
235 | changefreq: "weekly"
236 | },
237 | }
238 | core.preLaunch();
239 | await core.sitemapMapper(config.pagesDirectory);
240 | core.finish();
241 |
242 | const sitemap = fs.readFileSync(
243 | path.resolve(config.targetDirectory, "./sitemap.xml"),
244 | { encoding: "UTF-8" }
245 | );
246 |
247 | expect(sitemap).toMatchSnapshot();
248 | });
249 | })
250 |
251 | describe("TestCore with nextConfig", () => {
252 |
253 | beforeEach(() => {
254 | MockDate.set("2020-01-01T12:00:00Z");
255 | });
256 |
257 | afterAll(() => {
258 | MockDate.reset();
259 | });
260 |
261 | function getCoreWithNextConfig(nextConfig) {
262 | const core = new Core(config);
263 |
264 | core.nextConfig = nextConfig;
265 |
266 | return core;
267 | }
268 |
269 | it("should call exportPathMap from Next config", async () => {
270 | const core = getCoreWithNextConfig({
271 | async exportPathMap(defaultMap) {
272 | return {
273 | "/exportPathMapURL": { page: "/" }
274 | };
275 | }
276 | });
277 |
278 | const urls = await core.getSitemapURLs(config.pagesDirectory);
279 |
280 | expect(urls).toEqual([
281 | {
282 | changefreq: "",
283 | outputPath: "/exportPathMapURL",
284 | pagePath: "/exportPathMapURL",
285 | priority: ""
286 | }
287 | ]);
288 | });
289 |
290 | it("should not append a slash to url that already ends in a slash", async () => {
291 | const core = getCoreWithNextConfig({
292 | exportTrailingSlash: true,
293 | async exportPathMap(defaultMap) {
294 | return {
295 | "/": { page: "/" },
296 | };
297 | }
298 | });
299 |
300 | const urls = await core.getSitemapURLs(config.pagesDirectory);
301 |
302 | expect(urls).toEqual([
303 | {
304 | changefreq: "",
305 | outputPath: "/",
306 | pagePath: "/",
307 | priority: ""
308 | }
309 | ]);
310 | });
311 |
312 | it("should check if exportTrailingSlash exists in Next config", async () => {
313 | const core = getCoreWithNextConfig({
314 | exportTrailingSlash: true
315 | });
316 |
317 | expect(core.checkTrailingSlash()).toBe(true);
318 | });
319 |
320 | it("should check if trailingSlash exists in Next config", async () => {
321 | const core = getCoreWithNextConfig({
322 | trailingSlash: true
323 | });
324 |
325 | expect(core.checkTrailingSlash()).toBe(true);
326 | });
327 |
328 | it("should check that exportTrailingSlash no exists in Next config", async () => {
329 | const core = getCoreWithNextConfig({
330 | exportTrailingSlash: false
331 | });
332 |
333 | expect(core.checkTrailingSlash()).toBe(false);
334 | });
335 |
336 | it("should check that trailingSlash no exists in Next config", async () => {
337 | const core = getCoreWithNextConfig({
338 | trailingSlash: false
339 | });
340 |
341 | expect(core.checkTrailingSlash()).toBe(false);
342 | });
343 |
344 | it("should respect exportTrailingSlash from Next config", async () => {
345 | const core = getCoreWithNextConfig({
346 | exportTrailingSlash: true
347 | });
348 |
349 | const urls = await core.getSitemapURLs(config.pagesDirectory);
350 |
351 | const outputPaths = urls.map(url => url.outputPath);
352 | expect(outputPaths.every(outputPath => outputPath.endsWith("/")));
353 | expect(urls).toMatchSnapshot()
354 | });
355 |
356 | it("should exclude ignoredPaths returned by exportPathMap", async () => {
357 | const core = getCoreWithNextConfig({
358 | async exportPathMap(defaultMap) {
359 | return {
360 | "/admin/": { page: "/" } // should be filtered out by ignoredPaths
361 | };
362 | },
363 | });
364 |
365 | core.preLaunch();
366 | await core.sitemapMapper(config.pagesDirectory);
367 | core.finish();
368 |
369 | const sitemap = fs.readFileSync(
370 | path.resolve(config.targetDirectory, "./sitemap.xml"),
371 | { encoding: "UTF-8" }
372 | );
373 |
374 | expect(sitemap).toMatchSnapshot()
375 | });
376 |
377 | it("should generate valid sitemap", async () => {
378 | const core = getCoreWithNextConfig({
379 | async exportPathMap(defaultMap) {
380 | return {
381 | "/exportPathMapURL": { page: "/" }
382 | };
383 | },
384 | exportTrailingSlash: true
385 | });
386 |
387 | core.preLaunch();
388 | await core.sitemapMapper(config.pagesDirectory);
389 | core.finish();
390 |
391 | const sitemap = fs.readFileSync(
392 | path.resolve(config.targetDirectory, "./sitemap.xml"),
393 | { encoding: "UTF-8" }
394 | );
395 |
396 | expect(sitemap).toMatchSnapshot()
397 | });
398 | });
--------------------------------------------------------------------------------
/src/core.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import { format } from 'date-fns'
3 | import path from 'path'
4 | // eslint-disable-next-line no-unused-vars
5 | import Config, { SitemapStyleFile } from './InterfaceConfig'
6 |
7 | class SiteMapper {
8 | pagesConfig?: object;
9 |
10 | alternatesUrls?: object;
11 |
12 | baseUrl: string;
13 |
14 | ignoredPaths?: Array;
15 |
16 | extraPaths?: Array;
17 |
18 | ignoreIndexFiles?: Array | boolean;
19 |
20 | ignoredExtensions?: Array;
21 |
22 | pagesdirectory: string;
23 |
24 | sitemapPath: string;
25 |
26 | nextConfigPath?: string;
27 |
28 | sitemapTag: string;
29 |
30 | sitemapUrlSet: string;
31 |
32 | nextConfig: any;
33 |
34 | targetDirectory: string;
35 |
36 | sitemapFilename?: string;
37 |
38 | sitemapStylesheet?: Array;
39 |
40 | allowFileExtensions?: boolean;
41 |
42 | constructor ({
43 | alternateUrls,
44 | baseUrl,
45 | extraPaths,
46 | ignoreIndexFiles,
47 | ignoredPaths,
48 | pagesDirectory,
49 | targetDirectory,
50 | sitemapFilename,
51 | nextConfigPath,
52 | ignoredExtensions,
53 | pagesConfig,
54 | sitemapStylesheet,
55 | allowFileExtensions
56 | }: Config) {
57 | this.pagesConfig = pagesConfig || {}
58 | this.alternatesUrls = alternateUrls || {}
59 | this.baseUrl = baseUrl
60 | this.ignoredPaths = ignoredPaths || []
61 | this.extraPaths = extraPaths || []
62 | this.ignoreIndexFiles = ignoreIndexFiles || false
63 | this.ignoredExtensions = ignoredExtensions || []
64 | this.pagesdirectory = pagesDirectory
65 | this.targetDirectory = targetDirectory
66 | this.sitemapFilename = sitemapFilename || 'sitemap.xml'
67 | this.nextConfigPath = nextConfigPath
68 | this.sitemapStylesheet = sitemapStylesheet || []
69 | this.allowFileExtensions = allowFileExtensions || false
70 | this.sitemapTag = ''
71 | this.sitemapUrlSet = `
72 |
77 | `
78 |
79 | if (this.nextConfigPath) {
80 | this.nextConfig = require(nextConfigPath)
81 | if (typeof this.nextConfig === 'function') {
82 | this.nextConfig = this.nextConfig([], {})
83 | }
84 | }
85 | }
86 |
87 | preLaunch () {
88 | let xmlStyle = ''
89 |
90 | if (this.sitemapStylesheet) {
91 | this.sitemapStylesheet.forEach(({ type, styleFile }) => {
92 | xmlStyle += `\n`
93 | })
94 | }
95 | fs.writeFileSync(
96 | path.resolve(this.targetDirectory, './', this.sitemapFilename),
97 | this.sitemapTag + xmlStyle + this.sitemapUrlSet,
98 | {
99 | flag: 'w'
100 | }
101 | )
102 | }
103 |
104 | finish () {
105 | fs.writeFileSync(
106 | path.resolve(this.targetDirectory, './', this.sitemapFilename),
107 | '',
108 | {
109 | flag: 'as'
110 | }
111 | )
112 | }
113 |
114 | isReservedPage (site: string): boolean {
115 | let isReserved = false
116 | if (site.charAt(0) === '_' || site.charAt(0) === '.') isReserved = true
117 |
118 | return isReserved
119 | }
120 |
121 | isIgnoredPath (site: string) {
122 | let toIgnore = false
123 | for (const ignoredPath of this.ignoredPaths) {
124 | if (ignoredPath instanceof RegExp) {
125 | if (ignoredPath.test(site)) toIgnore = true
126 | } else {
127 | if (site.includes(ignoredPath)) toIgnore = true
128 | }
129 | }
130 |
131 | return toIgnore
132 | }
133 |
134 | isIgnoredExtension (fileExtension: string) {
135 | let toIgnoreExtension = false
136 | for (const extensionToIgnore of this.ignoredExtensions) {
137 | if (extensionToIgnore === fileExtension) toIgnoreExtension = true
138 | }
139 |
140 | return toIgnoreExtension
141 | }
142 |
143 | mergePath (basePath: string, currentPage: string) {
144 | let newBasePath: string = basePath
145 | if (!basePath && !currentPage) return ''
146 |
147 | if (!newBasePath) {
148 | newBasePath = '/'
149 | } else if (currentPage) {
150 | newBasePath += '/'
151 | }
152 |
153 | return newBasePath + currentPage
154 | }
155 |
156 | buildPathMap (dir) {
157 | let pathMap: object = {}
158 | const data = fs.readdirSync(dir)
159 |
160 | for (const site of data) {
161 | if (this.isReservedPage(site)) continue
162 |
163 | // Filter directories
164 | const nextPath: string = dir + path.sep + site
165 | if (fs.lstatSync(nextPath).isDirectory()) {
166 | pathMap = {
167 | ...pathMap,
168 | ...this.buildPathMap(dir + path.sep + site)
169 | }
170 | continue
171 | }
172 |
173 | const fileExtension = site.split('.').pop()
174 | if (this.isIgnoredExtension(fileExtension)) continue
175 |
176 | let fileNameWithoutExtension = site.substring(
177 | 0,
178 | site.length - (fileExtension.length + 1)
179 | )
180 | fileNameWithoutExtension =
181 | this.ignoreIndexFiles && fileNameWithoutExtension === 'index'
182 | ? ''
183 | : fileNameWithoutExtension
184 |
185 | let newDir = dir.replace(this.pagesdirectory, '').replace(/\\/g, '/')
186 |
187 | if (newDir === '/index') newDir = ''
188 |
189 | const pagePath = this.mergePath(newDir, this.allowFileExtensions ? site : fileNameWithoutExtension)
190 |
191 | pathMap[pagePath] = {
192 | page: pagePath
193 | }
194 | }
195 |
196 | return pathMap
197 | }
198 |
199 | checkTrailingSlash () {
200 | if (!this.nextConfig) return false
201 | const { exportTrailingSlash, trailingSlash } = this.nextConfig
202 | const next9OrlowerVersion = typeof exportTrailingSlash !== 'undefined'
203 | const next10Version = typeof trailingSlash !== 'undefined'
204 | if (
205 | (next9OrlowerVersion || next10Version) &&
206 | (exportTrailingSlash || trailingSlash)
207 | ) { return true }
208 |
209 | return false
210 | }
211 |
212 | async getSitemapURLs (dir) {
213 | let pathMap = this.buildPathMap(dir)
214 |
215 | const exportTrailingSlash = this.checkTrailingSlash()
216 |
217 | const exportPathMap = this.nextConfig && this.nextConfig.exportPathMap
218 | if (exportPathMap) {
219 | try {
220 | pathMap = await exportPathMap(pathMap, {})
221 | } catch (err) {
222 | console.log(err)
223 | }
224 | }
225 |
226 | const paths = Object.keys(pathMap).concat(this.extraPaths)
227 |
228 | return paths.map((pagePath) => {
229 | let outputPath = pagePath
230 |
231 | if (exportTrailingSlash && !this.allowFileExtensions && outputPath.slice(-1) !== '/') {
232 | outputPath += '/'
233 | }
234 |
235 | let priority = ''
236 | let changefreq = ''
237 |
238 | if (!this.pagesConfig) {
239 | return {
240 | pagePath,
241 | outputPath,
242 | priority,
243 | changefreq
244 | }
245 | }
246 |
247 | Object.entries(this.pagesConfig).forEach(([key, val]) => {
248 | if (key.includes('*')) {
249 | const regex = new RegExp(key, 'i')
250 | if (regex.test(pagePath)) {
251 | priority = val.priority
252 | changefreq = val.changefreq
253 | }
254 | }
255 | })
256 |
257 | if (this.pagesConfig[pagePath.toLowerCase()]) {
258 | const pageConfig = this.pagesConfig[pagePath.toLowerCase()]
259 | priority = pageConfig.priority
260 | changefreq = pageConfig.changefreq
261 | }
262 |
263 | return {
264 | pagePath,
265 | outputPath,
266 | priority,
267 | changefreq
268 | }
269 | })
270 | }
271 |
272 | async sitemapMapper (dir) {
273 | const urls = await this.getSitemapURLs(dir)
274 |
275 | const filteredURLs = urls.filter(
276 | (url) => !this.isIgnoredPath(url.pagePath)
277 | )
278 |
279 | const date = format(new Date(), 'yyyy-MM-dd')
280 |
281 | filteredURLs.forEach((url) => {
282 | let xmlObject = '\n\t'
283 |
284 | const location = `${this.baseUrl}${url.outputPath}`
285 | xmlObject += `\n\t\t${location}`
286 |
287 | let alternates = ''
288 | for (const langSite in this.alternatesUrls) {
289 | alternates += ``
290 | }
291 | if (alternates !== '') {
292 | xmlObject += `\n\t\t${alternates}`
293 | }
294 |
295 | if (url.priority) {
296 | const priority = `${url.priority}`
297 | xmlObject += `\n\t\t${priority}`
298 | }
299 |
300 | if (url.changefreq) {
301 | const changefreq = `${url.changefreq}`
302 | xmlObject += `\n\t\t${changefreq}`
303 | }
304 |
305 | const lastmod = `${date}`
306 | xmlObject += `\n\t\t${lastmod}\n\t\n`
307 |
308 | fs.writeFileSync(
309 | path.resolve(this.targetDirectory, './', this.sitemapFilename),
310 | xmlObject,
311 | {
312 | flag: 'as'
313 | }
314 | )
315 | })
316 | }
317 | }
318 |
319 | export default SiteMapper
320 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Core from './core'
2 | import InterfaceConfig from './InterfaceConfig'
3 |
4 | export = async function(config: InterfaceConfig) {
5 | if (!config) {
6 | throw new Error('Config is mandatory')
7 | }
8 |
9 | const coreMapper = new Core(config)
10 |
11 | coreMapper.preLaunch()
12 | await coreMapper.sitemapMapper(config.pagesDirectory)
13 | coreMapper.finish()
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./lib",
4 | "declaration": true,
5 | "module": "commonjs",
6 | "preserveConstEnums": true,
7 | "sourceMap": false,
8 | "allowJs": true,
9 | "allowSyntheticDefaultImports": true,
10 | "esModuleInterop": true,
11 | "target": "ES2018",
12 | "moduleResolution": "node"
13 | },
14 | "include": [
15 | "src/**/*"
16 | ],
17 | "exclude": [
18 | "node_modules",
19 | "**/*.spec.ts",
20 | "**/*.test.ts",
21 | "example"
22 | ]
23 | }
--------------------------------------------------------------------------------