PostCSS Static Site Starter

PostCSS Logo

TL;DR

Set up a simple and basic 'static site generator' with PostCSS plugins that covers creating your own dev workflow that recreates SASS pre-processor features, importing files, linting and autoprefixer for development. Publish the site with build steps for removing unused css, minifying and then inlining critical css for above the fold to boost page load performance. Plus some other cool postcss plugins.

Check out the static-site-starter which combines the dev and build processes from the previous post on setting up a PostHTML workflow with the PostCSS setup that will follow.

What is PostCSS?

PostCSS is not a CSS pre or post-processor, it is a javascript toolset that does whatever you can think of and program. You can replicate many of the features of SASS and really so much more.

PostCSS takes in CSS, turns it into an Abstract Syntax Tree (AST) which parses each rule and provides access to each property and value allowing you to analyze and perform changes to them. This is how Autoprefixer works. It reads each property and provides cross-browser vendor prefixes based on Can I Use. You can write your core css styles and not have to worry about covering every possible browser quirk. It's makes development life better.

Motivation

There are lot of good articles showing how to use PostCSS plugins to recreate the features of SASS with sample code of the before syntax and final ouput. There wasn't the corresponding javascript examples and connecting glue code that is required to make it work in a real life project.

I wanted to demonstrate starting a 'simple' project from scratch, setting up the project structure, configuring the package.json with dev and build workflows so that you can see how it comes together. This article will provide the steps to go from nothing to a working minimal static site workflow.

You can follow the instructions below or clone the repo.

Setup

Just like the last post, we'll be using NPM to run our dev and build processes. I've created a bash script that will setup our project structure and install all NPM packages. Create a new folder, cd into it and create a bash script called setup.sh.

setup.sh
        
          mkdir postcss-starter && cd postcss-starter && touch setup.sh
        

Copy and paste the code below. Run bash setup.sh in your console.

setup.sh
      
        mkdir -p {css,html,js,img,fonts}
        cd css && mkdir {styles,scripts}
        cd scripts && touch imports.js critical.js postcss.config.js && cd ..
        cd styles && mkdir {base,config,objects,globals,components,utilities,pages}
        touch imports.css
        cd base && touch defaults.css normalize.css typography.css && cd ..
        cd components && touch component.css && cd ..
        cd config && touch mixins.css functions.css variables.css && cd ..
        cd globals && touch global.css && cd ..
        cd objects && touch object.css && cd ..
        cd pages && touch page.css && cd ..
        cd utilities && touch utility.css && cd ..
        cd ../..
        cd js && touch scripts.js && echo "console.log('scripts.js is working');" > scripts.js && cd ..
        cd html && mkdir -p {pages,templates}
        cd templates && mkdir -p {views,components}
        cd views && touch index.html
        cd ../../..
        touch README.md
        npm init -y
        npm i -D postcss postcss-cli postcss-load-config postcss-import postcss-preset-env purgecss critical stylelint stylelint-config-rational-order stylelint-config-standard cssnano autoprefixer browser-sync npm-run-all onchange
        git init
        touch .gitignore
        echo "node_modules/" >> .gitignore
        echo "**/.DS_Store" >> .gitignore
      

postcss-import and autoprefixer require postcss version 7.0.34 to run. PostCSS is currently being updated to v8 and these plug-ins have not been migrated to use it just yet. Copy the <package.json from the repo and run npm i to install the older but working dependencies if you have problems.

The -D flag will install these as developer dependencies which are packages we use for building the site instead of the site using these tools for functionality.

For our development workflow, we'll need browser-sync, npm-run-all and onchange. I covered these tools in a previous post.

The CSS structure includes a scripts folder which contains build files that PostCSS will need. They are support files for the dev/build workflow and don't belong in /js.

  • css
    • styles.css
    • scripts
      • imports.js
      • critical.js
    • styles
      • base
        • defaults.css
        • normalize.css
        • typography.css
      • components
        • components.css
      • config
        • functions.css
        • mixins.css
        • variables.css
      • globals
        • global.css
      • objects
        • object.css
      • pages
        • pages.css
      • utilities
        • utility.css
      • imports.css

