Adding Svelte to an Existing Website

Want to get started with Svelte in the context of an existing project? Here are some strategies to integrate it into your existing codebase.

Posted July 26, 2021 Web Development

Integrating a new frontend framework into an existing web application or site can be a challenge. Generally, the documentation around frontend frameworks is centered on the use-case of greenfield development; that is, starting a project completely from scratch.

What if you have a project that could benefit from frontend changes, without ripping out everything that's there and starting from scratch? I encountered this on a project I completed at work, where I took a full-stack Flask application and rewrote the frontend to use Svelte. Here are some of the lessons I learned from the process, and some ideas if you want to try the same thing.

Injecting Svelte into Existing HTML

Every good solution to a coding problem, of course, begins with Google. So, after searching for "svelte existing HTML," I found this resource from David Tang that got me started on the right path. When starting with the standard Svelte template, main.js will look like this:

import App from './App.svelte';

const app = new App({
	target: document.body,
	props: {
		name: 'world'
	}
});

export default app;

That's the "entry point" into your Svelte application. Once you compile to bundle.js and include the bundle in your index.html file, your Svelte application will render in the <body> tag of the HTML.

As David's post, shows, though, you don't have to use <body> as your target. In his case, he replaced the target value with document.querySelector('#hello-world-container') instead of document.body. By putting an element inside his HTML with the id of hello-world-container, that element is then where the Svelte application will render.

Scaling it up

How do we get from a Hello World example to something a little more practical, though? I'll walk you through the details on how I used the above to integrate Svelte into an existing Flask project.

Updating the HTML Template

Flask uses the Jinja templating engine, so this portion is written from that perspective. You would take a similar approach, though, if you are using a different backend framework such as Express, Rails, or Laravel.

I created a file called svelte_template.html and populated it with the following:

<head>
    <title>{% block page_title %}{{!app_name!}}{% endblock %}</title>

    {% block head_meta %}
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="">
    {% endblock %}
    {% block head_css %}
    <link href="{{url_for('static',filename='bundle.css')}}?u={{ last_updated }}" rel="stylesheet" />
    <link href="{{url_for('static',filename='global.css')}}?u={{ last_updated }}" rel="stylesheet" />
    {% endblock %}
    {% block head_js %}
    <script src="https://code.iconify.design/1/1.0.7/iconify.min.js"></script>
    <link rel="preconnect" href="https://fonts.gstatic.com">
	<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet">
    {% if extra_data != None %}
    <script id="data" type="application/json">{{ extra_data | safe }}</script>
    {% endif %}
    <script defer src="{{url_for('static',filename='bundle.js')}}?u={{ last_updated }}" id="svelte" data-component={{ component }} data-title="{{ title }}"></script>
    {% endblock %}

</head>

<body>
    {% if show_nav %}
    <header>
        <nav>
            <ul class="nav">
                <li>
                <a href="/">
                    <img src="{{url_for('static',filename='logo.png')}}" class="nav-logo">
                </a>
                </li>
                {% include '/navbar_menu.html' %}
                {% include /navbar_right.html' %}
            </ul>
        </nav>
    </header>
    {% endif %}
    <div id="svelteBody"></div>
</body>

</html>

Let's focus on a couple lines of this code:

<link href="{{url_for('static',filename='bundle.css')}}?u={{ last_updated }}" rel="stylesheet" />

This line imports the bundled CSS that Svelte generates.

<script defer src="{{url_for('static',filename='bundle.js')}}?u={{ last_updated }}" id="svelte" data-component={{ component }} data-title="{{ title }}"></script>

This line imports the bundled JavaScript that Svelte generates. Note that both of these bundle filenames have a dynamic URL parameter that I've configured Flask to pass to the template. This parameter represents the date/time the static content folder was updated, ensuring that cache-busting will properly occur when new changes to the frontend are pushed to the server.

I'm also passing data-component and data-title parameters to the Svelte bundle, which Svelte will consume later on.

<div id="svelteBody"></div>

I included the other portions of the body to provide context. This div is where our Svelte will render, in context with the rest of the code. So, in this case, I'm still generating the web application's header using Jinja, as I pass in the logo, navigation links, and login context. That will be included with the HTML sent on each page load, and then the rest of the page will dynamically fill in the svelteBody div, thanks to Svelte.

{% if extra_data != None %}
    <script id="data" type="application/json">{{ extra_data | safe }}</script>
{% endif %}

This is something extra that I added to allow the backend to pass in additional data to Svelte. In most cases, I'm hitting REST APIs to get data from the Svelte side, but there were a few instances where I felt it would be more efficient to fetch and pass the data with the rendered Jinja template. This is optionally creating data JSON that is embedded with the HTML, and that JSON can then be consumed by Svelte (I'll outline this more below).

Rendering the Template

On the Flask side, I created the following function:

def render_svelte(component, title=None, extra_data=None):
    return render_template('svelte_template.html', component=component, title=title, extra_data=extra_data, last_updated=dir_last_updated('app/static'))

