How to build a blog with NextJS and TakeShape

Ashutosh K Singh

JavaScript Developer | Technical Writer

In this tutorial, we will learn how to build a simple blog with Next.js using TakeShape as a Headless content API.

You can check out the final project here and the complete code on GitHub.

Prerequisites

Setup

This tutorial will use Create Next App to set up the initial Next app quickly.

In your project's root directory, run the following commands in the command prompt.

npx create-next-app takeshape-next-blog
# or
yarn create next-app takeshape-next-blog

Start the development server by running the following command.

cd takeshape-next-blog
npm run dev

The last command will start the development server on your system's port 3000. Head over to http://localhost:3000 in your browser. You will see the following:


The next step is to clean the sample code that is generated by create-next-app

  • Delete pages/api folder.
  • Update styles/global.css like this:
html {
    box-sizing: border-box;
    font-size: 16px;
    font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

*,
*:before,
*:after {
    box-sizing: inherit;
}

body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
    margin: 0;
    padding: 0;
    font-weight: normal;
}

a {
    color: inherit;
    text-decoration: none;
}
  • Modify pages/index.js like this.
// pages/index.js
import Head from "next/head";
import styles from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>TakeShape Blog with NextJS</title>
      </Head>
    </div>
  );
}
  • Update  styles/Home.module.css like this.
/* styles/Home.module.css */ 
 .container {
     min-height: 100vh;
     padding: 0 1.5rem;
     display: flex;
     flex-direction: column;
     background-color: white;
 }

Again head over to http://localhost:3000; it will be blank now.

Note: Throughout the tutorial, we will use CSS modules to style our app. You can read more about it here.

Create a free TakeShape account

Sign up for a free developer account on TakeShape.

Configure your project, as shown below. Give a name to your project; this tutorial uses a project named  next-blog-demo .

Now, click on Create Project.

On your dashboard, head over to the Post menu. You will see the sample blog posts present in this project.

Generate API keys

The next step is to generate API keys to authenticate your Next project with TakeShape CMS. Click on the down arrow present next to your project's name on your dashboard.


In the drop-down menu, click on API Keys.

Click on New API Key.

Name this API key, and since you will only use this on the client-side to read the blog posts, you can set Permissions to Read. Click on Create API Key.

Copy the API key to a secure location; remember you will only see them once.

Note: These credentials belong to a deleted project; hence I have not hidden them throughout this tutorial to give you a better understanding of the process and steps. You should never disclose your private API keys to anyone.

On the API Keys page, you will also see your TakeShape project id, i.e., the value between /project/ and /v3/graphql in your API endpoint; copy this project id.

Create .env file

Next.js comes with built-in support for environment variables, so you don't have to install any separate package for it. You can read more about it here.

Run the following commands to create a .env file that will store your project's API keys.

touch .env

In your .env file, add the environment variables.

# .env
TAKESHAPE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TAKESHAPE_PROJECT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Create Header Component

Create a new folder named components in your project's root directory to store different components.

mkdir components
cd components
touch header.js

In your header.js file, add the following code.

// components/header.js
import styles from "../styles/Header.module.css";

export default function Header({ title }) {
  return <h2 className={styles.heading}>{title}</h2>;
}

Create a new file named Header.module.css in the styles folder.

cd styles
touch Header.module.css

Add the following CSS to Header.module.css.

@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700");
.heading {
    margin-bottom: 20px;
    line-height: 1.15;
    font-size: 3.5rem;
    font-family: Montserrat;
    cursor: pointer;
    width: fit-content;
}

.heading:hover,
.heading:focus,
.heading:active {
    color: #1abc9c;
}

@media only screen and (max-width: 500px) {
    .heading {
        font-size: 2.5rem;
    }
}

Import and use this Header component in your index.js file.

// pages/index.js
import Head from "next/head";
import styles from "../styles/Home.module.css";
import Header from "../components/header";

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>TakeShape Blog with NextJS</title>
      </Head>
      <Header title="TakeShape Blog with NextJS" />
    </div>
  );
}

