Gatsby fetching Wordpress Custom Post Types and create pages - reactjs

I read the documentation and tried several tutorials, but I am stuck on fetching custom post types with GatsbyJS.
I tried several approaches, but none of them are working as expected. I always receive a 404.
This is a part of the snippet I am using, which works fine with pages and posts, but not with a custom post type.
The projects pages should be created under a project subfolder/path. Like: example.com/project/my-first-project
The part of the gatsby-node.js looks like that:
const createSinglePages = async ({ posts, gatsbyUtilities }) =>
Promise.all(
posts.map(({ previous, post, next }) =>
// createPage is an action passed to createPages
// See https://www.gatsbyjs.com/docs/actions#createPage for more info
gatsbyUtilities.actions.createPage({
// Use the WordPress uri as the Gatsby page path
// This is a good idea so that internal links and menus work ๐Ÿ‘
path: post.uri,
// use the blog post template as the page component
component: path.resolve(
`./src/templates/${post.__typename.replace(`Wp`, ``)}.js`
),
// `context` is available in the template as a prop and
// as a variable in GraphQL.
context: {
// we need to add the post id here
// so our blog post template knows which blog post
// the current page is (when you open it in a browser)
id: post.id,
// We also use the next and previous id's to query them and add links!
previousPostId: previous ? previous.id : null,
nextPostId: next ? next.id : null,
},
})
)
);
The src/template/project.js file looks like this:
import React from "react";
import { Link, graphql } from "gatsby";
import Image from "gatsby-image";
import parse from "html-react-parser";
import Layout from "../components/Layout";
import Seo from "../components/Seo";
const ProjectTemplate = ({ data: { post } }) => {
const featuredImage = {
fluid: post.featuredImage?.node?.localFile?.childImageSharp?.fluid,
alt: post.featuredImage?.node?.alt || ``,
};
return (
<Layout>
<Seo title={post.title} description={post.excerpt} />
<article
className="blog-post"
itemScope
itemType="http://schema.org/Article"
>
<header>
<h1 itemProp="headline">{parse(post.title)}</h1>
<p>{post.date}</p>
{/* if we have a featured image for this post let's display it */}
{featuredImage?.fluid && (
<Image
fluid={featuredImage.fluid}
alt={featuredImage.alt}
style={{ marginBottom: 50 }}
/>
)}
</header>
{!!post.content && (
<section itemProp="articleBody">{parse(post.content)}</section>
)}
</article>
</Layout>
);
};
export default ProjectTemplate;
export const pageQuery = graphql`
query ProjectById(
# these variables are passed in via createPage.pageContext in gatsby-node.js
$id: String!
) {
# selecting the current post by id
post: wpProject(id: { eq: $id }) {
id
content
title
date(formatString: "MMMM DD, YYYY")
featuredImage {
node {
altText
localFile {
childImageSharp {
fluid(maxWidth: 1000, quality: 100) {
...GatsbyImageSharpFluid_tracedSVG
}
}
}
}
}
}
}
`;
Is the Gatsby API creating a subfolder automatically, or do I need to define that somewhere for each post type?
Any help appreciated!

