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>
2026-08-25 10:08:12 +02:00
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import { publicPaths, type SiteShape } from '@/lib/publicPages';
|
|
|
|
|
|
|
|
|
|
const site = (over: Partial<SiteShape> = {}): SiteShape => ({
|
|
|
|
|
siteLocked: false,
|
|
|
|
|
subpages: [],
|
|
|
|
|
standaloneAlbums: [],
|
|
|
|
|
journal: [],
|
|
|
|
|
isAlbumProtected: () => false,
|
|
|
|
|
aboutEnabled: false,
|
|
|
|
|
mapEnabled: false,
|
|
|
|
|
journalEnabled: false,
|
|
|
|
|
...over,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const openSubpage = (slug: string, albums: { id: string; slug: string }[] = []) => ({
|
|
|
|
|
slug,
|
|
|
|
|
enabled: true,
|
|
|
|
|
hidden: false,
|
|
|
|
|
isProtected: false,
|
|
|
|
|
albums,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('publicPaths', () => {
|
|
|
|
|
it('always lists the home page', () => {
|
|
|
|
|
expect(publicPaths(site())).toEqual(['/']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lists the static pages that are switched on', () => {
|
|
|
|
|
const paths = publicPaths(site({ aboutEnabled: true, mapEnabled: true, journalEnabled: true }));
|
|
|
|
|
expect(paths).toEqual(['/', '/about', '/map', '/journal']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lists a subpage and its albums', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({ subpages: [openSubpage('travel', [{ id: 'a', slug: 'iceland' }])] }),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/', '/travel', '/travel/iceland']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lists standalone albums at the root', () => {
|
|
|
|
|
expect(publicPaths(site({ standaloneAlbums: [{ id: 'a', slug: 'portraits' }] }))).toEqual([
|
|
|
|
|
'/',
|
|
|
|
|
'/portraits',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Exclusions ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
it('yields nothing at all for a locked site', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
siteLocked: true,
|
|
|
|
|
aboutEnabled: true,
|
|
|
|
|
subpages: [openSubpage('travel', [{ id: 'a', slug: 'iceland' }])],
|
|
|
|
|
standaloneAlbums: [{ id: 'b', slug: 'portraits' }],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits a disabled subpage', () => {
|
|
|
|
|
const sp = { ...openSubpage('travel', [{ id: 'a', slug: 'iceland' }]), enabled: false };
|
|
|
|
|
expect(publicPaths(site({ subpages: [sp] }))).toEqual(['/']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits a hidden subpage — a listing is the opposite of a direct link', () => {
|
|
|
|
|
const sp = { ...openSubpage('travel', [{ id: 'a', slug: 'iceland' }]), hidden: true };
|
|
|
|
|
expect(publicPaths(site({ subpages: [sp] }))).toEqual(['/']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits a password-protected subpage', () => {
|
|
|
|
|
const sp = { ...openSubpage('clients', [{ id: 'a', slug: 'wedding' }]), isProtected: true };
|
|
|
|
|
expect(publicPaths(site({ subpages: [sp] }))).toEqual(['/']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits a protected album inside an open subpage', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
subpages: [
|
|
|
|
|
openSubpage('travel', [
|
|
|
|
|
{ id: 'open', slug: 'iceland' },
|
|
|
|
|
{ id: 'locked', slug: 'private' },
|
|
|
|
|
]),
|
|
|
|
|
],
|
|
|
|
|
isAlbumProtected: (id) => id === 'locked',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/', '/travel', '/travel/iceland']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits a protected standalone album', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
standaloneAlbums: [
|
|
|
|
|
{ id: 'open', slug: 'portraits' },
|
|
|
|
|
{ id: 'locked', slug: 'clients' },
|
|
|
|
|
],
|
|
|
|
|
isAlbumProtected: (id) => id === 'locked',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/', '/portraits']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Inheritance — the case the issue calls out. The album itself has no
|
|
|
|
|
* password, but it can only be reached through a subpage that does.
|
|
|
|
|
*/
|
|
|
|
|
it('omits a public album that sits under a protected subpage', () => {
|
|
|
|
|
const sp = { ...openSubpage('clients', [{ id: 'open', slug: 'wedding' }]), isProtected: true };
|
|
|
|
|
const paths = publicPaths(site({ subpages: [sp] }));
|
|
|
|
|
expect(paths).not.toContain('/clients/wedding');
|
|
|
|
|
expect(paths).toEqual(['/']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits a public album under a hidden subpage for the same reason', () => {
|
|
|
|
|
const sp = { ...openSubpage('secret', [{ id: 'open', slug: 'wedding' }]), hidden: true };
|
|
|
|
|
expect(publicPaths(site({ subpages: [sp] }))).not.toContain('/secret/wedding');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits journal drafts', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
journalEnabled: true,
|
|
|
|
|
journal: [
|
2026-09-29 22:50:30 +02:00
|
|
|
{ slug: 'published', draft: false, isProtected: false },
|
|
|
|
|
{ slug: 'wip', draft: true, isProtected: false },
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/', '/journal', '/journal/published']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits a password-protected journal entry, as the journal index does', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
journalEnabled: true,
|
|
|
|
|
journal: [
|
|
|
|
|
{ slug: 'published', draft: false, isProtected: false },
|
|
|
|
|
{ slug: 'private', draft: false, isProtected: true },
|
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>
2026-08-25 10:08:12 +02:00
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/', '/journal', '/journal/published']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('omits journal entries entirely when the journal is off', () => {
|
|
|
|
|
const paths = publicPaths(
|
2026-09-29 22:50:30 +02:00
|
|
|
site({
|
|
|
|
|
journalEnabled: false,
|
|
|
|
|
journal: [{ slug: 'published', draft: false, isProtected: false }],
|
|
|
|
|
}),
|
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>
2026-08-25 10:08:12 +02:00
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Shape ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
it('emits no duplicates when a subpage and an album share a slug', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
subpages: [openSubpage('travel')],
|
|
|
|
|
standaloneAlbums: [{ id: 'a', slug: 'travel' }],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual([...new Set(paths)]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-29 22:50:30 +02:00
|
|
|
it('lists a published content page, whether or not it is in the menu (#722)', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({ pages: [{ slug: 'pricing', draft: false, isProtected: false }] }),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/', '/pricing']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('leaves out a draft content page and a password-protected one', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
pages: [
|
|
|
|
|
{ slug: 'draft', draft: true, isProtected: false },
|
|
|
|
|
{ slug: 'secret', draft: false, isProtected: true },
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual(['/']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lists no content page on a locked site', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({ siteLocked: true, pages: [{ slug: 'pricing', draft: false, isProtected: false }] }),
|
|
|
|
|
);
|
|
|
|
|
expect(paths).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
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>
2026-08-25 10:08:12 +02:00
|
|
|
it('emits only absolute-looking, single-slash paths', () => {
|
|
|
|
|
const paths = publicPaths(
|
|
|
|
|
site({
|
|
|
|
|
aboutEnabled: true,
|
|
|
|
|
journalEnabled: true,
|
2026-09-29 22:50:30 +02:00
|
|
|
journal: [{ slug: 'story', draft: false, isProtected: false }],
|
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>
2026-08-25 10:08:12 +02:00
|
|
|
subpages: [openSubpage('travel', [{ id: 'a', slug: 'iceland' }])],
|
|
|
|
|
standaloneAlbums: [{ id: 'b', slug: 'portraits' }],
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
for (const p of paths) {
|
|
|
|
|
expect(p.startsWith('/')).toBe(true);
|
|
|
|
|
expect(p).not.toMatch(/\/\//);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|