tldr
repo: https://github.com/gobeli/11ty-svelte
demo: https://gobeli.github.io/11ty-svelte (have a look at the network tab and see the prerendered markup)
Intro
Back in June I wrote a post about prerendering svelte components. You can check it out here. The article gives a basic overview of how you would go about prerendering a svelte app. The approach used is not really sophisticated and integrated easily with existing sites / static site generators (SSGs).
Recently I have become quite fond of eleventy and have used it for some projects, that is why I would like to expand on the previous post by giving an example of integrating svelte prerendering in an 11ty website.
Why though?
Static websites and SSGs are awesome, but often times parts of our websites are dynamic and need a bit of JavaScript. Svelte is great at integrating into an existing site and does not require the whole app to be written in it. For SEO and performance purposes, it's a good idea to prerender the dynamic parts of your website and not just build them at runtime in the browser.
Let's get into it
Overview
We will be writing our 11ty website with the Nunjucks templating language and leveraging shortcodes and other eleventy features to create our demo site.
Furthermore we will use rollup to generate the code for the prerendering as well as the client side bundle.
Creating the Site
The site we will create is pretty basic, there will be a single index.html
and one svelte component which will be included in the index page.
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<main>
<h1>Svelte x 11ty</h1>
{% svelte "Test.svelte" %}
</main>
<script async defer src="scripts/index.js"></script>
</body>
</html>
I have already added a svelte
shortcode in here, which is not yet defined as well as a script
which we also need to implement.
The Svelte Component
Our svelte component is quite simple, it takes a name and makes it editable via an input:
<script>
export let name = 'Etienne';
</script>
<input type="text" bind:value={name}> My name is {name}
Shortcode for Prerendering
Shortcodes can be used to create reusable content within an eleventy site. This is perfect for reusable svelte components. The shortcode will take the name of a svelte file as well as optional props for the component. It will then create an SSR bundle of the component and immediately invoke it to return the static html.
First let's create a function to render the component out as html. The components markup itself is not enough since the client side bundle needs to have a root which it can use to hydrate the component. We also make sure, that the static props are passed down to the template via a data
-attribute:
function renderComponent(component, filename, props) {
return `
<div class="svelte--${filename}" data-props='${JSON.stringify(props || {})}'>
${component.render(props).html}
</div>
`
}
Next, let's create the actual shortcode used within the index.html
:
const path = require('path')
const rollup = require('rollup');
const svelte = require('rollup-plugin-svelte');
module.exports = async function svelteShortcode(filename, props) {
// find the component which is requested
const input = path.join(process.cwd(), 'src', 'content', 'scripts', 'components', filename);
// create the rollup ssr build
const build = await rollup
.rollup({
input,
plugins: [
svelte({
generate: 'ssr',
hydratable: true,
css: false,
}),
],
external: [/^svelte/],
});
// generate the bundle
const { output: [ main ] } = await build.generate({
format: 'cjs',
exports: 'named',
})
if (main.facadeModuleId) {
const Component = requireFromString(main.code, main.facadeModuleId).default;
return renderComponent(Component, filename, props);
}
}
The requireFromString
function is used to immediately require the rollup generated code from memory. (See https://stackoverflow.com/questions/17581830/load-node-js-module-from-string-in-memory).
Make sure to add the shortcode in your .eleventyconfig.js
as an NunjucksAsyncShortcode
: config.addNunjucksAsyncShortcode('svelte', svelte);
Now, if we run npx eleventy
we can already see how the component is rendered into the final output:
<div class="svelte--Test.svelte" data-props='{}'>
<input type="text" value="Etienne"> My name is Etienne
</div>
To see the props in action just add your own name as the second parameter of the shortcode in the index.html
, like this: {% svelte "Test.svelte", { name: 'not Etienne' } %}
and the output will be:
<div class="svelte--Test.svelte" data-props='{"name":"not Etienne"}'>
<input type="text" value="not Etienne"> My name is not Etienne
</div>
Hydrate
So far so good, but half of the fun of svelte is it's dynamic capabilities within the browser, so let's make sure we can hydrate the markup which we already have.
To do that we will first create an entry point for the client side code. Let's create a new JS file and within it a function which gets the wrapper around the svelte components by their class and hydrates them:
function registerComponent (component, name) {
document.querySelectorAll(`.${CSS.escape(name)}`).forEach($el => {
// parse the props given from the dataset
const props = JSON.parse($el.dataset.props);
new component({
target: $el,
props,
hydrate: true
})
})
}
The CSS.escape
is needed, because we have a .
in our class name.
To register a component just use the function and pass the css class to it:
import Test from './components/Test.svelte';
registerComponent(Test, 'svelte--Test.svelte');
Awesome, only one more step to go: We need to compile the client side code for it to run in the browser. To do that, let's create a new eleventy JavaScript page, it's not going to be an actual html page but a JavaScript bundle.
Within the page similarly to the shortcode we will create a rollup bundle, but this time, it will be compiled for client side use and return the JS code and not the rendered html:
const rollup = require('rollup');
const svelte = require('rollup-plugin-svelte');
const nodeResolve = require('@rollup/plugin-node-resolve');
const path = require('path')
module.exports = class Scripts {
data () {
return {
permalink: '/scripts/index.js',
eleventyExcludeFromCollections: true,
}
}
async render () {
const build = await rollup.rollup({
input: path.join(process.cwd(), 'src', 'content', 'scripts', 'index.js'),
plugins: [
svelte({
hydratable: true,
}),
nodeResolve.default({
browser: true,
dedupe: ['svelte'],
}),
]
});
const { output: [ main ] } = await build.generate({
format: 'iife',
});
if (main.facadeModuleId) {
return main.code;
}
}
}
Et voila, your component is hydrated and the app is fully functional.
Next steps
Here are some ideas to expand on this simple prototype:
- Use terser to minify the client side bundle in production
- Handle css used within the svelte component
- Handle content which is written into the
head
from the components - Make the directory of your svelte component configurable
Top comments (9)
Clean and simple. Thanks!
Thank you Joakim!
There's just one small issue (there always is isn't there!). If I
import {…} from date-fns
in a Svelte component, Rollup will write'date-fns' is imported by […].svelte, but could not be resolved – treating it as an external dependency
to the terminal on every build. Do you know if there's a way to silence that message? I think it comes from@rollup/plugin-node-resolve
.Yeah, external dependencies are not resolved for the SSR build, to silence the warning you can explicitly treat
date-fns
as an external dependency. To do that, just add it to theexternal:
array in thesvelte.js
shortcode, it should then look like this:external: [/^svelte/, 'date-fns'],
Nice, thank you so much :)
this is very cool!
Thanks for the feedback Ryan. Appreciate it!
Awesome! I'm also looking for a something similar and your post help me alot. Thank you! <3
Thanks! I learn something new today