NextJS Blog with Strapi | Deploy to Heroku and Vercel

Strapi is a free and open-source headless CMS. It gives us an API and lets us choose whatever language we use.

In this tutorial we are going to use NextJS because of its SEO, image optimization, functions to fetch data for pre-rendering which comes out of the box with it. If you didn’t get these terms don’t worry, I’ll explain about it more below.

We’ll create a simple blog where you can post different articles with different categories and also let the user post the articles from the website. And at the end we’ll also deploy the strapi app in heroku and the main NextJS website in vercel for free.

This is the final blog that we’ll develop in this tutorial.
Link: https://my-nextjs-blog-phi.vercel.app/
Final code: https://github.com/DibasDauliya/nextJS-blog-with-stapi-api

So let’s start with STRAPI:

Run this command in your terminal.

$ npx create-strapi-app strapi --quickstart

It is recommended to use npx instead of npm because npm installs the packages and npx only executes the packages. You can read more about it in this free code camp’s blog.

Also we added --quickstart at the end of command because we want to use SQLite database; you can also use MongoDB, SQL or Postgres as your main database. And strapi in the above command is the folder name where we want to keep our project.

After running above command it will give this URL http://localhost:1337/admin

Open it and register your name, email and password which you can use to access the Strapi admin panel.

Let's make a user so we can connect it with our posts as an author.

Click on “Users” of collection types and click “+ Add new users”. Fill in the details and save it.

After that go to ‘Content-Types Builder’ then ‘+ Create new collection type’ as shown in picture.

Then enter the display name as “post” (strapi will convert that into Posts). After that click the add field option. Select three text fields, a rich text field, and UID then name them as title, category, description, content, and slug respectively. Rich text will allow us to write markdown and we’ll extract it using react-markdown npm package from NextJS.

UID is useful for slug and URL. Make sure to choose ‘title’ from the dropdown of attached field in the UID. It will automatically remove the space of title and concatenate them with dash.

After that, select the relation field and choose “User (from-permission)” from the dropdown. Then among six options choose fourth one i.e. User has many Posts and name it as author.

Then click finish and save it (green button at R.H.S). It might say an error occurred or something like that, just make sure you clicked that save button and if such error occurs do refresh the page and you must see “Posts” in collection types below the strapi logo.

Yay, now let’s add some posts. Go to Posts and click “+Add New Posts”.

Fill the fields and choose the author from the dropdown at the right side as shown in figure.

Now click save and publish it.

We can access those posts from the “/posts” route but at this point it will show 403 forbidden error because we should give permission.

So, let’s go to settings and then “Roles” under “USERS & PERMISSIONS PLUGIN”. Click on “Public” then go to the application section under permissions. After that, choose “find”, “find one” and “create” and save it.

Utilizing data using NextJS

Run this command in your terminal.

$ npx create-next-app my-blog

