Static Website Boilerplate
by Will Moody

Approximate Reading Time: 7 minutes
Not all websites warrant a CMS such as Perch and are simply static sites where the content does not change, these can easily be built using a boilerplate. So I have just spent a few hours developing a static website boilerplate that will allow me to build a website very quickly with the minimum of fuss which is here.
The boilerplate needed to do several tasks, which are:-
- Streaming build system
- Dynamic HTML templating using Handlebars.js
- Sass compilation, vendor prefixing with Autoprefixer, and minification with CssNano
- JavaScript module bundling, optimization, and linting with Webpack
- Image optimization with Imagemin
- Cache any updates
To make life easier I took a pre-existing boilerplate skeleton which was the Dynamit front end boilerplate, this removes a lot of the donkey work and is a very good basepoint.
To the package.json I added some dependencies which I use in most projects such as Slick-carousel and CookieConsent, and also changed to the more up-to-date Babel-preset-env to give a final package.json of:-
package.json
{
"author": "FatBuddhaDesigns",
"license": "UNLICENSED",
"private": true,
"version": "1.0.0",
"scripts": {
"setup": "npm install && mv README.tpl.md README.md",
"prestart": "npm install",
"build": "npm run prestart && gulp",
"start": "gulp --dev",
"lint": "eslint .",
"validate": "npm prune && npm ls"
},
"engines": {
"node": "^4",
"npm": "^2"
},
"devDependencies": {
"babel-core": "^6.5.2",
"babel-loader": "^6.2.2",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-2": "^6.11.0",
"browser-sync": "^2.8.0",
"del": "^2.2.0",
"eslint": "3.19.0",
"eslint-config-airbnb-base": "^11.1.3",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-node": "^4.2.2",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"fabricator-assemble": "^1.2.0",
"gulp": "^3.9.0",
"gulp-autoprefixer": "^3.1.0",
"gulp-cssnano": "^2.1.1",
"gulp-if": "^2.0.0",
"gulp-imagemin": "^3.4.0",
"gulp-plumber": "^1.0.1",
"gulp-postcss": "^6.2.0",
"gulp-sass": "^2.0.4",
"gulp-sourcemaps": "^1.6.0",
"gulp-util": "^3.0.1",
"handlebars-loader": "^1.1.4",
"imports-loader": "^0.6.4",
"istanbul-instrumenter-loader": "^1.0.0",
"jasmine-core": "^2.5.2",
"karma": "^1.3.0",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.0.2",
"karma-phantomjs-launcher": "^1.0.2",
"karma-spec-reporter": "0.0.26",
"karma-webpack": "^1.8.0",
"node-notifier": "^5.0.2",
"postcss-import": "^8.1.2",
"postcss-preset-env": "^5.3.0",
"precommit-hook-eslint": "^3.0.0",
"precss": "^1.4.0",
"run-sequence": "^1.0.2",
"scripts-loader": "^0.1.3",
"webpack": "^1.10.5"
},
"pre-commit": [
"lint",
"validate",
"test"
],
"dependencies": {
"aos": "^2.2.0",
"cookieconsent": "^3.1.0",
"jquery": "^3.3.1",
"slick-carousel": "^1.8.1",
"svgxuse": "^1.2.6"
}
}
From there it's a case of building on the Dyanimit gulpfile.js, the original file only optimized images and I like to build most websites using SVG icons from Font Awesome and so I added a task to optimizes those along, I also added a task to copy over any fonts that I use, and lastly I added a task that will copy over files such as .htaccess, robots.txt and favicons on build.
The finished gulpfile.js looks like this:-
gulpfile.js
const assemble = require('fabricator-assemble');
const autoprefixer = require('gulp-autoprefixer');
const browserSync = require('browser-sync');
const cssnano = require('gulp-cssnano');
const del = require('del');
const gulp = require('gulp');
const gulpif = require('gulp-if');
const gutil = require('gulp-util');
const imagemin = require('gulp-imagemin');
const notifier = require('node-notifier');
const runSequence = require('run-sequence');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const webpack = require('webpack');
const Server = require('karma').Server;
const reload = browserSync.reload;
// configuration
const config = {
templates: {
src: ['src/templates/**/*', '!src/templates/+(layouts|components)/**'],
dest: 'dist',
watch: ['src/templates/**/*', 'src/data/**/*.{json,yml}'],
layouts: 'src/templates/layouts/*',
partials: ['src/templates/components/**/*'],
data: 'src/data/**/*.{json,yml}',
},
scripts: {
src: './src/assets/scripts/main.js',
dest: 'dist/assets/scripts',
watch: 'src/assets/scripts/**/*',
},
styles: {
src: 'src/assets/styles/main.scss',
dest: 'dist/assets/styles',
watch: 'src/assets/styles/**/*',
browsers: ['last 2 version'],
},
print: {
src: 'src/assets/styles/print.scss',
dest: 'dist/assets/styles',
watch: 'src/assets/styles/print.scss',
browsers: ['last 2 version'],
},
images: {
src: 'src/assets/images/**/*',
dest: 'dist/assets/images',
watch: 'src/assets/images/**/*',
},
svg: {
src: 'src/assets/svg/**/*',
dest: 'dist/assets/svg',
watch: 'src/assets/svg/**/*',
},
copyodd: {
src: 'src/assets/build_files/**/*',
dest: 'dist/',
watch: 'src/assets/build_files/**/*',
},
fonts: {
src: 'src/assets/fonts/**/*',
dest: 'dist/assets/fonts',
watch: 'src/assets/fonts/**/*',
},
dev: gutil.env.dev,
};
// clean
gulp.task('clean', del.bind(null, ['dist']));
// tests
gulp.task('test', (done) => {
new Server({
configFile: `${__dirname}/karma.conf.js`,
singleRun: false,
}, done).start();
});
// templates
gulp.task('templates', (done) => {
assemble({
layouts: config.templates.layouts,
views: config.templates.src,
materials: config.templates.partials,
data: config.templates.data,
keys: {
views: 'templates',
materials: 'components',
},
dest: config.templates.dest,
logErrors: config.dev,
helpers: {
// head <link rel="stylesheet" href="{{baseurl}}/assets/styles/main.css?v={{release}}">
release: function release() {
return new Date().getTime();
},
// {{ default description "string of content used if description var is undefined" }}
default: function defaultFn(...args) {
return args.find(value => !!value);
},
// {{ concat str1 "string 2" }}
concat: function concat(...args) {
return args.slice(0, args.length - 1).join('');
},
// {{> (dynamicPartial name) }} ---- name = 'nameOfComponent'
dynamicPartial: function dynamicPartial(name) {
return name;
},
eq: function eq(v1, v2) {
return v1 === v2;
},
ne: function ne(v1, v2) {
return v1 !== v2;
},
and: function and(v1, v2) {
return v1 && v2;
},
or: function or(v1, v2) {
return v1 || v2;
},
not: function not(v1) {
return !v1;
},
gte: function gte(a, b) {
return +a >= +b;
},
lte: function lte(a, b) {
return +a <= +b;
},
plus: function plus(a, b) {
return +a + +b;
},
minus: function minus(a, b) {
return +a - +b;
},
divide: function divide(a, b) {
return +a / +b;
},
multiply: function multiply(a, b) {
return +a * +b;
},
abs: function abs(a) {
return Math.abs(a);
},
mod: function mod(a, b) {
return +a % +b;
},
},
});
done();
});
// scripts
const webpackConfig = require('./webpack.config')(config);
gulp.task('scripts', (done) => {
webpack(webpackConfig, (err, stats) => {
if (err) {
gutil.log(gutil.colors.red(err()));
}
const result = stats.toJson();
if (result.errors.length) {
result.errors.forEach((error) => {
gutil.log(gutil.colors.red(error));
notifier.notify({
title: 'JS Build Error',
message: error,
});
});
}
done();
});
});
// SASS styles
gulp.task('styles', () =>
gulp.src(config.styles.src)
.pipe(sourcemaps.init())
.pipe(sass({
includePaths: './node_modules',
}).on('error', sass.logError))
.pipe(autoprefixer({
browsers: config.styles.browsers,
}))
.pipe(gulpif(!config.dev, cssnano({
autoprefixer: false,
})))
.pipe(sourcemaps.write())
.pipe(gulp.dest(config.styles.dest))
.pipe(gulpif(config.dev, reload({
stream: true,
}))));
// Print styles
gulp.task('print', () =>
gulp.src(config.print.src)
.pipe(sourcemaps.init())
.pipe(sass({}).on('error', sass.logError))
.pipe(autoprefixer({
browsers: config.print.browsers,
}))
.pipe(gulpif(!config.dev, cssnano({
autoprefixer: false,
})))
.pipe(sourcemaps.write())
.pipe(gulp.dest(config.print.dest))
.pipe(gulpif(config.dev, reload({
stream: true,
}))));
// PostCSS Styles
/*
gulp.task('styles', () => {
return gulp.src(config.styles.src)
.pipe(postcss(postCSSConfig))
.pipe(gulpif(config.dev, reload({ stream: true })))
.pipe(gulp.dest(config.styles.dest));
});
*/
// images
gulp.task('images', () =>
gulp.src(config.images.src)
.pipe(imagemin({
progressive: true,
interlaced: true,
}))
.pipe(gulp.dest(config.images.dest)));
// svg
gulp.task('svg', () => {
return gulp.src(config.svg.src)
.pipe(imagemin([
imagemin.svgo({
plugins: [{
cleanupIDs: {
remove: false
}
},
{
cleanupNumericValues: {
floatPrecision: 2
}
},
{
removeStyleElement: true
},
{
removeTitle: true
},
]
})
]))
.pipe(gulp.dest(config.svg.dest));
});
// fonts
gulp.task('fonts', () =>
gulp.src('./src/assets/fonts/**/*')
.pipe(gulp.dest('./dist/assets/fonts')));
// copy odd
gulp.task('copyodd', () =>
gulp.src(['./src/build_files/**/*.php', './src/build_files/**/*.txt', './src/build_files/**/.htaccess', './src/build_files/**/*.xml', './src/build_files/**/*.png', './src/build_files/**/*.ico', './src/build_files/**/*.png', './src/build_files/**/*.json'])
.pipe(gulp.dest('./dist/')));
// server
gulp.task('serve', () => {
browserSync({
server: {
baseDir: config.templates.dest,
},
notify: false,
logPrefix: 'BrowserSync',
});
gulp.task('templates:watch', ['templates'], reload);
gulp.watch(config.templates.watch, ['templates:watch']);
gulp.task('styles:watch', ['styles']);
gulp.watch(config.styles.watch, ['styles:watch']);
gulp.task('print:watch', ['print']);
gulp.watch(config.print.watch, ['print:watch']);
gulp.task('scripts:watch', ['scripts'], reload);
gulp.watch(config.scripts.watch, ['scripts:watch']);
gulp.task('svg:watch', ['svg'], reload);
gulp.watch(config.svg.watch, ['svg:watch']);
gulp.task('fonts:watch', ['fonts'], reload);
gulp.watch(config.fonts.watch, ['fonts:watch']);
gulp.task('copyodd:watch', ['copyodd'], reload);
gulp.watch(config.copyodd.watch, ['copyodd:watch']);
gulp.task('images:watch', ['images'], reload);
gulp.watch(config.images.watch, ['images:watch']);
});
// default build task
gulp.task('default', ['clean'], () => {
// define build tasks
const tasks = [
'templates',
'scripts',
'styles',
'print',
'images',
'svg',
'copyodd',
'fonts',
];
// run build
runSequence(tasks, () => {
if (config.dev) {
gulp.start('serve');
}
});
});
Once you have these two files in place it's relatively easy to spin up a website.
Firstly, copy all the files in the original Dynamit front end boilerplate download along with your modified package.json and gulpfile.js and with any SCR files you have to a new directory; then using Git navigate to that directory and type npm install, this will load all node_modules files as required.
Once this is completed it's a case of using the other npm commands, firstly it's npm start, this will produce a /dist folder and using Browsersync will load the page to your browser and then when you have a finished building your website it's npm run build to produce the final optimized files, images, SVG's etc.
Along the way I have found some handy methods which reduce the time needed to produce a static website using this boilerplate, these are all detailed in these posts:-