Grunt opsætning

Her er et indlæg om, hvordan jeg har sat Grunt op til at kompilere min side, både mens jeg redigerer indlæg/layouts, og når jeg er klar til at offentliggøre mine ændringer.

Først lidt baggrund

Jeg skriver mine stylesheets i Sass, som derfor skal laves om til .css. Til dette benytter jeg Grunt.

Når jeg skriver stylesheets eller javascript, fordeler jeg det ofte ud i flere filer for overskuelighedens skyld, som kan ses nedenfor.

$ tree themes/morsby/assets/

  themes/morsby.hack/assets/
  ├── _scss
  │   ├── _includes.scss
  │   ├── _variables.scss
  │   ├── hackcss
  │   │   ├── dark-grey.css
  │   │   ├── dark.css
  │   │   ├── hack.css
  │   │   ├── solarized-dark.css
  │   │   └── standard.css
  │   ├── main.scss
  │   └── pygments
  │       ├── _colorful
  │       └── _monokai.css
  └── js
      ├── jquery.js
      └── main.js

  4 directories, 12 files

Det samme gør frameworks som fx Bootstrap eller MaterializeCSS (hvis man henter dem i Sass/less-format), som ofte kan indeholde mange filer, og ofte en fil dedikeret til variable (fx farver). Benytter man så Sass/less, kan man nøjes med at ændre en variabel ét sted i én fil, frem for at skulle overskrive det mange steder.

Da load time er gud, og flere filer hentes langsommere end én fil – og fordi større filer er længere tid om at blive hentet end mindre filer – slår jeg al javascript sammen til én .js-fil (+JQuery) og alle stylesheets sammen til én .css-fil (fraset evt. kode, der kun benyttes enkelte steder), og derefter fjerner jeg alle unødvendige tegn i filerne. Alt dette lyder bøvlet, men Grunt gør arbejdet for mig.

Og Grunt gør mere: Grunt laver en lokal webserver til mig, Grunt tjekker efter ændringer og opdaterer så den lokale server, og grunt tilføjer vendor prefixes til mine .css-filer. Alt sammen på ingen tid, og det hele sker af sig selv.

Nedenfor ses min opsætning af Grunt med en package.json, der definerer hvilke pakker, jeg skal bruge, og en Gruntfile.js, der så bestemmer, hvad der egentlig sker, når Grunt kører.

Jeg håber, kommentarerne er forklaring nok.

Package.json

{
  "name": "morsby.hugo",
  "version": "1.0.0",
  "description": "En personlig hjemmeside for en 24-årig medicinstuderende fra Aarhus.",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Morsby",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "https://bitbucket.org/Morsby/morsby.hugo.git"
  },
  "devDependencies": {
    "grunt": "^1.0.1",
    "grunt-autoprefixer": "^3.0.4",
    "grunt-contrib-connect": "^1.0.2",
    "grunt-contrib-copy": "^1.0.0",
    "grunt-contrib-uglify": "*",
    "grunt-contrib-watch": "^1.0.0",
    "grunt-sass": "^1.2.0",
    "jquery": "^3.1.0"
  },
  "dependencies": {
    "hack": "*"
  }
}

Gruntfile.js

