Global error page: ErrorView with per-status copy, did-you-mean suggestions, error ids from handleError, static error.html; 404 instead of 500 for bad progress ids; tolerate full header line in DAISY_TASK_WEBHOOK_HEADER and stop logging raw fetch errors
All checks were successful
Deploy production / deploy (push) Successful in 1m9s

This commit is contained in:
2026-10-25 13:10:50 +00:00
parent f86c8ef294
commit a89a187346
13 changed files with 665 additions and 30 deletions

18
src/routes/+error.svelte Normal file
View File

@@ -0,0 +1,18 @@
<script lang="ts">
// Global error page: every 404/5xx anywhere on the site renders here, inside the root
// layout (header + footer). Errors in the root layout load itself fall back to
// src/error.html.
import { page } from '$app/state';
import ErrorView from '#lib/components/ErrorView.svelte';
const data = $derived(page.data as { user?: unknown; isOwner?: boolean });
</script>
<ErrorView
status={page.status}
message={page.error?.message}
errorId={page.error?.id}
path={page.url.pathname}
signedIn={!!data.user}
isOwner={!!data.isOwner}
/>

View File

@@ -2,12 +2,19 @@
import '../app.css';
import { dev } from '$app/env';
import { page } from '$app/state';
import { errorCopy } from '#lib/errors.js';
import { getSeo } from '#lib/seo.js';
import { formatTitle } from '#lib/title.js';
// The header/footer are in #lib/components/SiteChrome.svelte, which +layout.ts loads only
// for pages that show them (not the full-screen book reader).
let { data, children } = $props();
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
const seo = $derived.by(() => {
const seo = getSeo(page.url.pathname, page.data, data.environment);
if (!page.error) return seo;
const title = `${page.status} ${errorCopy(page.status).title}`;
return { ...seo, title: formatTitle(title, data.environment), indexable: false };
});
const Chrome = $derived(data.Chrome);
</script>

View File

@@ -3,22 +3,27 @@ import type { Actions, PageServerLoad } from './$types';
import { db } from '#lib/server/db/index.js';
import { tasks, tasksToTopics, topics } from '#lib/server/db/schema/index.js';
import { eq, and } from 'drizzle-orm';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async (event) => {
const { params } = event;
return {
task: await db.query.tasks.findFirst({
with: {
tasksToTopics: {
with: {
topic: true
}
const id = Number(event.params.id);
// Non-numeric ids used to reach Postgres as NaN and 500.
if (!Number.isSafeInteger(id) || id < 1) error(404, 'Not Found');
const task = await db.query.tasks.findFirst({
with: {
tasksToTopics: {
with: {
topic: true
}
},
where: {
id: Number(params.id)
}
}),
},
where: {
id
}
});
if (!task) error(404, 'Not Found');
return {
task,
topics: await db.query.topics.findMany(),
editable: isAdmin(event)
};

View File

@@ -1,18 +1,21 @@
import { error } from '@sveltejs/kit';
import { db } from '#lib/server/db/index.js';
import type { PageServerLoad } from './$types';
export const load = async ({ params }) => {
return {
topic: await db.query.topics.findFirst({
with: {
topicsToTasks: {
with: {
task: true
}
export const load: PageServerLoad = async ({ params }) => {
const id = Number(params.id);
// Non-numeric ids used to reach Postgres as NaN and 500.
if (!Number.isSafeInteger(id) || id < 1) error(404, 'Not Found');
const topic = await db.query.topics.findFirst({
with: {
topicsToTasks: {
with: {
task: true
}
},
where: {
id: Number(params.id)
}
})
};
},
where: { id }
});
if (!topic) error(404, 'Not Found');
return { topic };
};