How To Create a WordPress Shortcode for flickr Albums

How to create a WordPress custom shortcode to display photos from flickr. This post documents using the Slick Lazy Load Photo Grid along with the flickr API to render photo albums in Wordpress Posts wherever the shortcode is entered. Webpack 4, autoprefixer, and babel are included for building the JS and CSS.

Requirements

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 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

At or near the bottom of the functions.php, add this require 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 throughout 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. If you want a completely blank slate instead, you could use the _s theme I’ve written about here.

Download or clone the TwentySeventeen Theme 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., and 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

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

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 on GitHub.

Source Code
comments powered by Disqus