Hugo Scroll theme has no gallery shortcode. Some alternatives are discussed in #70. We will pick the gallery-slider from gethugothemes/hugo-modules.

This post is quite short. Index:

Installation

Follow the steps of gethugothemes/hugo-modules.

Modules

$EDITOR hugo.toml. Add the gallery-slider module:

[[module.imports]]
  path = "github.com/gethugothemes/hugo-modules/gallery-slider"
  disable = false

For example, a Hugo Scroll theme user needs:

[module]
  proxy = "direct"
  [module.hugoVersion]
    extended = true
    min = "0.132.0"
  [[module.imports]]
    path = "github.com/zjedi/hugo-scroll"
    disable = false
  [[module.imports]]
    path = "github.com/gethugothemes/hugo-modules/gallery-slider"
    disable = false

Note. The Hugo Scroll theme installation via Hugo modules was explained here.

Then run:

hugo mod get github.com/gethugothemes/hugo-modules/gallery-slider@latest

Check it with hugo mod graph.

> hugo mod graph
project github.com/zjedi/hugo-scroll@v0.0.0-20250604223730-54f7b8543f18+vendor
project github.com/gethugothemes/hugo-modules/gallery-slider@v0.0.0-20250702070945-cd8319c6b26e+vendor

Optionally run hugo mod vendor to create a local copy:

> tree _vendor/github.com/gethugothemes/hugo-modules/gallery-slider/
_vendor/github.com/gethugothemes/hugo-modules/gallery-slider/
├── assets
│   ├── css
│   │   └── gallery-slider.css
│   ├── js
│   │   └── gallery-slider.js
│   ├── plugins
│   │   ├── glightbox
│   │   │   ├── glightbox.css
│   │   │   └── glightbox.js
│   │   └── swiper
│   │       ├── swiper-bundle.css
│   │       └── swiper-bundle.js
│   └── scss
│       └── gallery-slider.scss
└── layouts
    ├── partials
    │   ├── gallery.html
    │   ├── image-pipe.html
    │   └── slider.html
    └── shortcodes
        ├── gallery.html
        └── slider.html

Plugins

$EDITOR hugo.toml and add

[[params.plugins.css]]
link = "plugins/swiper/swiper-bundle.min.css"
[[params.plugins.css]]
link = "plugins/glightbox/glightbox.min.css"
[[params.plugins.js]]
link = "plugins/swiper/swiper-bundle.min.js"
[[params.plugins.js]]
link = "plugins/glightbox/glightbox.min.js"
[[params.plugins.js]]
link = "js/gallery-slider.js"

As #16 points out, just previous steps are not enough:

I didn’t get glightbox.js or glightbox.css files in static folder of my project

The guide misses next step:

I managed to figure out how to implement. All resources should be processed with a hugo pipeline.

Lets do it. $EDITOR layouts/partials/custom_head.html to append:

{{ $swiperCSS := resources.Get "plugins/swiper/swiper-bundle.css" }}
{{ $glightboxCSS := resources.Get "plugins/glightbox/glightbox.css" }}
{{ $gallerySliderCSS := resources.Get "css/gallery-slider.css" }}
{{ $swiperJS := resources.Get "plugins/swiper/swiper-bundle.js" }}
{{ $glightboxJS := resources.Get "plugins/glightbox/glightbox.js" }}
{{ $gallerySliderJS := resources.Get "js/gallery-slider.js" }}

{{ if $swiperCSS }}<link rel="stylesheet" href="{{ $swiperCSS.RelPermalink }}">{{ end }}
{{ if $glightboxCSS }}<link rel="stylesheet" href="{{ $glightboxCSS.RelPermalink }}">{{ end }}
{{ if $gallerySliderCSS }}<link rel="stylesheet" href="{{ $gallerySliderCSS.RelPermalink }}">{{ end }}

{{ if $swiperJS }}<script src="{{ $swiperJS.RelPermalink }}"></script>{{ end }}
{{ if $glightboxJS }}<script src="{{ $glightboxJS.RelPermalink }}"></script>{{ end }}
{{ if $gallerySliderJS }}<script src="{{ $gallerySliderJS.RelPermalink }}"></script>{{ end }}

