Static Website Boilerplate

Posted By Will Moody On , Reading Time - 7 minutes

Static

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 that is here.

The boilerplate needed to do several tasks, which are:-

  1. Streaming build system
  2. Dynamic HTML templating using Handlebars.js
  3. Sass compilation, vendor prefixing with Autoprefixer, and minification with CssNano
  4. JavaScript module bundling, optimization, and linting with Webpack
  5. Image optimization with Imagemin
  6. 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 the 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 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:-

Sitewide Handlebars Details

Useful Mixins

Handlebars 'each' block helper and JSON file

Cache Busting Css and Js files