postcss-load-config allows several configurations including setting up a postcss block in our package.json. Typically, you use a postcss.config.js file to load plug-ins, but we can use the package.json to configure our PostCSS setup. Add this block and whenever we run the PostCSS command it will apply the plug-ins in the order listed.

package.json
      
        "postcss": {
          "plugins": {
            "postcss-nested": {},
            "postcss-mixins": {},
            "postcss-preset-env": {},
            "stylelint": {
              "fix": "true"
            },
            "autoprefixer": {}
          }
        }
      

Atom Editor

You'll notice there are no .scss files, just plain ol' .css. When you start writing PostCSS in Atom editor, we'll need language-postcss to will provide syntax highlighting for nesting, mixins and so on in our .css files. Super helpful.

Imports

A slighty tricky first step is importing our css files. Without SASS imports we will use postcss-import which requires us to write some javascript code to process the imports but it is very straightforward.

css/styles/imports.css
      
        @import 'config/functions.css';
        @import 'config/mixins.css';
        @import 'config/variables.css';

        @import 'base/normalize.css';
        @import 'base/typography.css';
        @import 'base/defaults.css';

        @import 'components/component.css';
        @import 'globals/global.css';
        @import 'pages/page.css';
        @import 'objects/object.css';
        @import 'utilities/utility.css';
      

Let's create our imports file and add the following code:

css/scripts/imports.js
      
        var fs = require("fs");
        var postcss = require("postcss");
        var atImport = require("postcss-import");

        var css = fs.readFileSync("css/styles/imports.css", "utf8");

        postcss()
          .use(atImport())
          .process(css, {
            from: "css/styles/imports.css",
            to: "css/styles.compiled.css"
          })
          .then(function (result) {
            fs.writeFileSync("css/styles.compiled.css", result.css);
          })
      

This is taken from the packages's github page and simply writes a new css file, concatenating all the imports into css/styles.compiled.css. We'll use this file to pre-process in the next step. To run this, add the below code to the package.json:

package.json
      
        "styles:import": "node css/scripts/imports.js"
      

SASS-Like Pre-Processor

We can duplicate the powers of SASS with precss, a package that allows for nesting, variables, mixins and more. This package has not been updated for about 2 years and the issues board has many open tickets that have not been resolved. And it doesn't actually support mixins, so we won't be using precss and will install the individual packages that we need.

Let's add our plugins to the config file:

The css/styles.compiled.css will now be run through the plugins in the package.json and output to css/styles.css.

package.json
      
        "styles:process": "postcss css/styles.compiled.css -o css/styles.css",
      

Nesting

It's a best practice to avoid nesting more than 3 levels. This article details some of the pros and cons.

Do note that css/styles.compiled.css will display the nesting because it has not been processed by the plugins.

css/styles/pages/page.css
      
        .page {
          display: flex;
          background-color: salmon;
          color: pink;

          h1 {
            color: red;
          }
        }
      

Turns into plain CSS:

css/styles.css
      
        .page {
          display: flex;
          background-color: salmon;
          color: pink;
        }

        .page h1 {
          color: red;
        }
      

Mixins

postcss-mixins is actively maintained and gives us SASS-like mixins. You simply define the mixins in css/styles/config/mixins.css file and they can be used in other css files.

Just make the mixins.css is imported before other files that use them!

You can use @mixin-content to pass in rules that can be wrapped by a media query, just like SASS.

css/styles/config/mixins.css

      
        @define-mixin transition $property: all, $time: 150ms, $easing: ease-out {
          transition: $(property) $(time) $(easing);
        }

        @define-mixin viewport-min {
          @media (min-width: 40rem) {
            @mixin-content;
          }
        }
      

Use the mixins

css/styles/pages/page.css
      
        body {
          @mixin transition color, 2s, ease-in;
        }

        header {
          @mixin viewport-min {
            background-color: red;
          }
        }
      

Now becomes:

css/styles.css
      
        body {
          transition: color 2s ease-in;
        }

        header {
          @media (min-width: 40rem) {
            background-color: red;
          }
        }
      

Future CSS

postcss-preset-env contains a bunch of features for using modern css into current code that browsers can use. Here is a list of what it supports.It's great for using custom properties which is something that SASS has problems with. You could cut out this plug-in and simply use postcss-custom-properties.

