Skip to content
Gertjan van den Burg edited this page Feb 11, 2017 · 13 revisions

HugoPhotoSwipe Walkthrough

In this guide we will set up a simple website using Hugo and HugoPhotoSwipe.
We will first set up our entire Hugo configuration and then set up the HugoPhotoSwipe configuration. You can find the complete source code for the final result of this tutorial here with a demonstration page here.

Table of Contents

Installation

Before we begin, make sure you install hugo using the installation instructions on the site. Next, make sure to install HugoPhotoSwipe with

pip install --process-dependency-links hugophotoswipe

Finally, download PhotoSwipe from the PhotoSwipe Github. The easiest way to do this is to either clone the repository (if you know what that is) or download the files using the "clone or download -> Download ZIP" button on Github. Extract the ZIP file to somewhere on your computer, we'll use these files later.

Setting up Hugo

Initializing our site

We will start by setting up a basic Hugo photo site. Create a new Hugo site using:

hugo new site hps_example

Change to the newly created website with

cd hps_example

The directory in which you are now should contain:

archetypes/  config.toml  content/  data/  layouts/  static/  themes/

This directory will be called the Hugo root directory below.

We will assume that one of the types of content on your site will be photo galleries. So, we start off by creating a new subdirectory in the content directory as follows

mkdir content/galleries

Adding layouts and shortcodes

