release: v0.20.0

This commit is contained in:
Ralksta
2026-10-02 18:14:23 +02:00
committed by GitHub
parent 81ce886b01
commit 5f831f719a
209 changed files with 13807 additions and 803 deletions

View File

@@ -0,0 +1,68 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
/**
* P-17: /about shared with no picture. A page's openGraph replaces the
* layout's whole, so leaving `images` out dropped the site card too.
*/
let aboutMd = '';
vi.mock('fs', async (orig) => {
const actual = await orig<typeof import('fs')>();
return {
...actual,
promises: {
...actual.promises,
readFile: async (path: string, enc: string) =>
String(path).endsWith('about.md')
? aboutMd
: actual.promises.readFile(path, enc as 'utf-8'),
},
};
});
vi.mock('@/lib/config', () => ({
getConfig: () => ({ authSecret: 'test-secret', aboutEnabled: true }),
getConfigOrNull: () => ({ lang: 'en' }),
}));
vi.mock('@/lib/urls', () => ({
imageUrl: (ref: string | { id: string; isEdited?: boolean }, size: string) =>
typeof ref === 'string'
? `/api/image/tok-${ref}?size=${size}`
: `/api/image/tok-${ref.id}?size=${size}${ref.isEdited ? '&e=edit' : ''}`,
assetPlaceholder: () => null,
}));
const portraitAsset = vi.hoisted(() => ({ value: null as null | Record<string, unknown> }));
vi.mock('@/lib/immich', () => ({
immich: { getAssetInfo: async () => portraitAsset.value },
}));
const { generateMetadata } = await import('../page');
describe('/about metadata', () => {
beforeEach(() => {
aboutMd = '';
portraitAsset.value = null;
});
it('shares a portrait edited in Immich under its edited URL (#831)', async () => {
aboutMd = '---\nname: Ada\nportrait: portrait-uuid\n---\nHello.';
portraitAsset.value = { id: 'portrait-uuid', isEdited: true };
const m = await generateMetadata();
expect(m.openGraph?.images).toEqual(['/api/image/tok-portrait-uuid?size=preview&e=edit']);
});
it('uses the portrait as the share image', async () => {
aboutMd = '---\nname: Ada\nportrait: portrait-uuid\n---\nHello.';
const m = await generateMetadata();
expect(m.openGraph?.images).toEqual(['/api/image/tok-portrait-uuid?size=preview']);
expect(m.twitter?.images).toEqual(['/api/image/tok-portrait-uuid?size=preview']);
});
it('falls back to the generated card without a portrait', async () => {
aboutMd = '---\nname: Ada\n---\nHello.';
const m = await generateMetadata();
const [image] = m.openGraph?.images as string[];
expect(image).toMatch(/^\/api\/og\?/);
expect(new URL(image, 'https://x.test').searchParams.get('title')).toBe(m.title);
});
});

View File

@@ -9,6 +9,7 @@ import yaml from 'js-yaml';
import Image from 'next/image';
import type { Metadata } from 'next';
import { imageUrl, assetPlaceholder } from '@/lib/urls';
import { ogImageUrl } from '@/lib/ogImage';
import { immich } from '@/lib/immich';
import { getConfig } from '@/lib/config';
import { getServerDictionary } from '@/lib/i18n/server';
@@ -56,6 +57,17 @@ export async function generateMetadata(): Promise<Metadata> {
const t = getServerDictionary();
const title = meta.name ? t.about.metaTitle(meta.name) : t.about.title;
const description = body ? body.slice(0, 160).replace(/\s+/g, ' ').trim() : undefined;
// A page's openGraph replaces the layout's whole, images included, so
// leaving them out shared /about with no picture at all. The portrait is the
// obvious one (it is a published asset); without it, the generated card.
// The portrait's asset for its edit marker (#831), from the cache the page
// body reads as well.
const portraitAsset = meta.portrait
? await immich.getAssetInfo(meta.portrait).catch(() => null)
: null;
const ogImage = meta.portrait
? imageUrl(portraitAsset ?? meta.portrait, 'preview')
: ogImageUrl(title);
return {
title,
@@ -63,11 +75,13 @@ export async function generateMetadata(): Promise<Metadata> {
openGraph: {
title,
description,
images: [ogImage],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: [ogImage],
},
};
}
@@ -89,7 +103,7 @@ export default async function AboutPage() {
<div className="about__portrait-col">
{meta.portrait ? (
<Image
src={imageUrl(meta.portrait, 'preview')}
src={imageUrl(portraitAsset ?? meta.portrait, 'preview')}
alt={meta.name || t.about.portraitAlt}
className="about__portrait"
fill