Skip to content

Commit

Permalink
ssr/router/hydrate options (SPA mode! 0kb JS pages!) (#713)
Browse files Browse the repository at this point in the history
* documentation for ssr/router/hydrate (#231)

* buncha failing tests

* make ssr disable-able

* implement hydrate option

* prevent router from initing if router=false

* make router=false work for already inited apps

* implement page-level options

* rename doc section

* address feedback

* clarify
  • Loading branch information
Rich Harris authored Mar 29, 2021
1 parent e053a6b commit a59fbd0
Show file tree
Hide file tree
Showing 26 changed files with 531 additions and 245 deletions.
31 changes: 0 additions & 31 deletions documentation/docs/11-prerendering.md

This file was deleted.

81 changes: 81 additions & 0 deletions documentation/docs/11-ssr-and-javascript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
title: SSR and JavaScript
---

By default, SvelteKit will server-render pages on demand, then **hydrate** the rendered HTML in the client with an interactive Svelte app while initialising a **router** that takes over subsequent navigations.

You can control each of these on a per-app or per-page basis. Note that each of the per-page settings use [`context="module"`](https://svelte.dev/docs#script_context_module), and only apply to page components, _not_ [layout](#layouts) components.

If both are specified, per-page settings override per-app settings in case of conflicts. Each setting can be controlled independently, but `ssr` and `hydrate` cannot both be `false` since that would result in nothing being rendered at all.

### ssr

Disabling server-side rendering effectively turns your SvelteKit app into a **single-page app** or SPA.

> In most situations this is not recommended: it harms SEO, tends to slow down perceived performance, and makes your app inaccessible to users if JavaScript fails or is disabled (which happens [more often than you probably think](https://kryogenix.org/code/browser/everyonehasjs.html)). Sometimes it's appropriate or even necessary, but consider alternatives before disabling SSR.
You can disable SSR app-wide with the [`ssr` config option](#configuration-ssr), or a page-level `ssr` export:

```html
<script context="module">
export const ssr = false;
</script>
```

### router

SvelteKit includes a client-side router that intercepts navigations (from the user clicking on links, or interacting with the back/forward buttons) and updates the page contents, rather than letting the browser handle the navigation by reloading.

In certain circumstances you might need to disable this behaviour with the app-wide [`router` config option](#configuration-router) or the page-level `router` export:

```html
<script context="module">
export const router = false;
</script>
```

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

### hydrate

Ordinarily, SvelteKit 'hydrates' 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 [`hydrate` config option](#configuration-hydrate) or the page-level `hydrate` export:

```html
<script context="module">
export const hydrate = false;
</script>
```

> If `hydrate` and `router` are both `false`, SvelteKit will not add any JavaScript to the page at all.
### prerender

It's likely that at least some pages of your app can be represented as a simple HTML file, since they contain no dynamic or user-specific data. These pages can be _prerendered_ by your [adapter](#adapters).

If your entire app is suitable for prerendering, you could use [`adapter-static`](https://github.com/sveltejs/kit/tree/master/packages/adapter-static), which will generate HTML files for every page, plus additional files that are requested by `load` functions in those pages.

In many cases, you'll only want to prerender specific pages in your app. You'll need to annotate these pages:

```html
<script context="module">
export const prerender = true;
</script>
```

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 `pages` option in the [prerender configuration](#configuration-prerender).

#### 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.

> In other words, any app that involves user sessions or authentication is _not_ a candidate for `adapter-static`, even if individual pages within an app _are_ suitable for prerendering.
Note that you can still prerender pages that load data based on the page's parameters, like our `src/routes/blog/[slug].svelte` example from earlier. The prerenderer will intercept requests made inside `load`, so the data served from `src/routes/blog/[slug].json.js` will also be captured.

#### Route conflicts

Because prerendering writes to the filesystem, it isn't possible to have two endpoints that would cause a directory and a file to have the same name. For example, `src/routes/foo/index.js` and `src/routes/foo/bar.js` would try to create `foo` and `foo/bar`, which is impossible.

For that reason among others, it's recommended that you always include a file extension — `src/routes/foo/index.json.js` and `src/routes/foo/bar.json.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`.
15 changes: 15 additions & 0 deletions documentation/docs/13-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
},
host: null,
hostHeader: null,
hydrate: true,
paths: {
assets: '',
base: ''
Expand All @@ -37,6 +38,8 @@ module.exports = {
force: false,
pages: ['*']
},
router: true,
ssr: true,
target: null,
vite: () => ({})
},
Expand Down Expand Up @@ -88,6 +91,10 @@ module.exports = {

**You should only do this if you trust the reverse proxy**, which is why it isn't the default.

#### hydrate

Whether to [hydrate](#ssr-and-javascript-hydrate) the server-rendered HTML with a client-side app. (It's rare that you would set this to `false` on an app-wide basis.)

#### paths

An object containing zero or more of the following `string` values:
Expand All @@ -104,6 +111,14 @@ See [Prerendering](#prerendering). An object containing zero or more of the foll
- `force` — if `true`, a page that fails to render will _not_ cause the entire build to fail
- `pages` — 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]` )

#### router

Enables or disables the client-side [router](#ssr-and-javascript-router) app-wide.

#### ssr

Enables or disables [server-side rendering](#ssr-and-javascript-ssr) app-wide.

#### target

Specifies an element to mount the app to. It must be a DOM selector that identifies an element that exists in your template file. If unspecified, the app will be mounted to `document.body`.
Expand Down
1 change: 1 addition & 0 deletions examples/hn.svelte.dev/src/routes/about.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script context="module">
export const hydrate = false;
export const prerender = true;
</script>

Expand Down
2 changes: 2 additions & 0 deletions examples/hn.svelte.dev/src/routes/user/[name].svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script context="module">
export const hydrate = false;
export async function load({ page, fetch }) {
const res = await fetch(`https://api.hnpwa.com/v0/user/${page.params.name}.json`);
const user = await res.json();
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/core/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,10 @@ async function build_server(
get_component_path: id => ${s(`${config.kit.paths.assets}/${config.kit.appDir}/`)} + client_component_lookup[id],
get_stack: error => error.stack,
get_static_file,
get_amp_css: dep => amp_css_lookup[dep]
get_amp_css: dep => amp_css_lookup[dep],
ssr: ${s(config.kit.ssr)},
router: ${s(config.kit.router)},
hydrate: ${s(config.kit.hydrate)}
});
}
`
Expand Down
5 changes: 4 additions & 1 deletion packages/kit/src/core/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,10 @@ class Watcher extends EventEmitter {
},
get_static_file: (file) =>
fs.readFileSync(path.join(this.config.kit.files.assets, file)),
get_amp_css: (url) => '' // TODO: implement this
get_amp_css: (url) => '', // TODO: implement this
ssr: this.config.kit.ssr,
router: this.config.kit.router,
hydrate: this.config.kit.hydrate
}
);

Expand Down
6 changes: 6 additions & 0 deletions packages/kit/src/core/load_config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test('fills in defaults', () => {
},
host: null,
hostHeader: null,
hydrate: true,
paths: {
base: '',
assets: '/.'
Expand All @@ -35,6 +36,8 @@ test('fills in defaults', () => {
force: false,
pages: ['*']
},
router: true,
ssr: true,
target: null
},
preprocess: null
Expand Down Expand Up @@ -102,6 +105,7 @@ test('fills in partial blanks', () => {
},
host: null,
hostHeader: null,
hydrate: true,
paths: {
base: '',
assets: '/.'
Expand All @@ -112,6 +116,8 @@ test('fills in partial blanks', () => {
force: false,
pages: ['*']
},
router: true,
ssr: true,
target: null
},
preprocess: null
Expand Down
6 changes: 6 additions & 0 deletions packages/kit/src/core/load_config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const options = {

hostHeader: expect_string(null),

hydrate: expect_boolean(true),

paths: {
type: 'branch',
children: {
Expand Down Expand Up @@ -112,6 +114,10 @@ const options = {
}
},

router: expect_boolean(true),

ssr: expect_boolean(true),

target: expect_string(null),

vite: {
Expand Down
3 changes: 3 additions & 0 deletions packages/kit/src/core/load_config/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ suite('load default config', async () => {
},
host: null,
hostHeader: null,
hydrate: true,
paths: { base: '', assets: '/.' },
prerender: { crawl: true, enabled: true, force: false, pages: ['*'] },
router: true,
ssr: true,
target: null
},
preprocess: null
Expand Down
Loading

0 comments on commit a59fbd0

Please sign in to comment.