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.
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>
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"
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>
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).
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 :
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.
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 } } } `;