How do I render embedded code blocks from Contentful with Prism in Astro? - prism

I decided to try out Astro by building a web development blog integrated with contentful. I got everything working except for the code blocks. I create an embedded entry for the code block in my content model, which returns the following nodeType:
{
nodeType: 'embedded-entry-block',
data: {
target: {
metadata: [Object],
sys: [Object],
fields: {
code:
'```html\n<!DOCTYPE html>\n<html>\n<head>\n<title>My First Astro App</title>\n</head>\n <body>\n<h1>Hello, World!</h1>\n</body>\n</html>\n```'
}
}
},
content: []
}
I cannot figure out how to use documentToHtmlString from "#contentful/rich-text-html-renderer" to display the code block in my blog with a themed Prism component.
I passed a custom rendering component for the embedded entry to the documentToHtmlStringoptions, and extracted the lang and HTML code, but then I could not figure out how to return a themed Prism code block. However, I did manage to get it working without styling using the runHighlighterWithAstro function from the #astrojs/prism library.
---
import { documentToHtmlString } from "#contentful/rich-text-html-renderer";
import { BLOCKS } from "#contentful/rich-text-types";
import ContentLayout from "#layouts/ContentLayout.astro";
import BlogPostMeta from "#components/BlogPostMeta.astro";
import { BlogPost, contentfulClient } from "#lib/contentful";
import { Prism } from "#astrojs/prism";
import { runHighlighterWithAstro } from "#astrojs/prism/dist/highlighter";
export async function getStaticPaths() {
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
function extractCode(str: string): { lang: string; code: string } | null {
const match = str?.match(/```(\w+)\n([\s\S]+)\n```/);
if (match) {
return {
lang: match[1],
code: match[2],
};
}
return null;
}
const renderOptions = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node: any) => {
const { code } = node.data.target.fields;
const extractedCode = extractCode(code);
if (extractedCode) {
const { lang, code } = extractedCode;
const { classLanguage, html } = runHighlighterWithAstro(lang, code);
return `<pre class=${classLanguage} set:html=${html}></pre>`;
}
return "";
},
},
};
const pages = entries.items.map((item) => ({
params: { slug: item.fields.slug },
props: {
title: item.fields.title,
description: item.fields.description,
content: documentToHtmlString(item.fields.content, renderOptions),
date: new Date(item.fields.date).toLocaleDateString(),
},
}));
return pages;
}
const { slug } = Astro.params;
if (typeof slug !== "string") {
throw Error(`Slug should be a string. Received: ${slug}`);
}
const { description, content, title, date } = Astro.props;
---
<ContentLayout title={title} date={date}>
<BlogPostMeta
title={title}
description={description}
publishDate={date}
pagePath={`/posts/${slug}`}
slot='meta'
/>
<body>
<h1>{title}</h1>
<time>{date}</time>
<article slot='content' set:html={content} />
</body>
</ContentLayout>
I wanted to use a component, CodeBlockthat contains my styled Prism element. Unfortunately, I may be misunderstanding something about Astro, coming from React land.
---
import { Prism } from "#astrojs/prism";
const { language, code } = Astro.props;
---
<Prism lang={language} code={code} />
<style is:global>...
</style>

Related

Dynamic routing using graphQL in a Next.js app

