@videinfra/static-website-builder 2.0.9 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [2.1.0] - 2026-03-04
8
+ ### Changed
9
+ - Updated gulp-sass to use sass-embedded instead of sass
10
+ - Updated gulp-sass to remove legacy code and use only sync mode
11
+
7
12
  ## [2.0.9] - 2026-02-19
8
13
  ### Fixed
9
14
  - Removed file extension from the sitemap URLs
@@ -1,13 +1,15 @@
1
+ @use 'sass:map';
2
+
1
3
  .env-test:before {
2
- content: map-get($env, host);
4
+ content: map.get($env, host);
3
5
  }
4
6
 
5
7
  :root {
6
- #{ --env-test-type-number }: map-get($env, typeNumber);
7
- #{ --env-test-type-empty }: map-get($env, typeEmpty);
8
+ #{ --env-test-type-number }: map.get($env, typeNumber);
9
+ #{ --env-test-type-empty }: map.get($env, typeEmpty);
8
10
  }
9
11
 
10
- @if map-get($env, typeBoolTrue) {
12
+ @if map.get($env, typeBoolTrue) {
11
13
  :root {
12
14
  --env-test-bool-true: true;
13
15
  }
@@ -17,7 +19,7 @@
17
19
  }
18
20
  }
19
21
 
20
- @if map-get($env, typeBoolFalse) {
22
+ @if map.get($env, typeBoolFalse) {
21
23
  :root {
22
24
  --env-test-bool-false: true;
23
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@videinfra/static-website-builder",
3
- "version": "2.0.9",
3
+ "version": "2.1.0",
4
4
  "description": "Customizable static site project builder",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -58,7 +58,7 @@
58
58
  "minimist": "^1.2.8",
59
59
  "nano-memoize": "3.0.16",
60
60
  "rolldown": "^1.0.0-rc.1",
61
- "sass": "^1.97.3",
61
+ "sass-embedded": "^1.97.3",
62
62
  "through": "^2.3.8",
63
63
  "twig": "^1.17.1"
64
64
  },
@@ -4,7 +4,7 @@ import merge from './../../lib/merge.js';
4
4
  import getEnvData from './../../tasks/env/get-env.js';
5
5
  import gulpSass from '../../vendor/gulp-sass/index.js';
6
6
  import assign from 'lodash/assign.js';
7
- import * as sass from 'sass';
7
+ import * as sass from 'sass-embedded';
8
8
 
9
9
  /**
10
10
  * Modify configuration
@@ -32,7 +32,15 @@ export default function processSASSConfig(config, fullConfig) {
32
32
  });
33
33
 
34
34
  // Engine is a function which returns a gulp pipe function
35
- config.engine = function getSASSEngine() {
35
+ config._engineCache = null;
36
+ config.engine = function getSASSEngine(disposeOnly = false) {
37
+ if (config._engineCache) {
38
+ config._engineCache.dispose();
39
+ }
40
+ if (disposeOnly) {
41
+ return;
42
+ }
43
+
36
44
  const sassEngine = gulpSass(sass);
37
45
  const sassConfig = getTaskConfig('stylesheets', 'sass');
38
46
 
@@ -41,7 +49,10 @@ export default function processSASSConfig(config, fullConfig) {
41
49
  }
42
50
 
43
51
  sassConfig.data = merge(getEnvData().sass, sassConfig.data || {});
44
- return sassEngine(sassConfig, /* sync */ true).on('error', sassEngine.logError);
52
+ config._engineCache = sassEngine(sassConfig).on('error', sassEngine.logError);
53
+ config._engineCache.dispose = sassEngine.dispose;
54
+
55
+ return config._engineCache;
45
56
  };
46
57
 
47
58
  // Main 'dependents' config is shared between all tasks
@@ -2,7 +2,7 @@ import gulp from 'gulp';
2
2
  import gulpif from 'gulp-if';
3
3
  import nanomemoize from 'nano-memoize';
4
4
  import ignore from 'gulp-ignore';
5
- import gulpSitemap from 'gulp-sitemap';
5
+ import gulpSitemap from '../../vendor/gulp-sitemap/index.js';
6
6
 
7
7
  import { getDestPath } from './../../lib/get-path.js';
8
8
  import { getTaskConfig } from './../../lib/get-config.js';
@@ -33,13 +33,13 @@ const getGlobPaths = nanomemoize.nanomemoize(function () {
33
33
  });
34
34
 
35
35
 
