Static Website Boilerplate

by Will Moody

Static Website Boilerplate

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:-

Source files

Sitewide Handlebars Details

Scss includes for Grid Layout

Useful Mixins

Handlebars 'each' block helper and json file

Cache Busting Css and Js files