I'm building a webpage that consumes the spaceX graphQL api, using apollo as a client. On the landing page I want to display a 'launches' card, that when clicked on, directs to a new page with details about that particular launch, as below:
index.js
import { ApolloClient, InMemoryCache, gql } from "#apollo/client"
import Link from 'next/link'
export const getStaticProps = async () => {
const client = new ApolloClient({
uri: 'https://api.spacex.land/graphql/',
cache: new InMemoryCache()
})
const { data } = await client.query({
query: gql`
query GetLaunches {
launchesPast(limit: 10) {
id
mission_name
launch_date_local
launch_site {
site_name_long
}
links {
article_link
video_link
mission_patch
}
rocket {
rocket_name
}
}
}
`
});
return {
props: {
launches: data.launchesPast
}
}
}
export default function Home({ launches }) {
return (
<div>
{launches.map(launch => {
return(
<Link href = {`/items/${launch.id}`} key = {launch.id}>
<a>
<p>{launch.mission_name}</p>
</a>
</Link>
)
})}
</div>
)
}
I've set up a new page items/[id].js to display information about individual launches, but this is where the confusion is. Using a standard REST api I'd simply use fetch, then append the id to the end of the url to retrieve the desired data. However I'm not sure how to do the equivalent in graphQL, using the getStaticPaths function. Any suggestions?
Here's items/[id]/js, where I'm trying to render the individual launch data:
import { ApolloClient, InMemoryCache, gql } from "#apollo/client"
export const getStaticPaths = async () => {
const client = new ApolloClient({
uri: "https://api.spacex.land/graphql/",
cache: new InMemoryCache(),
});
const { data } = await client.query({
query: gql`
query GetLaunches {
launchesPast(limit: 10) {
id
}
}
`,
});
const paths = data.map((launch) => {
return {
params: { id: launch.id.toString() },
};
});
return {
paths,
fallback:false
}
};
export const getStaticProps = async (context) => {
const id = context.params.id
// not sure what to do here
}
const Items = () => {
return (
<div>
this is items
</div>
);
}
export default Items;
for getStaticPaths
export const getStaticPaths = async () => {
const { data } = await client.query({
query: launchesPastQuery, // this will query the id only
});
return {
paths: data.CHANGE_THIS.map((param) => ({
params: { id: param.id },
})),
fallback: false,
};
};
CHANGE_THIS is the Query Type that follows data in the JSON response.
for getStaticProps
export const getStaticProps = async ({
params,
}) => {
const { data } = await client.query({
query: GetLaunchPastByID ,
variables: { LaunchID: params.id, idType: "UUID" }, // the idType is optional, and the LaunchID is what you'll use for querying by it*
});
return {
props: {
launchesPast: data.CHANGE_THIS,
},
};
The launchPastQueryByID is like:
const GetLaunchPastByID = gql`
query LaunchPastByID($LaunchID: UUID!) { // UUID is id type
CHANGE_THIS(id: $LaunchID) {
id
//...
}
}
`;
sorry for not giving you the correct queries, spacex.land is currently down.

Cannot destructure property of {intermediate value} as it is undefined

I have just started using graphql for the first time as I have integrated my NEXTJS app with strapi. But I have received this error message Cannot destructure property 'data' of '(intermediate value)' as it is undefined.
I followed this tutorial - enter link description here
Just modified it to what I wanted. This is my graphql:
query {
posts {
data {
attributes {
heading
}
}
}
}
And this is my vs code:
export async function getStaticProps() {
const client = new ApolloClient({
url: 'http://localhost:1337/graphql/',
cache: new InMemoryCache(),
})
const { data } = await client.query({
query: gql`
query {
posts {
data {
attributes {
heading
}
}
}
}
`,
})
return {
props: {
posts: data.posts,
},
}
}
FULL CODE:
import { ApolloClient, InMemoryCache, gql } from '#apollo/client'
export default function Blog({ posts }) {
console.log('posts', posts)
return (
<div>
{posts.map(post => {
return (
<div>
<p>{posts.heading}</p>
</div>
)
})}
</div>
)
}
export async function getStaticProps() {
const client = new ApolloClient({
url: 'http://localhost:1337/graphql/',
cache: new InMemoryCache(),
})
const { data } = await client.query({
query: gql`
query {
posts {
data {
attributes {
heading
}
}
}
}
`,
})
return {
props: {
posts: data.posts,
},
}
}
I really don't know where to begin with this.
Firstly check whether or not you are receiving empty data from API.
If its array, check its length or use methods like Array.isArray(myArray).
If its object, make a function like this to check objects.
function isObjectEmpty(obj) {
return (
!!obj && // πŸ‘ˆ null and undefined check
Object.keys(obj).length === 0 &&
obj.constructor === Object
)
}
export default isObjectEmpty
if the data is empty return notFound prop as true to show your 404 page.
// This function gets called at build time
export async function getStaticProps({ params, preview = false }) {
// fetch posts
// check validity data
return isObjectEmpty(pageData)
? { notFound: true }
: {
props: {
posts
}
}
}
Secondly add a failsafe mechanism like the use of optional-chaining to securely access nested values/properties.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
export default function Blog({ posts }) {
console.log('posts', posts)
return (
<div>
{posts?.length && posts?.map(post => {
return (
<div>
<p>{posts?.heading}</p>
</div>
)
})}
</div>
)
}
I was running into the same error while testing using Jest.
It turns out I was mocking all of graphql, but I had to specifically mock the return value.

Gatsby Contentful embedded image

As I see there is no json option anymore when querying the contentfulBlogPost only raw. I was able to make some changes to get everything from the body, except the image from that post.
If I made a test in GraphQL Playground I can get the image id and url but that's it.
query {
allContentfulAsset {
edges {
node {
id
file {
url
}
}
}
}
}
I tried to find an example how to get embedded images but no luck....
import React from 'react'
import { graphql } from 'gatsby'
import { documentToReactComponents } from '#contentful/rich-text-react-renderer'
import Layout from '../components/layout'
export const query = graphql`
query($slug: String!) {
contentfulBlogPost(slug: {eq: $slug}) {
title
publishedDate(formatString: "MMMM Do, YYYY")
body {
raw
}
}
allContentfulAsset {
edges {
node {
id
file {
url
}
}
}
}
}
`
const Blog = (props) => {
const options = {
renderNode: {
"embedded-asset-block": (node) => {
const alt = node.data.title
const url = node.file.url
return <img alt={alt} src={url}/>
}
}
}
return (
<Layout>
<h1>{props.data.contentfulBlogPost.title}</h1>
<p>{props.data.contentfulBlogPost.publishedDate}</p>
{documentToReactComponents(JSON.parse(props.data.contentfulBlogPost.body.raw, options))}
</Layout>
)
}
export default Blog
Plugins:
...
'gatsby-plugin-sharp',
{
resolve: 'gatsby-transformer-remark',
options: {
plugins: [
'gatsby-remark-relative-images',
{
resolve: 'gatsby-remark-images-contentful',
options: {
maxWidth: 750,
linkImagesToOriginal: false
}
}
]
}
}
],
}
Hi I saw this solution in a Youtube comment. First thing you have to do is change your Graphql query to something like this:
query ($slug: String!) {
contentfulBlogPost(slug: {eq: $slug}) {
id
title
publishedDate(formatString: "MMMM Do, YYYY")
body {
raw
references {
... on ContentfulAsset {
contentful_id
title
file {
url
}
}
}
}
}
}
Then change your options constant to:
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: node => {
console.log(node);
const imageID = node.data.target.sys.id;
const {
file: {url},
title
} = props.data.contentfulBlogPost.body.references.find(({contentful_id: id}) => id === imageID);
return <img src={url} alt={title} />
}
}
}
Use something 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>,
[BLOCKS.EMBEDDED_ASSET]: node => {
return (
<>
<h2>Embedded Asset</h2>
<pre>
<code>{JSON.stringify(node, null, 2)}</code>
</pre>
</>
)
},
},
}
​
renderRichText(node.bodyRichText, options)
Source: https://www.contentful.com/developers/docs/tutorials/general/rich-text-and-gatsby/
The return statement in BLOCKS.EMBEDDED_ASSET entry will contain your data, adapt to your needs. If you go inside the dependency, you'll see all the exposed methods, so you will have also a BLOCKS.EMBEDDED_ENTRY entry for your embedded entries. Apply it like:
[BLOCKS.EMBEDDED_ENTRY]: node => {
// your logic to manipulate the entry here
return (
<>
<div>whatever</div>
</>
)
},
For anyone that is still struggling to find the "references" field in graphql, remember that you HAVE TO first create an entry in contentful by adding at least one image. Otherwise, the references field will not show up in graphql, hence you can not query it.

