I have two markdown collection routes which I want to apply to two different sets of markdowns separated by subfolders.
My folder structure is as follows
appfolder
content
projects
project1.md
project2.md
article
article1.md
article2.md
src
pages
projects
{MarkdownRemark.frontmatter__slug}.js
articles
{MarkdownRemark.frontmatter__slug}.js
The content of projects/{MarkdownRemark.frontmatter__slug}.js is as follows
import React from 'react';
import Layout from "../../components/Layout";
import Nav from "../../components/Nav";
import PageHeader from "../../components/PageHeader";
import Footer from "../../components/Footer";
import SimpleReactLightbox from 'simple-react-lightbox'
import { graphql } from 'gatsby'
const ProjectPage = ({data}) => {
const fm = data.markdownRemark.frontmatter;
const html = data.markdownRemark.html;
return (
<SimpleReactLightbox>
<Layout pageTitle={fm.title}>
<Nav />
<PageHeader title={fm.title} />
<Footer />
</Layout>
</SimpleReactLightbox>
);
};
export const query = graphql`
query($id: String!) {
markdownRemark(id: { eq: $id },fileAbsolutePath: {regex: "/(projects)/" }) {
html
frontmatter {
slug
title
summary
icon
}
}
}
`
export default ProjectPage;
But GraphiQL shows that the pages are generated for all md files. How do I restrict each collection route to respective subfolder.
Think your issues falls at fileAbsolutePath for the regex.
Change (projects) to just projects
In that cases, I think the easiest approach is adding a "key" value in your frontmatter to make the filter. For example:
---
key: article
title: Test Title
anotherField: Another field value
---
The body of the markdown
Note: change key: article for key: projects when needed
Then, in your GraphQL query use the filter with something like:
export const query = graphql`
query($id: String!) {
markdownRemark(id: { eq: $id }, filter: { frontmatter: { key: { eq: "article" },
}
},, ) {
html
frontmatter {
slug
title
summary
icon
}
}
}
`
Tweak it as you need and check the availability of the filters in the localhost:8000/___graphql playground.
I just ran into the same problem. It seems like there are no ready-made solutions for your current organization of src/pages. But after running through the discussions on Gatsby, I found there are some workarounds, though I think it's a bit inconvenient.
Like this comment said:
If your source nodes contain something like category: "article" | "blog", you could generate separate routes with file nested in
category directory like /{SourceNode.category}/{SourceNode.slug}.tsx.
If your source node does not contain such a category field, then you
can append it to the nodes in onCreateNode hook, or create entirely
new nodes for your purposes
In your case, if you can add a category field to the frontmatter in your .md files, you can change your organization of src to
appfolder
content
projects
project1.md
project2.md
article
article1.md
article2.md
src
templates
project-template.js
article-template.js
pages
{MarkdownRemark.frontmatter__category}
{MarkdownRemark.frontmatter__slug}.js
Then projects/{MarkdownRemark.frontmatter__slug}.js goes to templates/project-template.js and the article one is similar.
Now in your {MarkdownRemark.frontmatter__slug}.js file, you can forward the data queried by GraphQL to the corresponding template.
// {MarkdownRemark.frontmatter__slug}.js
const GeneratedPage = ({ data }) => {
const templates = {
project: <ProjectTemplate data={data} />,
article: <ArticleTemplate data={data} />,
};
return templates[data.MarkdownRemark.frontmatter.category];
};
If it's not possible to add the category field into the .md file, make use of the createNodeField API to programmatically add it to MarkdownRemark nodes in gatsby-node.js. It maybe something like this:
// gatsby-node.js
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
if (node.internal.type === `MarkdownRemark`) {
const value = ...; // something mapping the directory of node to certain catecory
createNodeField({
name: 'category',
node,
value,
});
}
};
Hope this can be helpful to you.
Related
I have a development on Gatsby with allMdx. I created a "Category" taxonomy and to create a category page I used a file gatsby-node.js . There's such a code inside.
const _ = require("lodash")
const { transliterate } = require('./src/functions/transletter');
function dedupeCategories(allMdx) {
const uniqueCategories = new Set()
// Iterate over all articles
allMdx.edges.forEach(({ node }) => {
// Iterate over each category in an article
node.frontmatter.categories.forEach(category => {
uniqueCategories.add(category)
})
})
// Create new array with duplicates removed
return Array.from(uniqueCategories)
}
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
// Query markdown files including data from frontmatter
const { data: { allMdx } } = await graphql(`
query {
allMdx {
edges {
node {
id
frontmatter {
categories
tags
slug
}
}
}
}
}
`)
// Create array of every category without duplicates
const dedupedCategories = dedupeCategories(allMdx)
// Iterate over categories and create page for each
dedupedCategories.forEach(category => {
reporter.info(`Creating page: blog/category/${category}`)
createPage({
path: `blog/category/${_.kebabCase(transliterate(category))}`,
component: require.resolve("./src/templates/categories.js"),
// Create props for our CategoryList.js component
context: {
category,
// Create an array of ids of articles in this category
ids: allMdx.edges
.filter(({ node }) => {
return node.frontmatter.categories.includes(category)
})
.map(({node}) => node.id),
},
})
})
}
Now I want to create a "Tag" taconomy, but I can't figure out how to do it beautifully and briefly, what and where to add to the gatsby-node.js so that I have two taxonomies created that work the same way as one. It is clear that you can simply duplicate this code and write "tag" instead of "category", but this is not very nice.
Just in case, here is my template code category.js
import React from "react"
import { Link, graphql } from "gatsby"
import Layout from '../components/layout'
import Seo from '../components/seo'
const CategoryList = ({ pageContext: { category }, data: { allMdx }, }) =>
(
<Layout pageTitle={category}>
{
allMdx.edges.map(({ node }) => {
return (
<article key={node.id}>
<h2>
<Link to={`/blog/${node.frontmatter.slug}`}>
{node.frontmatter.title}
</Link>
</h2>
<p>Posted: {node.frontmatter.date}</p>
<p>{node.excerpt}</p>
</article>
)
})
}
</Layout>
)
export const query = graphql`
query CategoryListQuery($ids: [String]!) {
allMdx (filter: { id: { in: $ids } }) {
edges {
node {
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
slug
}
id
excerpt
}
}
}
}
`
export const Head = ({ pageContext }) => (
<Seo
title={pageContext.category}
description={`Статьи из категории ${pageContext.category}`}
/>
)
export default CategoryList
I don't see anything wrong in your approach. In fact, is the way to go in terms of getting all tags and categories (hence a map) and loop through them to create the pages, at least given your approach.
However, I think you can save some steps if you change your markdown structure: if each MDX has a key attribute (or similar) containing the type of taxonomy it is (article, tag, category, page, etc) you can create more succinct GraphQL queries, hence you can save the filters.
For instance, in that way, you would be able to create a single query tags and categories:
const tagsQuery = await graphql(`
query getAllTags {
allTags: allMarkdownRemark (
filter: { frontmatter: { key: { in: ["tag"] }}}) {
edges {
node {
id
frontmatter {
name
slug
type
key
}
}
}
}
}
`);
Note: you can use eq operator instead of in. In this case the array will accept more types of tags like projectTags, articleTags, etc.
This allows you to create more specific approach because your data will contain allTags and allCategories, so you can create a general dedupeCategories (which will be named dedupeMdx) which no matter the input, will return a unique array (of tags or categories) because you don't care about the data, all MDX will have the same internal structure to the loop and the function can be agnostic to that.
Following that approach, you can omit the following filter:
context: {
category,
// Create an array of ids of articles in this category
ids: allMdx.edges
.filter(({ node }) => {
return node.frontmatter.categories.includes(category)
})
.map(({node}) => node.id),
},
The filter won't be necessary if you pass the dedupedCategories array (and so with the tags) and use a filter GraphQL in the template query, which in fact is what you would do either way, so you are saving one step.
In other words, you create pages for each category (or tag), pass the array of categories via context and get, from allMdx (filtered by the key + the unique array) the needed data.
I'm making some changes to a Gatsby starter and ran into an annoying problem with fontawesome brand icons. The problem is that unlike most icons, that are called faPhone or something like that, the brand icons also need a prefix ,fab.
So I can make them work in the body of my posts, like this: <Icon icon={['fab', 'github']} />
The problem is when I wanna include it on the frontmatter metadata like:
---
category: 'socials'
title: 'Github'
icon: 'fab github'
content: 'janedoegithub'
---
I have tried passing it like the above example, as 2 separate strings, as a list, and nothing seems to work. Does anyone know how to solve this?
Edit: both of these formats also don't work
---
category: 'socials'
title: 'Github'
icon:
- fab
- github
content: 'janedoegithub'
---
---
category: 'socials'
title: 'Github'
icon: ['fab', 'github']
content: 'janedoegithub'
---
when I try them I get this error:
GRAPHQL
There was an error in your GraphQL query:
Cannot query field "icon" on type "MdxFrontmatter".
If you don't expect "icon" to exist on the type "MdxFrontmatter" it is most
likely a typo.
However, if you expect "icon" to exist there are a couple of solutions to common
problems:
- If you added a new data source and/or changed something inside
gatsby-node.js/gatsby-config.js, please try a restart of your development server
- The field might be accessible in another subfield, please try your query in
GraphiQL and use the GraphiQL explorer to see which fields you can query and
what shape they have
- You want to optionally use your field "icon" and right now it is not used
anywhere. Therefore Gatsby can't infer the type and add it to the GraphQL
schema. A quick fix is to add at least one entry with that field ("dummy
content")
It is recommended to explicitly type your GraphQL schema if you want to use
optional fields. This way you don't have to add the mentioned "dummy content".
Visit our docs to learn how you can define the schema for "MdxFrontmatter":
https://www.gatsbyjs.org/docs/schema-customization/#creating-type-definitions
File: src/components/ContactInfo/index.js:25:15
this is the index.js mentioned in the error:
import React from 'react';
import { useStaticQuery, graphql } from 'gatsby';
import InfoBlock from 'components/ui/InfoBlock';
import Container from 'components/ui/Container';
import TitleSection from 'components/ui/TitleSection';
import * as Styled from './styles';
const ConctactInfo = () => {
const { mdx, allMdx } = useStaticQuery(graphql`
query {
mdx(frontmatter: { category: { eq: "socials section" } }) {
frontmatter {
title
subtitle
}
}
allMdx(filter: { frontmatter: { category: { eq: "socials" } } }, sort: { fields: fileAbsolutePath }) {
edges {
node {
id
frontmatter {
title
icon
content
}
}
}
}
}
`);
const sectionTitle = mdx.frontmatter;
const socials = allMdx.edges;
return (
<Container section>
<TitleSection title={sectionTitle.title} subtitle={sectionTitle.subtitle} center />
{socials.map((item) => {
const {
id,
frontmatter: { title, icon, content }
} = item.node;
return (
<Styled.ContactInfoItem key={id}>
<InfoBlock icon={icon} title={title} content={content} center />
</Styled.ContactInfoItem>
);
})}
</Container>
);
};
export default ConctactInfo;
As far as I understand your issue, you want to pass the icon data field from the markdown to the template/component. In that case, you can use:
---
category: 'socials'
title: 'Github'
icon:
- fab
- github
content: 'janedoegithub'
---
Note: be careful with the indentation
Or:
---
category: 'socials'
title: 'Github'
icon: ['fab', 'github']
content: 'janedoegithub'
---
You can store arrays in markdown files both ways.
Then, once your GraphQL query have retrieved and filtered your results, your component may look like:
<Icon icon={markdownData.icon} />
Pretty much follow this post Adding tags to Gatsby Contentful blog to a T but still can't find a reason why it's not working...
import React from "react"
import { graphql } from "gatsby"
export const query = graphql`
query($slug: String!, $tag: [String!]) {
contentfulBrunchPost(slug: { eq: $slug }, tag: { in: $tag }) {
title
slug
tag
}
}
`
const TagPost = props => {
return <h1>{props.data.contentfulBrunchPost.title}</h1>
}
export default TagPost
I get the error "Cannot read property 'contentfulBrunchPost' of undefined". Very new to graphiQL in contentful and gatsby so point in the right direction would be awesome
I suggest going to your local version of Graphiql at http://localhost:8000/___graphql and copying your exact query over. Does the query return data over there? If not, then your query may be incorrect. If it does return data, then you can drill down into it to determine the correct reference to use in your code.
Extending from:
appreciate the help! I get the error "message": "Variable "$slug" of
required type "String!" was not provided." any point in right
direction? –
You've set the slug field as non-nullable (like required field) because of the exclamation mark (!). This means that must be provided from the gatsby-node.js file to the template (TagPost) so ensure that the field is always present or remove the nullability of the field to make it nullable:
export const query = graphql`
query($slug: String, $tag: [String!]) {
contentfulBrunchPost(slug: { eq: $slug }, tag: { in: $tag }) {
title
slug
tag
}
}
Your createPage function should look like:
createPage({
path: `/tag/${slugifiedTag}`,
component: path.resolve("./src/templates/tag-post.tsx"), // your tagComponent
context: {
slug: edge.node.slug,
tag: edges.node.tag
},
})
I'm using Gatsby and MarkdownRemark.
I want to query the markdown files and then filter them down to the files contained within a sub-directory. My folder structure looks like this:
- src
- pages
-index.md
-about.md
- team
- team_member_1.md
- team_member_2.md
- team_member_3.md
So far I can query all the markdown pages in the directory but I'm having trouble trying to filter down path. There must be a way to do it with a graphQL query.
Instead what I do is map all the results and then check if the slug string includes 'team' this tells me that its in the 'team' folder. And then it makes the component.
import React from 'react'
import { useStaticQuery, graphql } from 'gatsby'
import TeamMember from '../components/TeamMember.js'
const Team = () => {
const data = useStaticQuery(graphql`
query {
allMarkdownRemark {
edges {
node {
fields{
slug
}
frontmatter {
name
position
image
}
}
}
}
}
`)
return (
<div>
{data.allMarkdownRemark.edges.map( (item, index) => {
if(item.node.fields.slug.includes('team')){
return <TeamMember key={`team_member_${index}`}{...item.node.frontmatter}/>
}
} )}
</div>
)
}
export default Team
This works fine. But I thought the whole point of graphQl is to query and filter to return the exact data I need. Instead I'm back at writing my own filter code in javascript:
if(item.node.fields.slug.includes('team'))
Is there a Gatsby plugin or a way to filter a query to contain items in a folder?
Have a look at the gatsby graphql reference for filter and make sure you are using graphiql to explore what is available within your schema.
query MyQuery {
allMarkdownRemark(filter: {fileAbsolutePath: {regex: "/(team)/" }}) {
nodes {
id
}
}
}
#ksav's answer works but it is important to note that regex: "/(team)/" also matches C:\user\gatsby\src\team2\other.md.
So I recommend using allMarkdownRemark(filter: {fileAbsolutePath: {regex: "/(/team/)/" }}) { instead.
So I've written a blog site in Gatsby and Remark. I've structured my posts like this:
Library/
-- category-name/
---- article-name/
------ index.md
This has worked really well and results in me being able to make paths like /category-name/article-name.
What I also would like to do is to be able to drop an image in there called 'hero.jpg' and for it to be automatically picked up by Gatsby without having to add a frontmatter reference to it.
I've managed to get so far by adding the following to 'gatsby-node.js':
const hero = (fs.existsSync(path.resolve(__dirname, `src/library/pages/${slug}hero.jpg`))) ? `${slug}hero.jpg` : ''
createNodeField({
node,
name: 'hero',
value: hero,
})
This works as far as the graphql data goes and I now see the following:
{
"node": {
"id": "3190922a-2207-5621-a7be-e02be9e8da24",
"fields": {
"hero": "/category-name/article-name/hero.jpg"
},
},
However on the actual page, the image link /category-name/article-name/hero.jpg doesn't exist so I get a dead image. I know this is because my image path is being transformed by gatsby-transformer-sharp but I don't know how to work out what it is being transformed to.
I believe I need to do something akin to the answers on this SO question but that seems to expect you to know that the relativePath is at the time you are writing your query but I won't have that information until after the query has returned the first time.
OnCreateNode hook added for claarity
exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField } = actions
// Add slug to MarkdownRemark node
if (node.internal.type === 'MarkdownRemark') {
const slug = createFilePath({ node, getNode, basePath: 'library' })
const hero = (fs.existsSync(path.resolve(__dirname, `src/library/pages/${slug}hero.jpg`))) ? './hero.jpg' : ''
createNodeField({
node,
name: 'slug',
value: slug,
})
createNodeField({
node,
name: 'hero',
value: hero,
})
}
}
I realized my previous answer was incorrect & overly complicated (It relies on node creation order, also there's no need to add fields to imageSharp nodes. Here's the link if someone's interested.). Here's the better answer:
Querying image of hero name in the same folder with mardown
Since the hero image is always at the same directory as the markdown file, we can simply query it based on its directory.
dir name ext
┌────────────┴────────────┐ ┌─┴─┐ ┌─┴─┐
absolute/path/to/directory/ hero .png
absolute/path/to/directory/ index .md
Graphql query:
file ( dir: { eq: "absolute/path/to/directory" }, name: { "hero" } ) {
childImageSharp {
fixed {
src
}
}
}
The only modification you need to make to your gatsby-node.js is to add this dir field to your page's context so we can use it as a variable.
We can get this dir by doing path.parse(node.fileAbsolutePath).dir, or get the dir field from remark's parent node getNode(node.parent.id).dir
+ const { dir } = getNode(node.parent.id)
createPage({
path: node.fields.slug,
component,
context: {
+ dir,
slug: node.fields.slug,
},
})
And query it like so:
export const pageQuery = graphql`
- query ArticleByPath($slug: String!) {
+ query ArticleByPath($slug: String!, $dir: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
id
htmlAst
frontmatter {
title
}
}
+ file (dir: { eq: $dir }, name: { eq: "hero" }) {
+ childImageSharp {
+ fixed {
+ src
+ }
+ }
+ }
}
`
And use it:
export default function Template({ data }) {
const post = data.markdownRemark
+ const hero = data.file ? data.file.childImageSharp : null
return (
<div className="landing-page-container">
<Helmet title={`Your Blog Name - ${post.frontmatter.title}`} />
<div className="blog-post">
+ {hero && <img src={hero.fixed.src} alt={post.frontmatter.title} />}
<h1>{post.frontmatter.title}</h1>
<div className="blog-post-content">{renderAst(post.htmlAst)}</div>
</div>
</div>
)
}
Here's the gist for article.js and gatsby-node.js.