Build a Landing Page with Webpack 4

  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

A simple, yet useful setup

You can find the complete source code on Github

A very simple landing page
  • CSS styles
  • A template engine
  • Basic JavaScript for newsletter sign-up, image sliders or support chat widgets
  • Nice custom fonts
{
"name": "awesome-service",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
const path = require('path');module.exports = {entry: './src/app.js',output: {filename: 'bundle.js',path: path.resolve(__dirname, '../dist')}};
"scripts": {"build": "webpack --config ./build/webpack.config.js","test": "echo \"Error: no test specified\" && exit 1"}
> webpack --config ./build/webpack.config.jsHash: c7c3ee3e2684c5f6aa62Version: webpack 4.1.1Time: 83msBuilt at: 2018-3-21 08:20:09Asset       Size  Chunks             Chunk Namesbundle.js  545 bytes       0  [emitted]  mainEntrypoint main = bundle.js[0] ./src/app.js 0 bytes {0} [built]WARNING in configurationThe 'mode' option has not been set. Set 'mode' option to 'development' or 'production' to enable defaults for this environment.

Compile Handlebars templates to HTML

<!doctype html><html lang="en-US"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>{{htmlWebpackPlugin.options.title}}</title><!--[if lt IE 9]><body></body></html>
  • handlebars-loader
  • html-webpack-plugin
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
bundle: './src/app.js'
} ,
output: {
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [
{ test: /\.handlebars$/, loader: "handlebars-loader" },
]
},
plugins: [
new webpack.LoaderOptionsPlugin({
options: {
handlebarsLoader: {}
}
}),
new HtmlWebpackPlugin({
title: 'My awesome service',
template: './src/index.handlebars'
})
]
};
/* ... */{ test: /\.handlebars$/, loader: "handlebars-loader" }/* ... */

Handling stylesheets with SCSS and Postcss

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: {
bundle: './src/app.js'
} ,
output: {
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [
{ test: /\.handlebars$/, loader: "handlebars-loader" },
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {

}
},
{
loader: "sass-loader",
options: {

}
}
]
}
]
},
plugins: [
/** Since Webpack 4 */
new webpack.LoaderOptionsPlugin({
options: {
handlebarsLoader: {}
}
}),
new MiniCssExtractPlugin({
filename: "[name]-styles.css",
chunkFilename: "[id].css"
}),
new HtmlWebpackPlugin({
title: 'My awesome service',
template: './src/index.handlebars'
})
]
};
/* ... */
test: /\.(scss|css)$/
/* ... */
{loader: "sass-loader",options: {}}
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');
module.exports = {
entry: {
bundle: './src/app.js'
} ,
output: {
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [
{ test: /\.handlebars$/, loader: "handlebars-loader" },
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {

}
},
{
loader: "postcss-loader",
options: {
autoprefixer: {
browsers: ["last 2 versions"]
},
plugins: () => [
autoprefixer
]
},
},
{
loader: "sass-loader",
options: {
}
}
]
}
]
},
plugins: [
/** Since Webpack 4 */
new webpack.LoaderOptionsPlugin({
options: {
handlebarsLoader: {}
}
}),
new MiniCssExtractPlugin({
filename: "[name]-styles.css",
chunkFilename: "[id].css"
}),
new HtmlWebpackPlugin({
title: 'My awesome service',
template: './src/index.handlebars'
})
]
};

Adding Source maps support

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');
module.exports = {
entry: {
bundle: './src/app.js'
} ,
output: {
path: path.resolve(__dirname, '../dist')
},
devtool: "source-map",
module: {
rules: [
{ test: /\.handlebars$/, loader: "handlebars-loader" },
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: true
}
},
{
loader: "postcss-loader",
options: {
autoprefixer: {
browsers: ["last 2 versions"]
},
sourceMap: true,
plugins: () => [
autoprefixer
]
},
},
{
loader: "sass-loader",
options: {
sourceMap: true
}
}
]
}
]
},
plugins: [
/** Since Webpack 4 */
new webpack.LoaderOptionsPlugin({
options: {
handlebarsLoader: {}
}
}),
new MiniCssExtractPlugin({
filename: "[name]-styles.css",
chunkFilename: "[id].css"
}),
new HtmlWebpackPlugin({
title: 'My awesome service',
template: './src/index.handlebars'
})
]
};
Sourcemap support in Google Chrome

Load image files

/* ... */
{
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

/* ... */
devServer: {
port: 3000,
open: true
}
/* ... */
{
"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": {}
}

Adding markup and styles to build the final landing page

Markup

<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{htmlWebpackPlugin.options.title}}</title>
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="wrapper">
<div class="content">
{{> partials/header title=htmlWebpackPlugin.options.title}}
{{> partials/newsletter}}
{{> partials/social}}
</div>
<div class="visual">
</div>
</div>
<link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans+Condensed:600|IBM+Plex+Sans:400,500|IBM+Plex+Serif" rel="stylesheet">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.8/css/brands.css" integrity="sha384-IiIL1/ODJBRTrDTFk/pW8j0DUI5/z9m1KYsTm/RjZTNV8RHLGZXkUDwgRRbbQ+Jh"
crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.8/css/fontawesome.css" integrity="sha384-q3jl8XQu1OpdLgGFvNRnPdj5VIlCvgsDQTQB6owSOHWlAurxul7f+JpUOVdAiJ5P"
crossorigin="anonymous">
</body>
</html>
{{> partials/header title=htmlWebpackPlugin.options.title}}
{{> partials/newsletter}}
{{> partials/social}}
<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>
<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>
<div class="form">
<div class="form-wrapper">
<input type="text" placeholder="your@email.com" title="Newsletter Sign-Up">
<button type="submit">Sign-Up</button>
</div>
</div>
</section>
<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