css/styles/globals/global.css
      
        :root {
          --font-size: 2.45rem;
        }
      
css/styles/globals/global.css
      
        .page {
          display: flex;
          background-color: salmon;
          color: pink;
          font-size: var(--font-size);
        }
      

Now becomes:

css/styles.css
      
        .page {
          display: flex;
          color: pink;
          background-color: salmon;
          font-size: var(--font-size);
        }
      

Stylelinting

stylelint will lint our CSS and make fixes to our new output CSS. I'll be adding stylelint-config-standard and stylelint-config-rational-order.

stylelint-config-rational-order will group our properties by:

  1. Positioning
  2. Box Model
  3. Typography
  4. Visual
  5. Animation
  6. Misc

You could also use the Atom package postcss-sorting to resort on save. I already write my styles according to the above template, but this could help those who don't know where properties belong.

You'll need to add a stylelint block to package.json to make it work.

package.json
      
        "stylelint": {
          "extends": [
            "stylelint-config-standard",
            "stylelint-config-rational-order"
          ]
        },
      

We pass the fix: true flag to the stylelint plugin and it will automatically make fixes to our styles, things like indentation and spaces between lines.

pacakge.json
      
        "stylelint": {
          "fix": "true"
        }
      

Autoprefixer

And the most important plugin of all, Autoprefixer will add vendor prefixes to your CSS to make it compatible across browsers. This allows us the ability to write our core CSS without having to lookup all the quirks and arcane patches for certain browsers.

This is added at the end of the plugin list. You will need to use a Browserlist configuration to get it working. Read the documentation to make the best selection of browswer support, I will be going with one of the recommended defaults.

Add this block:

package.json
      
        "browserslist": [
          "last 2 versions"
        ],
      

Now our CSS will contain prefixes for flexbox and other properties:

css/styles.css
      
        .page {
          display: -webkit-box;
          display: -ms-flexbox;
          display: flex;
          background-color: salmon;
          color: pink;
        }
      

Build Process

We have a good development workflow going and now it's time to publish our site. There are many modifications and performance tweaks we can apply.

  1. Remove unused CSS
  2. Minify CSS
  3. Inline above the fold critical CSS

Let's start with removing old builds and creating new /dist for each build run. It's also a good idea to re-run our CSS through the postcss commands to make sure they have compiled correctly.

package.json
      
        "build:clean": "rm -rf dist",
        "build:dist": "mkdir -p dist/{css,js,img,fonts/web}",
        "build:styles-compile": "run-s styles:import styles:process",
      

Remove Unused Styles

PurgeCSS will remove unused CSS to keep our final file as small as possible.

The --content flag will go through all the html pages we have to create the new CSS.

package.json
      
        "build:styles-unused": "purgecss -css css/styles.css --content html/pages/*.html --output css/styles.clean.css",
      

Minify

CSSnano will minify our css, removing whitespace which saves space and cut's file size down. This will be saved to /dist/css/styles.css.

package.json
      
        "build:styles-minify": "postcss css/styles.clean.css > dist/css/styles.css --use cssnano"
      

Inline Critical Styles

Our CSS is almost ready. We will use critical to inline above the fold styles directly into the head of our index.html. This will improve our page speed when users visit our site. You could apply this to other pages too if need be.

There are many examples on the github repo. We'll use this code to run critical.

css/scripts/critical.js
      
        var critical = require('critical');

        critical.generate({
            inline: true,
            base: 'dist',
            src: 'index.html',
            target: {
              html: 'index.html'
            },
            minify: true,
            dimensions: [
              {
                width: 320,
                height: 480
              },
              {
                width: 2000,
                height: 1000
              }
            ]
        });
      
package.json
      
        "build:styles-critical": "node css/scripts/critical.js"
      

Take a look at the inlined styles in the style tag. Very cool!

Critical also applies this strange syntax to our styles.css link tag.

dist/index.html
      
        <link rel="stylesheet" href="css/styles.css" media="print" onload="this.media='all'">
      

This will asynchronsouly load the CSS increasing page render performance.

All Done

Overall, the benefits of having a streamlined development workflow makes building things easier and faster.