Rendering Rich Text from CMS in Gatsby v4 - reactjs

I'm trying to render rich text in my Gatsby v4 about page for my site, but I'm having trouble finding any info on how to render the data. I've read about adding blocks, but I'm lost on what I should be including or how to go about this. I really just need to render links, headers, and body text in the rich text. Could someone walk me through this?
Here's my component code snippet. The data is all coming through the query correctly in the page so far. I just want the text to go where it says "TEXT GOES HERE"
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import { GatsbyImage } from "gatsby-plugin-image"
import {renderRichText} from "gatsby-source-contentful/rich-text"
import {BLOCKS, MARKS} from "#contentful/rich-text-types"
import * as aboutStyles from "../styles/about.module.scss"
const query = graphql`
{
contentfulAbout {
about
bioImage {
title
url
gatsbyImageData(
layout: FULL_WIDTH
placeholder: BLURRED
resizingBehavior: SCALE
width: 1000
)
}
aboutText {
raw
}
}
}
`
const AboutSection = () => {
const data = useStaticQuery(query);
const contentfulAbout = data.contentfulAbout
return (
<div className={aboutStyles.parent}>
<section className={aboutStyles.container}>
<div className={aboutStyles.image}>
<GatsbyImage className={aboutStyles.bioImage} image={contentfulAbout.bioImage.gatsbyImageData} alt={contentfulAbout.bioImage.title} />
</div>
<div className={aboutStyles.text}>
<h2>{contentfulAbout.about}</h2>
<p>TEXT GOES HERE</p>
</div>
</section>
</div>
)
}

The idea is to use the exposed BLOCKS and MARKS to fully customize the fetched data from Contentful like:
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
​
const Bold = ({ children }) => <span className="bold">{children}</span>
const Text = ({ children }) => <p className="align-center">{children}</p>
​
const options = {
renderMark: {
[MARKS.BOLD]: text => <Bold>{text}</Bold>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
},
},
}
​
renderRichText(node.bodyRichText, options)
Source: https://www.contentful.com/developers/docs/tutorials/general/rich-text-and-gatsby/
In that way, you can render a span with "bold" className when a MARKS.BOLD is fetched, using your own customized output.
In the snippet above, there's missing the implementation into a "standard" component, but the idea relies on the same fact. Using renderRichText what accepts two arguments:
The first one: your rich text node (aboutText in your case)
The second argument: the options with your custom output
Applied to your code, it should look like:
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import { GatsbyImage } from "gatsby-plugin-image"
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
import * as aboutStyles from "../styles/about.module.scss"
const options = {
renderMark: {
[MARKS.BOLD]: text => <strong>{text}</strong>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <p>{children}</p>,
},
}
const query = graphql`
{
contentfulAbout {
about
bioImage {
title
url
gatsbyImageData(
layout: FULL_WIDTH
placeholder: BLURRED
resizingBehavior: SCALE
width: 1000
)
}
aboutText {
raw
}
}
}
`
const AboutSection = () => {
const data = useStaticQuery(query);
const contentfulAbout = data.contentfulAbout
return (
<div className={aboutStyles.parent}>
<section className={aboutStyles.container}>
<div className={aboutStyles.image}>
<GatsbyImage className={aboutStyles.bioImage} image={contentfulAbout.bioImage.gatsbyImageData} alt={contentfulAbout.bioImage.title} />
</div>
<div className={aboutStyles.text}>
<h2>{contentfulAbout.about}</h2>
<p>{renderRichText(contentfulAbout.aboutText, options)}</p>
</div>
</section>
</div>
)
}
Of course, it may need some tweaking. Note that I've simplified the output so customize it as you wish/need.
Other resources:
https://www.contentful.com/developers/docs/concepts/rich-text/

Related

Problem to get an image in a blogpost with NextJS and WpGraphQL

I have another question in the project I'm doing, using NextJS to create a site with a blog part in Wordpress, through WPGraphQL, and I need support in a specific part. Let's go...
I managed to pull the highlighted image with almost no problems, I broke my head a bit, but it worked. The result that's functioning is the post excerpt, the code and the query were as follows (in time: image merely to test, it was the first idea that came to my mind, the site is not about Pokemon):
Image with working image in the excerpt, codes below
NextJS code:
import { LastPosts, PostContainer } from "./Styled";
const RecentBlogPosts = ({lastPosts}) => {
const posts = lastPosts;
return (
<LastPosts>
<h1> ÚLTIMAS POSTAGENS </h1>
{posts?.map((post) => {
return (
<PostContainer key={post.slug}>
<img src={post.featuredImage?.node.sourceUrl} alt="" />
<Link href={`/post/${post.slug}`}>
<a>
<h3> { post.title } </h3>
<div dangerouslySetInnerHTML={{ __html: post.excerpt }} />
<button> Saiba mais </button>
</a>
</Link>
</PostContainer>
)
})}
</LastPosts>
)
};
export default RecentBlogPosts;
Query for this part:
export const RECENT_POSTS = `query RecentPosts {
posts(where: {orderby: {field: DATE, order: DESC}}, first: 2) {
nodes {
id
slug
title
excerpt
featuredImage {
node {
sourceUrl
}
}
}
}
}`;
But I tried to pull the same image in the full blogpsot and it wasn't working... It appears when I view the post from the generic WordPress admin template, but not at my NextJS site, which i See through localhost:3000/post/[here would be the post title in slug] that I'm using. The rest is normal, text with all fonts and specifications with styled components, as well as tags, they work without any problem. The following is the same schema: image with result, code and query that I am using, this time for the part where I'm having problems:
Image with blogpost not working, codes below
NextJS code:
import fetcher from "../../lib/fetcher";
import { GET_ALL_POSTS_WITH_SLUG, POST_BY_SLUG } from "../../lib/wordpress/api";
import { useRouter } from "next/router";
import { Reset } from "../../constants/StyledConstants";
import Header from "../../components/Header/Header";
import { BlogArticle, BlogPostContent, TagLinks, TagWrapper } from "./StyledPost";
import Footer from "../../components/Footer/Footer";
const post = ({ postData }) => {
const blogPost = postData.data.post;
console.log(postData);
const tags = postData.data.post.tags.nodes;
const router = useRouter;
if(!router.isFallback && !blogPost?.slug) {
return <div>erro</div>
}
return (
<>
<Reset />
<Header />
<BlogPostContent>
{router.isFallback ? (
<div> Carregando...... </div>
) : (
<div>
<h1> { blogPost.title } </h1>
<img src={post.featuredImage?.node.sourceUrl} alt="imagem não aparece" />
<BlogArticle dangerouslySetInnerHTML={{ __html: blogPost.content }} />
<TagWrapper>
{tags.map((tag) => <TagLinks href={`/tags/${tag.slug}`} key={tag.slug}> { tag.name } </TagLinks>)}
</TagWrapper>
</div>
)}
</BlogPostContent>
<Footer />
</>
)
}
export default post;
export async function getStaticPaths() {
const response = await fetcher(GET_ALL_POSTS_WITH_SLUG);
const allposts = await response.data.posts.nodes;
return {
paths: allposts.map((post) => `/post/${post.slug}`) || [],
fallback: false
};
}
export async function getStaticProps({ params }) {
const variables = {
id: params.slug,
idType: "SLUG"
};
const data = await fetcher(POST_BY_SLUG, { variables })
return {
props: {
postData: data
},
};
}
Query being used:
export const POST_BY_SLUG = `query PostBySlug($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
title
slug
date
content
featuredImage {
node {
sourceUrl
}
}
tags {
nodes {
name
slug
}
}
}
}`;
I tried to use {post.featuredImage?.node.sourceUrl} because, as far as I understand, following the same basis I did for the excerpt in the blogspot, it should work, but I guess I was wrong... I tried to think of other ways to do it to get to the image, without success... Could someone help to point out where I am wrong please? Thank you very much in advance!!

Gatsby StaticImage from array

I am trying to make an image slider with React and Gatsby but the images aren't rendering and I get this in console
static-image.server.tsx:51 No data found for image "undefined"
Could not find values for the following props at build time: src
This is my code. When I change the image source to '../images/1.png' instead of images[sliderIndex] the image will show.
const ImageSlider = (props) => {
const [sliderIndex, setSliderIndex] = useState(0);
const images = ['../images/1.png', '../images/2.png']
useEffect(() => {
const sliderLoop = setInterval(() => {
if(sliderIndex+1 > images.length-1) {
setSliderIndex(0)
} else {
setSliderIndex(sliderIndex+1)
}
}, 5000)
return () => clearInterval(sliderLoop)
}, [sliderIndex])
console.log(sliderIndex)
return (
<>
{props.children}
<StaticImage src={images[sliderIndex]} alt=""/>
</>
)
}
<StaticImage /> would only works with a static string, see this part of the docs for more info.
You'd have to use the <GatsbyImage /> component instead & query image data manually, as instructed here. I copied the relevant sample code below.
import { graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
function BlogPost({ data }) {
const image = getImage(data.blogPost.avatar)
return (
<section>
<h2>{data.blogPost.title}</h2>
<GatsbyImage image={image} alt={data.blogPost.author} />
<p>{data.blogPost.body}</p>
</section>
)
}
export const pageQuery = graphql`
query {
blogPost(id: { eq: $Id }) {
title
body
author
avatar {
childImageSharp {
gatsbyImageData(
width: 200
placeholder: BLURRED
formats: [AUTO, WEBP, AVIF]
)
}
}
}
}
`

Next.js with Sanity not building Blog pages

I'm working on my first integration of Sanity with Next.Js, trying to add a blog to a personal site. Dev works fine, but when I go to deploy, or do a build, I get an error that it can't find one of the props for the blog page.
Error thrown is this:
TypeError: Cannot read property 'title' of undefined
This is what I'm using for my [slug].js file:
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Layout from '../../components/layout';
import Scrollbar from 'react-scrollbars-custom';
import Transitions from '../../lib/transitions';
import BlockContent from '#sanity/block-content-to-react';
import { postQuery, postSlugsQuery } from '../../lib/grocQueries';
import { getClient, overlayDrafts, sanityClient } from '../../lib/sanity.server';
import { urlForImage, usePreviewSubscription } from '../../lib/sanity';
const pageVariants = Transitions.pageVariant;
const pageTransition = Transitions.pageTransition;
export const Post = ({ data = {}, preview }) => {
const router = useRouter();
const slug = data?.post?.slug;
const {
data: { post, morePosts },
} = usePreviewSubscription(postQuery, {
params: { slug },
initialData: data,
enabled: preview && slug,
});
return (
<Layout>
<motion.article className='blog-article' initial='initial' animate='in' exit='out' variants={pageVariants} transition={pageTransition}>
<Scrollbar style={{ width: '100%', height: '100%' }}>
<figure className='hero-container'>
<h1 className='blog-title'>{post.title} </h1>
{post.mainImage && <img className='blog-hero' alt='Some alt Text' src={urlForImage(post.mainImage).url()} />}
</figure>
<div className='copy-block'>
<BlockContent blocks={post.body} imageOptions={{ w: 860, fit: 'max' }} {...sanityClient.config()} />
</div>
</Scrollbar>
</motion.article>
</Layout>
);
};
export async function getStaticProps({ params, preview = false }) {
const { post, morePosts } = await getClient(preview).fetch(postQuery, {
slug: params.slug,
});
return {
props: {
preview,
data: {
post,
morePosts: overlayDrafts(morePosts),
},
},
};
}
export async function getStaticPaths() {
const paths = await sanityClient.fetch(postSlugsQuery);
return {
paths: paths.map((slug) => ({ params: { slug } })),
fallback: true,
};
}
export default Post;
<figure className='hero-container'>
<h1 className='blog-title'>{post?.title} </h1>
{post?.mainImage && <img className='blog-hero' alt='Some alt Text' src={urlForImage(post?.mainImage).url()} />}
</figure>
During the build time next not aware of the post object and its key so it's better to make an optional chaining.
There are 2 things may want to add a in static generated page so it knows what to do if there isn't any props.
Add props checking in page, so it will not fail build.
if (!props) return null // or !data in your case.
Add notFound return props in getStaticProps so it knows how to handle 404 properly.
return {
notFound: true,
revalidate: 300,
}

How to query full size image using gatsby-image?

I'd need a bit of advice with gatsby-image. I'm building a gallery with a custom lightbox.
Query:
export const getAllPhotos = graphql`
query GetAllPhotos {
allStrapiPortfolio {
photos: nodes {
categories {
name
}
photo {
childImageSharp {
fluid(quality: 100) {
...GatsbyImageSharpFluid
}
}
}
strapiId
}
}
}
`;
I'm displaying my images in a grid. photos prop contains images from the query above.
Gallery.js
const Gallery = ({ photos }) => {
const [currentPhotoId, setCurrentPhotoId] = useState(null);
const handleClick = (e) => {
const lightbox = document.getElementById("lightbox");
const div = parseInt(e.target.parentElement.parentElement.className.split(" ")[0]);
const imgSelected = e.target.parentElement.parentElement.className.includes("gatsby-image-wrapper");
if (imgSelected) {
setCurrentPhotoId(div);
lightbox.classList.add("lightbox-active");
} else {
setCurrentPhotoId(null);
lightbox.classList.remove("lightbox-active");
}
};
return (
<main className="portfolio-gallery" onClick={(e) => handleClick(e)}>
photos.map((item) => {
return (
<Image
key={item.strapiId}
fluid={item.photo.childImageSharp.fluid}
data-categories={item.categories[0]}
alt={item.categories[0].name}
className={`${item.strapiId}`}
/>
);
})}
<Lightbox photos={photos} currentPhotoId={currentPhotoId} />
</main>
);
};
export default Gallery;
And after an image is clicked I display my lightbox component
lightbox.js
const Lightbox = ({ photos, currentPhotoId }) => {
const currentPhoto = photos.filter((photo) => photo.strapiId === currentPhotoId)[0];
return currentPhotoId === null ? (
<div id="lightbox" className="lightbox">
<h4>Nothing to display, this is hidden currently</h4>
</div>
) : (
<div id="lightbox" className="lightbox">
<Image fluid={currentPhoto.photo.childImageSharp.fluid} />
</div>
);
};
export default Lightbox;
Problem is that when I display my image in the lightbox stretched across the screen it is in a bad quality as the query downloads images in small sizes. However, my original images are 5000px wide. Unfortunately, as gatsby page queries are generated at the build I'm stuck with this quality.
Any idea about a workaround?
The idea when dealing with multiple images with gatsby-images for your kind of use-case is to query different image resolutions using the art director workaround (breakpoints). The idea, based on the documentation is to:
import React from "react"
import { graphql } from "gatsby"
import Img from "gatsby-image"
export default ({ data }) => {
// Set up the array of image data and `media` keys.
// You can have as many entries as you'd like.
const sources = [
data.mobileImage.childImageSharp.fluid,
{
...data.desktopImage.childImageSharp.fluid,
media: `(min-width: 768px)`,
},
]
return (
<div>
<h1>Hello art-directed gatsby-image</h1>
<Img fluid={sources} />
</div>
)
}
export const query = graphql`
query {
mobileImage: file(relativePath: { eq: "blog/avatars/kyle-mathews.jpeg" }) {
childImageSharp {
fluid(maxWidth: 1000, quality: 100) {
...GatsbyImageSharpFluid
}
}
}
desktopImage: file(
relativePath: { eq: "blog/avatars/kyle-mathews-desktop.jpeg" }
) {
childImageSharp {
fluid(maxWidth: 2000, quality: 100) {
...GatsbyImageSharpFluid
}
}
}
}
`

using renderRichText - receiving an error Node is not defined

I am having an error "TypeError: Cannot read property 'map' of undefined" when I tried to use documentToReactComponents to output the body of contentful post. If I comment it out, and try to console log, it displays documents etc.
Having edited the code to the suggested answer, I am receiving another error "Node is not defined" using renderRichText
import React from 'react'
import Layout from '../components/layout'
import { graphql } from 'gatsby'
import Head from '../components/head'
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
export const query = graphql`
query($slug: String!) {
contentfulBlogPost(
slug: {
eq: $slug
}
) {
title
slug
publishedDate(formatString: "MMM Do, YYYY")
body {
raw
}
}
}
`
const Blog = (props) => {
const Bold = ({ children }) => <span className="bold">{children}</span>
const Text = ({ children }) => <p className="align-center">{children}</p>
const options = {
renderMark: {
[MARKS.BOLD]: text => <Bold>{text}</Bold>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
[BLOCKS.EMBEDDED_ASSET]: node => {
const alt= node.data.target.fields.title['en-US']
const url= node.data.target.fields.file['en-US'].url
return (
<>
<img src={url} alt={alt} />
</>
)
},
[BLOCKS.EMBEDDED_ENTRY]: node => {
return (
<Layout>
<Head home_title="Blog Post" />
<h1>{props.data.contentfulBlogPost.title}</h1>
<p>{props.data.contentfulBlogPost.publishedDate}</p>
{props.data.contentfulBlogPost.body}
</Layout>
)
},
},
}
renderRichText(node.bodyRichText, options)
}
export default Blog
I have tried to read through from Github but still receiving the same error. I need help.
All you need to do is to add references to the graphql query, unlike with json, the body has raw and references. With that being said, you need to append this to the body after the raw;
references{
contentful_id
title
fixed{
src
}
}
Then you can you id to search for the title and src as you can see above.
import React from 'react'
import Layout from '../components/layout'
import { graphql } from 'gatsby'
import Head from '../components/head'
import { documentToReactComponents } from '#contentful/rich-text-react-renderer'
import { BLOCKS } from "#contentful/rich-text-types"
export const query = graphql`
query($slug: String!){
post: contentfulBlogPost(slug:{eq:$slug}){
title
publishedDate(formatString: "MMMM Do, YYYY")
body{
raw
references{
contentful_id
title
fixed{
src
}
}
}
}
}
`
const Blog = (props) => {
const assets = new Map(props.data.post.body.references.map(ref => [ref.contentful_id,ref]))
const options = {
renderNode:{
[BLOCKS.EMBEDDED_ASSET]: node => {
const url = assets.get(node.data.target.sys.id).fixed.src
const alt = assets.get(node.data.target.sys.id).title
return <img alt={alt} src={url}/>
}
}
}
return(
<Layout>
<h1>{props.data.post.title}</h1>
<p>{props.data.post.publishedDate}</p>
{documentToReactComponents(JSON.parse(props.data.post.body.raw),options)}
</Layout>
)
}
export default Blog
I would suggest using the following approach:
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
​
const Bold = ({ children }) => <span className="bold">{children}</span>
const Text = ({ children }) => <p className="align-center">{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>Embedded Asset</h2>
<pre>
<code>{JSON.stringify(node, null, 2)}</code>
</pre>
</>
)
},
},
}
​
renderRichText(node.bodyRichText, options)
Note: extracted from Contentful docs
Using renderRichText built-in method (from gatsby-source-contentful/rich-text). Your code seems a little bit deprecated or at least, some methods.
The following code will never work:
"embedded-asset-block" : (node) => {
const alt= node.data.target.fields.title['en-US']
const url= node.data.target.fields.file['en-US'].url
return <imag src={url} alt={alt} />
}
You need to use the providers of the dependency (#contentful/rich-text-types), that are BLOCKS and MARKS. You can always access to the dependency to check the methods, however, embedded-asset-block stands for BLOCKS.EMBEDDED_ASSET or BLOCKS.EMBEDDED_ENTRY, so:
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
​
const Bold = ({ children }) => <span className="bold">{children}</span>
const Text = ({ children }) => <p className="align-center">{children}</p>
​
const options = {
renderMark: {
[MARKS.BOLD]: text => <Bold>{text}</Bold>,
},
renderNode: {
[BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
[BLOCKS.EMBEDDED_ASSET]: node => {
// manipulate here your embedded asset
return (
<>
<h2>Embedded Asset</h2>
<pre>
<code>{JSON.stringify(node, null, 2)}</code>
</pre>
</>
)
},
[BLOCKS.EMBEDDED_ENTRY]: node => {
// manipulate here your embedded entry
return (
<>
<div>I'm an embedded entry</div>
</>
)
},
},
}
​
renderRichText(node.bodyRichText, options)

Resources