How to Serve a Svelte Site on WordPress and Why You Shouldn’t

Say you want to build a site that has a dynamic front page and a blog. Then, due to varying factors (client requirements, cost, preference of content managers, etc.), the web site should be done in WordPress.

WordPress can do both?it’s good at doing the blog thing, but writing a fancy front page especially will be like pulling teeth. So, why not just build the front page using a framework with a better developer experience, like Svelte? That works but you do need the blog section. So let’s just have a separate WordPress site. Want them in the same domain? Just use NGINX to do the routing.

Now you need to have an NGINX server running, alongside the Svelte front page and WordPress for the blog.

But now you’re paying for 2 things: the NGINX server (you can put the Svelte static site on here) and your WordPress account. (It needs to be one of the higher tiers because you’re using a custom domain) Oh no, you’ve doubled your operating cost, and you’ve made your site analytics impossibly difficult to manage!

Okay, we have a great idea: let’s make the WordPress deployment serve the Svelte front page! But how? (Oh that’s what this article is for)

Wait, you don’t want every change in copy to be a rebuild of the site? I suppose you can change your frontend to use the WordPress REST API, which is not bad in itself, but now your static page is not so static. And what about web crawlers and SEO? By this point you’ve well and thoroughly lost your mind.

I’m not saying that was us. But I’m also not not saying that was us.

Let me be clear: you probably should not be trying to serve a Svelte site on WordPress.

But you can. And this is how, if you absolutely must, having considered all the downsides I outlined above.

Later in this article, I’ll be showing you what you can do instead; it’ll take more effort than the Svelte way, but at least you won’t be fighting the universe.

Fighting the Universe

The Plan

  1. Set up your theme directory
  2. Configure and build your Svelte project
  3. Add WordPress hooks to enqueue and serve the appropriate files

Directory Setup

You will first need a functional Svelte project that is served on Vite, rather than SvelteKit. If your site is built on the latter, then you must migrate to the former—it’s annoying but not too difficult. That means moving files around, even editing some code. Currently trying to make a SvelteKit project work is a fool’s errand, full of path and import errors. This is not the battle we will be fighting today. I have already tried and failed.

First, you need a local WordPress install. I am used to using Local, but wp-env should work similarly.

Then, we need our Svelte project to live somewhere. Let’s make a child theme of a relatively minimalist theme that we can drop our Svelte project in.

Yes, it’s supposed to look like this. Our child theme shown beside its parent, Seedlet.

For creating a Svelte site, the documentation will recommend you use SvelteKit. Most of the time this is a good thing—SvelteKit has a bunch of really useful features, but it simply won’t work for what we’re trying to do. Simply scroll down until you find the section detailing alternatives to SvelteKit.

npm run dev from the Svelte project directory to verify that things work.

Building Svelte

When we want to show a user something on the web, it all boils down to serving raw HTML, JS, CSS, and media to the web browser.

The Svelte infrastructure (or in this case, more like Vite) gives us a single command that, if you’re building only one page, is enough: npm run build.

But what if your Svelte project actually has several entrypoints? For example, a separate, entirely custom contact page?

You need to make a new HTML file (let’s call it contact.html)

Here, we’re adding the necessary files: contact.html is more or less a clone of index.html, except instead of referring to main.ts, it refers to contact.ts (which, in turn, is similar to main.ts). For Contact.svelte, it’s just a simple svelte component with text—certainly simpler than App.svelte.

Finally, we add the build configuration in vite.config.ts. Normally, the build key is absent, and by default Vite will use index.html as the input, but since we want to use multiple inputs (i.e. include contact.html), we have to fully specify this config.

Compare this to the first time we invoked npm run build. The dist and dist/assets directories now contain new output versions of the files we created. If more entrypoints are needed, one simply needs to match the pattern.

Serving Svelte

We’re going to serve our site by making use of the “old” way of making theme templates: PHP files! Here’s our plan.

  1. Enqueue all the assets so that WordPress knows how to serve those non-HTML files
  2. Make a PHP Page Template file per Svelte entrypoint (i.e. index and contact) and each one will be its own template.
  3. On the PHP Page Template files, pull the HTML files and echo their contents.
  4. Rewrite the HTML string to fix the paths pointing to the assets.

Let’s first write some utility functions in functions.php:

PHP
// Apply a $callback to all paths in the template directory matching a $pattern.
function pattern_map( callable $callback, string $pattern ) {
	return array_map( $callback, glob( get_template_directory() . $pattern ) );
}

// Convert a template directory path to a URI.
function glob_path_to_uri( string $file_path ) {
	$template_directory_length = strlen( get_template_directory() );
	$file_url = get_template_directory_uri() . substr( $file_path, $template_directory_length );
	return $file_url;
}
PHP

Then, we’ll enqueue our assets.

PHP
function enqueue_svelte_styles () {
	$path = "/svelte-project/dist/assets/";
	$style_uris = pattern_map("glob_path_to_uri", $path . "*.css");
	foreach ($style_uris as $style_uri) {
		wp_enqueue_style(basename($style_uri), $style_uri);
	}
};
add_action('wp_enqueue_styles', 'enqueue_svelte_styles');

function enqueue_svelte_scripts () {
  $path = "/svelte-project/dist/assets/";
	$script_uris = pattern_map("glob_path_to_uri", $path . "*.js");
	foreach ($script_uris as $script_uri) {
		wp_enqueue_script(basename($script_uri), $script_uri);
	}
};
add_action('wp_enqueue_scripts', 'enqueue_svelte_scripts');
PHP

Enqueueing styles and scripts is pretty easy. They more or less have the same implementation, with the exception of the extension, and the specific enqueueing function.

The tricky part is making sure that the output JS files can properly import other JS files—for this, the script tag must be of type module rather than javascript. We don’t have direct control over setting the type of the script tag (we can’t set it using wp_enqueue_script), but thankfully WordPress exposes filters that allow us to modify tags before serving.

PHP
// Make a type="module" script tag
function script_tag(string $src_handle, string $src_url) {
	$escaped_url = esc_url($src_url);
	return "<script id=\"{$src_handle}\" type=\"module\" crossorigin src=\"{$escaped_url}\"></script>";
}

// Converts script tags that load svelte scripts into scripts of type="module".
// Ignores scripts that aren't part svelte scripts.
//
// Currently enqueued scripts are loaded via bare script tags.
// Meanwhile, Svelte needs the script tags to be of type="module"
function add_module_to_svelte_scripts ($tag, $handle, $src) {
  $path = "/svelte-project/dist/assets/";
	$script_uris = pattern_map("glob_path_to_uri", $path . "*.js");
	// $handle corresponds to the first argument to wp_enqueue_script
  // Acts as an ID for a URI. Since our wp_enqueue_scripts use the basename
  // we must use the basename again here.
  $script_handles = array_map('basename', $script_uris);
	if (!in_array($handle, $script_handles)) {
		return $tag;
	}
	return script_tag($handle, $src);
};


add_filter('script_loader_tag', 'add_module_to_svelte_scripts' , 10, 3);
PHP

Basically add_module_to_svelte_scripts checks if the script’s handle matches any of the other scripts in the assets, then manually create the script tag of type=module.

Next, we can make a template.

According to the documentation, all we have to do to make the Page Template is to add a specific key-value pair as a comment.

Even with an entirely blank body, this is enough to allow us to select it to be the template for any page.

The next step is simply to pull the entirety of the desired index file into the page template PHP file.

PHP
$html = file_get_contents( dirname( __FILE__, 1 ) . "/svelte-project/dist/index.html" );
echo $html;
PHP

If we just leave it at this, we’ll have a bunch of errors when we visit the page.

This is happening because the asset paths are all rooted within the svelte-project directory. We need to raise the root of the paths into the theme directory. Unfortunately, there is no way we know of to do this other than to do a string replace on the URLs.

PHP
function fix_asset_uri( string $html, string $abs_file_path ) {
	$path_to_dist = "/svelte-project/dist";
  // If we're working on a non-child theme, we would be using 
  // get_template_directory and get_template_directory_uri instead.
	$abs_path_to_dist = get_stylesheet_directory() . $path_to_dist;
	$abs_uri_to_dist = get_stylesheet_directory_uri() . $path_to_dist;
	// One might think we can replace $full_path_to_dist with $full_uri_to_dist,
	// But note that in the HTML, we're replacing a path that is relative to root.
	$rel_file_path = str_replace( $abs_path_to_dist, "", $abs_file_path );
	$full_uri = $abs_uri_to_dist . $rel_file_path;
	$insertion_index = strpos( $html, $rel_file_path );
	// $qualified_file_name was not found in $html, so do not replace anything.
	if ( ! $insertion_index ) {
		return $html;
	}
	return substr_replace( $html, $full_uri, $insertion_index, strlen( $rel_file_path ) );
}

function fix_asset_uris( string $html ) {
	$path_to_dist = "/svelte-project/dist";
  // If we're working on a non-child theme, we would be using get_template_directory instead.
	$script_file_paths = glob( get_stylesheet_directory() . $path_to_dist . "/assets/*.js" );
	$style_file_paths = glob( get_stylesheet_directory() . $path_to_dist . "/assets/*.css" );
	$favicon_file_paths = glob( get_stylesheet_directory() . $path_to_dist . "/*.svg" );
	$file_paths = array_merge( $script_file_paths, $style_file_paths, $favicon_file_paths );
	$mutable_html = $html;
	foreach ( $file_paths as $file_path ) {
		$mutable_html = fix_asset_uri( $mutable_html, $file_path );
	}
	return $mutable_html;
}
PHP

This is a pretty fat block of code, but in essence, it just takes a bunch of paths that match a glob pattern, and replaces those paths with the correct URL. This more or less works.

Yes, I’ve noticed too that the Vite logo is broken. The way this sample app was made, Vite is set up to serve the contents of the public directory as root in the final build. This is so that the image itself can also be used as a favicon. This unfortunately runs afoul of a quirk WordPress has in dealing with SVG images: WordPress absolutely refuses to serve them unless you install plugins. So we’ll simply copy over the Vite logo into the assets directory and update App.svelte‘s import statement for the SVG file.

When we run the build script, we can see that our svelte site works!

At this point, you can simply use the WordPress REST API, and have what is essentially a headless WordPress site… being served by WordPress.

Not Fighting the Universe

This is cool and all, but again, I must caution you that if you must interact deeper with any of WordPress’s strengths—blogging, content management, even simply the ability for non-devs to make changes (because let’s be real, a content manager isn’t going to want to edit svelte files directly, and neither would you want them to.)

What we initially did was create Blurb content types that the Svelte frontend would pull and use in specific parts of the website. But that was extremely cumbersome. Further, we had to doubly implement the header and footer: we have them for the Svelte page, but we also need it for all the other WordPress pages. It was simply too awkward.

Instead, I recommend you rebuild that website in WordPress Blocks. It sucks to repeat work, but in the end it’s worth it.

Here at Bonito Tech, we get how frustrating it can be to re-do things. That’s why we focus on optimizing existing content and streamlining website development, whether using WordPress or Svelte, to craft a custom solution that meet your specific needs and goals. We also offer a wide range of services that go beyond development, including consultation, design and more! Take a look at the rest of our services here.

Contact us today to discuss your project and see how we can save you time and resources.