How to add variables to graphQL query?

I am using Gatsby.
I need to increase the limit of 3 to each onCLick.
I've tried to follow this post, but with no success, so I removed the edits, and here is the original code...
This will help me to load more posts.
export const LatestNews = ({data}) => {
console.log(data);
return (
<section id="news_posts_section">
<p>some data</p>
</section>
);
};
export const latestNewsQuery = graphql`
query myquery{
allMarkdownRemark(
filter: { frontmatter: { layout: { eq: "news" } } }
sort: { fields: frontmatter___date, order: DESC }
limit: 2
) {
nodes {
frontmatter {
layout
title
path
date
featuredImage
thumbnail
}
excerpt(pruneLength: 325)
}
}
}
`;
Here is my gatsby-node:
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
const blogPostTemplate = path.resolve('src/templates/blog-post/BlogPost.js');
const newsTemplate = path.resolve('src/templates/news-single/NewsSingle.js');
const latestNewsPage = path.resolve(
'src/components/pages-implementation/news/sections/LatestNews.js',
);
const blog = graphql(`
{
blog: allMarkdownRemark(
filter: { frontmatter: { layout: { eq: "blog" } } }
) {
edges {
node {
frontmatter {
path
}
}
}
}
}
`).then((result) => {
if (result.errors) {
result.errors.forEach((e) => console.error(e.toString()));
return Promise.reject(result.errors);
}
const posts = result.data.blog.edges;
posts.forEach((edge) => {
const { path } = edge.node.frontmatter;
createPage({
path: path,
component: blogPostTemplate,
context: {},
});
});
});
const news = graphql(`
{
news: allMarkdownRemark(
filter: { frontmatter: { layout: { eq: "news" } } }
) {
edges {
node {
frontmatter {
path
}
}
}
}
}
`).then((result) => {
if (result.errors) {
result.errors.forEach((e) => console.error(e.toString()));
return Promise.reject(result.errors);
}
const news = result.data.news.edges;
news.forEach((edge) => {
const { path } = edge.node.frontmatter;
createPage({
path: path,
component: newsTemplate,
context: {},
});
});
news.forEach(edge => {
createPage({
path: `/news/`,
component: latestNewsPage,
context: {
// This time the entire product is passed down as context
product: edge
}
});
});
});
return Promise.all([blog, news]);
};
Edit 21 November:
I replaced the above code with my attempt to use a non-static query
I added the gatsby-node config
I give here a little explanation: the BlogPost.js and NewsSingle.js are templates that create new pages for each post or news post (from Netlify CMS)
The LatestNews.js is a component in a page. This is the main page of the news. Where are shown all the news? With a static query, it works fine, however, I need to make the "limit" a variable in order to apply a load more button, that onClick will increase the limit, thus showing more news posts on the loop.
With the above configuration it says:
warning The GraphQL query in the non-page component "/home/user/projectname/src/components/pages-implementation/news/sections/LatestNews.js" will not be run.
Exported queries are only executed for Page components. It's possible you're
trying to create pages in your gatsby-node.js and that's failing for some
reason.
useStaticQuery (hence the name) does not allow to receive variables. If you take a look at the docs:
useStaticQuery does not accept variables (hence the name β€œstatic”),
but can be used in any component, including pages
The only way to pass variables in a GraphQL query is by using the context API in the gatsby-node.js. For example:
queryResults.data.allProducts.nodes.forEach(node => {
createPage({
path: `/products/${node.id}`,
component: productTemplate,
context: {
// This time the entire product is passed down as context
product: node
}
});
});
};
In the snippet above, will be a product variable in the context with the whole node. It can be accessed through pageContext prop in the destination template or used as a query parameter.

