├── .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 | ![npmv1](https://img.shields.io/npm/v/nextjs-sitemap-generator.svg) 3 | 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#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 | } --------------------------------------------------------------------------------