Skip to content

Commit c9e9809

Browse files
[breaking] remove createIndexFiles option, derive from trailingSlash instead (#3801)
* move createIndexFiles option to adapter-static * dont provide default value here * add missing tests * derive createIndexFiles from trailingSlash * robustify prerendering with paths.base * update tests * update changeset * remove internal createIndexFiles option * update docs * fix test * Update packages/kit/src/core/config/options.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
1 parent dac52a7 commit c9e9809

File tree

11 files changed

+114
-106
lines changed

11 files changed

+114
-106
lines changed

.changeset/flat-stingrays-talk.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@sveltejs/adapter-static': patch
3+
'@sveltejs/kit': patch
4+
---
5+
6+
[breaking] remove `createIndexFiles` option, derive from `trailingSlash` instead

documentation/docs/14-configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const config = {
5656
prerender: {
5757
concurrency: 1,
5858
crawl: true,
59-
createIndexFiles: true,
6059
enabled: true,
6160
entries: ['*'],
6261
onError: 'fail'
@@ -203,7 +202,6 @@ See [Prerendering](/docs/page-options#prerender). An object containing zero or m
203202

204203
- `concurrency` — how many pages can be prerendered simultaneously. JS is single-threaded, but in cases where prerendering performance is network-bound (for example loading content from a remote CMS) this can speed things up by processing other tasks while waiting on the network response
205204
- `crawl` — determines whether SvelteKit should find pages to prerender by following links from the seed page(s)
206-
- `createIndexFiles` - if set to `false`, will render `about.html` instead of `about/index.html`
207205
- `enabled` — set to `false` to disable prerendering altogether
208206
- `entries` — an array of pages to prerender, or start crawling from (if `crawl: true`). The `*` string includes all non-dynamic routes (i.e. pages with no `[parameters]` )
209207
- `onError`
@@ -249,6 +247,8 @@ Whether to remove, append, or ignore trailing slashes when resolving URLs to rou
249247
- `"always"` — redirect `/x` to `/x/`
250248
- `"ignore"` — don't automatically add or remove trailing slashes. `/x` and `/x/` will be treated equivalently
251249
250+
This option also affects [prerendering](/docs/page-options#prerender). If `trailingSlash` is `always`, a route like `/about` will result in an `about/index.html` file, otherwise it will create `about.html`, mirroring static webserver conventions.
251+
252252
> Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](/docs/hooks#handle) function.
253253
254254
### version

packages/adapter-static/test/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ run('spa', (test) => {
2323
});
2424

2525
test('prerenders page with prerender=true', ({ cwd }) => {
26-
assert.ok(fs.existsSync(`${cwd}/build/about/index.html`));
26+
assert.ok(fs.existsSync(`${cwd}/build/about.html`));
2727
});
2828

2929
test('renders content in fallback page when JS runs', async ({ base, page }) => {

packages/kit/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@
6666
"prepublishOnly": "npm run build",
6767
"test": "npm run test:unit && npm run test:packaging && npm run test:prerendering && npm run test:integration",
6868
"test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\" -i packaging",
69-
"test:prerendering": "pnpm test:prerendering:basics",
69+
"test:prerendering": "pnpm test:prerendering:basics && pnpm test:prerendering:options",
7070
"test:prerendering:basics": "cd test/prerendering/basics && pnpm test",
71+
"test:prerendering:options": "cd test/prerendering/options && pnpm test",
7172
"test:packaging": "uvu src/packaging \"(spec\\.js|test[\\\\/]index\\.js)\"",
7273
"test:integration": "pnpm test:integration:amp && pnpm test:integration:basics && pnpm test:integration:options && pnpm test:integration:options-2",
7374
"test:integration:amp": "cd test/apps/amp && pnpm test",

packages/kit/src/core/adapt/prerender/prerender.js

Lines changed: 89 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,21 @@ import { escape_html_attr } from '../../../utils/escape.js';
1515
* @typedef {import('types/internal').Logger} Logger
1616
*/
1717

18-
/** @type {(errorDetails: Parameters<PrerenderErrorHandler>[0] ) => string} */
19-
function errorDetailsToString({ status, path, referrer, referenceType }) {
18+
/** @type {(details: Parameters<PrerenderErrorHandler>[0] ) => string} */
19+
function format_error({ status, path, referrer, referenceType }) {
2020
return `${status} ${path}${referrer ? ` (${referenceType} from ${referrer})` : ''}`;
2121
}
2222

2323
/** @type {(log: Logger, onError: OnError) => PrerenderErrorHandler} */
24-
function chooseErrorHandler(log, onError) {
24+
function normalise_error_handler(log, onError) {
2525
switch (onError) {
2626
case 'continue':
27-
return (errorDetails) => {
28-
log.error(errorDetailsToString(errorDetails));
27+
return (details) => {
28+
log.error(format_error(details));
2929
};
3030
case 'fail':
31-
return (errorDetails) => {
32-
throw new Error(errorDetailsToString(errorDetails));
31+
return (details) => {
32+
throw new Error(format_error(details));
3333
};
3434
default:
3535
return onError;
@@ -79,7 +79,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
7979

8080
const app = new App(manifest);
8181

82-
const error = chooseErrorHandler(log, config.kit.prerender.onError);
82+
const error = normalise_error_handler(log, config.kit.prerender.onError);
8383

8484
const files = new Set([
8585
...build_data.static,
@@ -116,12 +116,15 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
116116
* @param {boolean} is_html
117117
*/
118118
function output_filename(path, is_html) {
119+
path = path.slice(config.kit.paths.base.length) || '/';
120+
119121
if (path === '/') {
120122
return '/index.html';
121123
}
124+
122125
const parts = path.split('/');
123126
if (is_html && parts[parts.length - 1] !== 'index.html') {
124-
if (config.kit.prerender.createIndexFiles) {
127+
if (config.kit.trailingSlash === 'always') {
125128
parts.push('index.html');
126129
} else {
127130
parts[parts.length - 1] += '.html';
@@ -149,116 +152,115 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
149152
* @param {string?} referrer
150153
*/
151154
async function visit(path, decoded_path, referrer) {
155+
if (!path.startsWith(config.kit.paths.base)) {
156+
error({ status: 404, path, referrer, referenceType: 'linked' });
157+
return;
158+
}
159+
152160
/** @type {Map<string, import('types/internal').PrerenderDependency>} */
153161
const dependencies = new Map();
154162

155-
const render_path = config.kit.paths?.base
156-
? `http://sveltekit-prerender${config.kit.paths.base}${path === '/' ? '' : path}`
157-
: `http://sveltekit-prerender${path}`;
158-
159-
const rendered = await app.render(new Request(render_path), {
163+
const rendered = await app.render(new Request(`http://sveltekit-prerender${path}`), {
160164
prerender: {
161165
all,
162166
dependencies
163167
}
164168
});
165169

166-
if (rendered) {
167-
const response_type = Math.floor(rendered.status / 100);
168-
const type = rendered.headers.get('content-type');
169-
const is_html = response_type === REDIRECT || type === 'text/html';
170+
const response_type = Math.floor(rendered.status / 100);
171+
const type = rendered.headers.get('content-type');
172+
const is_html = response_type === REDIRECT || type === 'text/html';
170173

171-
const file = `${out}${output_filename(decoded_path, is_html)}`;
174+
const file = `${out}${output_filename(decoded_path, is_html)}`;
172175

173-
if (response_type === REDIRECT) {
174-
const location = rendered.headers.get('location');
176+
if (response_type === REDIRECT) {
177+
const location = rendered.headers.get('location');
175178

176-
if (location) {
177-
mkdirp(dirname(file));
179+
if (location) {
180+
mkdirp(dirname(file));
178181

179-
log.warn(`${rendered.status} ${decoded_path} -> ${location}`);
182+
log.warn(`${rendered.status} ${decoded_path} -> ${location}`);
180183

181-
writeFileSync(
182-
file,
183-
`<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>`
184-
);
184+
writeFileSync(
185+
file,
186+
`<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>`
187+
);
185188

186-
const resolved = resolve(path, location);
187-
if (is_root_relative(resolved)) {
188-
enqueue(resolved, path);
189-
}
190-
} else {
191-
log.warn(`location header missing on redirect received from ${decoded_path}`);
189+
const resolved = resolve(path, location);
190+
if (is_root_relative(resolved)) {
191+
enqueue(resolved, path);
192192
}
193-
194-
return;
193+
} else {
194+
log.warn(`location header missing on redirect received from ${decoded_path}`);
195195
}
196196

197-
const text = await rendered.text();
197+
return;
198+
}
198199

199-
if (rendered.status === 200) {
200-
mkdirp(dirname(file));
200+
const text = await rendered.text();
201201

202-
log.info(`${rendered.status} ${decoded_path}`);
203-
writeFileSync(file, text);
204-
paths.push(normalize(decoded_path));
205-
} else if (response_type !== OK) {
206-
error({ status: rendered.status, path, referrer, referenceType: 'linked' });
207-
}
202+
if (rendered.status === 200) {
203+
mkdirp(dirname(file));
208204

209-
for (const [dependency_path, result] of dependencies) {
210-
const { status, headers } = result.response;
205+
log.info(`${rendered.status} ${decoded_path}`);
206+
writeFileSync(file, text);
207+
paths.push(normalize(decoded_path));
208+
} else if (response_type !== OK) {
209+
error({ status: rendered.status, path, referrer, referenceType: 'linked' });
210+
}
211211

212-
const response_type = Math.floor(status / 100);
212+
for (const [dependency_path, result] of dependencies) {
213+
const { status, headers } = result.response;
213214

214-
const is_html = headers.get('content-type') === 'text/html';
215+
const response_type = Math.floor(status / 100);
215216

216-
const file = `${out}${output_filename(dependency_path, is_html)}`;
217-
mkdirp(dirname(file));
217+
const is_html = headers.get('content-type') === 'text/html';
218218

219-
writeFileSync(
220-
file,
221-
result.body === null ? new Uint8Array(await result.response.arrayBuffer()) : result.body
222-
);
223-
paths.push(dependency_path);
224-
225-
if (response_type === OK) {
226-
log.info(`${status} ${dependency_path}`);
227-
} else {
228-
error({
229-
status,
230-
path: dependency_path,
231-
referrer: path,
232-
referenceType: 'fetched'
233-
});
234-
}
235-
}
219+
const file = `${out}${output_filename(dependency_path, is_html)}`;
220+
mkdirp(dirname(file));
236221

237-
if (is_html && config.kit.prerender.crawl) {
238-
for (const href of crawl(text)) {
239-
if (href.startsWith('data:') || href.startsWith('#')) continue;
222+
writeFileSync(
223+
file,
224+
result.body === null ? new Uint8Array(await result.response.arrayBuffer()) : result.body
225+
);
226+
paths.push(dependency_path);
240227

241-
const resolved = resolve(path, href);
242-
if (!is_root_relative(resolved)) continue;
228+
if (response_type === OK) {
229+
log.info(`${status} ${dependency_path}`);
230+
} else {
231+
error({
232+
status,
233+
path: dependency_path,
234+
referrer: path,
235+
referenceType: 'fetched'
236+
});
237+
}
238+
}
243239

244-
const parsed = new URL(resolved, 'http://localhost');
240+
if (is_html && config.kit.prerender.crawl) {
241+
for (const href of crawl(text)) {
242+
if (href.startsWith('data:') || href.startsWith('#')) continue;
245243

246-
let pathname = decodeURI(parsed.pathname);
244+
const resolved = resolve(path, href);
245+
if (!is_root_relative(resolved)) continue;
247246

248-
if (config.kit.paths.base) {
249-
if (!pathname.startsWith(config.kit.paths.base)) continue;
250-
pathname = pathname.slice(config.kit.paths.base.length) || '/';
251-
}
247+
const parsed = new URL(resolved, 'http://localhost');
252248

253-
const file = pathname.slice(1);
254-
if (files.has(file)) continue;
249+
let pathname = decodeURI(parsed.pathname);
255250

256-
if (parsed.search) {
257-
// TODO warn that query strings have no effect on statically-exported pages
258-
}
251+
if (config.kit.paths.base) {
252+
if (!pathname.startsWith(config.kit.paths.base)) continue;
253+
pathname = pathname.slice(config.kit.paths.base.length) || '/';
254+
}
255+
256+
const file = pathname.slice(1);
257+
if (files.has(file)) continue;
259258

260-
enqueue(pathname, path);
259+
if (parsed.search) {
260+
// TODO warn that query strings have no effect on statically-exported pages
261261
}
262+
263+
enqueue(pathname, path);
262264
}
263265
}
264266
}
@@ -267,10 +269,10 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
267269
for (const entry of config.kit.prerender.entries) {
268270
if (entry === '*') {
269271
for (const entry of build_data.entries) {
270-
enqueue(entry, null);
272+
enqueue(config.kit.paths.base + entry, null);
271273
}
272274
} else {
273-
enqueue(entry, null);
275+
enqueue(config.kit.paths.base + entry, null);
274276
}
275277
}
276278

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const get_defaults = (prefix = '') => ({
8585
prerender: {
8686
concurrency: 1,
8787
crawl: true,
88-
createIndexFiles: true,
88+
createIndexFiles: undefined,
8989
enabled: true,
9090
entries: ['*'],
9191
force: undefined,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ const options = object(
186186
prerender: object({
187187
concurrency: number(1),
188188
crawl: boolean(true),
189-
createIndexFiles: boolean(true),
189+
createIndexFiles: error(
190+
(keypath) => `${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/configuration#trailingslash`
191+
),
190192
enabled: boolean(true),
191193
entries: validate(['*'], (input, keypath) => {
192194
if (!Array.isArray(input) || !input.every((page) => typeof page === 'string')) {

0 commit comments

Comments
 (0)