Make a blog with svelte kit and host on github (Updated on Feb 7 - 2023)
After reading this post, you should be able to make a blog using svelte kit, and host it on github or any other hosting service of your choice. You can write your posts as markdown files.
This post assumes that you know what svelte is and also what hosting a static site means. You can read more about svelte here - https://svelte.dev. And more about svelte kit here - https://kit.svelte.dev.
I recently moved by teeny tiny unmaintained blog from gatsby to svelte kit. Removing gatsby removed a lot of complexity which comes with gatsby. I no longer understood what was going on under the hood and was too scared to upgrade gatsby.
Switching to svelte-kit was relatively straight forward except for a few quirks. The quirks mainly had to do with rendering markdown as html and figuring out how to publish the final site through github.
If you don’t want to read the whole article, you can checkout the final code here - https://github.com/mukeshsoni/mukeshsoni.github.io/tree/svelte.
Step 1 - Setup svelte kit template
You can get started with svelte kit template using their npm init
command -
npm create svelte@latest <project_name>
cd <project_name>
npm install
npm run dev -- --open
When you run npm create svelte@latest <your_project_name>
, you will be asked a set of questions. E.g. Do you want to enable typescript in this project? Once you answer all the questions, the command will create a set of files in the <your_project_name> folder. Think of the command as a helper which chooses the right template for you.
Once the create
command creates the files, you can go inside the folder and install the npm dependencies. The create command does not install the dependencies. It only specifies the list of dependencies in the package.json file.
After the dependencies are installed, you can invoke the dev
script which is already defined in your package.json
file by the create
command - npm run dev -- --open
. This should start a localhost server on port 5273. Whatever port it starts the server on will be shown on the terminal. sveltekit
uses vite for building and serving the site.
Step 2 - Add mdsvex to convert markdown files to html
npm install -D mdsvex
Add mdsvex
preprocessor to your svelte.config.js
file -
const config = {
extensions: ['.svelte', '.md'],
preprocess: [
vitePreprocess(),
mdsvex({
extensions: ['.md'],
layout: {
blog: './src/routes/blog/post_layout.svelte'
}
})
],
// rest of the config
You will have to restart the dev server for mdsvex preprocessor to kick in.
We have also added link to blog post layout in layout
option passed to mdsvex
function. This tells mdsvex
to use the post_layout.svelte
layout for all the stuff rendered inside blog
route. My post_layout.svelte
file looks like this -
<script>
import PostHeader from '$lib/PostHeader.svelte';
import Bio from '$lib/Bio.svelte';
// if i don't add the exports here, i can't access title and created as props
export let title;
export let created;
</script>
<div class="post">
<PostHeader {title} {created} />
<slot />
<br />
<a href="/">Browse more posts</a>
<hr />
<h3>About me</h3>
<Bio />
</div>
PostHeader
and Bio
are regular svelte component which are present inside src/lib
folder. <slot/>
is where the content of our markdown file will be put.
Try adding a markdown file called +page.md
somewhere in your routes
folder and going to the route pointed by the folder inside the routes
folder. E.g. Add a file src/routes/my-awesome-post/+page.md
and put some content in there. You should be able to view that content on localhost:5173/my-awesome-post
.
Note that simply adding a markdown file with any name will not work. sveltekit
after v1.0.0
only treats files starting with +page
or +server
for routing purposes. This will give us some problem when trying to render our blog posts dynamically.
Step 3 - Show a list of all blog posts on home page
We want to collect all our markdown files and show the list of blog posts on the home page or on some other path, so that our users can browse the super amazing content we publish.
We will create an api endpoint to get the list of posts. We will use import.meta.glob
to get the metadata for each of the markdown files. . If we try to get the metadata using a glob pattern in our routes/+page.svelte
file, it will load all the blog posts upfront too. Which might be a disaster if we have too many blogs posts or our posts are heavy.
So we create an endpoint which returns the metadata for blogs in
/src/routes/api/posts/+server.ts
file. Just like sveltekit automatically treats any file called +page.svelte
as a route file when it’s inside the routes
folder, it treats +server.ts
(or +server.js
) file as a server endpoint. We can define a function inside this file which might then respond to a GET
or POST
or DELETE
or UPDATE
http request. In our case we need it to respond to a GET
request.
// src/routes/api/posts/+server.ts file
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
interface Post {
created: string;
title: string;
slug: string;
}
function dateSort(a: Post, b: Post) {
// have to use getTime to satisfy typescript
// And if i make this file a js file, the endpoint is not detected since
// it's a typescript svelte project
return new Date(b.created).getTime() - new Date(a.created).getTime();
}
export const GET = (async () => {
const modules = import.meta.glob('../../blog/*.md');
const posts: Array<Post> = [];
await Promise.all(
Object.entries(modules).map(async ([_, module]) => {
const { metadata } = await module();
posts.push(metadata);
})
);
// Newest first
posts.sort(dateSort);
return json({
posts: posts
});
}) satisfies RequestHandler;
A few things to keep in mind for the above endpoint -
- We don’t return our object directly but whatever
json
function exposed by sveltekit returns. If you look at the docs, it will tell you to returnnew Response('your response')
. But that doesn’t work for a json response. Thejson
utility makes sure that the right headers are set for json response type. - We make sure that our GET function adheres to
RequestHandler
type which helps us catch any type related bugs.
We will now fetch these posts using the endpoint we created. We create a +page.ts
file inside routes
folder, which is executed by sveltekit on both server and client before it renders +page.svelte
file. The +page.ts
file will export a load
function which can return whatever it likes. Whatever is returned from the load
function can then be accessed in +page.svelte
as a variable named data
by exporting it - export let data
.
// src/routes/+page.ts file
export async function load({ fetch }) {
const response = await fetch('/api/posts');
const { posts } = await response.json();
return {
posts
};
}
Does having an endpoint mean that we need a server too? Ans - No. When we run the build script, which runs the static adapter, the endpoint is called and the post metadata is resolved before the final output is produced.
Now, posts should have the list of posts with their metadata, as defined by you at the top of each post. E.g. If you have this at the top of a post -
---
title: Make a blog with svelte kit and host on github
date: '2021-05-18'
slug: '2021-05-81-make-blog-with-sveltekit'
---
That post’s metadata would be an object and look like this
{
title: Make a blog with svelte kit and host on github,
date: '2021-05-18',
slug: '2021-05-81-make-blog-with-sveltekit',
}
You can then render whatever metadata you want to.
{#each posts as post}
<li>
<a href="/blog/{post.metadata.slug}">
<h2>{post.metadata.title}</h2>
</a>
</li>
{/each}
Inside +page.svelte
file
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<div>
<h2>Blog posts</h2>
<ul class="posts">
{#each data.posts as post}
<li>
<a rel="prefetch" href="blog/{post.slug}">
<h2>
{post.title}
</h2>
</a>
<div class="date">
{post.created}
</div>
</li>
{/each}
</ul>
</div>
Step 4 - Add a layout component for the blog
If you have a header which you want to show for each page in the blog/site, you can add that as a layout component. For e.g. adding routes/+layout.svelte
file, with a
<main style="max-width: 42rem;margin:auto">
<h1 class="logo logo-big">
<a class="header-link" href="/"> unstack.in </a>
</h1>
<slot />
</main>
You can read more about svelte kit layout components here - https://kit.svelte.dev/docs/routing#layout
Step 5 - Add a layout component for individual blog posts
This was trickier than i thought. At least my first attempt. And when i found the solution, it was not as tricky as i thought. At least not as tricky as trying to read this paragraph.
I thought i would utilize svelte-kit’s layout capability, with +layout.svelte
file in the blog folder. But I wanted the layout component for blog post to take care of the post title. My first attempt was to use the load
function provided by svelte-kit
and then fetching the metadata for the post by importing that file. That worked during development but didn’t work with static adapter. Since i was importing file dynamically, it needed a server.
Then i found out about layouts in mdsvex. All i had to do was add a svelte component somewhere, say src/routes/blog
folder, and then point mdsvex to that file. That is done in mdsvex.config.cjs
// in svelte.config.js file somewhere
mdsvex({
extensions: ['.md'],
layout: {
blog: './src/routes/blog/post_layout.svelte'
}
});
And the post_layout.svelte
component looked like this
<script>
import PostHeader from '$lib/PostHeader.svelte';
// if i don't add the exports here, i can't access title and date as props
export let title;
export let date;
</script>
<div class="post">
<PostHeader {title} {date} />
<slot />
</div>
Now all my blog posts had a title and date at the top. The layout component also enabled styling the container for each post, if i wanted to.
More about mdsvex
layout components here - https://mdsvex.pngwn.io/docs#layouts
Step 6 - Rendering individual blog post
Let’s say we have 20 odd blog posts on our blog. Each in their own markdown file. And we want to render them on a route like <my-site>.com/blog/blog-post-title
. We cannot just create a file named blog-post-title.md
inside src/routes/blog/
folder and be done. Because those files will not be treated as routes by sveltekit
. Remember, sveltekit
only treats a file as a route if it starts with +page
. So one way to render our blog posts would be to create one folder per blog post and then create a +page.md
file inside each of those folders which then contains the post content. E.g.
src
routes
blog
blog-post-title
+page.md
asinine-personal-details-post
+page.md
very-basic-trick-but-my-god-its-popular
+page.md
super-complicated-post-no-one-wants-to-read
+page.md
more-about-myself-me-me-me-me
+page.md
Now we should be able to access localhost:5173/blog/blog-post-title
or localhost:5173/blog/asinite-personal-details-post
.
I actually like this technique since most of us don’t write too many posts. I would bet most of us write less than 10 blog posts in our life time. And this is a perfectly file way to render those mostly asinine posts.
But we are certainly taking this much pain to render a super duper simple blog post because we like writing posts. We are doing it because we want to have some fun :). Let’s look at another way to render the same blog posts in a more dynamic way.
Step 6.2 - Rendering blog post dynamically
Here’s what we want to do -
- Put all our post related markdown files in one folder
- Write a script which then dynamically looks for the required file from the url, picks it up and renders it.
sveltekit
supports something called dynamic routes for scenarios where we don’t have fixed routes like /about
or /home
etc. We want our /blog/blog-title
routes to automagically pick the right content.
sveltekit
allows us to create a folder whose name looks like this [some-name]
inside the routes
folder. When sveltekit
comes across such a folder, it will then forwards all request coming to that path, to the +page.svelte
file inside that folder. E.g. if we create a folder like src/routes/blog/[slug]/
then sveltekit
will render the +page.svelte
file inside src/routes/blog/[slug]/
folder for request to /blog/abc
or /blog/new-post
. It will use the name slug
inside the square brackets ([]
) to pass as params property to load
function in +page.ts
file inside [slug]
folder. E.g.
// /src/blog/[slug]/+page.ts file
function load({ params }) {
console.log(params.slug); // will print whatever comes after /blog/ in the url
}
We will now use the load
function in +page.ts
to load the blog post asked for
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
const { slug } = params;
// try {
const post = await import(`/src/routes/blog/post-${slug}.md`);
return {
content: post.default
};
// } catch (e) {
// return {
// error: 'Could not load page'
// };
// }
}) satisfies PageLoad;
We use import
to import the intended blog post. If we don’t file it in our file system, we return an error and hopefully also render an error page.
A few things to note above -
- One very weird thing happened when i had the
import
for themd
file for theslug
inside a try-catch block. Theadapter-static
could not figure out that it needed to prerender themd
file and would try to load themd
file from the route with aGET
request. That would fail with a 404 http code. I removed the try-catch as seen in code above, and adapter-static started doing it’s work perfectly. - You will see in the above import is that i have a
post-
prefix before the${slug}.md
part of the file name for the blog post. We need that because otherwise vite complains that it’s not able to resolve the dynamic import. It needs some prefix to narrow down the list of files to look at inside the folder.
When use use import
to get our md
file, it is automatically passed through mdsvex
which then returns a svelte component for the imported md
file. We return that svelte component as content
property from whatever object is returned from load
function. This svelte
component returned by import
cannot be render directly. We need to use <svelte:compoennt>
directive to render a svelte component dynamically.
We do that inside src/blog/[slug]/+page.svelte
file -
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<div>
<svelte:component this={data.content} />
</div>
Now we should be in a good place to render all our blog posts dynamically. Not that it matters.
Step 7 - Hosting the blog on github
Github allows static site hosting in 2 ways - either as a user site or as a project site. You can read more about github hosting here. I have my site hosted as a user site. Which meant i had to push the output of the static adapter to my master branch. But what is this static adapter thingy?
svelte kit provides various adapters to convert your site in ways which are suited for your deployment environemnt. In our case, we want the site to be hosted on github (or netlify etc.) as a static site. No server. Just html pages with some css and js. For that, we use the svelte-kit static adapter
Install svelte static adapter
We can install the static adapter from npm.
npm i -D @sveltejs/adapter-static@next
We then have to change the svelte config to tell it to use static adapter -
import staticAdapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', ...mdsvexConfig.extensions],
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [mdsvex(mdsvexConfig), preprocess()],
kit: {
adapter: staticAdapter({
// default options are shown
pages: 'build',
assets: 'build',
fallback: null
})
}
};
Generate content to host as static site
Once we have setup the static adapter, calling npm run build
on the terminal should generate the build folder.
npm run build
cd build
npx serve
We can test the build by going inside the build folder and using serve
or any other server to serve the files. serve
starts a server on localhost:5000
by default.
Install gh-pages and deploy site
Now that we have the built files, we have to push them to the master branch, for user site, or to the gh-pages
branch for a project site. Once we update the branch and push to github, github will then deploy the files for us and we should be able to see our site come alive.
gh-pages
is a handy tool to push the contents of any folder to any other branch in your repository. Installing gh-pages
is straight forward with npm.
npm install -D gh-pages
I then added an npm script to make deployment easy -
"scripts": {
"deploy": "npm run build && gh-pages -b master -d build -t"
},
If you are deploying a project site, you can remove the -b master
flag from gh-pages command. gh-pages
pushes to gh-pages
branch by default.
gh-pages
will also push the branch to remote. So if all goes well, you should be able to see your site live after some time.
Fixing _apps/.js and _apps/.css paths returning 404 on the deployed site
After you deploy your site by pushing the github branch, you will probably see your site looking a little different to what it looks on your development environment. That’s because some of the js and css is missing. If you open the devtools and then the network tab, a number of js and css files might be returning 404. All of those would be serving files from inside the __app
folder. That is because github does not deploy the __app
folder. All folders starting with an _
are to be ignored for sites built with jekyll. Read more about it here.
To get around the problem, we have to add an empty file named .nojekyll
to our static folder. npm run build
will then copy that file to the build
folder and gh-pages
will then push that file to the required github branch.
You might find that it still doesn’t work. Mainly because gh-pages
is not pushing the .nojekyll
file to the github branch, because gh-pages
tool ignores all files starting with a .
(dot). We can tell the gh-pages
tool to also push dot files from the build folder by using the -t
flag. If you copied the deploy command from above, it’s already there.
That is it! Go ahead and add a lot of blog posts as markdown files and deploy to your site. I am kidding. Building the blog with svelte kit was fun though.
Link to final code - https://github.com/mukeshsoni/mukeshsoni.github.io/tree/svelte