Gulp workflow for Perch

What is Gulp?

Gulp is a tool for automating tasks in your development workflow such as using CSS preprocessors and optimising assets for production.

New to Gulp?

If you are new to Gulp, it would be helpful to learn the basics first. Zell Liew’s Gulp for Beginners article is a great introduction and it’s very easy to follow.

What we are doing:

Notes:

* While we can use gulp-inject plugin to insert the appropriate tags for our assets, we will handle this in a neater and more maintainable way with Pipit Pinion, a Perch Feather that helps you manage front-end assets such as CSS and Javascript through Perch.

We won’t do anything significant to our JS workflow other than minification for production. You can build upon this part of the workflow if you need to do more such as concatenation or using CoffeeScript.

** We’ll rely on Sass partials for concatenating our CSS instead of using a Gulp plugin. So if you want to write your Sass in multiple files and combines them later, you can take advantage of Sass imports.

Let’s say your main file is styles.scss. You can create partials _footer.scss and _blog.scss and include them in your styles.scss with:

body {
    background-color: tomato;
}

/* more styles */

@import 'blog';
@import 'footer';

The above combines your Sass files before compiling and outputs a single CSS file styles.css. If you are new to Sass, Robert Cooper has written a good introduction.

Inserting Assets:

We will be using Perch Layouts and the Pinion Feather to manage what (and when) assets are added to our documents.

Using Perch Layouts

Layouts are a common way of including repeated sections in Perch websites. For instance, we can use Layouts to include the <head> on all pages with a simple perch_layout() call and manage what goes in the <head> from one file.

We will be using Layouts to insert CSS assets in the <head> and Javascript assets just before the closing tag </body>. You can change this easily if you require a different setup.

We will be using 2 layouts: global/top.php and global/bottom.php. These go into your perch/templates/layouts folder.

global/top.php:

<!doctype html>
<html>
<head>
<!-- We will insert assets here -->
</head>
<body>

global/bottom.php:

<!-- we will insert assets here -->
</body>
</html>

In our pages page.php we add the global/top.php layout at the top of the document (after the Perch runtime include):

<?php perch_layout('global/top'); ?>

and add the global/bottom.php layout at the bottom of the document:

<?php perch_layout('global/bottom'); ?>

Using Pipit Pinion

As mentioned above, we will use a Perch Feather called Pinion to manage our assets. You can download Pinion from here. Installation instructions are in the documentation.

Here’s the file structure we will be using for our assets:

website/
  ├ perch/
  ├ src/
    ├ scss/
    ├ js/
    └ vendor/
  ├ tmp/
  └ assets/

src is where our development assets are. Uncompiled Sass, unprefixed CSS, unminified JS, etc. You may also have some assets that you only use for development in there. This is where we work.

tmp is where our compiled assets will be generated during development. These are the assets we serve during development. They may not be fully optimised for production. If you’re using Git, you may choose to gitignore this entire folder.

assets is where our production-ready assets will be. They will also be generated later on.

So let’s configure Pinion to use this structure:

Open the config.php file placed in perch/addons/feathers/pipit_pinion. Here we can define what folder will have our production-ready assets, and what folder we want to use during development.

The PIPIT_PINION_ASSETS_DIR is the folder we want to use by default for our assets in production. In this example we are using the assets folder:

define('PIPIT_PINION_ASSETS_DIR', 'assets');

The PIPIT_PINION_ASSETS_DEV_DIR is the folder we want to use by default for our assets in development. In this example we are using the tmp folder:

define('PIPIT_PINION_ASSETS_DEV_DIR', 'tmp');

Pipit Pinion has 2 functions you can use: perch_get_css() and perch_get_javascript(). The functions have flexible options which you can learn more about in the documentation.

By default, perch_get_css() and perch_get_javascript() inserts all CSS files and Javascript files from your PIPIT_PINION_ASSETS_DIR/css and PIPIT_PINION_ASSETS_DIR/js respectively into your document.

