DEV Community

loading...
Cover image for Svelte x 11ty

Svelte x 11ty

Etienne
Updated on ・5 min read

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>
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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>
  `
} 
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Photo by Sigmund on Unsplash

Discussion (7)

Collapse
joakim profile image
Joakim

Clean and simple. Thanks!

Collapse
gobeli profile image
Etienne Author

Thank you Joakim!

Collapse
joakim profile image
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.

Thread Thread
gobeli profile image
Etienne Author

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 the external: array in the svelte.js shortcode, it should then look like this: external: [/^svelte/, 'date-fns'],

Thread Thread
joakim profile image
Joakim

Nice, thank you so much :)

Collapse
ryanfiller profile image
ryanfiller

this is very cool!

Collapse
gobeli profile image
Etienne Author

Thanks for the feedback Ryan. Appreciate it!