Gatsby's mapping between markdown files - graphql

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)
}

Related

Dynamically create pages with Gatsby based on many Contentful references

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.

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.

Query an array with unstructured objects on GraphQL

I'm trying to use GraphQL to query an unstructured array with objects in Gridsome. It is currently looking very messy and it feels like there should be a better way to do this.
The data that gets loaded into GraphQL from the CMS looks like this:
{
title: "Homepage",
top_image: "imgurl.jpg",
page_builder: [
{
type: "slider",
field: "data example",
different_field: "data example"
},
{
type: "call_to_action",
field_for_cta: "data example",
different_cta_field: "data example"
}
]
}
As you can see the objects in page_builder will have different fields depening on how the client is building this section.
When I try to query this in GraphQL. It will become very messy:
<page-query>
query {
data: pages(path: "/pages") {
title,
top_image,
page_builder {
type,
field,
different_field,
type,
field_for_cta,
different_cta_field
#this list will have way more fields depending on all the page builder elements
}
}
}
</page-query>
Is there a way to organize this fields by type and only return the fields of this specific type?
Assuming gridsome supports fragments, you can do something like this:
<page-query>
query {
data: pages(path: "/pages") {
title,
top_image,
page_builder {
...A #include(if: $includeA)
...B #include(if: $includeB)
...C #include(if: $includeC)
}
}
}
# Note: Replace PageBuilderType with appropriate type
fragment A on PageBuilderType {
# your fields here
}
fragment B on PageBuilderType {
# your fields here
}
fragment C on PageBuilderType {
# your fields here
}
</page-query>
You can then define the variables when calling createPage as shown here:
api.createPages(({ createPage }) => {
createPage({
path: '/my-page',
component: './src/templates/MyPage.vue',
queryVariables: {
includeA: someCondition,
includeB: someCondition,
includeC: someCondition,
},
})
})
}

How should I query and match data from the same response in GraphQL with Apollo Client and Link Rest?

I have the following query:
const getPage = gql`
query Page($path: String!) {
page(path: $path) #rest(type: "Page", path: "{args.path}") {
blocks #type(name: Block) {
name
posts #type(name: Post) {
body
author
}
}
authors #type(name: Author) {
name
}
}
}
In blocks.posts.author there's only an AuthorId. The authors object is containing all the available authors.
I'd like to replace/match the AuthorId with it's corresponding object. Is it possible to do this within one query?
I also wouldn't mind to have a separate query for Author only (fetch will be cached, no new request would be made), but I still don't know how would I match it through 2 queries.
Example API response
{
blocks: [
{
posts: [
{
id: 1,
title: 'My post',
author: 12,
}
]
}
],
authors: [
{
id: 12,
name: 'John Doe'
}
]
}
What I want with 1 query that author inside a post becomes the full author object.
Great question. With GraphQL, you have the power to expand any field and select the exact subfields you want from it, so if you were using GraphQL on your backend as well this would be a non-issue. There are some workarounds you can do here:
If all of the Author objects are in your Apollo cache and you have access to each Author's id, you could use ApolloClient.readFragment to access other properties, like this:
const authorId = ...; // the id of the author
const authorInfo = client.readFragment({
id: authorId,
fragment: gql`
fragment AuthorInfo on Author {
id
name
# anything else you want here
}
`,
});
Although it's worth noting that with your original query in the question, if you have all of the Author objects as a property of the query, you could just use Javascript operations to go from Author id to object.
const authorId = ...; // the id of the author
data.page.authors.find(author => author.id === authorId);
The following should work.
First, capture the author id as a variable using the #export directive. Then add a new field with some name other than author and decorate it with the #rest, using the exported variable inside the path.
So the query would look something like this:
query Page($path: String!) {
page(path: $path) #rest(type: "Page", path: "{args.path}") {
blocks #type(name: Block) {
name
posts #type(name: Post) {
body
author #export(as: "authorId")
authorFull #rest(
path: '/authors/{exportVariables.authorId}'
type: 'Author'
) {
name
}
}
}
authors #type(name: Author) {
name
}
}
}
You can use the fieldNameNormalizer option to rename the author property in the response to a field with a different name (for example, authorId). Ideally, that should still work with the above so you can avoid having a weird field name like authorFull but apollo-link-rest is a bit wonky so no promises.

Resources