The layouts directory in the Hugo root directory contains all the information for how our site will look (we won't use themes in this example).
First, we create a file called index.html in the layouts directory, which contains the following HTML code

<html>
  <body>
    <main class="container" style="margin-left: calc((100vw - 2*600px - 2*40px)/2);margin-right: auto;">
    {{ range .Data.Pages }}
    <a href="{{ .Permalink }}" style="float: left;margin: 20px;">
        <h1 id="title">{{ .Title }}</h1>
        <img alt="{{ .Title }}" src="{{ .Params.cover }}">
      </a>
    {{ end }}
    </main>
  </body>
</html>

Exactly how this works will become more clear later on, but for now we can see that we iterate over all pages, add a title for each page and add an image for each page. These images will be the coverimages of the photo albums.

Moving on, we now add the shortcodes that Hugo will use to process the output of HugoPhotoSwipe. First, create a shortcodes subdirectory in the layouts directory. Now, in this shortcodes directory we place two files. The first file is photo.html, with the following content

<figure itemprop="associatedMedia" itemscope
itemtype="https://schema.org/ImageObject">
  <a href="{{ .Get "href"}}" itemprop="contentUrl" data-size="{{ .Get "largeDim"}}" data-medium-url="{{ .Get "smallUrl" }}" data-medium-size="{{ .Get "smallDim" }}">
    <img alt="{{ .Get "alt"}}" data-size="{{ .Get "thumbSize"}}" itemprop="thumbnail" src="{{ .Get "thumbUrl"}}">
  </a>
  <figcaption itemprop="caption description">
    {{ .Get "caption"}}
    {{ if (not (eq (.Get "copyright") "")) }}
    <span itemprop="copyrightHolder">&#169; {{ .Get "copyright"}}</span>
    {{ end }}
  </figcaption>
</figure>

The second file is wrap.html, which simply contains

{{ .Inner }}

Finally, we need to tell Hugo what HTML we want to use for the galleries themselves. For this, we create a subdirectory of the layouts directory called galleries. In this subdirectory we place the file single.html, with the content:

<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
    <title>
      {{ if ne .URL "/" }} {{ .Title }} &middot; {{ end }} {{ .Site.Title }}
    </title>

    <link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/photoswipe.css"/>
    <link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/default-skin/default-skin.css"/>
    <script type="application/javascript" src="{{ .Site.BaseURL }}js/photoswipe.min.js"></script>
    <script type="application/javascript" src="{{ .Site.BaseURL }}js/photoswipe-ui-default.min.js"></script>

  </head>
  <body>

    <!-- Start copied from GitHub docs HugoPhotoSwipe -->

    <main class="container">
    <h1 class="text-primary">{{ .Title }}</h1>
    <div class="gallery" itemscope itemtype="https://schema.org/ImageGallery">
      {{ .Content }}
    </div>

    <div class="pswp" role="dialog" aria-hidden="true" tabindex="-1">
      <div class="pswp__bg"></div>
      <div class="pswp__scroll-wrap">
        <div class="pswp__container">
          <div class="pswp__item"></div>
          <div class="pswp__item"></div>
          <div class="pswp__item"></div>
        </div>
        <div class="pswp__ui pswp__ui--hidden">
          <div class="pswp__top-bar">
            <div class="pswp__counter"></div>
            <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
            <button class="pswp__button pswp__button--share" title="Share"></button>
            <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
            <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
            <div class="pswp__preloader">
              <div class="pswp__preloader__icn">
                <div class="pswp__preloader__cut">
                  <div class="pswp__preloader__donut"></div>
                </div>
              </div>
            </div>
          </div>
          <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
            <div class="pswp__share-tooltip"></div>
          </div>
          <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>
          <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button>
          <div class="pswp__caption">
            <div class="pswp__caption__center"></div>
          </div>
        </div>
      </div>
    </div>
    </main>
    <script type="application/javascript" src="{{ .Site.BaseURL }}js/pswp_gallery.js"></script>

    <!-- End copied from GitHub docs HugoPhotoSwipe -->

    <script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
  </body>
</html>

Adding static files

For our PhotoSwipe galleries to actually work, we will need the CSS and Javascript files that come with PhotoSwipe, and one extra Javascript file. All these files will be placed in the static directory of your Hugo root directory. To keep things organized, we create two subdirectories in the static directory:

mkdir static/{css,js}

Next, copy all the CSS files from PhotoSwipe into the static/css folder. This should then contain a photoswipe.css file and a default-skin folder.

Then, copy all the PhotoSwipe Javascript files that you downloaded before and place them in the static/js directory. This folder should now contain:

photoswipe.js  photoswipe.min.js  photoswipe-ui-default.js photoswipe-ui-default.min.js

Finally, in the static/js folder create a file pswp_gallery.js, with the following content

var initPhotoSwipeFromDOM = function(gallerySelector) {

    // parse slide data (url, title, size ...) from DOM elements
    // (children of gallerySelector)
    var parseThumbnailElements = function(el) {
        var thumbElements = el.childNodes,
            numNodes = thumbElements.length,
            items = [],
            figureEl,
            linkEl,
            size,
            item;

        for(var i = 0; i < numNodes; i++) {

            figureEl = thumbElements[i]; // <figure> element

            // include only element nodes
            if(figureEl.nodeType !== 1) {
                continue;
            }

            linkEl = figureEl.children[0]; // <a> element
	    if (linkEl === undefined)
		    continue;

            size = linkEl.getAttribute('data-size').split('x');
	    mediumSize = linkEl.getAttribute('data-medium-size').split('x');

            // create slide object
            item = {
		    originalImage: {
			    src: linkEl.getAttribute('href'),
                	    w: parseInt(size[0], 10),
                            h: parseInt(size[1], 10)
		    },
		    mediumImage: {
			    src: linkEl.getAttribute('data-medium-url'),
			    w: parseInt(mediumSize[0], 10),
			    h: parseInt(mediumSize[1], 10)
		    }
            };

            if(figureEl.children.length > 1) {
                // <figcaption> content
                item.title = figureEl.children[1].innerHTML;
            }

            if(linkEl.children.length > 0) {
                // <img> thumbnail element, retrieving thumbnail url
		//item.msrc = linkEl.children[0].getAttribute('src');
            }

            item.el = figureEl; // save link to element for getThumbBoundsFn
            items.push(item);
        }

        return items;
    };

    // find nearest parent element
    var closest = function closest(el, fn) {
        return el && ( fn(el) ? el : closest(el.parentNode, fn) );
    };

    // triggers when user clicks on thumbnail
    var onThumbnailsClick = function(e) {
        e = e || window.event;
        e.preventDefault ? e.preventDefault() : e.returnValue = false;

        var eTarget = e.target || e.srcElement;

        // find root element of slide
        var clickedListItem = closest(eTarget, function(el) {
            return (el.tagName && el.tagName.toUpperCase() === 'FIGURE');
        });

        if(!clickedListItem) {
            return;
        }

        // find index of clicked item by looping through all child nodes
        // alternatively, you may define index via data- attribute
        var clickedGallery = clickedListItem.parentNode,
            childNodes = clickedListItem.parentNode.childNodes,
            numChildNodes = childNodes.length,
            nodeIndex = 0,
            index;

        for (var i = 0; i < numChildNodes; i++) {
            if(childNodes[i].nodeType !== 1) {
                continue;
            }

            if(childNodes[i] === clickedListItem) {
                index = nodeIndex;
                break;
            }
            nodeIndex++;
        }



        if(index >= 0) {
            // open PhotoSwipe if valid index found
            openPhotoSwipe( index, clickedGallery );
        }
        return false;
    };

    // parse picture index and gallery index from URL (#&pid=1&gid=2)
    var photoswipeParseHash = function() {
        var hash = window.location.hash.substring(1),
        params = {};

        if(hash.length < 5) {
            return params;
        }

        var vars = hash.split('&');
        for (var i = 0; i < vars.length; i++) {
            if(!vars[i]) {
                continue;
            }
            var pair = vars[i].split('=');
            if(pair.length < 2) {
                continue;
            }
            params[pair[0]] = pair[1];
        }

        if(params.gid) {
            params.gid = parseInt(params.gid, 10);
        }

        return params;
    };

    var openPhotoSwipe = function(index, galleryElement, disableAnimation, fromURL) {
        var pswpElement = document.querySelectorAll('.pswp')[0],
            gallery,
            options,
            items;

        items = parseThumbnailElements(galleryElement);

        // define options (if needed)
        options = {

            // define gallery index (for URL)
            galleryUID: galleryElement.getAttribute('data-pswp-uid'),

            getThumbBoundsFn: function(index) {
                // See Options -> getThumbBoundsFn section of documentation for more info
                var thumbnail = items[index].el.getElementsByTagName('img')[0], // find thumbnail
                    pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
                    rect = thumbnail.getBoundingClientRect();

                return {x:rect.left, y:rect.top + pageYScroll, w:rect.width};
            }

        };

        // PhotoSwipe opened from URL
        if(fromURL) {
            if(options.galleryPIDs) {
                // parse real index when custom PIDs are used
                // http://photoswipe.com/documentation/faq.html#custom-pid-in-url
                for(var j = 0; j < items.length; j++) {
                    if(items[j].pid == index) {
                        options.index = j;
                        break;
                    }
                }
            } else {
                // in URL indexes start from 1
                options.index = parseInt(index, 10) - 1;
            }
        } else {
            options.index = parseInt(index, 10);
        }

        // exit if index not found
        if( isNaN(options.index) ) {
            return;
        }

        if(disableAnimation) {
            options.showAnimationDuration = 0;
        }

	// Gertjan: this was added because thumbnails are square and pictures
	// are typically not.
	options.showHideOpacity = true;

	// Pass data to PhotoSwipe and initialize it
        gallery = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, items, options);

	// Added by Gertjan
	var realViewportWidth,
	    useLargeImages = false,
	    firstResize = true,
	    imageSrcWillChange;

	gallery.listen('beforeResize', function() {
		// gallery.viewportSize.x - width of PhotoSwipe viewport
		// gallery.viewportSize.y - height of PhotoSwipe viewport
		// window.devicePixelRatio - ratio between physical pixels and
		// device independent pixels (Number). 1 (regular display), 2
		// (@2x, retina), ...

		// calculate real pixels when size changes
		realViewportWidth = gallery.viewportSize.x * window.devicePixelRatio;

		// Code below is needed if you want to switch dynamically on
		// window.resize

		// Find out if current images need to be changed
		if (useLargeImages && realViewportWidth< 1000) {
			useLargeImages = false;
			imageSrcWillChange = true;
		} else if (!useLargeImages && realViewportWidth >= 1000) {
			useLargeImages = true;
			imageSrcWillChange = true;
		}

		// Invalidate items only when source is changed and when it's
		// not the first update
		if (imageSrcWillChange && !firstResize) {
			// invalidateCurrItems sets a flag on slides that are
			// in DOM, which will force update of content (image)
			// on window.resize
			gallery.invalidateCurrItems();
		}

		if (firstResize) {
			firstResize = false;
		}

		imageSrcWillChange = false;
	});

	// gettingData event fires each time PhotoSwipe retrieves image source
	// and size
	gallery.listen('gettingData', function(index, item) {

		// set image source & size based on real viewport width
		if (useLargeImages) {
			item.src = item.originalImage.src;
			item.w = item.originalImage.w;
			item.h = item.originalImage.h;
		} else {
			item.src = item.mediumImage.src;
			item.w = item.mediumImage.w;
			item.h = item.mediumImage.h;
		}
	});

        gallery.init();
    };

    // loop through all gallery elements and bind events
    var galleryElements = document.querySelectorAll( gallerySelector );

    for(var i = 0, l = galleryElements.length; i < l; i++) {
        galleryElements[i].setAttribute('data-pswp-uid', i+1);
        galleryElements[i].onclick = onThumbnailsClick;
    }

    // Parse URL and open gallery if it contains #&pid=3&gid=1
    var hashData = photoswipeParseHash();
    if(hashData.pid && hashData.gid) {
        openPhotoSwipe( hashData.pid ,  galleryElements[ hashData.gid - 1 ], true, true );
    }
};

