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
Lissy93 / my-website
β¨ My personal homepage. A developer portfolio site that aggregates all your projects, blog posts, and stats in one place
β¨ My Website
A re-usable aggregated portfolio and blog site for developers
aliciasykes.com
Intro
This is my personal website. It's configurable, so feel free to use it, or any parts of it for yourself :)
About
A self-hosted developer homepage, to showcase your projects, posts, coding stats, and more.
Data is fetched from external sources (GitHub, RSS, social platforms...), so no need for a CMS.
Crafted with SvelteKit + TypeScript- prioritising SEO, performance, accessibility, and compatibility.
Contents
A tutorial, for how to build something similar is available on DEV.to
A mirror of this repository is available at codeberg.org/alicia/devolio
Pages
Portfolio Page - Displays projects from GitHub
The portfolio page displays the projects you've built. Data is fetched from your GitHub profile, with optional extra fields added in the config.
Each project can include: name, description, thumbnailβ¦
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 settingin: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!
- Install the adapter for your desired provider
- E.g. for Netlify:
npm i --save-dev @sveltejs/adapter-netlify
- E.g. for Netlify:
- Import said adaptor in your
svelte.config.js
file- E.g.
import netlifyAdapter from '@sveltejs/adapter-netlify';
- E.g.
- Initiate the adaptor, within the config object, under
kit
- kit:
{ adapter: netlifyAdapter() }
- kit:
- 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)
Projects Page (fetched from GitHub)
Social Media Links (stats fetched from APIs)
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 :)
Top comments (10)
Nice article! I just thought the title should be SvelteKit 1.0 - Build a blog, fetching posts from your DEV profile π¦
Oh yeah, you're right - I've updated it :) Thanks!
Seriously π,it's like you just heard my voice
Alicia, your concise explanation of how Sveltekit works is hands done, one of the best I've stumbled upon. β€οΈ
Hi Lissy,
great article π.
Maybe this is interesting for the long time if you fetch the API: kit.svelte.dev/docs/load#streaming...
This is absolutely perfect for a great introduction. I'm almost done with going through the doc's but this in tandem will do nicely! Thank you very much for this!
So neat
Pretty good one. I also made a template like that but it's only for blog,
SveDev.
thanks
Awesome, thanks for sharing!