/* Variables */$body-font: 'IBM Plex Sans', sans-serif;
$heading-font: 'IBM Plex Sans Condensed', sans-serif;
$base-font-size: 16px;
$font-color: #252227;
$gray: #eee;
$accent: #9c2532;
$pinterest: #bd081c;
$twitter: #1da1f2;
$instagram: #000;
$tablet: 768px;
$desktop: 1024px;
body, html {
padding: 0;
margin: 0;
}
body {
font-family: $body-font;
font-size: $base-font-size;
color: $font-color;
}

div.wrapper {
display: grid;
width: 100vw;
grid-template-areas: "content" "visual";
grid-template-rows: 61.8034% 38.1966%;
text-align: center;
@media screen and (min-width: $tablet){
grid-template-areas: "content visual";
grid-template-columns: 61.8034% 38.1966%;
grid-template-rows: 1fr 0fr;
height: 100vh;
}
@media screen and (min-width: $desktop){
grid-template-areas: "content visual";
grid-template-columns: 61.8034% 38.1966%;
grid-template-rows: 1fr 0fr;
height: 100vh;
text-align: left;
}
}
a {
text-decoration: none;
color: black;
&:hover {
color: $accent;
cursor: pointer;
}
}
.content {
grid-area: content;
display: grid;
grid-template-areas: "header" "sign-up" "social";
grid-template-rows: 1fr 2fr 1fr;
padding: 2em;
}
.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');
}
}
header {
grid-area: header;
display: flex;
flex-direction: column;
h1 {
display: inline-block;
font-family: $heading-font;
}
nav {
ul {
display: flex;
flex-direction: row;
list-style-type: none;
margin: 0;
padding: 0;
justify-content: center;
@media screen and (min-width: $desktop) {
justify-content: flex-start;
}
li {
padding: 0 1em;
border-left: 1px solid $gray;
&:first-child {
padding-left: 0;
border: none;
}
&:last-child {
padding-right: 0;
}

}
}
}
}
.newsletter {
grid-area: sign-up;
h2 {
font-family: $heading-font;
}
.form-wrapper {
display: flex;
flex-direction: column;
@media screen and (min-width: $desktop) {
flex-direction: row;
}
input {
margin: 1em 0;
flex-grow: 1;
height: 2em;
border-top: 1px solid $gray;
padding: 1em;
border-left: 3px solid $accent;
box-shadow: 2px 3px 12px $gray;
font-size: 16px;
}
button {
margin: 1em 0;
background-color: $accent;
width: 100%;
color: white;
font-size: 16px;
font-weight: 700;
border: 1px solid $accent;
height: 2em;
padding: 2em;
line-height: 0em;
box-shadow: 2px 3px 12px $gray;
&:hover {
background-color: lighten($accent, 10);
border: 1px solid lighten($accent, 10);
}
@media screen and (min-width: $desktop) {
margin-left: 24px;
width: auto;
}
}
}
}
.social {ul {
display: flex;
flex-direction: row;
list-style-type: none;
margin: 0;
padding: 0;
justify-content: center;
@media screen and (min-width: $desktop) {
justify-content: flex-start;
}
li {
padding: 0 1em;
font-size: 1.5em;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}

}
.fa-instagram {
color: $instagram;
}
.fa-pinterest {
color: $pinterest;
}
.fa-twitter {
color: $twitter;
}
}
}

Images

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

Development vs. Production Builds

"build": "webpack --config ./build/webpack.config.js"
"build": "NODE_ENV=production webpack --config ./build/webpack.config.js --mode=production"
  • 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');
const isDevelopment = process.env.NODE_ENV !== 'production';module.exports = {
entry: {
bundle: './src/app.js'
} ,
output: {
path: path.resolve(__dirname, '../dist')
},
devtool: isDevelopment && "source-map",
devServer: {
port: 3000,
open: true,
contentBase: path.join(__dirname, "../src"),
},
module: {
rules: [
{ test: /\.handlebars$/, loader: "handlebars-loader" },
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: isDevelopment,
minimize: !isDevelopment
}
},
{
loader: "postcss-loader",
options: {
autoprefixer: {
browsers: ["last 2 versions"]
},
sourceMap: isDevelopment,
plugins: () => [
autoprefixer
]
},
},
{
loader: "sass-loader",
options: {
sourceMap: isDevelopment
}
}
]
},
{
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
}
}
}
]
}
]
},
plugins: [
/** Since Webpack 4 */
new webpack.LoaderOptionsPlugin({
options: {
handlebarsLoader: {}
}
}),
new MiniCssExtractPlugin({
filename: "[name]-styles.css",
chunkFilename: "[id].css"
}),

new HtmlWebpackPlugin({
title: 'My awesome service',
template: './src/index.handlebars',
minify: !isDevelopment && {
html5: true,
collapseWhitespace: true,
caseSensitive: true,
removeComments: true,
removeEmptyElements: true
},
})
]
};

Check out the source code on Github.

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

Thanks for reading!

--

--

https://jduri.com/

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store