Sometimes it makes total sense to build an old fashioned static website. It can be not only faster but also simpler than throwing in a full JavaScript framework just to build a website with only a few pages. In the following, I'll create a template with scss, linting, minifying and more using npm scripts.
For the finished repository, which can be used as a template go to https://github.com/wwebdev/static-website-template
Initial Setup
Requirements:
Installed node.js / npm
First of all, I'll initialize an empty project by opening the console and typing npm init
.
Then I create the initial index.html
in the root directory:
<!DOCTYPE html>
<html lang="de">
<head>
<title>Static Website Template</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h1>Static Website Template</h1>
</body>
</html>
Now you can just open the index.html
in your browser.
This doesn't look very exiting yet - so let's add some styling.
The CSS
For the CSS I will implement
- Sass: This is a preprocessor, which will compile
.scss
files into CSS. With scss you can use variables, nesting, partials, modules and more. It makes writing CSS a lot easier, clearer and more modular. - Autoprefixer: This will add vendor prefixes, to improve the compatibility of your CSS for different browsers.
- Linting: This will help to avoid errors in your CSS code and to enforce code conventions.
Sass
To be able to use all the fancy features of Sass, open the console, navigate to your project directory and type:
npm i -D node-sass
This will install the package node-sass, into the dev dependencies. It enables us to compile .scss files to CSS.
When the installation is done, open the package.json
and add the following script to your scripts:
"css:scss": "node-sass --output-style compressed -o dist src/scss"
This will compile your scss from /src/scss
into /dist
. Additionally the --output-style compressed
will remove all line-breaks and whitespaces to reduce the file size.
Now your package.json
should then look like this:
{
"name": "static-website",
"version": "1.0.0",
"description": "this is an example for a static website",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"css:scss": "node-sass --output-style compressed -o dist src/scss"
},
"author": "you",
"license": "ISC",
"devDependencies": {
"node-sass": "^4.13.1"
}
}
Now create the directory src
and inside of src scss
. Then you can create your first .scss
file. I will name it index.scss
If you now run npm run css:scss
in your console, it will compile your file into the dist
folder. Your project structure should then look like this:
Now you can include the compiled CSS file in the <head>
of your index.html
<link rel="stylesheet" type="text/css" href="dist/index.css">
To keep the scss organized, add a file _variables.scss
to your /src/sass
directory. The underscore at the beginning of the filename will make the file private and not being compiled. Now you can add some variables to this file like this:
$primary: #16a085;
To be able to use the variable $primary
, you need to include _variables.scss
in your index.scss:
@import 'variables.scss';
body {
color: $primary;
}
Now run npm run css:scss
again and refresh your browser to see the changes (We'll automate this step later).
I'd recommend to create new files (eg. _someModule.scss
) for every part you create, to keep the scss organized. But I won't go into detail about the organization of CSS, as it is a big topic itself.
Autoprefixer
For implementing the autoprefixer we will use the npm module autoprefixer, together with postcss-cli. So go ahead and type in your console:
npm i -D autoprefixer postcss-cli
Then let's add the autoprefixer script to the scripts of your package.json
"css:autoprefixer": "postcss -u autoprefixer -r dist/*.css"
This will check your CSS files in the dist
directory and add the prefixes for them. So you need to run npm run css:scss
first, to compile your scss into dist
and afterward run npm run css:autoprefixer
to add vendor-prefixes to your compiled CSS file. As we don't want to run the scripts one after another, let's add another script to the package.json
, which will run the scripts sequentially by connecting the scripts with&&
.
"build:css": "npm run css:scss && npm run css:autoprefixer"
By running npm run build:css
, you will now run firstly npm run css:scss
and afterward npm run css:autoprefixer
.
Linting
Finally, we'll use stylelint to make sure we have no errors in our CSS and to be able to enforce code conventions.
Therefore install the npm module stylelint:
npm i -D stylelint
Before we're able to add the scripts for linting, we have to add the file .stylelintrc
. This file will contain the rules, which stylelint
should apply. For more information about the rules, you can use check the documentation.
"rules": {
"block-no-empty": true,
"color-hex-case": "lower",
"color-hex-length": "short",
"color-no-invalid-hex": true,
"declaration-colon-space-after": "always",
"max-empty-lines": 2
}
Afterward we can add the script for linting and update our build:css
to start with linting, so we catch errors before the file is compiled.
"css:lint": "stylelint src/scss/*.scss --syntax scss || true",
"build:css": "npm run css:lint && npm run css:scss && npm run css:autoprefixer"
Now npm run build:css
should execute successfully (except if you have errors in your css). Next I will add some automation, as we don't want to run the script manually everytime we change something.
Simplifying The Build
First I'll add a script, which will watch the /src/scss
directory for changes and will run build:css
, whenever something changes. For that, I will use onchange.
npm i -D onchange
Now add the script to the package.json
:
"watch:css": "onchange \"src/scss\" -- npm run build:css"
If you now run npm run watch:css
it should automatically run your build:css
script whenever you change something in an scss file. Let's get rid of the manual browser refresh next. For this, we'll add browser-sync, to auto-refresh the browser. This will run a local server, which also enables us to test directly on other devices.
To install it run:
npm i -D browser-sync
Afterward, the corresponding script can be added to the package.json
"serve": "browser-sync start --server \"dist\" --files \"dist\""
As this will only watch the dist
directory, so we need to move our index.html
there. No worries, we'll have a better solution later, when we come to the HTML part :)
After moving the index.html
, we need to update the stylesheet link to href="/index.css"
. If you now run npm run serve
your website should automatically open in the browser and you should see something like this in your console:
This means you don't need to open the index.html
to preview your website, but can just visitlocalhost:3000
. This page will automatically refresh if something in your dist
directory changes. If you want to see your website on other devices, you can do that now by opening the external URL on your device.
And as a last step for the CSS, I will add a script to run the watch
script and the browser-sync together. Sadly npm doesn't have a native way to run scripts in parallel for all operating systems. (the &
operator only works on UNIX environments). Thus I'll install npm-run-all.
npm i -D npm-run-all
Afterward, we can add the script to the package.json
"watch": "run-p serve watch:css"
Now we're already in a good state for developing modern websites. By runningnpm run watch
we are watching for changes in our directorysrc/scss
and compile our scss
into thedist
directory. Also, we have autoprefixing and linting for our CSS in place. Additionally, the script is running a development server, which automatically refreshes our browser whenever something in dist
changes.
Next, we'll have a look at how to add images to our build process.
The Images
The only thing we'll do here is adding a script, which will minify the images. This will improve the page speed as the images have a reduced file size. To do this, go to your console again and install imagemin-cli.
npm i -D imagemin-cli
Afterward we can add the scripts for building and watching the directorysrc/images
.
"build:images": "imagemin src/images/**/* --out-dir=dist/images",
"watch:images": "onchange \"src/images\" -- npm run build:images"
If you now run npm run build:images
it will minify all images in your directory src/images
and store them in dist/images
. The watch script will look for changes in src/images
and will run the build script whenever something changes. Now we'll add a build script. This will run all the scripts, which start with build:
. Also let's update the watch
script to run both, the watch:css
and watch:images
script.
"watch": "run-p serve watch:*",
"build": "run-p build:*"
The *
operator makes the scripts run all other scripts starting with watch:
orbuild:
. If you have your npm run watch
script still running, you need to restart it.
Now we're ready to integrate JavaScript on our website.
The JavaScript
Webpack & Babel
For JavaScript, I want to be able to use modern syntax without having to worry about browser compatibility. Therefore I'll use webpack together with babel. So let's install the required npm modules:
npm i -D webpack webpack-cli babel-loader @babel/preset-env
Afterward we need to add a configuration file to make this work. So create a file webpack.config.js
in your project root with the following content:
module.exports = {
entry: './src/js/main.js',
output: {
path: __dirname + '/dist',
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
}
}
Next let's update the package.json
with the scripts for our JavaScript build.
"build:js": "webpack --mode=production",
"watch:js": "onchange \"src/js\" -- webpack --mode=development"
This will use the development mode when we're watching the files and the production mode when we run npm run build
. Now we can add our entry point for the js. This will be src/js/main.js
. I'd recommend keeping the JavaScript modular by adding the logic into other files and include them in the main.js
with the import syntax (eg. import './someModule'
)
Of course, we still need to include the bundled JavaScript at right before the </body>
of our index.html
.
<script src="./bundle.js"></script>
Linting
For JavaScript, it makes also sense to include a linter to keep the code more consistent and to avoid bugs. To introduce linting I'll use eslint.
npm i -D eslint eslint-loader
To enable the linting, we still need to create a configuration file and add the module to our webpack configuration. First, let's create the configuration file .eslintrc
in our project root. The following is the default configuration - you can, of course, change the rules according to your preferences.
If you want to read more about JavaScript linting, I'd recommend reading the blog post ESLint configuration and best practices
{
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
Afterwards we add a new rule to our webpack.config.js
to check eslint before running the babel-loader
.
module.exports = {
entry: './src/js/main.js',
output: {
path: __dirname + '/dist',
filename: 'bundle.js'
},
module: {
rules: [
{
enforce: 'pre',
test: /.js$/,
exclude: /node_modules/,
loader: 'eslint-loader'
},
{
test: /.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
}
}
Now our JavaScript is setup to use modern syntax and to check our code for errors and consistency. As a last thing we will add HTML processing to our project.
The HTML
For the HTML I'll introduce posthtml, together with posthtml-modules. This will enable us to use HTML partials. This makes most sense if you're building a website with multiple pages, to reduce code duplication. Also, we'll add htmlnano to minify the HTML and reduce the file size.
npm i -D posthtml posthtml-cli posthtml-modules htmlnano
Then create the file posthtml.json
in the project root, which will contain the configuration for the posthtml build.
{
"input": "src/views/*.html",
"output": "dist",
"plugins": {
"posthtml-modules": {
"root": "./src/views",
"initial": true
},
"htmlnano": {}
}
}
This will tell posthtml to render all HTML files from src/views
into dist. Now we only need to move the index.html
from dist
to src/views
and update the package.json
with the last scripts.
"build:html": "posthtml -c posthtml.json",
"watch:html": "onchange \"src/views\" -- npm run build:html"
Now we're able to use split our HTML into modules. For example I'll move the code from the head to a new file in src/views/components/head.html
.
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" href="/index.css">
Now we can add this module easily in every file by including
<module href="/components/head.html"></module>
So our final index.html
for this demo should look like this:
<!DOCTYPE html>
<html lang="de">
<head>
<title>Static Website Template</title>
<module href="/components/head.html"></module>
</head>
<body>
<h1>Static Website Template</h1>
<script src="./bundle.js"></script>
</body>
</html>
That's it. The final project structure will look like this:
The final repository can be found on GitHub.