Recently I have been working on a Single Page Application (SPA) using Svelte where some pages like Terms of Service were static HTML pages. This had the downside that when the user clicked on a link to one of these pages he would leave the SPA and trigger a page load, potentially loosing application state when hitting the browsers back button. So I decided to convert these static HTML pages to Svelte components. As these pages mainly contain legal content I thought it might be still a good idea to have them available as static HTML pages. But what I clearly did not want to do is to maintain two versions of the same content, one static page and one Svelte component, so I sat down and added static rendering of selected components to the rollup build.
Note that there are full-featured frameworks that use a technique named Server-Side Rendering (SSR) to solve similar problems. The one available for Svelte is Sapper.
Sapper is a great choice if:
- Your SPA has more than just a few well-known static pages,
- Search-Engine Optimization (SEO) is important to you,
- time to first paint matters,
- or your backend already uses node.js.
None of these applies to our SPA. In our case the backend is written in Rust so requiring a second backend that just serves the purpose of SSR for a dozen pages would have been total overkill.
Rendering a Svelte component to HTML
Let’s say you have a Test
component living in ./Test.svelte
:
<script>
export let title;
</script>
<div>{title}</div>
To render this to HTML we use the following script render.js
:
require("svelte/register");
console.log(require("./Test.svelte").default.render({title: "Hello world"}).html);
Running node render.js
outputs the HTML on the console:
<div>Hello world</div>
As Svelte components can contain component-local CSS, the result of the
render()
function also contains a css
field (and also a head
field).
Integration with rollup
I use a very simple template for all my statically generated HTML pages. This is also used to generate
the root index.html
:
<!-- file: index.template.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>%%TITLE%%</title>
<link ref="icon" type="image/png" href="/favicon.png" />
<link rel="stylesheet" href="/css/main.css" />
%%SCRIPTS%%
</head>
<body>
%%BODY%%
</body>
</html>
As can be seen below in the excerpt of my rollup.config.js
, I use
@rollup/plugin-html
to create HTML pages from within rollup (note that I
create the required directory structure outside of rollup). In the template
shown above we want to replace %%BODY%%
with the rendered output of the
Svelte component. makeTemplate()
function below is a little bit more complex
than it has to be. This is because I create a bundle-[hash].js
file and from
within makeTemplate()
I do not know the actual value of hash
. So instead, I
scan through all the entries of bundle
and look for any javascript file and
inject it into the template at the %%SCRIPTS%%
location. If you don’t use a
hashed bundle you can simply get rid of the scripts
code and replace
%%SCRIPTS%%
for instance with <script defer src="/bundle.js"></script>
.
// file: rollup.config.js
import svelte from "rollup-plugin-svelte";
import html from "@rollup/plugin-html"
import fs from "fs";
require("svelte/register");
function makeTemplate(templateData, svelteComponent) {
return ({ attributes, bundle, files, publicPath, title }) => {
const scripts = Object.entries(bundle)
.filter(([key, value]) => value.isEntry && key.endsWith(".js"))
.map(([key, value]) => `<script defer src="/${key}"></script>`)
.join();
const body = svelteComponent.render({}).html;
return templateData
.replace("%%BODY%%", body)
.replace("%%TITLE%%", title)
.replace("%%SCRIPTS%%", scripts);
};
}
export default {
// ...
plugins: [
svelte({
// ...
hydratable: true, // This is important!
}),
html({
fileName: "privacy/index.html",
title: "Privacy Policy",
template: makeTemplate(
fs.readFileSync("./index.template.html", "utf8"),
require("./src/App/components/PrivacyPage.svelte").default
),
}),
// ... more calls to html({})
],
}
The hydratable: true
option will tell Svelte to re-render the page using the dynamic Svelte component.
This is all it needs to add static rendering of Svelte components to a project.