After running this command go to the my-blog folder, open the project in your preferred code editor, run npm run dev command and go to this link (http://localhost:3000/) to view the website.

I edited the default code into something like this (just changed some text and added basic stylings). 😀

Now let’s use the getStaticProps function to fetch data for pre-rendering. It will build all our posts during build time in the server and serve those posts to users fastly. Learn more at Basic Features: Data Fetching | Next.js (nextjs.org).

The following code may clarify you more. You should refresh your site or re-run the server after implementing getStraticProps.

export default function Home(props) {
  console.log(props.data)
  console.log('everyone can get me')

  return (
{/* ... */}
    )
}

export async function getStaticProps() {
  console.log("I'm executed by server. Browser can't get me. 😋")

  return {
    props: { data: 'Data returned by getStaticProps' }
  }
}

This function only runs in the files inside pages folder.

And in this case we are utilizing Incremental Static Regeneration (ISR) by adding revialte key with value in seconds at return of the geStaticProps function because it helps to rebuild our required page at a certain interval of given time without having to rebuild the whole site.

Even if we don't use revialte property in geyStaticProps while development and after doing refresh it will show the updated content but it is not the same during production. If you don't use ISR we should rebuild our site every time we make changes to see the updates.

So, let's make “.env.local” file in the root path and add strapi url.

NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337

Now, we will be using a “category text field” to filter the posts and fill data in different sections.

export default function Home(props) {
  const { htmlCatRes, jsCatRes } = props

  return (
    <div className={styles.container}>
<Head>
        <title>My Blog</title>
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Welcome to My Blog!</h1>

       {/* ... */}

        {/* category one */}
        <div className={styles.catCard}>
          <h2>HTML and CSS Category</h2>

          {htmlCatRes.map(({ title, description, id, slug }) => (
            <a href={`/${slug}`} className={styles.card} key={id}>
              <h2>{title}</h2>
              <p>{description}</p>
            </a>
          ))}
        </div>

        {/* category two */}
        <div className={styles.catCard>...  
</main>

      <footer className={styles.footer}>My Blog</footer>
    </div>
  )
}

export async function getStaticProps() {
  const htmlCat = await fetch(
    `${process.env.NEXT_PUBLIC_STRAPI_API_URL}/posts?category=html-css`
  )
  const htmlCatRes = await htmlCat.json()

  const jsCat = await fetch(
    `${process.env.NEXT_PUBLIC_STRAPI_API_URL}/posts?category=javascript`
  )
  const jsCatRes = await jsCat.json()

  return {
    props: { htmlCatRes, jsCatRes },
    revalidate: 10 //will regenerate page when request comes at every 10      seconds
  }
}

We can use the meta titles, descriptions, images, open graph tags for the specific page using the Head component of nextJS to improve our SEO.

Now let’s make a dynamic page [post].js under the same pages folder where we can get more info about our posts. The word post in [post].js will be replaced by our post’s slug.

This time, to fetch data, we should also use the getStaticPaths function because it is a dynamic route and nextJS doesn't know which word will replace that [post] in [post].js we made. We should tell nextJS that these are the words that will replace [post] beforehand. So it can make all the possible pages for use during build time.

To parse the markdown data into html let’s install the react-markdown npm package.

$ npm i react-markdown

Now, we will get the slug passed in the href of the link and use it to filter the posts and get the data of that specific slug.

export default function Post({ finalData }) {
  const { title, author, category, content, created_at } = finalData
  return (
    <>
      <Head>
        <title>{title}</title>
      </Head>

      <style>
        {` ...
          `}
      </style>

      <div className='container'>
        <article className='flow article'>
          <header className='header'>
            <h1>{title}</h1>
            <div className='author'>
              <span>By {author?.username}</span>
              <span>{new Date(created_at).toLocaleDateString()}</span>
              <span>{category}</span>
            </div>
          </header>
          <main className='flow-sm'>
            <ReactMarkdown>{content}</ReactMarkdown>
          </main>
        </article>
      </div>
    </>
  )
}

export async function getStaticProps({ params }) {
  const { post } = params

  const data = await fetch(
    `${process.env.NEXT_PUBLIC_STRAPI_API_URL}/posts?slug=${post}`
  )
  const resData = await data.json()

  const finalData = resData[0]

  return {
    props: { finalData },
    revalidate: 10 //will regenerate page when request comes at every 10 seconds

  }
}

export async function getStaticPaths() {
  const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_API_URL}/posts`)
  const posts = await res.json()

  const paths = posts?.map(({ slug }) => ({
    params: { post: slug }
  }))

  return {
    paths,
    fallback: 'blocking'
  }
}

To create a post from website

Make a new page called create.js in the pages folder and Add /create in the value of href attribute in the link of the home page.

Then create a simple form with controlled input fields. If the value is driven by the react state then we can call the input fields as controlled components.

After that, I fetched the strapi API and used it to post our content.

export default function Create() {
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    content: '',
    category: 'html-css',
    slug: ''
  })
  const [isLoading, setLoading] = useState(false)

  async function handleSubmit(e) {
    e.preventDefault()

    setLoading(true)

    const add = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_API_URL}/posts`, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(formData)
    })

    const addRes = await add.json()

    console.log(addRes)
    setFormData({
      title: '',
      description: '',
      content: '',
      category: 'html-css',
      slug: ''
    })

    setLoading(false)
  }

  function handleChange(e) {
    const name = e.target.name
    setFormData((prev) => {
      return { ...prev, [name]: e.target.value }
    })
  }
  return (
    <>
      <Head>
        <title>Create post</title>
      </Head>

      <style jsx>
        {` ...
        `}
      </style>

      <main className='container'>
        <Link href='/'>
          <a>
            <h1>Home</h1>
          </a>
        </Link>
        <h1>Create Post</h1>
        <form>
          <div className='form-group'>
            <label htmlFor='title'>Title</label>
            <input
              type='text'
              name='title'
              id='title'
              required
              placeholder='Enter title'
              value={formData.title}
              onChange={(e) => handleChange(e)}
            />
          </div>
          <div className='form-group'>
            <label htmlFor='category'>Choose category</label>
            <select
              name='category'
              id='category'
              onChange={(e) => handleChange(e)}
            >
              <option value='html-css'>HTML and CSS</option>
              <option value='javascript'>Javascript </option>
            </select>
          </div>
          <div className='form-group'>
            <label htmlFor='slug'>Slug</label>
            <input
              type='text'
              name='slug'
              id='slug'
              onChange={(e) => handleChange(e)}
              value={formData.slug}
              placeholder='Enter a slug'
            />
          </div>
          <div className='form-group'>
            <label htmlFor='description'>Description</label>
            <input
              type='text'
              name='description'
              id='description'
              required
              placeholder='Enter description'
              value={formData.description}
              onChange={(e) => handleChange(e)}
            />
          </div>
          <div className='form-group'>
            <label htmlFor='content'>Content (Markdown is supported)</label>
            <textarea
              rows={5}
              type='text'
              name='content'
              id='content'
              required
              placeholder='Enter content'
              value={formData.content}
              onChange={(e) => handleChange(e)}
            ></textarea>
          </div>
          <div className='form-group'>
            <button onClick={(e) => handleSubmit(e)}>
              {isLoading ? 'Loading...' : 'Submit'}
            </button>
          </div>
        </form>
      </main>
    </>
  )
}

