Adding a photoswipe gallery as a shortcode using Page.Resources

The code below adds this shortcode {{ gallery folder="" title=""}}

folder: must be a subdirectory in that Page Bundle. Required. I need to test if this allows multiple galleries in the same page.

Title: used in the initial H4 of the gallery. Optional.

This version is mostly a proof of concept and requires that you install PhotoSwipe, and optionaly Masonry, in your themes/YOURTHEME/static/plugins directory, or /static/plugins for example.

To do: Define a default value for folder and find a way to load the javascript and css only once per page

Hope it’s useful for someone. :slight_smile:

Example front matter:

- src: "gallery/*.jpg"
  name: gallery-:counter
  title: gallery-title-:counter

Shortcode: gallery.html

<link rel="stylesheet" href="/plugins/photoswipe/photoswipe.css">
<link rel="stylesheet" href="/plugins/photoswipe/default-skin/default-skin.css">
{{ $galleryimages := .Page.Resources.Match (printf "%s-*" (.Get "folder")) }}

<h4 class="title">{{ .Get "title" }}</h4>
<div class="row">
  <div class="12u$">
	<div class="gallery" itemscope itemtype="">
{{ range $galleryimages }}
{{ $thumbnail := .Resize "320x" }}

		<figure itemscope itemtype="" class="image gallery-item">
		  <a href="{{ .Permalink }}" itemprop="contentUrl" data-size="{{ .Width }}x{{ .Height }}" >
		      <img src="{{ $thumbnail.Permalink }}" itemprop="thumbnail" alt="galleryImage" />

		  <figcaption itemprop="caption description">
		    <span itemprop="copyrightHolder"></span>

{{ end }}

/* Extra Small Devices, Phones */ 
@media only screen and (min-width : 480px) {
  .gallery {
    column-count: 1;
    column-gap: 3px;
/* Small Devices, Tablets */
@media only screen and (min-width : 768px) {
  .gallery {
    column-count: 3;
    column-gap: 3px;

/* Medium Devices, Desktops */
@media only screen and (min-width : 992px) {
  .gallery { /* Masonry container */
      column-count: 4;
      column-gap: 3px;

/* Large Devices, Wide Screens */
@media only screen and (min-width : 1200px) {
    column-count: 4;
    column-gap: 3px;

.gallery-item { /* Masonry bricks or child elements */
    background-color: #eee;
    display: inline-block;
    margin: 0 0 0em;
    width: 100%;

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
<!-- Background of PhotoSwipe.
     It's a separate element, as animating opacity is faster than rgba(). -->
<div class="pswp__bg"></div>
<!-- Slides wrapper with overflow:hidden. -->
<div class="pswp__scroll-wrap">
    <!-- Container that holds slides.
      PhotoSwipe keeps only 3 of them in DOM to save memory.
      Don't modify these 3 pswp__item elements, data is added later on. -->
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      <!--  Controls are self-explanatory. Order can be changed. -->
      <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>
      <!-- Preloader demo -->
      <!-- element will get class pswp__preloader--active when preloader is running -->
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
<script src="/plugins/photoswipe/photoswipe.min.js"></script>
<script src="/plugins/photoswipe/photoswipe-ui-default.min.js"></script>
<script type="text/javascript">
var initPhotoSwipeFromDOM = function(gallerySelector) {

// parse slide data (url, title, size ...) from DOM elements
// (children of gallerySelector)
var parseThumbnailElements = function(el) {
    var thumbElements = el.getElementsByTagName('figure'),
        numNodes = thumbElements.length,
        items = [],

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

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

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

        linkEl = figureEl.children[0]; // <a> element

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

        // create slide object
        item = {
            src: linkEl.getAttribute('href'),
            w: parseInt(size[0], 10),
            h: parseInt(size[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

    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.srcElement;

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

    if(!clickedListItem) {

    // 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.getElementsByTagName('figure'),
        numChildNodes = childNodes.length,
        nodeIndex = 0,

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

        if(childNodes[i] === clickedListItem) {
            index = 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]) {
        var pair = vars[i].split('=');
        if(pair.length < 2) {
        params[pair[0]] = pair[1];

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

    if(!params.hasOwnProperty('pid')) {
        return params;
    } = parseInt(, 10);
    return params;

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

    items = parseThumbnailElements(galleryElement);

    // define options (if needed)
    options = {
        index: index,

        // 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, + pageYScroll, w:rect.width};


    if(disableAnimation) {
        options.showAnimationDuration = 0;

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

// 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( > 0 && hashData.gid > 0) {
    openPhotoSwipe( - 1 ,  galleryElements[ hashData.gid - 1 ], true );

Tip: Use .HasShortcode "gallery" (replace gallery with your shortcode name) in your template.


Thanks @bep, that solves the issue for me. I will see if I can use it to make this shortcode as contained as possible, the ideal scenario would be for it to be installed without touching the original template.

I’ve made improvements on the code above, it now uses a pure CSS masonry layout with css columns.

Minor setback, you can’t order the photos per row. Still trying to think up ways to load the JS for photoswipe only once.

Great stuff! I’ve done a similar gallery with directory crawling but pre-v0.32 image processing. I’m late to the party (learned about image processing last week) and was planning to try what you’ve done here. Thanks for sharing.

Side note: I also see some classes :wink:

1 Like

Glad to know it’s useful :slight_smile:

Keep in mind what @bep said, about putting .HasShortcode "gallery" in the footer to load the required JS.

1 Like

I was about to start the same thing. Thanks a lot !!
When you said improve the code, you mean what we see is optimized ? Our do you have better code to share ? Anyway thanks again @brunoamaral

1 Like

The one above is the latest and should work almost out-of-the-box. It’s just that right now you can’t use the shortcode more than once in a page or post. :frowning:



I seem to struggle with implementing the gallery.
I’m using the following shortcode in the post:
{{% gallery folder="img/gallery1/*.jpg" title="gallery" %}}
The jpg files are located in static/gallery1.

But the rendered page only shows the title gallery on the page, not the pictures.

Any ideas? Thanks for your help!

your folder should be gallery1, try

{{% gallery folder="gallery1" title="gallery" %}}

If your folder gallery1 is inside img/ then it is something I didn’t test before. Let me know how it goes. :slight_smile:
EDIT: Sorry only now did I notice that your files are in static. That won’t work. they need to be inside the page-bundle. So something like this path:
/content/post/my-post/ <-- your post and frontmatter.

and then /content/post/my-post/gallery1 <-- the images for your gallery

1 Like

If have the same problem. Only the title shows up.

title: foo
- src: "gallery1/*.jpg"
  name: Fotorundgang
  title: Fotorundgang

{{% gallery folder="gallery1" title="Rundgang" %}}

Images are in the folder gallery1 in the content folder


<link rel="stylesheet" href="/plugins/photoswipe/photoswipe.css">
<link rel="stylesheet" href="/plugins/photoswipe/default-skin/default-skin.css">

<h4 class="title">Rundgang</h4>
<div class="row">
<div class="12u$">
<div class="gallery" itemscope itemtype="">


Followed by style and js code of the shortcode

My guess is that the code does not play with hugo v0.54

The resource name must match the folder name. That’s why it isn’t working.

This got so much attention that I went ahead and put everything in a blog post:


Thanks a lot.

With this exact snippet it works:

  • src: “gallery/*.jpg”
    name: gallery-:counter
    title: gallery-title-:counter

I thought that “name” and “title” could be customized.

1 Like

Does photo swipe support .avif .webp etc have you try to set it up to load

and then fallback to the browser supported format folder

This is a question for the PhotoSwipe maintainer. It is OT in this forum.