English | 简体中文
An S3-first photo gallery with a zero-credential local mode and static Vercel deployment
Quick Start • Features • Deployment • Live Demo
This repository is a customized fork of Afilmory, focused on static site deployment. Source photos can stay in S3-compatible object storage (the deployment default) or come from a local directory for a self-contained, zero-credential build. The build produces a static web app, generated thumbnails, RSS, sitemap, Open Graph assets, and a JSON photo manifest.
- ✅ S3-first static deployment - S3-compatible storage remains the default, with an explicit local-filesystem mode for self-contained builds.
- ✅ Vercel-ready build -
vercel.jsonrunsscripts/build-static.shand outputsapps/web/dist. - ✅ Manifest-driven runtime - the browser reads generated JSON data instead of calling a database or backend service.
- ✅ Optional remote metadata cache -
REPO_URLandREPO_TOKENcan persist generated manifest/thumbnails between CI builds. - ✅ One-click deployment - the Vercel deploy button is ready for the required S3 environment variables.
Huge thanks to Innei and the Afilmory team for creating this excellent photo gallery generator.
💡 If you need the complete upstream feature set and latest upstream changes, use the original Afilmory.
- 🖼️ High-performance WebGL renderer - custom React 19 WebGL viewer with smooth zooming, panning, tiled loading, and fallback error callbacks.
- 📱 Responsive masonry layout - custom pure-computed virtual masonry with integer-pixel geometry and no scroll-time DOM measurement.
- 🎨 Modern UI design - glassmorphic interface built with Tailwind CSS 4, Radix UI primitives, and Motion.
- ⚡ Incremental builds - existing manifest data, thumbnails, EXIF, and tone analysis are reused when source photos have not changed.
- 🌐 Internationalization - bundled language resources from
locales/app/*.json. - 🔗 Crawler-ready photo pages - build-time home Open Graph image, per-photo canonical/OG/JSON-LD HTML shells,
feed.xml, andsitemap.xml.
- 🔄 HEIC/HEIF/HIF support - Apple formats are converted during processing.
- 📷 TIFF/TIF, WebP, BMP, PNG, JPG/JPEG support - supported extensions are defined in
packages/builder/src/constants/index.ts. - 🖼️ Generated thumbnails - thumbnails are written to
apps/web/public/thumbnailsand included in the static output. - 📊 EXIF display - metadata is extracted with
exiftool-vendoredin the builder and can be inspected in the web viewer. - 🌈 ThumbHash placeholders - compact placeholders are stored as
thumbHashin the manifest for progressive loading. - 📱 Live Photo and Motion Photo support - sidecar video pairs and embedded motion-photo metadata are represented as manifest video sources.
- ☀️ HDR metadata support - Ultra HDR gain map metadata is detected when present.
- ☁️ S3-compatible source photos - works with AWS S3, MinIO, Aliyun OSS, Tencent COS, and other S3-compatible services.
- 💻 Local-filesystem source photos - set
PHOTO_STORAGE_PROVIDER=localto build without object-storage credentials. - 🌍 CDN-friendly URLs -
S3_CUSTOM_DOMAINcan be used for public photo URLs. - 📦 Provider-aware static output - S3 originals remain in object storage; local-mode originals are copied into the static output under their configured URL prefix.
- 🚀 Progressive static runtime - production emits a small content-addressed
gallery-indexplus stable ID-hash photo-detail shards and a map shard. Routes hydrate only the data they need throughwindow.__AFILMORY__.manifest.
- Official Demo - Official Afilmory demo
- Xudong's Lens
- Gallery by mxte
- Photography by pseudoyu
- Afilmory by magren
To explore the complete UI without credentials or personal photos:
pnpm install
pnpm dev:demoThis serves the committed synthetic gallery at http://127.0.0.1:1924 and
does not read .env, S3 credentials, or your generated manifest.
Click the button below and follow the prompts to configure S3-related environment variables:
Deployment steps:
- Click the deploy button above.
- Sign in to Vercel and fork/import the repository.
- Configure the S3 bucket and either an explicit key pair or another supported AWS credential source.
- Click Deploy.
- The Vercel build runs
scripts/build-static.sh, which runspnpm build; precheck refreshes the manifest when the bucket and credential source are valid.
Environment overrides are merged into site.config.ts by site.config.build.ts during build. Client-side code receives the final config through window.__AFILMORY__.config; it does not read process.env at runtime.
| Variable | Description | Default |
|---|---|---|
PHOTO_STORAGE_PROVIDER |
Source adapter: s3 or local |
s3 |
LOCAL_PHOTOS_PATH |
Local source directory, relative to the repo root | photos |
LOCAL_PHOTOS_BASE_URL |
URL prefix used for local originals | /originals |
Local mode needs no S3 credentials:
PHOTO_STORAGE_PROVIDER=local
LOCAL_PHOTOS_PATH=photos
LOCAL_PHOTOS_BASE_URL=/originalsPut source images under LOCAL_PHOTOS_PATH. The dev server serves them at
LOCAL_PHOTOS_BASE_URL; production builds copy them into apps/web/dist at
the matching path. Keep the reserved-route-safe /originals default unless
your static host is configured for another non-reserved public path. Custom
prefixes use portable ASCII path segments; /photos, /assets, /thumbnails,
and /vendor belong to the application.
When PHOTO_STORAGE_PROVIDER=s3, only the bucket name is always required:
| Variable | Description | Example |
|---|---|---|
S3_BUCKET_NAME |
S3 bucket name | my-photos |
S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY are an optional pair. Set both
when using explicit credentials; setting only one is a configuration error. If
both are omitted, the AWS SDK default credential chain is used (shared
config/SSO, Web Identity, ECS/EC2 roles, and other supported sources). Most
non-AWS S3-compatible services still require the explicit pair.
| Variable | Description | Default | Example |
|---|---|---|---|
S3_REGION |
S3 region | us-east-1 |
us-west-2 |
S3_ENDPOINT |
S3 endpoint | https://s3.us-east-1.amazonaws.com |
https://oss-cn-hangzhou.aliyuncs.com |
S3_PREFIX |
Path prefix for photos | empty | photos/ |
S3_CUSTOM_DOMAIN |
Custom CDN domain | empty | https://cdn.example.com |
S3_EXCLUDE_REGEX |
Regex for excluding files | empty | .*\.txt$ |
⚠️ CORS requirement for the full-resolution viewer. The WebGL viewer fetches the original image bytes viafetch/XHR, so the host that serves your originals (S3_CUSTOM_DOMAINor the S3 endpoint) must return anAccess-Control-Allow-Originheader that allows your site origin (SITE_URL) whenever the two are different origins (e.g.cdn.example.comvsgallery.example.com). Thumbnails are same-origin (/thumbnails) and need no CORS, so a missing CORS header shows as: thumbnails load fine but opening a photo gets stuck on "Failed to load image". This also affects localvite previewagainst a CDN that only allow-lists your production domain.
| Variable | Description |
|---|---|
REPO_URL |
Git repository used to cache generated photos-manifest.json and thumbnails |
REPO_TOKEN |
Token used by the artifact cache script when pushing cache updates |
BUILDER_REPO_URL |
Backward-compatible alias for REPO_URL |
GIT_TOKEN |
Backward-compatible alias for REPO_TOKEN |
This cache is not a photo storage backend. Source photos still come from the configured S3 or local provider.
| Variable | Description | Example |
|---|---|---|
SITE_NAME |
Site name | My Photo Gallery |
SITE_TITLE |
Site title | My Photo Gallery |
SITE_DESCRIPTION |
Site description | Capturing beautiful moments in life |
SITE_URL |
Site URL | https://your-site.vercel.app |
SITE_ACCENT_COLOR |
Accent color | #007bff |
| Variable | Description | Example |
|---|---|---|
AUTHOR_NAME |
Author name | Your Name |
AUTHOR_URL |
Author website | https://your-website.com |
AUTHOR_AVATAR |
Author avatar URL | https://example.com/a.png |
| Variable | Description | Example |
|---|---|---|
SOCIAL_GITHUB |
GitHub username | your-github-username |
SOCIAL_TWITTER |
Twitter/X handle | your-twitter-username |
SOCIAL_RSS |
Enable RSS link | true or false |
| Variable | Description | Example |
|---|---|---|
FEED_FOLO_FEED_ID |
Folo Feed ID | your-feed-id |
FEED_FOLO_USER_ID |
Folo User ID | your-user-id |
| Variable | Description | Default | Possible values |
|---|---|---|---|
MAP_STYLE |
Map style | builtin |
builtin or custom URL |
MAP_PROJECTION |
Map projection | mercator |
globe or mercator |
PHOTO_LOCATION_MODE=coarse is the privacy-preserving default. Public
coordinates are rounded to two decimal places (kilometre scale) before they
enter the manifest or leave the builder. Use strip to publish no coordinates
or place names. exact publishes camera GPS unchanged and should be used only
with the informed consent of photographed people and property owners.
Reverse geocoding is disabled by default because it sends the selected
location to an external provider. Set GEOCODING_ENABLED=true to opt in and
set GEOCODING_USER_AGENT to a real identifier per the
Nominatim usage policy
(max 1 request/second). GEOCODING_PROVIDER=mapbox plus MAPBOX_TOKEN is an
alternative. strip always suppresses geocoding; coarse never sends exact
camera coordinates. See .env.template for all privacy and provider options.
cp .env.template .envExample:
PHOTO_STORAGE_PROVIDER=s3
S3_BUCKET_NAME=my-photos
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=your-access-key-id
S3_SECRET_ACCESS_KEY=your-secret-access-key
SITE_NAME=My Photo Gallery
SITE_TITLE=My Photo Gallery
SITE_DESCRIPTION=Capturing beautiful moments in life
SITE_URL=https://your-site.vercel.app
AUTHOR_NAME=Your Name
AUTHOR_URL=https://your-website.com
AUTHOR_AVATAR=https://example.com/avatar.png
SOCIAL_GITHUB=your-github-username
SOCIAL_RSS=trueFor a zero-credential local setup, replace the S3 block with the three local variables shown under Photo source selection.
- Node.js
^20.19.0 || >=22.12.0(Vite 7 requirement) - pnpm 10.19.0
- Either S3-compatible object storage or a local photo directory
git clone https://github.com/vsxd/afilmory-vercel.git
cd afilmory-vercel
pnpm installUse an S3-compatible object store, or set PHOTO_STORAGE_PROVIDER=local and put photos under LOCAL_PHOTOS_PATH. Supported image extensions are .jpg, .jpeg, .png, .webp, .bmp, .tiff, .tif, .heic, .heif, and .hif.
In S3 mode the manifest points to S3/CDN URLs and originals are not bundled. In local mode the build copies originals into apps/web/dist under LOCAL_PHOTOS_BASE_URL so the resulting site remains self-contained.
# Development server. Runs precheck first.
pnpm dev
# Full static build: precheck, then Vite web build. Workspace packages are
# consumed from TypeScript source, so deployments never build package dist/.
pnpm build
# Refresh manifest and thumbnails only.
pnpm build:manifest
# Build only the frontend from an existing manifest.
pnpm build:web
# Preview apps/web/dist locally.
pnpm preview
# Regenerate favicon assets into apps/web/public.
pnpm generate:faviconAll @afilmory/* packages are workspace-internal — they are consumed directly
from TypeScript source and are not published to npm.
Open http://localhost:4173 after pnpm preview.
pnpm devandpnpm buildrunapps/web/scripts/precheck.tsfirst.- In local mode, precheck runs the builder directly and does not require S3 credentials.
- If the S3 bucket and a valid credential source are available, precheck refreshes the manifest through the builder.
- If required S3 configuration is missing but
generated/photos-manifest.jsonexists, precheck reuses the existing manifest. - If the builder fails, preview builds continue only when the manifest currently on disk still passes strict validation. Precheck never rolls back the JSON file by itself, because a late builder failure may occur after the new manifest was atomically committed and old content-addressed thumbnails were collected.
SKIP_MANIFEST_BUILD=true pnpm buildintentionally skips builder refresh.- Production web builds convert the Builder's manifest v2 into Web Delivery Manifest v3: a hashed gallery index, immutable stable-ID detail shards, and a map shard. Set
AFILMORY_EMBED_MANIFEST=trueto inline v2 for constrained deployments orfalseto force progressive external loading.
pnpm build:manifest -- --force
pnpm build:manifest -- --force-thumbnails
pnpm build:manifest -- --force-manifestVercel uses:
- Build command:
sh scripts/build-static.sh - Output directory:
apps/web/dist
When REPO_URL and REPO_TOKEN are configured, scripts/build-static.sh restores the cached manifest and thumbnails before running the build, then pushes refreshed artifacts. Treat the separate cache repository as private: exact mode may also cache precise coordinates. coarse and strip never transfer geocoding-cache.json and remove legacy exact caches at this boundary. The cache defaults to the dedicated afilmory-cache branch, requires a least-privilege token, and refuses source/protected branches. See the cache security guide.
scripts/build-static.sh always runs pnpm build; all freshness and fallback decisions live in apps/web/scripts/precheck.ts. When required S3 configuration is missing but a reusable generated/photos-manifest.json exists, precheck reuses it so preview deployments still succeed. Production deploys (VERCEL_ENV=production, or REQUIRE_FRESH_BUILD=true on other platforms) fail instead of publishing a stale manifest.
CI/Vercel builds also fail when shipped Project Code differs from the advertised Git revision. Commit the deployment source, or set AFILMORY_CORRESPONDING_SOURCE_URL to a public archive/tree containing the exact deployed source. Local dirty-tree builds remain available for preview and are labeled as non-exact in the footer.
For local-provider deployments, make LOCAL_PHOTOS_PATH available in the build
workspace. The build copies those originals into the static output; do not add
private photos to a public repository by accident.
Deploy the contents of apps/web/dist to any static hosting provider:
- Cloudflare Pages
- Netlify
- GitHub Pages
- Any static host that can serve a SPA fallback to
index.html
Use pnpm build as the build command.
- Upload new or changed photos to your S3 bucket, or update
LOCAL_PHOTOS_PATHin local mode. - Trigger a new deployment or run
pnpm build:manifest. - The builder compares source object metadata with the existing manifest and processes only changed work when possible.
- React 19 with React Compiler
- TypeScript 5.9
- Vite 7
- Tailwind CSS 4
- Radix UI
- Motion
- Jotai
- React Router 7
- i18next and react-i18next
- MapLibre GL and react-map-gl
- Node.js
- pnpm workspace
- Sharp for image processing and generated OG images
- exiftool-vendored for EXIF extraction
- AWS SDK v3 for S3 access
- node:cluster worker processes or an in-process concurrency pool
- thumbhash for compact image placeholders
afilmory/
├── apps/
│ └── web/ # Frontend SPA
├── packages/
│ ├── build-assets/ # Build-time OG image, feed.xml, and sitemap.xml generation
│ ├── builder/ # Photo processing and manifest builder
│ ├── media/ # Zero-dependency thumbhash byte/hex codec leaf
│ ├── schema/ # Manifest contract: types + strict/lenient parsers
│ ├── ui/ # Shared UI primitives and hooks
│ └── webgl-viewer/ # WebGL image viewer package
├── docs/
│ ├── assets/ # README images
│ ├── CONTRIBUTING.md # Contributor setup and workflow
│ ├── rss-exif-extension.md # RSS EXIF extension notes
│ ├── security-notes.md # Security-relevant configuration notes
│ └── testing.md # Vitest and Playwright test/CI guide
├── generated/ # Generated photos-manifest.json
├── locales/app/ # i18n JSON resources
├── scripts/ # Build-time helper scripts
├── site.config.ts # Client-safe site defaults
├── site.config.build.ts # Build-time environment merge
├── builder.config.ts # S3-backed builder configuration
└── vercel.json # Static deployment configuration
Use SITE_ACCENT_COLOR or edit site.config.ts:
export const siteConfig: SiteConfig = {
// ...
accentColor: "#ff6b6b",
};Use MAP_STYLE and MAP_PROJECTION, or edit site.config.ts:
export const siteConfig: SiteConfig = {
// ...
map: ["maplibre"],
mapStyle: "https://your-map-style.json",
mapProjection: "globe",
};Language files are located under locales/app/*.json. To add a language:
- Add the new JSON file under
locales/app. - Import and register it in
apps/web/src/@types/resources.ts. - Add the language code to
apps/web/src/@types/constants.ts.
Contributions, issues, and feature requests are welcome.
See the Contributing Guide for setup and verification, the Security Policy for private reporting, and the Code of Conduct for community expectations.
This project is based on Afilmory and follows the same licenses:
Attribution Network License (ANL) v1.0
- Library code: MIT
- Project code: AGPL-3.0-or-later with UI attribution requirement
See LICENSE, the machine-readable ANL-MANIFEST, and the licensing map for details.
- Original Afilmory: github.com/Afilmory/Afilmory
- Official demo: afilmory.innei.in
- Issue tracker: GitHub Issues
- Original author blog: innei.in
- Thanks to Innei and the Afilmory team for the original project.
- Thanks to all photographers using this project.
- Thanks to all open-source contributors.
If this project helps you, please consider giving it a star on GitHub.


