Nextjs - Contentful - Tags - Dynamic pages - graphql

I would like to create dynamic pages when I click a tag in an article or elsewhere on my website.
I'm using Next.js, SSG, and fetching the articles containing the tags from Contentful with the following GraphQL queries:
export async function getArticles() {
const articlesQuery = gql`
{
articleCollection(order: date_DESC) {
items {
title
slug
excerpt
date
contentfulMetadata {
tags {
name
id
}
}
featuredImage {
title
url
width
height
}
author {
name
photo {
fileName
url
width
height
}
title
twitterProfile
linkedInProfile
slug
}
}
}
}
`;
return graphQLClient.request(articlesQuery);
}
export async function getArticle(slug) {
const articleQuery = gql`
query getArticle($slug: String!) {
articleCollection(limit: 1, where: { slug: $slug }) {
items {
title
slug
excerpt
date
contentfulMetadata {
tags {
name
id
}
}
featuredImage {
title
url
width
height
}
author {
name
photo {
fileName
url
width
height
}
title
twitterProfile
linkedInProfile
slug
}
content {
json
links {
entries {
block {
sys {
id
}
__typename
... on VideoEmbed {
title
embedUrl
}
... on CodeBlock {
description
language
code
}
}
}
assets {
block {
sys {
id
}
url
title
width
height
}
}
}
}
}
}
}
`;
return graphQLClient.request(articleQuery, {
slug,
});
}
The contentfulMetadata is where the tags come from:
contentfulMetadata {
tags {
name
id
}
}
This is my [id].jsx file:
import { getArticles, getArticle } from "#utils/contentful";
export async function getStaticPaths() {
const data = await getArticles();
return {
paths: data.articleCollection.items.map((article) => ({
params: { id: article.contentfulMetadata.tags[0].id },
})),
fallback: false,
};
}
export async function getStaticProps(context) {
const data = await getArticle(context.params.id);
return {
props: { article: data.articleCollection.items[0] },
};
}
export default function TagPage({ article }) {
return (
<div>
<h1>{article.contentfulMetadata.tags.id}</h1>
</div>
);
}
I get the following error:
Error: Error serializing `.article` returned from `getStaticProps` in "/tags/[id]". Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.
When console.log(data.articleCollection.items.contentfulMetadata.tags.id); or console.log(data.articleCollection.items.contentfulMetadata.tags[0].id); within getStaticPaths function it provides the following error:
TypeError: Cannot read property 'tags' of undefined
Can anyone show how to create a dynamic page ([id].jsx) file, which shows the tag id as the header <h1> as well as all articles containing the same tag?

Contentful DevRel here 👋🏼.
article.contentfulMetadata.tags is an array, as an entry can have more than one tag. So you'll need to access the tag you want via article.contentfulMetadata.tags[0].id or article.contentfulMetadata.tags[desired_index].id and so on.
Here's an example GraphQL query:
query {
blogPostCollection {
items {
contentfulMetadata {
tags {
id
name
}
}
}
}
}
And here's the response with tags as an array:
"data": {
"blogPostCollection": {
"items": [
{
"contentfulMetadata": {
"tags": [
{
"id": "salmastag",
"name": "Salma s tag"
}
]
}
},
{
"contentfulMetadata": {
"tags": []
}
}
]
}
}
}
Notice how if a blog post doesn't have any PUBLIC tags assigned (the second entry in the response), an empty array is returned — you might want to do some safety checking in your code for this.

Related

How to pass variables to strapi graphql api?

I have the following graphql code in my strapi and nextjs project. The code is working in the graphql end point playground but some how it keeps getting error in my local environment showing message: 'Syntax Error: Expected Name, found <EOF>.', when I try to fetch data from nextjs. I have ready this strapi documentation but cant seems to find anything relevant with my current issues. Any can please tell me what error I am making here?
export async function getPostAndMorePosts(slug, preview) {
const data = await fetchAPI(
`
fragment FileParts on UploadFileEntityResponse {
data {
id
attributes {
alternativeText
width
height
mime
url
formats
}
}
}
query Posts($slug: String!) {
posts(filters: {slug: {eq: $slug}}) {
data{
attributes{
title
content
excerpt
slug
category{
data{
attributes{
name
}
}
}
coverImage{
...FileParts
}
}
}
}
morePost: posts(filters: {slug: {ne: $slug}},pagination: { start: 0, limit: 4 }) {
data{
attributes{
title
content
excerpt
slug
category{
data{
attributes{
name
}
}
}
coverImage{
...FileParts
}
publishedAt
}
}
}
}`,
{variables:{
slug:slug
}
}
)
return data
}
Your code needs a closing bracket which is misaligned at the moment! Look at the code below
export async function getPostAndMorePosts(slug, preview) {
const data = await fetchAPI(
`
fragment FileParts on UploadFileEntityResponse {
data {
id
attributes {
alternativeText
width
height
mime
url
formats
}
}
}
} -> closed here
query Posts($slug: String!) {
posts(filters: {slug: {eq: $slug}}) {
data{
attributes{
title
content
excerpt
slug
category{
data{
attributes{
name
}
}
}
coverImage{
...FileParts
}
}
}
}
morePost: posts(filters: {slug: {ne: $slug}},pagination: { start: 0, limit: 4 }) {
data{
attributes{
title
content
excerpt
slug
category{
data{
attributes{
name
}
}
}
coverImage{
...FileParts
}
publishedAt
}
}
}
`, -> removed from here
{variables:{
slug:slug
}
}
)
return data
}
So it's just an issue of {} alligment.

WpGraphQL query returns null

I'm having this GraphQL query from headless Wordpress in Nexjs via WpGraphQl plugin:
export const GET_POSTS_BY_CATEGORY_SLUG = gql`
query GET_POSTS_BY_CATEGORY_SLUG( $slug: String, $uri: String, $perPage: Int, $offset: Int ) {
${HeaderFooter}
page: pageBy(uri: $uri) {
id
title
content
slug
uri
seo {
...SeoFragment
}
}
categories(where: {slug: $slug}) {
edges {
node {
slug
posts: posts(where: { offsetPagination: { size: $perPage, offset: $offset }}) {
edges {
node {
id
title
excerpt
slug
featuredImage {
node {
...ImageFragment
}
}
}
}
pageInfo {
offsetPagination {
total
}
}
}
}
}
}
}
${MenuFragment}
${ImageFragment}
${SeoFragment}
`;
And this is my getStaticProps function:
export async function getStaticProps(context) {
const { data: category_IDD } = await client.query({
query: GET_POSTS_BY_CATEGORY_SLUG,
});
const defaultProps = {
props: {
cat_test: JSON.parse(JSON.stringify([category_IDD])),
},
revalidate: 1,
};
return handleRedirectsAndReturnData(defaultProps, data, errors, "posts");
}
If i pass it like this in props:
const defaultProps = {
props: {
cat_test: category_IDD,
},
i get an error saying:
SerializableError: Error serializing `.cat_test` returned from `getStaticProps` in "/category/[slug]". Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.
But when i JSON.parse as the code above, i get null
Whats wrong with this query?
Just noticed that the $slug is an array of strings, so here should be:
query GET_POSTS_BY_CATEGORY_SLUG( $slug: [String], $uri: String, $perPage: Int, $offset: Int )
instead of $slug: String
You're not actually passing the $slug variable to the query.
For instance if your page route is /category/[slug].js your getStaticProps should look something like this.
export async function getStaticProps(context) {
const { slug } = context.params;
const { data: category_IDD } = await client.query({
query: GET_POSTS_BY_CATEGORY_SLUG,
variables: { slug },
});
const defaultProps = {
props: {
cat_test: JSON.parse(JSON.stringify([category_IDD])),
},
revalidate: 1,
};
return handleRedirectsAndReturnData(defaultProps, data, errors, "posts");
}

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 work around GraphQL error, field "x" must not have a selection since type "String" has no subfields

I am using Gatsby and GraphQL, and I am new to GraphQL.
I have the following schema definition:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
const typeDefs = `
type MarkdownRemark implements Node {
frontmatter: Frontmatter
}
type Frontmatter {
title: String!
products: [Product]
}
type Product #dontInfer {
name: String!
price(price: Int = 1): Float
description: String
images: [ProductImage]
}
type ProductImage {
url: String
}
`;
createTypes(typeDefs);
};
Then on my page I use the following query:
query {
markdownRemark(fileRelativePath: { eq: "/content/pages/products.md" }) {
...TinaRemark
frontmatter {
title
products {
name
price
description
images {
url {
childImageSharp {
fluid(maxWidth: 1920) {
...GatsbyImageSharpFluid_withWebp
}
}
}
}
}
}
html
}
}
I then receive the following error:
Field "url" must not have a selection since type "String" has no subfields.
Does anyone have any suggestions on how to work around this error?
Also, what is childImageSharp? I'm wondering what the terminology is to define it. Is it a GraphQL "selector" or "function"?
It should be
query {
markdownRemark(fileRelativePath: { eq: "/content/pages/products.md" }) {
...TinaRemark
frontmatter {
title
products {
name
price
description
images {
url
}
}
}
html
}
}
Because you definition is
type ProductImage {
url: String
}
The url apparently has no sub fields.
For what it's worth (I don't know if this is related to your specific issue.) If your markdown path for the image file is invalid, GraphQL will return this error, interpreting the path as a string. I had this problem and it went away when I realized I had misspelled the path in the markdown.
productImage {
childImageSharp {
gatsbyImageData(width: 200)
}
}
I had a similar problem with returning a boolean. For me, instead of something like this
mutation {
someFunc(
memo: "test memo"
) {
success
}
}
I needed this
mutation {
someFunc(
memo: "test memo"
)
}

Relay: Optimistic update not causing a component rerender

I'm using PostgraphQL (https://github.com/calebmer/postgraphql) with Relay and wired a UpdateQuestionMutation into my app. However, I do not get optimistic updating to work.(When I enable network throttling in chrome I can see that the the optimistic update gets handled but the component still shows the old title).Do I miss something? I have following pieces :
class QuestionClass extends Component<IQuestion, void> {
save = (item) => {
this.props.relay.commitUpdate(
new UpdateQuestionMutation({store: this.props.store, patch: item})
);
this.isEditing = false;
};
public render(): JSX.Element {
const item = this.props.store;
console.log(item);
...
const Question = Relay.createContainer(QuestionClass, {
fragments: {
// The property name here reflects what is added to `this.props` above.
// This template string will be parsed by babel-relay-plugin.
store: () => Relay.QL`
fragment on Question {
${UpdateQuestionMutation.getFragment('store')}
title
description
userByAuthor {
${User.getFragment('store')}
}
}`,
},
});
...
export default class UpdateQuestionMutation extends Relay.Mutation<any, any> {
getMutation() {
return Relay.QL `mutation { updateQuestion }`
}
getVariables() {
console.log(this.props);
return {
id: this.props.store.id,
questionPatch: this.props.patch
}
}
getFatQuery() {
return Relay.QL `fragment on UpdateQuestionPayload { question }`
}
getConfigs() {
return [{
type: "FIELDS_CHANGE",
fieldIDs: {
question: this.props.store.id
}
}]
}
getOptimisticResponse() {
return {
store: this.props.patch
}
}
// This mutation has a hard dependency on the question's ID. We specify this
// dependency declaratively here as a GraphQL query fragment. Relay will
// use this fragment to ensure that the question's ID is available wherever
// this mutation is used.
static fragments = {
store: () => Relay.QL`
fragment on Question {
id
}
`,
};
}
Edit: That's what I see in the postgraphql logs:
mutation UpdateQuestion($input_0: UpdateQuestionInput!) { updateQuestion(input: $input_0) { clientMutationId ...F1 } } fragment F0 on Question { id rowId title description userByAuthor { id rowId username } } fragment F1 on UpdateQuestionPayload { question { id ...F0 } }

Resources