Kyle Sferrazza

notes, posts, /etc

Svelte Components in Hugo

Making interactive components simple in Hugo.

5 minute read

Try this interactive component:

This component was created using Svelte, which is a javascript (non-)framework for creating interactive components. Svelte components are defined in self-contained .svelte files, which describe all of the CSS styles, HTML markup, and javascript functionality necessary for the component to run. The Svelte compiler is then run on these .svelte files, and produces vanilla JS from them, which can be used anywhere.

This article details how to integrate Svelte components into a Hugo site (like the one you’re on now).

Installing Dependencies

All of the paths starting with / in the remainder of the article refer to the root of your Hugo site’s repository.

Rollup is a javascript “module bundler”. We can configure it to automatically find Svelte files in a particular directory, and compile them into /static. As long as Rollup runs before Hugo, we can then include those compiled JS files into the Hugo site as if we had defined them by hand in the /static directory.

In order to start using Svelte, install yarn and use yarn init to set up package.json with some initial values.

Install Rollup and the plugins we’ll be using:

yarn add -D rollup @rollup/plugin-node-resolve @rollup/plugin-replace

Install Svelte and its associated Rollup plugin:

yarn add -D svelte rollup-plugin-svelte

Install Typescript (typescript is optional, just a personal preference):

yarn add -D @rollup/plugin-typescript tslib typescript

Directory Structure

The rollup config file can be stored anywhere, but I chose the following directory structure in my Hugo site for simplicity:

svelte/
├─ setup/                     # files needed for svelte setup
│  ├─ rollup.config.ts        # the Rollup configuration
│  └─ embed.js                # more on this later, will be used to# facilitate including Svelte components# in the middle of an article
└─ components/                # a directory to hold all of the Svelte components
   └─ HelloFromSvelte.svelte  # the demo component you see at the top of this article

Configuring Rollup

The next step is configuring Rollup, so that it knows where to find Svelte components, and where to install their compiled vanilla javascript files.

This article from codeandlife.com made setting up Rollup for embeddable Svelte components much easier.

My rollup.config.ts file looks as follows:

import fs from 'fs';

import replace from '@rollup/plugin-replace';
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import { RollupOptions } from 'rollup';

const COMPONENT_DIR = './svelte/components';

// Search for .svelte files in /content and place js files with matching URL tree in /static

interface SvelteComponentLocation {
  // The path to the .svelte file relative to /svelte/setup/
  inputPath: string;

  // The path to the .svelte.js file relative to /static/
  outputPath: string;
}

function findSvelteComponents(): SvelteComponentLocation[] {
  const files = fs.readdirSync(COMPONENT_DIR);
  return files.map(fname => ({
    inputPath: `../components/${fname}`,
    outputPath: `components/${fname}.js`,
  }));
}

function buildOptions(componentLoc: SvelteComponentLocation): RollupOptions {
  return {
    input: './svelte/setup/embed.js',
    output: {
      format: 'iife',
      file: `static/${componentLoc.outputPath}`,
      sourcemap: false,
    },
    plugins: [
      replace({
        delimiters: ['%%%', '%%%'],
        preventAssignment: true,
        values: {
          'SVELTE_FILE_PATH': componentLoc.inputPath,
        },
      }),
      svelte({
        emitCss: false,
      }),
      resolve({
        browser: true,
        dedupe: ['svelte'],
      }),
    ],
  };
}

const cfg: RollupOptions[] = findSvelteComponents().map(buildOptions);

export default cfg;

This code searches the svelte/components directory (configurable via the COMPONENT_DIR variable) for .svelte files, and returns separate Rollup configs for each one. For each file, the buildOptions function returns a configuration object that tells Rollup a few things:

  • build embed.js
  • replace all %%%SVELTE_FILE_PATH%%% instances with the path to the current .svelte file
  • run the svelte compiler
  • output to /static/components/{filename}.js i.e. /static/components/SomeComponent.svelte.js

In order to understand how the above set of steps plays out, we also need to take a look at embed.js:

import Component from '%%%SVELTE_FILE_PATH%%%';

var div = document.createElement('DIV');
var script = document.currentScript;
script.parentNode.insertBefore(div, script);

new Component({
  target: div,
});

This javascript file imports a Svelte component from some path (which is filled in by Rollup), and renders it to the DOM, directly above where the current <script> tag is (the current <script> tag is acquired via document.currentScript).

Combining these two parts, we should be able to embed compiled Svelte components by simply including a script tag that looks like <script src="/components/MyComponent.svelte.js"></script>. We’ll take a look at making that even easier in the next section.

Setting up the Hugo Shortcode

The next step is to setup a Hugo “shortcode” to make embedding Svelte components from content pages easier than manually creating <script> tags. Simply add to /layouts/shortcodes/svelte.html:

<noscript>Javascript is disabled.</noscript>
<script src="/components/{{ .Get 0 }}.svelte.js"></script>

The <noscript> tag includes a message to show to browsers where javascript is disabled.

With that svelte shortcode added, Svelte components can be added from content pages very simply:

{{< svelte "HelloFromSvelte" >}}

{{ .Get 0 }} is replaced by Hugo’s templating engine with the first argument to the shortcode (in this case, HelloFromSvelte), and we are left with:

<script src="/components/HelloFromSvelte.svelte.js"></script>

Since Rollup compiled /svelte/components/HelloFromSvelte.svelte, and imported it into embed.js with output at /static/components/HelloFromSvelte.svelte.js, the resulting script tag is javascript that has the necessary code to create a div right above the script tag and then mount the Svelte component into that div.

Fin

I hope this post helped you add some interactivity to your Hugo site.

Let me know if you have any questions or thoughts by commenting below!