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.
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.
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 processAdded 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.
Create a new file named
+page.gql
insidesrc/route
and paste the PostPreview GQL query.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>
@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!