With your development server still running, navigate to http://localhost:3000 in your browser. Here is how your app will look.

Fetching Data from TakeShape

In this section, we will define the functions that will fetch data from TakeShape CMS.

Create a new file named api.js under the lib directory.

mkdir lib
cd lib
touch api.js

Add the following code for a function that will fetch data from TakeShape to api.js . This modified fetch() function will remove the same code's repetition for different GraphQL queries.

//lib/api.js
const API_ENDPOINT = `https://api.takeshape.io/project/${process.env.TAKESHAPE_PROJECT}/v3/graphql`;
const TAKESHAPE_API_KEY = process.env.TAKESHAPE_API_KEY;

const fetchData = async(query, { variables } = {}) =>{
  const res = await fetch(API_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${TAKESHAPE_API_KEY}`,
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  });

  const responseJson = await res.json();
  if (responseJson.errors) {
    console.error("Something went Wrong. Failed to fetch API!!" + responseJson.errors);
  }
  return responseJson.data;
}

fetchData() is a function that receives GraphQL query and variables(if any) as parameters and fetch data from TakeShape based on that query.

For example, consider a GraphQL query to get the name of all authors.

query MyQuery {
  getAuthorList {
    items {
      name
    }
  }
}

You can head over to API Explorer to explore and create different queries based on your needs. The Explorer is simple, straightforward, and fun to use.

Here is how this query will look when executed.

Here is the same query is passed in the fetchData().

export const getAllAuthors = async () => {
  const data = await fetchData(`
  { 
    allAuthors: getAuthorList {
    items {
      name
    }
  }
}`);
  return data;
};
// data
{
  allAuthors: items: [
    {
      name: "C.S. Lewis",
    },
    {
      name: "J. M. Barrie",
    },
    {
      name: "Robert Louis Stevenson",
    },
    {
      name: "Lewis Carroll",
    },
  ];
}

Defining Queries

For this demo blog, we will define three GraphQL queries.

  • getAllPosts: get all posts to display on the landing page
  • getPostBySlug : get a single post based on the slug passed
  • getAllSlugs : get all slugs of the posts to generate static paths

In your lib/api.js add the following code.

// lib/api.js
const API_ENDPOINT = `https://api.takeshape.io/project/${process.env.TAKESHAPE_PROJECT}/v3/graphql`;
const TAKESHAPE_API_KEY = process.env.TAKESHAPE_API_KEY;

const fetchData = async (query, { variables } = {}) => {
  const res = await fetch(API_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${TAKESHAPE_API_KEY}`,
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  });

  const responseJson = await res.json();
  if (responseJson.errors) {
    console.error(
      "Something went Wrong. Failed to fetch API!!" + responseJson.errors
    );
  }
  return responseJson.data;
};

// get all posts to display on landing page
export async function getAllPosts() {
  const data = await fetchData(
    `
      query AllPosts {
        allPosts: getPostList {
            items {
              _id
              title
              deck
              slug
              author {
                name
              }
            }
          }
      }
    `
  );
  return data.allPosts.items;
}

// get all slugs of the posts to generate static paths
export async function getAllSlugs() {
  const data = await fetchData(`
      {
        allPosts: getPostList {
          items {
            slug
          }
        }
      }
    `);
  return data.allPosts.items;
}

// get single post based on the slug passed
export async function getPostBySlug(slug) {
  const data = await fetchData(
    `
      query PostBySlug($slug: String) {
        post: getPostList(where: {slug: {eq: $slug}}) {
          items {
            _id
            title
            slug
            deck
            bodyHtml
            author{
              name
            }
          }
        }
      }`,
    {
      variables: {
        slug,
      },
    }
  );
  return data.post.items[0];
}

In getPostBySlug() function, the slug is passed as a parameter, and using it the corresponding post is returned. You can easily create queries like this using the Explorer tab in the API Explorer menu on your TakeShape dashboard.

Create PostContainer component

We will now create a container component to pass the post data as props and display them on our home page.

This image shows the container component.

Create a new file named post-container.js under the components directory. In your project's root directory, run the following command.

cd components
touch post-container.js

Add the following code to the post-containter.js file.

// components/post-container.js
import Link from "next/link";
import styles from "../styles/PostContainer.module.css";

export default function PostContainer({ title, deck, slug, author }) {
  return (
    <div className={styles.container}>
      <div className={styles.title}>
        <Link href={`/blog/${slug}`}>{title}</Link>{" "}
      </div>
      <div className={styles.author}> {author.name}</div>
      <div className={styles.deck}>
        <p>{deck}</p>
      </div>
      <div className={styles.read}>
        <Link href={`/blog/${slug}`}>Read More</Link>
      </div>
    </div>
  );
}

You are more than welcome to add more data to this container component like featureImage, tags , etc.

To style this component, create a new file named PostContainer.module.css under styles directory. Run the following commands in your project's root directory.

cd styles
touch PostContainer.module.css

Add the following code to PostContainer.module.css file.

/* styles/PostContainer.module.css */
@import url("https://fonts.googleapis.com/css2?family=Syne+Mono");
.container {
    margin: 1rem;
    flex-basis: 55%;
    padding: 1.25rem;
    text-align: left;
    text-decoration: none;
    border: 1px solid #bdc3c7;
    border-radius: 10px;
    background-color: white;
    transition: color 0.15s ease, border-color 0.15s ease;
}

.container:hover,
.container:focus,
.container:active {
    border-color: #1abc9c;
    box-shadow: 2px 2px 1px #1abc9c;
}

.container p {
    margin: 0;
    font-size: 1.25rem;
    line-height: 1.5;
}

.title {
    color: teal;
    text-decoration: none;
    margin: 0;
    font-family: Verdana, Geneva, Tahoma, sans-serif;
    font-size: 2rem;
}

.title a:hover,
.title a:focus,
.title a:active {
    color: #1abc9c;
    text-decoration: underline;
}

.author {
    color: inherit;
    font-family: 'Syne Mono', monospace;
    font-style: italic;
    font-size: 1rem;
    padding: 5px 10px;
    display: inline-block;
    margin: 0 2px;
}

.deck {
    margin: 5px 0;
}

.deck p {
    font-size: 1.15rem;
    line-height: 1.5rem;
    margin: 2px 0;
}

.read {
    background-color: teal;
    font-size: 1rem;
    color: whitesmoke;
    padding: 10px 15px;
    text-align: center;
    display: inline-block;
    margin-top: 5px;
    cursor: pointer;
    border-radius: 50px;
}

.read:hover,
.read:focus,
.read:active {
    color: white;
    background-color: #1abc9c;
}

@media only screen and (max-width: 500px) {
    .title {
        font-size: 1.5rem;
    }
    .deck p {
        font-size: 1rem;
    }
    .author {
        font-size: 0.8rem;
    }
    .container {
        margin: 0.5rem 0.35rem;
        padding: 1rem;
    }
}

Displaying Posts on the Homepage

Before we use the PostContainer component in the index.js file, we need to fetch the data to pass into the component.

We will create and export an asynchronous function getStaticProps() to fetch data at build time. If you are not familiar with getStaticProps() , you can read more about how it's used here.

One of the use cases of getStaticProps() is to fetch data from a remote API, which is exactly what we are doing.

Update your pages/index.js file like this.

// pages/index.js
import Head from "next/head";
import styles from "../styles/Home.module.css";
import Header from "../components/header";
import PostContainer from "../components/post-container";
import { getAllPosts } from "../lib/api";

export default function Home({ posts }) {
  return (
    <div className={styles.container}>
      <Head>
        <title>TakeShape Blog with NextJS</title>
      </Head>
      <Header title="TakeShape Blog with NextJS" />
      {posts.map((post) => (
        <PostContainer
          key={post._id}
          title={post.title}
          slug={post.slug}
          author={post.author}
          deck={post.deck}
        />
      ))}
    </div>
  );
}

export async function getStaticProps() {
  const posts = await getAllPosts();
  return {
    props: {
      posts,
    },
  };
}

Head over to http://localhost:3000. Here is how your app will look.

Your Home Page is now complete. You will notice the links do not work yet, but we'll fix that by creating routes for the posts.

Creating Dynamic routes for Posts

Creating dynamic routes is easy in Next.js, you can add brackets to a page name ([param]) to create a dynamic routes. You can read more about it here.

Create a new file named [slug].js  under blog directory in the pages folder. In your project's root directory, run the following commands.

cd pages
mkdir blog
cd blog
touch [slug].js

Add the following code to [slug].js

// pages/blog/[slug].js
import Head from "next/head";
import Link from "next/link";
import styles from "..//../styles/Posts.module.css";
import { getAllSlugs, getPostBySlug } from "../../lib/api";
import Header from "../../components/header";

function Posts({ post }) {
  return (
    <div>
      <Head>
        <title key={post.title}>{post.title}</title>
      </Head>
      <div className={styles.container}>
        <div className={styles.header}>
          <Header title={post.title} />
          <h2 className={styles.home}>
            <Link href={`/`}>🏠 Home</Link>
          </h2>
        </div>
        <div className={styles.info}>By: {post.author.name}</div>
        <div className={styles.body}>
          {" "}
          <main dangerouslySetInnerHTML={{ __html: post.bodyHtml }} />
        </div>
      </div>
    </div>
  );
}

export async function getStaticPaths() {
  const allPosts = await getAllSlugs();
  const paths = allPosts.map((post) => ({
    params: { slug: post.slug },
  }));
  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const post = await getPostBySlug(params.slug);

  return {
    props: {
      post,
    },
  };
}

export default Posts;

You will notice in the above code, we have used an asynchronous function getStaticPaths() in which we fetch slugs of all the posts, getStaticPaths() will use these slugs to generate paths for all the posts in your app. You can read more about getStaticPaths() here.

The same slugs are passed to getStaticProps() and then to the getPostBySlug() function to retrieve a single post corresponding to that slug.

To style [slug].js , create a new file named Posts.module.js under stlyes directory.

cd styles
touch Posts.module.css

Add the following CSS to Posts.module.css file.

/* Posts.module.css */
@import url("https://fonts.googleapis.com/css2?family=Syne+Mono");
.container {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background-color: #fafafa;
}

.home {
    background-color: #1abc9c;
    font-size: 1.5rem;
    color: white;
    text-align: center;
    display: inline;
    padding: 5px 7.5px;
    margin: 5px;
    cursor: pointer;
    border-radius: 20px;
}

.home:hover,
.home:active,
.home:focus {
    background-color: teal;
}

.header {
    padding: 0.5rem 1.5rem;
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    background-color: white;
}

.info {
    font-size: 1.5rem;
    padding: 1rem 2rem;
    margin: 5px;
    font-weight: bold;
    font-family: 'Syne Mono', monospace;
}

.body {
    margin: 10px 70px;
    font-size: 1.25rem;
}

.body img,
.body iframe {
    max-width: 100%;
    height: auto;
}

.body figcaption {
    font-size: 1rem;
    font-weight: 100;
}

@media only screen and (max-width: 500px) {
    .header {
        display: inline;
    }
    .home {
        font-size: 1.2rem;
    }
    .body {
        margin: 10px 30px;
        font-size: 1rem;
    }
    .body figcaption {
        font-size: 0.8rem;
    }
}

And we're done! Your blog with TakeShape and Next.js is now complete.

Head over to http://localhost:3000  and click on any of the posts. Here is how your blog page will look.

You can view the finished website here and the code for the project here.

Conclusion

In this article, we discussed how to create a blog site using Next.js and TakeShape CMS. You can follow this tutorial and create your own version of this project. There are many features and functionality that you can add to this project.

Here are a few ideas to get you started:

  • Add Author routes and link them to the blog posts.
  • Add Sort, Filter, and Search functionality.
  • Style the app using UI libraries like Chakra UI, Material UI, etc.

Here are some additional resources that can be helpful.

Happy coding!