Actually also enable a initializer:

{{ $gallerySliderInitJS := resources.Get "js/gallery-slider-init.js" | minify | fingerprint }}
<script src="{{ $gallerySliderInitJS.RelPermalink }}" defer></script>

$EDITOR assets/js/gallery-slider-init.js:

// Wait for the page to fully load and content to be rendered
function initializeSliders() {
  const sliders = document.querySelectorAll('.gallery-slider');
  console.log('Found sliders to initialize:', sliders.length);
  
  sliders.forEach(slider => {
    // Skip if already initialized
    if (slider.swiper) {
      console.log('Slider already initialized');
      return;
    }
    
    // Initialize Swiper
    try {
      const swiper = new Swiper(slider, {
        slidesPerView: 1,
        loop: true,
        spaceBetween: 30,
        navigation: {
          nextEl: slider.querySelector('.swiper-button-next'),
          prevEl: slider.querySelector('.swiper-button-prev'),
        },
        on: {
          init: function() {
            console.log('Swiper initialized successfully!');
          }
        }
      });
    } catch (error) {
      console.error('Swiper initialization error:', error);
    }
  });
}

// Initialize after a short delay to ensure DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', function() {
    // Small delay to ensure shortcodes are rendered
    setTimeout(initializeSliders, 100);
  });
} else {
  // DOM already loaded, but wait for shortcodes
  setTimeout(initializeSliders, 100);
}

// Also try after all resources are loaded
window.addEventListener('load', function() {
  setTimeout(initializeSliders, 200);
});

// MutationObserver as fallback for dynamically added content
if (typeof MutationObserver !== 'undefined') {
  const observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.addedNodes.length) {
        // Check if any new nodes contain sliders
        const hasNewSliders = Array.from(mutation.addedNodes).some(node => {
          return node.nodeType === 1 && (
            node.classList?.contains('gallery-slider') || 
            node.querySelector?.('.gallery-slider')
          );
        });
        if (hasNewSliders) {
          setTimeout(initializeSliders, 50);
        }
      }
    });
  });
  
  observer.observe(document.body, {
    childList: true,
    subtree: true
  });
}

Usage

In a markdown file use the slider shortcode via {{< slider dir="images/...." >}}:

For example my Instagram mosaic generator post has next images. Move your cursor inside the picture to see the right arrow, then click it.

More parameters can be passed. Check:

The arrows to slide might need customization. The white color can be confused with the background of the picture.

In layouts/partials/custom_head.html we could edit the style, since the arrow color is defined via a SCSS variable:

:root {
  [...]
  --swiper-navigation-color: #246B9F;
}

Though this has no effect. Inspecting in the browser we check that the color: var(--swiper-navigation-color, var(--swiper-theme-color)) field-value of .swiper-button-next is striked. I.e. this SCSS is overridden by a more specific rule.

In that same HTML file add in <style>:

.gallery-slider .swiper-button-next::after,
.gallery-slider .swiper-button-prev::after {
  color: #ffffff;
  background-color: #246B9F;
  border-radius: 100%;
}

Online images

To use non-local images create a new shortcode.

$EDITOR layouts/shortcodes/slider-external.html with code:

{{ $loading := .Get "loading" | default "lazy" }}
{{ $zoomable := .Get "zoomable" | default "true" }}

<div class="swiper gallery-slider">
  <div class="swiper-wrapper">
    {{ range $index := seq 1 20 }}
      {{ $imageParam := printf "image%d" $index }}
      {{ $imageUrl := $.Get $imageParam }}
      {{ if $imageUrl }}
        {{ $altParam := printf "alt%d" $index }}
        {{ $imageAlt := $.Get $altParam | default (printf "Image %d" $index) }}
        
        <div class="swiper-slide {{ if eq $zoomable `true` }}zoomable{{ end }}">
          {{ if eq $zoomable `true` }}
            <a href="{{ $imageUrl }}" class="glightbox" style="display: block;">
              <img
                loading="{{ $loading }}"
                src="{{ $imageUrl }}"
                class="img"
                style="[...]"
                alt="{{ $imageAlt }}" />
            </a>
          {{ else }}
            <img
              loading="{{ $loading }}"
              src="{{ $imageUrl }}"
              class="img"
              style="[...]"
              alt="{{ $imageAlt }}" />
          {{ end }}
        </div>
      {{ end }}
    {{ end }}
  </div>
  <span class="swiper-button-prev"></span>
  <span class="swiper-button-next"></span>
</div

Example usage:

{{< slider-external 
    image1="https://hugocodex.org/uploads/slider/image1.jpg"
    image2="https://hugocodex.org/uploads/slider/image2.jpg"
    >}}

Which renders to:

Btw. This images are courtesy of https://hugocodex.org/add-ons/slider-carousel/, another alternative suggested in #70.

JSON format

Previous shortcode did not accept the link to redirect to, nor the alt-ernative text if the picture is not found.

With layouts/shortcodes/slider-external-json.html we add those features.

{{ $loading := .Get "loading" | default "lazy" }}
{{ $zoomable := .Get "zoomable" | default "true" }}

{{ $jsonData := .Inner | transform.Unmarshal }}
{{ if $jsonData }}
<div class="swiper gallery-slider">
  <div class="swiper-wrapper">
    {{ range $index, $item := $jsonData }}
      {{ if $item.src }}
        {{ $href := $item.href | default $item.src }}
        {{ $alt := $item.alt | default $item.src }}
        
        <div class="swiper-slide {{ if eq $zoomable `true` }}zoomable{{ end }}">
          {{ if eq $zoomable `true` }}
            <a href="{{ $href }}" class="glightbox" style="display: block;">
              <img
                loading="{{ $loading }}"
                src="{{ $item.src }}"
                class="img"
                style="[...]"
                alt="{{ $alt }}" />
            </a>
          {{ else }}
            <img
              loading="{{ $loading }}"
              src="{{ $item.src }}"
              class="img"
              style="[...]"
              alt="{{ $alt }}" />
          {{ end }}
        </div>
      {{ else }}
        {{ errorf "Swiper gallery item %d is missing 'src' field" $index }}
      {{ end }}
    {{ end }}
  </div>
  <span class="swiper-button-prev"></span>
  <span class="swiper-button-next"></span>
</div>
{{ else }}
  {{ errorf "Invalid JSON in swiper_gallery shortcode: %s" .Inner }}
{{ end }}

Example usage:

  • First and second image with all three fields explicitly set.
  • Third image lacks alt field. Thus, it defaults to src field-value.
  • Forth JSON object is missing href field. Therefore, it falls to src.
  • Fifth picture contains only src. So rest of fields are default to the src value.
  • The last image is called still with a valid JSON, it’s like the previous picture but with a src not to be found (invalid URL) to test the alt display render directly (no need to inspect the HTML code rendered).
{{< slider-external-json loading="lazy" >}}
[
  {
    "src": "https://hugocodex.org/uploads/slider/image1.jpg",
    "href": "https://hugocodex.org/add-ons/slider-carousel/",
    "alt": "Hugo Codex Slider/Carousel image 1"
  },
  {
    "src": "https://hugocodex.org/uploads/slider/image2.jpg",
    "href": "https://hugocodex.org/add-ons/slider-carousel/",
    "alt": "Hugo Codex Slider/Carousel image 2"
  },
  {
    "src": "https://images.pexels.com/photos/1108099/pexels-photo-1108099.jpeg",
    "href": "https://www.pexels.com/photo/two-yellow-labrador-retriever-puppies-1108099/"
  },
  {
    "src": "https://images.pexels.com/photos/406014/pexels-photo-406014.jpeg",
    "alt": "Closeup Photo of Brown and Black Dog Face"
  },
  {
    "src": "https://images.pexels.com/photos/7210754/pexels-photo-7210754.jpeg"
  },
  {
    "src": "URL_no_valid_for_testing"
  }
]
{{< /slider-external-json >}}

