Your Blog, Your Way: SvelteKit and Headless Hashnode Make It Possible

Your Blog, Your Way: SvelteKit and Headless Hashnode Make It Possible

ยท

6 min read

๐Ÿ’ก
How to build a simple, completely customised blog from scratch using Headless Hashnode and SvelteKit.

Why headless?

As developers, we can't help but build and customise. If you're anything like me it's easier to build things instead of writing my blog posts ๐Ÿ˜… now we can do both!

I decided to use Hashnode because I wanted as few barriers to entry as possible to get blogging, whilst using my own domain. Now that Hashnode have released headless (an API that allows you to query your posts, comments and profile etc.) I can start writing my posts safely in the knowledge that I can take my time creating a perfectly customised site without compromising on getting the posts written.

Once I'm ready I simply flip the switch...

Hashnode has provided a starter-kit for NextJS and Vercel but if like me, you are fully on the Svelte hype train (or just want to see how easy it is to build without the template) I've got your fix here.

We will be creating our site using:

  • SvelteKit

  • Tailwind CSS

  • Houdini (for the GraphQL connection)

Using the Hashnode API

First GraphQL Query

The Hashnode API utilises GraphQL - if you've not used it before, it's an alternative to REST where you explicitly ask for the data you need in the form of a 'query' rather than targeting a particular endpoint.

In our case, instead of querying, as an example, api.hashnode.com/your-blog-id/posts and getting a large (and perhaps slow) response instead you would send a request to gql.hashnode.com with the below query that only returns what we've specified.

โ—
You will need to change the host to your blog domain
query PostPreview {
    publication(host: "engineering.hashnode.com") {
        title
        posts(first: 10) {
            edges {
                node {
                    title
                    brief
                    slug
                    url
                    coverImage {
                        url
                    }
                }
            }
        }
    }
}

API Playground

Hashnode offers a great API playground for experimenting with queries to ensure they are well-formed and return the data we expect.

We can see our query is correct and is returning all the data we need to create a list of preview cards that link to the individual blog pages.

Screenshot of GQL playground

Project Set Up

Create your project and follow the Tailwind docs to bootstrap your project

npm create svelte@latest sveltekit-hashnode-blog

or follow along in the repo:

bentilling/sveltekit-hashnode-blog-template

Setting up Houdini

Setting up the boilerplate code to get a GraphQL client set up in your project can sometimes be a bit of a headache, but with SvelteKit we can utilise the great library Houdini.

This perfectly integrates into the existing file structure to get easy boilerplate-free access to our response data as we will see shortly.

Simply install the package

npm install --save-dev houdini
npx houdini@latest init

and enter https://gql.hashnode.com when prompted for the URL, and Houdini takes care of all the setup.

If you run this you can see Houdini has (amongst other things):

  • Added a houdini.config.js file which contains the GQL endpoint

  • Updated vite.config.ts to ensure Houdini runs in the build process

  • Added a $houdini folder that contains the types for a great local development experience

Creating Post Overview Page

Let's use Houdini to create a post overview page.

  1. Create a new file named +page.gql inside src/route and paste the PostPreview GQL query.

  2. Create the +page.svelte file in the same directory.

// src/routes/+page.svelte
<script lang="ts">
    import type { PageData } from '$houdini/types/src/routes/$houdini';

    export let data: PageData;

    $: ({ PostPreview } = data);
</script>

{#if $PostPreview?.data?.publication?.posts}
    <ul>
        {#each $PostPreview.data.publication.posts.edges as { node: { title, slug, brief, coverImage } }}
            <a href={`/posts/${slug}`}>
                <div class="p-6 bg-white rounded shadow-sm my-4">
                    <h2 class="text-xl pb-5 font-semibold">{title}</h2>
                    <div class="flex">
                        <img class="rounded-lg shadow-lg w-1/2" src={coverImage?.url} alt={title} />
                        <p class="m-4 w-1/2">{brief}</p>
                    </div>
                </div>
            </a>
        {/each}
    </ul>
{/if}

There is some magic happening here so let's break it down.

When this page is navigated to, queries in the +page.gql file are executed - in our case, the PostPreview query.

The results of this query are passed via the PageData prop - identical to how you would receive data from +page.ts and +page.server.ts load functions.

This data is provided in a Svelte store so that we can react to changes. This is important because unlike in REST where we wait for a response and get a success or failure, GraphQL queries respond with a loading state to indicate if the data is currently being fetched - when this is happening we will have empty data and will want our UI to respond appropriately.

We then destructure the data from inside the store and use it as normal to build our preview components.

Adding a simple universal layout with a nav bar and blog title...

// src/routes/layout.svelte
<script>
    import '../app.css';
</script>

<div class="flex flex-col bg-gray-200 w-screen">
    <nav class="bg-gray-600 text-white flex justify-between items-center">
        <h1 class=" font-mono text-xl p-6">Awesome blog built with Svelte and Hashnode Headless</h1>
    </nav>
    <div class="mx-auto grow max-w-3xl">
        <slot />
    </div>
</div>ss

and we have a basic blog home page using data from Hashnode!

Creating the post pages

Now that we've got our post previews working, let's create the individual blog pages.

First, add a new directory post/[slug] with two files +page.svelte and +page.gql, your directory should now look like this:

Then, update +page.gql with the query for our post data. You will notice the addition of $slug.

query Post($slug: String!) {
    publication(host: "engineering.hashnode.com") {
        post(slug: $slug) {
            title
            content {
                html
            }
        }
    }
}

Here $slug is a variable that the query is expecting and is used to filter to the specific post ID.

This is automatically provided by Houdini for any matching parameters in the route path. In other words, if a user navigates to your-blog.com/posts/great-article SvelteKit knows that great-article is the [slug] and passes it as a variable to the query.

As before, let's use the data from the query to create a simple page:

// src/routes/posts/[slug]/+page.svelte
<script lang="ts">
    import type { PageData } from './$houdini';

    export let data: PageData;

    $: ({ Post } = data);
</script>

<article class="bg-white px-6 max-w-screen-lg">
    {#if $Post.data?.publication?.post}
        <h1 class="text-4xl font-bold pt-10">{$Post.data.publication?.post?.title}</h1>
        {@html $Post.data.publication?.post?.content.html}
    {/if}
</article>
โ—
We are using @html tag here to directly render the HTML, make sure you trust the source of this data!

Uh oh, you might notice an issue, because we're directly rendering the HTML we have no default styling and it looks a mess...๐Ÿ˜Ÿ

Tailwind comes to the rescue again...

Tailwind CSS Typography Plugin

Beautiful typographic defaults for HTML you don't control.

Tailwind CSS have a library for exactly this purpose. First, add it to our dependencies.

npm install -D @tailwindcss/typography

Now we simply add the prose class and our text is magically formatted, we can further customise by using the prose-[tag]:[attribute] e.g. below we have underlined the h2 tags.

...
        <div class="prose prose-h2:underline">
            {@html $Post.data.publication?.post?.content.html}
        </div>
...

Much better!

And that's it! ๐ŸŽ‰ Now simply deploy your code using your favourite platform and you have the beginnings of your fully customised blog powered by Hashnode!

ย