How to Build a Nextjs Blog with Hygraph and Deploy to Vercel in 2023.
Tags: webdev, javascript, beginners, tutorial
Published: 2023-02-21 00:52:36

As a part of my 100 days coding challenge this year I decided to build a Next.js blog with Hygraph powering my backend and to also document the process in this article.

In this write-up, I will teach you how to build a fully responsive blog in two simple steps.

Here's a [preview](https://nextblogs.vercel.app/) of the blog you'll build.

You should have a basic knowledge of how to build React and Node applications to follow through this tutorial.

## What's Next.js?
Next.js is a react framework for building web apps. Next.js ships with features like Server Side Rendering, Static Site Generation, and Incremental Static Regeneration that abstract a lot of work from the developer so that you can focus on the UI.

## What's GraphQL?
GraphQL is a query language for making CRUD operations to an API.

## What's Hygraph?
Hygraph is headless CMS that can be used as the backend for your web applications. Hygraph uses GraphQL for making CRUD operations, allowing you to request specific data from an API.

## What's Vercel?
Vercel is a platform for hosting your single-page application.

Without further ado let's dive right in.

## STEP 1: SETTING UP THE HYGRAPH BACKEND.
Go to [hygraph](https://hygraph.com/) and create an account if you don't already have one.

Now, click **Add Project** to create a new project. Give the project a name, and description, and select the region you'd like to serve your content from.

![Image of hygraph dashbord](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kzxp5h27vkmhbjjfr2g3.png)

## ADDING CONTENT SCHEMA's

Now, let's add two Models to the project, one for blog posts and the other for author information.

Models in Hygraph define the structure for adding data. The fields for a model can be added from the **Add field** section on the right side.

From your Hygraph dashboard, Click on Schema and then click on **`+ Add`** on the right side of **MODELs**.

![image of schema and model section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9jagppauykbqdcn2z17o.png)

Now, input **"Author"** as the display name and click **Add Model**.

In the author model, add two fields Author name field and the Author description field.

Author name: Add a **Single line field** from the right sidebar and set it as required under the Advanced tab.

Author description: Add a **Multi line field** from the right sidebar and set it as required under the Advanced tab.

![Image of author model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rbtjpymdnhef6tjvwbw0.png)

Hurray! you've created your first Model.

Now let's add the Blogpost model, this model comprises a Title field, an Excerpt field, a Slug field, an asset field for the post's featured image, a rich text format field for post content, and a reference field to link the Author model to Post model.

Title: add a **Single line field**, set as required.

Excerpt: add a **Multi line field**, set as required.

Slug: add a Slug field. For the Slug field, you'll need to check **Generate slug from template** under slug options and enter `{title}` into the slug template field. This allows the slug to be auto-generated using the title field.

Under the validations tab, check **Make the field required**, **Set field as unique**, **Match specific pattern,** and **case insensitive**.

Featured Image: add an **Asset picker field**, use coverPhoto as the display name, and set it as required.

Content: Add a rich text field from the right sidebar, and set it as required.

Author reference: Add a reference field to the BlogPost model.
On the Define relationship tab, select Allow only one model to be referenced, Two-way reference, and allow multiple Blogposts per Author.
Now proceed to configure the reference and give it a display name of Author.
Under the reverse field tab, give it a display name of Blogposts.

Well, this is all we'll need for the post model.

Let's add one more thing to the **Asset field**, go to **Asset** on the left of your dashboard and add a single line field for the image alt attribute.

You should have a setup like the one below

![Image of blogpost model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ukqwgbyecokj7tax8sk.png)

**Recommended:** Populate your Post model before continuing with the rest of the article.

## STEP 2: BUILDING THE FRONTEND

Now, let's get into some action developing our blog front with Next.js.

To kickstart a Next.js app, run the following command in your terminal:
```
npx create-next-app@latest
```

Latest versions of create-next-app will ask if you'd like to use `/src` or the experimental `/app` directory, for this tutorial opt for `/src` directory.

After Next.js installs successfully, run the following command in the terminal:
```
cd <project name>
code .
```
Before starting your development server run the command below to install a few dependencies:
```
npm install next-mdx-remote graphql-request graphql
```
Now, run the following command to start the development server on localhost:3000.
```
npm run dev
```

Now, structure your **./src** directory to look like this.

```
|--/src
|  |--/Components
|  |  |--Layout.js
|  |  |--Nav.js
|  |--/pages
|  |  |--/api
|  |  |--_app.js
|  |  |--_document.js
|  |  |--index.js
|  |  |--[slug].js
|  |--/styles
|  |  |--global.css
|  |  |--Home.module.css
|  |  |--Layout.module.css
|  |  |--SinglePost.module.css
```



The **/Component** directory will contain components that your project will reuse. For example the layout component.

The **/pages** directory contains files that define the routes of our application.

The **/styles** directory contains the CSS files for styling our components.

## Creating Nav component.

In the Component directory, create a **Nav.js** file containing the code below:

```js
import Style from "../styles/Nav.module.css";
import Link from "next/link";



const menu_itemList = [
  { name: "Home", href: "/" },
  { name: "About", href: "/about" },
];

function Nav({navActive,setNavActive}) {
 

  return (
    <div className={Style.navBar}>
      
      <Link href={"/"} onClick={()=>{
          setNavActive(false)
           }}><h1>NextBlog</h1></Link>
      <div
        className={Style.mobiNav}
        onClick={() => {
          return setNavActive(!navActive);
        }}
      >
        <div></div>
        <div></div>
        <div></div>
      </div>

      {/*  */}
      <div className={`${Style.navItems} ${navActive ? Style.active : ""}`}>
        <ul className={Style.navItem}>
          {menu_itemList.map((menu) => {
            return (
              <div
                key={menu.name}
                onClick={() => {
                  setNavActive(false); 
                }}
              >
                <Nav_menu  {...menu} />
              </div>
            );
          })}
        </ul>
      </div>
    </div>
    
  );
}
export function Nav_menu({ name, href }) {
  return (
    <Link href={href} >
      <li>{name}</li>
    </Link>
  );
}
export default Nav;
```

The Nav component contains the code for the top navigation bar.

## Creating Layout.js component.

In the Component directory, create a **Layout.js** file containing the code below:

```js
import { useState } from "react";
import styles from "../styles/Layout.module.css";

import Nav from "./Nav";

function Layout({ children }) {
  const [navActive, setNavActive] = useState(false);

  return (
    <>
      <Nav navActive={navActive} setNavActive={setNavActive} />
      <main className={styles.main_style}>{children}</main>
    </>
  );
}

export default Layout;
```

The code block exports a function that accepts `{children}` props as an argument and returns the Nav component and the children props wrapped in a fragment.

## Styling the Components.

Create a Nav.module.css file containing the following code in the /styles directory.

```css
.navBar {
  display: flex;
  justify-content: space-between;
  padding: 20px 30px;
  background-color: rgb(0, 0, 0);
  color: whitesmoke;
  position: relative;
}
.mobiNav {
  display: none;
}
.navItems ul {
  display: flex;
  justify-content: center;
  align-items: center;
  list-style: none;
  column-gap: 30px;
}
.navItem li {
  text-align: center;
  padding: 5px 10px;
  font-size: 1.26rem;
  position: relative;
  color: rgba(235, 235, 235, 0.841);
  position: relative;
  /* z-index: inherit; */
}
.navItem li::before {
  content: "";
  width: 0%;
  height: 6px;
  background-color: rgb(255, 255, 255);
  position: absolute;
  bottom: -10px;
  left: 0;
  transition: all 0.3s;
}

.navItem li:hover::before {
  width: 100%;
}
.navItem li:hover {
  color: white;
}


@media screen and (max-width: 700px) {
  .navBar {
    position:fixed;
    top:0;
    width: 100%;
    z-index: 1000;
  }
  .navBar h1{
    font-size: 1.5rem;
  }
  .navBar li {

    font-size: 1rem
  }
  .mobiNav {
    display: flex;
    flex-direction: column;
    row-gap: 4px;
    padding: 6px;
  }
  .mobiNav div {
    width: 23px;
    height: 1.5px;
    border-radius: 1px;
    background-color: rgb(255, 255, 255);
  }
 .navItems{
  position: absolute;
    right: 0;
    top: 67px;
 }
  .navItems ul {
    position: absolute;
    top: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    justify-content: unset;
    row-gap: 13px;
    list-style: none;
    width: 0;
    height: 100vh;
    overflow: hidden;
        background-color: rgb(0, 0, 0);
    transition: 0.5s;
  }


  .navItems.active ul {
    width: 100vw;

  }
 
  .navItem li::before {
    display: none;
  }

}

```

Create a Layout.module.css file containing the following code in the /styles directory.

```css

@media screen and (max-width:700px){
  .main_style{
    width: 100%;
     position: absolute;
     top: 67px;
    
  }
 
}

```



Override the default **global.css** file with the code below.

```css
:root {
  --max-width: 1100px;
  --border-radius: 12px;
  --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
    "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
    "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
}



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

html,
body {
  max-width: 100vw;
  overflow-x: hidden;
}

a {
  color: inherit;
  text-decoration: none;
}

Layout Nav{
  z-index: 99999999999!important;
}
```



## Making our shared component global.
In **pages/_app.js**, replace the content with the code below:

```js
import '@/styles/globals.css'
import Layout from '../Components/Layout'

export default function App({ Component, pageProps }) {
  return (
  <Layout>
    <Component {...pageProps} />
  </Layout>
  )
}
```

The code block imports the layout component you have created and wraps it around '**<Components {...pageProps}/>**'. The **_app.js** file serves as the entry point for all routes in the **/pages** directory.

## Fetching data from Hygraph.

You can see that the pages folder contains an **index.js** file. Let's override it by adding a new index.js file containing the following code:

```js
import Head from "next/head";
import Style from "../styles/Home.module.css";
import Image from "next/image";
import { GraphQLClient, gql } from "graphql-request";
import Link from "next/link";

const url = `${process.env.ENDPOINT}`;

  // instantiating a graphql client...
const graphConnect = new GraphQLClient(url);

const query = gql`
  query {
    blogposts {
      title
      slug
      coverPhoto {
        url
      }
      excerpt
      id
      author {
        authorName
      }
    }
  }
`;

export async function getServerSideProps() {

  // making request to hygraph for posts
  const { blogposts } = await graphConnect.request(query);

  return { props: { blogposts } };
}

function Homepage({ blogposts }) {
  return (
    <>
      <Head>
        <title>Blog Tutorial</title>
      </Head>
      <main className={Style.postcontainer}>
  {/* using array.map() method to iterate each post returned from hygraph */}
        {blogposts.map((blogposts) => {
          return (
            <div  key={blogposts.id}>
              <div className={Style.inside}>
                <div className={Style.img}>
                  <Image
                    src={blogposts.coverPhoto.url}
                    alt="featured text"
                    fill
                  />
                </div>
                <div className={Style.container}>
                  <Link href={blogposts.slug}>
                    <h2>{blogposts.title}</h2>
                  </Link>
                  <p>{blogposts.excerpt}</p>
                  <p>By {blogposts.author.authorName}</p>
                  <Link href={blogposts.slug}>
                    <button className={Style.readButton}>Read More</button>{" "}
                  </Link>
                </div>
              </div>
            </div>
          );
        })}
      </main>

   
    </>
  );
}

export default Homepage;

```

Go to your Hygraph project, navigate to **Project Settings > API Access**, and copy the API endpoint under the "Content API" section.

Now, scroll down to Public Content API and toggle **`read all...`** under content permissions.

Paste the endpoint in a `.env.local` like so

```
ENDPOINT = 'Paste API enpoint here'
```

In the **index.js** file code block, the const query variable is assigned a graphql query string, which is used to query data from Hygraph.

The `**new GraphQLClient()**` method instantiates a new GraphQLClient and takes our API endpoint as an argument and creates a connection with our project in Hygraph.

You can also notice that the code block contains two functions `getServerSideProps` function and the Home function.

The **`async`** `getServerSideProps` function runs on a per-request basis and allows us to make a request to hygraph for data, it then returns an array of data(blogposts) as props which is passed to the Home function as an argument.

The Home function accepts props from the `getServerSideProps` function as an argument, iterates through the array, and returns the data to the UI.

## Creating SinglePost Page.**

Now, let's create a single page for each of our blog posts.
Create a new file **[slug].js** containing the following code.

```js
import { GraphQLClient, gql } from "graphql-request";
import Head from "next/head";
import Style from "../styles/SinglePost.module.css";
import Image from "next/image";
import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";

const url = `${process.env.ENDPOINT}`;


  //instantiating a graphqlclient...
const graphConnect = new GraphQLClient(url);

const query = gql`
  query MyQuery($slug: String!) {
    blogpost(where: { slug: $slug }) {
      title
      author {
        authorName
      }
      content {
        markdown
      }
      coverPhoto {
        alt
        url
      }
    }
  }
`;

export async function getStaticPaths() {

  // querying for slugs from hygraph...
  const { blogposts } = await graphConnect.request(gql`
    query {
      blogposts {
        slug
      }
    }
  `);

  return {
    paths: blogposts.map(({ slug }) => ({
      params: {
        slug,
      },
    })),
    fallback: "blocking",
  };
}

export async function getStaticProps({ params }) {
  // making request to hygraph for each post matching a slug
  const { blogpost } = await graphConnect.request(query, { slug: params.slug });
  const content = blogpost.content.markdown;

  //serializing my markdown response from the rich text field
  const MdxSource = await serialize(content);

  //passing the post together with the serialized post.
  return { props: { post: blogpost, source: MdxSource } };
}

function SinglePost({ post, source }) {
  return (
    <>
      <Head>
        <title>Blog</title>
      </Head>

      <main className={Style.main}>
        <div className={Style.header}>
          <h1>{post.title}</h1>
          <h3>Author: {post.author.authorName}</h3>
        </div>
        <div className={Style.img}>
          <Image src={post.coverPhoto.url} alt={post.coverPhoto.alt} fill />
        </div>

        <div className={Style.mdxs}>
          <MDXRemote {...source} />
        </div>
      </main>
    </>
  );
}

export default SinglePost;

```



The code block above has three functions `getStaticPath`, `getStaticProps`, and a function component.

The `getStaticPaths` function makes a query to our API and returns paths(slug) as props to getStaticProps.

The `getStaticProps` function takes the props returned by `getStaticPaths` and uses it as a variable to query for a post that matches the slug,  and returns the post and the serialized content to the function component for rendering.

[next-mdx-remote](https://www.npmjs.com/package/next-mdx-remote)is used to serialize the markdown content into a readable format.

## Adding Support for external Images.

To be able to use external images from our Hygraph you'd need to tweak your **next.config** file a bit.

Override the **next.config** file with the code below:

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'media.graphassets.com',
        port: '',
        pathname: '/**',
      },
    ],
  },
}

