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

@@ -381,6 +381,63 @@ describe('ImmichClient', () => {
});
});
// #522: a CJK album name slugified to '', so its link was "/" and the album
// was unreachable — no URL segment can ever be the empty string.
describe('getAlbumBySlug() with non-Latin names', () => {
function mockList(albumName: string) {
const json = (body: unknown) => ({
ok: true,
headers: { get: () => 'application/json' },
json: async () => body,
});
mockFetch.mockImplementation(async (url: string) => {
if (url.includes('/search/metadata')) {
return json({ assets: { items: [], nextPage: null, total: 0 } });
}
if (/\/albums\/album-2/.test(url)) {
return json({ id: 'album-2', albumName, assetCount: 0, assets: [], order: 'desc' });
}
return json([{ id: 'album-2', albumName, assetCount: 0 }]);
});
}
it('gives a CJK album a non-empty slug and resolves it', async () => {
mockList('家族相册');
const albums = await immich.getAlbums();
expect(albums[0].slug).toBe('家族相册');
const album = await immich.getAlbumBySlug('家族相册');
expect(album?.id).toBe('album-2');
});
it('resolves a Hangul slug that arrives decomposed (NFD) from the URL', async () => {
mockList('가족 앨범');
await immich.getAlbums();
const album = await immich.getAlbumBySlug('가족-앨범'.normalize('NFD'));
expect(album?.id).toBe('album-2');
});
it('resolves a slug that arrives percent-encoded from the route', async () => {
mockList('家族相册');
await immich.getAlbums();
const encoded = encodeURIComponent('家族相册');
expect(encoded).not.toBe('家族相册');
const album = await immich.getAlbumBySlug(encoded);
expect(album?.id).toBe('album-2');
});
it('falls back to the album id when the name has no letters at all', async () => {
mockList('🎉');
const albums = await immich.getAlbums();
expect(albums[0].slug).toBe('album-2');
const album = await immich.getAlbumBySlug('album-2');
expect(album?.id).toBe('album-2');
});
});
describe('getStandaloneAlbums()', () => {
it('orders albums by gallery.yaml order, not Immich API order', async () => {
// The Immich API returns albums in its own (creation/update) order;