I just want to be able to share a link to several images at once.
5 Answers
The most simple way would be transforming file listings of nginx. You can do that by making nginx output listings as XML and then transform them using XSLT. Built-in module ngx_http_autoindex_module
will do the former and usually dynamic module ngx_http_xslt_filter_module
(aka ngx_http_xslt_module
) will do the latter.
First, load the module in nginx.conf
if needed:
load_module "/usr/lib/nginx/modules/ngx_http_xslt_filter_module.so";
Then, in your sites-available/website.com
, add a location that tells nginx to transform the xml index using stlylesheet gal.xslt
and pass a the name of the folder as a parameter.
location ~ /gal/([A-z]+)/$ {
autoindex on;
autoindex_format xml;
xslt_string_param title $1;
xslt_stylesheet gal.xslt;
try_files $uri/ =404;
}
Finally, create gal.xslt
in /etc/nginx
that says,
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="utf-8" indent="yes" />
<xsl:template match="/">
<xsl:text disable-output-escaping='yes'><!DOCTYPE html></xsl:text>
<html>
<head>
<title><xsl:value-of select="$title" /></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
img, video {
display: block;
max-width: 20cm;
max-height: 20cm;
margin: 2mm;
vertical-align: bottom;
image-orientation: from-image;
}
@media all and (max-width: 20.4cm) {
img {
max-width: calc(100% - 4mm);
}
}
body {
margin: 0;
}
</style>
</head>
<body>
<xsl:for-each select="list/file">
<xsl:choose>
<xsl:when test="contains(' mp4 webm mkv avi wmv flv ogv ', concat(' ', substring-after(., '.'), ' '))">
<video controls="" src="{.}" alt="{.}" title="{.}"/>
</xsl:when>
<xsl:otherwise>
<img src="{.}" alt="{.}" title="{.}"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Now put some images into /var/www/html/gal/foo
, restart nginx, navigate to website.com/gal/foo
and you will see a simple but usable and responsive image gallery.

- 5,114
- 4
- 31
- 43
-
This is purely amazing! Thank you. – gerasalus May 16 '17 at 08:42
-
Beautiful and simple! the images were getting a 404 status code. I had to change `try_files $uri/ =404;` to `try_files $uri $uri/ =404;` on Nginx 1.19 – Lee Irvine Sep 21 '20 at 05:05
Thanks @squirrel that's really simply yet powerful.
I've tweaked the xslt - this version gal.xslt below:
- Displays tumbnails in a grid, four columns wide.
- Resizes according to page size.
Each image is click-able, so you can see the full size image.
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" encoding="utf-8" indent="yes" /> <xsl:template match="/"> <xsl:text disable-output-escaping='yes'><!DOCTYPE html></xsl:text> <html> <head> <title><xsl:value-of select="$title" /></title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <style> img { display: inline; width: 23%; margin: 2mm; vertical-align: bottom; } @media all and (max-width: 20.4cm) { img { max-width: calc(100% - 4mm); } } body { margin: 0; } </style> </head> <body> <xsl:for-each select="list/file"> <a href="{.}" title="click to enlarge"> <img src="{.}" alt="{.}"/> </a> </xsl:for-each> </body> </html> </xsl:template> </xsl:stylesheet>
Cheers,
Greg

- 71
- 1
- 4
-
1Nice job, next step is to mix in the image filter module to generate and cache the thumbnails server side ;) – miknik Aug 28 '18 at 23:04
I tweaked a bit more: fancybox, thumbnail handling (with pregenerated thumbnails in the thumbs directory), ability to download all images as a browser generated zip file.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="utf-8" indent="yes" />
<xsl:template match="/">
<xsl:text disable-output-escaping='yes'><!DOCTYPE html></xsl:text>
<html>
<head>
<title><xsl:value-of select="$title" /></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css" integrity="sha256-Vzbj7sDDS/woiFS3uNKo8eIuni59rjyNGtXfstRzStA=" crossorigin="anonymous"/>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js" integrity="sha256-yt2kYMy0w8AbtF89WXb2P1rfjcP/HTHLT7097U8Y5b8=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.1.5/dist/jszip.min.js" integrity="sha256-PZ/OvdXxEW1u3nuTAUCSjd4lyaoJ3UJpv/X11x2Gi5c=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js" integrity="sha256-xoh0y6ov0WULfXcLMoaA6nZfszdgI8w2CEJ/3k8NBIE=" crossorigin="anonymous"></script>
<style>img { display: block; }</style>
</head>
<body>
<h1 style="text-align: center;"><xsl:value-of select="$title"/></h1>
<div style="border-bottom: 1px solid gray; margin-bottom: 1rem;"></div>
<div style="display: flex; flex-wrap: wrap; gap: 2px; justify-content: center;">
<xsl:for-each select="list/file">
<a href="{.}" data-fancybox="gallery">
<xsl:choose>
<xsl:when test="count(/list/directory[text() = 'thumbs'])">
<img loading="lazy" src="thumbs/{.}"/>
</xsl:when>
<xsl:otherwise>
<img loading="lazy" src="{.}" height="200"/>
</xsl:otherwise>
</xsl:choose>
</a>
</xsl:for-each>
</div>
<script>
async function downloadAll() {
const zip = JSZip();
const folder = zip.folder('<xsl:value-of select="$title" />');
const files = [
<xsl:for-each select="list/file">
'<xsl:value-of select="." />',
</xsl:for-each>
];
for(const i in files) {
const file = files[i];
const resp = await fetch(file);
folder.file(file, resp.blob());
$.fancybox.animate($.fancybox.getInstance().SlideShow.$progress.show(),{scaleX: i/files.length}, 0.1);
}
const zipFile = await zip.generateAsync({type: 'blob'});
saveAs(zipFile, '<xsl:value-of select="$title" />' + '.zip');
$.fancybox.animate($.fancybox.getInstance().SlideShow.$progress.show(),{scaleX: 0}, 0.1);
}
$('[data-fancybox="gallery"]').fancybox({
buttons: [
"zoom",
"slideShow",
"fullScreen",
"download",
"downloadAll",
"close"
],
btnTpl: {
downloadAll:
'<a class="fancybox-button fancybox-button--download" title="Download All" href="javascript:downloadAll()">' +
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" style="fill:unset; stroke-width:2"/></svg>' +
'</a>',
},
});
$('[data-fancybox="gallery"]')[0].click();
</script>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

- 316
- 2
- 10
I tweaked also a bit more :-) and prepare generated configuration with the JS and CS. Main features are:
- images...
- reading EXIF (via exifr)
- show GPS on the Google Maps
- view photo sphere panoramas (via Photo Sphere Viewer)
- an optional automatic image thumbnails with an optional cache
- videos (via Plyr) with optional auto conversion SRT subtitles to supported VTT
- other files (just view and download)
- sharing link to the gallery
- sharing on social networks (via shareon)
- as lightbox is used GLightbox
- HTTP or HTTPS (Let's encrypt support - via external tools - dehyhrated for example)
- custom gallery title/favicon and many more settings
- password access (via HTTP Basic Auth)
You can check it on https://github.com/forrest79/StaticNginxGallery.

- 56
- 4
@squirrel's fantastic answer inspired me to bundle this all into a container for easy deployment. https://github.com/stuart23/image_server
Instructions are in the readme - it is as simple as:
docker build -t image_server .
docker run -p 8000:8000 -v /local/path/to/gallery:/var/www image_server

- 1,574
- 16
- 25