module.exports = nextConfig
```



## Styling the pages.

Now, Let's style our component. Create three CSS module files with the following codes.

Create a **Home.module.css** to contain this code.

```css
.postcontainer {
  max-width: 800px;
  margin: auto;
  margin-top: 30px;
  margin-bottom: 30px;
  padding: 0 5px;
}
.inside {
  margin-bottom: 30px;
  padding: 10px 20px;
  box-shadow: 0 5px 12px rgb(218, 218, 218), 4px 8px 12px rgb(218, 218, 218);
  display: flex;
  flex-direction: row;
}

.inside div:last-child {
  margin-left: 25px;
}

.inside h2 {
  text-align: justify;
  margin-bottom: 15px;
  font-size: 1.5rem;
  font-weight: 600;
}

.inside p {
  margin-bottom: 15px;
  font-size: 1rem;
}

.readButton {
  padding: 10px 16px;
  background-color: aliceblue;
  font-weight: 500;
  cursor: pointer;
}

.img {
  width: 260px;
  height: 200px;
  position: relative;
}
.container {
  width: 555px;
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
  .postcontainer {
    max-width: 80%;
    margin: auto;
    margin-top: 30px;
    margin-bottom: 30px;
  }
  .inside {
    margin-bottom: 30px;
    padding: 10px 20px;
    box-shadow: 0 5px 12px rgb(218, 218, 218), 4px 8px 12px rgb(218, 218, 218);
    display: flex;
    flex-direction: row;
  }

  .postcontainer h2 {
    text-align: justify;
    margin-bottom: 10px;
    font-size: 1.25rem;
    font-weight: 600;
  }
  .inside p {
    margin-bottom: 10px;
    font-size: 1rem;
  }

  .readButton {
    padding: 10px 16px;
    background-color: aliceblue;
    font-weight: 500;
  }

  .img {
    width: 250px;
    height: 200px;
    position: relative;
  }
  .container {
    width: 65%;
  }
}

