SvelteKit Starter Blog with GraphCMS
Tags: webdev, svelte, sveltekit
Published: 2021-06-02 14:25:59

---
title: SvelteKit Starter Blog with GraphCMS
published: true
description: Get up and running with this starter, follow the guide, or use the completed project to get started in minutes.
tags: webdev, svelte, sveltekit
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wyl5o097qe9obfzsxwmr.png
canonical_url: https://graphcms.com/blog/sveltekit-starter-blog-with-graphcms
---

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](https://github.com/spences10/sveltekit-graphcms-starter-blog) 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](https://app.graphcms.com).

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.

![graphcms-dashboard-image.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bu64tnk544vlgzuuazoi.png)

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:

```bash
npm init svelte@next sveltekit-graphcms-starter-blog
```

I'm selecting Skeleton project, no TypeScript, ESLint and Prettier:

```bash
✔ 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:

```bash
# change directory
cd sveltekit-graphcms-starter-blog
# install dependencies
npm i
# open with VSCode
code .
```

The project outline looks a little like this:

```text
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:

```bash
# 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:

```graphql
query PostsIndex {
  posts {
    id
    title
    slug
    date
    excerpt
  }
}
```

![index-query-from-graphcms-playground.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yjbsea9tuska1r94730k.png)

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!

```bash
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:

```bash
# 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:

```html
<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:

![index-page-showing-api-data.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkbm42s4jbzaw4wjtw0h.png)

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:

```bash
# 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:

```html
<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:

```bash
# 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:

```html
<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](https://nextjs.org/docs/routing/dynamic-routes) and [Gatsby's File System Route API](https://graphcms.com/blog/using-the-gatsby-file-system-route-api-with-graphcms) (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:

```bash
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:

```html
<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](https://kit.svelte.dev/docs#adapters) that are available.

```bash
npm i -D @sveltejs/adapter-vercel@next
```

Then I'll need to add that to the `svelte.config` file:

```js
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:

```bash
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:

```bash
# 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:

```text
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:

```bash
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:

```bash
# 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:

```bash
touch src/app.css
```

I'll add the `base`, `components` and `utilities` Tailwind directives to it:

```css
@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:

```html
<script>
  import '../app.css'
</script>

<nav>
  <a href="/">Home</a>
</nav>

<slot />
```

Now onto adding the styles, I'm going to be using [daisyUI](https://daisyui.com/) 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](https://github.com/tailwindlabs/tailwindcss-typography) for styling the markdown.

```bash
npm i -D daisyui @tailwindcss/typography
```

Then add those to the Tailwind plugins array in the `tailwind.config.cjs` file:

```js
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:

```bash
mkdir src/lib
touch src/lib/nav.svelte
```

In the `nav.svelte` I'll add in one of the nav examples from daisyUI:

```html
<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/`:

```bash
touch src/routes/about.svelte
```

I'll use one of the daisyUI hero components and leave the default Picsum image in there:

```html
<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:

```html
<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:

```graphql
query PostsIndex {
  posts {
    title
    slug
    date
    excerpt
    coverImage {
      fileName
      url
    }
  }
}
```

And I'm using that data in a daisyUI card component:

```html
<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!

![styled-index-page.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kp1jycvevvsot1gmq8zk.png)


### 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:

```graphql
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:

```html
<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.

![styled-post-page.png](https://media.graphcms.com/lnbqeyiTlyq4SeGoFQLM)

## Conclusion

That's it! A full blog built with SvelteKit and styled with Tailwind and daisyUI components!