feat: simplify gallery.yaml and add support for album name overrides
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
129
lib/config.ts
129
lib/config.ts
@@ -148,9 +148,11 @@ interface GalleryYaml {
|
||||
email?: string;
|
||||
website?: string;
|
||||
};
|
||||
subpages?: Array<{
|
||||
subpages?:
|
||||
| Record<string, string[] | Array<string | Record<string, string>>>
|
||||
| Array<{
|
||||
name: string;
|
||||
albums: string[];
|
||||
albums: Array<string | Record<string, string>>;
|
||||
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<string, string>;
|
||||
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<string, string> = {};
|
||||
|
||||
function processAlbumEntry(entry: string | Record<string, string>, 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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user