Build a Landing Page with Webpack 4

Jonas Duri
Mar 26, 2018 · 16 min read

If you are a designer, a frontend developer or just someone who would like to learn how to use Webpack to build an awesome landing page, then this is for you.

Using build tools can massively improve your development speed and your developer happiness, but learning how to master those tools can be overwhelming, especially if you are new to JavaScript.

Here are the steps you need to take to start making your developer life easier with tools.

  1. Choose Terminal or Powershell, or install a Terminal. For example Cmder for Windows or iTerm2 for MacOS.
  2. Install Nodejs on your machine. Here are some resources to help you if you are on Windows, MacOS or Linux
  3. Get familiar with NPM, a Nodejs package manager. https://www.sitepoint.com/beginners-guide-node-package-manager/
  4. Learn the very basics of Sass and Handlebars.
  5. Take a look at the Webpack website

Don’t choose a pre-made build system yet

There are a number of really great Starter projects for various technology stacks available.

Those projects are very, very good resources for advanced Webpack configurations, but they are not for you if you try to get started.

Unless you know exactly what you are about to build and who is going to use it, there is a large chance that you end up over-engineering right from the start.

Webpack shines when it comes to versatility. It can easily scale from small asset bundling to large corporate build systems.

A simple, yet useful setup

This is the website we are going to create. A simple marketing landing page for your next project.

You can find the complete source code on Github

A very simple landing page

Let’s jump into defining our needs.

Here are some things that every web projects might need in 2018:

  • CSS styles
  • A template engine
  • Basic JavaScript for newsletter sign-up, image sliders or support chat widgets
  • Nice custom fonts

Start by creating a new folder, let’s call it awesome-service

Execute to following commands in your terminal or create the folder manually

mkdir awesome-service
cd awesome-service

Every terminal command in this post assumes that you are in the awesome-service directory.

The first thing that we need is a package.json file to install 3rd party libraries like webpack through npm.

Create a new file by running npm init in your terminal and leave all options as is.

The output should look like this.

