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

@@ -0,0 +1,90 @@
import { describe, it, expect } from 'vitest';
import { albumStructuredData } from '@/lib/structuredData';
const base = {
siteUrl: 'https://folio.example',
pageUrl: 'https://folio.example/travel/iceland',
albumName: 'Iceland',
};
describe('albumStructuredData', () => {
it('describes the album as an ImageGallery', () => {
const data = albumStructuredData(base)!;
expect(data['@context']).toBe('https://schema.org');
expect(data['@type']).toBe('ImageGallery');
expect(data.name).toBe('Iceland');
expect(data.url).toBe('https://folio.example/travel/iceland');
});
/**
* Structured data is a set of claims about absolute URLs. Without one there
* is nothing truthful to claim, so nothing is emitted.
*/
it.each([
['no site URL', { ...base, siteUrl: null }],
['no page URL', { ...base, pageUrl: null }],
['a blank album name', { ...base, albumName: ' ' }],
])('emits nothing for %s', (_label, input) => {
expect(albumStructuredData(input)).toBeNull();
});
it('omits optional fields rather than emitting empty ones', () => {
const data = albumStructuredData(base)!;
for (const key of ['description', 'creator', 'license', 'image', 'numberOfItems']) {
expect(data).not.toHaveProperty(key);
}
});
it('carries creator and license when configured', () => {
const data = albumStructuredData({
...base,
creator: 'Jane Doe',
license: 'https://creativecommons.org/licenses/by-nc/4.0/',
})!;
expect(data.creator).toEqual({ '@type': 'Person', name: 'Jane Doe' });
expect(data.license).toBe('https://creativecommons.org/licenses/by-nc/4.0/');
});
it('repeats creator and license on the cover image', () => {
const data = albumStructuredData({
...base,
coverUrl: 'https://folio.example/api/image/tok',
creator: 'Jane Doe',
license: 'CC BY-NC 4.0',
})!;
expect(data.image).toEqual({
'@type': 'ImageObject',
contentUrl: 'https://folio.example/api/image/tok',
creator: { '@type': 'Person', name: 'Jane Doe' },
license: 'CC BY-NC 4.0',
});
});
it('treats blank creator and license as absent', () => {
const data = albumStructuredData({ ...base, creator: ' ', license: ' ' })!;
expect(data).not.toHaveProperty('creator');
expect(data).not.toHaveProperty('license');
});
it('trims the values it does emit', () => {
const data = albumStructuredData({
...base,
albumName: ' Iceland ',
description: ' Winter light. ',
creator: ' Jane Doe ',
})!;
expect(data.name).toBe('Iceland');
expect(data.description).toBe('Winter light.');
expect(data.creator).toEqual({ '@type': 'Person', name: 'Jane Doe' });
});
it('reports the photo count when given', () => {
expect(albumStructuredData({ ...base, photoCount: 42 })!.numberOfItems).toBe(42);
expect(albumStructuredData({ ...base, photoCount: 0 })!.numberOfItems).toBe(0);
});
it('survives JSON serialisation, which is how it is emitted', () => {
const data = albumStructuredData({ ...base, creator: 'Jane Doe', photoCount: 3 })!;
expect(() => JSON.parse(JSON.stringify(data))).not.toThrow();
});
});