handleError for SvelteKit 3: ids only for unknown errors, compact 404 log line; error page suggestions prefer the real parent over the section
All checks were successful
Deploy production / deploy (push) Successful in 1m10s

This commit is contained in:
2026-10-25 13:15:41 +00:00
parent 92d6da959d
commit 20ad42b247
5 changed files with 35 additions and 22 deletions

View File

@@ -8,7 +8,12 @@
<style> <style>
:root { :root {
color-scheme: dark light; color-scheme: dark light;
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; font-family:
ui-sans-serif,
system-ui,
-apple-system,
'Segoe UI',
sans-serif;
} }
body { body {
margin: 0; margin: 0;

View File

@@ -1,10 +1,10 @@
import type { HandleClientError } from '@sveltejs/kit'; import type { HandleClientError } from '@sveltejs/kit';
// Errors thrown while rendering/loading in the browser: same shape as the server (short id, // Unknown errors thrown while loading/rendering in the browser get an id like on the server, so
// generic message) so the error page looks the same either way. // the error page looks the same either way. App and framework errors keep their defaults.
export const handleError: HandleClientError = ({ error, status, message }) => { export const handleError: HandleClientError = (input) => {
if (status === 404) return { message: 'Not Found' }; if (input.kind !== 'unknown') return;
const id = crypto.randomUUID().slice(0, 8); const id = crypto.randomUUID().slice(0, 8);
console.error(`[error ${id}] ${status}`, error); console.error(`[error ${id}]`, input.error);
return { message: status >= 500 ? 'Internal Error' : message, id }; return { id };
}; };

View File

@@ -42,18 +42,20 @@ export const handle: Handle = async ({ event, resolve }) => {
return response; return response;
}; };
// Unexpected errors (anything not thrown with error()) and unknown routes. 5xx get a short id // Runs for every error (SvelteKit 3 passes a `kind`). Unknown errors (bugs, failing services)
// that the error page shows and the log line starts with ("[error ab12cd34]"), so a screenshot // get a short id that the error page shows and the log line starts with ("[error ab12cd34]"),
// or report can be matched to the stack trace. The client only gets a generic message: no // so a screenshot or report can be matched to the stack trace; the client only gets the
// stack, no internals. Query strings are left out of the log (they can carry tokens). // generic "Internal Error", never the stack. 404s get one compact line (Caddy keeps no access
export const handleError: HandleServerError = ({ error, event, status, message }) => { // log), other error() calls are expected and stay quiet. Query strings are never logged.
const route = event.route.id ?? '(no route)'; export const handleError: HandleServerError = (input) => {
const where = `${event.request.method} ${event.url.pathname} route=${route}`; const { event } = input;
if (status === 404) { const where = `${event.request.method} ${event.url.pathname} route=${event.route.id ?? '(none)'}`;
console.warn(`[404] ${where}`); if (input.kind === 'unknown') {
return { message: 'Not Found' };
}
const id = crypto.randomUUID().slice(0, 8); const id = crypto.randomUUID().slice(0, 8);
console.error(`[error ${id}] ${status} ${where}`, error); console.error(`[error ${id}] 500 ${where}`, input.error);
return { message: status >= 500 ? 'Internal Error' : message, id }; return { id };
}
if (input.error.status === 404) console.warn(`[404] ${where}`);
else if (input.error.status >= 500)
console.error(`[${input.error.status}] ${where} ${input.error.message}`);
}; };

View File

@@ -27,6 +27,10 @@ describe('suggestPages', () => {
expect(hrefs('/trackers/helth')).not.toContain('/trackers/health'); expect(hrefs('/trackers/helth')).not.toContain('/trackers/health');
expect(hrefs('/trackers/helth', true)).toContain('/trackers/health'); expect(hrefs('/trackers/helth', true)).toContain('/trackers/health');
}); });
it('ranks the tracker above the section it lives in', () => {
expect(hrefs('/trackers/carz')[0]).toBe('/trackers/cars');
expect(hrefs('/trackers/doesnotexist')).not.toContain('/trackers');
});
it('returns nothing for the home page or noise', () => { it('returns nothing for the home page or noise', () => {
expect(hrefs('/')).toEqual([]); expect(hrefs('/')).toEqual([]);
expect(hrefs('/zzqxj')).toEqual([]); expect(hrefs('/zzqxj')).toEqual([]);

View File

@@ -122,8 +122,10 @@ export function suggestPages(pathname: string, isOwner: boolean, limit = 3): Sug
let score = 0; let score = 0;
for (const s of segments) score = Math.max(score, similarity(s, name), similarity(s, slug)); for (const s of segments) score = Math.max(score, similarity(s, name), similarity(s, slug));
// Generic section names only count when spelled out: they match too much otherwise. // Generic section names only count when spelled out: they match too much otherwise.
if (d.kind === 'page') score *= 0.8; if (d.kind === 'page') score = path.startsWith(`${d.href}/`) ? 0 : score * 0.8;
if (d.href !== '/' && path.startsWith(`${d.href}/`)) // The closest real parent (/trackers/cars for /trackers/cars/nope); sections like
// /trackers are already in the navigation tiles.
if (d.kind !== 'page' && path.startsWith(`${d.href}/`))
score = Math.max(score, 0.9 + d.href.length / 1000); score = Math.max(score, 0.9 + d.href.length / 1000);
return { d, score }; return { d, score };
}) })