Home
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.
Throughout the demo commands that are meant to be typed into a terminal will
be marked with a $
symbol.
Before we begin, make sure you install hugo using the installation instructions on the site. Next, make sure to install HugoPhotoSwipe with
$ pip install 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.
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
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).
If you do have a theme with your site, you'll have to incorporate this
configuration into the layout directory of your theme.
First, we create a file called index.html
in the layouts
directory
for our gallery overview page (which contains the cover images).
This file simply iterates over each of the galleries and adds a cover image
that links to to the full gallery:
<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 cover images 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">© {{ .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 }} · {{ 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>
Again, if you have an existing theme or layout, you may have to incorporate this into an existing file.
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}
In the PhotoSwipe folder you downloaded at the begining, go into the dist/
directory.
Into the static/css/
folder in your hugo site, copy photoswipe.css
and the entire folder default-skin/
Into the static/js/
folder in your hugo site, copy all the Javascript (.js
) files.
the directory structure of static
is now like this:
├── css
│ ├── default-skin
│ │ ├── default-skin.css
│ │ ├── default-skin.png
│ │ ├── default-skin.svg
│ │ └── preloader.gif
│ └── photoswipe.css
└── js
├── photoswipe-ui-default.js
├── photoswipe-ui-default.min.js
├── photoswipe.js
└── photoswipe.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.
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.
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 two details that might trip you up:
-
The quotes around the
album_date
field, these are important. -
The
animal
field is preceeded by 3 spaces. Using atab
(\t
) or no spaces with result in an error at the next step.
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:
-
Coverimage. The coverimage is a 600x600 cropped version of the
pexels-photo-127028.jpeg
photo, which we specified in thealbum.yml
file to be the coverimage. Note that the 600x600 dimension is due to the settingsdim_max_cover
andsquare_coverimage
in thehugophotoswipe.yml
file. -
Large photos. The resized versions of the photos in the
large
directory all have the maximum dimension of 1600, corresponding to the settingdim_max_large
in thehugophotoswipe.yml
file. -
Small photos. Similarly, the resized versions of the photos in the
small
directory all have the maximum dimension of 800, corresponding to the settingdim_max_small
in thehugophotoswipe.yml
file. -
Thumbnails. The thumbnails in the
thumb
directory are 256x256 and are square. This is due to thedim_max_thumb
and thesquare_thumbnails
settings of thehugophotoswipe.yml
file, respectively.
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.
One of the key features of HugoPhotoSwipe is that it allows you to easily
define names, alt text, and captions of your photos. Adding this information
makes it easier for search engines to understand what your photo is about,
which in turn makes it more likely to show up in search results. This
information can be added in the album.yml
file, where for each photo there
is a name
, alt
, and caption
field. Note that the name
field
will be used for the name of the rescaled photo files, so spaces will
automatically be replaced with underscores by HugoPhotoSwipe.
To show an example, for the first photo in the dog album, we set:
photos:
- file: chihuahua-dog-puppy-cute-39317.jpeg
name: chihuahua in a cup
alt: cute chihuahua in a cup
caption: Look at this tiny Chihuahua in a cup!
...
Run hps update
for the changes to take effect. Note that now there are two
versions of this photo in the output directory, because we've changed the
name. Usually this is not a problem, but you can always run hps clean
to
cleanup all files generated by HugoPhotoSwipe.
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.
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 toTrue
, extra effort is taken to find the best way to save the image file. -
If the option
jpeg_progressive
is set toTrue
, 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.
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.
HugoPhotoSwipe allows you to add additional fields to the TOML front matter of
the Markdown file of a gallery, using the properties
field in the
album.yml
file. Here's an example:
If you place this in the album.yml
file:
properties:
animal: dog
then you get this in the front matter of your markdown file:
animal = """dog"""
This allows you to use your gallery whatever way you like, especially when
combining these properties with the Hugo
markdownify function.
For example, say you have some long text in the album.yml
file:
properties:
text: >
This is some text that we want to have *above* our gallery.
We want newlines in there too. We also want a
[link](https://github.com/GjjvdBurg/HugoPhotoSwipe).
This gives the following in the front matter of the markdown file:
text: """This is some text that we want to have *above* our gallery.
We want newlines in there too. We also want a
[link](https://github.com/GjjvdBurg/HugoPhotoSwipe).
"""
Note that we lost a newline in the conversion, so two newlines in yaml are one newline in the TOML. You can use this as follows in a Hugo template:
{{ .Params.text | markdownify }}
which would convert this to HTML.