The redesign of Meritocracy

The redesign of Meritocracy

In the last month, we sent online a new version of Meritocracy called v3.5. There are multiple reasons why we did this, among which:

  1. Old design
  2. Very bad speed performance
  3. Not mobile optimized
  4. Poor UX
  5. Terrible file organization

 

Now and before Now and before

Files tree

After our designers sent us the new templates and a lot of meetings, we started over trashing old files and creating new files:

  • [DIR] assets
    • [DIR] js
      • [DIR] pages – They will be minified but not merged to others. The name of files will be jobs.js, homepage.js and so on
      • [DIR] vendor – javascript files of vendors (like bootstrap, jQuery etc.)
      • [FILES] *.js – All javascript files
    • [DIR] less
      • [DIR] vendor – LESS files of vendors
      • [FILES] *.less – LESS files, the names will be jobs.less, homepage.less
    • [DIR] vendor-css (all non-LESS files from various plugins)

Inside the js directory there are four important files:

  • config.js various configurations
  • app.js main app file
  • formValidator.js our custom form validator class written in ES6 (and then babelized)
  • utils.js various functions

Inside the less directory the important ones are three:

  • app.less main app file
  • helpers.less all helpers, like classes for margins (margin-top-20), paddings (padding-left-10), text styles (bold, no-bold)
  • variables.less variables like colors, media queries.

Gulp

The tasks of our Gulp usage are multiple:

  • less/*.less files will be merged and minified in a app.min.css file in a public folder
  • vendor-css/* files in vendor.min.css
  • js/*.js files in app.min.js
  • js/pages/* will be minified only with the same name inside the public dir (jobs.js will be jobs.js but minified)
  • js/vendor/* minified and merged in vendor.min.js.

Our gulpfile.js is like this:

// Load plugins
var gulp = require('gulp');
var babel = require('gulp-babel');
var $ = require('gulp-load-plugins')();
var LessCleanCSS = require('less-plugin-clean-css');
var cleanCSS = new LessCleanCSS();

var paths = {
    js: {
        vendor: {
            source: './resources/assets/js/vendor/**/*.js',
            partial: './resources/assets/js/vendor/',
            filename: 'vendor.min.js'
        },
        pages: {
            source: './resources/assets/js/pages/*.js',
            dest: './public/assets/js/pages/'
        },
        source: './resources/assets/js/*.js',
        partial: './resources/assets/js',
        dest: './public/assets/js/',
        filename: 'app.min.js'
    },
    less: {
        source: './resources/assets/less/**/*.less',
        dest: './public/assets/css/',
        filename: 'app.min.css'
    },
    vendorCss: {
        source: './resources/assets/vendor-css/**/*.css',
        dest: './public/assets/css/',
        filename: 'vendor.min.css'
    }
};

gulp.task('default', ['css:vendor', 'less', 'js', 'js:vendor', 'js:pages', 'watch']);

// CSS vendor
gulp.task('css:vendor', function() {
    return gulp.src(paths.vendorCss.source)
        .pipe($.concat(paths.vendorCss.filename))
        .pipe($.uglifycss({
            uglyComments: true
        }))
        .pipe(gulp.dest(paths.vendorCss.dest))
});

// Less
gulp.task('less', function() {
    return gulp.src(paths.less.source)
        .pipe($.concat(paths.less.filename))
        .pipe($.less({
            plugins: [cleanCSS]
        }))
        .pipe(gulp.dest(paths.less.dest))
});

// Vendor javascripts
gulp.task('js:vendor', function() {
    return gulp.src([
        paths.js.vendor.partial + 'jquery-1.11.1.min.js',
        paths.js.vendor.partial + 'jquery-ui.min.js',
        paths.js.vendor.partial + 'bootstrap.min.js',
        paths.js.vendor.source
    ])
        .pipe($.concat(paths.js.vendor.filename))
        .pipe($.uglify())
        .pipe(gulp.dest(paths.js.dest));
});

// Custom javascripts
gulp.task('js', function() {
    return gulp.src([
        paths.js.partial + '/config.js',
        paths.js.partial + '/utils.js',
        paths.js.partial + '/formValidator.js',
        paths.js.source
    ])
        .pipe(babel({
            presets: ['es2015']
        }))
        .pipe($.concat(paths.js.filename))
        .pipe($.uglify())
        .pipe(gulp.dest(paths.js.dest));
});

gulp.task('js:pages', function() {
    return gulp.src(paths.js.pages.source)
        .pipe(babel({
            presets: ['es2015']
        }))
        .pipe($.uglify())
        .pipe(gulp.dest(paths.js.pages.dest));
});

// Watch
gulp.task('watch', ['css:vendor', 'less', 'js', 'js:vendor', 'js:pages'], function() {
    gulp.watch(paths.vendorCss.source, ['css:vendor']);
    gulp.watch(paths.less.source, ['less']);
    gulp.watch(paths.js.source, ['js']);
    gulp.watch(paths.js.vendor.source, ['js:vendor']);
    gulp.watch(paths.js.pages.source, ['js:pages']);
});

And you can call it simple with gulp command and it will activate the watch function to auto rebuild files.

Speed & SEO

The key factor are two, and they are linked to each other: speed and SEO (which is optimized with the speed of your site).

Following Varvy.com pagespeed optimization we enabled Gzip and keep-alive in our NGINX servers, but the harder points were the defer loading of images and javascripts.

After reading Varvy tips and examples, we achieved these goals creating a file called init.js in our assets/js/pages dir, so it will keep its name when minified.

It’s content is really simple, but working pretty good.

We splitted it in two main functions: one to defer load JS files and one for images (and background images).

window.onload = function() {
    initDeferImg();
    initDeferJs(jsd);
};

function initDeferJs(list) {
    for (var js in list) {
        if (list.hasOwnProperty(js)) {
            var children = list[js];
            var element = document.createElement('script');
            element.src = (isNaN(js) ? js : children);
            document.body.appendChild(element);

            // Append children when loaded
            element.onload = element.onreadystatechange = function () {
                if ((!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete') && (isNaN(js) && !(children instanceof String))) {
                    initDeferJs(children);
                }
            }
        }
    }
}

function initDeferImg() {
    var imgDefer = document.getElementsByTagName('img');
    for (var i = 0; i < imgDefer.length; i++) {
        if (imgDefer[i].getAttribute('data-src')) {
            imgDefer[i].setAttribute('src', imgDefer[i].getAttribute('data-src'));
        }
    }

    var bgImgDefer = document.querySelectorAll('[data-bg-image]');
    for (var i = 0; i < bgImgDefer.length; i++) {
        if (bgImgDefer[i].getAttribute('data-bg-image')) {
            bgImgDefer[i].style.backgroundImage = 'url(' + bgImgDefer[i].getAttribute('data-bg-image') + ')';
            bgImgDefer[i].removeAttribute('data-bg-image');
        }
    }
}

In initDeferJs function we loop over each items passed (we see this later) and we make an important check: if it’s a file or an array of files.

Why this? Because we want to load some files after others, for example we want to load jobs.js after app.js and with this check we can achieve it.

For initDeferImg we don’t have to check this, so it simply fetchs all img, grabs their data-src attribute and set it as src. The same for background images, it grabs data-bg-image attribute and set it as background-image.

Finally, before the </body> we can call this only script, where app.min.js is loaded after vendor.min.js.

<script defer>
var jsd = {'vendor.min.js': ['app.min.js']};
var e = document.createElement('script');
e.src = 'init.js';
document.body.appendChild(e);
</script>

Leave a Reply

Your email address will not be published. Required fields are marked *