From 2a16a9386fa2f53beb10816c3ec0c44eea71ea3a Mon Sep 17 00:00:00 2001 From: ralksta Date: Thu, 5 Mar 2026 16:52:01 +0100 Subject: [PATCH] feat: simplify gallery.yaml and add support for album name overrides --- content/gallery.yaml | 21 +++--- content/gallery.yaml.example | 23 +++---- lib/config.ts | 129 ++++++++++++++++++++++++++--------- lib/immich.ts | 17 +++-- 4 files changed, 126 insertions(+), 64 deletions(-) diff --git a/content/gallery.yaml b/content/gallery.yaml index 367132a..7792640 100644 --- a/content/gallery.yaml +++ b/content/gallery.yaml @@ -9,23 +9,20 @@ hero: # Standalone albums shown directly on the homepage albums: - - a89c6085-a105-46c2-baa4-338bc69cab93 - - 9aee9367-a239-4651-980d-3368bc70b7b6 + - a89c6085-a105-46c2-baa4-338bc69cab93: "Wüstenzauber" + - 9aee9367-a239-4651-980d-3368bc70b7b6: "Heimatliebe" # Subpages group multiple albums under one URL path subpages: - - name: Deutschland - albums: - - 9aee9367-a239-4651-980d-3368bc70b7b6 - - - name: Dubai - albums: - - a89c6085-a105-46c2-baa4-338bc69cab93 - - - name: Polen + Deutschland: + - 9aee9367-a239-4651-980d-3368bc70b7b6 + Dubai: + - a89c6085-a105-46c2-baa4-338bc69cab93: "Burj Khalifa & Co" + # Support for subpages with passwords or custom grids in the map format + Polen: password: clientpass123 albums: - - 4ad0ed89-55de-4e69-937f-2d9988f63ad1 + - 4ad0ed89-55de-4e69-937f-2d9988f63ad1: "Krakau & mehr" # Footer — optional minimal footer with social links footer: diff --git a/content/gallery.yaml.example b/content/gallery.yaml.example index fc90f0e..eb9889d 100644 --- a/content/gallery.yaml.example +++ b/content/gallery.yaml.example @@ -13,8 +13,9 @@ hero: 00000000-0000-0000-0000-000000000000 # - 22222222-2222-2222-2222-222222222222 # Standalone albums — shown directly on the homepage as cards +# Use a string for the UUID, or a map for an inline name override. albums: - - 11111111-1111-1111-1111-111111111111 + - 11111111-1111-1111-1111-111111111111: "Meine Favoriten" # inline override - 22222222-2222-2222-2222-222222222222 # Show camera/lens EXIF on photo hover (default: true) @@ -30,21 +31,17 @@ albums: # aspectRatio: "1" # "1", "3/2", "auto", etc. default: "1" # layout: masonry # "masonry" or "uniform", default: "masonry" -# Subpages — group albums into collections at /subpage-slug/album-slug +# Subpages — group albums into collections at /subpage-slug/album-slug. +# You can use a simple map for concise definitions, or a full object for passwords/grids. subpages: - - name: Japan - grid: # override global grid for this subpage - columns: 4 - layout: uniform - aspectRatio: "3/2" - albums: - - 33333333-3333-3333-3333-333333333333 # Tokyo - - 44444444-4444-4444-4444-444444444444 # Kyoto + Japan: # Simple map format + - 33333333-3333-3333-3333-333333333333: "Tokyo Nights" + - 44444444-4444-4444-4444-444444444444 - - name: Wedding – Smith - password: clientpass123 # visitors must enter this to view + Wedding: # Advanced object format (needed for passwords/grid overrides) + password: clientpass123 albums: - - 55555555-5555-5555-5555-555555555555 + - 55555555-5555-5555-5555-555555555555: "The Big Day" # Footer — optional minimal footer with social links # footer: diff --git a/lib/config.ts b/lib/config.ts index f4b5546..0a1703a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -148,9 +148,11 @@ interface GalleryYaml { email?: string; website?: string; }; - subpages?: Array<{ + subpages?: + | Record>> + | Array<{ name: string; - albums: string[]; + albums: Array>; password?: string; grid?: { columns?: number; @@ -239,6 +241,8 @@ export interface AppConfig { footer: FooterConfig | null; /** Show the /map page. Default: false. */ map: boolean; + /** Explicit album name overrides from gallery.yaml. */ + albumOverrides: Record; cacheTtl: number; rateLimitRpm: number; } @@ -318,45 +322,101 @@ export function getConfig(): AppConfig { // Resolve theme: string shorthand → preset, object → merged with preset base const theme = resolveTheme(gallery.theme); + const albumOverrides: Record = {}; + + function processAlbumEntry(entry: string | Record, context: string): string { + if (typeof entry === 'string') { + return validateUuid(entry, context); + } + // Map format: { "uuid": "Custom Name" } + const [uuid, name] = Object.entries(entry)[0]; + const validatedUuid = validateUuid(uuid, context); + albumOverrides[validatedUuid] = name; + return validatedUuid; + } + // Parse standalone albums - const standaloneAlbumIds = (gallery.albums ?? []).map((id) => - validateUuid(id, 'gallery.yaml albums'), + const standaloneAlbumIds = (gallery.albums ?? []).map((entry) => + processAlbumEntry(entry, 'gallery.yaml albums'), ); - if (standaloneAlbumIds.length === 0 && (!gallery.subpages || gallery.subpages.length === 0)) { + if (standaloneAlbumIds.length === 0 && (!gallery.subpages || (Array.isArray(gallery.subpages) ? gallery.subpages.length === 0 : Object.keys(gallery.subpages).length === 0))) { throw new Error('gallery.yaml must define at least one album or subpage'); } // Parse subpages - const subpages: SubpageConfig[] = (gallery.subpages ?? []).map((sp) => { - if (!sp.name || !sp.albums || sp.albums.length === 0) { - throw new Error( - `Subpage "${sp.name || '(unnamed)'}" must have a name and at least one album`, - ); - } - return { - name: sp.name, - slug: slugify(sp.name), - albumIds: sp.albums.map((id) => validateUuid(id, `subpage "${sp.name}"`)), - password: sp.password, - ...(sp.grid - ? { - grid: { - ...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}), - ...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}), - ...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}), - ...(sp.grid.layout != null - ? { - layout: (VALID_LAYOUTS.includes(sp.grid.layout) - ? sp.grid.layout - : 'masonry') as GridConfig['layout'], - } - : {}), - }, - } - : {}), - }; - }); + let subpages: SubpageConfig[] = []; + + if (Array.isArray(gallery.subpages)) { + // Original list format + subpages = gallery.subpages.map((sp) => { + if (!sp.name || !sp.albums || sp.albums.length === 0) { + throw new Error( + `Subpage "${sp.name || '(unnamed)'}" must have a name and at least one album`, + ); + } + return { + name: sp.name, + slug: slugify(sp.name), + albumIds: sp.albums.map((entry) => processAlbumEntry(entry, `subpage "${sp.name}"`)), + password: sp.password, + ...(sp.grid + ? { + grid: { + ...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}), + ...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}), + ...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}), + ...(sp.grid.layout != null + ? { + layout: (VALID_LAYOUTS.includes(sp.grid.layout) + ? sp.grid.layout + : 'masonry') as GridConfig['layout'], + } + : {}), + }, + } + : {}), + }; + }); + } else if (gallery.subpages) { + // New map format: { "Subpage Name": ["uuid1", { "uuid2": "Name" }] } OR { "Subpage Name": { password: "...", albums: [...] } } + subpages = Object.entries(gallery.subpages).map(([name, value]) => { + // If it's an array, it's just albums + if (Array.isArray(value)) { + return { + name, + slug: slugify(name), + albumIds: value.map((entry) => processAlbumEntry(entry, `subpage "${name}"`)), + }; + } + + // If it's an object, it might have password, grid, albums + const sp = value as any; + const albumEntries = sp.albums || []; + return { + name, + slug: slugify(name), + albumIds: albumEntries.map((entry: any) => processAlbumEntry(entry, `subpage "${name}"`)), + password: sp.password, + ...(sp.grid + ? { + grid: { + ...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}), + ...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}), + ...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}), + ...(sp.grid.layout != null + ? { + layout: (VALID_LAYOUTS.includes(sp.grid.layout) + ? sp.grid.layout + : 'masonry') as GridConfig['layout'], + } + : {}), + }, + } + : {}), + }; + }); + } // Collect ALL album IDs (standalone + subpage) — deduplicated const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds)); @@ -396,6 +456,7 @@ export function getConfig(): AppConfig { } : null, map: gallery.map === true, + albumOverrides, cacheTtl: env.CACHE_TTL * 1000, rateLimitRpm: env.RATE_LIMIT_RPM, }; diff --git a/lib/immich.ts b/lib/immich.ts index a3968e5..f8c2c04 100644 --- a/lib/immich.ts +++ b/lib/immich.ts @@ -166,10 +166,14 @@ class ImmichClient { const allowedIds = new Set(this.config.albums); const filtered = all .filter((album) => allowedIds.has(album.id)) - .map((album) => ({ - ...album, - slug: slugify(album.albumName), - })); + .map((album) => { + const name = this.config.albumOverrides[album.id] ?? album.albumName; + return { + ...album, + albumName: name, + slug: slugify(name), + }; + }); // Log album summary on first load so admins can see what's published if (!this.hasLoggedAlbums) { @@ -277,7 +281,10 @@ class ImmichClient { // Filter out trashed assets album.assets = (album.assets || []).filter((a) => !a.isTrashed); - album.slug = slugify(album.albumName); + + const name = this.config.albumOverrides[album.id] ?? album.albumName; + album.albumName = name; + album.slug = slugify(name); cache.set(cacheKey, album, this.config.cacheTtl); return album;