How to get Gatsby Images based on results of PageQuery?

I'd like to do something like the following so I can get Gatsby Images dynamically:
const image = 'gastby-astronaut.png';
export const imageQuery = graphql`
{ allImageSharp (
filter: {
fluid: {
originalName: {
regex: "/${image}/"
}
}
}
){
edges {
node {
fluid {
originalName
}
}
}
}
}
`;
However, I can't figure out how to connect this query to an initial query that would get the 'gatsby-astronaut.png', or perform this query from a subcomponent with a . I get this error when I try this:
Error: BabelPluginRemoveGraphQL: String interpolations are not allowed
in graphql fragments. Included fragments should be referenced as
`...MyModule_foo`.
Any suggestions on the proper way to return Gatsby Images dynamically?
Ah, yeah Gatsby extracts GraphQL queries from your pages through static analysis: they load the file as text, parse it, and extract the query, all before the actual file gets executed. This means that your typical tagged-template literal functionality isn't there.
The only way to filter is through context provided when createPage is called from gatsby-node.js. I.e. if you do this:
exports.createPages = ({ graphql, actions }) =>
graphql(`some query here`).then(result => {
actions.createPage({
path: "/output-path/",
component: path.resolve(`./src/templates/your_template.jsx`),
context: { image: result.data.yourImage },
})
})
Then you can do this in your page query:
query SomePage($image: String!) {
allImageSharp (
filter: {
fluid: {
originalName: {
regex: $image
}
}
}
){
edges {
node {
fluid {
originalName
}
}
}
}
}
Here's a solution I came up with... pretty janky, but it works:
import PropTypes from 'prop-types';
import React from 'react';
import Img from 'gatsby-image';
import { useStaticQuery, graphql } from 'gatsby';
const Image = ({ imageYouWant }) => {
const data = useStaticQuery(
graphql`
query allTheImagesQuery{
allImageSharp {
edges {
node {
fluid(maxWidth:1000) {
...GatsbyImageSharpFluid
originalName
}
}
}
}
}`,
);
const TheImageYouWant = data.allImageSharp.edges
.filter(edge => edge.node.fluid.originalName === imageYouWant)
.map(myImage => <Img fluid={myImage.node.fluid} />);
return (
<>
{ TheImageYouWant }
</>
);
};
Image.propTypes = {
imageYouWant: PropTypes.string,
};
Image.defaultProps = {
imageYouWant: '',
};
export default Image;

Resources