/* Mobile */
@media (max-width: 700px) {
  .postcontainer {
    max-width: 90%;
    margin: auto;
    margin-top: 30px;
    margin-bottom: 30px;
  }
  .inside {
    margin-bottom: 30px;
    padding: 10px 20px;
    box-shadow: 0 5px 12px rgb(218, 218, 218), 4px 8px 12px rgb(218, 218, 218);
    display: flex;
    flex-direction: column;
  }
  .inside div:last-child {
    margin-left: 0;
  }

  .postcontainer h2 {
    text-align: justify;
    margin: 10px 0;
    font-size: 1.25rem;
    font-weight: 600;
  }
  .inside p {
    margin-bottom: 10px;
    font-size: 1rem;
  }

  .readButton {
    padding: 10px 16px;
    background-color: aliceblue;
    font-weight: 500;
   
  }

  .img {
    width: 100%;
    height: 200px;
    position: relative;
    display: block;
  }
  .container {
    width: 100%;
  }
}

```



Now, create a **SinglePost.module.css** to contain this code.

```css
.main {
  max-width: 700px;
  margin: auto;
}
.header {
  margin-top: 40px;
}
.header h1,
.header h3 {
  margin-top: 10px;
}
.header h1 {
  font-size: 2rem;
}
.header h3 {
  color: #777777;
}
.img {
  margin-top: 20px;
  width: 100%;
  height: 500px;
  position: relative;
}
.mdxs {
  padding: 60px 0;
  font-size: 1.4rem;
  line-height: 1.5;
}
/* CSS for tablets and small laptops */
@media (min-width: 701px) and (max-width: 1120px) {
  .main {
    max-width: 90%;
    margin: auto;
  }
  .header {
    margin-top: 40px;
  }
  .header h1,
  .header h3 {
    margin-top: 10px;
  }
  .header h1 {
    font-size: 2.1rem;
  }
  .header h3 {
    color: #777777;
    font-size: 1.8rem;
  }
  .img {
    margin-top: 20px;
    width: 100%;
    height: 470px;
    position: relative;
  }
  .mdxs {
    padding: 30px 0;
    font-size: 1.15rem;
    line-height: 1.5;
  }
}

