Menampilkan Kode dari Contentful Rich Text di Gatsbyjs

Pernah nggak sih mau menampilkan kode di gatsbyjs dari Contentful rich text, mari saya mulai dari awal ceritanya. Jadi Di dalam blog ini : https://zul...

Ditulis Oleh zidan Pada

Pernah nggak sih mau menampilkan kode di gatsbyjs dari Contentful rich text, mari saya mulai dari awal ceritanya. Jadi Di dalam blog ini : https://zulzidan.com/blog/apa-itu-wsl-postingan-bagi-kamu-yang-malas-dual-boot-unix-dan-windows saya ingin memberikan snipet kodingan, akan tetapi saya ingin itu tetap cantik dan saya ingin hal itu bisa di kopi.

sepertinya kalau sesama orang yang suka koding, kita semua mau melakukan implementasi blog dengan menampilkan snippet kodingan.

Part 1 : Persiapan sebelum implementasi

1. Fungsi Copy

pertama fungsi kopi, jadi di setelah mencari-cari bagaimana cara mengkopi teks dengan button, ada package yang namanya react-copy-to-clipboard : https://www.npmjs.com/package/react-copy-to-clipboard. ini persiapan untuk mengkopi seberapa panjang pun kode yang kita tampilkan nantinya akan bisa di kopi dengan cepat hanya dengan satu tombol.

berikut cara menggunakannya dari documentasinya:

import {CopyToClipboard} from 'react-copy-to-clipboard';

<CopyToClipboard text={this.state.value} onCopy={() => this.setState({copied: true})}>
    <button>Copy to clipboard with span</button>
</CopyToClipboard>

2. blog template gatsbyjs

akan tetapi timbul masalah lagi, teknologi yang saya gunakan untuk blog ini adalah gatsbyjs lalu saya mengambil data atau menulis blog dari contentful.

nah di /template/blog sebelum ada nya kodingan menambahkan kodingan :

import React from 'react'
import { graphql } from 'gatsby'
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
import { renderRichText } from 'gatsby-source-contentful/rich-text'
import { BLOCKS, MARKS } from "@contentful/rich-text-types"

const Bold = ({ children }) => <span className="font-bold">{children}</span>
const Text = ({ children }) => <p className="">{children}</p>

