release: v0.19.0

Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of
[CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md)
for the full list.

Highlights: client proofing links (#683), custom content pages (#738),
asset picker search (#740), concurrent-edit detection (#742), env-locked
settings (#739), an accessibility baseline (#741), three security fixes
with advisories (#737, #755, #759), and a pre-release quality pass
(#780–#797).

---------

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>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
This commit is contained in:
Ralksta
2026-09-29 22:50:30 +02:00
committed by GitHub
parent 5f251923f0
commit ad8b09700b
297 changed files with 19398 additions and 1800 deletions

View File

@@ -62,6 +62,26 @@ function siteGate(request: NextRequest): NextResponse | null {
return NextResponse.rewrite(new URL(GATE_PATH, request.url));
}
/**
* The first segments that belong to a fixed route under app/ rather than to
* the app/[...path] catch-all. A deep path below one of these is either served
* by that route or answered 404 by Next itself, so isKnownMissing() leaves it
* alone. lib/__tests__/proxy.test.ts holds this list to the app/ directory.
*/
export const TOP_LEVEL_ROUTES: ReadonlySet<string> = new Set([
'about',
'admin',
'api',
'contact',
'gate',
'impressum',
'install',
'journal',
'map',
'privacy',
'proof',
]);
/**
* Pages that are known not to exist before anything renders.
*
@@ -71,6 +91,10 @@ function siteGate(request: NextRequest): NextResponse | null {
* For the fixed routes whose existence depends only on settings.yaml or a file
* in content/journal/, the answer is known here, before streaming starts, so
* they can get a real 404. Album and subpage slugs need Immich and stay soft.
*
* The same holds for any path deeper than two segments that lands in the
* catch-all: app/[...path] only serves `/<slug>` and `/<subpage>/<album>`, and
* calls notFound() for everything longer without asking Immich.
*/
export function isKnownMissing(
pathname: string,
@@ -86,7 +110,14 @@ export function isKnownMissing(
const journal = /^\/journal\/([^/]+)\/?$/.exec(pathname);
if (journal) {
const slug = decodeURIComponent(journal[1]);
let slug: string;
try {
slug = decodeURIComponent(journal[1]);
} catch {
// A malformed escape such as /journal/%E0 names no entry, and must not
// throw out of the proxy.
return true;
}
if (!isValidSlug(slug)) return true;
// A draft still has its file, so it stays a soft 404 for visitors and
// reachable for a signed-in admin, which only the page can tell apart.
@@ -94,6 +125,15 @@ export function isKnownMissing(
fs.existsSync(path.join(contentDir, dir, `${slug}.md`)),
);
}
const segments = pathname.split('/').filter(Boolean);
if (segments.length > 2) {
const [first] = segments;
// Next's own paths (/_next/…, /__nextjs_…) and dotfiles such as
// /.well-known/… are not the catch-all's to answer.
if (first.startsWith('_') || first.startsWith('.')) return false;
return !TOP_LEVEL_ROUTES.has(first);
}
return false;
}
@@ -155,18 +195,19 @@ export function proxy(request: NextRequest) {
requestHeaders.set('x-pathname', request.nextUrl.pathname);
requestHeaders.set('Content-Security-Policy', cspDirectives);
// The page itself still renders and calls notFound(); this only lets the
// The page itself still renders and calls notFound(); the 404 only lets the
// status say so before streaming starts. See isKnownMissing().
const response = isKnownMissing(request.nextUrl.pathname)
? NextResponse.rewrite(request.nextUrl, {
request: { headers: requestHeaders },
status: 404,
})
: NextResponse.next({
request: {
headers: requestHeaders,
},
});
//
// This is NextResponse.next() carrying a status, not a rewrite to the same
// URL. A rewrite destination whose origin differs from the one the server
// resolved the request against is proxied as an external URL, and
// request.nextUrl reports 127.0.0.1 and ::1 as "localhost". Behind
// `next start -H 127.0.0.1` every such rewrite went out to localhost, which
// resolved to ::1 where nothing listens: a 30 s hang, then a 500.
const response = NextResponse.next({
request: { headers: requestHeaders },
...(isKnownMissing(request.nextUrl.pathname) ? { status: 404 } : {}),
});
// Only the CSP is set here. Every other security header comes from
// next.config.ts, which also covers /api and static assets. Setting a header