// execute above function
initPhotoSwipeFromDOM('.gallery');

This completes the Hugo configuration.

Setting up HugoPhotoSwipe

Initializing HugoPhotoSwipe

In your Hugo root directory directory, create a src directory, with a subdirectory for the photos:

mkdir -p src/photos

You can use the src directory also for other things (for instance for sass files), but we won't go into that here. Change to the newly created directory

cd src/photos

Now we can initialize HugoPhotoSwipe. We do this with:

hps init

This creates a settings file called hugophotoswipe.yml. If you're not familiar with the YAML fileformat, you might want to quickly check out the Wikipedia article about it. In the hugophotoswipe.yml file, we first change the markdown_dir variable:

markdown_dir: ../../content/galleries/

The markdown_dir variable is the place where Hugo expects to find the Markdown files to process. Next, we change the output_dir variable:

output_dir: ../../static/pics/

This tells HugoPhotoSwipe where to place the resized photos that it will create. Finally, we change the url_prefix variable to:

url_prefix: /pics/

The url_prefix variable will be used to make sure the links on your site actually point to the photo files. This variable is closely linked to the output_dir variable. Because Hugo maps static to / in the final website, static/pics/ will be mapped to /pics/.

Side-note: The setup we're describing here is hosted on GitHub pages, at gjjvdburg.github.io/HugoPhotoSwipe-Demo. In that case, /static/pics is actually mapped to HugoPhotoSwipe-Demo/pics (remember / is the root of your domain, so that would be gjjvdburg.github.io for me). So in the configuration you'll find on GitHub we use url_prefix: /HugoPhotoSwipe-Demo/pics. So remember: if you're photos are not showing up as expected, make sure that the urls in the HTML that Hugo generates correspond to where the files actually are.

