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

SPA mode #1181

Merged
merged 18 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
benmccann marked this conversation as resolved.
Show resolved Hide resolved
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 } = {}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this file need to be renamed to .cjs?

Copy link
Contributor

@ivoreis ivoreis Apr 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/** @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"
Conduitry marked this conversation as resolved.
Show resolved Hide resolved
}
}
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');
});
});