DEV Community

Cover image for How to Build a Nextjs Blog with Hygraph and Deploy to Vercel in 2023.
Frank Otabil Amissah
Frank Otabil Amissah

Posted on • Updated on

How to Build a Nextjs Blog with Hygraph and Deploy to Vercel in 2023.

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

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

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

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

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

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

Before starting your development server run the command below to install a few dependencies:

npm install next-mdx-remote graphql-request graphql
Enter fullscreen mode Exit fullscreen mode

Now, run the following command to start the development server on localhost:3000.

npm run dev
Enter fullscreen mode Exit fullscreen mode

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

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:

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

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:

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

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.

.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;
  }

}

Enter fullscreen mode Exit fullscreen mode

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


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

  }

}

Enter fullscreen mode Exit fullscreen mode

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

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

Making our shared component global.

In pages/_app.js, replace the content with the code below:

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

export default function App({ Component, pageProps }) {
  return (
  <Layout>
    <Component {...pageProps} />
  </Layout>
  )
}
Enter fullscreen mode Exit fullscreen mode

The code block imports the layout component you have created and wraps it around ''. 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:

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;

Enter fullscreen mode Exit fullscreen mode

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

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.

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;

Enter fullscreen mode Exit fullscreen mode

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

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

module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

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.

.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%;
  }
}

Enter fullscreen mode Exit fullscreen mode

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

.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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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

The code runs eslint 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>
Enter fullscreen mode Exit fullscreen mode
  1. Change your branch to match the branch on GitHub by running
git branch -M main
Enter fullscreen mode Exit fullscreen mode
  1. Run the following command to stage and commit your changes
git add --all
git commit -m "<message>"
Enter fullscreen mode Exit fullscreen mode
  1. Finally, run the following command to push to GitHub
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Deploying To Vercel.

First, sign up at Vercel 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

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

Learn more about Hygraph.

You can also find more about Next.js here.

Top comments (2)

Collapse
 
russell_crichton_a982a774 profile image
Russell Crichton

I followed your tutorial but am receiving an error:
error - TypeError: Only absolute URLs are supported
do you have a solution?

Collapse
 
amissah17 profile image
Frank Otabil Amissah

Can you please post a screenshot and where the error occurred.