Skip to content

Commit

Permalink
SPA mode (#1181)
Browse files Browse the repository at this point in the history
* rename ssr to respond, since ssr is sometimes false

* add tests to adapter-static

* add failing test for #754

* implement fallback rendering

* render fallback page

* update lockfile

* changeset

* add readme for adapter-static

* formatting

* gah

* missing full stop

* remove ESM export for now, no benefit to it

* windows

* lockfile shenanigans

* argh windows

* ugh WHAT NOW windows

* try this, you dumb timewaster
  • Loading branch information
Rich Harris committed Apr 23, 2021
1 parent 1a7ce01 commit 4131467
Show file tree
Hide file tree
Showing 39 changed files with 513 additions and 58 deletions.
6 changes: 6 additions & 0 deletions .changeset/popular-masks-cheat.md
@@ -0,0 +1,6 @@
---
'@sveltejs/adapter-static': patch
'@sveltejs/kit': patch
---

Prerender fallback page for SPAs
2 changes: 2 additions & 0 deletions packages/adapter-static/.gitignore
@@ -1,2 +1,4 @@
.DS_Store
node_modules
.svelte
build
63 changes: 60 additions & 3 deletions packages/adapter-static/README.md
@@ -1,5 +1,62 @@
# adapter-static
# @sveltejs/adapter-static

Adapter for Svelte apps that prerenders your entire site as a collection of static files, which is equivalent to `sapper export`.
[Adapter](https://kit.svelte.dev/docs#adapters) for SvelteKit apps that prerenders your site as a collection of static files.

This is very experimental. The adapter API is still in flux and will likely change before 1.0.
```js
// svelte.config.cjs
const adapter = require('@sveltejs/adapter-static');

module.exports = {
kit: {
adapter: adapter({
// default options are shown
pages: 'build',
assets: 'build',
fallback: null
})
}
};
```

Unless you're in [SPA mode](#spa-mode), the adapter will attempt to prerender every page of your app, regardless of whether the [`prerender`](https://kit.svelte.dev/docs#ssr-and-javascript-prerender) option is set.

## Options

### pages

The directory to write prerendered pages to. It defaults to `build`.

### assets

The directory to write static assets (the contents of `static`, plus client-side JS and CSS generated by SvelteKit) to. Ordinarily this should be the same as `pages`, and it will default to whatever the value of `pages` is, but in rare circumstances you might need to output pages and assets to separate locations.

### fallback

Specify a fallback page for SPA mode, e.g. `index.html` or `200.html` or `404.html`.

## SPA mode

You can use `adapter-static` to create a single-page app or SPA by specifying a **fallback page**.

> 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)).
The fallback page is a blank HTML page that loads your SvelteKit app and navigates to the correct route. For example [Surge](https://surge.sh/help/adding-a-200-page-for-client-side-routing), a static web host, lets you add a `200.html` file that will handle any requests that don't otherwise match. We can create that file like so:

```js
// svelte.config.cjs
const adapter = require('@sveltejs/adapter-static');

module.exports = {
kit: {
adapter: adapter({
fallback: '200.html'
})
}
};
```

When operating in SPA mode, only pages that have the [`prerender`](https://kit.svelte.dev/docs#ssr-and-javascript-prerender) option set will be prerendered.

## License

[MIT](LICENSE)
@@ -1,4 +1,4 @@
module.exports = function ({ pages = 'build', assets = 'build' } = {}) {
module.exports = function ({ pages = 'build', assets = pages, fallback = null } = {}) {
/** @type {import('@sveltejs/kit').Adapter} */
const adapter = {
name: '@sveltejs/adapter-static',
Expand All @@ -8,7 +8,8 @@ module.exports = function ({ pages = 'build', assets = 'build' } = {}) {
utils.copy_client_files(assets);

await utils.prerender({
force: true,
fallback,
all: !fallback,
dest: pages
});
}
Expand Down
14 changes: 12 additions & 2 deletions packages/adapter-static/package.json
Expand Up @@ -3,11 +3,21 @@
"version": "1.0.0-next.4",
"scripts": {
"lint": "eslint --ignore-path .gitignore \"**/*.{ts,js,svelte}\" && npm run check-format",
"check": "tsc",
"format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore",
"check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore"
"check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
"test": "uvu test test.js"
},
"devDependencies": {
"@sveltejs/kit": "workspace:*",
"typescript": "^4.2.3"
"playwright-chromium": "^1.10.0",
"port-authority": "^1.1.2",
"sirv": "^1.0.11",
"typescript": "^4.2.4"
},
"type": "module",
"main": "index.cjs",
"exports": {
"require": "./index.cjs"
}
}
5 changes: 5 additions & 0 deletions packages/adapter-static/test/apps/prerendered/.gitignore
@@ -0,0 +1,5 @@
.DS_Store
node_modules
/.svelte
/build
/functions
1 change: 1 addition & 0 deletions packages/adapter-static/test/apps/prerendered/.npmrc
@@ -0,0 +1 @@
engine-strict=true
15 changes: 15 additions & 0 deletions packages/adapter-static/test/apps/prerendered/package.json
@@ -0,0 +1,15 @@
{
"name": "~TODO~",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"start": "svelte-kit start"
},
"devDependencies": {
"@sveltejs/kit": "next",
"svelte": "^3.29.0",
"vite": "^2.1.0"
},
"type": "module"
}
11 changes: 11 additions & 0 deletions packages/adapter-static/test/apps/prerendered/src/app.html
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/adapter-static/test/apps/prerendered/src/global.d.ts
@@ -0,0 +1,3 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="svelte" />
/// <reference types="vite/client" />
@@ -0,0 +1 @@
<h1>This page was prerendered</h1>
@@ -0,0 +1,7 @@
/** @type {import('@sveltejs/kit').Config} */
module.exports = {
kit: {
adapter: require('../../../index.cjs')(),
target: '#svelte'
}
};
5 changes: 5 additions & 0 deletions packages/adapter-static/test/apps/spa/.gitignore
@@ -0,0 +1,5 @@
.DS_Store
node_modules
/.svelte
/build
/functions
1 change: 1 addition & 0 deletions packages/adapter-static/test/apps/spa/.npmrc
@@ -0,0 +1 @@
engine-strict=true
38 changes: 38 additions & 0 deletions packages/adapter-static/test/apps/spa/README.md
@@ -0,0 +1,38 @@
# create-svelte

Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);

## Creating a project

If you're seeing this, you've probably already done this step. Congrats!

```bash
# create a new project in the current directory
npm init svelte@next

# create a new project in my-app
npm init svelte@next my-app
```

> Note: the `@next` is temporary
## Developing

Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:

```bash
npm run dev

# or start the server and open the app in a new browser tab
npm run dev -- --open
```

## Building

Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:

```bash
npm run build
```

> You can preview the built app with `npm start`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.
9 changes: 9 additions & 0 deletions packages/adapter-static/test/apps/spa/jsconfig.json
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}
16 changes: 16 additions & 0 deletions packages/adapter-static/test/apps/spa/package.json
@@ -0,0 +1,16 @@
{
"name": "~TODO~",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"start": "svelte-kit start"
},
"devDependencies": {
"@sveltejs/adapter-node": "next",
"@sveltejs/kit": "next",
"svelte": "^3.29.0",
"vite": "^2.1.0"
},
"type": "module"
}
11 changes: 11 additions & 0 deletions packages/adapter-static/test/apps/spa/src/app.html
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/adapter-static/test/apps/spa/src/global.d.ts
@@ -0,0 +1,3 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="svelte" />
/// <reference types="vite/client" />
@@ -0,0 +1,6 @@
<nav>
<a href="/">home</a>
<a href="/about">about</a>
</nav>