const options = {
  renderMark: {
    [MARKS.BOLD]: text => <Bold>{text}</Bold>,
  },
  renderNode: {
    [BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
    [BLOCKS.EMBEDDED_ASSET]: node => {
      return (
        <>
          <h2 className="text-2xl font-bold">Embedded Asset</h2>
          <pre className="bg-gray-200 p-4">{JSON.stringify(node, null, 2)}</pre>
        </>
      )
    },
  },
}

const BlogPagesComponents = ({ data, location }) => {
  const { title, content, featuredMedia } = data.contentfulBlog
  const image = getImage(featuredMedia.gatsbyImageData)

  return (
    <div>
      <div className="px-4 py-8 sm:px-8 md:px-16 lg:px-20  prose max-w-none">
        <div className='mb-24'>
          {image && <GatsbyImage image={image} alt={featuredMedia.title} />}
        </div>
        <h1 className="text-3xl font-bold mb-4">{title}</h1>
        {content && renderRichText(content, options)}
      </div>
    </div>
  )
}

export default BlogPagesComponents

export const query = graphql`
  query($slug: String!) {
    contentfulBlog(slug: { eq: $slug }) {
      title
      slug
      content {
        raw
      }
      featuredMedia {
        title
        gatsbyImageData(
          layout: CONSTRAINED
          placeholder: BLURRED
          formats: [AUTO, WEBP]
        )
        description
      }
    }
  }
`

jadi seperti yang bisa kita lihat saya menggunakan dua package ini untuk merender rich text dari contenful.

import { renderRichText } from 'gatsby-source-contentful/rich-text'
import { BLOCKS, MARKS } from "@contentful/rich-text-types"

3. Fungsi Syntax Highlighter

Sebelum kita melangkah terlalu jauh sepertinya saya harus menjelaskan bagaimana menggunakan primsjs dulu, berikut kodenya :

import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";

<SyntaxHighlighter language={languange} style={vscDarkPlus}>
  {content}
</SyntaxHighlighter>

4. Set up Contenful Block Code

buat persiapan seperti dibawah dulu di contentful,  fields description (mirip fungsinya dengan judul | short text), language (bahasa yang kita gunakan | short text) dan code (type markdown | long text).

set up block code in contenful

5. (skip this tho) kesalahan saya adalah berfikir bisa pakai marks.code

Cukup skip part ini karena hanya cerita saja, nggak ada gunanya hehe, jadi ketika saya membuat atau menulis blog ini, saya berfikir saya bisa menggunakan marks.code, yah karena hal itu lebih mudah untuk di implementasikan, akan tetapi ternyata kode yang panjang itu nggak akan di prettify, jadi yah nggak bagus aja kalau kodingannya panjang. udah lanjut.

lalu pertanyaannya adalah bagaimana dengan code ?, ternyata menurut dokumentasinya https://github.com/contentful/rich-text/blob/master/packages/rich-text-types/src/marks.ts, code masuk kedalam mark, jadi untuk merender costum code, kita bisa melakukan ini :

[MARKS.CODE]: (node) => {
        return (
          <pre>
           <code>{node}</code>
          </pre>
        );
      },

akan tetapi, ada satu masalah jika kita melanjutka menggunakan mark.code ini, kita tidak akan bisa mengatur prettiernya, saya sudah mencoba beberapa package untuk membuatnya pretty, tapi tidak berhasil juga beberapa package yang sudah saya coba :

  1. https://www.npmjs.com/package/pretty

  2. https://www.npmjs.com/package/prettier

  3. https://zebzhao.github.io/indent.js/

lalu saya mendapatkan solusi dari stackoverflow di sini : https://stackoverflow.com/questions/57149824/how-to-format-code-snippets-with-pre-tags-using-contentfuls-rich-text-react-r dan kesimpulannya marks.code bagus jika hanya satu baris kode saja, jika terlalu panjang maka baiknya Anda menggunakan yang namanya embed entry.

Part 2 : Menampilkan kodenya

Atur Graphql dulu :

        references {
          ... on ContentfulCodeBlock {
            __typename
            contentful_id
            language
            code {
              code
            }
          }
        }

lalu render kodenya menggunakan primjs seperti ini :

[BLOCKS.EMBEDDED_ENTRY]: (node, index) => {
  const content = node.data.target.code.code
   return (
         <div>
            <SyntaxHighlighter language={language} style={vscDarkPlus}>
              {code.code}
            </SyntaxHighlighter>
          </div>
        )
      },

full kodenya yang hanya menampilkan kode saja :

import React from 'react';
import { graphql } from 'gatsby';
import { renderRichText } from 'gatsby-source-contentful/rich-text';
import { BLOCKS } from "@contentful/rich-text-types";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";

const BlogPagesComponents = ({ data, location }) => {
    const {code, language} = node.data.target

    const options = {

        renderNode: {
            [BLOCKS.EMBEDDED_ENTRY]: (node, index) => {
                const { code, language } = node.data.target.code
                const handleCopy = (contentfulId) => {
                    setIsCopied((prevIsCopied) => {
                        return {
                            ...prevIsCopied,
                            [contentfulId]: true,
                        };
                    });

                    setTimeout(() => {
                        setIsCopied((prevIsCopied) => {
                            return {
                                ...prevIsCopied,
                                [contentfulId]: false,
                            };
                        });
                    }, 2000);
                };

                return (
                    <div className='relative'>
                        <SyntaxHighlighter language={language} tyle{vscDarkPlus}>
                            {code}
                        </SyntaxHighlighter>
                    </div>
                )
            },

        }
    };

    return (
        <div >
            {content && renderRichText(content, options)}
        </div>
    );
};

export default BlogPagesComponents;

export const query = graphql`
  query($slug: String!) {
    contentfulBlog(slug: { eq: $slug }) {
      content {
        raw
        references {
          ... on ContentfulCodeBlock {
            __typename
            contentful_id
            language
            code {
              code
            }
          }
        }
      }
    }
  }
`;

adapun full code untuk blog template untuk blog ini dimana saya mengimplementasikan prismjs, copy code, dan menggunakan tailiwindcss, react-icons, serta framer-motion untuk stylenya :

import React, { useState } from 'react';
import { graphql } from 'gatsby';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { renderRichText } from 'gatsby-source-contentful/rich-text';
import { BLOCKS, MARKS, INLINES } from "@contentful/rich-text-types";
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { FiCheck, FiCopy } from "react-icons/fi";
import { motion } from "framer-motion";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";

import Layout from '../components/layout';
import SEOHead from "../components/head";

const generateExcerpt = (rawContent) => {
  const content = JSON.parse(rawContent);
  let plainText = '';

  const extractText = (node) => {
    if (node.nodeType === 'text') {
      plainText += node.value.trim() + ' ';
    }

    if (node.content) {
      node.content.forEach(extractText);
    }
  };

  content.content.forEach(extractText);

  plainText = plainText
    .replace(/\n/g, '') // Remove newlines
    .replace(/\s+/g, ' ') // Replace multiple whitespaces with a single space
    .trim();

  const maxLength = 150; // Set the maximum number of characters for the excerpt
  if (plainText.length <= maxLength) {
    return plainText;
  }
  return `${plainText.slice(0, maxLength)}...`;
};

const BlogPagesComponents = ({ data, location }) => {
  const { title, content, featuredMedia } = data.contentfulBlog;
  const image = getImage(featuredMedia.gatsbyImageData);
  const { references } = content;
  const [isCopied, setIsCopied] = useState({});

  const Bold = ({ children }) => <span className="font-bold">{children}</span>;
  const Text = ({ children }) => <p className="">{children}</p>;

  const options = {
    renderMark: {
      [MARKS.BOLD]: text => <Bold>{text}</Bold>,
      [MARKS.CODE]: code => <code className='bg-yellow-200'>{code}</code>
    },
    renderNode: {
      [INLINES.HYPERLINK]: node => {
        return (
          <a className='text-black hover:text-yellow-600' href={node.data.uri}>
            {node.content[0].value}
          </a>
        )
      },
      [BLOCKS.PARAGRAPH]: (node, children) => {
        return <Text>{children}</Text>;
      },
      [BLOCKS.EMBEDDED_ENTRY]: (node, index) => {
        const {code, language} = node.data.target
        const handleCopy = (contentfulId) => {
          setIsCopied((prevIsCopied) => {
            return {
              ...prevIsCopied,
              [contentfulId]: true,
            };
          });

          setTimeout(() => {
            setIsCopied((prevIsCopied) => {
              return {
                ...prevIsCopied,
                [contentfulId]: false,
              };
            });
          }, 2000);
        };

        return (
         <div className='relative'>
            <div className='absolute right-3 top-0'>
              <CopyToClipboard text={code.code} onCopy={() => handleCopy(node.data.target.contentful_id)}>
                <motion.button
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                  className="flex items-center px-2 py-1 mt-2 text-sm text-stone-50 bg-stone-800 rounded-md hover:bg-stone-300 hover:text-stone-900 focus:outline-none focus:ring focus:ring-gray-400"
                >
                  {/* {console.log(node.data.target.contentful_id)} */}
                  {isCopied[node.data.target.contentful_id] ? (
                    <>
                      <FiCheck className="mr-1" />
                      Copied!
                    </>
                  ) : (
                    <>
                      <FiCopy className="mr-1" />
                      Copy
                    </>
                  )}
                </motion.button>
              </CopyToClipboard>
            </div>
            {console.log(language)}
            <SyntaxHighlighter language={language} style={vscDarkPlus}>
              {code.code}
            </SyntaxHighlighter>
          </div>
        )
      },
      [BLOCKS.EMBEDDED_ASSET]: node => {
        return (
          <>
            <GatsbyImage image={getImage(node.data.target.gatsbyImageData)} alt={node.data.target.title} />           
          </>
        );
      },

    }
  };

  return (
    <Layout location={location}>
      <div className="px-4 py-8 sm:px-8 md:px-16 lg:px-20 prose prose-h3:text-xl prose-a: max-w-none">
        <div className='mb-24'>
          {image && <GatsbyImage image={image} alt={featuredMedia.title} />}
        </div>
        <h1 className="text-3xl font-bold mb-4">{title}</h1>
        {content && renderRichText(content, options)}
      </div>
    </Layout>
  );
};

export const Head = ({ data }) => {
  const { title, content, featuredMedia } = data.contentfulBlog;
  return <SEOHead title={title} description={generateExcerpt(content.raw)} image={featuredMedia} />;
};

export default BlogPagesComponents;

export const query = graphql`
  query($slug: String!) {
    contentfulBlog(slug: { eq: $slug }) {
      title
      slug
      content {
        raw
        references {
           ... on ContentfulAsset {
              gatsbyImageData(
                layout: CONSTRAINED
                placeholder: BLURRED
                formats: [AUTO, WEBP]
              )
              __typename
              contentful_id
              title
            }
          ... on ContentfulCodeBlock {
            __typename
            contentful_id
            language
            code {              
              code
            }
          }
        }
      }
      featuredMedia {
        title
        gatsbyImageData(
          layout: CONSTRAINED
          placeholder: BLURRED
          formats: [AUTO, WEBP]
        )
        description
      }
    }
  }
`;