SvelteKit 1.0 - Building a Blog that fetches from RSS ๐Ÿฆ„

The aim of this post is to provide a whistle-stop tour of the latest version of SvelteKit. We're going to build a developer portfolio and blog website, that fetches data from your RSS feed, as well as the GitHub API.

Contents


Intro to SvelteKit

Svelte has pretty quickly taken the top spot for most loved web framework [SO Survey], and with the recent release of SvelteKit 1.0, you should expect to see demand for Svelte + SvelteKit developers increase, as more projects adopt it.

SvelteKit to Svelte, is sort of like what Next.js is to React - it handles routing, layouts, server-side rendering, deployment and makes developing quality web apps quicker, easier and much more fun.

But why SvekteKit? ... You'll see! It's just so easy to get a fully-featured dynamic web application up and running, with all the quality metrics which would usually take days, or even weeks to implement in traditional frameworks. Think great performance, simple deployments, easy code structures and a sweet sweet developer experience.


What we're going to build

Most of us have a blog, weather it's here on Dev.to, or on another platform. Today we're going to build and deploy you a personal blog, that aggregates all your posts from other platforms, into a single site.

Since I don't know what blogging platforms you're using, I don't want to rely on individual APIs. But thankfully there's a simple solution to this - RSS! Almost all modern (and old) providers support RSS, and it'll let us easily fetch all your posts from a single URL. (For example, here on DEV: https://dev.to/feed/[your-username]).

Here's a live demo: devolio.netlify.app/blog

And here's the full source: @Lissy93/Devolio

To deploy it yourself - just fork it, update the config with your RSS feed URL(s), and use one of the 1-click deploy options.


Let's get Started!

Step #0 - Prerequisites

You'll need Node.js (LTS or latest) installed. It's also recommended to have Git, a code editor (like VS Code), and access to a terminal. Alternatively, you can use a cloud service, like Codespaces.


Step #1 - Project Setup

We can easily create our project by running:

npm create svelte@latest dev-blog

When prompted, select SvelteKit, then decide weather you'd like TypeScript, ESLint, Prettier, Playwright, Vitest.

Next, we need to navigate into our project (with cd dev-blog), and install dependencies (with npm install).

To launch the app, with live reload enabled, run:

npm run dev

Then open localhost:5173


Step #2 - Finish Setup

To avoid the typical ugly ../../../ in import statements, we're going to add an alias within our svelte.config.js file.

This can be done by just adding alias object under config.sveltekit. Here's an example, where I'll map ./src/ to $src.

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
    alias: {
      '$src/*': 'src/*',
    },
  },
};

export default config;

We can come back to the svelte.config file later, as it's where we put adaptors to deploy to various platforms, like Netlify.

If you'd like to use your own Prettier, ESLint or TypeScript config, you can update .prettierrc, .eslintrc.cjsprett and tsconfig.json respectively. Run npm run format to apply Prettier rules, and npm run check to verify.


Step #3 - Components

Before we proceed, we need to know the basics of components.
One of the reason that Svelte (and SvelteKit) is so easy to work with, is because pretty much everything is just a component. And the structure of components are really, really simple. Here's an example:

<script>
// All JavaScript logic and imports go here
// Append lang="ts" to use TypeScript
</script>

<!-- All markup goes here -->
<p>Example Component</p>

<style>
// All styles go here, and are scoped to the current component
// Append lang="scss" to use SCSS (or another pre-processor)
p {
    color: hotpink;
}
</style>

Here's a real-world example, where we're making a re-usable heading component, with optional level (h1, h2, etc), color, size and font.

<script lang="ts">

// Parameters
export let level: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' = 'h1'; // The semantic heading level
export let color: string | undefined = undefined; // An optional override color (defaults to accent)
export let size: string | undefined = undefined; // An optional override size (default depends on level)
export let font: string | undefined = undefined; // An optional override font (defaults to FiraCode)

