Dynamically create pages with Gatsby based on many Contentful references - graphql

I am currently using Gatsby's collection routes API to create pages for a simple blog with data coming from Contentful.
For example, creating a page for each blogpost category :
-- src/pages/categories/{contentfulBlogPost.category}.js
export const query = graphql`
query categoriesQuery($category: String = "") {
allContentfulBlogPost(filter: { category: { eq: $category } }) {
edges {
node {
title
category
description {
description
}
...
}
}
}
}
...
[React component mapping all blogposts from each category in a list]
...
This is working fine.
But now I would like to have multiple categories per blogpost, so I switched to Contentful's references, many content-type, which allows to have multiple entries for a field :
Now the result of my graphQL query on field category2 is an array of different categories for each blogpost :
Query :
query categoriesQuery {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
Output :
{
"data": {
"allContentfulBlogPost": {
"edges": [
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
},
{
"id": "568r9e48-t1i8-sx4t8-9742-cdf70c4ed789vtu",
"name": "Test2",
"slug": "test-2"
}
]
}
},
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
}
]
}
},
...
Now that categories are inside an array, I don't know how to :
write a query variable to filter categories names ;
use the slug field as a route to dynamically create the page.
For blogposts authors I was doing :
query authorsQuery($author__slug: String = "") {
allContentfulBlogPost(filter: { author: { slug: { eq: $author__slug } } }) {
edges {
node {
id
author {
slug
name
}
...
}
...
}
And creating pages with src/pages/authors/{contentfulBlogPost.author__slug}.js
I guess I'll have to use the createPages API instead.

You can achieve the result using the Filesystem API, something like this may work:
src/pages/category/{contentfulBlogPost.category2__name}.js
In this case, it seems that this approach may lead to some caveats, since you may potentially create duplicated pages with the same URL (slug) because the posts can contain multiple and repeated categories.
However, I think it's more succinct to use the createPages API as you said, keeping in mind that you will need to treat the categories to avoid duplicities because they are in a one-to-many relationship.
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
`)
let categories= { slugs: [], names: [] };
result.data.allContentfulBlogPost.edges.map(({node}))=> {
let { name, slug } = node.category2;
// make some checks if needed here
categories.slugs.push(slug);
categories.names.push(name);
return new Set(categories.slugs) && new Set(categories.names);
});
categories.slugs.forEach((category, index) => {
let name = categories.names[index];
createPage({
path: `category/${category}`,
component: path.resolve(`./src/templates/your-category-template.js`),
context: {
name
}
});
});
}
The code's quite self-explanatory. Basically you are defining an empty object (categories) that contains two arrays, slugs and names:
let categories= { slugs: [], names: [] };
After that, you only need to loop through the result of the query (result) and push the field values (name, slug, and others if needed) to the previous array, making the needed checks if you want (to avoid pushing empty values, or that matches some regular expression, etc) and return a new Set to remove the duplicates.
Then, you only need to loop through the slugs to create pages using createPage API and pass the needed data via context:
context: {
name
}
Because of redundancy, this is the same than doing:
context: {
name: name
}
So, in your template, you will get the name in pageContext props. Replace it with the slug if needed, depending on your situation and your use case, the approach is exactly the same.

Related

Contentful graphql one too many relationship

I am trying to achieve this: getArticleBySlugWithFilteredTags('tag1', 'tag2', 'tag3') using 1 query ( 1 request ) and avoid clientside filtering ( grab many and filter out with javascript ).
I have content type Article that has an entry type as list: Tag ( another custom content type ).
So there is a one too many relationship: an Article can have multiple Tags.
Now getting back to this: getArticleBySlugWithFilteredTags('tag1', 'tag2', 'tag3').
Attempt using custom content type: Tag
Query:
data: articleCollection(limit: 1, where: {
slug: "article-unique-1",
}) {
items {
title
tagsCollection(limit: 5) { // here it would be nice if I can use "where": {name: "tag1"}
items {
name
value
linkedFrom {
relatedArticles: articleCollection(limit: 7) { // other related articles that has the same tag as parent Article
items {
slug
title
category
}
}
}
}
}
}
}
}
The only thing that is missing here is the that I need to filter out the tagsCollection ( based on some property: name or value ).
I see that I am limited to use "where" on tagsCollection.
Attempt using contentfulMetadata tags
Query
{
data: articleCollection(where:
{
slug: "article-unique-1",
contentfulMetadata: {
tags_exists: true,
tags: {
id_contains_some: ["tag1", "tag2"]
}
}
}) {
items {
contentfulMetadata {
tags {
id
name
linkedFrom { // I can't use this here
relatedArticles: articleCollection(limit: 7) {
items {
slug
title
category
}
}
}
}
}
slug
title
publicationDate
}
}
}
With this approach I am not able to use the linkedFrom in order to get also other related articles that have the same contentfulMetadata tags. What should I do in other to achieve this making 1 query and no clientside filtering with javascript ?