{
"name": "awesome-service",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

Next, we are going to install webpack and webpack-cli by running npm install --save-dev webpack webpack-cli

Now its time to think a little bit about our workflow. The power of tooling in front-end projects is the ability to separate our working code from the code that our tools output. To introduce a convention we create two folders. src where all our working code lives and dist where the generated files will be outputted.

mkdir src && mkdir dist && mkdir build

The build directory is used to organize all build-system related files including the configurations for Webpack. By conventions this file is called webpack.config.js so let's stick to this name and create the file inside the build directory.

touch build/webpack.config.js

A very basic Webpack configuration should look like this.

const path = require('path');

Let’s give it a try. For that, we create an empty app.js file in the src directory. After that, we add a script to our package.json file which runs Webpack with our config file.

"scripts": {

Now you can use the command npm run build to trigger Webpack. The output should look similar to this.

> webpack --config ./build/webpack.config.js

Compile Handlebars templates to HTML

The next thing we are going to add is support for a template engine. In this example we choose Handlebars, but this process is easily applicable to other engines like pug, Mustache or ejs.

In our src folder we create the index.handlebars as our main handlebars file.

<!doctype html>

In order to tell webpack to use this file and compile it to regular html, we need something called a loader.

Loaders are Webpack’s way of handling all sorts of files. Think of it like that — “We want to load our index.handlebarsfile and output the result into the dist folder."

You will get used to this concept, I promise!

To make it work we need to install two modules.

  • handlebars-loader
  • html-webpack-plugin

npm install --save-dev handlebars-loader html-webpack-plugin

Webpack is designed to accept an entry file and do it’s magic from there. This behavior can be enhanced with plugins. The html-webpack-plugin takes your html file and places all generated scripts, stylesheets and more inside without you taking care of it manually.

Edit your webpack.config.js file so it looks like this.

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

As you can see, we describe our loaders in the modules object in the rules array. This is where all your Webpack rules go. Right now we only have one which is the rule for loading handlebars templates.

/* ... */

It tests all processed files for the .handlebars extension and applies the handlebars-loader to it.

The HtmlWebpackPlugin introduces the index.handlebars file to our webpack build pipeline. We have to specify the path where our template is located plus any amount of arbitrary data. Here it is just a title that we can use in our template like this: <title>{{htmlWebpackPlugin.options.title}}</title>.

Handling stylesheets with SCSS and Postcss

The next thing we need is a way to integrate our stylesheet. For this example, we use sass to pre-compile our styles. To handle scss files properly we also need more loaders and plugins.

npm install sass-loader node-sass postcss-loader mini-css-extract-plugin css-loader --save-dev

Now we can tell Webpack how to handle the new .scss files.

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

To use the stylesheets it is getting a bit more complex, so let’s go through this step by step.

Again we are testing for a new filetype, in this case scss and also css files if you choose to use regular css files alongside.

/* ... */
test: /\.(scss|css)$/
/* ... */

Then we are telling Webpack to use a whole chain of loaders to process each file. When Webpack discovers a file that is matching the test pattern, it will execute the loaders from Bottom to Top.

So the first loader that kicks in is the sass-loader.

{

It transpiles scss files into css files.

The next loader is the css-loader. It accepts the output from the previous loader (sass-loader) and transforms it into a raw string value. Webpack then passes this string to the MiniCssExtractPlugin.loader which takes care of creating a css file in our dist directory.

When you now run the build with npm run build you will notice that nothing happened, but don't worry. There is just one more piece to the puzzle missing.

The most confusing part is the fact, that the styles don’t get loaded into the distfolder, unless you require or import them inside your app.js file. The reason for this is, that webpack was designed with modern single-page apps in mind. Many frontend frameworks like React or Angular inline the styles into the javascript bundles to dynamically create <style> tags.

For now, we want our styles as a separate .css file in our dist directory since we don't use a javascript framework.

To fix this and have the stylesheets loaded correctly add
import './main.scss'; to your app.js file. Now Webpack is aware of our stylesheet (remember that app.js is our entry file) and can process it.

Alongside the power of sass, we can also use the really nice tool postcss to further automate our workflow. In this example we use postcss with the autoprefixer plugin to automatically add browser prefixes for our style rules.

Postcss helps us to focus on writing modern css without keeping all the browser prefixes in mind.

To add Postcss with the autoprefixer functionality we need to add the postcss-loader to the Webpack system. Install postcss-loader and autoprefixer with npm install --save-dev postcss-loader autoprefixer and edit the webpack.config.js file.

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const autoprefixer = require('autoprefixer');

autoprefixer allows you to specify a set of browsers you want to target. Here we want to add vendor prefixes for the last 2 versions of all major browsers.

If you need more control over the output visit the Browserlist project to see more options https://github.com/ai/browserslist.

Adding Source maps support

Webpack’s generated output is not meant to be human-readable. You basically lose the ability to debug your code while visiting the site in your browser.

Sourcemaps help you to map the generated output to your original source file. These source maps can be either inline at the end of your output file or in a separate .map file. Browsers automatically pick up those files and use them to show you the proper lines in your source code for a style rule or a line of JavaScript code.

To add source map support, edit the loader options in your webpack.config.js file.

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const autoprefixer = require('autoprefixer');

This tells webpack to actually output the generated source maps. You might choose to not do this when you bundle your project files for production use.

When you inspect an element in your browser you can see that it actually maps to the main.scss file instead of the generated bundle-styles.css file.

Sourcemap support in Google Chrome

Load image files

Another thing we need is the ability to load images. For example, if you want to add a background image with css.

Install the following packages with npm install --save-dev file-loader image-webpack-loader.

The file-loader will take care of loading the images and placing them into the correct folder in your dist directory. image-webpack-loader automatically optimizes your images so that we don't accidentally serve a 3.5 MB large image to our users.

Add this loader configuration to your rules array in your webpack.config.js file.

/* ... */
{
test: /\.(jpg|png|gif)$/,
use: [
{
loader: "file-loader",
options: {
name: '[name].[ext]',
outputPath: 'static/',
useRelativePath: true,
}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
optipng: {
enabled: true,
},
pngquant: {
quality: '65-90',
speed: 4
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75
}
}
}
]
}
/* ... */

Adding a development server

Running npm run build after every single change to your source code can get frustrating and is not the way we want to use Webpack in your workflow.

To have a development server while building the site we can use webpack-dev-server.

It’s a tool which serves all of our files in watch mode while we are developing. So every time we make a change Webpack will recompile all files and serve them to our local machine.

npm install --save-dev webpack-dev-server

The development server accepts a number of different configuration options. A very common way to configure webpack-dev-server is by using a devServer property in our webpack.config.js file.

Add the following code in your config file just after devtool: "source-map", .

/* ... */
devServer: {
port: 3000,
open: true
}
/* ... */

This is a basic configuration for webpack-dev-server. To start a web server with an npm script add a start command to your package.json file.

{
"name": "awesome-service",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --config ./build/webpack.config.js",
"build": "webpack --config ./build/webpack.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"autoprefixer": "^8.1.0",
"css-loader": "^0.28.11",
"handlebars": "^4.0.11",
"handlebars-loader": "^1.7.0",
"html-webpack-plugin": "^3.0.7",
"mini-css-extract-plugin": "^0.2.0",
"node-sass": "^4.7.2",
"postcss-loader": "^2.1.1",
"postcss-normalize": "^4.0.0",
"sass-loader": "^6.0.7",
"style-loader": "^0.20.3",
"webpack": "^4.2.0",
"webpack-cli": "^2.0.12",
"webpack-dev-server": "^3.1.1"
},
"dependencies": {}
}

Back in our console, we can now run npm run start. Our development server starts and opens your default browser at http://localhost:3000/.

Adding markup and styles to build the final landing page

Our build system is now in good shape and we can start building our website. I’ll go just very quickly over this so that you get the idea.

Markup

Edit index.handlebars to add custom fonts and the basic structure of the website.

<!doctype html>
<html lang="en-US">

We are using handlebars partials features to spit our markup into multiple files.

{{> partials/header title=htmlWebpackPlugin.options.title}}
{{> partials/newsletter}}
{{> partials/social}}

To use these partials we need to create a partials folder in our src directory and add the following files.

touch src/partials && touch src/partials/header.handlebars && touch src/partials/newsletter.handlebars && touch src/partials/social.handlebars

Add the markup to the partial files.

header.handlebars

<header>
<h1 class="logo">Awesome Service</h1>
<nav class="navigation">
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/">About</a>
</li>
<li>
<a href="/">Contact</a>
</li>
</ul>
</nav>
</header>

newsletter.handlebars

<section class="newsletter">
<h2>Sign-up for the newsletter</h2>
<p>Be one of the first users who joins our awesome service. By registering to the newsletter we'll send you a message as soon as we launch.</p>

social.handlebars

<div class="social">
<h2>Follow us</h2>
<ul>
<li>
<a href="">
<i class="fab fa-instagram"></i>
</a>
</li>
<li>
<a href="">
<i class="fab fa-twitter"></i>
</a>
</li>
<li>
<a href="">
<i class="fab fa-pinterest"></i>
</a>
</li>
</ul>
</div>

Styles

Next, create the main.scss file in your src directory and insert these styles.

Remember to write import './main.scss'; in your app.js files or the styles won't show up in your dist folder.

/* Variables */

Images

We are using a free image here as our background image.

.visual {
grid-area: visual;
background-color: $gray;
background-repeat: no-repeat;
background-size: cover;
background-position-x: 50%;
@media screen and (min-width: $tablet) {
background-image: url('static/scott-webb-207709-unsplash.jpg');
}
}

You can download this free image on Unsplash. Create a static folder in your srcdirectory and save the image here. Thx to Scott Webb for being so kind to share his great photographs.

Development vs. Production Builds

Webpack supports a mode option. Setting mode to development gives you the best development experience, while production optimizes the output for performance and load times.

To start optimizing the output for production modify your package.json file.

Before:

"build": "webpack --config ./build/webpack.config.js"

After:

"build": "NODE_ENV=production webpack --config ./build/webpack.config.js --mode=production"

The next step is to edit the webpack.config.js file to set some options for production use.

  • Disable source maps in “production” mode
  • Minify CSS output
  • Minify JS output
  • Minify HTML output
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin')
const autoprefixer = require('autoprefixer');

When you run npm run start in your terminal, you should see the website in your browser at http://localhost:3000. When you are happy with the results, you can run npm run build and deploy your dist folder to a static site host like Amazon S3 or Netlify

Check out the source code on Github.

What things are you missing for your project?

  • Do you prefer Pug over Handlebars?
  • Less, Sass or CSS variables?
  • Are you missing multi-page support?
  • How to handle environment specific configurations?

Let me known in the comments.

Thanks for reading!

Jonas Duri

Written by

Software engineer on a mission to help people build better digital products. https://jduri.com/

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade