In this post, I'm going to detail setting up a starter blog using SvelteKit and GraphCMS. This will be a guide on creating a basic blog using SvelteKit and GraphCMS.
SvelteKit for the bleeding edge goodness that that brings and the GraphCMS starter blog so I'm up and running quickly with content that I can later build on when I want to add more content and functionality to the project.
If you prefer to just have the starter files you can get the repo here and if you only want to have a template to get started with then check out the Deploy button on the GitHub repo.
Prerequisites
There are a few things that you'll need if you're following along:
- basic web development setup: node, terminal (bash, zsh, or fish), and a text editor (VSCode).
- GitHub account
Create the back-end with GraphCMS
For this starter, I'll be using the predefined and pre-populated "Blog Starter" template available to you on your GraphCMS dashboard. The starter comes with a content schema already defined for you.
If you haven't signed up already and created a GraphCMS account you can create a free account.
On your GraphCMS dashboard select the "Blog Starter" template from the "Create a new project" section, you're then prompted to give it a name and description. I'll call my one "My Blog", pay attention to the "Include template content?" checkbox, in my case I'm going to leave this checked. I'll then select my region and "Create project".
I'm selecting the community pricing plan and I'll select "Invite later" for "Invite team members".
From the project dashboard, I can see from the quick start guide that the schema is already created and the API is accessible.
Clicking on the "Make your API accessible" section of the quick start guide I will be taken to the "API access" settings panel, from here I'm going to check the "Content from stage Published" checkbox and click "Save" so the API is publicly accessible.
Now, I have an accessible GraphQL endpoint to access the blog data from. Time to scaffold out the project to get the data.
Initialise the project with npm
For this example I'm using the npm init
command for SvelteKit:
npm init svelte@next sveltekit-graphcms-starter-blog
I'm selecting Skeleton project, no TypeScript, ESLint and Prettier:
✔ Which Svelte app template? › Skeleton project
✔ Use TypeScript? … No
✔ Add ESLint for code linting? … Yes
✔ Add Prettier for code formatting? … Yes
Now I have the absolute bare-bones starter for a SvelteKit project.
Popping open VSCode in the project directory I can see what I have to work with:
# change directory
cd sveltekit-graphcms-starter-blog
# install dependencies
npm i
# open with VSCode
code .
The project outline looks a little like this:
sveltekit-graphcms-starter-blog/
├─ src/
│ ├─ routes
│ │ └─ index.svelte
│ ├─ app.html
│ └─ global.d.ts
Running npm run dev
from the terminal will start the dev server on the default localhost:3000
you can edit this command to open the web browser on that port with some additional flags being passed to the npm run dev
command:
# double dash -- to pass additional parameters
# --port or -p to specify the localhost port
# --open or -o to open the browser tab
npm run dev -- -o -p 3300
Get posts data from GraphCMS API
Cool, cool, cool, now to start getting the data from the API to display on the client.
If I pop open the API playground on my GraphCMS project, I can start shaping the data I want to get for the index page of my project.
I'll want to query all posts available and grab these fields to display on the index page:
query PostsIndex {
posts {
id
title
slug
date
excerpt
}
}
I'm going to install graphql-request
and graphql
as dependencies so I can query the GraphCMS GraphQL API endpoint with the query I defined in the API playground!
npm i -D graphql-request graphql
I'm copying my Access URL endpoint to a .env
file which can be accessed in Vite with import.meta.env.VITE_GRAPHCMS_URL
. First up I'll create the file first, then add the variable to it with the accompanying Access URL:
# creathe the file
touch .env
# ignore the .env file in git
echo .env >> .gitignore
# add the environment variable to the .env file
echo VITE_GRAPHCMS_URL=https://myendpoint.com >> .env
In the src/routes/index.svelte
file I'm using Svelte's script context="module"
so that that I can get the posts from the GraphCMS endpoint ahead of time. That means it’s run when the page is loaded and the posts can be loaded ahead of the page being rendered on the screen.
This can be abstracted away later for now it's to see some results on the screen:
<script context="module">
import { gql, GraphQLClient } from 'graphql-request'
export async function load() {
const graphcms = new GraphQLClient(
import.meta.env.VITE_GRAPHCMS_URL,
{
headers: {},
}
)
const query = gql`
query PostsIndex {
posts {
id
title
slug
date
excerpt
}
}
`
const { posts } = await graphcms.request(query)
return {
props: {
posts,
},
}
}
</script>
<script>
export let posts
</script>
<h1>GraphCMS starter blog</h1>
<ul>
{#each posts as post}
<li>
<a href="/post/{post.slug}">{post.title}</a>
</li>
{/each}
</ul>
I'll quickly break down what's happening here, as I mentioned earlier the first section contained in the <script context="module">
block is being requested before the page renders and that returns props: { posts }
.
The next section is accepting the posts
as a prop to use them in the markup the {#each posts as post}
block is looping through the posts
props and rendering out a list item for each post.slug
with the post.title
.
Running the dev server will now give me a list of posts available:
Cool! Clicking one of the links will give me a 404 error though, next up I'll create a layout and an error page.
Create 404 page and layout
Adding a layout will mean certain elements will be available on each page load, like a navbar and footer:
# create the layout file
touch src/routes/__layout.svelte
For now, a super simple layout so that users can navigate back to the index page:
<nav>
<a href="/">Home</a>
</nav>
<slot />
The <slot />
is the same as you would have in a React component that would wrap a children
prop. Now everything in src/routes
will have the same layout. As mentioned this is pretty simple but allows for styling of everything wrapped by the layout. Not mentioned styling yet, more on that soon(ish).
Now to the not found (404) page:
# create the layout file
touch src/routes/__error.svelte
Then to add some basic information so the user can see they've hit an undefined route:
<script context="module">
export function load({ error, status }) {
return {
props: { error, status },
}
}
</script>
<script>
export let error, status
</script>
<svelte:head>
<title>{status}</title>
</svelte:head>
<h1>{status}: {error.message}</h1>
Clicking on any of the links on the index page will now use this error page. Next up, creating the pages for the blog posts.
You may have noticed <svelte:head>
in there, that's to add amongst other things SEO information, I'll use that in the components from now on where applicable.
Creating routes for the blog posts
SvelteKit uses file-based routing much the same as in NextJS and Gatsby's File System Route API (and nowhere near as much of a mouth full!)
That's a fancy way of saying the URL path for each blog post is generated programmatically.
Create a [slug].svelte
file and posts
folder in the src
directory:
mkdir src/routes/post/
# not the quotes around the path here 👇
touch 'src/routes/post/[slug].svelte'
In <script context="module">
pretty much the same query as before but this time for a single post and using the GraphQL post
query from the GraphCMS endpoint.
To get the slug
for the query I'm passing in the page context
where I'm getting the slug from the page params:
<script context="module">
import { gql, GraphQLClient } from 'graphql-request'
export async function load(context) {
const graphcms = new GraphQLClient(
import.meta.env.VITE_GRAPHCMS_URL,
{
headers: {},
}
)
const query = gql`
query PostPageQuery($slug: String!) {
post(where: { slug: $slug }) {
title
date
content {
html
}
tags
author {
id
name
}
}
}
`
const variables = {
slug: context.page.params.slug,
}
const { post } = await graphcms.request(query, variables)
return {
props: {
post,
},
}
}
</script>
<script>
export let post
</script>
<svelte:head>
<title>{post.title}</title>
</svelte:head>
<h1>{post.title}</h1>
<p>{post.author.name}</p>
<p>{post.date}</p>
<p>{post.tags}</p>
{@html post.content.html}
Take note of the last line here where I’m using the Svelte @html
tag, this renders content with HTML in it.
Now I have a functional blog, not pretty, but functional.
Deploy
Now to deploy this to Vercel! You're not bound to Vercel, you can use one of the SvelteKit adapters that are available.
npm i -D @sveltejs/adapter-vercel@next
Then I'll need to add that to the svelte.config
file:
import vercel from '@sveltejs/adapter-vercel'
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte',
adapter: vercel(),
},
}
export default config
Now to deploy to Vercel, I'll need the Vercel CLI installed, that's a global npm or Yarn install:
npm i -g vercel
# log into vercel
vc login
I'm prompted to verify the log in via email then I can use the CLI. I'll deploy with the vercel
command from the terminal:
# run from the root of the project
vercel # or vc
I can go straight to production with this by using the --prod
flag but before I do that I'll add the VITE_GRAPHCMS_URL
env value to the setting panel on the Vercel project page for what I've just deployed.
In the settings page of the Project, there's a section for "Environment Variables", the URL will be specific to your username and project name, it should look something like this:
https://vercel.com/yourusername/your-project-name/settings/environment-variables
In the Vercel UI for the Environment Variables, I'll add in the VITE_GRAPHCMS_URL
for the name and the GraphCMS API endpoint for the value.
Now I can build again this time with the --prod
flag:
vc --prod
That's it, I now have a functional blog in production! If you want to go ahead and style it yourself go for it! If you need a bit of direction on that I got you too!
Style it!
Now time to make it not look like 💩!
I'll be using Tailwind for styling the project
Tailwind can be added to SvelteKit projects by using the svelte-add
helper, there's loads to choose from in this case I'll be using it with JIT enabled:
# use svelte-add to install Tailwind
npx svelte-add tailwindcss --jit
# re-install dependencies
npm i
Tailwind will need the @tailwind
directives so I'll create an app.css
file in the src
directory:
touch src/app.css
I'll add the base
, components
and utilities
Tailwind directives to it:
@tailwind base;
@tailwind components;
@tailwind utilities;
Then I'll bring that into the layout, it could also be added directly to the src/app.html
file as a link, in this instance I'll add it to the layout:
<script>
import '../app.css'
</script>
<nav>
<a href="/">Home</a>
</nav>
<slot />
Now onto adding the styles, I'm going to be using daisyUI which has a lot of pre-built components to use for scaffolding out a pretty decent UI quickly. I'm also going to use Tailwind Typography for styling the markdown.
npm i -D daisyui @tailwindcss/typography
Then add those to the Tailwind plugins array in the tailwind.config.cjs
file:
module.exports = {
mode: 'jit',
purge: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [require('daisyui'), require('@tailwindcss/typography')],
}
Add nav component
I'm going to move the navigation out to its own component and import that into the layout, first up I'll create the lib
folder, where component lives in SvelteKit:
mkdir src/lib
touch src/lib/nav.svelte
In the nav.svelte
I'll add in one of the nav examples from daisyUI:
<div class="navbar mb-5 shadow-lg bg-neutral text-neutral-content">
<div class="flex-none px-2 mx-2">
<span class="text-lg font-bold">Starter Blog</span>
</div>
<div class="flex-1 px-2 mx-2">
<div class="items-stretch hidden lg:flex">
<a href="/" class="btn btn-ghost btn-sm rounded-btn">Home</a>
<a href="/about" class="btn btn-ghost btn-sm rounded-btn"> About </a>
</div>
</div>
</div>
Add about page
There's an about page in the markup here so I'll quickly make an about page in src/routes/
:
touch src/routes/about.svelte
I'll use one of the daisyUI hero components and leave the default Picsum image in there:
<div class="hero min-h-[90vh]" style='background-image: url("https://picsum.photos/id/1005/1600/1400");'>
<div class="hero-overlay bg-opacity-60" />
<div class="text-center hero-content text-neutral-content">
<div class="max-w-md">
<h1 class="mb-5 text-5xl font-bold">GraphCMS blog starter</h1>
<p class="mb-5">GraphCMS blog starter built with SvelteKit, Tailwind and daisyUI</p>
</div>
</div>
</div>
Add nav to layout
I can now import the newly created nav into src/routes/__layout.svelte
, I'll also add a basic container to wrap the slot:
<script>
import Nav from '$lib/nav.svelte'
import '../app.css'
</script>
<nav />
<div class="container max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<slot />
</div>
Style the index page
There's some additional fields I'm going to use from GraphCMS for a post image, here's what the GraphQL query looks like now:
query PostsIndex {
posts {
title
slug
date
excerpt
coverImage {
fileName
url
}
}
}
And I'm using that data in a daisyUI card component:
<h1 class="text-4xl font-semibold mb-7 text-gray-700">GraphCMS starter blog</h1>
<ul>
<li>
{#each posts as post}
<div class="card text-center shadow-xl border mb-10">
<figure class="px-10 pt-10">
<img src="{post.coverImage.url}" alt="{post.coverImage.fileName}" class="rounded-xl" />
</figure>
<div class="card-body">
<h2 class="card-title">{post.title}</h2>
<p class="prose-lg">{post.excerpt}</p>
<div class="justify-end card-actions">
<a href="post/{post.slug}" class="btn btn-secondary">➜ {post.title}</a>
</div>
</div>
</div>
{/each}
</li>
</ul>
Looking a bit better now!
Style the post (slug) page
Same with the post page I'v amended the GraphQL query to bring back some additional data for the author image and the post tags; Here's what the query looks like:
query PostPageQuery($slug: String!) {
post(where: { slug: $slug }) {
title
date
content {
html
}
tags
author {
name
title
picture {
fileName
url
}
}
coverImage {
fileName
url
}
}
}
I've added some Tailwind styles and brought in the author image. I'm also rendering out any tags if they're in the post data:
<h1 class="text-4xl font-semibold mb-7 text-gray-700">{post.title}</h1>
<a href="/" class="inline-flex items-center mb-6">
<img alt="{post.author.picture.fileName}" src="{post.author.picture.url}" class="w-12 h-12 rounded-full flex-shrink-0 object-cover object-center" />
<span class="flex-grow flex flex-col pl-4">
<span class="title-font font-medium text-gray-900">{post.author.name}</span>
<span class="text-gray-400 text-xs tracking-widest mt-0.5">{post.author.title}</span>
</span>
</a>
<div class="mb-6 flex justify-between">
<div>
{#if post.tags} {#each post.tags as tag}
<span class="py-1 px-2 mr-2 rounded bg-indigo-50 text-indigo-500 text-xs font-medium tracking-widest">{tag}</span>
{/each} {/if}
</div>
<p class="text-gray-400 text-xs tracking-widest mt-0.5">{new Date(post.date).toDateString()}</p>
</div>
<article class="prose-xl">{@html post.content.html}</article>
Take note here of the last line where I'm using Tailwind Typography to style the Markdown.
Conclusion
That's it! A full blog built with SvelteKit and styled with Tailwind and daisyUI components!