Filter by Distinct Values in Gatsby

I am trying to display a list of unique subject categories on a Gatsby site, which I will use to create unique pages. These will serve as taxonomy terms, of sorts. A limited version of my source json file looks like:
[
{
"BookID": "4176",
"Title": "Book Title 1",
"Subject": {
"subjectID": "HR",
"name": "Civil War & Reconstruction"
}
},
{
"BookID": "3619",
"Title": "Book Title 2",
"Subject": {
"subjectID": "AR",
"name": "Fine Art & Photography"
}
},
{
"BookID": "3619",
"Title": "Book Title 3",
"Subject": {
"subjectID": "AR",
"name": "Fine Art & Photography"
}
}
]
In my gatsby-node.js file, I can create pages using a list of distinct values of IDs to serve as the slugs to create my subject categories. As below:
allSubjects: allBooksJson {
distinct(field: Subject___subjectID)
}
However, I also need the name associated with these. I have not yet seen a way to use this as a filter, in order to deduplicate the results of a query.
So what I would ultimately like to is return all the unique subject objects so I can use the subjectID as a slug and the full name where needed on the individual pages.
Still learning Gatsby, so this may be the wrong approach, and any advice would be appreciated.
The idea of creating dynamic pages, is to get all the needed values in your gatsby-node.js using a GraphQL query, to create a bunch of pages and then, use the context to send a unique identifier to the template, to filter again the pages to get the specific data for each entry (books in your case). So:
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allBookJson {
edges {
node {
Subject{
subjectID
}
}
}
}
}
`)
result.data.allBookJson.edges.forEach(({ node }) => {
createPage({
path: `books/${node.Subject.subjectID}`, // change it as you wish
component: path.resolve(`./src/templates/book.js`), // change it as you wish
context: {
subjectID: node.fields.slug,
},
})
})
}
Note: adapt the snippet (query, loop, and variables) to your needs. You don't need to filter anything at this point, since you are only fetching the subjectID of all books.
If the values are likely to be repeated, use the new Set to remove the duplicates, then, you can loop through them to create pages dynamically:
let unique = [...new Set(result.data.allBookJson.edges.node)];
You are sending the subjectID to your templates/book.js file via context, so it will be available to be used as a pageContext.
Anytime you want just to get a list of all books, you can create a page query or a static query and loop through them at any time.
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
export default function Book({ data }) {
const books = data.allBookJson
return (
<Layout>
<div>
{books.map(book=>{
return <div>book.title</div>
})}
</div>
</Layout>
)
}
export const query = graphql`
query($subjectID: String) {
allBookJson(Subject___subjectID: { eq: $subjectID } ) {
edges{
node{
title
}
}
}
}
`
Note: again, test your query and adapt it to your needs at localhost:8000/___graphql. If you have duplicate results use the new Set.
It's difficult to guess your data structure without knowing it properly, the idea is to create a unique query based on the context value subjectID and filter the values. Use the GraphQL playground as support to know how the query and the filters should look like.
Further details: https://www.gatsbyjs.com/docs/tutorial/part-seven/

Gatsby's mapping between markdown files

I'm creating a multi-author site (using gatsby-plugin-mdx) and have the following file structure:
/posts
- /post-1/index.mdx
- /post-2/index.mdx
- ...
/members
- /member-a/index.mdx
- /member-b/index.mdx
- ...
In the frontmatter of the post page I have an array of authors like
authors: [Member A, Member B]
and I have the name of the author in the frontmatter of the author's markdown file.
I'd like to set the schema up so that when I query the post, I also get the details of the authors as well (name, email, etc.).
From reading this page it seems like I need to create a custom resolver... but all the examples I see have all the authors in one json file (so you have two collections, MarkdownRemark and AuthorJson... while I think for my case all my posts and members are in MarkdownRemark collection.
Thanks so much!
I end up doing something like this. Surely there's a cleaner way, but it works for me. It goes through all the Mdx and add a field called authors, which is queried, to all Mdx types.
One problem with this is that there's also authors under members, which is not ideal. A better approach is to define new types and change Mdx in the last resolver to your new post data type. Not sure how to get that to work though. At the end, I could query something like:
query MyQuery {
posts {
frontmatter {
title
subtitle
}
authors {
frontmatter {
name
email
}
}
}
}
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Mdx: {
authors: {
type: ["Mdx"],
resolve(source, args, context, info) {
return context.nodeModel.runQuery({
query: {
filter: {
fields: {
collection: { eq: "members" }
},
frontmatter: {
memberid: { in: source.frontmatter.authors },
},
},
},
type: "Mdx",
firstOnly: false,
})
}
}
},
}
createResolvers(resolvers)
}

Strapi GraphQL search by multiple attributes

I've got a very simple Nuxt app with Strapi GraphQL backend that I'm trying to use and learn more about GraphQL in the process.
One of my last features is to implement a search feature where a user enters a search query, and Strapi/GraphQL performs that search based on attributes such as image name and tag names that are associated with that image. I've been reading the Strapi documentation and there's a segment about performing a search.
So in my schema.graphql, I've added this line:
type Query {
...other generated queries
searchImages(searchQuery: String): [Image
}
Then in the /api/image/config/schema.graphql.js file, I've added this:
module.exports = {
query: `
searchImages(searchQuery: String): [Image]
`,
resolver: {
Query: {
searchImages: {
resolverOf: 'Image.find',
async resolver(_, { searchQuery }) {
if (searchQuery) {
const params = {
name_contains: searchQuery,
// tags_contains: searchQuery,
// location_contains: searchQuery,
}
const searchResults = await strapi.services.image.search(params);
console.log('searchResults: ', searchResults);
return searchResults;
}
}
}
},
},
};
At this point I'm just trying to return results in the GraphQL playground, however when I run something simple in the Playground like:
query($searchQuery: String!) {
searchImages(searchQuery:$searchQuery) {
id
name
}
}
I get the error: "TypeError: Cannot read property 'split' of undefined".
Any ideas what might be going on here?
UPDATE:
For now, I'm using deep filtering instead of the search like so:
query($searchQuery: String) {
images(
where: {
tags: { title_contains: $searchQuery }
name_contains: $searchQuery
}
) {
id
name
slug
src {
url
formats
}
}
}
This is not ideal because it's not an OR/WHERE operator, meaning it's not searching by tag title or image name. It seems to only hit the first where. Ideally I would like to use Strapi's search service.
I actually ran into this problem not to recently and took a different solution.
the where condition can be combined with using either _and or _or. as seen below.
_or
articles(where: {
_or: [
{ content_contains: $dataContains },
{ description_contains: $dataContains }
]})
_and
(where: {
_and: [
{slug_contains: $categoriesContains}
]})
Additionally, these operators can be combined given that where in this instance is an object.
For your solution I would presume you want an or condition in your where filter predicate like below
images(where: {
_or: [
{ title_contains: $searchQuery },
{ name_contains: $searchQuery }
]})
Lastly, you can perform a query that filters by a predicate by creating an event schema and adding the #search directive as seen here

How to transform Markdown query to Strapi query on GraphiQL?

I'm trying to get my hands on Gatsby + Strapi development (adding Material for styling), I'm new to both Gatsby and Strapi although I have some basic knowledge of React and it's making the way easier to follow.
I'm using this Gatsby Starter: https://www.gatsbyjs.org/starters/Vagr9K/gatsby-material-starter/ which includes the Material design I'm trying to achieve, but I'm having some trouble changing the Markdown queries to Strapi queries to avoid making a lot of code changes (posts are exactly the same but stored in Strapi). I have three Content Types in Strapi corresponding to the three different pages the original starter provides: Post, Category, and Tag.
This is the original MarkdownRemark graphQL query included in the post.jsx template:
query BlogPostBySlug($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
timeToRead
excerpt
frontmatter {
title
cover
date
category
tags
}
fields {
slug
date
}
}
}
How can I change it to retrieve the same info from Strapi?
I'm really new to this Strapi world so I'm having a lot of doubts with the GraphQL, as I can't follow the guide from the Markdown query because the Information displayed is not the same.
I'm also having trouble differentiating between allStrapiArticles and StrapiArticle, what's the main purpose of those?
EDIT: I've been testing the existing queries on GraphiQL to check what they are returning and this is what I'm seeing:
For the tag.jsx query:
query TagPage($tag: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [fields___date], order: DESC }
filter: { frontmatter: { tags: { in: [$tag] } } }
) {
totalCount
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
cover
date
}
}
}
}
}
GraphiQL returns nothing:
{
"data": {
"allMarkdownRemark": {
"totalCount": 0,
"edges": []
}
}
}
For the category.jsx query:
query CategoryPage($category: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [fields___date], order: DESC }
filter: { frontmatter: { category: { eq: $category } } }
) {
totalCount
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
cover
date
}
}
}
}
}
In this case, everything works fine and it retrieves article data.
And for the case of the query I've added as an example in this post (upper part of the question) I'm getting the following error:
"errors": [
{
"message": "Variable \"$slug\" of required type \"String!\" was not provided."
...
Make sure you are passing your variable in through Query Variables at the bottom of GraphiQL.
First, I would query AllMarkdownRemark to make sure you're getting the nodes from Gatsby. Something like:
query MyQuery {
allMarkdownRemark {
edges {
node {
fields {
slug
}
frontmatter {
title
}
}
}
}
}
If the slug is showing up, then this should work:
Sometimes a slug is not being generated. Which should show up checking allMarkdownRemark.

Resources