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
  • Appwrite - The Open Source Firebase alternative introduces iOS support
  • Klotho - AWS Cloud-aware infrastructure-from-code toolbox [NEW]
  • InfluxDB - Build time-series-based applications quickly and at scale.
  • Sonar - Write Clean JavaScript Code. Always.
  • 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 (
    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 />
                    
                     handleSubmit()}>Submit
                >
            )
        }
    
        const CreatePost = () => {
            const { isOpen, onOpen, onClose } = useDisclosure()
            return (
                <>
                    Create Post
                    
                        
                        
                            Modal Title
                            
                            
                                
                            
                        
                    
                >
            )
        }
        export default CreatePost
    
    Enter fullscreen mode Exit fullscreen mode

    We used our generateSocialImage Cloudinary function inside the createModal component and update page to generate a social card. Here is an isolated version:

        //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);
    
    Enter fullscreen mode Exit fullscreen mode

    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!

    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:

        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;
          }
        }
    
    Enter fullscreen mode Exit fullscreen mode

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

       # NPM
        npm run dev
    
        # Yarn
        yarn run dev
    
    Enter fullscreen mode Exit fullscreen mode

    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.

    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 for more insight on deploying to Netlify.

    Our live link on Netlify 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:

  • Appwrite

    Appwrite - The Open Source Firebase alternative introduces iOS support . Appwrite is an open source backend server that helps you build native iOS applications much faster with realtime APIs for authentication, databases, files storage, cloud functions and much more!

  • 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