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
-
flickr API key. From their API page, select the Create an App link for more information on how to request an API key.
-
Node.js with NPM
-
If you’re looking for a local Wordpress development environment, may I suggest giving docker4wordpress a try which I have written about in my post titled, 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 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.