How to build a static website without frameworks using npm scripts
Tags: npm, webdev, tutorial, static
Published: 2020-02-07 08:15:36

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](https://github.com/Vincenius/static-website-template)

*   [Initial Setup](#initialSetup)
*   [The CSS](#css)
    *   [Sass](#sass)
    *   [Autoprefixer](#autoprefixer)
    *   [Linting](#csslinting)
*   [Simplifying The Build](#simplifybuild)
*   [The Images](#images)
*   [The JavaScript](#javascript)
    *   [Webpack & Babel](#webpackbabel)
    *   [Linting](#jslinting)
*   [The HTML](#html)

## 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](https://sass-lang.com/):** 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](https://autoprefixer.github.io/):** This will add [vendor prefixes](https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix), to improve the compatibility of your CSS for different browsers.
*   **[Linting](https://stylelint.io/):** 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](https://www.npmjs.com/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:

![structure of the project](https://wweb.dev/blog/static-website/dirStructure1.png)

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](https://www.npmjs.com/package/autoprefixer), together with [postcss-cli](https://www.npmjs.com/package/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](https://www.npmjs.com/package/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](https://stylelint.io/user-guide/rules).

    "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](https://www.npmjs.com/package/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](https://www.npmjs.com/package/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:

![browser sync console output](https://wweb.dev/blog/static-website/browser-sync.png)

This means you don't need to open the `index.html` to preview your website, but can just visit`localhost: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](https://www.npmjs.com/package/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 running`npm run watch` we are watching for changes in our directory`src/scss` and compile our `scss` into the`dist` 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](https://www.npmjs.com/package/imagemin-cli).

`npm i -D imagemin-cli`  

Afterward we can add the scripts for building and watching the directory`src/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:` or`build:`. 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](https://www.npmjs.com/package/webpack-cli) together with [babel](https://babeljs.io). 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](https://www.npmjs.com/package/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](https://blog.geographer.fr/eslint-guide?utm_source=wweb.dev)

    {
        "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](https://www.npmjs.com/package/posthtml), together with [posthtml-modules](https://www.npmjs.com/package/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](https://www.npmjs.com/package/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:

![final structure of the project](https://wweb.dev/blog/static-website/finalstructure.png)

The final repository can be found on [GitHub](https://github.com/wwebdev/static-website-template).