36
- const getEngine = function () {
36
+ const getEngine = function (disposeOnly = false) {
37
37
  const engine = getTaskConfig('stylesheets', 'engine');
38
- return engine ? engine() : (() => {});
38
+ return engine ? engine(disposeOnly) : (() => {});
39
39
  };
40
40
 
41
41
 
42
- function stylesheets () {
42
+ function stylesheetsBuild () {
43
43
  return gulp.src(getGlobPaths())
44
44
  .pipe(taskStart())
45
45
 
@@ -65,9 +65,14 @@ function stylesheets () {
65
65
  }
66
66
 
67
67
  function stylesheetsWatch () {
68
- return taskWatch(getWatchGlobPaths(true), stylesheets);
68
+ return taskWatch(getWatchGlobPaths(true), stylesheetsBuild);
69
+ }
70
+
71
+ function stylesheetsBuildEnd () {
72
+ getEngine(true);
73
+ return Promise.resolve();
69
74
  }
70
75
 
71
76
 
72
- export const build = stylesheets;
77
+ export const build = gulp.series(stylesheetsBuild, stylesheetsBuildEnd);
73
78
  export const watch = stylesheetsWatch;
@@ -1,4 +1,6 @@
1
1
  // 2025-01-20, Kaspars Zuks: added "options.data" support for variables
2
+ // 2026-03-04, Kaspars Zuks: updated to use "compileString" instead of "renderSync"
3
+ // 2026-03-04, Kaspars Zuks: removed async version
2
4
  'use strict';
3
5
 
4
6
  import path from 'path';
@@ -8,54 +10,17 @@ import PluginError from 'plugin-error';
8
10
  import replaceExtension from 'replace-ext';
9
11
  import stripAnsi from 'strip-ansi';
10
12
  import clonedeep from 'lodash.clonedeep';
11
- import applySourceMap from 'vinyl-sourcemaps-apply';
12
13
  import sassStingify from './sass-stringify.js';
13
14
 
14
15
  const PLUGIN_NAME = 'gulp-sass';
15
16
 
16
- const MISSING_COMPILER_MESSAGE = `
17
- gulp-sass no longer has a default Sass compiler; please set one yourself.
18
- Both the "sass" and "node-sass" packages are permitted.
19
- For example, in your gulpfile:
20
-
21
- import * as sass from 'sass';
22
- import gulpSass from 'gulp-sass';
23
- const instance = gulpSass(sass);
24
- `;
25
-
26
17
  const transfob = (transform) => new Transform({ transform, objectMode: true });
27
18
 
28
19
  /**
29
20
  * Handles returning the file to the stream
30
21
  */
31
- const filePush = (file, sassObject, callback) => {
32
- // Build Source Maps!
33
- if (sassObject.map) {
34
- // Transform map into JSON
35
- const sassMap = JSON.parse(sassObject.map.toString());
36
- // Grab the stdout and transform it into stdin
37
- const sassMapFile = sassMap.file.replace(/^stdout$/, 'stdin');
38
- // Grab the base filename that's being worked on
39
- const sassFileSrc = file.relative;
40
- // Grab the path portion of the file that's being worked on
41
- const sassFileSrcPath = path.dirname(sassFileSrc);
42
-
43
- if (sassFileSrcPath) {
44
- const sourceFileIndex = sassMap.sources.indexOf(sassMapFile);
45
- // Prepend the path to all files in the sources array except the file that's being worked on
46
- sassMap.sources = sassMap.sources.map((source, index) => (index === sourceFileIndex ? source : path.join(sassFileSrcPath, source)));
47
- }
48
-
49
- // Remove 'stdin' from souces and replace with filenames!
50
- sassMap.sources = sassMap.sources.filter((src) => src !== 'stdin' && src);
51
-
52
- // Replace the map file with the original filename (but new extension)
53
- sassMap.file = replaceExtension(sassFileSrc, '.css');
54
- // Apply the map
55
- applySourceMap(file, sassMap);
56
- }
57
-
58
- file.contents = sassObject.css;
22
+ const filePush = (file, css, callback) => {
23
+ file.contents = css;
59
24
  file.path = replaceExtension(file.path, '.css');
60
25
 
61
26
  if (file.stat) {
@@ -81,32 +46,13 @@ const handleError = (error, file, callback) => {
81
46
  return callback(new PluginError(PLUGIN_NAME, error));
82
47
  };
83
48
 
84
- /**
85
- * Escape SCSS variable value for output in SCSS
86
- * @param {any} value Value
87
- * @returns {string} Escaped value
88
- */
89
- const escapeSCSSVariable = (value) => {
90
- if (value !== '' && (value === true || value === false || !isNaN(value))) {
91
- return String(value);
92
- } else {
93
- // Convert to string
94
- return "'" + value.toString().replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n') + "'";
95
- }
96
- };
97
-
98
49
  /**
99
50
  * Main Gulp Sass function
100
51
  */
101
52
 
102
53
  // eslint-disable-next-line arrow-body-style
103
- const gulpSass = (options, sync) => {
54
+ const gulpSass = (options) => {
104
55
  return transfob((file, encoding, callback) => {
105
- if (file.isNull()) {
106
- callback(null, file);
107
- return;
108
- }
109
-
110
56
  if (file.isStream()) {
111
57
  callback(new PluginError(PLUGIN_NAME, 'Streaming not supported'));
112
58
  return;
@@ -117,23 +63,21 @@ const gulpSass = (options, sync) => {
117
63
  return;
118
64
  }
119
65
 
120
- if (!file.contents.length) {
66
+ if (file.contents && !file.contents.length) {
121
67
  file.path = replaceExtension(file.path, '.css');
122
68
  callback(null, file);
123
69
  return;
124
70
  }
125
71
 
126
72
  const opts = clonedeep(options || {});
127
- opts.data = file.contents.toString();
73
+ let source = file.contents.toString();
128
74
 
129
75
  // Stringiyfy variables
130
- if (options.data) {
131
- opts.data = sassStingify(options.data) + opts.data;
76
+ if (opts.data) {
77
+ source = sassStingify(opts.data) + source;
78
+ delete(opts.data);
132
79
  }
133
80
 
134
- // We set the file path here so that libsass can correctly resolve import paths
135
- opts.file = file.path;
136
-
137
81
  // Ensure `indentedSyntax` is true if a `.sass` file
138
82
  if (path.extname(file.path) === '.sass') {
139
83
  opts.indentedSyntax = true;
@@ -150,59 +94,35 @@ const gulpSass = (options, sync) => {
150
94
 
151
95
  opts.includePaths.unshift(path.dirname(file.path));
152
96
 
153
- // Generate Source Maps if the source-map plugin is present
154
- if (file.sourceMap) {
155
- opts.sourceMap = file.path;
156
- opts.omitSourceMapUrl = true;
157
- opts.sourceMapContents = true;
158
- }
97
+ // Rename `includePaths` to `loadPaths`
98
+ opts.loadPaths = opts.includePaths;
99
+ delete(opts.includePaths);
159
100
 
160
- if (sync !== true) {
161
- /**
162
- * Async Sass render
163
- */
164
- gulpSass.compiler.render(opts, (error, obj) => {
165
- if (error) {
166
- handleError(error, file, callback);
167
- return;
168
- }
169
-
170
- filePush(file, obj, callback);
171
- });
172
- } else {
173
- /**
174
- * Sync Sass render
175
- */
176
- try {
177
- filePush(file, gulpSass.compiler.renderSync(opts), callback);
178
- } catch (error) {
179
- handleError(error, file, callback);
180
- }
101
+ try {
102
+ const result = gulpSass.compiler.compileString(source, opts);
103
+ filePush(file, Buffer.from(result.css, 'utf8'), callback);
104
+ } catch (error) {
105
+ handleError(error, file, callback);
181
106
  }
182
107
  });
183
108
  };
184
109
 
185
- /**
186
- * Sync Sass render
187
- */
188
- gulpSass.sync = (options) => gulpSass(options, true);
189
-
190
110
  /**
191
111
  * Log errors nicely
192
112
  */
193
113
  gulpSass.logError = function logError(error) {
194
- const message = new PluginError('sass', error.messageFormatted).toString();
114
+ const message = new PluginError('sass', error.messageFormatted || error.message).toString();
195
115
  process.stderr.write(`${message}\n`);
196
116
  this.emit('end');
197
117
  };
198
118
 
199
- export default (compiler) => {
200
- if (!compiler || !compiler.render) {
201
- const message = new PluginError(PLUGIN_NAME, MISSING_COMPILER_MESSAGE, { showProperties: false }).toString();
202
- process.stderr.write(`${message}\n`);
203
- process.exit(1);
119
+ gulpSass.dispose = function () {
120
+ if (gulpSass.compiler.dispose) {
121
+ gulpSass.compiler.dispose();
204
122
  }
123
+ }
205
124
 
206
- gulpSass.compiler = compiler;
125
+ export default (compiler) => {
126
+ gulpSass.compiler = compiler.initCompiler();
207
127
  return gulpSass;
208
128
  };
@@ -55,7 +55,7 @@
55
55
  "node-sass": "^7.0.1",
56
56
  "postcss": "^8.4.5",
57
57
  "rimraf": "^3.0.2",
58
- "sass": "^1.45.1",
58
+ "sass-embedded": "^1.97.3",
59
59
  "vinyl": "^2.2.1"
60
60
  }
61
61
  }
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Gilad Peleg <gilad@giladpeleg.com> (https://www.giladpeleg.com)
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,293 @@
1
+ # [gulp](https://github.com/wearefractal/gulp)-sitemap
2
+ > Generate a search engine friendly sitemap.xml using a Gulp stream
3
+
4
+ [![NPM version](http://img.shields.io/npm/v/gulp-sitemap.svg?style=flat)](https://www.npmjs.org/package/gulp-sitemap)
5
+ [![NPM Downloads](http://img.shields.io/npm/dm/gulp-sitemap.svg?style=flat)](https://www.npmjs.org/package/gulp-sitemap)
6
+ [![Build Status](http://img.shields.io/travis/pgilad/gulp-sitemap/master.svg?style=flat)](https://travis-ci.org/pgilad/gulp-sitemap)
7
+
8
+ Easily generate a search engine friendly sitemap.xml from your project.
9
+
10
+ :bowtie: Search engines love the sitemap.xml and it helps SEO as well.
11
+
12
+ For information about sitemap properties and structure, see the [wiki for sitemaps](http://www.wikiwand.com/en/Sitemaps)
13
+
14
+ ## Install
15
+
16
+ Install with [npm](https://npmjs.org/package/gulp-sitemap)
17
+
18
+ ```bash
19
+ $ npm install --save-dev gulp-sitemap
20
+ ```
21
+
22
+ ## Example
23
+
24
+ ```js
25
+ var gulp = require('gulp');
26
+ var sitemap = require('gulp-sitemap');
27
+
28
+ gulp.task('sitemap', function () {
29
+ gulp.src('build/**/*.html', {
30
+ read: false
31
+ })
32
+ .pipe(sitemap({
33
+ siteUrl: 'http://www.amazon.com'
34
+ }))
35
+ .pipe(gulp.dest('./build'));
36
+ });
37
+ ```
38
+
39
+ * `siteUrl` is required.
40
+ * `index.html` will be turned into directory path `/`.
41
+ * `404.html` will be skipped automatically. No need to unglob it.
42
+
43
+ Let's see an example of how we can create and output a sitemap, and then return to the original stream files:
44
+ ```js
45
+ var gulp = require('gulp');
46
+ var sitemap = require('gulp-sitemap');
47
+ var save = require('gulp-save');
48
+
49
+ gulp.task('html', function() {
50
+ gulp.src('*.html', {
51
+ read: false
52
+ })
53
+ .pipe(save('before-sitemap'))
54
+ .pipe(sitemap({
55
+ siteUrl: 'http://www.amazon.com'
56
+ })) // Returns sitemap.xml
57
+ .pipe(gulp.dest('./dist'))
58
+ .pipe(save.restore('before-sitemap')) //restore all files to the state when we cached them
59
+ // -> continue stream with original html files
60
+ // ...
61
+ });
62
+ ```
63
+
64
+ ## Options
65
+
66
+ ### siteUrl
67
+
68
+ Your website's base url. This gets prepended to all documents locations.
69
+
70
+ Type: `string`
71
+
72
+ Required: `true`
73
+
74
+ ### fileName
75
+
76
+ Determine the output filename for the sitemap.
77
+
78
+ Type: `string`
79
+
80
+ Default: `sitemap.xml`
81
+
82
+ Required: `false`
83
+
84
+ ### changefreq
85
+
86
+ Gets filled inside the sitemap in the tag `<changefreq>`. Not added by default.
87
+
88
+ Type: `string`
89
+
90
+ Default: `undefined`
91
+
92
+ Valid Values: `['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']`
93
+
94
+ Required: `false`
95
+
96
+ **Note: any falsey value is also valid and will skip this xml tag**
97
+
98
+ ### priority
99
+
100
+ Gets filled inside the sitemap in the tag `<priority>`. Not added by default.
101
+
102
+ Type: `string|function`
103
+
104
+ Default: `undefined`
105
+
106
+ Valid Values: `0.0` to `1.0`
107
+
108
+ Required: `false`
109
+
110
+ **Note: any falsey (non-zero) value is also valid and will skip this xml tag**
111
+
112
+ Example using a function as `priority`:
113
+
114
+
115
+ ```js
116
+ priority: function(siteUrl, loc, entry) {
117
+ // Give pages inside root path (i.e. no slashes) a higher priority
118
+ return loc.split('/').length === 0 ? 1 : 0.5;
119
+ }
120
+ ```
121
+
122
+ ### lastmod
123
+
124
+ The file last modified time.
125
+
126
+ - If `null` then this plugin will try to get the last modified time from the stream vinyl file, or use `Date.now()` as lastmod.
127
+ - If the value is not `null` - It will be used as lastmod.
128
+ - When `lastmod` is a function, it is executed with the current file given as parameter. (Note: the function is expected to be sync).
129
+ - A string can be used to manually set a fixed `lastmod`.
130
+
131
+ Type: `string|datetime|function`
132
+
133
+ Default: `null`
134
+
135
+ Required: `false`
136
+
137
+ Example that uses git to get lastmod from the latest commit of a file:
138
+
139
+ ```js
140
+ lastmod: function(file) {
141
+ var cmd = 'git log -1 --format=%cI "' + file.relative + '"';
142
+ return execSync(cmd, {
143
+ cwd: file.base
144
+ }).toString().trim();
145
+ }
146
+ ```
147
+
148
+ **Note: any falsey (other than null) value is also valid and will skip this xml tag**
149
+
150
+ ### newLine
151
+
152
+ How to join line in the target sitemap file.
153
+
154
+ Type: `string`
155
+
156
+ Default: Your OS's new line, mostly: `\n`
157
+
158
+ Required: `false`
159
+
160
+ ### spacing
161
+
162
+ How should the sitemap xml file be spaced. You can use `\t` for tabs, or ` ` with 2
163
+ spaces if you'd like.
164
+
165
+ Type: `string`
166
+
167
+ Default: ` ` (4 spaces)
168
+
169
+ Required: `false`
170
+
171
+ ### noindex
172
+
173
+ Exclude pages from the sitemap when the `robots` meta tag is set to `noindex`. The plugin needs to be able to read the contents of the files for this to have an effect.
174
+
175
+ Type: `boolean`
176
+
177
+ Default: `false`
178
+
179
+ Required: `false`
180
+
181
+ ### images
182
+
183
+ For generate sitemap for images per page, just enable images flag to `true`
184
+
185
+ Type: `boolean`
186
+
187
+ Default: `undefined`
188
+
189
+ Required: `false`
190
+
191
+
192
+ ### mappings
193
+
194
+ An object to custom map pages to their own configuration.
195
+
196
+ This should be an array with the following structure:
197
+
198
+ Type: `array`
199
+
200
+ Default: `[]`
201
+
202
+ Required: `false`
203
+
204
+ Example:
205
+
206
+ ```js
207
+ mappings: [{
208
+ pages: [ 'minimatch pattern' ],
209
+ changefreq: 'hourly',
210
+ priority: 0.5,
211
+ lastmod: Date.now(),
212
+ getLoc(siteUrl, loc, entry) {
213
+ // Removes the file extension if it exists
214
+ return loc.replace(/\.\w+$/, '');
215
+ },
216
+ hreflang: [{
217
+ lang: 'ru',
218
+ getHref(siteUrl, file, lang, loc) {
219
+ return 'http://www.amazon.ru/' + file;
220
+ }
221
+ }]
222
+ },
223
+ //....
224
+ ]
225
+ ```
226
+
227
+ - Every file will be matched against the supplied patterns
228
+ - Only defined attributes for a matched file are applied.
229
+ - Only the first match will apply, so consequent matches for the filename will not apply.
230
+ - Possible attributes to set: `hreflang`, `changefreq`, `priority`, `loc` and `lastmod`.
231
+ - All rules applying to [options](#options) apply to the attributes that can overridden.
232
+
233
+ ##### pages
234
+
235
+ Type: `array`
236
+
237
+ Required: `true`
238
+
239
+ This is an array with [minimatch](https://github.com/isaacs/minimatch) patterns to match the
240
+ relevant pages to override.
241
+ Every file will be matched against the supplied patterns.
242
+
243
+ Uses [multimatch](https://github.com/sindresorhus/multimatch) to match patterns against filenames.
244
+
245
+ Example: `pages: ['home/index.html', 'home/see-*.html', '!home/see-admin.html']`
246
+
247
+ ##### hreflang
248
+
249
+ Matching pages can get their `hreflang` tags set using this option.
250
+
251
+ The input is an array like so:
252
+
253
+ ```js
254
+ hreflang: [{
255
+ lang: 'ru',
256
+ getHref: function(siteUrl, file, lang, loc) {
257
+ // return href src for the hreflang. For example:
258
+ return 'http://www.amazon.ru/' + file;
259
+ }
260
+ }]
261
+ ```
262
+
263
+ ##### getLoc
264
+
265
+ Matching pages can get their `loc` tag modified by using a function.
266
+
267
+ ```js
268
+ getLoc: function(siteUrl, loc, entry) {
269
+ return loc.replace(/\.\w+$/, '');
270
+ }
271
+ ```
272
+
273
+ #### verbose
274
+
275
+ Type: `boolean`
276
+
277
+ Required: `false`
278
+
279
+ Default: `false`
280
+
281
+ If true, will log the number of files that where handled.
282
+
283
+ ## Complementary plugins
284
+
285
+ - [gulp-sitemap-files](https://github.com/adam-lynch/gulp-sitemap-files) - Get all files listed in a sitemap (Perhaps one generated from this plugin)
286
+
287
+ ## Thanks
288
+
289
+ To [grunt-sitemap](https://github.com/RayViljoen/grunt-sitemap) for the inspiration on writing this.
290
+
291
+ ## License
292
+
293
+ MIT © [Gilad Peleg](https://www.giladpeleg.com)
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+ const chalk = require('chalk');
3
+ const defaults = require('lodash/defaults');
4
+ const log = require('fancy-log');
5
+ const path = require('path');
6
+ const PluginError = require('plugin-error');
7
+ const through = require('through2');
8
+ const Vinyl = require('vinyl');
9
+
10
+ const pluginName = 'gulp-sitemap';
11
+ const sitemap = require('./lib/sitemap');
12
+
13
+ module.exports = function (options = {}) {
14
+ const config = defaults({}, options, {
15
+ changefreq: undefined,
16
+ fileName: 'sitemap.xml',
17
+ lastmod: null,
18
+ mappings: [],
19
+ newLine: '\n',
20
+ priority: undefined,
21
+ spacing: ' ',
22
+ verbose: false,
23
+ noindex: false
24
+ });
25
+ const entries = [];
26
+ let firstFile;
27
+ let msg;
28
+
29
+ if (!config.siteUrl) {
30
+ msg = 'siteUrl is a required param';
31
+ throw new PluginError(pluginName, msg);
32
+ }
33
+ if (options.changeFreq) {
34
+ msg = chalk.magenta('changeFreq') + ' has been deprecated. Please use ' + chalk.cyan('changefreq');
35
+ throw new PluginError(pluginName, msg);
36
+ }
37
+ // site url should have a trailing slash
38
+ if (config.siteUrl.slice(-1) !== '/') {
39
+ config.siteUrl = config.siteUrl + '/';
40
+ }
41
+
42
+ return through.obj(function (file, enc, callback) {
43
+ //we handle null files (that have no contents), but not dirs
44
+ if (file.isDirectory()) {
45
+ return callback(null, file);
46
+ }
47
+
48
+ if (file.isStream()) {
49
+ msg = 'Streaming not supported';
50
+ return callback(new PluginError(pluginName), msg);
51
+ }
52
+
53
+ //skip 404 file
54
+ if (/404\.html?$/i.test(file.relative)) {
55
+ return callback();
56
+ }
57
+
58
+ if(options.noindex){
59
+ const contents = file.contents.toString();
60
+ if (/<meta [^>]*?noindex/i.test(contents)) {
61
+ return callback();
62
+ }
63
+ }
64
+
65
+ if (!firstFile) {
66
+ firstFile = file;
67
+ }
68
+
69
+ const entry = sitemap.getEntryConfig(file, config);
70
+ entries.push(entry);
71
+ callback();
72
+ },
73
+ function (callback) {
74
+ if (!firstFile) {
75
+ return callback();
76
+ }
77
+ const contents = sitemap.prepareSitemap(entries, config);
78
+ if (options.verbose) {
79
+ msg = 'Files in sitemap: ' + entries.length;
80
+ log(pluginName, msg);
81
+ }
82
+ //create and push new vinyl file for sitemap
83
+ this.push(new Vinyl({
84
+ cwd: firstFile.cwd,
85
+ base: firstFile.cwd,
86
+ path: path.join(firstFile.cwd, config.fileName),
87
+ contents: Buffer.from(contents)
88
+ }));
89
+ callback();
90
+ });
91
+ };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Changes by Kaspars Zuks:
3
+ * - Removed xmlns:image attribute from `urlset` if images is false
4
+ */
5
+ 'use strict';
6
+ const fs = require('fs');
7
+ const defaults = require('lodash/defaults');
8
+ const find = require('lodash/find');
9
+ const includes = require('lodash/includes');
10
+ const multimatch = require('multimatch');
11
+ const slash = require('slash');
12
+ const pick = require('lodash/pick');
13
+
14
+ const header = [
15
+ '<?xml version="1.0" encoding="UTF-8"?>',
16
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
17
+ ];
18
+
19
+ const headerHref = [
20
+ '<?xml version="1.0" encoding="UTF-8"?>',
21
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">'
22
+ ];
23
+
24
+ const footer = ['</urlset>'];
25
+ const allowedProperties = ['getLoc', 'lastmod', 'priority', 'changefreq', 'hreflang'];
26
+
27
+ function getEntryConfig(file, siteConfig) {
28
+ const relativePath = file.relative;
29
+ const mappingsForFile = find(siteConfig.mappings, function(item) {
30
+ return multimatch(relativePath, item.pages).length > 0;
31
+ }) || {};
32
+
33
+ const entry = defaults(
34
+ {},
35
+ pick(mappingsForFile, allowedProperties),
36
+ pick(siteConfig, allowedProperties)
37
+ );
38
+
39
+ // if images enabled add headerHref
40
+ if (siteConfig.images) {
41
+ header[1] = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">';
42
+ headerHref[1] = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">'
43
+ }
44
+
45
+ if (entry.lastmod === null) {
46
+ // calculate mtime manually
47
+ entry.lastmod = file.stat && file.stat.mtime || Date.now();
48
+ } else if (typeof entry.lastmod === 'function') {
49
+ entry.lastmod = entry.lastmod(file);
50
+ }
51
+
52
+ //turn index.html into -> /
53
+ let relativeFile = relativePath.replace(/(\/|\\|^)index\.html?$/, '/');
54
+ if (relativeFile === '/') {
55
+ relativeFile = '';
56
+ }
57
+ //url location. Use slash to convert windows \\ or \ to /
58
+ const adjustedFile = slash(relativeFile);
59
+ entry.loc = siteConfig.siteUrl + adjustedFile;
60
+ entry.file = adjustedFile;
61
+ entry.source = file.history[0];
62
+
63
+ return entry;
64
+ }
65
+
66
+ function wrapTag(tag, value) {
67
+ return `
68
+ <${ tag }>${ value }</${ tag }>
69
+ `;
70
+ }
71
+
72
+ function createImageSitemap(imageURL) {
73
+ return `
74
+ <image:image>
75
+ <image:loc>${imageURL}</image:loc>
76
+ </image:image>
77
+ `;
78
+ }
79
+
80
+ function notIsHTTPURL(text) {
81
+ return !(/https?:\/\//ig).test(text);
82
+ }
83
+
84
+ function getImagesUrl(entry, siteConfig) {
85
+ const reImageMatch = /<img\s+src="((https?:\/\/)?[\w\.\/\-@\?=&]+)"/ig;
86
+ const reSourceMatch = /"((https?:\/\/)?[\w\.\/\-@\?=&]+)"/ig;
87
+ const html = fs.readFileSync(entry.source, { encoding : 'utf8'})
88
+ const images = html.match(reImageMatch);
89
+
90
+ if (images === null) {
91
+ return [];
92
+ }
93
+
94
+ const URLs = images.map(image => {
95
+ let currentURL = image.match(reSourceMatch)[0].replace(/^\"|\"$/ig, '');
96
+
97
+ if (notIsHTTPURL(currentURL)) {
98
+ currentURL = currentURL.replace(/^\/|\.\//, '');
99
+ currentURL = siteConfig.siteUrl + currentURL;
100
+ }
101
+
102
+ return currentURL;
103
+ });
104
+
105
+ return URLs;
106
+ }
107
+
108
+ function generateImagesMap(entry, siteConfig) {
109
+ let imagesURLS = [];
110
+ let XMLImageList = '';
111
+
112
+ // Crawling page for images
113
+ imagesURLS = getImagesUrl(entry, siteConfig);
114
+
115
+ // if truthy create image sitemap
116
+ if (imagesURLS.length) {
117
+ XMLImageList = imagesURLS.map(imageURL => createImageSitemap(imageURL)).join('')
118
+ }
119
+
120
+ return XMLImageList;
121
+ }
122
+
123
+ /**
124
+ * processEntry
125
+ *
126
+ * @param entry
127
+ * @param siteConfig
128
+ * @return {string}
129
+ */
130
+ function processEntry(entry, siteConfig) {
131
+ const returnArr = [siteConfig.spacing + '<url>'];
132
+
133
+ const loc = entry.getLoc ? entry.getLoc(siteConfig.siteUrl, entry.loc, entry) : entry.loc;
134
+ returnArr.push(siteConfig.spacing + siteConfig.spacing + wrapTag('loc', loc) + (siteConfig.images ? generateImagesMap(entry, siteConfig) : ''));
135
+
136
+ let lastmod = entry.lastmod;
137
+ if (lastmod) {
138
+ //format mtime to ISO (same as +00:00)
139
+ lastmod = new Date(lastmod).toISOString();
140
+ returnArr.push(siteConfig.spacing + siteConfig.spacing + wrapTag('lastmod', lastmod));
141
+ }
142
+
143
+ const changefreq = entry.changefreq;
144
+ if (changefreq) {
145
+ returnArr.push(siteConfig.spacing + siteConfig.spacing + wrapTag('changefreq', changefreq));
146
+ }
147
+
148
+ let priority;
149
+
150
+ if (typeof entry.priority === 'function') {
151
+ priority = entry.priority(siteConfig.siteUrl, entry.loc, entry);
152
+ } else if (typeof entry.priority !== 'undefined') {
153
+ priority = entry.priority;
154
+ }
155
+ if (typeof priority !== 'undefined') {
156
+ returnArr.push(siteConfig.spacing + siteConfig.spacing + wrapTag('priority', priority));
157
+ }
158
+
159
+ const hreflang = entry.hreflang;
160
+ if (hreflang && hreflang.length > 0) {
161
+ const file = entry.file;
162
+ hreflang.forEach(function(item) {
163
+ const href = item.getHref(siteConfig.siteUrl, file, item.lang, loc);
164
+ const tag = '<xhtml:link rel="alternate" hreflang="' + item.lang + '" href="' + href + '" />';
165
+ returnArr.push(siteConfig.spacing + siteConfig.spacing + tag);
166
+ });
167
+ }
168
+
169
+ returnArr.push(siteConfig.spacing + '</url>');
170
+ return returnArr.join(siteConfig.newLine);
171
+ }
172
+
173
+ function prepareSitemap(entries, config) {
174
+ const entriesHref = entries.some(function(entry) {
175
+ return entry && entry.hreflang && entry.hreflang.length;
176
+ });
177
+ return (entriesHref ? headerHref : header)
178
+ .concat(entries.map(function(entry) {
179
+ return processEntry(entry, config);
180
+ }))
181
+ .concat(footer)
182
+ .join(config.newLine)
183
+ .toString();
184
+ }
185
+
186
+ const VALID_CHANGE_FREQUENCIES = [
187
+ 'always',
188
+ 'hourly',
189
+ 'daily',
190
+ 'weekly',
191
+ 'monthly',
192
+ 'yearly',
193
+ 'never'
194
+ ];
195
+
196
+ function isChangefreqValid(changefreq) {
197
+ // empty changefreq is valid
198
+ if (!changefreq) {
199
+ return true;
200
+ }
201
+ return includes(VALID_CHANGE_FREQUENCIES, changefreq.toLowerCase());
202
+ }
203
+
204
+ exports.getEntryConfig = getEntryConfig;
205
+ exports.isChangefreqValid = isChangefreqValid;
206
+ exports.prepareSitemap = prepareSitemap;
207
+ exports.processEntry = processEntry;
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "gulp-sitemap",
3
+ "version": "8.0.0",
4
+ "description": "Generate a search engine friendly sitemap.xml using a Gulp stream",
5
+ "repository": "pgilad/gulp-sitemap",
6
+ "license": "MIT",
7
+ "author": "Gilad Peleg <gilad@giladpeleg.com> (https://www.giladpeleg.com)",
8
+ "main": "index.js",
9
+ "files": [
10
+ "index.js",
11
+ "lib"
12
+ ],
13
+ "engines": {
14
+ "node": ">=10"
15
+ },
16
+ "scripts": {
17
+ "test": "mocha -R spec test/*.spec.js"
18
+ },
19
+ "keywords": [
20
+ "gulpplugin",
21
+ "gulp",
22
+ "js",
23
+ "javascript",
24
+ "SEO",
25
+ "sitemap",
26
+ "sitemap.xml",
27
+ "Google",
28
+ "search-engine",
29
+ "xml"
30
+ ],
31
+ "dependencies": {
32
+ "chalk": "^2.4.2",
33
+ "fancy-log": "^1.3.3",
34
+ "lodash": "^4.17.15",
35
+ "multimatch": "^4.0.0",
36
+ "plugin-error": "^1.0.1",
37
+ "slash": "^3.0.0",
38
+ "through2": "^3.0.1",
39
+ "vinyl": "^2.2.0"
40
+ },
41
+ "devDependencies": {
42
+ "gulp": "^4.0.2",
43
+ "gulp-rename": "^1.4.0",
44
+ "mocha": "^6.2.2",
45
+ "should": "^13.2.3",
46
+ "strip-ansi": "^5.2.0"
47
+ }
48
+ }