@trippnology/imgwall
v1.0.1
Published
Infinite-scroll masonry image gallery server
Downloads
105
Maintainers
Readme
Image Wall
__ __ __ ______ __ __ ______ __ __
/\ \ /\ "-./ \ /\ ___\ /\ \ _ \ \ /\ __ \ /\ \ /\ \
\ \ \\ \ \-./\ \\ \ \__ \\ \ \/ ".\ \\ \ __ \\ \ \____\ \ \____
\ \_\\ \_\ \ \_\\ \_____\\ \__/".~\_\\ \_\ \_\\ \_____\\ \_____\
\/_/ \/_/ \/_/ \/_____/ \/_/ \/_/ \/_/\/_/ \/_____/ \/_____/Infinite-scroll masonry gallery server. Point it at a directory of images and it serves a web page with a masonry wall, thumbnails, and a lightbox — no build step, nothing written to disk.

Requirements
- Node.js 18+
Install
npm install -g @trippnology/imgwallOr from a checkout of this repo:
npm install -g .Then run from anywhere:
imgwall [options]Quick start
# Serve the current directory
imgwall
# Serve two directories, sorted by newest first, on port 8080
imgwall -d ~/pictures,~/wallpapers -s date-des -p 8080The server picks a random free port, prints the URL, and opens your default browser at it.
CLI options
| Option | Description |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| -d, --directory <dirs> | Comma-separated directories to scan (default: current working directory) |
| -e, --exclude-dirs <dirs> | Comma-separated subdirectory names to exclude (use with -r) |
| -s, --sort <mode> | random (default), name-asc, name-des, date-asc, date-des |
| -r, --recursive | Include subdirectories (default: top-level only) |
| -a, --autoscroll [rate] | Auto-scroll the wall at rate px/sec (default 50); pauses on user input (5s idle resumes) and while the lightbox is open |
| -p, --port <number> | Port (default: random free port) |
| -l, --lan | Listen on all network interfaces — reachable from the local network (default: localhost only) |
| --auth | Require a session token in the URL to view the wall (see below) |
| --no-animation | Flatten animated images to static thumbnails |
| --no-loop | Stop at the end of the image list instead of looping forever (default: loop) |
Auth
--auth requires a session token in the URL. A token is generated per run with crypto.randomBytes and printed in the startup URLs (e.g. http://localhost:1234/?auth=abc…). The first valid visit trades the ?auth= query for a 30-day HttpOnly cookie and is redirected to the clean URL; everything else gets a 401.
The token rotates every run, so bookmarked URLs from a previous run stop working. Run without --auth if that matters.
Scanning
- Extensions:
.jpg,.jpeg,.png,.gif,.webp(case-insensitive) - Date sort uses file mtime
- File list + metadata (name, path, mtime) loaded once at startup; sort/shuffle applied once then held in memory — random order stays stable for the life of the server so infinite scroll doesn't skip or duplicate
Server
Built on hapi (ESM). Everything in memory; nothing written to disk.
Binds localhost by default; -l, --lan binds all interfaces (0.0.0.0) and startup prints LAN URLs too. --auth gates every route behind the token check (independent of --lan, timing-safe compare, token never echoed in responses).
Caching: a per-run version hash is appended to /img, /thumb, manifest, and icon URLs as ?v= — a restart reshuffles ids, so the version guarantees a stale cached thumb can never be served under a recycled id. Media responses get Cache-Control: public, max-age=86400 plus a strong ETag built from the source file's mtime and size (cheap 304 revalidation after expiry); icons and the manifest are immutable; the HTML shell and API are no-store.
Routes:
GET /— HTML shell (inline CSS/JS, no static files)GET /manifest.webmanifest— PWA manifest (display: standalone) so Android "Add to Home screen" opens without browser chrome; icons are sharp-generated PNGs served from memory (/icon-192.png,/icon-512.png, incl. maskable)GET /api/images?offset=&limit=— JSON batch of image metadata (batch size 25); client appends for infinite scroll. Unless disabled via--no-loop, the index wraps with modulo once the list is exhausted — same ids (and cache-hot thumbs) come round again, and the response'sloop:truetells the client to keep scrolling foreverGET /img/:id— full-size image streamGET /thumb/:id— thumbnail generated on demand via sharp (~400px wide, JPEG), cached in memory LRU (cap ~2000 entries)
Client
Inline in the HTML shell:
- Masonry layout via JS column allocator (flex columns, append each item to the shortest column) — new items slot into the staggered gaps and existing items never move
- Infinite scroll via IntersectionObserver sentinel → fetch next batch
- Virtualized scrolling: anchors more than 2 viewports above the viewport are detached (height cached, per-column spacer preserves layout) and re-inserted on scroll up; passes are gated by scroll distance with per-frame caps so cost stays constant with depth
- Lazy-load thumbnails (
loading="lazy") - Media URLs carry the run's
?v=cache version (see Server) - Fancybox v5 (vanilla, no jQuery) lightbox: chevrons, arrow keys, mousewheel scroll through images; carousel built from the full loaded gallery, unaffected by virtualization trimming
- On server start: auto-open default browser at served URL
Errors
- Missing or invalid directory → clear CLI error, exit 1
- Directory with zero images → error message, exit 1
Dependencies
commander, hapi, sharp, figlet, signale
License
MIT © Rikki Tripp - Trippnology
