Build a Jamstack Blog with Xata and Cloudinary
Tags: jamstack, cloudinary, nextjs, xata
Published: 2022-11-23 10:55:03

Blogging as a technical expertise or professional is one way to validate our skills, share knowledge with others, and also grow ourself in any technical field. Most times setting up a blog might be so burdensome, especially when considering the tech stacks to use so as to keep it simple.

## What we will be building

In this article, we will be learning how to set up a simple [Jamstack](https://www.freecodecamp.org/news/what-is-the-jamstack-and-how-do-i-host-my-website-on-it/) blog using Xata, Cloudinary, react-markdown, and Netlify. We will be utilizing the power of [Jamstack](https://www.freecodecamp.org/news/what-is-the-jamstack-and-how-do-i-host-my-website-on-it/) and some serverless technologies to achieve our goal.

Here is the source code for the working [application](https://github.com/DesmondSanctity/xata-cloudinary-blog).

## Prerequisites and what to do
- Set up our Xata account.
- Set up our Cloudinary account.
- Set up our application using Next.js and Chakra UI.
- Basic Knowledge of JavaScript, React, or Next.js.

## Setting up Xata and creating our first database table

[Xata](https://xata.io?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is a Serverless Data Platform. We can think of it as the combination of a serverless relational database, a search engine, and an analytics engine, all behind a single consistent API. It has first-class support for branches, a workflow for zero-downtime schema migrations, as well as support for edge caching.

To set up Xata, we first need an account. We can either use the SDK for our development or the REST API generated for our table to interact with Xata. For the purpose of this article, we will be using the Xata SDK for our development.

- Go to [Xata](https://xata.io?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) and signup to get access.
- We login to our account and create a workspace. We can create as many workspaces as we want depending on our subscription plan.
- Create a database inside the workspace as shown below:
![Create Xata database](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869035267_xata-2.PNG)
- Proceed to create a table and add our table items or data. We can specify data types for each item in our table depending on what we will store in them.

Our table items for the blog post are:

```
    title - string
    body - string
    image - string (we will be storing the image url here from cloudinary)
    tags - multiselect (this accepts array of data)
```

![Our Xata database table and data types available](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667868952771_xata-4.PNG)

## Creating Cloudinary account for our image transformation

[Cloudinary](https://cloudinary.com?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is a tool that helps with the transformation of images and videos to load faster with no visual degradation, automatically generates an image and video variants, and delivers high-quality responsive experiences to increase conversions. Whatever media optimizations we need for better performance in our applications can be achieved with this tool.

To set up our Cloudinary, we also have to create an account and get our cloud space on Cloudinary. We can access Cloudinary in many ways too using APIs or SDKs. Cloudinary offers its SDKs in different languages so feel free to explore it outside Jamstack as well. For the purpose of this article, we will be using the API from our Cloudinary cloud to generate social cards for our blog posts. No need to open Figma or other design tools every time, Cloudinary got you!


- Go to [Cloudinary](https://cloudinary.com?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) and sign up to get access to a cloud.
- We will login and confirm we can see our dashboard with information like cloudname, API Keys etc. We will be needing them later.
![Our Cloudinary dashboard with our information](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869089652_cloudinary-dash.PNG)

## Create Next JS app and connect Xata SDK

To set up a Next.js App, we run either of the following commands depending on our preference

```javascript
    npx create-next-app@latest
     or
    yarn create next-app
     or
    pnpm create next-app
```
After the setup, we will run this command to start the application. 

```javascript
    npm run dev
```
We should get a screen like this:

![Our Next.js project running](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869113684_next-running.PNG)

Now we have our application up and running, let's get to the real deal. We will be initializing the Xata SDK and writing a function that uses our **cloudname** and **image public id** on Cloudinary to generate a social card for our blog posts.

## Setting up APIs to interact with our Xata DB
- Firstly we open our terminal and run these commands in our root directory and project directory respectively

```javascript
    - Install the Xata CLI
    
    npm install @xata.io/cli -g
    
     or
    
    npm install @xata.io/client@latest
    
    - Initialize your project locally with the Xata CLI
    
    xata init --db https://<your workspace name>-cktogf.us-east-1.xata.sh/db/<database name>
```
- These commands can easily be copied from our database UI in Xata. See an example below:
![Xata UI showing our commands for installing](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869291108_xata-7.PNG)

- The last command will ask for some configurations according to our preference before it goes ahead to install the SDK. Also, it will trigger a window so we can connect our API key automatically to our project. See the example here:
![Initializing Xata form terminal](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869355042_Screenshot+41.png)
![Auto connecting API keys to our project](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667900225356_xata-5.PNG)

Now we are all set to start using Xata in our project!

## Social Image Generation for our Blog with Cloudinary

Now, let's set up our serverless function for image transformation using Cloudinary.

- First, we need our **cloudname**, a card design that contains either our brand or whatever we want to use and identify with as our social card, and a space where we will overlay text automatically using Cloudinary function. In other words, once anybody sees this social card, they know the owner of the articles.
- We can get design inspiration from this [article](https://www.learnwithjason.dev/blog/design-social-sharing-card/) or this [Figma](https://res.cloudinary.com/jlengstorf/raw/upload/v1578342420/social-sharing-cards/learnwithjason-social-card-template.fig) [design](https://res.cloudinary.com/jlengstorf/raw/upload/v1578342420/social-sharing-cards/learnwithjason-social-card-template.fig). Below is my default social card and the upload to Cloudinary. The image can be our logo, brand image, or anything we want to be our brand.
![Design card](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667899676990_example-black.png)
![Upload to Cloudinary](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667900052735_cloudinary-upload.PNG)
- Go to our project directory, create a directory component and create a file `GenerateImage.js`
- Paste the following code inside the file

```javascript
    function cleanText(text) {
        return encodeURIComponent(text).replace(/%(23|2C|2F|3F|5C)/g, '%25$1');
    }
    
    /**
     * Generates a social sharing image with custom text using Cloudinary’s APIs.
     *
     * @see https://cloudinary.com/documentation/image_transformations#adding_text_captions
     *
     */
    export default function generateSocialImage({
        title,
        tagline,
        cloudName,
        imagePublicID,
        cloudinaryUrlBase = 'https://res.cloudinary.com',
        titleFont = 'righteous',
        titleExtraConfig = '',
        taglineExtraConfig = '',
        taglineFont = 'caveat',
        imageWidth = 1280,
        imageHeight = 669,
        textAreaWidth = 760,
        textLeftOffset = 480,
        titleGravity = 'south_west',
        taglineGravity = 'north_west',
        titleLeftOffset = null,
        taglineLeftOffset = null,
        titleBottomOffset = 254,
        taglineTopOffset = 445,
        textColor = 'FFFFFF',
        titleColor,
        taglineColor,
        titleFontSize = 64,
        taglineFontSize = 48,
        version = null,
    }) {
        // configure social media image dimensions, quality, and format
        const imageConfig = [
            `w_${imageWidth}`,
            `h_${imageHeight}`,
            'c_fill',
            'q_auto',
            'f_auto',
        ].join(',');
    
        // configure the title text
        const titleConfig = [
            `w_${textAreaWidth}`,
            'c_fit',
            `co_rgb:${titleColor || textColor}`,
            `g_${titleGravity}`,
            `x_${titleLeftOffset || textLeftOffset}`,
            `y_${titleBottomOffset}`,
            `l_text:${titleFont}_${titleFontSize}${titleExtraConfig}:${cleanText(
                title,
            )}`,
        ].join(',');
    
        // configure the tagline text
        const taglineConfig = tagline
            ? [
                `w_${textAreaWidth}`,
                'c_fit',
                `co_rgb:${taglineColor || textColor}`,
                `g_${taglineGravity}`,
                `x_${taglineLeftOffset || textLeftOffset}`,
                `y_${taglineTopOffset}`,
                `l_text:${taglineFont}_${taglineFontSize}${taglineExtraConfig}:${cleanText(
                    tagline,
                )}`,
            ].join(',')
            : undefined;
    
        // combine all the pieces required to generate a Cloudinary URL
        const urlParts = [
            cloudinaryUrlBase,
            cloudName,
            'image',
            'upload',
            imageConfig,
            titleConfig,
            taglineConfig,
            version,
            imagePublicID,
        ];
    
        // remove any falsy sections of the URL (e.g. an undefined version)
        const validParts = urlParts.filter(Boolean);
    
        // join all the parts into a valid URL to the generated image
        return validParts.join('/');
    }
```

So from this code sample, we will be needing our cloudname and our custom image public id (we will upload it from our account on Cloudinary. It's quite straightforward to generate our social cover image. Other optional config items are fonts for our blog title and taglines. There are defaults for them in the function but we can override them wherever we call the function by passing it as a parameter. We will see this in our `CreateModal` component later, how we will automatically overlay text on our design image and transform it each time we create a blog post.

We are all set for our Cloudinary image transformation set-up!

## Building our CRUD APIs and Interfaces using Next.js and Chakra UI

We are getting closer to our final product. Next, we will be writing CRUD (Create, Read, Update, Delete) functions we will use to interact with our Xata DB and then present the data on the user interface.

For our user interface, we will be using [Next.js](https://nextjs.org) and [Chakra UI,](https://chakra-ui.com) our APIs will be stored in the Next.js `api` directory, and our dynamic pages in the `pages` directory.

![App directory tree](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869480192_project-dir.PNG)

### Our APIs for Interacting with Xata are as follows:
```javascript
    import { getXataClient } from '../../src/xata.js'
    
    const xata = getXataClient()
    
    // allpost.js. API to get/read all our blog posts
    export default async function getAllXata(req, res) {
        const records = await xata.db.posts.getAll();
        return res.json({
            ok: true,
            posts: records
        })
    }
    
    // create.js. API to get create our blog posts
    export default async function createToXata(req, res) {
        let posts = req.body
        await xata.db.posts.create(posts)
        res.json({
            ok: true,
        })
    }
    
    // update.js. API to update a blog post
    export default async function updateToXata(req, res) {
        let id = req.body.id
        let post = {
            title: req.body.post.title,
            body: req.body.post.body,
            image: req.body.post.image,
            tags: req.body.post.tags
        }
        console.log(id, post)
        await xata.db.posts.update(id, post);
        res.json({
            ok: true,
        })
    }
    
    //post.js API to read one post
    export default async function getOneXata(req, res) {
        let id = req.query
        console.log(id)
        const record = await xata.db.posts.read(id);
        return res.json({
            ok: true,
            post: record
        })
       
    }
    
    //delete.js API to delete a blog post
    const deleteItem = async (id) => {
      return await xata.db.posts.delete(id)
    }
    export default async function deleteFromXata(req,res) {
      const { id } = req.body
      await deleteItem(id)
      res.json({
        ok: true,
      })
    }
```

We can also get these codes from our Xata workspace according to the image below:

![Xata UI showing our API functions](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869598871_xata-7.PNG)

To start designing our user interface, we will install chakra-ui, chakra-ui/icons, react-icons, react-markdown and react-tostify. We will use Chakra UI and React Icons libraries to build the user interfaces of our application, React Markdown to allow markdown in our blog `textarea` field and React Toastify to show updates to user when they make any API call.

```javascript
    - Using NPM
    npm i @chakra-ui/react @chakra-ui/icons @emotion/react @emotion/styled framer-motion react-icons react-markdown react-toastify
    
    - Using Yarn
    yarn add @chakra-ui/react @chakra-ui/icons @emotion/react @emotion/styled framer-motion react-icons react-markdown react-toastify
```

Then we will wrap our `App` with providers from chakra-ui and also pass react-toastify to our `App` body as shown below:

```javascript
    import { ChakraProvider } from '@chakra-ui/react'
    import {ToastContainer} from 'react-toastify'
    import "react-toastify/dist/ReactToastify.min.css";
    
    
    function MyApp({ Component, pageProps }) {
      return (
        <ChakraProvider>
          <Component {...pageProps} />
          <ToastContainer />
        </ChakraProvider>
      )
    }
    
    export default MyApp
```

We will proceed to create our pages inside the `pages` directory. We will be updating the `index.js` page and creating two dynamic routed pages: one in the `pages/[id].js` directory and another in the `page/update/[id].js` directory which is a sub directory to `pages` as shown in the previous application tree.

So we can access `https://appurl/id` and `https://appurl/update/id` when we want to view a single blog post and update a blog post respectively. Read more about dynamic routing in Next.js [here](https://nextjs.org/learn/basics/dynamic-routes)
For our `pages/index.js` file, we will use this code below:

```javascript
    import { useState, useEffect } from 'react'
    import CreatePost from '../components/CreateModal'
    import { FaGithub } from 'react-icons/fa'
    import { Icon, ButtonGroup, Text, Spacer, Link } from '@chakra-ui/react'
    import AllPosts from '../components/AllPost'
    
    export default function Home() {
      const [posts, setPosts] = useState([])
      useEffect(() => {
        const getData = async () => {
          await fetch('/api/allposts', {
            method: 'GET',
          }).then((response) => response.json())
            .then((data) => setPosts(data.posts));
        }
        getData();
      }, [])
    
      return (
        <>
          <main className='main'>
              <div className='grid'>
                <Text as='b' fontSize='20px' color='black' >
                  Blog with Xata and Cloudinary
                </Text>
                <Spacer />
                <ButtonGroup gap={3} ml={5}>
                  <CreatePost />
                  <Link href='https://github.com/DesmondSanctity/xata-cloudinary-blog' isExternal><Icon as={FaGithub} w={10} h={10} /></Link>
                </ButtonGroup>
              </div>
              <AllPosts posts={posts} />
          </main>
          <footer className='footer'>
            <a href="https://dexcode.xyz" target="_blank" rel="noopener noreferrer">
              Created by&nbsp;<b>Anon</b>&nbsp;⚡️
            </a>
          </footer>
        </>
      )
    }
```

For our `pages/[id].js` file, we will use this code below:

```javascript
    import { useRouter } from 'next/router'
    import { Box, Image, Text, Icon, Link, Container, Spinner, Alert, AlertDescription, AlertIcon, AlertTitle } from '@chakra-ui/react'
    import { FaGithub } from 'react-icons/fa'
    import ReactMarkdown from 'react-markdown'
    import remarkGfm from 'remark-gfm'
    import rehypeHighlight from 'rehype-highlight'
    import rehypeRaw from 'rehype-raw'
    import rehypeSanitize from 'rehype-sanitize'
    import useSWR from 'swr'
    const fetcher = (...args) => fetch(...args).then((res) => res.json())
    
    
    const PostDetails = () => {
        const router = useRouter();
        const { id } = router.query;
        console.log({ router });
        //Getting data from xata using swr
        const { data, error } = useSWR(`/api/post?id=${id}`, fetcher)
        if (error) return (
            <div><Alert status='error'>
                <AlertIcon />
                <AlertTitle>Error!</AlertTitle>
                <AlertDescription>Failed to Load.</AlertDescription>
            </Alert></div>
        )
        if (!data) return <div><Spinner color='red.500' /></div>
        const post = data.post;
    
        return (
            <main className='main'>
                <div className='grid'>
                    <Text as='b' fontSize='20px' color='black' mt={5}>
                        Blog with Xata and Cloudinary
                    </Text>
                    <Link href='https://github.com/DesmondSanctity/xata-cloudinary-blog' isExternal><Icon as={FaGithub} w={10} h={10} mt={7} ml={5} /></Link>
                </div>
                <Container maxW='container.sm'>
                    <Image src={post.image} alt='blog-image' />
                    <Box p='6'>
                        <Box
                            mt='1'
                            fontWeight='light'
                            lineHeight='tight'
                            ml={5}
                        >
                            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight, rehypeRaw, rehypeSanitize]}>{post.body}</ReactMarkdown>
                        </Box>
                    </Box>
                </Container>
            </main>
        )
    }
    
    
    export default PostDetails
```

We used [SWR](https://swr.vercel.app/) (stale-while-revalidate) to fetch our data because it is an easier way to handle fetching data at the request time in Next.js. The team behind Next.js has created a React hook for data fetching called **SWR**. It is highly recommended if you’re fetching data on the client side. It handles caching, revalidation, focus tracking, refetching at intervals, and more. Read more about it [here](https://swr.vercel.app/).

For our `pages/update/[id].js` file, we will use this code below:

```javascript
    import { useState, useEffect } from 'react'
    import { useRouter } from 'next/router'
    import {
        Button,
        Textarea,
        Input,
        FormControl,
        FormLabel,
        Container,
        Text,
        Spacer,
        Icon,
        Link,
        Spinner,
        Alert,
        AlertDescription,
        AlertIcon,
        AlertTitle
    } from '@chakra-ui/react'
    import { FaGithub } from 'react-icons/fa'
    import { toast } from 'react-toastify'
    import generateSocialImage from '../../components/GenerateImg'
    import useSWR from 'swr'
    const fetcher = (...args) => fetch(...args).then((res) => res.json())
    const UpdatePost = () => {
        const [title, setTitle] = useState('');
        const [body, setBody] = useState('');
        const [tags, setTags] = useState('');
        const router = useRouter();
        const { id } = router.query;
        //Get data from xata db
        const { data, error } = useSWR(`/api/post?id=${id}`, fetcher)
        if (error) return (
            <div><Alert status='error'>
                <AlertIcon />
                <AlertTitle>Error!</AlertTitle>
                <AlertDescription>Failed to Load.</AlertDescription>
            </Alert></div>
        )
        if (!data) return <div><Spinner color='red.500' /></div>
        // store data in state
        const res = data.post;
    
        // handle form submit
        const handleSubmit = async () => {
            //Convert string tags to array
            const newTags = tags || res.tags.toString();
            console.log(newTags)
            // Reducing number of accepted tags to 4 if user inputs more
            const tagArr = newTags.split(/[, ]+/);
            let tags_new;
            if (tagArr.length >= 4) {
                tags_new = tagArr.slice(0, 4)
            } else tags_new = tagArr;
            console.log(tags_new);
            //Generate social card with cloudinary
            const socialImage = generateSocialImage({
                title: title || res.title,
                tagline: tags_new.map(tag => `#${tag}`).join(' '),
                cloudName: 'dqwrnan7f',
                imagePublicID: 'dex/example-black_iifqhm',
            });
            console.log(socialImage);
            //Make add create request
            let post = {
                title: title || res.title,
                body: body || res.body,
                image: socialImage,
                tags: tags_new,
            }
            const response = await fetch('/api/update', {
                method: 'POST',
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({ post, id })
            })
            if (response.ok) {
                toast.success("post updated successfully", {
                    theme: "dark",
                    autoClose: 8000
                })
                window?.location.replace('/');
            }
        }
        return (
            <main className='main'>
                <div className='grid'>
                    <Text as='b' fontSize='20px' color='black' mt={5} >
                        Blog with Xata and Cloudinary
                    </Text>
                    <Spacer />
                    <Link href='https://github.com/DesmondSanctity/xata-cloudinary-blog' isExternal><Icon as={FaGithub} w={10} h={10} mt={7} ml={5} /></Link>
                </div>
                <Container maxW='4xl' centerContent>
                    <FormControl >
                        <FormLabel>Post Title</FormLabel>
                        <Input placeholder='Title' defaultValue={res.title} onChange={e => { setTitle(e.target.value) }} />
                    </FormControl>
                    <FormControl mt={4}>
                        <FormLabel>Post Tags</FormLabel>
                        <Input placeholder='add tags separated by commas' defaultValue={res.tags} onChange={e => { setTags(e.target.value) }} />
                    </FormControl>
                    <FormControl mt={4}>
                        <FormLabel>Post Body</FormLabel>
                        <Textarea placeholder='you can use markdown here' size='sm' defaultValue={res.body} onChange={e => { setBody(e.target.value) }} />
                    </FormControl>
                    <Button colorScheme='black' variant='outline' type='submit' mt={5} onClick={() => handleSubmit()}>Submit</Button>
                </Container>
            </main>
        )
    }
    
    
    export default UpdatePost
```

For our other reusable components `AllPosts.js` and `CreateModal.js`, we have:

`AllPost.js`  

```javascript
    import NextLink from 'next/link'
    import { Box, Image, Badge, Flex, Spacer, ButtonGroup, Link } from '@chakra-ui/react';
    import { DeleteIcon, EditIcon, ExternalLinkIcon } from '@chakra-ui/icons'
    import { toast } from 'react-toastify'
    import ReactMarkdown from 'react-markdown'
    import remarkGfm from 'remark-gfm'
    const AllPosts = ({ posts }) => {
      const deleteData = async (id) => {
        const { status } = await fetch('/api/delete', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ id }),
        })
        if (status === 200) {
          toast.success("post deleted successfully", {
            theme: "dark",
            autoClose: 5000
          })
        }
        window?.location.reload()
      }
      return (
        <div className='grid'>
          {
            posts && posts.map((post, index) => {
              return (
                <div className='card' key={index}>
                  <Box maxW='sm' borderWidth='1px' borderRadius='lg' overflow='hidden' mt={5} mb={2}>
                    <Image src={post.image} alt='blog-image' />
                    <Box p='6'>
                      <Flex>
                        <Badge borderRadius='full' px='2' colorScheme='teal'>
                          Tags:
                        </Badge>
                        {post.tags.length > 0 && post.tags.map((tag, index) => {
                          return (
                            <Box key={index}
                              color='gray.500'
                              fontWeight='semibold'
                              letterSpacing='wide'
                              fontSize='xs'
                              textTransform='lowercase'
                              ml='2'
                            >
                              {(index ? ',' : '') + ' ' + tag}
                            </Box>
                          )
                        })}
                        <Spacer />
                        <ButtonGroup gap={2}>
                          <NextLink href={`/update/${post.id}`} legacyBehavior passHref>
                            <Link><EditIcon /></Link>
                          </NextLink>
                          <DeleteIcon onClick={() => deleteData(post.id)} mt={1} />
                        </ButtonGroup>
                      </Flex>
                      <Box
                        mt='1'
                        fontWeight='semibold'
                        lineHeight='tight'
                        noOfLines={3}
                      >
                        <NextLink href={`/${post.id}`} legacyBehavior passHref>
                          <Link>{post.title}<ExternalLinkIcon mx='2px' /></Link>
                        </NextLink>
                      </Box>
                      <Box
                        mt='1'
                        fontWeight='light'
                        lineHeight='tight'
                        noOfLines={5}
                      >
                        <ReactMarkdown remarkPlugins={[remarkGfm]}>{post.body}</ReactMarkdown>
                      </Box>
                    </Box>
                  </Box>
                </div>
              )
            })
          }
        </div>
      )
    }
    
    export default AllPosts
```

`CreateModal.js`

```javascript
    import { useState } from 'react'
    import {
        useDisclosure,
        Modal,
        ModalOverlay,
        ModalContent,
        ModalHeader,
        ModalBody,
        ModalCloseButton,
        Button,
        Textarea,
        Input,
        FormControl,
        FormLabel,
    } from '@chakra-ui/react'
    import { toast } from 'react-toastify'
    import generateSocialImage from './GenerateImg'
    
    const PostForm = () => {
        const [title, setTitle] = useState("");
        const [body, setBody] = useState("");
        const [tags, setTags] = useState("");
        //Convert string tags to array
        const newTags = tags;
    
        const handleSubmit = async () => {
            if (title == '' || body == '' || tags == '') {
                toast.warn("post cannot be empty", {
                    theme: "dark",
                    autoClose: 8000
                })
            } else {
                const tagArr = newTags.split(/[, ]+/);
                let tags_new;
                if (tagArr.length >= 4) {
                    tags_new = tagArr.slice(0, 4)
                } else tags_new = tagArr;
                console.log(tags_new);
                //Generate social card
                const socialImage = generateSocialImage({
                    title: title,
                    tagline: tags_new.map(tag => `#${tag}`).join(' '),
                    cloudName: 'dqwrnan7f',
                    imagePublicID: 'dex/example-black_iifqhm',
                });
                console.log(socialImage);
                //Make add create request
                let posts = {
                    title: title,
                    body: body,
                    image: socialImage,
                    tags: tags_new,
                }
                const response = await fetch('/api/create', {
                    method: 'POST',
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify(posts)
                })
                if (response.ok) {
                    toast.success("post created successfully", {
                        theme: "dark",
                        autoClose: 8000
                    })
                    window?.location.reload()
                }
            }
        }
        return (
            <>
                <FormControl >
                    <FormLabel>Post Title</FormLabel>
                    <Input placeholder='Title' onChange={e => { setTitle(e.target.value) }} required />
                </FormControl>
                <FormControl mt={4}>
                    <FormLabel>Post Tags</FormLabel>
                    <Input placeholder='add tags separated by commas' onChange={e => { setTags(e.target.value) }} required />
                </FormControl>
                <FormControl mt={4}>
                    <FormLabel>Post Body</FormLabel>
                    <Textarea placeholder='you can use markdown here' size={'lg'} onChange={e => { setBody(e.target.value) }} required />
                </FormControl>
                <Button colorScheme='black' variant='outline' mt={5} onClick={() => handleSubmit()}>Submit</Button>
            </>
        )
    }
    
    const CreatePost = () => {
        const { isOpen, onOpen, onClose } = useDisclosure()
        return (
            <>
                <Button colorScheme='black' variant='outline' onClick={onOpen} mt={2} size={'sm'}>Create Post</Button>
                <Modal isOpen={isOpen} onClose={onClose} size={'5xl'}>
                    <ModalOverlay />
                    <ModalContent>
                        <ModalHeader>Modal Title</ModalHeader>
                        <ModalCloseButton />
                        <ModalBody>
                            <PostForm />
                        </ModalBody>
                    </ModalContent>
                </Modal>
            </>
        )
    }
    export default CreatePost
```
We used our `generateSocialImage` Cloudinary function inside the `createModal` component and `update` page to generate a social card. Here is an isolated version:

```javascript
    //Generate social card
    const socialImage = generateSocialImage({
      title: title,
      tagline: tags_new.map(tag => `#${tag}`).join(' '),
      cloudName: 'dqwrnan7f',
      imagePublicID: 'dex/example-black_iifqhm',
    });
    console.log(socialImage);
```
We can see how we passed dynamic data to the function, our public image id, cloudname, title from our blog, and taglines from our blogpost too. Our social card will look like this when the function is executed:

![Our transformed social card - Amazing!](https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869644588_cloudinary-img.PNG)

If you followed up till this point, our application is almost ready! Finally, we will go ahead and add some CSS styles to the default stylesheet file in our project `globals.css`:

```javascript
    html,
    body {
      padding: 0;
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
        Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
    }
    a {
      color: inherit;
      text-decoration: none;
    }
    * {
      box-sizing: border-box;
    }
    @media (prefers-color-scheme: dark) {
      html {
        color-scheme: dark;
      }
      body {
        color: white;
        background: black;
      }
    }
    .main {
      padding: 5rem 0;
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    .footer {
      width: 100%;
      height: 100px;
      border-top: 1px solid #eaeaea;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .footer img {
      margin-left: 0.5rem;
    }
    .footer a {
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .grid {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: center;
      flex-wrap: wrap;
      max-width: 80%;
      margin-top: 3rem;
      margin: auto;
    }
    .card:hover,
    .card:focus,
    .card:active {
      color: #0070f3;
      border-color: #0070f3;
    }
    @media (max-width: 600px) {
      .grid {
        max-width: 100%;
        flex-direction: column;
      }
    }
```

We will then run our application using any of these commands to see the finished product.

```javascript
   # NPM
    npm run dev
    
    # Yarn
    yarn run dev
```
Now we have our blog running on Xata and Cloudinary serverless provisions. We can go ahead and improve the user interface, make it more responsive and even add some animations too. We can also host it on services like Netlify, Vercel, and any other client-side hosting platforms we can think of. For this article, we will be deploying to Netlify.

{% embed https://drive.google.com/file/d/1TYZjBAMwQGwPGKxYGVTDCH6ngsLSTC53/view?usp=drivesdk %}

One easy way to deploy to Netlify is to push our code to Github, connect our Github to Netlify and select the repository that contains our project. We will select the Next.js preset build command and everything will run and deploy automatically with fewer or no configurations. Check this [article](https://www.freecodecamp.org/news/publish-your-website-netlify-github/) for more insight on deploying to Netlify.

Our live link on [Netlify](https://xata-cloudinary-blog.netlify.app/) is ready.

## Conclusion

So we were able to learn from this article how we can use Jamstack through Next.js, Cloudinary, Xata, and Chakra UI to build a blog application with CRUD functions without setting up any server. Feel free to comment on what you learned, what we did not cover, possible improvements, and also any questions you might have. I will be glad to take your feedback and answer your questions.

## Resources

Here are some resources that might be helpful:

- [Image transformation using Cloudinary](https://cloudinary.com/documentation/react_image_transformations#adding_text_and_image_overlays).
- [Auto generated social images](https://www.learnwithjason.dev/blog/auto-generate-social-image/).
- [Learn about SWR](https://swr.vercel.app/).
- [How to design a social media card](https://www.learnwithjason.dev/blog/design-social-sharing-card/).