SvelteKit Starter Blog with GraphCMS

Scott Spence - Jun 2 '21 - - Dev Community

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.

graphcms-dashboard-image.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:

npm init svelte@next sveltekit-graphcms-starter-blog
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

The project outline looks a little like this:

sveltekit-graphcms-starter-blog/
├─ src/
│  ├─ routes
│  │  └─ index.svelte
│  ├─ app.html
│  └─ global.d.ts
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

index-query-from-graphcms-playground.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!

npm i -D graphql-request graphql
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

For now, a super simple layout so that users can navigate back to the index page:

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

<slot />
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Tailwind will need the @tailwind directives so I'll create an app.css file in the src directory:

touch src/app.css
Enter fullscreen mode Exit fullscreen mode

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

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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 />
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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')],
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Looking a bit better now!

styled-index-page.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:

query PostPageQuery($slug: String!) {
  post(where: { slug: $slug }) {
    title
    date
    content {
      html
    }
    tags
    author {
      name
      title
      picture {
        fileName
        url
      }
    }
    coverImage {
      fileName
      url
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Take note here of the last line where I'm using Tailwind Typography to style the Markdown.

styled-post-page.png

Conclusion

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .