Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
74016df
feat: It basically works lol
elliott-with-the-longest-name-on-github Mar 31, 2023
05c7d4e
feat: Typings for page routes
elliott-with-the-longest-name-on-github Apr 2, 2023
1b2e1e0
Revert "feat: Typings for page routes"
elliott-with-the-longest-name-on-github Apr 3, 2023
a3a7189
feat: Types!
elliott-with-the-longest-name-on-github Apr 3, 2023
d0389ca
fix: naming
elliott-with-the-longest-name-on-github Apr 3, 2023
fa48df5
fix: naming
elliott-with-the-longest-name-on-github Apr 3, 2023
7412ea5
fix: More future-proof types
elliott-with-the-longest-name-on-github Apr 4, 2023
e21340c
feat: Throw by default if entry generator matches a different route ID
elliott-with-the-longest-name-on-github Apr 4, 2023
7f88557
feat: Better validation, separage +page and +layout
elliott-with-the-longest-name-on-github Apr 4, 2023
d6655f4
feat: Tests for mismatch errors
elliott-with-the-longest-name-on-github Apr 4, 2023
d369212
feat: Tests for route_from_entry
elliott-with-the-longest-name-on-github Apr 4, 2023
a07bf20
fix: naming
elliott-with-the-longest-name-on-github Apr 4, 2023
df1acb9
feat: typedoc
elliott-with-the-longest-name-on-github Apr 4, 2023
7758ea9
fix: blargh
elliott-with-the-longest-name-on-github Apr 4, 2023
b0e0a54
feat: One-line docs -- where should we put an example?
elliott-with-the-longest-name-on-github Apr 4, 2023
41d7895
fix: nittiest of nits
elliott-with-the-longest-name-on-github Apr 4, 2023
0ae2351
feat: Changeset
elliott-with-the-longest-name-on-github Apr 4, 2023
57632cd
Update packages/kit/src/utils/routing.js
elliott-with-the-longest-name-on-github Apr 11, 2023
b1d8667
merge master
Apr 17, 2023
19ef1b8
rename route_from_entry -> resolve_entry
Apr 17, 2023
7ae49fc
fix lockfile
Apr 17, 2023
067b309
update lockfile
May 4, 2023
fcb442a
add docs
May 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-insects-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: route-level entry generators via `export const entries`
35 changes: 33 additions & 2 deletions documentation/docs/20-core-concepts/40-page-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const prerender = 'auto';

