Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[breaking] ssr/hydrate/router/prerender.default are now configurable in +page(.server).js and +layout(.server).js #6197

Merged
merged 27 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7a617f9
[feat] allow ssr to be configurable in +page.js and +layout.js
dummdidumm Aug 23, 2022
0bb5cd3
fix tests + docs
dummdidumm Aug 23, 2022
812e1b5
Update documentation/docs/12-page-options.md
dummdidumm Aug 24, 2022
3b60c40
Update documentation/docs/12-page-options.md
dummdidumm Aug 24, 2022
9a001dd
Update documentation/docs/12-page-options.md
dummdidumm Aug 24, 2022
6059112
Merge branch 'master' into ssr-option
dummdidumm Aug 27, 2022
0a62ec8
remove browser options in favor of layout/page exports
dummdidumm Aug 27, 2022
4f61f59
remove ssr option from handle, update docs
dummdidumm Aug 27, 2022
b63114a
test
dummdidumm Aug 27, 2022
5f72158
update changelog
dummdidumm Aug 27, 2022
396fea1
make prerender option work in layouts
dummdidumm Aug 28, 2022
dba1c56
adjust test
dummdidumm Aug 28, 2022
ccb5fcb
update SEO docs
Rich-Harris Aug 28, 2022
320bd87
Merge branch 'master' into ssr-option
dummdidumm Aug 30, 2022
fb8efce
remove prerender.default
dummdidumm Aug 30, 2022
94f3077
how did i miss these
dummdidumm Aug 30, 2022
332913e
merge master
Rich-Harris Aug 30, 2022
f0a69c3
fix bad link
Rich-Harris Aug 30, 2022
7f38aed
fix docs
Rich-Harris Aug 30, 2022
3db97a0
tweak docs
Rich-Harris Aug 30, 2022
195b75f
i think this order is slightly more logical
Rich-Harris Aug 30, 2022
ac666ad
add detail about prerendering server routes
Rich-Harris Aug 30, 2022
014a1da
typo
Rich-Harris Aug 30, 2022
dff921a
put link first
Rich-Harris Aug 30, 2022
8de3d4c
small tweak
Rich-Harris Aug 30, 2022
4dcee25
Update documentation/docs/03-routing.md
dummdidumm Aug 30, 2022
d111a21
Revert "Update documentation/docs/03-routing.md"
Rich-Harris Aug 30, 2022
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/poor-gifts-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[feat] allow ssr to be configurable in +page.js and +layout.js
25 changes: 20 additions & 5 deletions documentation/docs/12-page-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
title: Page options
---

By default, SvelteKit will render any component first on the server and send it to the client as HTML. It will then render the component again in the browser to make it interactive in a process called **hydration**. For this reason, you need to ensure that components can run in both places. SvelteKit will then initialise a [**router**](/docs/routing) that takes over subsequent navigations.

You can control each of these on a per-app (via `svelte.config.js`) or per-page (via `+page.js` or `+page.server.js`) basis. If both are specified, per-page settings override per-app settings in case of conflicts.
By default, SvelteKit will render any component first on the server and send it to the client as HTML. It will then render the component again in the browser to make it interactive in a process called **hydration**. For this reason, you need to ensure that components can run in both places. SvelteKit will then initialise a [**router**](/docs/routing) that takes over subsequent navigations. You can control this behavior through several options:

### router

