Build a Jamstack Blog with Xata and Cloudinary

This page summarizes the projects mentioned and recommended in the original post on dev.to

Our great sponsors
  • SurveyJS - Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App
  • InfluxDB - Power Real-Time Data Analytics at Scale
  • WorkOS - The modern identity platform for B2B SaaS
  • xata-cloudinary-blog

    A Next.js blog built with Xata, Cloudinary, React Markdown and Chakra UI

    Here is the source code for the working application.

  • SWR

    React Hooks for Data Fetching

    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 ( Error! Failed to Load. ) if (!data) return // 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 ( Blog with Xata and Cloudinary Post Title { setTitle(e.target.value) }} /> Post Tags { setTags(e.target.value) }} /> Post Body { setBody(e.target.value) }} /> handleSubmit()}>Submit ) } export default UpdatePost

    Enter fullscreen mode Exit fullscreen mode

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

    AllPost.js

        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 (
            
    { posts && posts.map((post, index) => { return (
    blog-image Tags: {post.tags.length > 0 && post.tags.map((tag, index) => { return ( {(index ? ',' : '') + ' ' + tag} ) })} deleteData(post.id)} mt={1} /> {post.title} {post.body}
    ) }) }
    ) } export default AllPosts
    Enter fullscreen mode Exit fullscreen mode

    CreateModal.js

        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 (
                <>
                    
                        Post Title
                         { setTitle(e.target.value) }} required />
                    
                    
                        Post Tags
                         { setTags(e.target.value) }} required />
                    
                    
                        Post Body
                         { 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
    </code></pre>
    <div class="highlight__panel js-actions-panel">
    <div class="highlight__panel-action js-fullscreen-code-action">
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title>
        <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path>
    </svg>
    
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title>
        <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path>
    </svg>
    
    </div>
    </div>
    </div>
    
    
    <p>We used our <code>generateSocialImage</code> Cloudinary function inside the <code>createModal</code> component and <code>update</code> page to generate a social card. Here is an isolated version:<br>
    </p>
    <div class="highlight js-code-highlight">
    <pre class="highlight plaintext"><code>    //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);
    </code></pre>
    <div class="highlight__panel js-actions-panel">
    <div class="highlight__panel-action js-fullscreen-code-action">
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title>
        <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path>
    </svg>
    
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title>
        <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path>
    </svg>
    
    </div>
    </div>
    </div>
    
    
    <p>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:</p>
    
    <p><a href="https://res.cloudinary.com/practicaldev/image/fetch/s---fr0it-I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869644588_cloudinary-img.PNG" class="article-body-image-wrapper"><img src="https://res.cloudinary.com/practicaldev/image/fetch/s---fr0it-I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paper-attachments.dropboxusercontent.com/s_955A41B77F74D83EE406CDA571D0497758A739361BC9BA9EFF9A23EECFDDE6B0_1667869644588_cloudinary-img.PNG" alt="Our transformed social card - Amazing!" loading="lazy" width="880" height="463"></a></p>
    
    <p>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 <code>globals.css</code>:<br>
    </p>
    <div class="highlight js-code-highlight">
    <pre class="highlight plaintext"><code>    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;
          }
        }
    </code></pre>
    <div class="highlight__panel js-actions-panel">
    <div class="highlight__panel-action js-fullscreen-code-action">
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title>
        <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path>
    </svg>
    
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title>
        <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path>
    </svg>
    
    </div>
    </div>
    </div>
    
    
    <p>We will then run our application using any of these commands to see the finished product.<br>
    </p>
    <div class="highlight js-code-highlight">
    <pre class="highlight plaintext"><code>   # NPM
        npm run dev
    
        # Yarn
        yarn run dev
    </code></pre>
    <div class="highlight__panel js-actions-panel">
    <div class="highlight__panel-action js-fullscreen-code-action">
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-on"><title>Enter fullscreen mode</title>
        <path d="M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"></path>
    </svg>
    
        <svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewbox="0 0 24 24" class="highlight-action crayons-icon highlight-action--fullscreen-off"><title>Exit fullscreen mode</title>
        <path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"></path>
    </svg>
    
    </div>
    </div>
    </div>
    
    
    <p>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.</p>
    
    
    <div class="crayons-card c-embed text-styles text-styles--secondary">
          <div class="c-embed__cover">
            <a href="https://drive.google.com/file/d/1TYZjBAMwQGwPGKxYGVTDCH6ngsLSTC53/view?usp=drivesdk" class="c-link s:max-w-50 align-middle" target="_blank" rel="noopener noreferrer">
              <img alt="" src="https://res.cloudinary.com/practicaldev/image/fetch/s--U2hedrbQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://lh5.googleusercontent.com/o38DhPpIExWjOBDuHDpZv4OavX-fn5WlY6bpAVcc_pZsEabb4jiafqe3tBWMoyZagVY%3Dw1200-h630-p" height="462" loading="lazy" class="m-0" width="880">
            </a>
          </div>
        <div class="c-embed__body">
          <h2 class="fs-xl lh-tight">
            <a href="https://drive.google.com/file/d/1TYZjBAMwQGwPGKxYGVTDCH6ngsLSTC53/view?usp=drivesdk" target="_blank" rel="noopener noreferrer" class="c-link">
              screen-capture.mp4 - Google Drive
            </a>
          </h2>
          <div class="color-secondary fs-s flex items-center">
              <img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://res.cloudinary.com/practicaldev/image/fetch/s--w5LNmP0W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://ssl.gstatic.com/images/branding/product/1x/drive_2020q4_32dp.png" loading="lazy" width="32" height="32">
            drive.google.com
          </div>
        </div>
    </div>
    
    
    
    <p>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 <a href="https://www.freecodecamp.org/news/publish-your-website-netlify-github/">article</a> for more insight on deploying to Netlify.</p>
    
    <p>Our live link on <a href="https://xata-cloudinary-blog.netlify.app/">Netlify</a> is ready.</p>
    
    <h2>
      <a name="conclusion" href="#conclusion">
      </a>
      Conclusion
    </h2>
    
    <p>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.</p>
    
    <h2>
      <a name="resources" href="#resources">
      </a>
      Resources
    </h2>
    
    <p>Here are some resources that might be helpful:</p>
    
    <ul>
    <li>
    <a href="https://cloudinary.com/documentation/react_image_transformations#adding_text_and_image_overlays">Image transformation using Cloudinary</a>.</li>
    <li>
    <a href="https://www.learnwithjason.dev/blog/auto-generate-social-image/">Auto generated social images</a>.</li>
    <li>
    <a href="https://swr.vercel.app/">Learn about SWR</a>.</li>
    <li>
    <a href="https://www.learnwithjason.dev/blog/design-social-sharing-card/">How to design a social media card</a>.</li>
    </ul>

  • SurveyJS

    Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App. With SurveyJS form UI libraries, you can build and style forms in a fully-integrated drag & drop form builder, render them in your JS app, and store form submission data in any backend, inc. PHP, ASP.NET Core, and Node.js.

  • Next.js

    The React Framework

    For our user interface, we will be using Next.js and Chakra UI, our APIs will be stored in the Next.js api directory, and our dynamic pages in the pages directory.

NOTE: The number of mentions on this list indicates mentions on common posts plus user suggested alternatives. Hence, a higher number means a more popular project.

Suggest a related project

Related posts