Skip to content

Commit 63c7515

Browse files
[breaking] add error.html (#6367)
* [breaking] add error.html This is a static error page that will be rendered by the server when everything else goes wrong Closes #3068 * await native navigations to prevent content flashes * thank you test for uncovering my inability to set the response's content-type * error page can retrieve status / message * fix test * shhh * note placeholders * move default error template into separate file * add some super basic css * fix test * rename options * remove error.html from create-svelte * fix tests * rename internal, too * fixes * Update documentation/docs/03-routing.md Co-authored-by: Rich Harris <hello@rich-harris.dev> Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
1 parent 4b48d1b commit 63c7515

File tree

24 files changed

+206
-42
lines changed

24 files changed

+206
-42
lines changed

.changeset/quiet-camels-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[breaking] add `error.html` page, rename `kit.config.files.template` to `kit.config.files.appTemplate`

documentation/docs/01-project-structure.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ my-project/
1414
│ ├ routes/
1515
│ │ └ [your routes]
1616
│ ├ app.html
17+
│ ├ error.html
1718
│ └ hooks.js
1819
├ static/
1920
│ └ [your static assets]
@@ -41,6 +42,9 @@ The `src` directory contains the meat of your project.
4142
- `%sveltekit.body%` — the markup for a rendered page. Typically this lives inside a `<div>` or other element, rather than directly inside `<body>`, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process
4243
- `%sveltekit.assets%` — either [`paths.assets`](/docs/configuration#paths), if specified, or a relative path to [`paths.base`](/docs/configuration#base)
4344
- `%sveltekit.nonce%` — a [CSP](/docs/configuration#csp) nonce for manually included links and scripts, if used
45+
- `error.html` (optional) is the page that is rendered when everything else fails. It can contain the following placeholders:
46+
- `%sveltekit.status%` — the HTTP status
47+
- `%sveltekit.message%` — the error message
4448
- `hooks.js` (optional) contains your application's [hooks](/docs/hooks)
4549
- `service-worker.js` (optional) contains your [service worker](/docs/service-workers)
4650

documentation/docs/03-routing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ If an error occurs during `load`, SvelteKit will render a default error page. Yo
190190
<h1>{$page.status}: {$page.error.message}</h1>
191191
```
192192
193-
SvelteKit will 'walk up the tree' looking for the closest error boundary — if the file above didn't exist it would try `src/routes/blog/+error.svelte` and `src/routes/+error.svelte` before rendering the default error page.
193+
SvelteKit will 'walk up the tree' looking for the closest error boundary — if the file above didn't exist it would try `src/routes/blog/+error.svelte` and `src/routes/+error.svelte` before rendering the default error page. If _that_ fails, SvelteKit will bail out and render a static fallback error page, which you can customise by creating a `src/error.html` file.
194194
195195
### +layout
196196

documentation/docs/15-configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const config = {
4040
params: 'src/params',
4141
routes: 'src/routes',
4242
serviceWorker: 'src/service-worker',
43-
template: 'src/app.html'
43+
appTemplate: 'src/app.html',
44+
errorTemplate: 'src/error.html'
4445
},
4546
inlineStyleThreshold: 0,
4647
methodOverride: {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>%sveltekit.message%</title>
6+
7+
<style>
8+
body {
9+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
10+
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
11+
display: flex;
12+
align-items: center;
13+
justify-content: center;
14+
height: 100vh;
15+
}
16+
17+
.error {
18+
display: flex;
19+
align-items: center;
20+
max-width: 32rem;
21+
margin: 0 1rem;
22+
}
23+
24+
.status {
25+
font-weight: 200;
26+
font-size: 3rem;
27+
line-height: 1;
28+
position: relative;
29+
top: -0.05rem;
30+
}
31+
32+
.message {
33+
border-left: 1px solid #ccc;
34+
padding: 0 0 0 1rem;
35+
margin: 0 0 0 1rem;
36+
min-height: 2.5rem;
37+
display: flex;
38+
align-items: center;
39+
}
40+
41+
.message h1 {
42+
font-weight: 400;
43+
font-size: 1em;
44+
margin: 0;
45+
}
46+
</style>
47+
</head>
48+
<body>
49+
<div class="error">
50+
<span class="status">%sveltekit.status%</span>
51+
<div class="message">
52+
<h1>%sveltekit.message%</h1>
53+
</div>
54+
</div>
55+
</body>
56+
</html>

packages/kit/src/core/config/index.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import options from './options.js';
1010
* @param {import('types').ValidatedConfig} config
1111
*/
1212
export function load_template(cwd, config) {
13-
const { template } = config.kit.files;
14-
const relative = path.relative(cwd, template);
13+
const { appTemplate } = config.kit.files;
14+
const relative = path.relative(cwd, appTemplate);
1515

16-
if (fs.existsSync(template)) {
17-
const contents = fs.readFileSync(template, 'utf8');
16+
if (fs.existsSync(appTemplate)) {
17+
const contents = fs.readFileSync(appTemplate, 'utf8');
1818

1919
// TODO remove this for 1.0
2020
const match = /%svelte\.([a-z]+)%/.exec(contents);
@@ -34,7 +34,17 @@ export function load_template(cwd, config) {
3434
throw new Error(`${relative} does not exist`);
3535
}
3636

37-
return fs.readFileSync(template, 'utf-8');
37+
return fs.readFileSync(appTemplate, 'utf-8');
38+
}
39+
40+
/**
41+
* Loads the error page (src/error.html by default) if it exists.
42+
* Falls back to a generic error page content.
43+
* @param {import('types').ValidatedConfig} config
44+
*/
45+
export function load_error_page(config) {
46+
const { errorTemplate } = config.kit.files;
47+
return fs.readFileSync(errorTemplate, 'utf-8');
3848
}
3949

4050
/**
@@ -64,10 +74,19 @@ function process_config(config, { cwd = process.cwd() } = {}) {
6474
validated.kit.outDir = path.resolve(cwd, validated.kit.outDir);
6575

6676
for (const key in validated.kit.files) {
77+
// TODO remove for 1.0
78+
if (key === 'template') continue;
79+
6780
// @ts-expect-error this is typescript at its stupidest
6881
validated.kit.files[key] = path.resolve(cwd, validated.kit.files[key]);
6982
}
7083

84+
if (!fs.existsSync(validated.kit.files.errorTemplate)) {
85+
validated.kit.files.errorTemplate = url.fileURLToPath(
86+
new URL('./default-error.html', import.meta.url)
87+
);
88+
}
89+
7190
return validated;
7291
}
7392

packages/kit/src/core/config/index.spec.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ const get_defaults = (prefix = '') => ({
8484
params: join(prefix, 'src/params'),
8585
routes: join(prefix, 'src/routes'),
8686
serviceWorker: join(prefix, 'src/service-worker'),
87-
template: join(prefix, 'src/app.html')
87+
appTemplate: join(prefix, 'src/app.html'),
88+
errorTemplate: join(prefix, 'src/error.html'),
89+
template: undefined
8890
},
8991
headers: undefined,
9092
host: undefined,
@@ -381,6 +383,9 @@ test('load default config (esm)', async () => {
381383

382384
const defaults = get_defaults(cwd + '/');
383385
defaults.kit.version.name = config.kit.version.name;
386+
defaults.kit.files.errorTemplate = fileURLToPath(
387+
new URL('./default-error.html', import.meta.url)
388+
);
384389

385390
assert.equal(config, defaults);
386391
});

packages/kit/src/core/config/options.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,12 @@ const options = object(
140140
params: string(join('src', 'params')),
141141
routes: string(join('src', 'routes')),
142142
serviceWorker: string(join('src', 'service-worker')),
143-
template: string(join('src', 'app.html'))
143+
appTemplate: string(join('src', 'app.html')),
144+
errorTemplate: string(join('src', 'error.html')),
145+
// TODO: remove this for the 1.0 release
146+
template: error(
147+
() => 'config.kit.files.template has been renamed to config.kit.files.appTemplate'
148+
)
144149
}),
145150

146151
// TODO: remove this for the 1.0 release

packages/kit/src/exports/vite/build/build_server.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs';
22
import path from 'path';
33
import { mkdirp, posixify } from '../../../utils/filesystem.js';
44
import { get_vite_config, merge_vite_configs, resolve_entry } from '../utils.js';
5-
import { load_template } from '../../../core/config/index.js';
5+
import { load_error_page, load_template } from '../../../core/config/index.js';
66
import { runtime_directory } from '../../../core/utils.js';
77
import { create_build, find_deps, get_default_build_config, is_http_method } from './utils.js';
88
import { s } from '../../../utils/misc.js';
@@ -14,22 +14,27 @@ import { s } from '../../../utils/misc.js';
1414
* has_service_worker: boolean;
1515
* runtime: string;
1616
* template: string;
17+
* error_page: string;
1718
* }} opts
1819
*/
19-
const server_template = ({ config, hooks, has_service_worker, runtime, template }) => `
20+
const server_template = ({ config, hooks, has_service_worker, runtime, template, error_page }) => `
2021
import root from '__GENERATED__/root.svelte';
2122
import { respond } from '${runtime}/server/index.js';
2223
import { set_paths, assets, base } from '${runtime}/paths.js';
2324
import { set_prerendering } from '${runtime}/env.js';
2425
import { set_private_env } from '${runtime}/env-private.js';
2526
import { set_public_env } from '${runtime}/env-public.js';
2627
27-
const template = ({ head, body, assets, nonce }) => ${s(template)
28+
const app_template = ({ head, body, assets, nonce }) => ${s(template)
2829
.replace('%sveltekit.head%', '" + head + "')
2930
.replace('%sveltekit.body%', '" + body + "')
3031
.replace(/%sveltekit\.assets%/g, '" + assets + "')
3132
.replace(/%sveltekit\.nonce%/g, '" + nonce + "')};
3233
34+
const error_template = ({ status, message }) => ${s(error_page)
35+
.replace(/%sveltekit\.status%/g, '" + status + "')
36+
.replace(/%sveltekit\.message%/g, '" + message + "')};
37+
3338
let read = null;
3439
3540
set_paths(${s(config.kit.paths)});
@@ -78,8 +83,9 @@ export class Server {
7883
root,
7984
service_worker: ${has_service_worker ? "base + '/service-worker.js'" : 'null'},
8085
router: ${s(config.kit.browser.router)},
81-
template,
82-
template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
86+
app_template,
87+
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
88+
error_template,
8389
trailing_slash: ${s(config.kit.trailingSlash)}
8490
};
8591
}
@@ -205,7 +211,8 @@ export async function build_server(options, client) {
205211
hooks: app_relative(hooks_file),
206212
has_service_worker: config.kit.serviceWorker.register && !!service_worker_entry_file,
207213
runtime: posixify(path.relative(build_dir, runtime_directory)),
208-
template: load_template(cwd, config)
214+
template: load_template(cwd, config),
215+
error_page: load_error_page(config)
209216
})
210217
);
211218

packages/kit/src/exports/vite/dev/index.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { getRequest, setResponse } from '../../../exports/node/index.js';
77
import { installPolyfills } from '../../../exports/node/polyfills.js';
88
import { coalesce_to_error } from '../../../utils/error.js';
99
import { posixify } from '../../../utils/filesystem.js';
10-
import { load_template } from '../../../core/config/index.js';
10+
import { load_error_page, load_template } from '../../../core/config/index.js';
1111
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
1212
import * as sync from '../../../core/sync/sync.js';
1313
import { get_mime_lookup, runtime_base, runtime_prefix } from '../../../core/utils.js';
@@ -371,6 +371,7 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
371371
}
372372

373373
const template = load_template(cwd, svelte_config);
374+
const error_page = load_error_page(svelte_config);
374375

375376
const rendered = await respond(
376377
request,
@@ -416,7 +417,7 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
416417
read: (file) => fs.readFileSync(path.join(svelte_config.kit.files.assets, file)),
417418
root,
418419
router: svelte_config.kit.browser.router,
419-
template: ({ head, body, assets, nonce }) => {
420+
app_template: ({ head, body, assets, nonce }) => {
420421
return (
421422
template
422423
.replace(/%sveltekit\.assets%/g, assets)
@@ -426,7 +427,12 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
426427
.replace('%sveltekit.body%', () => body)
427428
);
428429
},
429-
template_contains_nonce: template.includes('%sveltekit.nonce%'),
430+
app_template_contains_nonce: template.includes('%sveltekit.nonce%'),
431+
error_template: ({ status, message }) => {
432+
return error_page
433+
.replace(/%sveltekit\.status%/g, String(status))
434+
.replace(/%sveltekit\.message%/g, message);
435+
},
430436
trailing_slash: svelte_config.kit.trailingSlash
431437
},
432438
{

0 commit comments

Comments
 (0)