You define the "subfolder" under the path field in:
gatsbyUtilities.actions.createPage({
path: post.uri,
component: path.resolve(
`./src/templates/${post.__typename.replace(`Wp`, ``)}.js`
),
context: {
id: post.id,
previousPostId: previous ? previous.id : null,
nextPostId: next ? next.id : null,
},
})
You just need to do something like:
path: `projects/${post.id}`
Check the slashes and trailing slashes here.
You cna replace projects for your dynamic project type if you fetch that information for a more automatic approach (assuming it's post.__typename).

In order to use Custom Post Types with WPGraphQL, you must configure the Post Type to show_in_graphql using the following field:
show_in_graphql : true
While Registering a new Custom Post Type
This is an example of registering a new "docs" post_type and enabling GraphQL Support.
add_action( 'init', function() {
register_post_type( 'docs', [
'show_ui' => true,
'labels' => [
//#see https://developer.wordpress.org/themes/functionality/internationalization/
'menu_name' => __( 'Docs', 'your-textdomain' ),
],
'show_in_graphql' => true,
'hierarchical' => true,
'graphql_single_name' => 'document',
'graphql_plural_name' => 'documents',
] );
} );

Related

How to preset prop (logged in user) for React component unit test?

I'm rather new to testing React application, thank you for your time in advance for responding to a newbie question.
So I've been following tutorial on Full Stack Open and came across this challenge about writing tests for React. There is this component Blog which takes some props from App > Blog List > Blog, including one called 'user' which is the returned object from the login function storing username and token etc.
In the Blog's JSX there is a 'remove' button which is shown only to logged in users, controlled by its style determined by a function comparing the username of the original poster of the blog and that of the currently logged in user.
Right now I'm not writing test for username comparison function at all, but it just gets in the way because I can't seem to set a value for 'user' to be passed into the Blog component, and this error was returned during the test:
display: blog.user.username === user.username ? '' : 'none'
^
TypeError: Cannot read properties of undefined (reading 'username')
And here are the codes of the Blog component and the test at current state:
import { useState } from 'react'
const Blog = ({ blog, addLike, deleteBlog, user }) => {
const [showDetails, setShowDetails] = useState(false)
const showWhenDetailsTrue = { display: showDetails ? '' : 'none' }
const toggleDetails = () => {
setShowDetails(!showDetails)
}
const postedBySelf = async () => {
const style = await {
display: blog.user.username === user.username ? '' : 'none',
}
return style
}
return (
<div style={blogStyle}>
<div>
{blog.title} {blog.author}{' '}
<button onClick={toggleDetails}>{showDetails ? 'hide' : 'view'}</button>
</div>
<div style={showWhenDetailsTrue} className="defaultHidden">
<div>{blog.url}</div>
<div>
likes {blog.likes}
<button onClick={() => addLike(blog.id)}>like</button>
</div>
<div>{blog.author}</div>
<button onClick={() => deleteBlog(blog)} style={postedBySelf()}>
remove
</button>
</div>
</div>
)
}
export default Blog
The test file:
import React from 'react'
import '#testing-library/jest-dom/extend-expect'
import { render, screen } from '#testing-library/react'
import Blog from './Blog'
test('renders title and author, but not url or number of likes by default', async () => {
const blog = {
title: 'Blog title',
author: 'Blog author',
url: 'Blog url',
user: {
username: 'mockuser',
},
}
await render(<Blog blog={blog} user={{ username: 'mockuser' }} />)
screen.getByText('Blog title', { exact: false })
screen.getAllByText('Blog author', { exact: false })
const { container } = render(<Blog blog={blog} />)
const div = container.querySelector('.defaultHidden')
expect(div).toHaveStyle('display: none')
})
When the postedBySelf function and associated content are commented out the test is passed. My question is, how can I mock the 'user' object and pass it into the component during the test? I don't understand why it is undefined even if I explicitly declared its value.
Thanks again for your time and appreciate your advice.
Finally spotted my mistake, had to pass in the user in the second rendering of the Blog too.
I wasn't quite sure if I'm missing critical knowledge on this topic but this tutorial explains things very well and helped me spotted the issue in a way. Strongly recommended: https://www.youtube.com/watch?v=OVNjsIto9xM

Next.js: How do you pass data to a route created dynamically

I have a component that is receiving data:
const ProductTile = ({ data }) => {
let { productList } = data
var [products] = productList
var { products } = products;
return (
<div>
<div className="p-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 gap-5">
{products.reduce((products, product) => products.find(x => x.productId === product.productId) ? products : [...products, product], []).map(({ colorCode, defaultColorCode, now, productId, productCode, productDescription, }, index) => {
return (
<Link key={`${productId}${index}`}
href={{
pathname: '/s7-img-facade/[slug]',
query: { slug: productCode },
}}
passHref>
/* template */
</Link>
)
})}
</div>
</div>
)
}
export default ProductTile
It creates a grid of templates each wrapped in a <Link> component which is rendering a dynamic component;
/s7-img-facade/[product]
What I would like is for the dynamic component to have access to products object which is in the ProductTile .
I know that I can do a getStaticProps in the dynamic component to do another request but that seems redundant and not dry...
Any ideas how the dynamic component get access to the products object?
Thanks in advance!
You've got the right idea - you can pass additional properties in the query field, but you'll need to use getServerSideProps to extract those from the query param and pass it to the page component as props. Something like this:
// pages/product.js
...
<Link key={`${productId}${index}`}
href={{
pathname: '/s7-img-facade/[slug]',
query: {
description: productDescription,
slug: productCode
},
}}
passHref
>
/* template */
</Link>
...
// pages/s7-img-facase/[slug].js
export default function S7ImgFacasePage({ description }) {
return <p>{ description }</p>
}
export const getServerSideProps = ({ params }) => {
const description = { params }
return {
props: {
description
}
}
}
So basically you pass it from the first page in params, read that in getServerSideProps of the second page, and then pass that as a prop to the second page component.
You mentioned getStaticProps - this won't work with static pages because getStaticProps is only run at build time so it won't know anything about the params you send at run time. If you need a fully static site, then you should consider passing it as a url parameter and reading it in useEffect or passing all possible pages through getStaticPaths and getStaticProps to generate all your static pages.

There's not a page yet at /second%20post

I have an issue regarding my gatsby site. I am fetching content from contentful and according to the code in my gatsby-node.js it has to generate two pages and it does but only one of them is working when I click on it for the second one it show that
There's not a page yet at /second%20post
I am so confused cause most of the issues asked here telling that they are not able to generate the pages and I don't know if the page is created or not and if it is created then why it shows me the error message and also when got to the error page the
second post
link is given but it is non clickable. all the other code is in my git repository here at Github code
Pleas refer to image for clear understanding at
Image here
here is my gatsby-node.js code file
const path = require(`path`)
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions;
// Define a template for blog post
const blogPost = path.resolve(`./src/templates/blog-post-contentful.js`)
// Get all markdown blog posts sorted by date
const result = await graphql(
`
{
allContentfulBlockchainlearning{
edges{
node{
slug
title
subtitle
}
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading your blog posts`,
result.errors
)
return
}
const posts = result.data.allContentfulBlockchainlearning.edges
// Create blog posts pages
// But only if there's at least one markdown file found at "content/blog" (defined in gatsby-
config.js)
// `context` is available in the template as a prop and as a variable in GraphQL
if (posts.length > 0) {
posts.forEach((post, index) => {
const previousPostSlug = index === 0 ? null : posts[index - 1].id
const $nextPostSlug = index === posts.length - 1 ? null : posts[index + 1].id
createPage({
path: post.node.slug,
component: blogPost,
context: {
slug: post.node.slug,
previousPostSlug,
$nextPostSlug,
},
})
})
}
}
and here is my blog-post template I want to creat
import React from "react"
import { Link, graphql } from "gatsby"
import Bio from "../components/bio"
import Layout from "../components/layout"
import SEO from "../components/seo"
const BlogPostTemplate = ({ data, location }) => {
const post = data.contentfulBlockchainlearning
const siteTitle = data.site.siteMetadata?.title || `Title`
const { previous, next } = data
return (
<Layout location={location} title={siteTitle}>
<SEO
title={post.title}
description={post.subtitle}
/>
<article
className="blog-post"
itemScope
itemType="http://schema.org/Article"
>
<header>
<h1 itemProp="headline">{post.title}</h1>
<p>{post.date}</p>
</header>
<section
dangerouslySetInnerHTML={{ __html: post.content.raw }}
itemProp="articleBody"
/>
<hr />
<footer>
<Bio />
</footer>
</article>
<nav className="blog-post-nav">
<ul
style={{
display: `flex`,
flexWrap: `wrap`,
justifyContent: `space-between`,
listStyle: `none`,
padding: 0,
}}
>
<li>
{previous && (
<Link to={previous.slug} rel="prev">Hey There
โ† {previous.title}
</Link>
)}
</li>
<li>
{next && (
<Link to={next.slug} rel="next">
{next.title} โ†’
</Link>
)}
</li>
</ul>
</nav>
</Layout>
)
}
export default BlogPostTemplate
export const pageQuery = graphql`
query BlogPostBySlug(
$slug: String!
$previousPostSlug: String
$nextPostSlug: String
) {
site {
siteMetadata {
title
}
}
contentfulBlockchainlearning(slug: {eq: $slug}){
title
subtitle
content{
raw
}
}
previous: contentfulBlockchainlearning(slug: { eq: $previousPostSlug}) {
title
}
next: contentfulBlockchainlearning(slug: { eq: $nextPostSlug }) {
title
}
}
`
The issue is simple, you can't create a URL with whitespace like the one you are trying to create. second page should be parsed as second-page since the whitespace between second and page potentially will cause a lot of issues.
Gatsby is creating properly the pages since they appear on the 404 page (under gatsby develop, the 404 page lists all you created pages). However, it doesn't have a valid route because your slugs must be slugified. Ideally, the slug should be fetched with the correct format from the CMS already, however, you can add some controls to avoid this behaviour:
if (posts) {
posts.forEach((post, index) => {
let slugifiedPath= post.node.slug.toLowerCase().replace(/\s/g, '-');
const previousPostSlug = index === 0 ? null : posts[index - 1].id
const $nextPostSlug = index === posts.length - 1 ? null : posts[index + 1].id
createPage({
path: slugifiedPath,
component: blogPost,
context: {
slug: post.node.slug,
previousPostSlug,
$nextPostSlug,
},
})
})
}
It's quite self-explanatory but, since your paths are being fetched with a wrong format, you need to refactor them by:
let slugifiedPath= post.node.slug.toLowerCase().replace(/\s/g, '-');
It transforms it to lower case and it replaces all-white spaces globally (/\s/g) using a regular expression for hyphens (-), creating a valid slug.

