How to Build a Developer Blog with Storyblok and Nextjs.
Tags: webdev, showdev, javascript, programming
Published: 2023-05-08 19:06:20

In this tutorial, I walk you through concise steps to build a developer blog with syntax highlighting using Storyblok as the backend. 

[Here's a preview of what you'll build](https://devblok.netlify.app/).

## What's Storyblok?
Storyblok is a headless Content Management System.

You should have a basic knowledge of how to build [React](https://react.dev/) and [Node](https://nodejs.org/en/docs) applications to follow through this tutorial. 

You can install Node's latest version from their official [website](https://nodejs.org/en) if you don't already have it installed.

Without further ado let's dive in.

## Setting up Storyblok as backend.

Create a new [Storyblok](storyblok.com) account if you don't already have one and create a space for your project. 

In Storyblok, your projects are stored in spaces. 

## Creating Blogpost content folder.

Storyblok uses folders to group contents of similar structure in a space.

Follow these steps to make a folder:

1. In your content tab, click "+ Create new" and select Folder.

2. Input Blogpost for the folder name and select "Add new" under content-type.

3. Input Blogpost as the content-type and check both options under "Folder content settings". Click "Create" when you're done.

You should have a similar setup as the image below.

![Blogpost content type folder setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9douvp7v3cq8yohbp85n.png)

## Creating Author contents folder.

Follow the steps used in making the blogpost folder to create one for authors. You should have a similar setup as the image below.

![Author content-type folder setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4kpcs97azmdzmiwsq6r.png)

## Creating Blogpost schema.

Schema represents the field structure for data entry.

Follow the steps below to add fields for an excerpt, featured image, article body, and author in Blogpost:

1. Open the Blogpost folder, click "+ Create new" and select Story.

2. Enter your article title in the Name input field and click "Create".

![Creating a blogpost story](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hxoe5qt7ebhabbzye7sw.png)

To add the fields click "Define" to open the blocks tab on the right of your screen.


![Adding fields to schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sx5vnxc9ju2hrr9cql5f.png)

### Adding Excerpt field.

Now, follow these steps to add an excerpt field:
 
1. Input "Excerpt" as the Name for the field.

2. Click on the icon on the left of the Name input field.

3. Select "Textarea" and click "Add" to create an excerpt field.

![Adding excerpt field.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vc9wttc2j0uwov3u18gq.png)

### Adding Featured Image field.

Follow these steps to add Featured image field:
    
1. Enter "Featured_Image" in the Name input field.

2. Click the icon on the right, select "Asset" and click "Add" to create a featured image field. 

3. Click on the Featured_image field to open the Edit field tab and set the filetype to "Images" as shown in the image below.

Now, click "Save & Back to fields" to save the settings.

![Featured image edit field tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b09r8zeuwy0ympr5tyty.png)

### Adding Article body field.

Let's add Article body for the main part of the content.

1. Enter "Article_Body" in the Name field.

2. Click on the icon and select "Markdown" as the type.

3. Click "Add" to add the field.

### Adding Author field.

Now let's add your last field.

1. Enter "Author" in the Name field.

2. Click the icon, select "Author" and click Add to add the author field.

3. Click on the Author field to open the Edit field tab. Set Source to "stories" and restrict to content-type author as shown below.

The field creates a relationship between Blog posts and Authors, such that each post has an author assigned, and one author can have many different posts.

![Author edit field tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ry5xgudddklryek2t3y6.png)

## Creating Author schema.

For the author folder let's add two fields, one for profile pic and the other for bio.

### Adding Profile Photo field.

Navigate into the author folder and follow these steps to add Profile photo field.

1. Enter "Profile_image" in the Name field, select "Asset" and click "Add".

2. Click on the Profile_image field to open the Edit field tab, there set the filetype to "Images" and click "Save & Back to fields".

### Adding Author Bio field.

Input "Bio" as the Name for the field, select "Textarea" and click "Add" to create a Bio field.

Note: The title of the story entry will be the Author name of the Author so no need to add extra fields for that.

That's it! This should be okay for the tutorial.

I recommend you to add some content to your space before continuing with the rest of this tutorial.

## Building the front end.

To start a new Next.js application, run this command in your terminal:
```
npx create-next-app@latest
```
On installation choose the options as shown below in the light-blue color.

![Nextjs installation options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sw8evrxpyb9fzk29okds.png)

Note: This tutorial uses the pages directory.

After a successful installation, run the following command to navigate into your project's directory.
```
cd <project name>
```
Now, run the following command to install the application dependencies:
```
npm i next-mdx-remote graphql-request graphql rehype-prism-plus rehype-code-titles
```
The command above installs next-mdx-remote to serialize markdown returned from Storyblok, graphql and graphql-request to make [graphql]() requests to Storyblok's graphql API, and rehype plugins for syntax highlighting codes in your articles.

Now, run the following command to start a development server on **localhost:3000**:
```
npm run dev
```
If everything was successful, you should have something like this in your browser.

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

### Structuring directory.
Now, let's get ready to create our UI.

Open your text editor and structure your `/src` directory to look like so.

```
|--/src
|  |--/Components
|  |  |--Card.js
|  |--/pages
|  |  |--/api
|  |  |--_app.js
|  |  |--_document.js
|  |  |--index.js
|  |  |--/blog
|  |  |  |--[slug].js
|  |--/styles
|  |  |--global.css
|  |  |--Home.module.css
|  |  |--Slug.module.css
|  |  |--prismTheme.css
```
The "Component" directory is where your UI components will live. 

Note: The pages and styles directories are defaults to Next.js applications.

### Building UI components and pages.

In your `/Component/Card.js` file and add the following code:

```js
import Image from "next/image";
import style from "@/styles/Card.module.css";
import Link from "next/link";

function PostCard({ title, content, slug }) {

//post card for archive page

  return (
    <div className={style.postContainer}>
      <div>
        <div className={style.postImage}>
          <Image
            src={content.Featured_Image.filename}
            alt={content.Featured_Image.alt}
            fill
            className={style.imgs}
          />
        </div>
        <Link href={`blog/${slug}`}>
          <h2 className={style.postTitle}>{title}</h2>
        </Link>
      </div>

      {/* post author info */}
      <div className={style.postAuthorInfo}>
        <div className={style.avatar}>
          <Image
            src={content.Author.content.Cover_Photo.filename}
            alt="article image"
            fill
            className={style.avatarRad}
          />
        </div>
        <div>
          <div className={style.postInfo}>
          <h3>{content.Author.name}</h3>
          
            <p>April 26,2023</p>
          </div>
        </div>
      </div>
      {/* end of post author info */}
    </div>
  );
}

export default PostCard;
```
The code above creates the postcards for your archive posts as shown below.

![Post cards from devblok](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2e1h5ccppjrw21xhqxkk.png)

Now, let's build a single post page UI for posts.

Override the content inside `/pages/index.js` with the following code:
```js
import Head from "next/head";
import { gql, GraphQLClient } from "graphql-request";
import Image from "next/image";
import styles from "@/styles/Home.module.css";
import PostCard from "@/Components/Card";
import Link from "next/link";

export async function getServerSideProps() {

  const url = "https://gapi.storyblok.com/v1/api";
//instantiates a client to make request
  const client = new GraphQLClient(url, {
    headers: {
      token: process.env.PublicToken,
      version: "published",
    },
  });

//query strings for retrieving all blog posts
  const query = gql`
    {
      BlogpostItems {
        items {
          id
          name
          slug
          content {
            Featured_Image {
              filename
              alt
            }
            Author {
              name
              content
            }
          }
        }
      }
    }
  `;

// fetch multiple post from Storyblok
  const { BlogpostItems } = await client.request(query);
  return { props: { data: BlogpostItems.items } };
}

export default function Home({ data }) {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <div className={styles.itemContainer}>
          {data.map(({ id, name, slug, content }) => {
            return (
              <div key={id}>
                <PostCard title={name} content={content} slug={slug} id={id}/>
              </div>
            );
          })}
        </div>
      </main>
    </>
  );
}
```
The code above fetches data from Storyblok using graphql and renders them on your archive page.

Now, create a `pages/blog/[slug].js` file and add the following code:
```js
import { gql, GraphQLClient } from "graphql-request";
import { MDXRemote } from "next-mdx-remote";
import {serialize} from "next-mdx-remote/serialize";
import rehypePrism from 'rehype-prism-plus';
import rehypeCodeTitles from 'rehype-code-titles';
import style from "@/styles/Slug.module.css";
import Image from "next/image";

export async function getStaticPaths() {
  const url = "https://gapi.storyblok.com/v1/api";

//instantiates a new client
  const client = new GraphQLClient(url, {
    headers: {
      token: process.env.PublicToken,
      version: "published",
    }
  });

//query string to query  all slugs for pre-rendering
  const querySlug = gql`
    {
      BlogpostItems {
        items {
          slug
        }
      }
    }
  `;

// fetch for post slugs from Storyblok
  const { BlogpostItems } = await client.request(querySlug);
  const paths = BlogpostItems.items.map(({slug}) =>  ({ params: {slug} }));
  return { paths , fallback: "blocking" };
}

export async function getStaticProps({params}) {
  const url = "https://gapi.storyblok.com/v1/api";

  const client = new GraphQLClient(url, {
    headers: {
      token: process.env.PublicToken,
      version: "published",
    },
  });

//query string to query just one post content based on slug in url
  const query = gql`
  query mypost($id: ID!){
    BlogpostItem(id: $id){
      name
      content {
        Date
        Author{
          name
        }
        Article_Body
        Featured_Image {
          filename
          alt
        }
      }
    }
  }`;

// fetching data from Storyblok
  const { BlogpostItem } = await client.request(query, {id: `posts/${params.slug}`});
  
  const {name, content} = BlogpostItem;

// serialize content with next-mdx-remote.
  const source = await serialize(BlogpostItem.content.Article_Body, { mdxOptions: {
    rehypePlugins: [rehypeCodeTitles, rehypePrism]
  }})
  
  return { props: { Source: source, name, content } };
}

function SinglePost({ Source , name, content }) {
  return (
 
  <div className={style.wrapper}>
    
    <div className={style.imgWrapper}>
      <Image src={content.Featured_Image.filename} alt={content.Featured_Image.alt} fill />
  </div>
  <div><h1>{name}</h1></div>
  <div className={style.postContent}>

    <div>
      <MDXRemote {...Source}/>
    </div>
  </div>
  </div>);
}

export default SinglePost;
```
The code above check for post slugs matching the URL, fetch the data for that slug and renders them as a single post content on request.

Add the following code to the top of your `_app.js` file to apply the syntax highlighting theme.
```js
import '@/styles/prismTheme.css'
```

### Adding support for remote images.

By defaults, Nextjs prevents the use of external images in your application. 

To allow the use of images from an external source you must add the URL of that source to the `next.config` file.

In the root of your project directory, override `next.config.js` with the following:
```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.storyblok.com',
      },
    ],
  },
}

module.exports = nextConfig

```

That's it for our blog pages.

Add the following code to `env.local` in your root directory to authorize your requests.
```
PublicToken = "public token"
```
You can get your token from your space in `Settings>Access Tokens.`


![Storyblok settings tab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t36rg7poa6n393y5f1p8.png)

### Styling UI components.

In the styles folder, let's add CSS codes to style our card component and UI pages.

Override the `global.css` file with the code below:
```css
:root {
  --main-bgColor: #232323;
  --black: #000;
  --white: #fff;
  --gray: #9A9494;
  --light-gray: #eaeaea;
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

html,
body {
  max-width: 100vw;
  background-color: var(--main-bgColor);
  font-size: 1.1rem;
  overflow-x: hidden;
}

a {
  text-decoration: none;
  color: #000;
}
```
Now, create a `Card.module.css` and add the following code:
```css
.postContainer {
  box-sizing: border-box;
  padding: 20px 20px;
  width: 320px;
  height: 450px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--light-gray);
  border-radius: 12px;
}

.postTitle:hover {
opacity: 0.7;
}

.postImage {
  margin: auto;
  width: 280px;
  height: 198px;
  position: relative;
}
.postTitle {
  margin-top: 25px; 
 font-size: 1.2 rem;
  font-weight: 600;

}

.postTitle:hover {
opacity: 0.6;
}

.postAuthorInfo {
  display: flex;
  flex-direction: row;
  gap: 15px;
}
.postInfo {
  display: flex;
  flex-direction:column;
  gap: 7px;
}
.imgs {
  border-radius: 8px;
}
.avatar {
  width: 57px;
  height: 57px;
  position: relative;
  border-radius: 50%;
}
.avatarRad {
    border: 5px solid gray;
  border-radius: 100%;
}
```
The code above applies the styling to our `Card.js` components.

Now, override `Home.module.css` with the following code:
```css
.main {
  min-height: 100vh;
}

.itemContainer {
  max-width: 1020px;
  margin: 60px auto;
  display: flex;
  flex-direction: row;
  gap: 30px;
  flex-wrap: wrap;
}

/* Mobile */
@media (max-width: 700px) {
  .itemContainer {
    max-width: 320px;
  }
}

/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
  .itemContainer {
    max-width: 674px;
    }
}
```
This code styles your `index.js` page.

Create a `Slug.module.css` file and add the following code:
```css
.wrapper{
    background-color: whitesmoke;
    width: 800px;
    min-height: 700px;
    padding: 70px 70px;
    margin: 60px auto;
    border-radius: 10px;
    display: flex;
    flex-direction: column;
    gap: 30px;
}

.imgWrapper {
    width: 100%;
    height: 400px;
    position: relative;
}

/* Mobile */
@media (max-width: 700px) {
    .wrapper {
      max-width: 320px;
      padding: 20px 20px;
    }
    .imgWrapper {
        width: 100%;
        height: 200px;
    }
  }
  
  /* Tablet and Smaller Desktop */
  @media (min-width: 701px) and (max-width: 1120px) {
    .wrapper {
      max-width: 674px;
      }
  }
```
This code styles our `[slug].js` pages.

Add the following code to `prismTheme.css` to apply the syntax highlight theme.
```css
code[class*="language-"],
pre[class*="language-"] {
    -moz-tab-size: 2;
    -o-tab-size: 2;
    tab-size: 2;
    -webkit-hyphens: none;
    -moz-hyphens: none;
    -ms-hyphens: none;
    hyphens: none;
    white-space: pre;
    white-space: pre-wrap;
    word-wrap: normal;
    font-family: Menlo, Monaco, "Courier New", monospace;
    font-size: 14px;
    color: #76d9e6;
    text-shadow: none;
}

pre > code[class*="language-"] {
    font-size: 1em;
}

pre[class*="language-"],
:not(pre) > code[class*="language-"] {
    background: #2a2a2a;
}

pre[class*="language-"] {
    padding: 15px;
    border-radius: 4px;
    border: 1px solid #e1e1e8;
    overflow: auto;
    position: relative;
}

pre[class*="language-"] code {
    white-space: pre;
    display: block;
}

:not(pre) > code[class*="language-"] {
    padding: 0.15em 0.2em 0.05em;
    border-radius: .3em;
    border: 0.13em solid #7a6652;
    box-shadow: 1px 1px 0.3em -0.1em #000 inset;
}

.token.namespace {
    opacity: .7;
}

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
    color: #6f705e;
}

.token.operator,
.token.boolean,
.token.number {
    color: #a77afe;
}

.token.attr-name,
.token.string {
    color: #e6d06c;
}

.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
    color: #e6d06c;
}

.token.selector,
.token.inserted {
    color: #a6e22d;
}

.token.atrule,
.token.attr-value,
.token.keyword,
.token.important,
.token.deleted {
    color: #ef3b7d;
}

.token.regex,
.token.statement {
    color: #76d9e6;
}

.token.placeholder,
.token.variable {
    color: #fff;
}

.token.important,
.token.statement,
.token.bold {
    font-weight: bold;
}

.token.punctuation {
    color: #bebec5;
}

.token.entity {
    cursor: help;
}

.token.italic {
    font-style: italic;
}

code.language-markup {
    color: #f9f9f9;
}

code.language-markup .token.tag {
    color: #ef3b7d;
}

code.language-markup .token.attr-name {
    color: #a6e22d;
}

code.language-markup .token.attr-value {
    color: #e6d06c;
}

code.language-markup .token.style,
code.language-markup .token.script {
    color: #76d9e6;
}

code.language-markup .token.script .token.keyword {
    color: #76d9e6;
}

/* Line highlight plugin */
.line-highlight.line-highlight {
    padding: 0;
    background: rgba(255, 255, 255, 0.08);
}

.line-highlight.line-highlight:before,
.line-highlight.line-highlight[data-end]:after {
    padding: 0.2em 0.5em;
    background-color: rgba(255, 255, 255, 0.4);
    color: black;
    height: 1em;
    line-height: 1em;
    box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);
}
```
Feel free to use any other theme from [prism themes](https://github.com/PrismJS/prism-themes).

That's it for our styling! Reload your application to view changes.

## Conclusion

You've successfully built a developer blog that supports syntax highlighting with Storyblok and Next.js. I encourage you to deploy your application to any hosting platform and share it with friends.

Thanks for reading this article! If you like more of these articles please follow me. You can also find the source code on this [GitHub repo](https://github.com/amissah17/devblok).