In the following post I will show how to make use of server-side rendering in Svelte.
Most of the time you will probably run your Svelte code client-side, but there are scenarios where you can benefit from server-side Svelte rendering.
SEO
In my opinion the primary use case for server-side rendering is SEO. Doing an initial rendering on the server will make your website much more crawler accessible. Crawlers typically support some JavaScript execution, but a complex JavaScript application is unlikely to be indexed correctly.
My experience is that applications that make ajax calls to fetch data are particularly challenging to index. The crawler might successfully render the initial static part of the application, but the data will be missing since the crawler won't make the necessary ajax calls.
Doing the initial rendering on the server allows crawlers to download the application as fully constructed html. There is no need to execute JavaScript on the client to compose the initial view since the view was already built on the server.
Server-side vs Client-side
Technically you could build a Svelte application without any client side components, but it would be completely static. Basically the application would behave much like an old server-side PHP application. Great for crawlers, but real users typically expect a richer user experience.
This is where server-side meets client-side.
Once the server generated html is fully rendered in the browser, we can start a client-side counterpart of the application. The client-side version picks up where the server side application left off.
Both versions of the application may use the same Svelte components, but it's important to understand that they execute independently of each other. The server-side version does not know about the client-side version and vice versa.
It's also important to understand that there is no default state sharing between client and server (e.g. data property).
Article Carousel
I decided to use server side Svelte to build an article carousel for the landing page of my blog. The idea is to use a Svelte component to cycle through articles in four of my article categories.
The carousel should load instantly on page load, so I decided to render the initial view on the server. Once the page has loaded I start the client-side counterpart of the Svelte component to dynamically cycle through the articles.
I am not known for css or design skills, so don't expect a pretty UI, but here's how I wired everything up.
I start by creating an Article component that will be used to render the articles in the carousel. The component should probably be split into two components, but keeping it as one for the purposes of this blog post.
<div class="row">
<span class="slide-show-card col-sm-3">
<h4>Angular</h4>
<h5><a class="slide-show-link" href="{{angularUrl}}">{{angularTitle}}</a></h5>
<div class="label label-success slide-show-count">Viewed {{angular.viewCount}} times</div>
<div>{{angularIntro}}</div>
</span>
<span class="slide-show-card col-sm-3">
<h4>JavaScript</h4>
<h5><a class="slide-show-link" href="{{javascriptUrl}}">{{javascriptTitle}}</a></h5>
<div class="label label-success slide-show-count">Viewed {{javascript.viewCount}} times</div>
<div>{{javascriptIntro}}</div>
</span>
<span class="slide-show-card col-sm-3">
<h4>Svelte</h4>
<h5><a class="slide-show-link" href="{{svelteUrl}}">{{svelteTitle}}</a></h5>
<div class="label label-success slide-show-count">Viewed {{svelte.viewCount}} times</div>
<div>{{svelteIntro}}</div>
</span>
<span class="slide-show-card col-sm-3">
<h4>React</h4>
<h5><a class="slide-show-link" href="{{reactUrl}}">{{reactTitle}}</a></h5>
<div class="label label-success slide-show-count">Viewed {{react.viewCount}} times</div>
<div>{{reactIntro}}</div>
</span>
</div>
<script>
class ArticleService {
constructor() {
this.articles = [];
this.index = {
'angular': -1,
'javascript': -1,
'svelte': -1,
'react': -1
};
}
getArticles() {
return fetch('/full-article-list-json')
.then((data) => {
return data.json();
})
.then((articles) => {
this.articles = articles;
return articles;
});
}
getNextArticle(key) {
this.index[key] = (this.index[key] + 1) % this.articles[key].length;
let a = this.articles[key][this.index[key]];
return a;
}
}
let articleService = new ArticleService();
export default {
onrender() {
let update = () => {
this.set({angular: articleService.getNextArticle('angular')});
this.set({javascript: articleService.getNextArticle('javascript')});
this.set({svelte: articleService.getNextArticle('svelte')});
this.set({react: articleService.getNextArticle('react')});
};
articleService.getArticles()
.then((articles) => {
update();
if(typeof global === 'undefined') {
document.querySelector('#articles').innerHTML = '';
document.querySelector('#articles-client-side').style.display = "block";
}
})
.then(() => {
setInterval(() => {
articleService.getArticles()
.then((articles) => {
update();
});
}, 5000);
});
},
data () {
return {}
},
computed: {
angularTitle: angular => angular.title || '',
angularIntro: angular => angular.intro || '',
angularUrl: angular => `viewarticle/${angular.friendlyUrl}`,
javascriptTitle: javascript => javascript.title || '',
javascriptIntro: javascript => javascript.intro || '',
javascriptUrl: javascript => `viewarticle/${javascript.friendlyUrl}`,
svelteTitle: svelte => svelte.title || '',
svelteIntro: svelte => svelte.intro || '',
svelteUrl: svelte => `viewarticle/${svelte.friendlyUrl}`,
reactTitle: react => react.title || '',
reactIntro: react => react.intro || '',
reactUrl: react => `viewarticle/${react.friendlyUrl}`,
}
}
</script>
Server
Let's take a look at the server code.
The first thing we have to do is activate the compiler by requiring svelte/ssr/register
require( 'svelte/ssr/register' );
Next we have to require the component html file to get a handle to the component.
We then call the render method and pass it an initial data object. The data object is a standard Svelte data object.
app.get('/', function(req, res) {
var component = require('./app/article-show/Articles.html');
var articles = component.render({
angular: articles.angular,
svelte: articles.svelte,
react: articles.react,
javascript: articles.javascript
});
res.render('index.html', {articles: articles});
});
After calling render we get back a fully rendered component. We now have to pass this to the node view engine.
In my case I am using Express with Mustache, so I can just pass the component as an object to the render method.
Then in my index.html page I use Mustache view syntax with triple curly braces to render the component on the page like so.
{{{articles}}}
Client
What we have so far is enough to render the initial view of my component, but it won't support cycling through new articles every few seconds.
To achieve this we have to start up a client-side version of the Article component.
The client side version is loaded as a standard Svelte client-side component.
import articles from './articles';
var articlesSection = new articles({
target: document.querySelector( 'main' ),
data: {angular: {}, javascript: {}, svelte: {}, react: {}}
});
Once the client-side version is activated we will have two components in the DOM. As soon as the client-side version is ready to take over I wipe out the server-side version.
There might be a more elegant way to do this, but I simply clear out the server generated DOM element and flip a style on the client-side version.
Navigation
In addition to the article carousel I also built my main navigation as a Svelte server side component. The nav is a pure server side component with no client side counterpart.
The navigation is largely static, but it supports dynamic styling of the active nav item.
Here is the nav component code:
<div class="nav col-md-2 hidden-sm hidden-xs">
<a class="navLink" href="/"><div class="navBox {{home}}">Home</div></a>
<a class="navLink" href="/most-popular"><div class="navBox {{mostpopular}}">Most Popular</div></a>
<a class="navLink" href="/most-recent"><div class="navBox {{mostrecent}}">Most Recent</div></a>
<a class="navLink" href="/articleList/angular"><div class="navBox {{angular}}">Angular</div></a>
<a class="navLink" href="/articleList/react"><div class="navBox {{react}}">React</div></a>
<a class="navLink" href="/articleList/aurelia"><div class="navBox {{aurelia}}">Aurelia</div></a>
<a class="navLink" href="/articleList/javascript"><div class="navBox {{javascript}}">JavaScript</div></a>
<a class="navLink" href="/articleList/nodejs"><div class="navBox {{nodejs}}">NodeJS</div></a>
<a class="navLink" href="/articleList/vue"><div class="navBox {{vue}}">Vue</div></a>
<a class="navLink" href="/articleList/svelte"><div class="navBox {{svelte}}">Svelte</div></a>
<a class="navLink" href="/articleList/mongodb"><div class="navBox {{mongodb}}">MongoDB</div></a>
<a class="navLink" href="/articleList/unittesting"><div class="navBox {{unittesting}}">UnitTesting</div></a>
<a class="navLink" href="/articleList/dotnet"><div class="navBox {{dotnet}}">.Net</div></a>
<a class="navLink" href="/questions"><div class="navBox {{questions}}">Q&A</div></a>
<a class="navLink" href="/full-article-list"><div class="navBox {{all}}">All</div></a>
</div>
<script>
export default {
data () {
return {}
},
}
</script>
Because the nav is so static, there is no reason to regenerate the server side html for every request. As an optimization I have decided to cache the different variations of the nav. The only difference between the different versions of the nav is that the "active" nav items style is applied to different elements.
Here is some basic caching logic that ensures that we only generate each nav version once.
function getNavComponent(key) {
key = key || 'home';
key = key.replace('-', '');
if(!navCache[key]) {
navCache[key] = createNavComponent(key);
}
return navCache[key];
}
function createNavComponent(key) {
var nav = require('./app/article-show/Navigation.html');
var data = {};
data[key] = 'activeNav';
return nav.render(data);
}
Demo
If you want to test out my server-side vs client-side view you can find it here.
I also loaded my site in Google web master tools to compare a crawler's view of the component to a user's view
As you can tell from the screenshot my component looks pretty good to crawlers after adding server side rendering.
Left side is the crawler view and the right side is the user view.