> If your entire app is suitable for prerendering, you can use [`adapter-static`](https://github.com/sveltejs/kit/tree/master/packages/adapter-static), which will output files suitable for use with any static webserver.

The prerenderer will start at the root of your app and generate files for any prerenderable pages or `+server.js` routes it finds. Each page is scanned for `<a>` elements that point to other pages that are candidates for prerendering — because of this, you generally don't need to specify which pages should be accessed. If you _do_ need to specify which pages should be accessed by the prerenderer, you can do so with the `entries` option in the [prerender configuration](configuration#prerender).
The prerenderer will start at the root of your app and generate files for any prerenderable pages or `+server.js` routes it finds. Each page is scanned for `<a>` elements that point to other pages that are candidates for prerendering — because of this, you generally don't need to specify which pages should be accessed. If you _do_ need to specify which pages should be accessed by the prerenderer, you can do so with [`config.kit.prerender.entries`](configuration#prerender), or by exporting an [`entries`](#entries) function from your dynamic route.

While prerendering, the value of `building` imported from [`$app/environment`](modules#$app-environment) will be `true`.

Expand Down Expand Up @@ -84,9 +84,40 @@ If you encounter an error like 'The following routes were marked as prerenderabl

Since these routes cannot be dynamically server-rendered, this will cause errors when people try to access the route in question. There are two ways to fix it:

* Ensure that SvelteKit can find the route by following links from [`config.kit.prerender.entries`](configuration#prerender). Add links to dynamic routes (i.e. pages with `[parameters]` ) to this option if they are not found through crawling the other entry points, else they are not prerendered because SvelteKit doesn't know what value the parameters should have. Pages not marked as prerenderable will be ignored and their links to other pages will not be crawled, even if some of them would be prerenderable.
* Ensure that SvelteKit can find the route by following links from [`config.kit.prerender.entries`](configuration#prerender) or the [`entries`](#entries) page option. Add links to dynamic routes (i.e. pages with `[parameters]` ) to this option if they are not found through crawling the other entry points, else they are not prerendered because SvelteKit doesn't know what value the parameters should have. Pages not marked as prerenderable will be ignored and their links to other pages will not be crawled, even if some of them would be prerenderable.
* Change `export const prerender = true` to `export const prerender = 'auto'`. Routes with `'auto'` can be dynamically server rendered

## entries

SvelteKit will discover pages to prerender automatically, by starting at _entry points_ and crawling them. By default, all your non-dynamic routes are considered entry points — for example, if you have these routes...

```bash
/ # non-dynamic
/blog # non-dynamic
/blog/[slug] # dynamic, because of `[slug]`
```

...SvelteKit will prerender `/` and `/blog`, and in the process discover links like `<a href="/blog/hello-world">` which give it new pages to prerender.

Most of the time, that's enough. In some situations, links to pages like `/blog/hello-world` might not exist (or might not exist on prerendered pages), in which case we need to tell SvelteKit about their existence.

This can be done with [`config.kit.prerender.entries`](configuration#prerender), or by exporting an `entries` function from a `+page.js` or `+page.server.js` belonging to a dynamic route:

```js
/// file: src/routes/blog/[slug]/+page.server.js
/** @type {import('./$types').EntryGenerator} */
export function entries() {
return [
{ slug: 'hello-world' },
{ slug: 'another-blog-post' }
];
}

export const prerender = true;
```

`entries` can be an `async` function, allowing you to (for example) retrieve a list of posts from a CMS or database, in the example above.

## ssr

Normally, SvelteKit renders your page on the server first and sends that HTML to the client where it's [hydrated](glossary#hydration). If you set `ssr` to `false`, it renders an empty 'shell' page instead. This is useful if your page is unable to be rendered on the server (because you use browser-only globals like `document` for example), but in most situations it's not recommended ([see appendix](glossary#ssr)).
Expand Down
14 changes: 14 additions & 0 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,20 @@ const options = object(
}
),

handleEntryGeneratorMismatch: validate(
(/** @type {any} */ { message }) => {
throw new Error(
message +
`\nTo suppress or handle this error, implement \`handleEntryGeneratorMismatch\` in https://kit.svelte.dev/docs/configuration#prerender`
);
},
(input, keypath) => {
if (typeof input === 'function') return input;
if (['fail', 'warn', 'ignore'].includes(input)) return input;
throw new Error(`${keypath} should be "fail", "warn", "ignore" or a custom function`);
}
),

origin: validate('http://sveltekit-prerender', (input, keypath) => {
assert_string(input, keypath);

Expand Down
19 changes: 14 additions & 5 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { get_option } from '../../utils/options.js';
import {
validate_common_exports,
validate_layout_exports,
validate_layout_server_exports,
validate_page_exports,
validate_page_server_exports,
validate_server_exports
} from '../../utils/exports.js';
import { load_config } from '../config/index.js';
import { forked } from '../../utils/fork.js';
import { should_polyfill } from '../../utils/platform.js';
import { installPolyfills } from '../../exports/node/polyfills.js';
import { resolve_entry } from '../../utils/routing.js';

export default forked(import.meta.url, analyse);

Expand Down Expand Up @@ -72,6 +75,8 @@ async function analyse({ manifest_path, env }) {
let prerender = undefined;
/** @type {any} */
let config = undefined;
/** @type {import('types').PrerenderEntryGenerator | undefined} */
let entries = undefined;

if (route.endpoint) {
const mod = await route.endpoint();
Expand All @@ -95,6 +100,7 @@ async function analyse({ manifest_path, env }) {
if (mod.OPTIONS) api_methods.push('OPTIONS');

config = mod.config;
entries = mod.entries;
}

if (route.page) {
Expand All @@ -109,8 +115,8 @@ async function analyse({ manifest_path, env }) {

for (const layout of layouts) {
if (layout) {
validate_common_exports(layout.server, layout.server_id);
validate_common_exports(layout.universal, layout.universal_id);
validate_layout_server_exports(layout.server, layout.server_id);
validate_layout_exports(layout.universal, layout.universal_id);
}
}

Expand All @@ -119,12 +125,13 @@ async function analyse({ manifest_path, env }) {
if (page.server?.actions) page_methods.push('POST');

validate_page_server_exports(page.server, page.server_id);
validate_common_exports(page.universal, page.universal_id);
validate_page_exports(page.universal, page.universal_id);
}

prerender = get_option(nodes, 'prerender') ?? false;

config = get_config(nodes);
entries ??= get_option(nodes, 'entries');
}

metadata.routes.set(route.id, {
Expand All @@ -136,7 +143,9 @@ async function analyse({ manifest_path, env }) {
api: {
methods: api_methods
},
prerender
prerender,
entries:
entries && (await entries()).map((entry_object) => resolve_entry(route.id, entry_object))
});
}

Expand Down
45 changes: 42 additions & 3 deletions packages/kit/src/core/postbuild/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
}
);

const handle_entry_generator_mismatch = normalise_error_handler(
log,
config.prerender.handleEntryGeneratorMismatch,
({ generatedFromId, entry, matchedId }) => {
return `The entries export from ${generatedFromId} generated entry ${entry}, which was matched by ${matchedId} - see the \`handleEntryGeneratorMismatch\` option in https://kit.svelte.dev/docs/configuration#prerender for more info.`;
}
);

const q = queue(config.prerender.concurrency);

/**
Expand Down Expand Up @@ -164,23 +172,25 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
* @param {string | null} referrer
* @param {string} decoded
* @param {string} [encoded]
* @param {string} [generated_from_id]
*/
function enqueue(referrer, decoded, encoded) {
function enqueue(referrer, decoded, encoded, generated_from_id) {
if (seen.has(decoded)) return;
seen.add(decoded);

const file = decoded.slice(config.paths.base.length + 1);
if (files.has(file)) return;

return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer));
return q.add(() => visit(decoded, encoded || encodeURI(decoded), referrer, generated_from_id));
}

/**
* @param {string} decoded
* @param {string} encoded
* @param {string?} referrer
* @param {string} [generated_from_id]
*/
async function visit(decoded, encoded, referrer) {
async function visit(decoded, encoded, referrer, generated_from_id) {
if (!decoded.startsWith(config.paths.base)) {
handle_http_error({ status: 404, path: decoded, referrer, referenceType: 'linked' });
return;
Expand All @@ -206,6 +216,20 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
}
});

const encoded_id = response.headers.get('x-sveltekit-routeid');
const decoded_id = encoded_id && decode_uri(encoded_id);
if (
decoded_id !== null &&
generated_from_id !== undefined &&
decoded_id !== generated_from_id
) {
handle_entry_generator_mismatch({
generatedFromId: generated_from_id,
entry: decoded,
matchedId: decoded_id
});
}

const body = Buffer.from(await response.arrayBuffer());

save('pages', response, body, decoded, encoded, referrer, 'linked');
Expand Down Expand Up @@ -378,9 +402,18 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
saved.set(file, dest);
}

/** @type {Array<{ id: string, entries: Array<string>}>} */
const route_level_entries = [];
for (const [id, { entries }] of metadata.routes.entries()) {
if (entries) {
route_level_entries.push({ id, entries });
}
}

if (
config.prerender.entries.length > 1 ||
config.prerender.entries[0] !== '*' ||
route_level_entries.length > 0 ||
prerender_map.size > 0
) {
// Only log if we're actually going to do something to not confuse users
Expand All @@ -401,6 +434,12 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
}
}

for (const { id, entries } of route_level_entries) {
for (const entry of entries) {
enqueue(null, config.paths.base + entry, undefined, id);
}
}

await q.done();

// handle invalid fragment links
Expand Down
6 changes: 6 additions & 0 deletions packages/kit/src/core/sync/write_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ function update_types(config, routes, route, to_delete = new Set()) {
.join('; ')} }`
);

if (route.params.length > 0) {
exports.push(
`export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;`
);
}

declarations.push(`type RouteId = '${route.id}';`);

// These could also be placed in our public types, but it would bloat them unnecessarily and we may want to change these in the future
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { stores } from './singletons.js';
import { unwrap_promises } from '../../utils/promises.js';
import * as devalue from 'devalue';
import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js';
import { validate_common_exports } from '../../utils/exports.js';
import { validate_page_exports } from '../../utils/exports.js';
import { compact } from '../../utils/array.js';
import { INVALIDATED_PARAM, validate_depends } from '../shared.js';

Expand Down Expand Up @@ -428,7 +428,7 @@ export function create_client(app, target) {
const node = await loader();

if (DEV) {
validate_common_exports(node.universal);
validate_page_exports(node.universal);
}

if (node.universal?.load) {
Expand Down
13 changes: 9 additions & 4 deletions packages/kit/src/runtime/server/respond.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import { add_cookies_to_headers, get_cookies } from './cookie.js';
import { create_fetch } from './fetch.js';
import { Redirect } from '../control.js';
import {
validate_common_exports,
validate_layout_exports,
validate_layout_server_exports,
validate_page_exports,
validate_page_server_exports,
validate_server_exports
} from '../../utils/exports.js';
Expand Down Expand Up @@ -197,8 +199,11 @@ export async function respond(request, options, manifest, state) {

for (const layout of layouts) {
if (layout) {
validate_common_exports(layout.server, /** @type {string} */ (layout.server_id));
validate_common_exports(
validate_layout_server_exports(
layout.server,
/** @type {string} */ (layout.server_id)
);
validate_layout_exports(
layout.universal,
/** @type {string} */ (layout.universal_id)
);
Expand All @@ -207,7 +212,7 @@ export async function respond(request, options, manifest, state) {

if (page) {
validate_page_server_exports(page.server, /** @type {string} */ (page.server_id));
validate_common_exports(page.universal, /** @type {string} */ (page.universal_id));
validate_page_exports(page.universal, /** @type {string} */ (page.universal_id));
}
}

Expand Down
Loading