Getting data Gatsby.js and contentful

I have pages in contenful with different URLs. Now I'm getting all data from all pages, but I need to get different data for different URL. I'm tryin to filter it, but get error. So How I can check if url='something' I need query it ?
import React from "react";
import { StaticQuery, graphql } from "gatsby";
import ArticleMfo from "../components/articleMfo";
const Products = () => (
<StaticQuery
query={graphql`
query MyQuery {
allContentfulAllPages(filter: {link: {eq: $MYURL}}) {
edges {
node {
mfo {
__typename
... on ContentfulBank {
id
text
limit
rate
term
link
logo {
title
file {
url
}
}
}
}
}
}
}
}
`}
render={data => (
<Container className="container">
{data.allContentfulAllPages.edges.map(({ node }, i) => (
<div>
{node.mfo.map(mfos => (
<ArticleMfo key={mfos.id} content={mfos} />
))}
</div>
))}
</Container>
)}
/>
);
export default Products
Static query (hence the name) does not accept variables. As you can see from the Static Query docs:
StaticQuery does not accept variables (hence the name โ€œstaticโ€), but
can be used in any component, including pages
If you want to filter it, you will need to use a page query and pass the variable name (MYURL) via context on each page. In that case, you'll need to move your query to gatsby-node.js and, on every page creation, pass the variable through context to make it available to use as a filter. Something like:
const path = require("path")
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const result = await graphql(
`
{
allMarkdownRemark(limit: 1000) {
edges {
node {
frontmatter {
path
}
}
}
}
}
`
)
// Handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
const blogPostTemplate = path.resolve(`src/templates/blog-post.js`)
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
const path = node.frontmatter.path
createPage({
path,
component: blogPostTemplate,
// In your blog post template's graphql query, you can use pagePath
// as a GraphQL variable to query for data from the markdown file.
context: {
pagePath: path,
},
})
})
}
Note: Replace the query above and the resolvers for your data.
With the snippet above, every page created from the GraphQL query will have the path available (as pagePath) through context to filter, adapt it to your needs.

