/** * Catch-all route — handles both subpage listings and album detail pages. * * Single segment: * - If slug matches a subpage → render subpage album grid * - If slug matches a standalone album → render album detail * * Two segments: * - Treat as subpage-slug/album-slug → render album detail * with back-link to the subpage */ import Link from 'next/link'; import Image from 'next/image'; import { cookies } from 'next/headers'; import type { Metadata } from 'next'; import { immich, type ImmichAsset } from '@/lib/immich'; import { notFound } from 'next/navigation'; import { PhotoGrid, type PhotoItem } from './PhotoGrid'; import { imageUrl, exifUrl, assetPlaceholder, assetExifSummary, assetAspectRatio, } from '@/lib/urls'; import { getConfig, type GridConfig } from '@/lib/config'; import { isProtected, isAuthenticated } from '@/lib/auth'; import PasswordGate from '@/components/PasswordGate'; import { BackLink } from '@/components/BackLink'; // Render at request time — requires live Immich connection export const dynamic = 'force-dynamic'; interface PathPageProps { params: Promise<{ path: string[] }>; } export async function generateMetadata({ params }: PathPageProps): Promise { const { path } = await params; if (!path || path.length === 0) return {}; const slug = path[0]; let title = slug; let subtitle = ''; if (path.length === 2) { const album = await immich.getAlbumBySlug(path[1], slug); if (album) { title = album.albumName; const count = album.assets.filter((a) => a.type === 'IMAGE').length; subtitle = `${count} photo${count === 1 ? '' : 's'}`; } } else if (immich.isSubpageSlug(slug)) { const result = await immich.getSubpageAlbums(slug); if (result) { // Single album → use album name; multiple → use subpage name if (result.albums.length === 1) { const album = await immich.getAlbumBySlug(result.albums[0].slug, slug); if (album) { title = album.albumName; const count = album.assets.filter((a) => a.type === 'IMAGE').length; subtitle = `${count} photo${count === 1 ? '' : 's'}`; } } else { title = result.subpage.name; } } } else { const album = await immich.getAlbumBySlug(slug); if (album) { title = album.albumName; const count = album.assets.filter((a) => a.type === 'IMAGE').length; subtitle = `${count} photo${count === 1 ? '' : 's'}`; } } const ogUrl = `/api/og?title=${encodeURIComponent(title)}${subtitle ? `&subtitle=${encodeURIComponent(subtitle)}` : ''}`; return { title, openGraph: { title, images: [ogUrl] }, twitter: { card: 'summary_large_image', title, images: [ogUrl] }, }; } /** Map Immich assets to PhotoItem props for the grid/lightbox. */ function toPhotoItems(assets: ImmichAsset[], showExif: boolean): PhotoItem[] { return assets .filter((a) => a.type === 'IMAGE') .map((a) => { const ph = assetPlaceholder(a); const exif = showExif ? assetExifSummary(a) : undefined; return { id: a.id, thumbUrl: imageUrl(a.id, 'preview'), previewUrl: imageUrl(a.id, 'preview'), originalUrl: imageUrl(a.id, 'original'), exifUrl: exifUrl(a.id), ...(ph ? { blurDataURL: ph.blurDataURL, dominantColor: ph.dominantColor } : {}), ...(exif ?? {}), aspectRatio: assetAspectRatio(a), }; }); } export default async function PathPage({ params }: PathPageProps) { const { path } = await params; const config = getConfig(); // Build grid CSS custom properties, optionally merging subpage overrides const buildGridStyle = (overrides?: Partial): React.CSSProperties => { const g = { ...config.grid, ...overrides }; return { '--grid-columns': g.columns, '--grid-gap': `${g.gap}px`, '--grid-aspect-ratio': g.aspectRatio, } as React.CSSProperties; }; const resolveLayout = (overrides?: Partial) => overrides?.layout ?? config.grid.layout; if (!path || path.length === 0) { notFound(); } // ── Two segments: subpage/album ────────────────────────────── if (path.length === 2) { const [subpageSlug, albumSlug] = path; // Password gate for protected subpages if (isProtected(subpageSlug)) { const cookieStore = await cookies(); const getCookie = (name: string) => cookieStore.get(name)?.value; if (!isAuthenticated(subpageSlug, getCookie)) { const subpageData = await immich.getSubpageAlbums(subpageSlug); const subpageName = subpageData?.subpage.name ?? subpageSlug; return ; } } const album = await immich.getAlbumBySlug(albumSlug, subpageSlug); if (!album) { notFound(); } // Look up subpage config for grid overrides and back link const subpageData = await immich.getSubpageAlbums(subpageSlug); const spGrid = subpageData?.subpage.grid; const subpageName = subpageData?.subpage.name ?? subpageSlug; const images = toPhotoItems(album.assets, config.exifOnHover); return ( <>

{album.albumName}

{images.length} {images.length === 1 ? 'photo' : 'photos'} {album.description && ` · ${album.description}`}

); } // ── Single segment: subpage or standalone album ────────────── const slug = path[0]; // Check if it's a subpage if (immich.isSubpageSlug(slug)) { // Password gate for protected subpages if (isProtected(slug)) { const cookieStore = await cookies(); const getCookie = (name: string) => cookieStore.get(name)?.value; if (!isAuthenticated(slug, getCookie)) { const subpageData = await immich.getSubpageAlbums(slug); const subpageName = subpageData?.subpage.name ?? slug; return ; } } const result = await immich.getSubpageAlbums(slug); if (!result || result.albums.length === 0) { notFound(); } const { albums } = result; // ── Single album → full-bleed (skip album grid) ────────── if (albums.length === 1) { const album = await immich.getAlbumBySlug(albums[0].slug, slug); if (!album) notFound(); const images = toPhotoItems(album.assets, config.exifOnHover); return ( <>

{album.albumName}

{images.length} {images.length === 1 ? 'photo' : 'photos'} {album.description && ` · ${album.description}`}

); } // ── Multiple albums → show album grid ───────────────────── // Batch-fetch ThumbHash for album cover placeholders const coverPlaceholders = await Promise.all( albums.map(async (album) => { if (!album.albumThumbnailAssetId) return null; const asset = await immich.getAssetInfo(album.albumThumbnailAssetId); return asset ? assetPlaceholder(asset) : null; }), ); return (
{albums.map((album, i) => { const ph = coverPlaceholders[i]; return ( {album.albumThumbnailAssetId ? ( {album.albumName} ) : (
)} {album.assetCount} {album.assetCount === 1 ? 'photo' : 'photos'}
{album.albumName} {album.assetCount} {album.assetCount === 1 ? 'photo' : 'photos'}
); })}
); } // Otherwise treat as a standalone album slug const album = await immich.getAlbumBySlug(slug); if (!album) { notFound(); } const images = toPhotoItems(album.assets, config.exifOnHover); return ( <>

{album.albumName}

{images.length} {images.length === 1 ? 'photo' : 'photos'} {album.description && ` · ${album.description}`}

); }