Expand All @@ -13,23 +11,27 @@ SvelteKit includes a [client-side router](/docs/appendix#routing) that intercept
In certain circumstances you might need to disable [client-side routing](/docs/appendix#routing) with the app-wide [`browser.router` config option](/docs/configuration#browser) or the page-level `router` export:

```js
/// file: +page.js/+page.server.js
/// file: +page.js/+page.js
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
export const router = false;
```

Note that this will disable client-side routing for any navigation from this page, regardless of whether the router is already active.

You can control this setting on a per-app (via `svelte.config.js`) or per-page (via `+page.js`) basis. If both are specified, per-page settings override per-app settings in case of conflicts.

### hydrate

Ordinarily, SvelteKit [hydrates](/docs/appendix#hydration) your server-rendered HTML into an interactive page. Some pages don't require JavaScript at all — many blog posts and 'about' pages fall into this category. In these cases you can skip hydration when the app boots up with the app-wide [`browser.hydrate` config option](/docs/configuration#browser) or the page-level `hydrate` export:

```js
/// file: +page.js/+page.server.js
/// file: +page.js/+page.js
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
export const hydrate = false;
```

> If `hydrate` and `router` are both `false`, SvelteKit will not add any JavaScript to the page at all. If [server-side rendering](/docs/hooks#handle) is disabled in `handle`, `hydrate` must be `true` or no content will be rendered.

You can control this setting on a per-app (via `svelte.config.js`) or per-page (via `+page.js`) basis. If both are specified, per-page settings override per-app settings in case of conflicts.

### prerender

It's likely that at least some pages of your app can be represented as a simple HTML file generated at build time. These pages can be [_prerendered_](/docs/appendix#prerendering).
Expand All @@ -52,6 +54,8 @@ export const prerender = false;

The prerenderer will start at the root of your app and generate HTML for any prerenderable pages 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](/docs/configuration#prerender).

You can control this setting on a per-app (via `svelte.config.js`) or per-page (via `+page.js` or `+page.server.js`) basis. If both are specified, per-page settings override per-app settings in case of conflicts.

#### When not to prerender

The basic rule is this: for a page to be prerenderable, any two users hitting it directly must get the same content from the server.
Expand All @@ -69,3 +73,14 @@ Because prerendering writes to the filesystem, it isn't possible to have two end
For that reason among others, it's recommended that you always include a file extension — `src/routes/foo.json/+server.js` and `src/routes/foo/bar.json/+server.js` would result in `foo.json` and `foo/bar.json` files living harmoniously side-by-side.

For _pages_, we skirt around this problem by writing `foo/index.html` instead of `foo`.

### ssr

Normally, SvelteKit renders your page on the server first and sends that HTML to the client where it's hydrated. If you set `ssr` to `false`, it renders an empty 'shell' page instead. This is useful if your page accesses browser-only methods or objects, but in most situations it's not recommended ([see appendix](/docs/appendix#ssr)).

```js
/// file: +page.js/+page.js
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
export const ssr = false;
```

In contrast to the other options, you can set this option in both `+page.js` and `+layout.js`. `ssr` options in subsequent layouts or the page overwrite earlier options. You cannot set this option in `+page.server.js` or `+layout.server.js`. This option overwrites the `ssr` option [in the handle hook](/docs/hooks#handle).
32 changes: 13 additions & 19 deletions packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export async function render_page(event, route, options, state, resolve_opts) {
options.manifest._.nodes[route.leaf]()
]);

resolve_opts = {
...resolve_opts,
ssr:
nodes.reduce(
(ssr, node) => node?.shared?.ssr ?? ssr,
/** @type {boolean|undefined} */ (undefined)
) ?? resolve_opts.ssr
};

const leaf_node = /** @type {import('types').SSRNode} */ (nodes.at(-1));

let status = 200;
Expand Down Expand Up @@ -304,7 +313,10 @@ export async function render_page(event, route, options, state, resolve_opts) {
options,
state,
resolve_opts,
page_config: get_page_config(leaf_node, options),
page_config: {
router: leaf_node.shared?.router ?? options.router,
hydrate: leaf_node.shared?.hydrate ?? options.hydrate
},
status,
error: null,
branch: compact(branch),
Expand All @@ -328,24 +340,6 @@ export async function render_page(event, route, options, state, resolve_opts) {
}
}

/**
* @param {import('types').SSRNode} leaf
* @param {SSROptions} options
*/
function get_page_config(leaf, options) {
// TODO we can reinstate this now that it's in the module
if (leaf.shared && 'ssr' in leaf.shared) {
throw new Error(
'`export const ssr` has been removed — use the handle hook instead: https://kit.svelte.dev/docs/hooks#handle'
);
}

return {
router: leaf.shared?.router ?? options.router,
hydrate: leaf.shared?.hydrate ?? options.hydrate
};
}

/**
* @param {import('types').RequestEvent} event
* @param {import('types').SSROptions} options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
document;
</script>

{document}

<p>Works</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ssr = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script context="module">
document;
</script>

{document}

<p>Works</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ssr = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script context="module">
document;
</script>

{document}

<p>Works</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ssr = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script context="module">
document;
</script>

{document}

<p>You shouldn't see this</p>
40 changes: 36 additions & 4 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,42 @@ test.describe('Shadow DOM', () => {
});
});

test('Can use browser-only global on client-only page', async ({ page, read_errors }) => {
await page.goto('/no-ssr/browser-only-global');
await expect(page.locator('p')).toHaveText('Works');
expect(read_errors('/no-ssr/browser-only-global')).toBe(undefined);
test.describe('SPA mode / no SSR', () => {
test('Can use browser-only global on client-only page through ssr config in handle', async ({
page,
read_errors
}) => {
await page.goto('/no-ssr/browser-only-global');
await expect(page.locator('p')).toHaveText('Works');
expect(read_errors('/no-ssr/browser-only-global')).toBe(undefined);
});

test('Can use browser-only global on client-only page through ssr config in layout.js', async ({
page,
read_errors
}) => {
await page.goto('/no-ssr/ssr-page-config');
await expect(page.locator('p')).toHaveText('Works');
expect(read_errors('/no-ssr/ssr-page-config')).toBe(undefined);
});

test('Can use browser-only global on client-only page through ssr config in page.js', async ({
page,
read_errors
}) => {
await page.goto('/no-ssr/ssr-page-config/layout/inherit');
await expect(page.locator('p')).toHaveText('Works');
expect(read_errors('/no-ssr/ssr-page-config/layout/inherit')).toBe(undefined);
});

test('Cannot use browser-only global on page because of ssr config in page.js', async ({
page
}) => {
await page.goto('/no-ssr/ssr-page-config/layout/overwrite');
await expect(page.locator('p')).toHaveText(
'This is your custom error page saying: "document is not defined"'
);
});
});

test('can use $app/stores from anywhere on client', async ({ page }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/kit/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export interface SSRNode {
hydrate?: boolean;
prerender?: boolean;
router?: boolean;
ssr?: boolean;
};

server: {
Expand Down