<slot></slot>
5 changes: 5 additions & 0 deletions packages/adapter-static/test/apps/spa/src/routes/about.svelte
@@ -0,0 +1,5 @@
<script context="module">
export const prerender = true;
</script>

<h1>This page was prerendered</h1>
@@ -0,0 +1 @@
<h1>This page was not prerendered</h1>
9 changes: 9 additions & 0 deletions packages/adapter-static/test/apps/spa/svelte.config.cjs
@@ -0,0 +1,9 @@
/** @type {import('@sveltejs/kit').Config} */
module.exports = {
kit: {
adapter: require('../../../index.cjs')({
fallback: '200.html'
}),
target: '#svelte'
}
};
33 changes: 33 additions & 0 deletions packages/adapter-static/test/test.js
@@ -0,0 +1,33 @@
import fs from 'fs';
import * as assert from 'uvu/assert';
import { run } from './utils.js';

run('prerendered', (test) => {
test('generates HTML files', ({ cwd }) => {
assert.ok(fs.existsSync(`${cwd}/build/index.html`));
});

test('prerenders content', async ({ base, page }) => {
await page.goto(base);
assert.equal(await page.textContent('h1'), 'This page was prerendered');
});
});

run('spa', (test) => {
test('generates a fallback page', ({ cwd }) => {
assert.ok(fs.existsSync(`${cwd}/build/200.html`));
});

test('does not prerender pages without prerender=true', ({ cwd }) => {
assert.ok(!fs.existsSync(`${cwd}/build/index.html`));
});

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

test('renders content in fallback page when JS runs', async ({ base, page }) => {
await page.goto(base);
assert.equal(await page.textContent('h1'), 'This page was not prerendered');
});
});

0 comments on commit 4131467

Please sign in to comment.