How To Create a WordPress Shortcode for flickr Albums
Tags: wordpress, css, javascript, webpack
Published: 2019-09-23 12:01:00

How to create a WordPress custom shortcode to display photos from flickr. This post documents using the [Slick Lazy Load Photo Grid](https://jimfrenette.com/2017/12/slick-lazy-load-photo-grid-webpack-3/) along with the flickr API to render photo albums in Wordpress Posts wherever the shortcode is entered. [Webpack](https://webpack.js.org/) 4, [autoprefixer](https://github.com/postcss/autoprefixer), and [babel](https://babeljs.io/) are included for building the JS and CSS.

#### Requirements

*   [flickr API](https://www.flickr.com/services/api/) key. From their API page, select the Create an App link for more information on how to request an API key.
*   [Node.js](https://nodejs.org) with NPM

*   If you're looking for a local Wordpress development environment, may I suggest giving [docker4wordpress](https://github.com/wodby/docker4wordpress) a try which I have written about in my post titled, [WordPress from Development to Production using Docker](https://jimfrenette.com/2018/05/wordpress-from-development-to-production-using-docker/).

### Shortcode

Creating a custom shortcode is done by adding PHP code to the theme `functions.php` file. I prefer keeping my custon WordPress functions in separate files rather than lumping them all in the existing `functions.php` file. To do this, create a `functions` folder and add a new `slickflickr.php` file to it.

Add the following `php` code to the `slickflickr.php` file to register the custom `shortcode_handler` function. Be sure to replace the example `$api_key` value with your [flickr API](https://www.flickr.com/services/api/) key.

##### slickflickr.php

    <?php
    /**
     * Slick Flickr Album Shortcode
     *
     * @param array [photoset_id
     * example
     * [slickflickrshortcode photoset_id=72157685387149525]
     *
     * @return string Unordered list of images for lazy loading slick photo gallery
     *
     * url format reference:
     * https://www.flickr.com/services/api/misc.urls.html
     */
    function slickflickr_shortcode_handler($args) {
    
        $xml = slickflickr_get_photoset($args['photoset_id']);
    
        ob_start();
      ?>
      <ul class="photogrid">
      <?php
        foreach($xml->photoset[0]->photo as $photo) {
            echo '<li><img data-src="https://farm' . $photo['farm'] . '.staticflickr.com/' . $photo['server'] . '/' . $photo['id'] . '_' . $photo['secret'] . '_b.jpg" alt="' . $photo['title'] . '" /></li>';
        }
        ?>
        </ul>
        <?php
        return ob_get_clean();
    }
    add_shortcode( 'slickflickrshortcode',  'slickflickr_shortcode_handler' );
    
    function slickflickr_curl($url) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        $data = curl_exec($ch); // response
        curl_close($ch);
        return $data;
    }
    
    function slickflickr_get_photoset($photoset_id) {
        $results = null;
        $api_key = '1234a567bc89d101ef1ghij121314klm';
        $url = 'https://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos&api_key=' . $api_key . '&photoset_id=' . $photoset_id . '&format=rest';
        $xml = new \SimpleXmlElement(slickflickr_curl($url));
        return $xml;
    }
    

Reference: [WordPress Shortcode API](https://codex.wordpress.org/Shortcode_API)

At or near the bottom of the `functions.php`, add this [require](http://php.net/manual/en/function.require.php) statement to include the custom shortcode handler in the theme.

##### functions.php

    ...
    
    /**
    * Slick flickr Shortcode Handler
    */
    require get_parent_theme_file_path( '/functions/slickflickr.php' );
    

> Note that the ellipsis `...` in the code snippets throught this tutorial are not a part of the actual code and is there only to denote code that is being skipped and not applicable to the example.

Test the custom shortcode by entering the shortcode text in a post. For example, `[slickflickrshortcode photoset_id=72157685387149525]`. When you `view-source` for that post, you should see an unordered list with `img` elements. The flickr album image URL's are added to the `img` element `data-src` attribute for lazy-loading. The styling and image lazy-loading is handled by CSS and JavaScript code that we will add to the WordPress theme in a subsequent section.

* * *

### Theme

For the front end, we need to bring the UI build process for the JavaScript and CSS into a WordPress theme. In this tutorial, I'm going to use a copy of the TwentySeventeen Theme that comes with WordPress along with my [TwentySeventeen Theme Sass](https://github.com/jimfrenette/twentyseventeen-sass). If you want a completely blank slate instead, you could use the _s theme I've written about [here](https://jimfrenette.com/2018/08/completely-blank-no-css-_s-wordpress-starter-theme).

Download or clone the [TwentySeventeen Theme Sass](https://github.com/jimfrenette/twentyseventeen-sass) into the `mytwentyseventeen` folder. If you have an existing theme, download the zip file from github instead, extract, and copy the `src` folder into your theme.

    cd wp-content/themes
    
    git clone https://github.com/jimfrenette/twentyseventeen-sass.git mytwentyseventeen
    

Copy all of the `twentyseventeen` theme folders and files into the new `mytwentyseventeen` theme.

    cd wp-content/themes
    
    cp -r twentyseventeen/ mytwentyseventeen/
    

On Windows? I have written about some Unix like command line interfaces for it including [WSL ubuntu zsh nvm etc.](https://jimfrenette.com/2018/04/wsl-ubuntu-zsh-nvm-etc/), and [Cygwin Oh My ZSH Recipe](https://jimfrenette.com/2017/02/cygwin-oh-my-zsh-recipe/).

### UI Build

Since we're not using Gulp, delete `gulpfile.js` and `package.json`.

Create these files in the the new theme. We will edit them later. For example,

    cd wp-content/themes/mytwentyseventeen
    
    # create the .babelrc file
    touch .babelrc
    
    # create a new package.json file using npm
    npm init
    
    # create the postcss config
    touch postcss.config.js
    

#### NPM Packages

Install NPM packages to transpile the Javascript and Sass using Webpack 4.

    cd wp-content/themes/mytwentyseventeen
    
    npm i webpack webpack-cli webpack-merge --save-dev
    
    npm i autoprefixer --save-dev
    
    npm i babel-loader @babel/core @babel/preset-env --save-dev
    
    npm i css-loader style-loader --save-dev
    
    npm i mini-css-extract-plugin --save-dev
    
    npm i node-sass sass-loader --save-dev
    
    npm i optimize-css-assets-webpack-plugin --save-dev
    
    npm i postcss-loader --save-dev
    
    npm i resolve-url-loader url-loader --save-dev
    
    npm i uglifyjs-webpack-plugin --save-dev
    

#### Webpack 4 Configuration

Create a webpack folder with three `config.js` files.

    cd wp-content/themes/mytwentyseventeen
    
    mkdir webpack
    
    cd webpack
    
    touch base.config.js
    
    touch dev.config.js
    
    touch prod.config.js
    

The `base.config.js` file contains a configuration that is common to both dev and prod.

##### base.config.js

    const path = require('path');
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    
    module.exports = {
      context: path.resolve(__dirname, '../src'),
      entry: {
        app: './js/index.js'
      },
      output: {
        path: path.resolve(__dirname, '../dist')
      },
      plugins: [
        new MiniCssExtractPlugin({
          filename: "../style.css",
        })
      ],
      externals: {
        // require("jquery") is external and available
        // on the global var jQuery
        "jquery": "jQuery"
      },
      module: {
        rules: [
          {
            test: /\.(css|scss)$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader',
              {
                loader: 'postcss-loader' },
              {
                /* for ~slick-carousel/slick/slick-theme.scss */
                loader: 'resolve-url-loader' },
              {
                /* for resolve-url-loader:
                    source maps must be enabled on any preceding loader */
                loader: 'sass-loader?sourceMap' }
            ]
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: 'babel-loader'
          },
          { /* for ~slick-carousel/slick/slick-theme.scss */
            test: /\.(eot|woff|woff2|ttf|svg|png|jpg|gif)$/,
            loader: 'url-loader?limit=30000&name=[name]-[hash].[ext]'
          }
        ]
      }
    }
    

The `dev.config.js` file contains only configuration settings specifically for development. The configuration settings in the `base.config.js` file are combined with this dev configuration by `webpack-merge`.

##### dev.config.js

    const merge = require('webpack-merge');
    const baseConfig = require('./base.config.js');
    
    module.exports = merge(baseConfig, {
      devtool: 'eval-source-map',
    });
    

The `prod.config.js` file contains only configuration settings specifically for the production build. The configuration settings in the `base.config.js` file are also combined with this configuration by `webpack-merge`.

##### prod.config.js

    const merge = require('webpack-merge');
    const baseConfig = require('./base.config.js');
    const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
    const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
    
    module.exports = merge(baseConfig, {
      optimization: {
        minimizer: [
          new OptimizeCSSAssetsPlugin({})
        ]
      },
      plugins: [
        new UglifyJsPlugin({
          uglifyOptions: { output: { comments: false } }
        })
      ]
    });
    

Add the `presets` property to the `.babelrc` congfiguration file.

##### .babelrc

    {
      "presets": ["@babel/preset-env"]
    }
    

Add this `plugins` property to require the autoprefixer postcss plugin to the `postcss.config.js` configuration file.

##### postcss.config.js

    module.exports = {
      plugins: [
        require('autoprefixer')
      ]
    }

Add a `browserslist` property to the `package.json` file to inform our transpilers which browsers to support. Here, we're targeting browsers greater than 2% according to global usage statistics and Internet Explorer versions greater than 10.

##### package.json

    {
      "name": "mytwentyseventeen",
      "version": "1.0.0",
      "description": "WordPress Twenty Seventeen Theme",
      "main": "index.js",
      "browserslist": [
        "> 2%",
        "last 2 versions",
        "ie > 10"
      ],
    
      ...
    }
    

In the `src` folder, create a `js` folder containing an `index.js` entrypoint that imports the Sass.

##### index.js

    import style from '../sass/style.scss'
    

#### [npm-run-script](https://docs.npmjs.com/cli/run-script)

In the `package.json` file, add `dev` and `prod` properties to the `scripts` object for running webpack.

##### package.json

      ...
    
      "scripts": {
        "dev": "webpack --mode=development --watch --progress --colors --config webpack/dev.config.js",
        "build": "webpack --mode=production --progress --hide-modules --config webpack/prod.config.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
    
      ...
    }
    

### Lazy Load Album Images

This technique defers downloading of the images making the page load faster.

Inside the `src/js`, create a `lazyimage.js` module file.

##### lazyimage.js

    export default class Lazyimage {
      constructor(options) {
    
          this.init();
      }
    
      init() {
    
          [].forEach.call(document.querySelectorAll('img[data-src]'), function(img) {
              img.setAttribute('src', img.getAttribute('data-src'));
              img.onload = function() {
                  img.removeAttribute('data-src');
              };
          });
      }
    
    }
    

To import the `Lazyimage` class from the module, add this code to the `src/js/index.js` file.

##### index.js

    ...
    
    import Lazyimage from './lazyimage'
    
    new Lazyimage();
    

Inside the `src/sass` folder, create a `_photogrid.scss` and a `_lazyimage.scss` file. The Sass partial filenames have leading underscore to indicate they only get processed when imported.

##### _photogrid.scss

    ul.photogrid {
      margin: 0.5vw 0.5vw 0.5vw -0.5vw;
      font-size: 0;
      flex-flow: row wrap;
      display: flex;
    
      li {
        flex: auto;
        width: 200px;
        margin: 0.5vw;
      }
    }
    

##### _lazyimage.scss

    img {
      opacity: 1;
      transition: opacity 0.3s;
    }
    
    img[data-src] {
      opacity: 0;
    }
    

Update `src/sass/style.scss` to import the Sass partials.

##### style.scss

    ...
    
    @import "photogrid";
    @import "lazyimage";
    

#### Run the Build

One sec, we need to link the `dist/app.js` that is going to be built in the theme footer. Add a script element before the closing `body` tag.

##### footer.php

    ...
    
    <script async src="/wp-content/themes/mytwentyseventeen/dist/app.js"></script>
    
    </body>
    </html>
    

Before moving onto the lightbox and Slick slider Sass and JavaScript code, run the build to verify that everything is styled and working properly.

    npm run build
    

If all looks good after clearing the cache, run the development build to watch for changes and build incrementally when the remaining lightbox and slider code is added.

    npm run dev
    

### Slick Slider Lightbox

Install [Slick](http://kenwheeler.github.io/slick/)

    npm i slick-carousel --save
    

Inside the `src/js`, create a `lightbox.js` module file.

##### lightbox.js

    import $ from 'jquery'
    import 'slick-carousel'
    
    export default class Lightbox {
      constructor(options) {
        this.settings = $.extend({/* defaults */}, options);
        this.init();
      }
    
      init() {
        let source = $(this.settings.source);
    
        if (source.length) {
          source.each((index, el) => {
            this.create(index, el);
          });
        }
      }
    
      create(index, el) {
        let lightbox = this.settings.name + '__' + index,
        opener = $(el).find(this.settings.opener);
    
        $('body').append('<div data-lightbox="' + lightbox + '" class="lightbox"><div></div></div>');
        if (this.settings.type === 'slider') {
          $('div[data-lightbox="' + lightbox + '"] > div')
            .append('<div class="lightbox-slider"></div>');
    
          var slider = $('div[data-lightbox="' + lightbox + '"] .lightbox-slider');
          slider.slick({
            dots: true
          });
    
          opener.each((index, el) => {
            this.popSlider(lightbox, slider, el);
          });
        }
    
        // close button
        $('div[data-lightbox="' + lightbox + '"] > div')
          .prepend('<a class="lightbox-close" href="javascript:void(0)">+</a>');
    
        $('.lightbox-close').on( 'click', function() {
          $('[data-lightbox="' + lightbox + '"]').removeClass('is-open');
        });
    
        //close on outside click
        window.onclick = function(evt) {
          if (evt.target.dataset.lightbox == lightbox) {
            $('[data-lightbox="' + lightbox + '"]').removeClass('is-open');
          }
        }
    
        // close on escape
        $(document).keyup(function(evt) {
          if (evt.which === 27) {
            $('[data-lightbox="' + lightbox + '"]').removeClass('is-open');
          }
        });
      }
    
      popSlider(lightbox, slider, el) {
        let img = $(el).find('img'),
            src = img.prop('src'),
            slide = document.createElement('div'),
            slideImg = document.createElement('img');
    
        slideImg.src = src;
        slide.appendChild(slideImg);
    
        if (img.attr('alt')) {
          let caption = document.createElement('p'),
          captionText = document.createTextNode(img.attr('alt'));
          caption.appendChild(captionText);
          slide.appendChild(caption);
        }
    
        slider.slick('slickAdd', slide);
    
        img.wrap('<a href="' + src + '"></a>').on( 'click', function(evt) {
          evt.preventDefault();
          $('[data-lightbox="' + lightbox + '"]').addClass('is-open');
          let index = $(this).closest(el).index();
          slider.slick('slickGoTo', index);
        });
      }
    }
    

Update the `src/js/index.js` to import the `Lightbox` class from the module.

##### index.js

    import style from '../sass/style.scss'
    import Lazyimage from './lazyimage'
    import Lightbox from './lightbox'
    
    new Lazyimage();
    
    new Lightbox({
      name: 'lightbox',
      source: '.photogrid',
      opener: 'li',
      type: 'slider'
    });
    

Inside the `src/sass` folder, create a `_lightbox.scss`

##### _lightbox.scss

    /* overlay (background) */
    .lightbox {
      max-height: 0; /* instead of `display: none` so slider can init properly. */
    
      position: fixed; /* stay in place */
      z-index: 9;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      overflow: auto; /* enable scroll if needed */
      background-color: rgb(0,0,0); /* fallback color */
      background-color: rgba(0,0,0,0.80); /* black w/ opacity */
    
      > div {
        position: relative;
        background-color: rgb(0,0,0);
        padding: 20px;
        color: #fff;
        //width: 100%; /* could be more or less, depending on screen size */
        width: 90vw;
        margin: 5vw auto;
    
        .slick-prev,
        .slick-next {
          z-index: 10;
        }
        .slick-prev {
          left: -20px;
        }
        .slick-next {
          right: -20px;
        }
        .slick-dots {
          li button:before {
            color: #fff;
          }
        }
      }
    
      &.is-open {
        max-height: 100%; /* unhide */
      }
    
      @media only screen and (max-width: 600px) {
        background-color: rgba(0,0,0,0.95); /* black w/ opacity */
    
        > div {
          padding: 40px 0 20px 0;
          width: 100vw;
          margin: 0 auto;
    
          .slick-slide p {
            padding: 0 15px;
          }
    
          .slick-next, .slick-prev {
            display: none;
          }
        }
      }
    
      @media only screen and (min-width: 1025px) {
        > div {
          max-width: 1024px;
        }
      }
    }
    
    /* close button */
    .lightbox-close {
      position: absolute;
      top: 2px;
      right: 2px;
      text-align: center;
      line-height: 20px;
      font-size: 20px;
      font-weight: bold;
      color: rgba(255,255,255,0.75);
      width: 20px;
      height: 20px;
      transform: rotate(45deg);
      text-decoration: none;
      z-index: 10;
    
      &:hover {
        color: rgb(255,255,255);
      }
    
      @media only screen and (max-width: 600px) {
        top: 4px;
        right: 4px;
        line-height: 32px;
        font-size: 32px;
        width: 32px;
        height: 32px;
      }
    }
    

Update `src/sass/style.scss` adding the `lightbox` and slick-carousel imports.

##### style.scss

    ...
    
    @import "photogrid";
    @import "lazyimage";
    @import "lightbox";
    
    /* node_modules */
    @import "~slick-carousel/slick/slick.scss";
    @import "~slick-carousel/slick/slick-theme.scss";
    

For a complete listing of the source files that have been created or updated, I advise you to examine the [source code repository](https://github.com/jimfrenette/slickflickrshortcode) on GitHub.

<h5><i class="icon-github"></i> <a href="https://github.com/jimfrenette/slickflickrshortcode">Source Code</a></h5>

Originally posted at [jimfrenette.com/2018/11/how-to-create-wordpress-shortcode-for-flickr-albums](https://jimfrenette.com/2018/11/how-to-create-wordpress-shortcode-for-flickr-albums/)