Adding our first album

In the src/photos directory, we can now create a new album with

hps new cats

This creates a directory cats that contains an album.yml file and an empty photos directory. For this example, we fill the photos directory with photos of cats taken from pexels.com, but you can also use other photos if you like. Now that we've added our photos, we open the album.yml file to set the album properties. Change the first few fields to the following:

title: Cute Cats!
album_date: "2017-02-07T22:53:32-05:00"
properties:
   animal: cat
copyright:
coverimage: pexels-photo-127028.jpeg

Notice the quotes around the album_date field, these are important. Once you're done editing, go back to the directory with the hugophotoswipe.yml file, and type

hps update

This will create the resized photos in the output directory we specified in the hugophotoswipe.yml settings file, and it will create the cats.md file in the markdown directory we specified. So, in the content/galleries folder of Hugo we see a cats.md file and in the static/pics folder we see a cats directory.

Let's first look at the output of the resized photos in the static/pics/cats directory. There are a couple of things to notice here:

  1. Coverimage. The coverimage is a 600x600 cropped version of the pexels-photo-127028.jpeg photo, which we specified in the album.yml file to be the coverimage. Note that the 600x600 dimension is due to the settings dim_max_cover and square_coverimage in the hugophotoswipe.yml file.

  2. Large photos. The resized versions of the photos in the large directory all have the maximum dimension of 1600, corresponding to the setting dim_max_large in the hugophotoswipe.yml file.

  3. Small photos. Similarly, the resized versions of the photos in the small directory all have the maximum dimension of 800, corresponding to the setting dim_max_small in the hugophotoswipe.yml file.

  4. Thumbnails. The thumbnails in the thumb directory are 256x256 and are square. This is due to the dim_max_thumb and the square_thumbnails settings of the hugophotoswipe.yml file, respectively.