/* CSS for mobiles */
@media (max-width: 700px) {
  .main {
    max-width: 85%;
    margin: auto;
  }
  .header {
    margin-top: 40px;
  }
  .header h1,
  .header h3 {
    margin-top: 10px;
  }
  .header h1 {
    font-size: 1.34rem;
  }
  .header h3 {
    color: #777777;
    font-size: 1.15rem;
  }
  .img {
    margin-top: 20px;
    width: 100%;
    height: 280px;
    position: relative;
  }
  .mdxs {
    padding: 30px 0;
    font-size: 1rem;
    line-height: 1.5;
  }
}

```



That's it! your pages have been styled.



## Pushing your code to GitHub

If you've come this far then I'm assuming there's no error, but let's make some additional checks before deploying it to Vercel.

First, run the command below in your project terminal(you must be in your project directory):
```
npm run lint
```

The code runs [eslint](https://eslint.org/docs/latest/use/getting-started) to check for potential javascript errors.

If no errors are found you'd get a "No Eslint warnings or error" message in your terminal.

Now follow these steps to push your code to GitHub:

1. Log in to your GitHub account and make a new repository(use same name as your local project directory).

2. Back in your terminal connect the local repository to GitHub by running the following command:
```
git init
git remote add origin <url to your GitHub repository>
```

3. Change your branch to match the branch on GitHub by running 
```
git branch -M main
```

4. Run the following command to stage and commit your changes
```
git add --all
git commit -m "<message>"
```

5. Finally, run the following command to push to GitHub
```
git push -u origin main
```

## Deploying To Vercel.

First, sign up at [Vercel](https://vercel.com/signup) if you don't already have an account with them.

**Recommended**: Sign up with your GitHub account.

1. Log in and create a new project.

2. Import your project repository on Vercel.

3. After successfully importing, click on deploy to deploy your project with Vercel's default settings.

4. When the deployment is successful, you'd get deployment URLs to view your app and share with friends.

**Wrapping Up.**

That's it! you've completed this tutorial and deployed your first Single Page Application. Hope you enjoyed the journey. 

Find the Source Code on [GitHub](https://github.com/amissah17/nextblog0.1.0)

**Resources**:
To learn more about graphql, head over to https://graphql.org/.

Learn more about [Hygraph](https://hygraph.com/docs).

You can also find more about Next.js [here](https://nextjs.org/docs/getting-started).