ExposΓ© is a high-performance Bash script that helps photographers generate beautiful static websites to showcase their images. The result is a lightning-fast static website that focuses on displaying images without distracting elements or JavaScript dependencies.
Here are some examples of websites that use this script:
- https://kirkone.github.io/Expose/ (current example site)
- https://photo.kirk.one/ (my personal website)
- Intelligent EXIF Caching: Only processes changed images, dramatically reducing build times
- Batch Processing: Optimized for large collections (1000+ images)
- Template Optimization: Batch template processing reduces build time by 90%
- Skip Images Mode: HTML-only builds in seconds for quick previews
- 1100 images (16GB): ~5 minutes initial, ~3 seconds subsequent builds
- 27 images: 8 seconds initial, 3 seconds with cache
- Template changes: Seconds instead of minutes
- Zero Dependencies: Pure Bash script with standard Unix tools
- Static Website: No web server or database required
- OneDrive Integration: Automatic sync of images and markdown files from shared OneDrive folders
- Gallery Content: Rich text introductions for galleries using Markdown (
content.mdfiles) - Mustache-Light Templates: Conditional sections for flexible template design
- EXIF Integration: Automatic camera settings display (F-stop, ISO, focal length, etc.)
- Responsive Design: Mobile-friendly image galleries
- Hierarchical Navigation: Intelligent nested folder structure with mixed content support
- Smart Folder Types: Automatic detection of gallery folders vs. structural folders
- Markdown Support: Rich text descriptions for images and galleries
- Smart Caching: File modification detection for incremental builds
- Custom Themes: Fully customizable HTML templates
- Unix-based system (Linux, macOS)
- Bash 4.0+
- VIPS (libvips-tools) for ultra-fast image processing
- ExifTool (for EXIF data extraction)
- rsync (for file synchronization)
- jq (for JSON processing)
- bc (for calculations)
- Perl (for Markdown parsing)
Run the setup script to install all required dependencies:
./setup.shThe setup script will:
- Install VIPS (libvips-tools) for blazing-fast image processing
- Install ExifTool for EXIF metadata extraction
- Install all supporting tools (rsync, jq, bc, perl)
- Display installed versions for verification
The script will automatically check for missing dependencies when you run it. If any tools are missing, you'll see a helpful message:
β Missing required dependencies: vips perl bc
Please run the setup script to install all dependencies:
./setup.sh
# Generate the website
./expose.sh -p example.site
# Deploy to your web server
./deploy.sh -p example.site# First, sync images from OneDrive
./onedrive.sh -p example.site
# Then generate the website
./expose.sh -p example.site
# Deploy to your web server
./deploy.sh -p example.site./expose.sh -s -p example.site./expose.sh -c -p example.site# First-time setup (interactive)
./deploy.sh -p example.site
# Subsequent deployments (uses saved config)
./deploy.sh -p example.site
# Build and deploy in one command
./expose.sh -p example.site && ./deploy.sh -p example.siteπ Complete deployment guide: Deployment Documentation (DEPLOYMENT.md)
./expose.sh [options]
-p PROJECT Specify project folder (default: first in ./projects)
-s, --skip-images Skip image encoding (HTML only, much faster)
-c, --disable-cache Disable HTML cache (use after navigation/template changes)
-d Draft mode (single low resolution, deprecated)
-h, --help Show help messageprojects/
βββ example/
β βββ project.config # Project configuration
β βββ site.config # Site-wide content settings (optional)
β βββ input/ # Source images folder
β βββ image1.jpg # Root gallery images (Homepage)
β βββ image2.jpg
β βββ 01 Events/ # π Column 1 (navigation section)
β β βββ Fireworks/ # π Gallery folder
β β β βββ photo1.jpg
β β β βββ photo2.jpg
β β βββ Racing/ # π Gallery folder
β β βββ race1.jpg
β β βββ race2.jpg
β βββ 02 Miscellaneous/ # π Column 2 (navigation section)
β β βββ Branch 1/ # ποΈ Structure folder (no own images)
β β β βββ Leaf 1/ # π Gallery folder
β β β βββ img1.jpg
β β β βββ img2.jpg
β β βββ Gallery 1/ # π Gallery folder
β β β βββ photo1.jpg
β β β βββ IMG_001.txt # Image description (optional)
β β βββ Gallery 2/ # π Gallery folder
β β β βββ photo2.jpg
β β βββ Mixed/ # πποΈ Mixed folder (images + subfolders)
β β βββ mixed1.jpg # Own images
β β βββ Leaf 2/ # π Subfolder gallery
β β βββ sub1.jpg
β βββ 03 Pages/ # π Column 3 (navigation section)
β βββ About/ # π Gallery folder
β β βββ portrait.jpg
β βββ Gear/ # π Gallery folder
β βββ camera.jpg
output/
βββ example/ # Generated static website
βββ index.html # Root gallery (Homepage)
βββ events/ # Column URLs included!
β βββ fireworks/
β β βββ index.html # Fireworks gallery
β βββ racing/
β βββ index.html # Racing gallery
βββ miscellaneous/ # Column URLs included!
β βββ branch-1/
β β βββ leaf-1/
β β βββ index.html # Leaf 1 gallery
β βββ gallery-1/
β β βββ index.html # Gallery 1
β βββ gallery-2/
β β βββ index.html # Gallery 2
β βββ mixed/
β βββ index.html # Mixed gallery
β βββ leaf-2/
β βββ index.html # Leaf 2 gallery
βββ pages/ # Column URLs included!
β βββ about/
β β βββ index.html # About page
β βββ gear/
β βββ index.html # Gear page
βββ main.css # Theme resources
Important: Depth 1 folders (01 Events, 02 Miscellaneous, 03 Pages) become navigation columns and are part of the URL structure!
Expose uses two configuration files:
Located at projects/<project>/project.config:
# Structure Settings
root_gallery_name="Home"
show_home_in_nav="false"
# Theme
theme="default"
# Sorting
folder_sort_direction="desc" # or "asc"
image_sort_direction="desc"
# Image Processing
jpeg_quality=90
# Infrastructure (optional)
SHARE_URL="https://..." # OneDrive sync
REMOTE_HOST="example.com" # DeploymentPurpose: Controls how the site is built (structure, theme, sorting, quality, deployment).
Located at projects/<project>/site.config:
# Site Content Settings
site_title: My Photography
site_description: A portfolio showcasing my best work
site_author: John Doe
site_keywords: photography, portfolio, landscape, portrait
site_copyright: Β© 2025 Your Name
nav_title: PortfolioPurpose: Defines what is displayed (titles, copyright, labels). These values are available in all templates.
Separation: Config = Technical, Metadata = Content
# Image resolutions required by this theme
resolution=(640 1024 1280 1920 2560 3840)ExposΓ© supports intelligent folder sorting with numeric prefixes:
input/
βββ 01 Moon/ # Displays as "Moon" in navigation
βββ 02 Wood/ # Displays as "Wood" in navigation
βββ 03 Car/ # Displays as "Car" in navigation
- Numeric prefixes (like
01,02,99) are automatically stripped from display names - Sort order follows the numeric prefix, not alphabetical
- Mixed folders work perfectly:
01 Gallery,02 Structure/Subfolder,03 Mixed - Configurable sort direction with
folder_sort_direction="asc"or"desc"
ExposΓ© includes a high-performance OneDrive sync script for automatic downloading of images and markdown files from shared folders with structure preservation.
# Add OneDrive share URL to your project config
echo 'SHARE_URL="https://1drv.ms/f/s/your-link"' >> projects/myproject/project.config
# Sync images and markdown files, then generate website
./onedrive.sh -p myproject && ./expose.sh -p myprojectKey features: Recursive folder processing, auto-optimized performance (up to 43% faster), zero-config authentication, smart progress tracking, markdown file sync.
π Complete documentation: OneDrive Sync Guide (onedrive.md)
Add a content.md file to any gallery folder for a rich text introduction that appears before the images:
input/Events/Fireworks/content.md:
---
custom_title: Fireworks Gallery
year: 2025
---
## Spectacular Fireworks
"<cite>Fireworks</cite>" - a celebration captured in light and color.
These photos showcase the **amazing displays** from various events.Features:
- YAML metadata block (optional): Define custom template variables
- Markdown formatting: Full markdown support (headers, bold, italic, lists, links, etc.)
- Conditional rendering: Gallery intro section only appears when content.md exists
- Automatic HTML conversion: Markdown is parsed to HTML using Perl
The content.md file creates a gallery-intro section that appears before the image gallery, perfect for:
- Event descriptions
- Photo series context
- Gallery introductions
- Artist statements
./onedrive.sh -p myproject && ./expose.sh -p myproject
**Key features**: Recursive folder processing, auto-optimized performance (up to 43% faster), zero-config authentication, smart progress tracking.
π **Complete documentation**: [OneDrive Sync Guide (onedrive.md)](onedrive.md)
## π Adding Descriptions
### Gallery Metadata Files
Add `metadata.txt` to any gallery folder (`input/Gallery/`) for gallery-wide settings:
```yaml
# input/Summer-Vacation/metadata.txt
title: "Summer Vacation 2024"
description: "Photos from our trip to the mountains"
width: 12 # Grid layout width
Create text files with the same name as your images:
IMG_001.txt:
---
title: "Sunset at the Beach"
location: "California Coast"
---
This stunning sunset was captured during our evening walk.
The golden hour light created perfect reflections on the wet sand.ExposΓ© automatically extracts and displays camera information:
- Camera settings: F-stop, shutter speed, ISO, focal length
- Equipment: Camera model, lens information
- Smart formatting: Sony camera models get friendly names (Ξ± 7 IV, etc.)
Example output: F4.0 1/640 ISO800 162mm | Sony FE 70-200mm F4 G OSS on Ξ± 7 IV
ExposΓ© automatically detects folder types and generates appropriate navigation:
οΏ½ Column Folders (Depth 1 - contain only subfolders):
- Displayed as navigation section headers
- Part of the URL structure (
/events/fireworks/) - Group related galleries together
- Perfect for main categories
οΏ½π Gallery Folders (contain images):
- Displayed as clickable navigation links
- Generate their own gallery pages
- Support EXIF data and descriptions
ποΈ Structure Folders (contain only subfolders, Depth 2+):
- Displayed as non-clickable navigation labels
- Organize galleries hierarchically
- Perfect for grouping by year, event, etc.
πποΈ Mixed Folders (contain both images AND subfolders):
- Displayed as clickable links (have their own gallery)
- Show nested subfolders in navigation
- Best of both worlds - gallery + organization
οΏ½ 01 Events/ β Column (navigation section)
βββ π Fireworks/ β Gallery folder (clickable)
βββ π Racing/ β Gallery folder (clickable)
βββ πποΈ Summer 2024/ β Mixed folder (clickable + has subfolders)
βββ image1.jpg β Images in Summer 2024/
βββ image2.jpg
βββ π Conference/ β Subfolder of Summer 2024/
π 02 Miscellaneous/ β Column (navigation section)
βββ π Gallery 1/ β Gallery folder
βββ π Gallery 2/ β Gallery folder
π 03 Pages/ β Column (navigation section)
βββ ποΈ Info/ β Structure folder (label only)
βββ π About/ β Gallery folder
βββ π Gear/ β Gallery folder
Generated Navigation:
<section>
<h3>Events</h3>
<div>
<a href="./events/fireworks/" class=" ">Fireworks</a>
</div>
<div>
<a href="./events/racing/" class=" ">Racing</a>
</div>
<div>
<a href="./events/summer-2024/" class="active">Summer 2024</a>
<div>
<a href="./events/summer-2024/conference/" class=" ">Conference</a>
</div>
</div>
</section>
<section>
<h3>Miscellaneous</h3>
<div>
<a href="./miscellaneous/gallery-1/" class=" ">Gallery 1</a>
</div>
<div>
<a href="./miscellaneous/gallery-2/" class=" ">Gallery 2</a>
</div>
</section>
<section>
<h3>Pages</h3>
<div>
<span>Info</span>
<div>
<a href="./pages/info/about/" class=" ">About</a>
</div>
<div>
<a href="./pages/info/gear/" class=" ">Gear</a>
</div>
</div>
</section>- Depth 1: Navigation columns (section headers)
- Depth 2+: Nested galleries and structure folders
- Unlimited nesting depth supported
- Folders starting with
_are ignored - Automatic active state highlighting in navigation
- Images: Reverse alphabetical by default
- Folders: Configurable via
folder_sort_direction - Custom order: Add numerical prefixes (e.g.,
01 Events,02 Miscellaneous) - Prefixes are stripped from URLs and navigation (but not from section headers)
- EXIF Cache: Persistent storage of camera data, only processes changed files
- HTML Cache: Template processing results cached per image
- File Change Detection: Uses modification time + file size for fast detection
- Initial Build: Full processing of all images (~5 min for 1100 images)
- Incremental: Only new/changed images processed (~seconds)
- HTML Only: Skip image encoding for template testing (
-sflag)
- Chunked Processing: Processes images in batches of 100
- Memory Efficient: Avoids loading all EXIF data into memory
- Scalable: Tested with 1100+ image collections
ExposΓ© uses a custom Mustache-Light template engine that supports conditional sections for flexible template design:
Sections (conditional rendering):
{{#images}}
<section class="gallery">
<h2>{{gallerytitle}}</h2>
{{content}}
</section>
{{/images}}Inverted Sections (render when variable is empty/false):
{{^images}}
<p>No images in this gallery</p>
{{/images}}Comments:
{{! This is a comment and won't appear in output }}Default Values:
{{width:50}} <!-- defaults to 50 if not set -->
{{title:Untitled}} <!-- defaults to "Untitled" -->Key Features:
- Single-line processing for optimal performance
- Multi-line template authoring (automatically collapsed)
- Truthiness: Empty strings, "false", and "0" are falsy
- All templates support sections and variables
template.html (main page layout):
{{sitetitle}}- Site title from config{{sitecopyright}}- Copyright notice{{gallerytitle}}- Current gallery name{{navigation}}- Generated hierarchical navigation menu{{content}}- Gallery content area (images){{gallerybody}}- Gallery introduction from content.md{{basepath}}- Relative path to site root{{resourcepath}}- Path to gallery resources{{images}}- Conditional: truthy when gallery has images{{gallerybody}}- Conditional: truthy when content.md exists
post-template.html (individual image):
{{imageurl}}- Image directory path{{imagewidth}},{{imageheight}}- Image dimensions{{imagemd5}}- Unique image identifier- EXIF variables:
{{exif_FNumber}},{{exif_ExposureTime}},{{exif_ISO}}, etc. - Custom metadata: Any variables from image text files or content.md YAML
nav-column-template.html (column headers - depth 1):
{{text}}- Column name{{children}}- Nested navigation items (galleries within column)
nav-branch-template.html (structure folders - depth 2+):
{{text}}- Folder name{{active}}- CSS class for active state ("" or "active"){{children}}- Nested navigation items
nav-leaf-template.html (gallery folders):
{{text}}- Folder name{{uri}}- Link to gallery{{active}}- CSS class for active state ("" or "active"){{children}}- Nested navigation items (for mixed folders)
nav-column-template.html (navigation columns):
<section>
<h3>{{text}}</h3>
{{children}}
</section>nav-branch-template.html (non-clickable structure):
<div>
<span>{{text}}</span>
{{children}}
</div>nav-leaf-template.html (clickable gallery):
<a href="{{uri}}" class=" {{active}}">{{text}}</a>
{{children}}Use fallback syntax for optional variables:
{{width:50}} <!-- defaults to 50 if not set -->
{{title:Untitled}} <!-- defaults to "Untitled" -->- Copy
themes/default/tothemes/mytheme/ - Modify templates as needed:
template.html- Main page layoutpost-template.html- Individual imagesnav-column-template.html- Column headers (depth 1)nav-branch-template.html- Structure folders (depth 2+)nav-leaf-template.html- Gallery folders
- Update
project.config:theme="mytheme"
Development Workflow:
# Quick HTML preview while designing
./expose.sh -s -p myproject
# After changing navigation templates (disable cache)
./expose.sh -s -c -p myproject
# Full build when ready
./expose.sh -p myprojectProduction Workflow:
# Add new photos to project folder
cp new_photos/* projects/myproject/2024/
# Quick incremental build (only processes new photos)
./expose.sh -p myproject| Images | Initial Build | Cached Build | With Changes |
|---|---|---|---|
| 27 | 8 seconds | 3 seconds | 4-6 seconds |
| 100 | ~30 seconds | 3 seconds | 5-10 seconds |
| 1100 | ~5 minutes | 3 seconds | 10-60 seconds |
Times measured on modern hardware with SSD storage
- Slow builds: Use
-sflag first to test HTML generation - Missing EXIF: Ensure ExifTool is installed (
apt install libimage-exiftool-perl) - Image processing errors: Check VIPS installation (
vips --version) - Permission errors: Ensure write access to output directory
- Segfaults: Already handled with VIPS_CONCURRENCY=1 and retry logic
# Clear all cache (force full rebuild)
rm -rf .cache/
# Clear only EXIF cache
rm -rf .cache/*/exif/
# Clear only HTML cache
rm -rf .cache/*/html/Live examples:
- Demo Site - Example gallery
- Personal Portfolio - Real-world usage
This project is heavily based on Expose by Jack000, an excellent static site generator for photographers. The core concept, templating system, and workflow remain largely inspired by the original work.
Key enhancements in this fork:
- Performance optimizations: Intelligent EXIF and HTML caching for 90% faster builds
- Ultra-fast image processing: VIPS with parallel processing (79x faster than sequential!)
- Rock-solid stability: VIPS_CONCURRENCY=1 + retry logic prevents segfaults
- Batch processing: Template optimization for large collections (1000+ images)
- OneDrive integration: Automatic sync of images and markdown files with optimized concurrency (c=24 for 32-core)
- Gallery content feature: Rich text introductions using
content.mdfiles with YAML metadata support - Mustache-Light engine: Conditional sections (
{{#var}}...{{/var}}) for flexible template design - Extended EXIF support: Camera settings display with smart formatting
- Improved navigation: Support for mixed folders and hierarchical structures
- Modern tooling: VIPS for blazing-fast image processing with intelligent parallelization
This project is licensed under the MIT License. See the LICENSE file for details.
Made with β€οΈ for photographers who value performance and simplicity.
The script operates on your current working directory, and outputs a output directory.
Technical settings (theme, sorting, quality) are in project.config, content settings (titles, copyright) are in site.config:
# project.config (structure & technical)
theme="default"
jpeg_quality=90
# site.config (content)
site_title: My Photography Site
site_description: Showcasing my photography journey
site_author: John Doe
site_keywords: photography, art, nature
site_copyright: Β© 2025 Your Nameexpose.sh -p example.site
The -p flag provides the name of the project folder that should be processed. Defaults to the first folder in the ./projects folder.
expose.sh -d
The -d flag enables draft mode, where only a single low resolution is encoded. This can be used for a quick preview or for layout purposes.
Generated images are not overwritten, to do a completely clean build delete the existing output directory first.
The text associated with each image is read from any text file with the same filename as the image, eg:
Images are sorted by reverse alphabetical order. To arbitrarily order images, add a numerical prefix
You can put images in folders to organize them. The folders can be nested any number of times, and are also sorted alphabetically. The folder structure is used to generate a nested html menu.
To arbitrarily order folders, add a numerical prefix to the folder name. Any numerical prefixes are stripped from the url.
Any folders or images with an "_" prefix are ignored and excluded from the build.
ExposΓ© uses a hierarchical metadata system:
Located in projects/<project>/site.config - applies to the entire site:
site_title: My Photography
site_description: A portfolio showcasing my best work
site_author: John Doe
site_keywords: photography, portfolio, landscape
site_copyright: Β© 2025 Your Name
nav_title: PortfolioLocated in projects/<project>/input/GalleryName/metadata.txt - applies to a specific gallery:
width: 19
textcolor: #ffffffThese settings apply to all images in that gallery and override site-wide defaults.
Located next to the image file - applies to a single image:
---
width: 50
textcolor: #ff0000
---
Image description with **Markdown** support.Individual image settings override gallery and site-wide settings.
If the two built-in themes aren't your thing, you can create a new theme. There are only two template files in a theme:
template.html contains the global html for your page. It has access to the following built-in variables:
{{basepath}}- a path to the top level directory of the generated site with trailing slash, relative to the current html file{{resourcepath}}- a path to the gallery resource directory, relative to the current html file. This will be mostly empty (since the html page is in the resource directory), except for the top level index.html file, which necessarily draws resources from a subdirectory{{resolution}}- a list of horizontal resolutions, as specified in the config. This is a single string with space-delimited values{{content}}- where the text/images will go{{sitetitle}}- a global title for your site, as specified in the config{{site_copyright}}- a copyright for your site, as specified in the config{{gallerytitle}}- the title of the current gallery. This is just taken from the folder name{{navigation}}- a nested html menu generated from the folder structure
post-template.html contains the html fragment for each individual image. It has access to the following built-in variables:
{{imageurl}}- url of the directory which contains the image resources, relative to the current html file.- For images, this folder will contain all the scaled versions of the images, where the file name is simply the width of the image - eg. 640.jpg
{{imagewidth}}- maximum width that the source image can be downscaled to{{imageheight}}- maximum height, based on aspect ratio and max width
in addition to these, any variables specified in the YAML metadata of the post will also be available to the post template, eg:
---
mycustomvar: foo
---
this will cause {{mycustomvar}} to be replaced by "foo", in this particular post
Specify default values, in case of unset template variables in the form {{foo:bar}} eg:
{{width:50}}
will set width to 50 if no specific value has been assigned to it by the time page generation has finished.
Any unused {{xxx}} variables that did not have defaults are removed from the generated page.
Any non-template files (css, images, javascript) in the theme directory are simply copied into the output directory.
To avoid additional dependencies, the YAML parser and template engine is simply a sed regex. This means that YAML metadata must take the form of simple key:value pairs, and more complex liquid template syntax are not available.
This project is licensed under the MIT License. For more information, see the LICENSE file.