So, this is it. 😀

Let’s Deploy

Deploying Strapi app in Heroku

If you haven’t made a heroku account then you can make it from this link. It is free for hobby projects and doesn't ask for credit card details.

After that go to https://dashboard.heroku.com/apps and click the “new” button in the R.H.S. and then click “Create new app”. Fill the name of the app and it will take you to the deploy section.

After that go the "Overview" tab then click at "Configure addons" and search "Heroku Postgres" and add it in your project.

Now open strapi project in the code editor and install pg and pg-connection-string npm package.

npm i pg pg-connection-string

Then create env folder inside config then create production folder inside env. Inside production folder create two files named database.js and server.js.

In database.js

const { parse } = require("pg-connection-string");

module.exports = ({ env }) => {
  const { host, port, database, user, password } = parse(env("DATABASE_URL"));

  return {
    defaultConnection: "default",
    connections: {
      default: {
        connector: "bookshelf",
        settings: {
          client: "postgres",
          host,
          port,
          database,
          username: user,
          password,
          ssl: { rejectUnauthorized: false },
        },
        options: {
          ssl: false,
        },
      },
    },
  };
};

In server.js

module.exports = ({ env }) => ({
  url: env("HEROKU_URL"),
});

Heroku gives different options to deploy your apps. In this tutorial, let's use the Heroku CLI method. You’ll need to download Heroku CLI for this.
You may need to restart your system after installation and can check it by running this command in terminal.

$ heroku --version

After installing it, open your terminal and locate into the strapi project that we built and login to the heroku.

$ heroku login

After that exit from it by pressing CTRL + C and create a git repository.

$ git init
$ heroku git:remote -a your-app-name-in-heroku

Now commit and push the code into the Heroku main branch.

$ git add .
$ git commit -m "first commit"
$ git push heroku main

It might take a few minutes and after its completion it will give the deployed URL.

Now go the the settings tab of heroku dashboard and click “Show Config Vars”. Add “HEROKU_URL” as key and put your URL in the value. After that add “NODE_ENV” as key and “production” as value.

Then open your URL and go to ‘/admin’ route to access the dashboard.

After that add some data and replace the localhost URL in your .env.local file of nextJS with the deployed URL.

Also make sure to check the “find”, “finOne”, “create” of the post for the public under roles section of user & permission plugin.

Note: For security reasons, Strapi has disbled "Content-Types Builder" plugin in production. We should set it up locally and push it to the Heroku if we want to add or update new content-types.

Deploying nextJS app in Vercel.

NextJS apps can be deployed using any hosting that supports nodeJS but in this tutorial we are going to deploy in Vercel because it is also free for hobby projects and it is personalized for nextJS as nextJS is made by Vercel. If you haven’t registered you can do so via this link.

After that, let's upload our project on github. Make a new project in github and open the terminal and locate the nextJS app then run these commands.

$ git add .
$ git commit -m “first commit”
$ git remote add origin your-url
$ git push origin main

After uploading in gitHub let’s go back to Vercel via this link.

Then click “New Project” button in the R.H.S, then choose your project from github by connecting vercel with github

Make sure to add environment variables with the name “NEXT_PUBLIC_STRAPI_API_URL” and the value is the URL given by heroku.

Then click on deploy and it will give you the live URL with celebration. 🎉

Thank you. 😊

29