A static portfolio site generator that turns a directory of images into a responsive, single-page photo gallery.
- Static Generation: Python script processes images and generates a lightweight static site.
- Responsive Design: CSS Grid layout with a fixed sidebar on desktop and a hamburger menu on mobile.
- Lightbox: Integrated PhotoSwipe 5.0 for full-screen image viewing with keyboard support.
- Content Management: Simply add folders and images to the
Albums/directory.
- Backend (Build):
src/build.pyuses Pillow to resize images, extract EXIF metadata, and generatesdist/db.jsonanddist/index.htmlusing Jinja2. - Frontend: Vanilla JavaScript (
src/static/app.js) fetches the JSON data and renders the album grid client-side. Routing is handled via URL hash.
- Python 3.11+
- Clone the repository.
- Create a virtual environment:
python3 -m venv .venv source .venv/bin/activate - Install dependencies:
pip install -r requirements.txt
- Create a folder in
Albums/for each album (e.g.,Albums/Travel). - Add
.jpg,.png, or.webpimages to the folder. - The folder name will become the album title.
First, scan your photo albums to extract EXIF data, compute dimensions, and pre-calculate sort order:
python src/scan_albums.pyThis generates albums_metadata.json with:
- EXIF metadata (camera, lens, settings, date)
- Image dimensions (width, height, aspect ratio)
- Pre-computed sort order (portrait-pairing algorithm)
- Orientation classification (portrait/landscape)
Next, build the static photo gallery site:
python src/build.pyThis generates the dist/ folder with:
- Optimized images (large and thumbnail sizes)
- HTML pages for each album
- Global database (db.json)
- Per-album metadata files (metadata.json)
Serve the static site locally or deploy to hosting:
# Local preview
python -m http.server -d dist 8000
# Then visit http://localhost:8000You can manually select a cover photo for any album by editing albums_metadata.json:
{
"Portugal": {
"album_title": "Portugal Trip 2026",
"folder_name": "Portugal",
"cover_filename": "_DSC1012.JPG",
"photos": [...]
}
}Add the cover_filename field with the exact filename of the photo you want to use as the album cover. If not specified or if the file is not found, the first photo in the sorted order will be used.
After running the build process, the dist/ directory contains:
dist/
├── db.json # Global database (all albums)
├── index.html # Home page
├── 404.html # Error page
├── static/ # CSS and JavaScript
├── media/ # Optimized images
│ └── [album]/
│ ├── large_*.jpg # Large images (1600x1200)
│ └── thumb_*.jpg # Thumbnails (600x600)
└── [album]/
├── index.html # Album page
└── metadata.json # Per-album metadata (smaller, album-specific)
Each album contains:
album_title- Display titlefolder_name- Source folder namecover_filename- (Optional) Manual cover image selectionphotos[]- Array of photo objects with:filename- Original filenamewidth,height- Image dimensionsaspect_ratio- Computed ratio (width/height)orientation- "portrait" or "landscape"sort_index- Display order (0-based)metadata- EXIF data (camera, lens, settings, etc.)
To extract EXIF metadata (camera model, lens, exposure settings) from your images and generate a albums_metadata.json file:
python src/scan_albums.pyTo generate the site artifacts in the dist/ directory:
python src/build.pyTo preview the site locally, you can serve the dist directory using Python's built-in HTTP server:
# Ensure you have built the site first
python src/build.py
# Serve the dist directory
cd dist
python -m http.server 8000Open http://localhost:8000 in your browser.
The project is configured for automated deployment to GitHub Pages via GitHub Actions.
Any push to the main branch will trigger a build and deploy the content of dist/ to the gh-pages environment.