- Hero carousel: gallery.yaml hero accepts single UUID or array, crossfade component - Full-bleed single albums: subpages with 1 album skip grid, render photos directly - EXIF on hover: camera/lens/focal length overlay on grid items (toggle via exifOnHover) - Customizable grid: columns/gap/aspectRatio/layout (masonry|uniform) via gallery.yaml - Per-subpage grid overrides: each subpage can override global grid config - True masonry: per-image aspect ratios from EXIF dimensions - next/image migration: homepage hero, album covers, grid thumbnails, about portrait - ThumbHash blur placeholders and dominant color backgrounds - Custom next/image loader via image proxy with width/quality params - Lightbox fade-in transition with dominant color background - Password-protected subpages, health check, rate limiting, OG images
12 KiB
Developer Experience & Testing
Objective
Upgrade the developer tooling and quality infrastructure for immich-lightbox. This plan covers five features from IDEAS.md: migrating to CSS Modules, adding Zod-based env validation, configuring Prettier + stricter ESLint rules, adding Playwright end-to-end tests, and adding Vitest unit tests. The goal is a codebase that is easier to maintain, catches errors earlier, and has automated test coverage for critical paths.
Implementation Plan
1 · CSS Modules Migration
Important
This is the highest-effort item. The 832-line
globals.csswill be split into co-located module files, which is a broad refactor touching every component and page.
Current state: One monolithic globals.css (832 lines) containing all component styles via BEM-like class names, plus page.module.css and about.css. Next.js already supports CSS Modules natively — no config needed.
Approach — CSS Modules (not Tailwind): CSS Modules are the better fit here because the project already uses well-structured BEM classes, the design system relies on custom properties, and Tailwind would require learning a new utility API and add a build dependency. CSS Modules give local scoping with zero config in Next.js.
- 1. Create a global design-tokens file
app/tokens.csscontaining only CSS custom properties (:root { ... }), font imports, and base reset styles extracted from the top ~56 lines ofglobals.css - 2. Create
components/Lightbox.module.css— extract all.lightbox-*,.exif-panel-*rules (~140 lines) fromglobals.css - 3. Create
components/PasswordGate.module.css— extract.password-gate-*rules (~70 lines) fromglobals.css - 4. Create
components/SubpageNav.module.css— extract.subpage-nav-*rules if any fromglobals.css - 5. Create
app/[...path]/path.module.css— extract.photo-grid-*,.album-card-*,.album-grid-*rules (~120 lines) fromglobals.css - 6. Keep shared layout styles (
.header-*,.main,.hero-*,.section-label-*,.subpage-grid-*) in a slimmed-downglobals.csssince they are used across multiple pages. Convert the remaining BEM classes to camelCase where they become module-scoped - 7. Update all TSX files to import from their co-located
.module.cssand usestyles.classNamesyntax instead of string class names - 8. Move responsive media queries into each module file alongside their component styles rather than keeping them in one block at the bottom of
globals.css - 9. Verify the build succeeds and visually spot-check the homepage, a subpage, the about page, and the lightbox
2 · Zod Env Validation
Current state: config.ts uses a manual requireEnv() helper and parseInt() with defaults. Errors are thrown as plain strings with no type safety on the parsed result.
- 1. Install
zodas a production dependency - 2. Create
lib/env.ts— define a Zod schema for all environment variables (IMMICH_API_URL,IMMICH_API_KEY,SITE_TITLE,SITE_SUBTITLE,CACHE_TTL,RATE_LIMIT_RPM) with types, defaults, and transforms (e.g.coerce.number().default(300)forCACHE_TTL) - 3. Export a validated, typed
envobject fromlib/env.tsthat is parsed once at startup usingz.parse(process.env) - 4. Replace all
requireEnv()calls and manualprocess.envreads inlib/config.tswith imports fromlib/env.ts. Remove the now-unusedrequireEnv()function - 5. Update
.env.local.examplecomments to document the Zod-enforced constraints (required vs optional, types, defaults) - 6. Verify by running
npm run buildwith a valid.env.local— should succeed. Then temporarily removeIMMICH_API_KEYand confirm the build fails with a clear Zod validation error
3 · Prettier + ESLint Hardening
Current state: eslint.config.mjs uses ESLint v9 flat config with eslint-config-next core-web-vitals and typescript presets. No Prettier config exists. No format-on-save enforcement.
- 1. Install Prettier and the ESLint-Prettier integration packages:
prettier,eslint-config-prettier,eslint-plugin-prettier - 2. Create
.prettierrcwith project conventions: single quotes, trailing commas, 100 char print width, 2-space indent (matching the existing code style) - 3. Create
.prettierignoreto skipnode_modules,.next,build,out,*.yaml - 4. Add
eslint-config-prettierto the ESLint flat config to disable formatting rules that conflict with Prettier, and addeslint-plugin-prettierto surface Prettier violations as ESLint errors - 5. Add stricter custom ESLint rules:
no-console: warn,@typescript-eslint/no-unused-vars: error(with underscore-prefix ignore pattern),@typescript-eslint/no-explicit-any: warn - 6. Add npm scripts:
"format": "prettier --write ."and"format:check": "prettier --check ." - 7. Run
npx prettier --write .to format the entire codebase in one commit, then runnpm run lintto confirm no remaining lint errors - 8. Optionally add a
.vscode/settings.jsonrecommending format-on-save with Prettier
4 · Playwright E2E Tests
Current state: No test framework is installed. The app has 3 page routes (homepage /, catch-all [...path], about /about) and 5 API routes (/api/auth, /api/exif, /api/health, /api/image, /api/og).
Warning
E2E tests require a running dev server and a configured Immich instance (or mocked API responses). Tests will need environment setup documented in a test README or CI configuration.
- 1. Install Playwright as a dev dependency:
npm init playwright@latest— select TypeScript, put tests ine2e/, skip GitHub Actions for now - 2. Configure
playwright.config.ts— setbaseURLtohttp://localhost:3000, configurewebServerto runnpm run devautomatically, set screenshot-on-failure - 3. Write
e2e/health.spec.ts— testGET /api/healthreturns 200 with expected JSON shape - 4. Write
e2e/homepage.spec.ts— test that the homepage loads, renders the header with site title, shows hero section, and displays album cards or subpage grid - 5. Write
e2e/about.spec.ts— test that/aboutloads and shows the about page content - 6. Write
e2e/navigation.spec.ts— test clicking a subpage card navigates to the correct URL, and the back button works - 7. Write
e2e/lightbox.spec.ts— test clicking a photo opens the lightbox overlay, arrow keys navigate, Escape closes - 8. Write
e2e/password.spec.ts— test that a password-protected subpage shows the gate, rejects wrong passwords, and unlocks on correct entry - 9. Add
"test:e2e": "playwright test"to npm scripts - 10. Ensure all tests pass against the local dev server with the existing
gallery.yamland.env.local
5 · Vitest Unit Tests
Current state: No unit test framework installed. Key pure-logic modules suitable for unit testing: tokens.ts (encode/decode), config.ts (slugify, UUID validation, YAML parsing), urls.ts (URL generation), thumbhash.ts (ThumbHash conversion).
- 1. Install Vitest and related packages:
vitest,@vitest/coverage-v8as dev dependencies - 2. Create
vitest.config.tsat project root — configure path aliases to matchtsconfig.json(@/*), setenvironment: 'node', includelib/**/*.test.ts - 3. Write
lib/__tests__/tokens.test.ts— testencodeAssetIdproduces a stable base64url token for a given UUID,decodeAssetIdround-trips correctly, invalid tokens return null, and short tokens return null - 4. Write
lib/__tests__/config.test.ts— testslugify()with various inputs (diacritics, special chars, leading/trailing hyphens), test UUID validation rejects invalid UUIDs, test YAML parsing with a mockgallery.yamlfixture - 5. Write
lib/__tests__/urls.test.ts— testimageUrl()generates correct proxy paths with encoded tokens, testexifUrl(), testassetPlaceholder()returns null for missing thumbhash - 6. Write
lib/__tests__/thumbhash.test.ts— testthumbHashToBlurDataUrl()produces a valid data URL, testthumbHashToDominantHex()returns a valid hex color - 7. Add npm scripts:
"test": "vitest","test:unit": "vitest run","test:coverage": "vitest run --coverage" - 8. Run
npm run test:unitand confirm all tests pass
Verification Criteria
Automated Tests
- Vitest unit tests —
npm run test:unitmust pass all tests inlib/__tests__/ - Playwright e2e tests —
npm run test:e2emust pass all tests ine2e/, requires dev server and valid Immich connection - Lint check —
npm run lintmust produce zero errors - Format check —
npm run format:checkmust produce zero violations - Build —
npm run buildmust succeed (catches CSS Modules import errors, Zod schema problems, TypeScript issues)
Manual Verification
- Visual regression — After CSS Modules migration, open the gallery in a browser and visually check: homepage hero, subpage grid, album cards, photo grid, lightbox with EXIF panel, password gate, about page, and all responsive breakpoints (mobile, tablet, desktop)
- Zod errors — Temporarily remove
IMMICH_API_KEYfrom.env.localand runnpm run dev— should print a clear, typed Zod validation error instead of a generic "missing env" error
Potential Risks and Mitigations
-
CSS Modules migration breaks styling Mitigation: Migrate one component at a time, verify visually after each. Keep a slimmed
globals.cssfor truly shared styles to avoid duplication. -
Playwright tests are flaky without a stable Immich backend Mitigation: Start with the
/api/healthendpoint test which is self-contained. For other tests, document the required Immich setup. Consider adding API mocking with Playwright'sroute.fulfill()in a future iteration. -
Prettier reformats the entire codebase in one commit Mitigation: Apply Prettier in a dedicated "format" commit with no logic changes, making
git blameeasy to filter withgit blame --ignore-rev. -
Zod adds a runtime dependency Mitigation: Zod is ~13KB gzipped and only runs server-side at startup. No client bundle impact since
lib/env.tsis server-only.
Alternative Approaches
- Tailwind instead of CSS Modules — Would provide utility-first rapid styling, but the existing design system relies heavily on custom properties and BEM. Tailwind migration would be a bigger rewrite and add a PostCSS dependency. CSS Modules is the lower-risk choice here.
@t3-oss/env-nextjsinstead of raw Zod — A higher-level wrapper around Zod specifically for Next.js env vars with client/server separation. Worth considering if more env vars are added later, but raw Zod is simpler and sufficient for the current 6 variables.- Jest instead of Vitest — Jest works fine but Vitest is faster, has native ESM support, and shares Vite's config conventions which align better with modern Next.js tooling.
- Cypress instead of Playwright — Playwright is faster, has better multi-browser support, and is increasingly the standard for Next.js projects.
Suggested Implementation Order
| Phase | Feature | Dependency |
|---|---|---|
| 1 | Prettier + ESLint | None — clean baseline first |
| 2 | Vitest unit tests | None — tests existing code |
| 3 | Zod env validation | Unit tests can verify schemas |
| 4 | CSS Modules migration | Lint/format tools catch issues |
| 5 | Playwright e2e tests | All other changes landed first |