simple-gal 0.1.1

A minimal static site generator for fine art photography portfolios
simple-gal-0.1.1 is not a library.

Simple Gal

A minimal static site generator for fine art photography portfolios. Generates fast-loading, responsive photo albums from a filesystem-based data source.

How It Works

Simple Gal uses your filesystem as the data source. Directories become albums, images are ordered by numeric prefix, and markdown files become pages.

content/                         # Root directory
├── config.toml                  # Site configuration (optional)
├── 040-about.md                 # Page (numbered = shown in nav)
├── 050-github.md                # External link (URL-only .md file)
├── 010-Landscapes/              # Album (numbered = shown in nav)
│   ├── info.txt                 # Optional description
│   ├── 001-dawn.jpg             # Preview image (lowest number)
│   ├── 002-sunset.jpg
│   └── 010-mountains.jpg        # Non-contiguous numbering OK
├── 020-Travel/                  # Container directory (has subdirs)
│   ├── 010-Japan/               # Nested album
│   │   ├── info.txt
│   │   └── 001-tokyo.jpg
│   └── 020-Italy/
│       └── 001-rome.jpg
├── 030-Minimal/                 # Another album
│   └── 001-solo.jpg
└── wip-experiments/             # No number prefix = not in nav (still accessible)
    └── 001-draft.jpg

Naming Convention

All entities (albums, images, pages, container directories) follow the same NNN-name convention:

  • NNN- numeric prefix determines sort order (e.g., 001-, 020-, 100-)
  • Dashes in the name portion become spaces in display titles (010-My-Best-Photos → "My Best Photos")
  • Only numbered entries appear in the navigation menu
  • Unnumbered entries still exist and are accessible by URL, but are hidden from nav

Albums

  • A directory containing images (.jpg, .jpeg, .png, .webp)
  • Optional info.txt for a description shown on the album page
  • Preview image: Image numbered 001-* is used as the album thumbnail. Falls back to the first image by sort order if no 001 exists.
  • Directories cannot mix images and subdirectories

Image Metadata

Image titles and descriptions can come from multiple sources. The first available value wins:

  • Title: IPTC title (from Lightroom/Capture One) → filename stem → none
  • Description: sidecar .txt file → IPTC caption → none

A sidecar file uses the same stem as the image: 001-photo.txt for 001-photo.jpg. IPTC titles sourced from metadata are sanitized for use in URLs (truncated to 80 chars, special characters replaced).

Container Directories

  • A directory containing other directories (not images)
  • Numbered containers appear in nav as groups with their children nested underneath
  • Unnumbered containers are transparent: their children are promoted to the parent level in nav

Pages

Markdown files (.md) in the content root become site pages:

  • Content pages: Regular markdown rendered as HTML pages (e.g., 040-about.md)
  • Link pages: If the .md file contains only a URL, it renders as an external link in the nav (e.g., 050-github.md containing https://github.com/user/repo)
  • Pages appear in the navigation after albums, separated by a divider
  • An # H1 heading overrides the filename-derived title

Navigation Order

Navigation items are sorted by their numeric prefix. Albums and containers appear first, then a separator, then pages. Within each group, items are sorted by number.

Build Pipeline

1. Scan      →  manifest.json    (filesystem → structured data)
2. Process   →  processed/       (responsive sizes + thumbnails)
3. Generate  →  dist/            (final HTML site)

Each stage is independent and produces a manifest file that the next stage consumes. Image processing is cached — unchanged sources are skipped on subsequent builds.

Image Processing

Optimized for fine art photography:

  • Formats: AVIF (primary) + WebP (fallback)
  • Responsive sizes: 800px, 1400px, 2080px (configurable)
  • Thumbnails: Single size, cropped to configured aspect ratio (default 4:5)
  • Quality: 90% compression, EXIF preserved

Frontend

Pure HTML/CSS with minimal JS (89 lines):

  • No frameworks, no build tools, no npm
  • CSS custom properties for theming
  • Dark/light/auto color schemes (respects prefers-color-scheme)
  • Stories-style navigation (click/swipe left/right edges)
  • Keyboard navigation (arrow keys)
  • Responsive frames that preserve aspect ratio
  • View transitions (where supported)

Installation

Dependencies

  • Rust compiler (for building the CLI)
  • ImageMagick (convert and identify commands) with AVIF and WebP support

See DEPENDENCIES.md for platform-specific installation instructions, or run:

./scripts/install-deps.sh

Build

cargo build --release

Usage

# Full build (defaults: --source content --output dist)
simple-gal build

# Override paths
simple-gal --source photos --output public build

# Run stages individually
simple-gal scan
simple-gal process
simple-gal generate

CLI Options

Options:
  --source <DIR>      Content directory           [default: content]
  --output <DIR>      Output directory             [default: dist]
  --temp-dir <DIR>    Intermediate files directory  [default: .simple-gal-temp]

All options are global and shared across subcommands. Intermediate files (manifests, processed images) are stored in --temp-dir and preserved between builds for caching and debugging.

Build Script

# Full build using the same script as CI
./scripts/build.sh

Configuration

Place config.toml in your content root (e.g., content/config.toml). All options are optional — defaults are used for any missing values.

# Path to content directory (resolved relative to CWD)
content_root = "content"

[thumbnails]
aspect_ratio = [4, 5]     # width:height

[images]
sizes = [800, 1400, 2080] # Responsive sizes to generate
quality = 90              # AVIF/WebP quality (0-100)

[theme.frame_x]
size = "3vw"              # Preferred horizontal frame size
min = "1rem"              # Minimum horizontal frame size
max = "2.5rem"            # Maximum horizontal frame size

[theme.frame_y]
size = "6vw"              # Preferred vertical frame size
min = "2rem"              # Minimum vertical frame size
max = "5rem"              # Maximum vertical frame size

[colors.light]
background = "#ffffff"
text = "#111111"
text_muted = "#666666"    # Nav, breadcrumbs, captions
border = "#e0e0e0"
link = "#333333"
link_hover = "#000000"

[colors.dark]
background = "#0a0a0a"
text = "#eeeeee"
text_muted = "#999999"
border = "#333333"
link = "#cccccc"
link_hover = "#ffffff"

[processing]
max_processes = 4             # Max parallel workers (omit for auto = CPU cores)

Partial configuration is supported — override only the values you want to change:

[colors.light]
background = "#fafafa"

UI Behavior

Album View

  • Title + optional description
  • Grid of thumbnails (no pagination, controlled album sizes)

Image View

  • Breadcrumb navigation: Home > Album
  • Image in responsive frame with configurable padding
  • Navigation: click/tap right edge → next, left edge → previous
  • Keyboard: left/right arrow keys
  • First image ← goes to album, last image → goes to album

Color Schemes

  • Respects prefers-color-scheme
  • Light: white background, dark text
  • Dark: near-black background, light text

Development

# Run tests
cargo test

# Build and preview locally
./scripts/build.sh
python -m http.server -d dist

Project Structure

├── src/
│   ├── main.rs           # CLI entry point
│   ├── naming.rs         # Filename parsing (NNN-name convention)
│   ├── types.rs          # Shared types (Page, NavItem)
│   ├── config.rs         # Site configuration (config.toml)
│   ├── scan.rs           # Stage 1: filesystem → manifest
│   ├── process.rs        # Stage 2: image processing
│   ├── generate.rs       # Stage 3: HTML generation
│   └── imaging/          # ImageMagick backend
├── static/
│   ├── style.css         # Base styles (inlined at build)
│   └── nav.js            # Keyboard/touch navigation (inlined at build)
├── scripts/
│   ├── build.sh          # Full build (used by CI)
│   └── install-deps.sh   # System dependency installer
└── fixtures/             # Test data

Deployment

GitHub Actions workflow:

  1. Triggers on push to main
  2. Builds the CLI and runs the full pipeline
  3. Publishes to GitHub Pages