Which renders to next HTML. Note the data-swiper-slide-index starts in 5, then 0 to 5, and ends in 0; do not overthink it, somehow this is how the slider JavaScript works.

 1<div class="swiper gallery-slider swiper-initialized swiper-horizontal swiper-pointer-events swiper-backface-hidden">
 2  <div class="swiper-wrapper" [...]>
 3    <div class="swiper-slide zoomable swiper-slide-duplicate swiper-slide-prev"
 4         data-swiper-slide-index="5" [...]>
 5      <a href="URL_no_valid_for_testing" [...]>
 6        <img loading="lazy"
 7             class="img" style="[...]"
 8             src="URL_no_valid_for_testing"
 9             alt="URL_no_valid_for_testing">
10      </a>
11    </div>
12    <div class="swiper-slide zoomable swiper-slide-active"
13         data-swiper-slide-index="0" [...]>
14      <a href="https://hugocodex.org/add-ons/slider-carousel/" [...]>
15        <img loading="lazy"
16             class="img" style="[...]"
17             src="https://hugocodex.org/uploads/slider/image1.jpg"
18             alt="Hugo Codex Slider/Carousel image 1">
19      </a>
20    </div>
21    <div class="swiper-slide zoomable swiper-slide-next"
22         data-swiper-slide-index="1" [...]>
23      <a href="https://hugocodex.org/add-ons/slider-carousel/" [...]>
24        <img loading="lazy"
25             class="img" style="[...]"
26             src="https://hugocodex.org/uploads/slider/image2.jpg"
27             alt="Hugo Codex Slider/Carousel image 2">
28      </a>
29    </div>
30    <div class="swiper-slide zoomable"
31         data-swiper-slide-index="2" [...]>
32      <a href="https://www.pexels.com/photo/two-yellow-labrador-retriever-puppies-1108099/" [...]>
33        <img loading="lazy"
34             class="img" style="[...]"
35             src="https://images.pexels.com/photos/1108099/pexels-photo-1108099.jpeg"
36             alt="https://images.pexels.com/photos/1108099/pexels-photo-1108099.jpeg">
37      </a>
38    </div>
39    <div class="swiper-slide zoomable"
40         data-swiper-slide-index="3" [...]>
41      <a href="https://images.pexels.com/photos/406014/pexels-photo-406014.jpeg" [...]>
42        <img loading="lazy"
43             class="img" style="[...]"
44             src="https://images.pexels.com/photos/406014/pexels-photo-406014.jpeg"
45             alt="Closeup Photo of Brown and Black Dog Face">
46      </a>
47    </div>
48    <div class="swiper-slide zoomable"
49         data-swiper-slide-index="4" [...]>
50      <a href="https://images.pexels.com/photos/7210754/pexels-photo-7210754.jpeg" [...]>
51        <img loading="lazy"
52             class="img" style="[...]"
53             src="https://images.pexels.com/photos/7210754/pexels-photo-7210754.jpeg"
54             alt="https://images.pexels.com/photos/7210754/pexels-photo-7210754.jpeg">
55      </a>
56    </div>
57    <div class="swiper-slide zoomable swiper-slide-duplicate-prev"
58         data-swiper-slide-index="5" [...]>
59      <a href="URL_no_valid_for_testing" [...]>
60        <img loading="lazy"
61             class="img" style="[...]"
62             src="URL_no_valid_for_testing"
63             alt="URL_no_valid_for_testing">
64      </a>
65    </div>
66    <div class="swiper-slide zoomable swiper-slide-duplicate swiper-slide-duplicate-active"
67         data-swiper-slide-index="0"[...]>
68      <a href="https://hugocodex.org/add-ons/slider-carousel/" [...]>
69        <img loading="lazy"
70             class="img" style="[...]"
71             src="https://hugocodex.org/uploads/slider/image1.jpg"
72             alt="Hugo Codex Slider/Carousel image 1">
73      </a>
74    </div>
75  </div>
76  <span class="swiper-button-prev" [...]></span>
77  <span class="swiper-button-next" [...]></span>
78  <span class="swiper-notification" [...]></span>
79</div>

Test the hrefs and alts directly in: