MoMent is a lightweight open-source platform for creating and publishing minimal online photography exhibitions. It was originally developed for a cultural heritage photography club, but can be adapted for student groups, artists, archives, museums, and community exhibitions.
Small exhibition teams often need a quiet online gallery that can be edited locally and published as a static site without adopting a full CMS. MoMent keeps the public exhibition simple, stores photo metadata in SQLite, and exports a Cloudflare Pages-ready dist/ folder.
- Minimal public photography exhibition with intro overlay, a single-row responsive navigation, hover/tap metadata, and keyboard- and swipe-enabled lightbox viewing.
- Local-only administrator interface for one admin account.
- Photo upload, expanded metadata editing, deletion, and generated display/lightbox image variants.
- Gallery filtering by year, region, photographer, and place, with filters and the open photo reflected in the shareable URL.
- Integrated public guestbook, with photo-specific entries also shown in each lightbox.
- Photo lightbox with compact metadata, previous/next controls, real
정보/방명록tabs, and a separate map action. - Lazy-loaded MoMent Map using self-hosted Leaflet assets and OpenStreetMap tiles, with marker clustering, region chips, and a keyboard-friendly place list.
- SQLite storage for the local admin app.
- Static export for Cloudflare Pages or another static host.
- Public visit counter, latest-update marker, and guestbook APIs through Cloudflare Pages Functions and a D1 binding.
https://moment-exhibition.pages.dev/
No screenshots are committed yet. Add real screenshots captured from the local app or verified deployment before linking images in this section.
- Python 3.13 and 3.14
- SQLite
- Plain HTML, CSS, and JavaScript
- Pillow for image validation and display/lightbox variants
- Leaflet 1.9.4 vendored in
static/vendor/leaflet/for the optional map view - Cloudflare Pages Functions for public counters and guestbook APIs
- Python 3.13 or 3.14
pip- A shell that can run the
pylauncher on Windows, orpythonon other platforms
Install dependencies:
py -3.13 -m pip install -r requirements.txtOn non-Windows systems, use the equivalent Python command:
python -m pip install -r requirements.txtCreate the local administrator account:
py -3.13 manage.py init-admin --username adminThe command prompts for a password when --password is omitted. Use at least 8 characters. For local development only, you can also copy .env.example to .env and use the bootstrap admin variables described below.
py -3.13 run.pyOpen the printed local URL, then open the configured admin path. The default admin path is /admin.
The admin app binds to loopback by default. MoMent refuses non-loopback binds unless MOMENT_ALLOW_NETWORK_ADMIN=true is explicitly set. Use that exception only on a trusted private network, and never expose the Python admin server directly to the public internet.
- Start the local server.
- Open the admin path.
- Log in with the account created by
init-admin. - Add, edit, or delete photographs and metadata.
- Run a static export before publishing public changes.
The collection manager includes local search, collapsed photo editors, unsaved-change warnings, advanced location fields, and a publish checklist. These helpers do not deploy automatically; the export and deployment steps remain explicit.
Uploaded originals are stored in uploads/. Metadata is stored in data/moment.db. Both are local runtime data and are ignored by git except for uploads/.gitkeep.
Photo metadata includes the original date/location/photographer fields plus year, region, display place name, placeId, optional latitude/longitude, and an optional description. If placeId is left blank, MoMent generates a stable internal place id from the display place name.
Operational rules for guestbook deletion, photo descriptions, and representative map coordinates are documented in docs/operating-policy.md.
Export the public exhibition:
py -3.13 manage.py export-staticBy default this writes to dist/. The export contains:
index.html404.htmlfor real not-found responses instead of SPA soft 404sstatic/css/site.cssstatic/js/exhibition.jsstatic/js/modules/static/og/static/qr/static/icons/static/vendor/leaflet/uploads/display and lightbox images used by the public sitedata/photos.json_headersfor Cloudflare Pages cache and security headers.moment-static-exportmarker used by the exporter safety check
The admin page is not exported. Admin editing remains local in the Python app.
- Edit photos locally in the admin page.
- Run
py -3.13 manage.py export-static. - Commit or upload the generated
dist/folder to the repository connected to Cloudflare Pages. - Configure Cloudflare Pages:
- Framework preset:
None - Build command: leave empty when
dist/is already committed - Build output directory:
dist
- Framework preset:
- Bind a D1 database named
VISITS_DBif you want public visits, latest update, and guestbook APIs to work. - Back up the D1 database, then apply
migrations/0001_initial_schema.sqlbefore deploying the matching Functions. Seemigrations/README.md. - Generate a high-entropy deletion token with
py -3.13 manage.py generate-operator-token --purpose trace-delete, save the raw token in a password manager, and set its printed verifier as the Cloudflare secretTRACE_DELETE_TOKEN_HASH. - Generate a separate status token with
py -3.13 manage.py generate-operator-token --purpose status-updateand set its printed verifier asSTATUS_UPDATE_TOKEN_HASH. There is no committed fallback. Send the raw token only in the request header, for exampleAuthorization: Bearer <token>.
After each exhibition change, export again and redeploy the updated dist/.
The map view requests OpenStreetMap raster tiles from https://tile.openstreetmap.org only when a visitor opens the 지도 panel. Map popups group photos by place; 사진 보기 applies that place filter and returns to the gallery without selecting a representative student photograph. The static export CSP allows that tile host in img-src.
Copy .env.example to .env for local overrides. Do not commit .env.
| Variable | Default | Purpose |
|---|---|---|
MOMENT_HOST |
127.0.0.1 |
Local server bind host. Use 0.0.0.0 only for trusted local network sharing. |
MOMENT_ALLOW_NETWORK_ADMIN |
false |
Required explicit opt-in for any non-loopback bind. This does not make public exposure safe. |
MOMENT_PORT |
8000 |
Local server port. |
MOMENT_MAX_UPLOAD_BYTES |
104857600 |
Maximum request body size for uploads. |
MOMENT_ADMIN_PATH |
/admin |
Local admin route. This is not a substitute for authentication. |
MOMENT_SESSION_COOKIE |
moment_session |
Admin session cookie name. |
MOMENT_SESSION_MAX_AGE |
43200 |
Admin session lifetime in seconds. |
MOMENT_SECURE_COOKIES |
auto |
Use true behind HTTPS, false for local HTTP, or auto to infer from MOMENT_ADMIN_URL. |
MOMENT_PUBLIC_URL |
empty | Optional canonical public URL used in generated metadata. |
MOMENT_ADMIN_URL |
empty | Optional admin server URL used only for secure cookie auto-detection. |
MOMENT_TRACE_DELETE_TOKEN_HASH |
empty | Local SHA-256 verifier for a generated high-entropy hidden trace deletion token. |
MOMENT_STATUS_UPDATE_TOKEN_HASH |
empty | Optional SHA-256 hash accepted by the Cloudflare status-update Function as an alias for STATUS_UPDATE_TOKEN_HASH. |
STATUS_UPDATE_TOKEN |
empty | Local-only raw token used by operator scripts to call POST /api/status-update. Do not commit or share it. |
MOMENT_ADMIN_USERNAME |
empty | Optional first-run bootstrap admin username. |
MOMENT_ADMIN_PASSWORD |
empty | Optional first-run bootstrap admin password. Do not use in shared files. |
run.py: starts the local Python server.manage.py: initializes the single admin account and exports the static site.app/config.py: runtime paths and environment-based configuration.app/database.py: SQLite schema and data access.app/auth.py: password hashing and signed session tokens.app/rate_limit.py: small in-memory rate limiting helpers.app/security.py: shared security headers and cookie helpers.app/http_utils.py: JSON, multipart, photo field, and path helpers.app/photo_metadata.py: photo metadata normalization and generatedplaceIdhelpers.app/image_validation.py: image content validation.app/image_variants.py: display and lightbox image generation.app/guestbook.py: public general/photo guestbook validation and local rate limiting.app/operator_tokens.py: high-entropy operator token generation.app/public_site.py: public payload serialization and static export.static/: public and admin frontend assets.functions/api/: Cloudflare Pages Functions for public APIs.migrations/: versioned Cloudflare D1 schema and one-time maintenance SQL.dist/: generated static site currently used for deployment.docs/operating-policy.md: guestbook, description, and map coordinate operating policy.docs/2026-07-18-security-uiux-report.md: security remediation status and UI/UX priority rationale.docs/original-specification.md: preserved original project specification.tests/: automated tests.
- The Python admin server is a local management app, not a public production server. Non-loopback binds require the explicit
MOMENT_ALLOW_NETWORK_ADMIN=trueexception. - Keep
.env,data/moment.db,data/secret.key, logs, and original uploads out of commits. - The admin path can reduce casual discovery, but real protection comes from authentication, signed sessions, and not exposing the admin server publicly.
- Session cookies are
HttpOnlyandSameSite=Strict; setMOMENT_ADMIN_URL=https://...orMOMENT_SECURE_COOKIES=truewhen the admin server is served through HTTPS. - Admin login failures are rate-limited for 15 minutes by both IP+username pairs (5 failures) and the source IP overall (20 failures).
- Public photo JSON omits local original filenames and internal created/updated timestamps; those fields are only returned to the local admin API.
- Admin mutation requests require a same-origin
OriginorRefererheader in addition to the signed session. - The static export includes Cloudflare
_headers, HSTS, a restrictive CSP, a real404.html, long-lived asset caching for stable assets, and revalidation for JavaScript modules. - Cloudflare Functions require the committed D1 migration to be applied before deployment; they no longer run schema or moderation writes during public GET requests.
- The guestbook is public. It has basic validation and atomic D1-backed rate limiting, not full moderation tooling. Hidden deletion uses a generated high-entropy token and a separate rate-limit bucket.
- The visit counter deduplicates a long-lived anonymous browser token and rate-limits writes. It is still an approximate exhibition visit count, not an identity-verified unique-person metric.
See CONTRIBUTING.md. Keep changes focused, update tests and documentation with behavior changes, and verify both desktop and mobile views when changing UI.
Do not include photographs, personal data, API keys, database files, secrets, or private deployment settings in pull requests.
Source code is licensed under the MIT License. See LICENSE.
Photos, logos, exhibition text, and other media content are not automatically covered by the MIT License. See CONTENT_LICENSE.md before reusing any non-code content. Third-party users should replace the bundled exhibition content with content they own or have permission to use.