How do I fix a "IntegrationError: No such sku: "prod_....." error when using Gatsby and Stripe

I'm creating a test site using Gatsby and Stripe.
At the moment, I have 2 products set up in my Stripe account; I can render these successfully in a Gatsby site, created using the standard starter template.
In my gatsby-config.js file, I have this for the Stripe setup:
resolve: `gatsby-source-stripe`,
options: {
objects: ["Product", "Price"],
secretKey: process.env.STRIPE_SECRET_KEY,
downloadFiles: true,
},
The query and rendering code I'm using is this:
const Skus = () => {
return (
<StaticQuery
query={graphql`
query SkusForProduct {
skus: allStripePrice {
edges {
node {
id
currency
unit_amount
unit_amount_decimal
product {
images
unit_label
description
name
id
}
}
}
}
}
`}
render={({ skus }) => (
<div style={containerStyles}>
{skus.edges.map(({ node: sku }) => (
<SkuCard key={sku.product.id} sku={sku} stripePromise={stripePromise} />
))}
</div>
)}
/>
)
}
I have a separate component (SkuCard), which is used to render the details of each product - the core code looks like this:
const formatPrice = (amount, currency) => {
let price = (amount / 100).toFixed(2)
let numberFormat = new Intl.NumberFormat(["en-US"], {
style: "currency",
currency: currency,
currencyDisplay: "symbol",
})
return numberFormat.format(price)
}
const SkuCard = ({ sku, stripePromise }) => {
const redirectToCheckout = async (event, sku, quantity = 1) => {
event.preventDefault()
const stripe = await stripePromise
const { error } = await stripe.redirectToCheckout({
items: [{ sku, quantity }],
successUrl: `${window.location.origin}/page-2/`,
cancelUrl: `${window.location.origin}/advanced`,
})
if (error) {
console.warn("Error:", error)
}
}
return (
<div style={cardStyles}>
<h4>{sku.product.name}</h4>
<img src={sku.product.images} alt={sku.product.name} />
<p>Price: {formatPrice(sku.unit_amount, sku.currency)}</p>
<p>ID: {sku.product.id}</p>
<button
style={buttonStyles}
onClick={event => redirectToCheckout(event, sku.product.id)}
>
BUY ME
</button>
</div>
)
}
export default SkuCard
My Issue:
Each time I click on the Buy Me button, I'm getting this error in console log:
The steps I've taken:
Confirmed that the ID against the "No such sku" error is indeed one of my test products;
Tried altering the GraphQL query to see if I'm picking up the wrong ID entry - I have tried ones under sku.id and sku.product.id (using the GraphiQL browser), but neither work;
Added a tag to the products displayed to confirm what I believe to be the correct product ID (same as the one shown in the screenshot), can be rendered on screen in the product details - this is displayed without issue;
Scoured the internet to see if anyone else has done similar examples - nothing found;
Used the example code from the main Gatsby site to confirm that I can at least put a single "hard-coded" product through the checkout process - that works. (My chosen method though is to render products dynamically, not as hard-coded items on the page)
Checked the Stackoverflow site: questions have been asked about Stripe & Gatsby, but not found anything yet that is close to this issue.
I'm trying to figure out what is causing this error - can anyone help please?

Resources