Menu

Introduction

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.

Finished boilerplate code

For the impatient, head over to my Github repository for a boilerplate that can be cloned: link.

Initializing NodeJS project

# 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 ..

Install tools

Here we will install following dependencies:

  • gulp for building system
  • gulp-sass to compile SCSS to stylesheet
  • gulp-rev to revision files for production build
  • gulp-autoprefixer to prefix rules
  • gulp-size to display file sizes
  • gulp-pug to generate HTML files from Pug
  • gulp-util for useful tools to capture environment flags or print colours in console
  • gulp-imagemin to minimize images
  • gulp-plumber to handle build errors
  • gulp-replace to replace file names when using revision task
  • gulp-changed for images task to avoid minimizing all files when only one changes
  • webpack with babel-loader and babel-core to enjoy ES2017 (babel-preset-latest)
  • eslint and babel-eslint for maintaining given code standard across JavaScript code
  • browser-sync to run local server
  • run-sequence to run gulp tasks in sequence
  • rimraf-promise to delete build folder, before creating new files
  • lodash for utilities that are used in revision task

To 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

Configuration file

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']
    }]
  }
};

Webpack configuration file

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
};

Gulpfile

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;
    });
});

Babel and eslint configuration

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"
    ]
  }
}

Where fun begins

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;
}

Public tasks

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.