Adding our second album

Of course, a photo site with one album isn't much to look at, so we add a second album to our site as well. Go back to the src/photos directory, and type

hps new dogs

Again, we add some photos to the dogs/photos directory, and we edit the first few lines of the album.yml file to read:

title: Cute Dogs!
album_date: "2017-02-07T23:14:55-05:00"
properties:
    animal: dog
copyright:
coverimage: wall-animal-dog-pet.jpg

Go back to the src/photos directory (where the hugophotoswipe.yml file is), and type

hps update

If everything goes according to design, HugoPhotoSwipe will recognize that nothing has changed to the cats album and that a new dogs album has been added. It will therefore process the dogs photos only.

Viewing the site

To view the site, go back to the the Hugo root directory and type

hugo -w server

Then open localhost:1313 in your browser, and you should be able to see your Hugo site.

Extras

Optimizing JPEGs

On the web, bandwidth is important. To optimize the size of the photos that HugoPhotoSwipe generates a number of options are available in the configuration file. These options are only useful when the jpg output format is used and correspond to the Pillow save() method for JPEGs.

  • When the option jpeg_optimize is set to True, extra effort is taken to find the best way to save the image file.

  • If the option jpeg_progressive is set to True, the image will be saved as a progressive JPEG. When the image is viewed online, it will become progressively more detailed while the full image is downloaded.

  • Finally, the jpeg_quality settings determines the image quality on a scale from 1 (worst) to 95 (best). The higher this setting the larger the filesize will be.

Using Javascript SmartCrop

If you're unsatisfied with the cropped photos that SmartCrop.py generates, you may want to check if using SmartCrop.js improves things.

To use SmartCrop.js in HugoPhotoSwipe, install smartcrop-cli, which allows you to use SmartCrop from the command line. To install this, you'll first need to install npm if you haven't already (installation instructions are here).

Then, install smartcrop-cli using:

npm install -g smartcrop-cli

(-g installs it globally on your computer, if you don't want this simply remove this flag). After the installation, you should be left with a smartcrop-cli directory inside a node_modules directory. In there you'll find an executable file called smartcrop-cli.js. Record the location of this file.

Now we can configure HugoPhotoSwipe to use SmartCrop.js. To do this, open the hugophotoswipe.yml file, and edit the following lines to:

smartcrop_js_path: /path/to/smartcrop-cli.js
use_smartcrop_js: True

That's it! HugoPhotoSwipe should now use SmartCrop.js for thumbnail generation.

Clone this wiki locally