When you set PERCH_PRODUCTION_MODE to PERCH_DEVELOPMENT in your perch/config/config.php, the functions inserts all CSS files and Javascript files from your PIPIT_PINION_ASSETS_DEV_DIR/css and PIPIT_PINION_ASSETS_DEV_DIR/js into your document.

So in our case, we get the files in assets/css and assets/js by default. And when PERCH_PRODUCTION_MODE is set to PERCH_DEVELOPMENT, we get the files in tmp/css and tmp/js.

These are the defaults, and Pinion is flexible enough to allow us specify other folders or specific files if needed.

So let’s update our layouts:

global/top.php:

<!doctype html>
<html lang="en">
<head>
<?php 
    perch_get_css();

    // If your 'src/vendor' includes some CSS files
    perch_get_css(['dir' => 'src/vendor']);
?>
</head>
<body>

global/bottom.php:

<?php 
    // If your 'src/vendor' includes some JS files
    perch_get_javascript(['dir' => 'src/vendor']);

    perch_get_javascript(); 
?>
</body>
</html>

Gulp

Create a project by running the npm init command in your website root. Install Gulp into the project:

$ npm install gulp --save-dev

File Structure:

Here’s the file structure we want:

website/
  ├ perch/
  ├ src/
    ├ scss/
    ├ js/
    └ vendor/
  ├ tmp/
  ├ assets/
  ├ gulp/
  ├ gulpfile.js
  ├ package.json
  └ node_modules/

gulp is where we’ll place our gulp plugins. gulpfile.js stores our Gulp configuration.

node_modules is, surprise surprise, where the node modules are. This is automatically created when you install plugins.

package.json is an automatically created file that contains details about the project such as dependencies.

gulpfile.js

Before we start creating our tasks, install the following plugins:

Create your gulpfile.js and add the following

var gulp = require('gulp');
var plugins = require('gulp-load-plugins')({pattern: '*'});
var runSequence = require('run-sequence');
var del = plugins.del;
var browserSync = require('browser-sync').create();

function getTask(task) {
    return require('./gulp/' + task)(gulp, plugins);
}

The getTask() function gets the tasks we write in other files.

Browser Sync:

We are using Browser Sync which not only keeps multiple browsers in sync, but also any pages open on different devices. So when you are developing on your computer you can, for instance, have phones and tablets mounted next to your screen and Browser Sync will sync your scrolls and page reloads on all of them.

So if you have separate config files for different environments (development and production) like this:

switch($_SERVER['SERVER_NAME']) {
    case 'localhost':
        include(__DIR__.'/config.localhost.php');
        break;

    default:
        include('config.production.php');
        break;
}

You may need to add another case statement with your local IP address so you’re using the correct config file.

switch($_SERVER['SERVER_NAME']) {
    case 'localhost':
        include(__DIR__.'/config.localhost.php');
        break;

    case '192.168.10.136':
        include(__DIR__.'/config.localhost.php');
        break;

    default:
        include('config.production.php');
        break;
}

Instead of hardcoding the IP address you can also use getHostByName(getHostName()). More on this in the Multiple Server Config post.

Browser Sync requires you to add a script before the </body> closing tag. It tells you so when you run the task:

[Browsersync] Copy the following snippet into your website, just before the closing </body> tag
<script id="__bs_script__">//<![CDATA[
    document.write("<script async src='http://HOST:3000/browser-sync/browser-sync-client.js?v=2.18.13'><\/script>".replace("HOST", location.hostname));
//]]></script>

To only include in development mode:

<?php if(PERCH_PRODUCTION_MODE === 'PERCH_DEVELOPMENT'): ?>
    <script id="__bs_script__">//<![CDATA[
    document.write("<script async src='http://HOST:3000/browser-sync/browser-sync-client.js?v=2.18.13'><\/script>".replace("HOST", location.hostname));
    //]]></script>
<?php endif; ?>

Learn more about Perch Production Modes.

Now we can start writing our Gulp tasks.

Sass and CSS autoprefixing

gulp/sass.js handles Sass compilation and CSS autoprefixing. Required installs:

module.exports = function (gulp, plugins, config) {
    return function () {
        var destination = 'tmp/css';

        var stream = gulp.src('src/scss/**/*.scss')
            .pipe(plugins.sass())
            .pipe(plugins.autoprefixer({
                    browsers: ['last 6 versions'],
                    cascade: false
                }))
            .pipe(gulp.dest(destination));

        return stream;
    };
};

Javascript

This workflow doesn’t use any Javascript preprocessing. In this example, we’ll just copy our files to tmp/js as is, but we will only do so if the files has changed.

We’ll handle this in gulp/scripts.js. Required installs:

module.exports = function (gulp, plugins, config) {
    return function () {
        var destination = 'tmp/js';

        var stream = gulp.src('src/js/**/*.js')
            .pipe(plugins.newer(destination))
            .pipe(gulp.dest(destination));

        return stream;
    };
};

Back to gulpfile.js

Now we can go back to our gulpfile.js and set up our development build. Add the following to your file:

gulp.task('browserSync', function() {
  browserSync.init()
})

// deletes the contents of the 'tmp' folder
gulp.task('clean:tmp', function() {
  return del('tmp');
})

// get tasks from the 'gulp' folder
gulp.task('sass', getTask('sass'));
gulp.task('scripts', getTask('scripts'));

// use Gulp's watch to check whether certain files are saved
gulp.task('watch', function (){
    //if .scss file or .js file is saved, run the sass and script tasks respectively
    gulp.watch('src/scss/**/*.scss', ['sass']);
    gulp.watch('src/js/**/*.js', ['scripts']);

    // reload browser if perch template, master page or layout is saved
    gulp.watch('perch/templates/**/*.html', browserSync.reload);
    gulp.watch('perch/templates/**/*.php', browserSync.reload);

    // reload browser if any asset compiled and/or saved
    gulp.watch('tmp/**/*.*', browserSync.reload);
})

// This sequence of tasks is our development build
gulp.task('default', function (callback) {
  runSequence('clean:tmp',
        ['browserSync', 'sass', 'scripts'], 'watch', 
        callback)
})

Now to run our dev build, we run the default task. The default task can be run from the command line with:

$ gulp

If we change the name of the task to something else gulp.task('dev', ...), we run it with the command:

$ gulp dev

Remember to define your production mode in your perch perch/config/config.php (or config.environment.php) so Pipit Pinion gets the correct files.

define('PERCH_PRODUCTION_MODE', 'PERCH_DEVELOPMENT');

Minifying CSS

We’ll create gulp/minify-css.js to minify our CSS files. Required installs:

module.exports = function (gulp, plugins, config) {
    return function () {
        var destination = 'assets/css';

        var stream = gulp.src('tmp/css/**/*.css')
            .pipe(plugins.cssnano())
            .pipe(gulp.dest(destination));

        return stream;
    };
};

Minifying Javascript

Similarly we’ll create gulp/minify-js.js to minify our Javascript files. Required installs:

module.exports = function (gulp, plugins, config) {
    return function () {
        var destination = 'assets/js';

        var stream = gulp.src('tmp/js/**/*.js')
            .pipe(plugins.uglify())
            .pipe(gulp.dest(destination));

        return stream;
    };
};

Production Build

Now we can modify our gulpfile.js to include a production build task:

gulp.task('minify-css', getTask('minify-css'));
gulp.task('minify-js', getTask('minify-js'));

gulp.task('clean:dist', function() {
    return del('assets');
  })

gulp.task('build', function (callback) {
    runSequence('clean:dist',
        ['sass', 'scripts'], 
        ['minify-css', 'minify-js'], 
         callback)
  });

And to run the build task:

$ gulp build

Final words

There’s no one correct workflow and Gulp isn’t the only task runner option. The above is a generic and modular Gulp workflow for Perch which can be built upon.

And with Pipit Pinion’s flexibility you can manage what assets are added to your documents in an easy and maintainable way regardless of what task runner you use and regardless of whether you use a task runner at all.

All the Gulp files are available on GitHub.

link