release: v0.13.0 (#535)

Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.

Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.

Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ralksta
2026-08-25 10:08:12 +02:00
committed by GitHub
parent 10ec68f493
commit a813433891
73 changed files with 4697 additions and 453 deletions

View File

@@ -8,6 +8,9 @@
import crypto from 'crypto';
import { getConfig, SubpageConfig } from './config';
// From the pure schema module, not the config barrel: tests that stub out
// '@/lib/config' wholesale would otherwise lose this helper.
import { normalizeSlug } from './config/schema';
import { verifyScrypt, generateScryptHash, isScryptHash } from './password';
const TOKEN_EXPIRY_HOURS = 24;
@@ -92,11 +95,43 @@ function cookieName(key: string, type: ProtectedType): string {
*/
export const SITE_AUTH_COOKIE = 'lb_site_auth';
/**
* Whether this visitor may have the album at all — every gate on every route
* to it, not just the album's own (#475).
*
* A page checks the subpage's password before it renders anything; a route
* handler is reached without ever passing through that page, so it has to ask
* the same question itself. Checking only the album's own password let an
* album that carries none through, even when the sole way to it was a subpage
* that does.
*
* An album listed on the home page is published there whatever else lists it,
* so it stays reachable. Where the only routes are subpages, one of them has
* to be open — which mirrors what the visitor could actually have clicked.
*/
export function isAlbumReachable(
albumId: string,
getCookie: (name: string) => string | undefined,
): boolean {
if (!isAuthenticated(albumId, getCookie, 'album')) return false;
const config = getConfig();
if (config.standaloneAlbums?.includes(albumId)) return true;
const routes = config.subpages.filter(
(sp) => sp.enabled !== false && sp.albumIds.includes(albumId),
);
if (routes.length === 0) return true;
return routes.some((sp) => isAuthenticated(sp.slug, getCookie, 'subpage'));
}
/**
* Find the SubpageConfig for a given slug.
*/
export function findSubpageBySlug(slug: string): SubpageConfig | undefined {
return getConfig().subpages.find((sp) => sp.slug === slug);
const wanted = normalizeSlug(slug);
return getConfig().subpages.find((sp) => sp.slug === wanted);
}
/**
@@ -108,7 +143,8 @@ function findPassword(key: string, type: ProtectedType): string | undefined {
return config.sitePassword || undefined;
}
if (type === 'subpage') {
return config.subpages.find((sp) => sp.slug === key)?.password;
const wanted = normalizeSlug(key);
return config.subpages.find((sp) => sp.slug === wanted)?.password;
}
if (type === 'album') {
return config.albumPasswords[key];