// Computed values, for reactivity
$: computedColor = color ? `--headingColor: ${color};` : '';
$: computedSize = size ? `--headingSize: ${size};` : '';
$: computedFont = font ? `--headingFont: ${font};` : '';
$: computedStyles = `${computedColor} ${computedSize} ${computedFont}`;

</script>

<svelte:element this={level} style={computedStyles}>
  <slot></slot>
</svelte:element>


<style lang="scss">
  h1, h2, h3, h4, h5, h6 {
    font-weight: 700;
    transition: all .25s ease-in-out;
    font-family: var(--headingFont);
    color: var(--headingColor);
  }

  h1, h2, h3 { margin: 1rem 0; }
  h4, h5, h6 { margin: 0.5rem 0; }

  h1 { font-size: var(--headingSize, 2.8rem); }
  h2 { font-size: var(--headingSize, 2rem); }
  h3 { font-size: var(--headingSize, 1.75rem); }
  h4 { font-size: var(--headingSize, 1.5rem); }
  h5 { font-size: var(--headingSize, 1.25rem); }
  h6 { font-size: var(--headingSize, 1rem); }
</style>

Couple of things to note:

  • We are defining props with export let propName
  • We can make props optional, by giving them a default value
  • We can access any of these variables within our component, just surround them in braces {}
  • If we need attributes to be reactive, we use the $: variabeName syntax
  • We can specify what type of semantic element is used, with <svelte:element this="div">
  • A method of passing styles from JS into CSS is to define CSS variables, and pass them into the style prop
    • (This isn't as bad as it sounds, as all styles are scoped only to the current component!)

Step #4 - Creating a Route

Next we're going to create a blog page, where all our posts will be displayed. (This could be done on the homepage, in src/routes/+page.svelte, but this is a good opportunity to explain routeing)

SvelteKit will automatically create routes based on the directory structure within the routes directory. All you need is a directory named after the route name, containing a Svelte file names +page.svelte. So let's create that route, with: touch src/routes/blog/+page.svelte - the contents of this file will just be a normal Svelte component, like what we saw above.

<script lang="ts">
  let title = 'Blog Page';
</script>

<svelte:head>
  <title>{title}</title> 
</svelte:head>

<h2>{title}</h2>

<style lang="scss">
h2 {
  color: hotpink;
}
</style>

We'll also need a route that can render individual posts, but we want that URL path to be dynamic, maybe based on the posts title. For this we can create a directory called [slug] that the user will land on when they visit example.com/blog/example-post


Step #5 - Special Routes

Now's a good time to mention that we can have our routed inherit certain components that will appear on all pages, like a navbar and footer. For this, we can create a layout file, which needs to be called +layout.svelte, and since we want this on all pages, we'll put it into src/routes.

Populate this with something like:

<script lang="ts">
  import NavBar from '$src/components/NavBar.svelte';
  import Footer from '$src/components/Footer.svelte';
  import { fade } from 'svelte/transition';
  import { page } from '$app/stores';
</script>

<svelte:head>
  <title>{$page.url.pathname.replaceAll('-', ' ')}</title> 
</svelte:head>

<NavBar />

<main in:fade>
  <slot />
</main>

<Footer />

<style lang="scss">
  @import "$src/styles/color-palette.scss";
  @import "$src/styles/media-queries.scss";
  @import "$src/styles/typography.scss";
  @import "$src/styles/dimensions.scss";

  :global(html) {
    scroll-behavior: smooth;
  }
  :global(::selection) {
    background-color: var(--accent);
    color: var(--background);
  }
</style>

A couple of things to note here:

  • The main site content will be rendered where <slot /> is placed
  • We're adding a page transition animation, by importing svelte/transition and setting in:fade on the part of the page which will change
  • We can get information about the current page (like path), by using the page object (imported from $app/stores) - precede it with a $ to keep the value updated
  • If we need to set any tags within the <head> we can use <svelte:head> to do so
  • We can also pop any global style, like a reset or import CSS variables
  • Global styles can be applied using :global(body) (or whatever selector you're targeting) - but use this sparingly!

Another special route within SvelteKit, is +error.svelte, which will be rendered in place of the current route if an error is thrown within the load() function of any route.

Again, let's create that file in src/routes/+error.svelte and populate it with something like this. (Again, we can get info about the current route, including error code from the $page object)

<script>
    import { page } from '$app/stores';

    const emojis = {
        // TODO add the rest!
        404: '๐Ÿงฑ',
        420: '๐Ÿซ ',
        500: '๐Ÿ’ฅ'
    };
</script>

<h1>{$page.status} {$page.error.message}</h1>
<span style="font-size: 10em">
    {emojis[$page.status] ?? emojis[500]}
</span>

It's also worth noting, that you can create layout and error pages that are specific to certain routes, by nesting them within the correct route directory. If you need several layout pages, which share characteristics, you can extract those elements out into their own component, to make them more reusable.

By now, our routes directory structure should look something like this:

src/routes
โ”œโ”€โ”€ +error.svelte
โ”œโ”€โ”€ +layout.svelte
โ”œโ”€โ”€ +page.svelte
โ”œโ”€โ”€ about
โ”‚  โ””โ”€โ”€ +page.svelte
โ””โ”€โ”€ blog
   โ”œโ”€โ”€ +page.svelte
   โ”œโ”€โ”€ +page.ts
   โ””โ”€โ”€ [slug]
      โ”œโ”€โ”€ +page.svelte
      โ””โ”€โ”€ +page.ts

Step #6 - Fetching Data

Now it's time to get into the good stuff! We're going to fetch the list of blog posts, from the users RSS feed.

Now is a good time to mention, that within the path directory for each route, we can also have a +page.js / +page.ts file (alongside the +page.svelte). This is where we'll do our data fetching.

To keep things simple, we're going to use fast-xml-parser to parse the XML response, into JSON.

The following script simply fetches and parses feeds from a given XML RSS feed.

import { XMLParser } from 'fast-xml-parser';

const parseXml = (rawRssData) => {
  const parser = new XMLParser();
  return parser.parse(rawRssData);
};

/** @type {import('./$types').PageLoad} */
export const load = () => {
  const RSS_URL = `https://notes.aliciasykes.com/feed`;
  const posts = fetch(RSS_URL)
    .then((response) => response.text())
    .then((rawXml) => parseXml(rawXml).rss.channel.item);
  return { posts };
};

Step #7 - Render Results

Rendering the results from the returned data is really easy. In the blog/+page.svelte component (next to the +page.ts file), simply include export let data - this will be the result returned by our fetch function. We can now reference this data in the markup.

<script lang="ts">
  /** @type {import('./$types').PageData} */
  export let data;
</script>

Blog

{#each data.posts as post}
  <li>
    <a target="_blank" href={post.link} rel="noreferrer">
      {post.title}
    </a>
  </li>
{/each}

You'll notice we're using {#each data.posts as post} - this is just a for loop, as the data returned is an array.

This is part of Svelte's template syntax. There are other properties also, like {#if expression}...{/if} for conditionals, or {#await expression}...{:then name}...{/await} for promises, as well as a whole host of other useful features.


Step #8 - Server-Side

What we've got so far works great, but there are a few issues we may encounter with it:

  • Load times - RSS feeds are large, and fetching them client-side on each load isn't efficient
  • SEO - Dynamically loaded content isn't going to be crawlable by most search engine bots
  • CORS - Some RSS feeds won't allow client-side requests from cross-origin hosts

Thankfully, there's an easy fix for this. Renaming +page.ts to +page.server.ts will cause it to be rendered server-side, instead of on the users browser. This should fix those issues, and won't require any code changes.

Note that for server-side code, we cannot use any of the browser APIs. Since a lot of our code will be capable of being run both server and client-side, we will need to check certain features are available before attempting to use them. We can do this, by importing browser from $app/environment, then using if (browser) { /* Can access browser API here */ }


Step #9 - Build the Post Page

Finally, when the user clicks on a post, we'd like to render it. This is pretty straitforward, as the RSS response is already in HTML format, so it's just a case of using the @html directive, then styling it.

<main class="article-content">
  {@html content}
</main>

Step #10 - Deploy!

Now, let's get deploys setup. This is another reason why SvelteKit is so awesome, as deploying to pretty much any provider is just so easy!

  1. Install the adapter for your desired provider
    • E.g. for Netlify: npm i --save-dev @sveltejs/adapter-netlify
  2. Import said adaptor in your svelte.config.js file
    • E.g. import netlifyAdapter from '@sveltejs/adapter-netlify';
  3. Initiate the adaptor, within the config object, under kit
    • kit: { adapter: netlifyAdapter() }
  4. Deploy! Now just head to your Netlify dashboard, and import the project

If you wish to run your project on a VPS, we can use the @sveltejs/adapter-node. Repeat the process above, then run yarn build, and start the node server by running node build/index.js.

We may want to use multiple adapters, so that our project is compatible with several different hosting providers. Here's an example of my config file which does just this:

import autoAdapter from '@sveltejs/adapter-auto';
import netlifyAdapter from '@sveltejs/adapter-netlify';
import vercelAdapter from '@sveltejs/adapter-vercel';
import nodeAdapter from '@sveltejs/adapter-node';

import { vitePreprocess } from '@sveltejs/kit/vite';

const multiAdapter = (adapters) => {
  return {
    async adapt(argument) {
      await Promise.all(adapters.map(item =>
        Promise.resolve(item).then(resolved => resolved.adapt(argument))
      ))
    }
  };
};

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: multiAdapter([autoAdapter(), netlifyAdapter(), vercelAdapter(), nodeAdapter()]),
    alias: {
      '$src/*': 'src/*',
    },
  },
};

export default config;

(don't forget to npm i any adapters before using!)

Finally, let's talk about Docker. As it's a popular deployment method, here's a multi-arch Dockerfile I've written, with a build stage, deploy stage, and some healthchecks.

It's also published to DockerHub (under lissy93/devolio), so you should be able to use it with docker run -p 3000:80 lissy93/devolio - or use the docker-compose.yml as an template for your own container.


The Project

I've had to skip over a few details for the sake of brevity, but all the code is available on GitHub, so that should clear up anything that doesn't yet make sense - if it's still not, feel free to ask below :)

There's a few extra features that I also added:

  • Extracted all data into a config file, for easy usage, and made it stylable with custom colors and themes
  • I used a store to keep track of posts (in BlogStore.ts)
  • Added functionality for loading and combining multiple RSS feeds, as well as sorting and filtering results
  • Added internationalization functionality (in Language.ts)
  • I built a page to showcase your projects, with data fetched from your GitHub, via their API
  • And a contact page, with an email form, social media links and GPG keys
  • Added more adapters for deploying to various cloud services, and wrote a Dockerfile

Here's some screenshots (with just the plain theme)

Blog Page (fetched from RSS)

Blog Page

Projects Page (fetched from GitHub)

Projects Page

Social Media Links (stats fetched from APIs)

Social Media Links

I do plan on expanding the project, add some features and make it into an easily configurable, themeable developer portfolio website, that anyone can easily use. If you'd like to see the updates, drop the repo a star on GitHub :)

And if you'd like to contribute to the source, it's here (MIT) on GitHub, and I'll drop you a mention in the credits if you're able to submit a PR!


Thanks for sticking by this far! I know this post has been quite long, and is a little different from my usual format. If you've got any feedback, questions, suggestions, or comments - drop them below and I'll reply :)

If you like this kind of stuff,
consider following for more :)
Follow Lissy93 on GitHubFollow Lissy_Sykes on Twitter

You'll only receive email when they publish something new.

More from Alicia's Notes ๐Ÿš€
All posts