module.exports = function(grunt) {

  grunt.initConfig({
    //  Read the package file!
    pkg: grunt.file.readJSON('package.json'),

    /*
      Set up the live server
      Package: grunt-contrib-connect
    */
    connect: {
      mysite: {
        options: {
          hostname: '127.0.0.1',
          port: 8080,
          protocol: 'http',
          base: 'build/dev',
          livereload: true
        }
      }
    },

    /*
      ==========================================================================
      STYLESHEETS
      ==========================================================================
      Copy the hack.css styles and jQuery JS from node_modules
      Package: grunt-contrib-copy
    */
    copy: {
      hackcss: {
          expand:true,
          flatten:true,
          src:"node_modules/hack/dist/*.css",
          dest:"themes/morsby.hack/assets/_scss/hackcss"
      },
      jquery: {
        expand:true,
        flatten:true,
        src:"node_modules/jquery/dist/jquery.js",
        dest:"themes/morsby.hack/assets/js/"
      }
    },

    /*
      Set up the .sass config; minified/compressed for dist version,
      uncompressed for dev. Creates a single .css from main.scss.
      main.scss (in this setup) includes all other stylesheets
      Package: grunt-sass
    */
    sass: {
      dist: {
        options:{
          outputStyle:'compressed'
        },
        files: {
          'themes/morsby.hack/static/css/main.css': 'themes/morsby.hack/assets/_scss/main.scss'
        }
      },
      dev: {
        options:{
          outputStyle:'uncompressed'
        },
        files: {
          'themes/morsby.hack/static/css/main.css': 'themes/morsby.hack/assets/_scss/main.scss'
        }
      }
    },

    /*
      Prefix vendor specifics on the just-generated .css file from the
      'sass'-part
      Package: grunt-autoprefixer
    */
    autoprefixer:{
      files:{
        'themes/morsby.hack/static/css/main.css':'themes/morsby.hack/static/css/main.css'
      }
    },


    /*
      ==========================================================================
      JAVASCRIPT
      ==========================================================================
      Minify/uglify the javascript -- takes the entire themes/.../assets/js dir
      and creates a minified version of each file.
      Package: grunt-contrib-uglify
    */
    uglify: {
      alljs: {
        files: [{
          expand: true,
          cwd:'themes/morsby.hack/assets/js/',
          src: '*.js',
          ext: '.min.js',
          dest: 'themes/morsby.hack/static/js'
        }]
      }
    },

    /*
      ==========================================================================
      WATCH
      ==========================================================================
      Watch for file changes, depending on the task -- and then runs the
      corresponding tasks
        - Uglify watches for .js changes
        - sass watches for sass changes
        - Hugo watches for changes to the Hugo config, content or theme/layout
      Package: grunt-contrib-watch
    */
    watch: {
      options: {
        atBegin: true,
        livereload: true
      },
      uglify: {
        files:['themes/**/*.js'],
        tasks:['uglify:alljs']
      },
      sass: {
        files: ['themes/**/*.scss'],
        tasks: ['sass:dev', 'autoprefixer:dev']
      },
      hugo: {
        files: ['config.toml', 'content/**', 'data/**', 'layouts/**', 'themes/morsby.hack/**', 'themes/**/*.html', '!themes/morsby.hack/*.css'],
        tasks: 'hugo:dev'
      },
    },
  });

    // Load all the required tasks.
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-connect');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-sass');
    grunt.loadNpmTasks('grunt-autoprefixer');

  /*
    ============================================================================
    Defining the tasks
    ============================================================================
    Create a dev task that runs the above commands except connect and
    watch with 'dist' options with 'dev' options.
    (see above)
  */
  grunt.registerTask('dev', [
    'copy:hackcss',
    'copy:jquery',
    'uglify:alljs',
    'sass:dev',
    'autoprefixer',
    'hugo:dev'
  ]);

  /*
    Create a standard task that runs the above commands except connect and
    watch with 'dist' options.
    Essentially the same as the 'dev' but for 'sass' and 'hugo'.
  */
  grunt.registerTask('default', [
    'copy:hackcss',
    'copy:jquery',
    'uglify:alljs',
    'sass:dist',
    'autoprefixer',
    'hugo:dist',
  ]);


  grunt.registerTask('edit', [
    'connect',
    'watch'
  ]);


  /*
    The following code registers a task that runs hugo to compile the site
    directory, placing the rendered result in either build/dev or build/dist
    (for development and deployment builds respectively). The target is chosen
    by referring to the task as hugo:dev or hugo:dist.
  */
  grunt.registerTask('hugo', function(target) {
    var args, done, e, hugo, i, len, ref, results;
    done = this.async();
    args = ['--source=.', "--destination=build/" + target];
    if (target === 'dev') {
      args.push('--baseUrl=http://localhost:8080/');
      args.push('--buildDrafts=true');
      args.push('--buildFuture=true');
    }
    hugo = require('child_process').spawn('hugo', args, {
      stdio: 'inherit'
    });
    ref = ['exit', 'error'];
    results = [];
    for (i = 0, len = ref.length; i < len; i++) {
      e = ref[i];
      results.push(hugo.on(e, function() {
        return done(true);
      }));
    }
    return results;
  });
};

Meget inspiration er hentet fra denne side, særligt den del af Gruntfile.js, der definerer Hugo-serveren.

Velkommen Hugo!

Jeg byder hermed hjerteligt velkommen til Hugo – en static site generator, der er kodet i Go. (Og til dig.)

Så i et forsøg på at lære Hugo at kende, har jeg lavet en ny hjemmeside til mig selv – drevet af Hugo.

At jeg forsøger at bevæge mig ind i verdenen af static site generators har to (tre) umiddelbare forklaringer:

  1. CMS‘er som WordPress (og det i mine øjne noget sjovere OctoberCMS) er forholdsvis langsomme, og det er svært at arbejde på siderne uden internetforbindelse, da de er kraftigt afhængige af databaser.1
  2. Et af de projekter, jeg arbejder på er en notesamling af over 400 lægemidler. I sin oprindelige form var de fanget i en database, hvilket gjorde redigering og opdatering meget besværligt (og igen krævede netforbindelse). Jeg har her hen over sommeren 2016 omprogrammeret siden, så den nu også bruger Hugo. Det tillader at hvert stof nu bare ligger som sin egen markdown-fil, og listen over stoffer genereres automatisk ud fra filerne i mappen. Det har gjort det betydeligt nemmere at holde siden opdateret. Og når nu det hele er HTML5 + JavaScript, er der også mulighed for uden alt for meget besvær at lave en app ud af det.
  3. Det er sjovt at nørde.

Som man nok kan gætte ud fra punkt 2, er der forholdsvis meget data, der skal arbejdes igennem i min notesamling (farma.morsby.dk). Jeg startede med at prøve Jekyll, men her det tog over 20 sekunder at generere siden. Som så ofte før når jeg prøver nye kodesprog, frameworks og lignende, er det ret meget trial and error til at starte med, og så var det altså jævnt irriterende at skulle vente 20 sekunder på at se, hvordan en ændring i koden påvirkede siden. Altså: Jekyll var for langsom. Hugo genererer siden på under 1.5 sekund (men med diverse Grunt-opgaver ender det på ca. 2.5 sekund), hvilket er noget nemmere at sluge.

Hugo er ekstremt hurtig sammenlignet med andre static site generators, hvilket gjorde den til et fornuftigt valg. Dette på trods af at den nuværende version er v. 0.16, og der altså er et pænt stykke vej til en stabil udgave. Og dokumentationen (og diverse eksempler på nettet) er ret forvirrende af samme grund – der er konstant ændringer, så man må nogle gange gå på opdagelse for at finde ud af, hvorfor i alverden ens kode ikke virker.

Jeg vil her på siden forsøge at beskrive, hvordan jeg har sat Hugo op og forbundet den med Grunt-opgaver for at muliggøre .scss-kompilering, .css-minification og nemt at skifte mellem lokal udvikling og at generere siden til live, online brug. Men det bliver først senere!


  1. OctoberCMS er godt nok “fil-baseret”, hvilket gør det muligt at arbejde på statiske sider offline, men fx en blog kræver stadig en forbindelse til databasen.