This function renders my svelte_template file, passing in the name of the Svelte component to render, the title to display, any extra data to pass in, in JSON format, and the last_updated value. I'm calling the following function to get the last updated value in milliseconds from the Unix epoch time. The function just takes a directory name and gets the updated date/time for the most recently touched file in the folder:

def dir_last_updated(folder):
    return str(max(os.path.getmtime(os.path.join(root_path, f))
                    for root_path, dirs, files in os.walk(folder)
                    for f in files))

Changes on the Svelte Side

We've finished the backend related pieces, so now it's time to make a few changes on the Svelte side. We'll need to load the right components and render the data passed in.

main.js

We'll start with the main.js file, which I've modified to look like this:

import App from './App.svelte';

let extraData;

const component = document.getElementById("svelte").getAttribute("data-component");
const title = document.getElementById("svelte").getAttribute("data-title");
if (document.getElementById('data') !== null) {	
	extraData = JSON.parse(document.getElementById('data').innerHTML);
}
else {
	extraData = null;
}

const app = new App({
	target: document.getElementById("svelteBody"),
	props: {
		component: component,
		title: title,
		extraData: extraData
	}
});

export default app;

Let's walk through it piece-by-piece. First, we're declaring an extraData variable, which we'll use later. Then, we're declaring component and setting it equal to the component name that was set in the Jinja template earlier and passed into it by the render_svelte function in Flask. Likewise, we're declaring title and setting it to the title passed from Flask, to the Jinja template, to this file.

Next, we're checking to see if there's any JSON data present in extraData, and we're setting the extraData variable if there is. Finally, the section from const App to the end of the file should look similar to what we looked at in the example at the start of this post. We're creating our entry point for Svelte, placing it in the svelteBody div, with the component, title, and optional extraData as props.

App.js

Here's what my App.js file looks like:

<script>
	// When adding a new component, it must be imported in this section.
	import Index from './index.svelte';
	import ReleaseNotes from './releaseNotes.svelte';
	import Login from './login.svelte';

	export let component;
	export let title;
	export let extraData;
	
	// Give component a name to match what is passed in from Flask backend
	let components = {
		"index": Index,
		"releasenotes": ReleaseNotes,
		"login": Login
	}
</script>

<main>
<svelte:component this={components[component]} {title} {extraData} />
</main>

At the top, I'm importing the parent component for each page of the website. This is a condensed example, but I have components here for the index, release notes, and login sections of the application. Next, I have the props I'm expecting from main.js for component, title, and extraData. Below that, I've created an object that maps the component names (the component prop I'm getting from Flask via main.js), to the actual Svelte components I've imported.

Finally, in the HTML section of my svelte file, I'm using the svelte:component element to dynamically load the component that corresponds to what Flask has passed in.

From this point on, everything is pretty much standard Svelte. The title and extraData props are passed on for the Svelte parent component to use as it wishes, and that parent component can proceed with importing and using any other components it wants.

rollup.config.js

Your rollup configuration is the last thing that would need to be changed. Usually, the export portion of the default Svelte template begins like this:

export default {
	input: 'src/main.js',
	output: {
		sourcemap: true,
		format: 'iife',
		name: 'app',
		file: 'public/build/bundle.js'
	},

You would need to adjust the output file location to be where your Jinja template is expecting to find it.

Visualizing the Process

Here's a rough diagram of what the flow of the above process looks like. Hopefully it helps if you're more of a visual learner:

Svelte-Flask data flow

Your folder structure would resemble the following:

|/backend
|- files for your backend framework of choice go here
|/frontend
|- rollup.config.js
|- package.json, etc.
|- /src
|-- main.js
|-- App.svelte
|-- /components
|--- files for your Svelte components go here

With a structure like this, your rollup config would be expected to be configured with the output location of wherever your static assets are served from, such as inside of the backend folder (but dependent on your backend framework and web server setup and configuration).

Another Option: Web Components

A second way to add some Svelte elements to an existing web project would be to use Web Components. Svelte offers the option of exporting your work as Web Components, rather than the normal bundle it generates. There are a number of caveats to this process, however. I opted to go the route I've outlined above after reviewing the pros and cons of Web Components, but if you are interested in learning more and considering it for your project, I'd recommend this post.

Summary

This is a great strategy to bring Svelte into your project, piece-by-piece. Like remodeling a house one room at a time, I was able to slowly convert sections of the site to use Svelte as the frontend, rather than ripping everything out and starting from scratch. It's also a great way to dip your toe into the water with Svelte, trying it out with an existing codebase, seeing how it works, and seeing if you experience any performance benefits from the change.

If you've made it this far, thanks for reading this tutorial! If you run into any questions or issues, I'm happy to try and help you through them! Feel free to reach out to me on Twitter or in the comments below.

Enjoyed this post? Consider buying me a coffee to fuel the creation of more content.