Creating a blog with Next, Chakra, Vercel, Ghost CMS, and Simply Analytics

In this article I explained how I made my personal website using some cool technologies I wanted to check out including NextJs, Vercel, Ghost CMS, and SimplyAnalytics

Creating this blog! NextJs + Chakra UI + Vercel + Ghost CMS + Simply Analytics


Ghost CMS Set Up (Digital Ocean)

I was using Strapi for most projects, but they got rid of the one click installation in Digital Ocean. From sheer laziness and curiosity of checking out Ghost CMS I decided to give it a shot and see how I like it. (Its been great so far)

Install Droplet with Digital Ocean 1-Click Installation

Very straight forward, I went with the cheapest option available, $5 a month plan, with the enabled backups for $1 a month (always good to have).

Set Up DNS A Records from your domain

Set up an A record in my DNS provider, in this instance I am using Vercel as they are managing the front end, I used grow.barsketis.com

Note this can take a while to propagate depending on your host (especially if for some reason your using a sub par service like GoDaddy). You can check if the change worked by typing into your terminal

host grow.barsketis.com

This will check the DNS records and propagated correctly will return your server IP address. Vercel was pretty quick, I know route 54 with AWS is as well.

Ssh into the project and go through the configuration (this took about 5 minutes, be patient while it runs).

ssh root@123.123.123.123

I had to type in my configured domain, and email address for LetsEncrypt SSL, and it worked! Note I am using a subdomain instead of my root domain, if you just want to use Ghost CMS vanilla installation use your root domain.

Go to https://grow.barsketis.com/ghost/ to finalize installation


Now at this point, you can call it quits and just use Ghost CMS out of th box. Ill have to admit, I almost scraped my old website completely and stuck with it, what a great service. Check out some of the templates and plethora of features.

I want to make a custom template with NextJs and just use Ghost as a headless CMS because I want to gave a bit more control over the website so ill be disabling the front end facing website using the password protection feature. Sorry Ghost!

Install Chakra UI + NextJS

I wont go into this as surely it will change by the time you read this, just follow the official instructions here.

Getting Started with Nextjs
A guide for installing Chakra UI with Nextjs projects

Integrating NextJs with GhostCMS

To start you need to create an API endpoint on your Ghost CMS, this is done clicking the "Add Custom Integration" in the settings, fairly straight forward.

Add the keys locally by creating a .env.local file, and reload your dev environment. (I did change my api keys guys, and ignore the :, should be an = sign in the second key :D)

Dont forget to add the environment variables to your production build, in this case I added them to my vercel project

Integrate the Javscript Client

Here are the official docs:

Content API JavaScript Client
JavaScript Client Library for fetching data from the Ghost Content API. Read more on Ghost Docs 👉
npm install @tryghost/content-api

Creating an article card

I took "inspiration" from Choc UI, they had a nice looking article card that is open source. It had some funky non default components however so I changed them out for the default Chakra components, and I also added in props for the eventual data being passed down.

Chakra UI Prebuilt Components | Choc UI
Choc UI is a set of accessible and reusable components that are commonly used in web applications.
import { Flex, Box, Text, Image, Link } from "@chakra-ui/react"

const ArticleCard = ({ published_at, title, excerpt, primary_tag, slug }) => {
  console.log(primary_tag)
  return (
    <Flex
      bg="#edf3f8"
      _dark={{
        bg: "#3e3e3e",
      }}
      p={50}
      w="full"
      alignItems="center"
      justifyContent="center"
    >
      <Box
        mx="auto"
        px={8}
        py={4}
        rounded="lg"
        shadow="lg"
        bg="white"
        _dark={{
          bg: "gray.800",
        }}
        maxW="2xl"
      >
        <Flex justifyContent="space-between" alignItems="center">
          <Text
            fontSize="sm"
            color="gray.600"
            _dark={{
              color: "gray.400",
            }}
          >
            {published_at}
          </Text>
          <Link
            px={3}
            py={1}
            bg="gray.600"
            color="gray.100"
            fontSize="sm"
            fontWeight="700"
            rounded="md"
            _hover={{
              bg: "gray.500",
            }}
          >
            {primary_tag}
          </Link>
        </Flex>

        <Box mt={2}>
          <Link
            fontSize="2xl"
            color="gray.700"
            href={`/articles/${slug}`}
            _dark={{
              color: "white",
            }}
            fontWeight="700"
            _hover={{
              color: "gray.600",
              _dark: {
                color: "gray.200",
              },
              textDecor: "underline",
            }}
          >
            {title}
          </Link>
          <Text
            mt={2}
            color="gray.600"
            _dark={{
              color: "gray.300",
            }}
          >
            {excerpt}
          </Text>
        </Box>

        <Flex justifyContent="space-between" alignItems="center" mt={4}>
          <Link
            color="brand.600"
            _dark={{
              color: "brand.400",
            }}
            _hover={{
              textDecor: "underline",
            }}
            href={`/articles/${slug}`}
          >
            Read more
          </Link>

          <Flex alignItems="center">
            <Image
              mx={4}
              w={10}
              h={10}
              rounded="full"
              fit="cover"
              display={{
                base: "none",
                sm: "block",
              }}
              src="/Justin-Barsketis-Headshot.jpg"
              alt="avatar"
            />
            <Link
              color="gray.700"
              _dark={{
                color: "gray.200",
              }}
              fontWeight="700"
              cursor="pointer"
            >
              Justin Barsketis
            </Link>
          </Flex>
        </Flex>
      </Box>
    </Flex>
  )
}
export default ArticleCard

And Finally, pulling down the data into the cards, I left some extra fields in case you had multiple authors or want to add a featured image.

export default function Articles() {
  const api = new GhostContentAPI({
    url: "https://grow.barsketis.com",
    key: process.env.CONTENT_API_KEY,
    version: "v5.0",
  })

  const [posts, setPosts] = useState([])
  useEffect(() => {
    api.posts
      .browse({
        limit: "all",
        include: "tags,authors",
      })
      .then((posts) => {
        console.log("posts", posts)
        setPosts(posts)
      })
      .catch((err) => {
        console.error(err)
      })
  }, [])

  return (
    <div>
      <Head>
        <title>Welcome to Articles</title>
        <meta name="description" content="Justin Barsketis Blog" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Header />

      <main style={{ flexGrow: "1" }}>
        {posts.map((post) => (
          <ArticleCard
            key={post.id}
            title={post.title}
            slug={post.slug}
            excerpt={post.excerpt}
            // feature_image={post.feature_image}
            // primary_author={post.primary_author}
            primary_tag={post.primary_tag.name}
            published_at={post.published_at}
          />
        ))}
      </main>

      <footer>{/* <Footer /> */}</footer>
    </div>
  )
}

Success!

Making an Article Page

Now we just need to make an article page using dynamic routes, easy peasy!

Subscribe to Justin's Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe