Nowadays, it is up to front end developer how they develop static site whether going old-school way aka using Notepad only or use NodeJS to make development much efficient and easier. However, because the latter environment offers many options that some have found to be very overwhelming, I will present my approach to this which will be a quick step-by-step tutorial to create static website boilerplate.
For the impatient, head over to my Github repository for a boilerplate that can be cloned: link.
# First create your project folder and change folder to it:
mkdir my-project && cd my-project
## Init package.json with default options:
npm init --yes
# Create source folder and change folder to it:
mkdir source && cd source
# Create asset folders and root files:
mkdir stylesheet javascript html images
# For UNIX users for creating files:
touch html/index.html javascript/main.js stylesheet/main.scss
# For Windows users for creating files:
cd. > html/index.html && cd. > javascript/main.js && cd. > stylesheet/main.scss
# Change directory to root folder
cd ..
Here we will install following dependencies:
gulp
for building systemgulp-sass
to compile SCSS to stylesheetgulp-rev
to revision files for production buildgulp-autoprefixer
to prefix rulesgulp-size
to display file sizesgulp-pug
to generate HTML files from Puggulp-util
for useful tools to capture environment flags or print colours in consolegulp-imagemin
to minimize imagesgulp-plumber
to handle build errorsgulp-replace
to replace file names when using revision taskgulp-changed
for images task to avoid minimizing all files when only one changeswebpack
with babel-loader
and babel-core
to enjoy ES2017 (babel-preset-latest
)eslint
and babel-eslint
for maintaining given code standard across JavaScript codebrowser-sync
to run local serverrun-sequence
to run gulp tasks in sequencerimraf-promise
to delete build folder, before creating new fileslodash
for utilities that are used in revision taskTo install all these dependencies, write in console (assuming you are at project root):
npm i gulp gulp-sass gulp-rev gulp-autoprefixer gulp-size gulp-pug gulp-util gulp-imagemin gulp-plumber webpack babel-loader babel-core babel-preset-latest eslint babel-eslint browser-sync run-sequence rimraf-promise lodash -D
To make development easier, create a configuration file, which can be used for getting paths such as source or build directory.
Along that, create gulp task related objects, where are both entry and output paths. A task can also have extra data, for instance, stylesheet object has autoprefixer and sass options object included. See below.
// my-project/config.js
import path from 'path';
import { env as $env } from 'gulp-util';
// Common paths used throughout the Gulp pipeline.
const sourceDir = path.join(__dirname, 'source');
const buildDir = path.join(__dirname, 'public');
const modulesDir = path.join(__dirname, 'node_modules');
// Supported CLI options.
const env = {
debug: !!($env.env === 'debug' || process.env.NODE_ENV === 'development')
};
// Exported configuration object.
export default {
env: env,
buildDir: buildDir,
sourceDir: sourceDir,
modulesDir: modulesDir,
images: {
entry: path.join(sourceDir, 'images', '**', '*.{jpg,jpeg,gif,png,svg,ico}'),
output: path.join(buildDir, 'assets', 'images')
},
javascripts: {
entry: path.join(sourceDir, 'javascript', 'main.js'),
output: path.join(buildDir, 'assets', 'javascript', 'bundle.js')
},
stylesheets: {
entry: path.join(sourceDir, 'stylesheet', 'main.{css,scss,sass}'),
output: path.join(buildDir, 'assets', 'stylesheet'),
sass: {
outputStyle: env.debug ? 'nested' : 'compressed',
precision: 3,
includePaths: [
path.join(sourceDir, 'stylesheet')
]
},
autoprefixer: {
browsers: ['> 1%', 'IE 8']
}
},
html: {
entry: path.join(sourceDir, 'html', '*.{pug,html}'),
output: path.join(buildDir)
},
rev: {
entry: path.join(buildDir, '**', '*.{css,jpg,jpeg,gif,png,svg,js,eot,svg,ttf,woff,woff2,ogv,mp4}'),
output: buildDir,
manifestFile: 'rev-manifest.json',
replace: path.join(buildDir, '**', '*.{css,scss,sass,js,html}')
},
watch: {
entries: [{
files: path.join('images', '**', '*.{jpg,jpeg,gif,png,svg}'),
tasks: ['images']
}, {
files: path.join('stylesheet', '**', '*.{css,scss,sass}'),
tasks: ['stylesheets']
}, {
files: path.join('html', '**', '*.{pug,html}'),
tasks: ['html']
}]
}
};
Create configuration file for Webpack.
// my-project/webpack.config.js
import webpack from 'webpack';
import config from './config';
let plugins = [];
if (!config.env.debug) {
plugins.push(new webpack.optimize.UglifyJsPlugin({
output: {
'comments': false
}
}));
}
export default {
cache: config.env.debug,
debug: config.env.debug,
entry: config.javascripts.entry,
output: {
filename: config.javascripts.output
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
},
resolve: {
extensions: ['', '.js', '.es6'],
modulesDirectories: [
config.modulesDir
]
},
plugins: plugins
};
Create gulpfile to handle building. Pay attention to .babel extension, without it the ES2015 modules are not supported nor certain features (depending on Node version you are using).
//my-project/gulpfile.babel.js
import config from './config';
import webpackConfig from './webpack.config';
import path from 'path';
import _ from 'lodash';
import gulp from 'gulp';
import webpack from 'webpack';
import rimraf from 'rimraf-promise';
import sequence from 'run-sequence';
import $plumber from 'gulp-plumber';
import $sass from 'gulp-sass';
import $sourcemaps from 'gulp-sourcemaps';
import $pug from 'gulp-pug';
import $util from 'gulp-util';
import $rev from 'gulp-rev';
import $replace from 'gulp-replace';
import $prefixer from 'gulp-autoprefixer';
import $size from 'gulp-size';
import $imagemin from 'gulp-imagemin';
import $changed from 'gulp-changed';
// Set environment variable.
process.env.NODE_ENV = config.env.debug ? 'development' : 'production';
// Create browserSync.
const browserSync = require('browser-sync').create();
// Rewrite gulp.src for better error handling.
let gulpSrc = gulp.src;
gulp.src = function () {
return gulpSrc(...arguments)
.pipe($plumber((error) => {
const { plugin, message } = error;
$util.log($util.colors.red(`Error (${plugin}): ${message}`));
this.emit('end');
}));
};
// Create server.
gulp.task('server', () => {
browserSync.init({
notify: false,
server: {
baseDir: config.buildDir
}
});
});
// Compiles and deploys images.
gulp.task('images', () => {
return gulp.src(config.images.entry)
.pipe($changed(config.images.output))
.pipe($imagemin())
.pipe($size({ title: '[images]', gzip: true }))
.pipe(gulp.dest(config.images.output));
});
// Compiles and deploys stylesheets.
gulp.task('stylesheets', () => {
if (config.env.debug) {
return gulp.src(config.stylesheets.entry)
.pipe($sourcemaps.init())
.pipe($sass(config.stylesheets.sass).on('error', $sass.logError))
.pipe($prefixer(config.stylesheets.autoprefixer))
.pipe($sourcemaps.write('/'))
.pipe(gulp.dest(config.stylesheets.output))
.pipe($size({ title: '[stylesheets]', gzip: true }))
.pipe(browserSync.stream({ match: '**/*.css' }));
} else {
return gulp.src(config.stylesheets.entry)
.pipe($sass(config.stylesheets.sass).on('error', $sass.logError))
.pipe($prefixer(config.stylesheets.autoprefixer))
.pipe(gulp.dest(config.stylesheets.output))
.pipe($size({ title: '[stylesheets]', gzip: true }));
}
});
// Compiles and deploys javascript files.
gulp.task('javascripts', (callback) => {
let guard = false;
if (config.env.debug) {
webpack(webpackConfig).watch(100, build(callback));
} else {
webpack(webpackConfig).run(build(callback));
}
function build (done) {
return (err, stats) => {
if (err) {
throw new $util.PluginError('webpack', err);
} else {
$util.log($util.colors.green('[webpack]'), stats.toString());
}
if (!guard && done) {
guard = true;
done();
}
};
}
});
// Compiles and deploys HTML files.
gulp.task('html', () => {
return gulp.src(config.html.entry)
.pipe($pug())
.pipe(gulp.dest(config.html.output));
});
// Files revision.
gulp.task('rev', (callback) => {
gulp.src(config.rev.entry)
.pipe($rev())
.pipe(gulp.dest(config.rev.output))
.pipe($rev.manifest(config.rev.manifestFile))
.pipe(gulp.dest(config.rev.output))
.on('end', () => {
const manifestFile = path.join(config.rev.output, config.rev.manifestFile);
const manifest = require(manifestFile);
let removables = [];
let pattern = (_.keys(manifest)).join('|');
for (let v in manifest) {
if (v !== manifest[v]) {
removables.push(path.join(config.rev.output, v));
}
}
removables.push(manifestFile);
rimraf(`{${removables.join(',')}}`)
.then(() => {
if (!_.isEmpty(config.cdn)) {
gulp.src(config.rev.replace)
.pipe($replace(new RegExp(`((?:\\.?\\.\\/?)+)?([\\/\\da-z\\.-]+)(${pattern})`, 'gi'), (m) => {
let k = m.match(new RegExp(pattern, 'i'))[0];
let v = manifest[k];
return m.replace(k, v).replace(/^((?:\.?\.?\/?)+)?/, _.endsWith(config.cdn, '/') ? config.cdn : `${config.cdn}/`);
}))
.pipe(gulp.dest(config.rev.output))
.on('end', callback)
.on('error', callback);
} else {
gulp.src(config.rev.replace)
.pipe($replace(new RegExp(`${pattern}`, 'gi'), (m) => (manifest[m])))
.pipe(gulp.dest(config.rev.output))
.on('end', callback)
.on('error', callback);
}
});
})
.on('error', callback);
});
// Watch for file changes.
gulp.task('watch', () => {
config.watch.entries.map((entry) => {
gulp.watch(entry.files, { cwd: config.sourceDir }, entry.tasks);
});
gulp.watch([
'public/**/*.html',
'public/**/*.js'
]).on('change', () => {
browserSync.reload();
});
});
gulp.task('default', () => {
let seq = [
'images',
'javascripts',
'stylesheets',
'html'
];
if (config.env.debug) {
seq.push('server');
seq.push('watch');
}
rimraf(config.buildDir)
.then(() => {
sequence(...seq);
})
.catch((error) => {
throw error;
});
});
To make gulpfile.babel.js
work and Webpack compile, create .babelrc
file with following content:
{
"presets": ["latest"]
}
Now, as far it goes with eslint to lint javascript files, create .eslintrc.json
:
{
"env": {
"browser": true,
"node": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"parser": "babel-eslint",
"rules": {
"strict": 0
},
"parserOptions": {
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"off",
"windows"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-console": [
"warn"
]
}
}
The hardest part of creating boilerplate is over, now add some contents to previously created empty files.
// my-project/source/html/index.html
doctype html
html(lang="en", dir="ltr")
head
title Hello word
meta(charset="utf-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0, minimal-ui")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
link(rel="stylesheet", media="all", href="/assets/stylesheet/main.css")
script(src="/assets/javascript/bundle.js")
body
h1 Hello word
// my-project/source/javascript/main.js
const func = () => `3 + 3 + 3 = ${3 + 3 + 3}`;
console.log(func());
// my-project/source/stylesheet/main.scss
$body-background: #eee;
body {
background: $body-background;
}
In package.json
add these two task to scripts object.
"scripts": {
"start": "gulp --env debug",
"build": "gulp --env production && gulp rev"
}
Now, when in developing mood, write in console:
npm run start
When you want production version of your code, write:
npm run build
And everything is set now, if you have any suggestions or criticism, feel free to contribute whether via comments or github repository.