Updated 2022-01-01: extended the gallery-image shortcode
Multiple shortcodes to enhance Hugo’s default figure capabilities.
Hugo supports figures out of the box by using the
figure shortcode or the markdown syntax. They both work well if you quickly want to add an image. The Hugo shortcode supports some customization e.g. adjust dimension and what not but we want to go further. First of all images should auto scale to the available page width, use
lazyload1 and have the ability to be shown modally. Images can come from the Page resources or external links. In addition some shortcodes are added to show images in different ways (gallery, grid, …).
This shortcode is a modified version of hugo’s default
figure shortcode (
source). Our additions include loading images from Hugo’s
page resources, use lazyloading and enabling an image overlay/gallery (see
section). The figure caption got extended as well. Overwriting the default shortcode is very simple, create a shortcode with the same name in your own shortcode directory (/layouts/shortcodes/figure.html) and you’re set.
Only the figure shortcode is overwritten, not the markdown syntax “shortcode” because I only the use the former. You can override the markdown “shortcode”, if so desired, and put it in layouts/_default/_markup/render-image.html.
Usage
All the default shortcode’s parameters are still supported and additional options were added
option
effect
optional
default
linkTxt
text which, if link is set, will be shown in the caption instead of the link itself
yes
link itself
overlay
whether or not the image can be shown modal (boolean)
yes
true
A subtile difference compared to the default however, dimensions (width and height) must be provided without unit and are assumed to be in px (support for other units can be added). The attr parameters will used set the source in the figure caption, it can be either a link or text. Images are stretched to fill the available width.
If your image comes from the page’s resources multiple resolutions are generated to have so called
responsive images (inspired by
henrik sommerfeld). If you have many images generating responsive images may have a significant performance impact when building the website as
stated by Hugo.
Below are a few examples:
{{< figure src="ganttExample.jpg" title="Gantt chart" caption="example with source and link" attr="wikidot.com" attrlink="http://jldiaz.wikidot.com/en-tikz-example:gantt" link="https://en.wikipedia.org/wiki/Gantt_chart" linkTxt="Gantt chart" >}}
{{< figure src="ganttExample.jpg" overlay="false" caption="no overlay (enabled by default)" >}}
no overlay (enabled by default)
{{< figure src="ganttExample.jpg" class="example-class-img" caption="figure with custom css class" >}}
.example-class-img {
padding-top: 1.5em;
background-color: darkorange;
text-align: center !important; // Center the img
}
.example-class-img figcaption {
color: black;
justify-content: center !important; // Center the caption
}
{{< figure src="https://upload.wikimedia.org/wikipedia/commons/e/e8/Point_de_vue_du_Gras_by_Ni%C3%A9pce%2C_1826.jpg" height="300" attr="wikipedia.com" link="https://en.wikipedia.org/wiki/View_from_the_Window_at_Le_Gras" linkTxt="View from the Window at Le Gras" >}}
Although strictly not required the
lazyload library is used to load images. At the time of writing 5.2.0.
Override the default shortcode by adding your version in `/layouts/shortcodes/figure.html`, the name of the file is important. The shortcode is quite heavily modified compared to the original, high-level this is the flow: "get" the image (as remote link or from the page resources), resize the image if required (only if it is a Page resource) and generate the responsive images if applicable (a small preview image is directly embedded in HTML as a Base64 encoded string, so that there is no extra request required to fetch the preview image), similar to the original shortcode add a `img` element with all the attributes and finally the `figcaption` if required.
If the image is a Page resource, and resized, a link to the original image in full resolution is added (as hsrc) as well which the user can open modally. The full resolution dimensions are also added because they are required upfront due to the library we use, see
below. Whether or not the image can be opened modally is determined by adding the modalImageTrigger class. All the caption information is also added as attributes because said data will be required later on (see
below).
The lazyload class is added for the
lazyload library which is used to only load the images when required and pick the appropriate responsive image (if applicable). Alteratively one could have used the default HTML
responsive image feature instead.
Don’t forget to load lazysizes at the bottom of your page, a
scratch variable was added in the figure shortcode to determine if loading the javascript library(s) is required. For example in this theme before closing the </body> tag i have the following code:
{{/* When the page contains images.
Note: will NOT work for default markdown images since we cannot add a scratch for them... not allowed in a goldmark context. */}}
{{/* Hugo server does not take scratch properly into account. */}}
{{ if or (.Scratch.Get "hasImages") $hugo.IsServer }}
{{ $thirdPartyLazysizesJS := resources.Get "js/thirdParty/lazysizes.min.js" }}
{{ $thirdPartyPhotoSwipeJS := resources.Get "js/thirdParty/photoswipe.js" }}
{{ $thirdPartyPhotoSwipeUIJS := resources.Get "js/thirdParty/photoswipe-ui-default.js" }}
{{ $gliderJS := resources.Get "js/thirdParty/glider.min.js" }}
{{ $imageSliderJS := resources.Get "js/content/image-slider.js" }}
{{ $imageGalleryJS := resources.Get "js/content/image-gallery.js" }}
{{ $imageModalJS := resources.Get "js/content/image-modal.js" }}
{{ if $hugo.IsServer }}
<scriptsrc="{{ $thirdPartyLazysizesJS.Permalink }}"></script>
<scriptsrc="{{ $thirdPartyPhotoSwipeJS.Permalink }}"></script>
<scriptsrc="{{ $thirdPartyPhotoSwipeUIJS.Permalink }}"></script>
<scriptsrc="{{ $gliderJS.Permalink }}"></script>
<scriptsrc="{{ $imageSliderJS.Permalink }}"></script>
<scriptsrc="{{ $imageGalleryJS.Permalink }}"></script>
<scriptsrc="{{ $imageModalJS.Permalink }}"></script>
{{ else }}
{{ with slice $thirdPartyLazysizesJS $thirdPartyPhotoSwipeJS $thirdPartyPhotoSwipeUIJS $gliderJS $imageSliderJS $imageGalleryJS $imageModalJS | resources.Concat "js/images.js" | minify | fingerprint }}
<scriptsrc="{{ .Permalink }}"integrity="{{ .Data.Integrity }}"></script>
{{ end }}
{{ end }}
In server builds all files are added individually to easy debugging but in a production builds everything is combined and minified in a single file to avoid multiple requests. The script files are only included if the page contains at least one image, this to improve the page’s performance if there are no images (many of these scripts execute code when the document is loaded).
Image links
Sometimes to save space (or avoid distracting the reader) you might want to link to an image but not show the image itself. To make this clear to the user the an image an icon is added ot the link. When the link is clicked the image will be shown modal (see
other section).
Usage
Image links support both page resources and external links. Roughly the same parameters as our figure shortcode are supported, irrelevant parameters like resizing or disabling modal image are left out. In addition the following options were added.
option
effect
optional
default
text
text of the link in the content
yes
This is an example on how it {{< figure-link text="looks like" src="ganttExample.jpg" caption="Example image" >}}
This is an example on how it
looks like
.
Code
The shortcode only adds an fake “link” element in the content with all required information stored in its attributes, the actual functionality of opening the image modally is implemented in Javascript (see
other section).
Image gallaries are ideal when you want to show many images that belong together. The concept is also known as carousel, slider, slideshow, … The alternative is to use a image grid, they typically take up more space though especially when using many images (+6).
Usage
The images sources can either be: external, from the Page resources or automatically taken from the a folder in the Page’s resources. The available options:
option
effect
optional
default
folder
load all images from the specified page resource folder
yes
captions
use the image name as cpation (boolean). Only applicable if folder is set
yes
true
title
title for the gallery
yes
caption
caption for the gallery
yes
overlay
whether or not the gallery images can be shown modal (boolean)
yes
true
If you do not use the folder option images have to be specified manually by using the gallery-image shortcode; supports external links and Page resources.
Examples
{{< gallery caption="An example of many images for which a grid layout would take up too much space." >}}
{{< gallery-image src="https://mdbootstrap.com/img/Photos/Horizontal/Nature/4-col/img%20(63).jpg" >}}
{{< gallery-image src="https://mdbootstrap.com/img/Photos/Horizontal/Nature/4-col/img%20(63).jpg" >}}
{{< gallery-image src="https://mdbootstrap.com/img/Photos/Horizontal/Nature/4-col/img%20(63).jpg" >}}
{{< gallery-image src="https://mdbootstrap.com/img/Photos/Horizontal/Nature/4-col/img%20(63).jpg" >}}
... x23
{{< /gallery >}}
An example of many images for which a grid layout would take up too much space.
{{< gallery folder="exampleGallery" title="Rainbow" caption="example images from a resource folder" />}}
The (fairly lightweight)
glider.js library is used to show the image gallery. At the time of writing 1.7.2.
There are 2 shortcodes (`gallery` and `gallery-image`) and a partial. The partial is required because if the gallery is generated from a folder the `gallery` shortcode will add the images (as `gallery-image` shortcode) itself; shortcodes cannot call other shortcodes. Therefore it must be able to refer to `gallery-image` so the actual code for the `gallery-image` shortcode is located in the partial and the `gallery-image` shortcode just calls the partial template with the correct arguments. This might sound more complicated than it actually is.
The gallery shortcode itself is fairly straight forward, add the images, add the controls and a caption. To automatically generate the images from a Page resource folder one can iterate through all the files in the folder and process them, this functionality is support out of the box by Hugo.
window.addEventListener("load", function () {
constgliderSettings= {
// Mobile first
slidesToShow:1,
slidesToScroll:1,
draggable:true,
duration:0.25, // Lower - better performance
dots:'#dots',
arrows: {
prev:'#glider-prev',
next:'#glider-next' },
responsive: [
{
// screens greater than >= XXX px
breakpoint:800,
settings: {
// Set to `auto` and provide item width to adjust to viewport
slidesToShow:'auto',
slidesToScroll:1,
itemWidth:150 }
}, {
breakpoint:1200,
settings: {
slidesToShow:'auto',
slidesToScroll:1,
itemWidth:350// This value should match the value in the gallery-img shortcode
}
}
]
};
document.querySelectorAll('.glider').forEach(g => {
// Patch the elements for this glider
constcontainer=g.parentElement.children[1].children;
gliderSettings.arrows.prev=container[0];
gliderSettings.dots=container[1];
gliderSettings.arrows.next=container[2];
newGlider(g, gliderSettings);
});
});
Do not forget to load Glider’s Javascript and CSS.
Further improvements
Support for custom image captions was left out and could should be added. This can be done in the gallery-image shortcode, no need to change/write Javascript code.
Image grid
Image grids can be used when one wants to show a few images that belong together and small thumbnails suffice. However if you want to show many images i would recommend using an
image gallery instead (especially for mobile users).
An advantage of image grids, they are very simple to implement and can be done with just CSS alone. Of course there are many Javascript libraries available if you want more features; or you can create your own. We go for a full CSS version however.
Images in the grid are not stretched to be the same size if they have a different aspect ratio which visually is not ideal. This can be corrected by changing the CSS rules.
Image slider
This section is a (heavily) modified version of the image comparison
tutorial, all credit goes to
www.w3schools.com.
If you want to compare 2 images an image slider is pretty neat and fortunately easy to make.
Usage
The shortcode is fairly simple, there are not many options available. Works for both external links and Page resources and will auto scale the slider to the page or image (whichever is smaller).
option
effect
optional
default
src1
source of the first image (background)
no
src2
source of the second image (overlay)
no
title
title for the slider
yes
caption
caption for the gallery
yes
height
limit the height (in px)
yes
width
limit the width (in px)
yes
overlay
whether or not the images can be shown modal (boolean)
{{ .Page.Scratch.Add "hasImages" "true" }}
{{- $width := .Get "width" -}}
{{- $height := .Get "height" -}}
{{/* First image */}}
{{- $img := .Get "src1" -}}
{{- $isRemote := strings.HasPrefix $img "http" -}}
{{- $originalImg := "" -}}
{{- $isResized := false -}}
{{- if not $isRemote -}}
{{- $img = .Page.Resources.GetMatch $img -}}
{{- if and (not $img) .Page.File -}}
{{ $path := path.Join .Page.File.Dir $img }}
{{- $img = resources.Get $path -}}
{{- end -}}
{{- $originalImg = $img -}}
{{- if or $width $height }}
{{- $isResized = true -}}
{{- if and $width $height -}}
{{- $img = ($img.Resize (print $width "x" $height)).RelPermalink -}}
{{- else if $width -}}
{{- $img = ($img.Resize (print $width "x")).RelPermalink -}}
{{- else if $height -}}
{{- $img = ($img.Resize (print "x" $height)).RelPermalink -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/* Second image */}}
{{- $img2 := .Get "src2" -}}
{{- $isRemote2 := strings.HasPrefix $img2 "http" -}}
{{- $originalImg2 := "" -}}
{{- $isResized2 := false -}}
{{- if not $isRemote2 -}}
{{- $img2 = .Page.Resources.GetMatch $img2 -}}
{{- if and (not $img2) .Page.File -}}
{{ $path := path.Join .Page.File.Dir $img2 }}
{{- $img2 = resources.Get $path -}}
{{- end -}}
{{- $originalImg2 = $img2 -}}
{{- if or $width $height }}
{{- $isResized2 = true -}}
{{- if and $width $height -}}
{{- $img2 = ($img2.Resize (print $width "x" $height)).RelPermalink -}}
{{- else if $width -}}
{{- $img2 = ($img2.Resize (print $width "x")).RelPermalink -}}
{{- else if $height -}}
{{- $img2 = ($img2.Resize (print "x" $height)).RelPermalink -}}
{{- end -}}
{{- end -}}
{{- end -}}
<divclass="img-comp-container">
<imgclass="lazyload img-comp-img img-comp-background"alt=""data-src="{{ $img }}"draggable="false"ondragstart="return false;"{{-with$width}}width="{{ . }}px"{{end-}}{{-with$height}}height="{{ . }}px"{{end-}}{{-ifand$height(not$width)}}style="height: {{ $height }}px;"{{end-}}{{-ifand(not$isRemote)$isResized}}hsrc="{{ $originalImg.RelPermalink }}"hwidth="{{ $originalImg.Width }}"hheight="{{ $originalImg.Height }}"{{end-}} />
<divclass="img-comp-img">
<imgclass="lazyload img-comp-overlay"alt=""data-src="{{ $img2 }}"draggable="false"ondragstart="return false;"{{-with$width}}width="{{ . }}px"{{end-}}{{-with$height}}height="{{ . }}px"{{end-}}{{-ifand$height(not$width)}}style="height: {{ $height }}px;"{{end-}}{{-ifand(not$isRemote2)$isResized2}}hsrc="{{ $originalImg2.RelPermalink }}"hwidth="{{ $originalImg2.Width }}"hheight="{{ $originalImg2.Height }}"{{end-}} />
</div>
<divclass="img-comp-controls">{{- if eq "true" (default "true" (.Get "overlay")) -}}<divclass="img-comp-expand"title="Expand">{{ partial "inline-svg" "expand-alt" }}</div>{{- end -}}</div>
</div>
{{- if or (.Get "title") (.Get "caption") -}}
<figcaption>
{{ with (.Get "title") -}}
<h4>{{ . }}</h4>
{{- end -}}
{{ with (.Get "caption") -}}
<p>{{ . }}</p>
{{- end -}}
</figcaption>
{{- end -}}
The Javascript code in an enhanced version of the tutorial. First add a slider element and initialize the dimensions of the control. The basic mechanism is, a static background image and an overlay image which is clipped depending on the slider’s position.
When the slider moves the clipped width of the overlay image is adjusted. If the whole control resizes, e.g. by changing the window size, all sizes are recalculated.
It can be further improved, for example when showing the image modally let the user toggle/choose between showing the images individually (as is currently implemented at the time of writing) or show the slider itself in full screen (or the image’s native resolution).
Modal image
To allow the user to see images in high detail, and save space by showing smaller version in the content, a modal image viewer is nice to have. There are plenty Javascript libraries available or you can make your own version if desired. If you do not want many features a pure CSS modal image viewer will do just fine. I went for an existing Javascript library however to save time and have many features.
Usage
Showing an image modally is entirely handled by Javascript. Except when explicitly disabled all figures on this page should be able to expand modally.
Code
The (fairly heavy)
PhotoSwipe library is used to show the image modally. At the time of writing 4.1.3.
There is no particular reason as to why this particular library was picked over others, it supports all features i desired, is robust and has good mobile support; ticks all boxes. Unfortunately it is rather big and requires quite a bit of CSS and additional Javascript.
To use the library one has to create a UI for it in HTML, there is a default UI available. You have to provide the trigger elements to open PhotoSwipe yourself, in our case implemented by the various shortcodes mentioned in this post. The triggers are wired to open the PhotoSwipe library through Javascript. PhotoSwipe has some quirks, the image with and height has to be known before the library is initialized. This is quit annoying if the image is not already loaded before opening it modally. However it will load images lazily and supports initial thumbnail placeholders.
The Javascript first gets the overlay element and configures the settings for PhotoSwipe which will also generate the HTML for the modal image caption. Next, for all element which should trigger modal image(s), wire up the onclick event to create a new PhotoSwipe instance. To pass the image information to PhotoSwipe the attributes of the HTML element are converted to the desired format. Passing the image dimensions for images from the Page resources is not a problem, the dimensions are added as attributes in HTML by Hugo. For external images the dimensions are taken from the already loaded image element itself. Only for the image link shortcode (with external links) the image dimensions are unknown. Therefore those images they have to be pre-fetched before opening PhotoSwipe so we can pass along the image dimensions. PhotoSwipe ironically supports lazy loading images. Other solutions to get the image dimensions exist but this solution works well, is easy to implement and has a low overhead. Some shortcodes downscale images, to pass the high resolution version to PhotoSwipe the high resolution source is used instead (hsrc attribute). The downscaled src image is used as preview image until PhotoSwipe has lazy loaded the high resolution version.
functiongetImgDetails(img) {
// High definition source
varhsrc=img.getAttribute("hsrc");
return {
src:hsrc?hsrc:img.src,
msrc:hsrc?img.src:"",
w:hsrc?img.getAttribute("hwidth") :img.naturalWidth,
h:hsrc?img.getAttribute("hheight") :img.naturalHeight,
};
}
functiongetImgElementDetails(element) {
return {
title:"0", // Dummy value; required otherwise PhotoGallery hides the caption. One could leave this empty if no meta-data is set so the caption bar is hidden.
heading:element.getAttribute("imgTitle"),
caption:element.getAttribute("imgCaption"),
attr:element.getAttribute("attr"),
attrLink:element.getAttribute("attrlink"),
linkTxt:element.getAttribute("linkTxt"),
link:element.getAttribute("linkhref"),
linktarget:element.getAttribute("linktarget"),
linkrel:element.getAttribute("linkrel"),
};
}
window.addEventListener("load", function () {
constpswpElement= document.getElementById('pswp');
constpswpSettings= {
// Core
index:0,
bgOpacity:0.8,
history:false,
hideAnimationDuration:0, // For better performance
showAnimationDuration:0,
// UI
indexIndicatorSep:' | ',
shareButtons: [{ id:'download', label:'Download image', url:'{{raw_image_url}}', download:true }],
addCaptionHTMLFn:function (item, captionEl, isFake) {
lethtmlString='';
if (item.heading) {
htmlString+='<strong>'+item.heading+'</strong>';
}
if (item.caption) {
htmlString+= ((htmlString.length===0) ?'':' ') +'<small>'+item.caption+'</small>';
}
if (item.attr) {
if (item.attrLink) {
htmlString+='<br/><small>Source: <a href="'+item.attrLink+'" target="_blank">'+item.attr+'</a></small>';
} else {
htmlString+='<br/><small>Source: '+item.attr+'</small>';
}
}
if (item.link) {
consthref='href="'+item.link+'"';
consttarget=' target="'+item.linktarget+'"';
constrel=item.linkrel?' rel="'+item.linkrel+'"':'';
htmlString+='<br/><small>Link: <a '+href+target+rel+'>'+ (item.linkTxt?item.linkTxt:item.link) +'</a></small>';
}
captionEl.children[0].innerHTML=htmlString;
returnhtmlString.length!==0; // Whether or not there is a caption
}
};
// Figure
// ------
document.querySelectorAll(".modalImageTrigger").forEach(i => {
i.onclick= () => {
// Images are guaranteed to be loaded at this point
newPhotoSwipe(pswpElement, PhotoSwipeUI_Default, [{ ...getImgDetails(i), ...getImgElementDetails(i) }], pswpSettings).init();
};
});
// Image link
// ----------
document.querySelectorAll(".modalImageTxtTrigger").forEach(i => {
i.onclick= () => {
// The image is, probably, not loaded yet.
// That is unfortunate given https://github.com/dimsemenov/PhotoSwipe/issues/741,
// this results in PhotoSwipe not lazy loading the img and thus it might be slower to start.
varimgSrc=newImage();
imgSrc.onload= () => {
newPhotoSwipe(pswpElement, PhotoSwipeUI_Default, [{ ...getImgDetails(imgSrc), ...getImgElementDetails(i) }], pswpSettings).init()
};
imgSrc.src=i.getAttribute("src");
};
});
// Image slider
// ------------
document.querySelectorAll(".img-comp-expand").forEach(s => {
s.onclick= () => {
// The images are guaranteed to be loaded
constimgs=s.parentNode.parentNode.querySelectorAll("img");
// Images are guaranteed to be loaded at this point
newPhotoSwipe(pswpElement, PhotoSwipeUI_Default, [{ ...getImgDetails(imgs[0]), ...getImgElementDetails(imgs[0]) }, { ...getImgDetails(imgs[1]), ...getImgElementDetails(imgs[1]) }], pswpSettings).init();
};
});
// Gallery (grid and glider)
// -------
document.querySelectorAll(".modalImageGalleryTrigger").forEach(g => {
constimqs=g.querySelectorAll("img");
constimages= [];
imqs.forEach(i =>
// Images are guaranteed to be loaded at this point (glider/grid does not lazy load by default)
images.push({ ...getImgDetails(i), ...getImgElementDetails(i) })
);
imqs.forEach((i, index) =>
i.onclick= () => {
pswpSettings.index=index;
newPhotoSwipe(pswpElement, PhotoSwipeUI_Default, images, pswpSettings).init();
pswpSettings.index=0; // Since Javascript has no out of the box deep copy we just reset it afterwards
}
);
});
});
For the overlay UI we use the default UI provided by PhotoSwipe itself.
In addition you should also load PhotoSwipe’s, Javascript, (S)CSS and images.
further improvements
When the image resolution is higher than the screen resolution we allow the user to zoom (and pan). It might be useful to always allow to zoom regardless of the original image size. This is in
principle supported by PhotoSwipe.
If you do not want to load a fairly heavy Javascript library (+ accompanying CSS) like PhotoSwipe just to show an image modally you can use this really lightweight solution instead. It does not have a gallery feature however.
This section is a modified version of the modal image
tutorial, all credit goes to
www.w3schools.com.
The code is not listed here but the files are included, it is rather self explanatory.
lazyload will only fetch images if they are/will become visible, it saves bandwidth in case the user does not go through the whole page and makes the page initially faster to load. ↩︎
in Hugo shortcodes and partial templates have a different way of passing parameters so we have to do a conversion. ↩︎
License: The text and content is licensed under CC BY-NC-SA 4.0.
All source code I wrote on this page is licensed under The Unlicense; do as you please, I'm not liable nor provide warranty.
Noticed an error in this post? Corrections are
spam.appreciated.