I am trying to create a gatsby site where I have projects posted, each with tags. I followed a basic tutorial on creating a gatsby blog, and am now working through gatsby's tags as documented on their website. Along the way while changing variable names from path to slug to avoid reserved words, I seem to have messed up because I can no longer create blogpost pages, nor tag pages. Each post is enclosed in a folder named by the post title, holding an index.md file defining its content, date, tag etc.
The following error is output when I try to run gatsby develop,
ERROR #11323
Your site's "gatsby-node.js" must set the page path when creating a page.
The page object passed to createPage:
{
"slug": "/an-example-post",
"component": "C:\\Users\\Monolith\\Documents\\programming\\webdev\\Gatsby-Portfolio\\src\\templates\\post.js",
"context": {
"slug": "/an-example-post"
}
}
My gatsby-node.js
const path = require('path');
exports.createPages = ({actions, graphql}) => {
const {createPage} = actions;
const postTemplate = path.resolve('src/templates/post.js');
const tagTemplate = path.resolve("src/templates/tags.js")
//const postTemplate = require.resolve('src/templates/post.js');
//const tagTemplate = require.resolve("src/templates/tags.js")
return graphql(`{
allMarkdownRemark{
edges{
node {
html
id
frontmatter{
slug
title
}
}
}
}
tagsGroup: allMarkdownRemark(limit: 2000) {
group(field: frontmatter___tags) {
fieldValue
}
}
}`)
.then(res => {
if(res.errors){
return Promise.reject(res.errors);
}
res.data.allMarkdownRemark.edges.forEach( ({node}) => {
createPage({
slug: node.frontmatter.slug,
component: postTemplate,
context: {
slug:node.frontmatter.slug
},
})
})
// Extract tag data from query
const tags = res.data.tagsGroup.group
// Make tag pages
tags.forEach(tag => {
createPage({
// path: `/tags/${_.kebabCase(tag.fieldValue)}/`,
slug: `/tags/${(tag.fieldValue)}/`,
component: tagTemplate,
context: {
tag: tag.fieldValue,
},
})
})
})
}
As previously mentioned I was concerned the path variable being a reserved word could be an issue, but I got further using it than omitting it so for now it has stayed.
Example post.js
import React from 'react';
import { graphql } from 'gatsby';
import Layout from "../components/layout"
export default function Template({data}) {
const {markdownRemark: post} = data;
return(
<Layout>
<div>
<h1 className="postTitle">{post.frontmatter.title}</h1>
<div className="tagGroup">
{post.frontmatter.tags.map((tag, index) => (
<div key={index}>
<h2 className = "tagStyle">{tag}</h2>
</div>
))}
</div>
<p>{post.frontmatter.description}</p>
<p>{post.frontmatter.date}</p>
{/* <h2 className="tagStyle">{post.frontmatter.tags + " "}</h2> */}
<div dangerouslySetInnerHTML={{__html: post.html}} />
</div>
</Layout>
)
}
export const postQuery = graphql`
#query BlogPostByPath($slug: String!) {
query($slug: String!) {
markdownRemark(frontmatter: { slug: {eq: $slug} }) {
html
frontmatter {
slug
title
description
date
tags
}
}
}
`
The tags.js is very similar to gatsby's default with slight alterations for content. Here is the Graphql query I am using.
Tags.propTypes = {
pageContext: PropTypes.shape({
tag: PropTypes.string.isRequired,
}),
data: PropTypes.shape({
allMarkdownRemark: PropTypes.shape({
totalCount: PropTypes.number.isRequired,
edges: PropTypes.arrayOf(
PropTypes.shape({
node: PropTypes.shape({
frontmatter: PropTypes.shape({
title: PropTypes.string.isRequired,
// slug: PropTypes.string.isRequired,
}),
fields: PropTypes.shape({
slug: PropTypes.string.isRequired,
}),
}),
}).isRequired
),
}),
}),
}
export default Tags
export const pageQuery = graphql`
query($tag: String) {
allMarkdownRemark(
limit: 2000
sort: { fields: [frontmatter___date], order: DESC }
filter: { frontmatter: { tags: { in: [$tag] } } }
) {
totalCount
edges {
node {
fields {
slug
}
frontmatter {
title
}
}
}
}
}
`
If anyone has any information to help point me in the right direction I would greatly appreciate it. I have been running through gatsby's documentation for hours and have yet to make substantial progress.
You are passing a tag in your context but expecting a slug:
createPage({
slug: `/tags/${(tag.fieldValue)}/`,
component: tagTemplate,
context: {
tag: tag.fieldValue,
},
})
And:
query($slug: String!) {}
The context is a way to pass data to your component template to use it to filter your data. Ideally should be a unique parameter (like a slug, id, etc). In that way, in your gatsby-node.js you should fetch all your posts, pass a unique field by context, and use that variable in your template to get all your needed data of each post.
In addition, you must provide a path, not a slug to your function.
You should change your createPage function to:
createPage({
path: `/tags/${tag.fieldValue}/`,
component: tagTemplate,
context: {
slug: tag.fieldValue,
},
})
Keep in mind that fieldValue should be a slug (or kind of) despite having different naming.
I would recommmend reading Gatsby's tutorial: https://www.gatsbyjs.com/tutorial/part-seven/
Related
Hello Everyone
I'm having a problem within my Typescript code, with my Next.js App.
i cannot find the right type to use with JSX rendering icons, that's fetched from database,
First part of code:
import * as bsIcons from 'react-icons/bs';
import * as faIcons from 'react-icons/fa';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import { IconType } from 'react-icons/lib';
type option = {
id: number,
opt: string,
url: string,
logo: any,
sub: string[]
}
export const getStaticProps: GetStaticProps<{ options: option[] }> = async () => {
const res = await fetch('http://localhost:3000/api/menu')
const options: option[] = await res.json()
return {
props: {
options
}
}
}
here i had imported the icons libs from react-icons/bs & react-icons/fa then i defined the type of the response option
then i used the Nextjs getStaticProps function to import menu items from the api.
Second part of code:
<ul>
{ options.map(elm => {
let iconList = elm.logo.split('.')[0]
let menuIcon:IconType = elm.logo.split('.')[1]
let Logo: Function;
if(iconList == 'bsIcons'){
Logo = bsIcons[menuIcon]
} else {
Logo = faIcons[menuIcon]
}
return (
<Link key={elm.id} href={`${elm.url}`} style={{ textDecoration: 'none', color: 'powderblue' }}>
<li className={styles.option}>
<Logo style={{padding: '0 .5rem'}}/>
<p>{elm.opt}</p>
</li>
</Link>
)
})}
</ul>
in this part i'm trying to render the fetched data, according to my code, it's working perfectly, but at the VS Code it shows a typescript error at the if statement,
Error:
let menuIcon: IconType
Type 'IconType' cannot be used as an index type.ts(2538)
i don't have any clue of the type i must use at this part, so i imported the IconType from the icons lib it self, but still i cannot fix it.
is there any suggestions for this ?
a Part of fetched data:
{
id: 1,
opt: 'Dashboard',
url: '/dash',
logo: `faIcons.FaGripHorizontal`,
sub: []
},
{
id: 2,
opt: 'Content management',
url: '/content',
logo: `faIcons.FaWarehouse`,
sub: []
},
}
My Next.js project is failing to build on Vercel.
I have a BlogAuthor.tsx file as so:
import Avatar from "../../Common/Avatar/Avatar";
import CopyLink from "../../Common/Widgets/CopyLink/CopyLink";
export default function BlogAuthor({
authorName,
authorAvatar,
postDate,
url,
}) {
return (
<div className="flex w-full lg:px-2">
<Avatar image={authorAvatar} alt={authorName} />
<div className="ml-3 flex flex-col">
<span className="font-semibold">{authorName}</span>
<time className="text-gray-500" dateTime={postDate}>
{new Date(postDate).toDateString()}
</time>
</div>
<div className="ml-auto">
<CopyLink url={url} />
</div>
</div>
);
}
During the build, on Vercel I get the following error:
> next build
info - Checking validity of types...
Failed to compile.
./components/Blog/BlogAuthor/BlogAuthor.tsx:1:20
Type error: Cannot find module '../../Common/Avatar/Avatar' or its corresponding type declarations.
> 1 | import Avatar from "../../Common/Avatar/Avatar";
| ^
2 | import CopyLink from "../../Common/Widgets/CopyLink/CopyLink";
3 |
4 | export default function BlogAuthor({
Error: Command "npm run build" exited with 1
The blog author is used within a BlogLayout component. Here is an example of a blog post file:
how-to-value.mdx:
---
slug: "how-to-value"
title: "Title"
description: "Meta description goes here"
coverImage:
src: "/img/blog/how-to-value/cover.jpg"
alt: "Image alt text"
date: "May 11 2022 12:00:00 AM"
author:
name: "squishyboots"
avatar: "/img/blog/authors/squishyboots.jpeg"
---
//ALL MARKDOWN STUFF GOES HERE
<BlogLink href="/signup" title="Sign up today!" text="Sign up" />
export default ({ children }) => (
<BlogLayout meta={meta}>{children}</BlogLayout>
);
And finally, here is what my [slug].tsx page looks like:
import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import BlogImage from "#components/Blog/BlogImage/BlogImage";
import BlogLayout from "#components/layouts/BlogLayout/BlogLayout";
import BlogLink from "#components/Blog/BlogLink/BlogLink";
import { HeadOptions } from "#interfaces/interfaces";
const components = {
BlogImage,
BlogLayout,
BlogLink,
};
const BlogPostPage = ({ data, mdxSource, slug }) => {
const headOptions: HeadOptions = {
title: data.title,
description: data.description,
url: slug,
image: data.coverImage,
articleAuthor: data.author,
date: data.date,
};
return (
<BlogLayout meta={headOptions}>
<MDXRemote {...mdxSource} components={components} />
</BlogLayout>
);
};
export async function getStaticPaths() {
const files = fs.readdirSync(path.join("_blog-posts"));
const paths = files.map((filename) => ({
params: {
slug: filename.replace(".mdx", ""),
},
}));
return {
paths,
fallback: false,
};
}
export async function getStaticProps({ params: { slug } }) {
const markdownWithMeta = fs.readFileSync(
path.join("_blog-posts", slug + ".mdx"),
"utf-8"
);
const { data: frontMatter, content } = matter(markdownWithMeta);
const mdxSource = await serialize(content);
return {
props: {
data: frontMatter,
slug,
mdxSource,
},
};
}
export default BlogPostPage;
Therefore, as you can see above I have a components object that's passed into MDXRemote. It contains components such as BlogLink, BlogLayout etc.
Do I have to add Avatar.tsx to this list as well? It is not used directly in an .mdx file.
If so, do I have to include every single component used in this list, even though its just a child of another component? (In this case, BlogAuthor.tsx)
If this is the case, won't I just have a giant list of components that need adding to every time I use something new in a blog post? That doesn't seem right to me
I'm a beginner on Gatsby. I try to display a batch of images in grid from a specific folder. I find a script to do that, but that's catch all pics from my data folder and my purpose is target a specific one. I try a script but that's don't work with allImageSharp when I try to filter this one allImageSharp(filter: { sourceInstanceName: { eq: "media" } }) {.
When I try allImageSharp(filter: { id: { regex: "/media/" } }) { just display a blank window, but that's work fine like that allImageSharp {
javascript
import React from "react"
import { graphql } from "gatsby"
import Img from "gatsby-image"
import Layout from "../components/layout"
const img_grid_style = {
display: "grid",
gridTemplateColumns: `repeat(auto-fill, 200px)`,
}
export default ({ data }) => (
<div>
<Layout title="IMAGE GRID"></Layout>
<div style={img_grid_style}>
{data.allImageSharp.edges.map(edge => (
<Img fluid={edge.node.fluid} />
))}
</div>
</div>
)
export const query = graphql`
query {
allImageSharp(filter: { sourceInstanceName: { eq: "media" } }) {
edges {
node {
id
fluid(maxWidth: 200, maxHeight: 200) {
...GatsbyImageSharpFluid
}
}
}
}
}
`
config
module.exports = {
plugins: [
`gatsby-transformer-sharp`,
{
resolve: `gatsby-plugin-sharp`,
options: {
// Available options and their defaults:
base64Width: 20,
// forceBase64Format: ``, // valid formats: png,jpg,webp // don't work on OSX
useMozJpeg: process.env.GATSBY_JPEG_ENCODER === `MOZJPEG`,
stripMetadata: true,
defaultQuality: 50,
failOnError: true,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `media`,
path: `./media/`,
},
},
],
}
Change your path in your gatsby-source-filesystem to:
{
resolve: `gatsby-source-filesystem`,
options: {
name: `media`,
path: `${__dirname}/media/`,
},
},
The snippet above will match. rootOfYourProject/media, change it to your media path keeping the ${__dirname}.
Now, your filesystem can be filtered by media (without slashes).
export const query = graphql`
{
allFile(filter: {sourceInstanceName: {eq: "media"}}) {
nodes {
childImageSharp {
fluid {
base64
tracedSVG
srcWebp
srcSetWebp
originalImg
originalName
}
}
}
}
}
`
Since you can't use fragments in the GraphQL playground (localhost:8000/___graphql) I've used the extended version, however, you should use the ...GatsbyImageSharpFluid once applied to your code.
allFile or allImageSharp should do the trick.
I've fixed your project in this PR. The query was retrieving properly the results, however, you were missing some nested objects to get the final fluid image:
export default ({ data }) => {
return <div>
<Layout title="IMAGE GRID FROM SPECIFIC FOLDER"></Layout>
<div style={img_grid_style}>
{data.allFile.edges.map(({ node }) => (
<Img fluid={node.childImageSharp.fluid} />
))}
</div>
</div>
}
Background: I've built a standard single file component that takes a name prop and looks in different places my app's directory structure and provides the first matched component with that name. It was created to allow for "child theming" in my Vue.js CMS, called Resto. It's a similar principle to how WordPress looks for template files, first by checking the Child theme location, then reverting to the parent them if not found, etc.
Usage : The component can be used like this:
<!-- Find the PageHeader component
in the current child theme, parent theme,
or base components folder --->
<theme-component name="PageHeader">
<h1>Maybe I'm a slot for the page title!</h1>
</theme-component>
My goal : I want to convert to a functional component so it doesn't affect my app's render performance or show up in the Vue devtools. It looks like this:
<template>
<component
:is="dynamicComponent"
v-if="dynamicComponent"
v-bind="{ ...$attrs, ...$props }"
v-on="$listeners"
#hook:mounted="$emit('mounted')"
>
<slot />
</component>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'ThemeComponent',
props: {
name: {
type: String,
required: true,
default: '',
},
},
data() {
return {
dynamicComponent: null,
resolvedPath: '',
}
},
computed: {
...mapGetters('site', ['getThemeName']),
customThemeLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying custom theme component for ${this.customThemePath}`)
return () => import(`#themes/${this.customThemePath}`)
},
defaultThemeLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying default component for ${this.name}`)
return () => import(`#restoBaseTheme/${this.componentPath}`)
},
baseComponentLoader() {
if (!this.name.length) {
return null
}
// console.log(`Trying base component for ${this.name}`)
return () => import(`#components/Base/${this.name}`)
},
componentPath() {
return `components/${this.name}`
}, // componentPath
customThemePath() {
return `${this.getThemeName}/${this.componentPath}`
}, // customThemePath()
},
mounted() {
this.customThemeLoader()
.then(() => {
// If found in the current custom Theme dir, load from there
this.dynamicComponent = () => this.customThemeLoader()
this.resolvedPath = `#themes/${this.customThemePath}`
})
.catch(() => {
this.defaultThemeLoader()
.then(() => {
// If found in the default Theme dir, load from there
this.dynamicComponent = () => this.defaultThemeLoader()
this.resolvedPath = `#restoBaseTheme/${this.defaultThemePath}`
})
.catch(() => {
this.baseComponentLoader()
.then(() => {
// Finally, if it can't be found, try the Base folder
this.dynamicComponent = () => this.baseComponentLoader()
this.resolvedPath = `#components/Base/${this.name}`
})
.catch(() => {
// If found in the /components dir, load from there
this.dynamicComponent = () => import(`#components/${this.name}`)
this.resolvedPath = `#components/${this.name}`
})
})
})
},
}
</script>
I've tried SO many different approaches but I'm fairly new to functional components and render functions (never got into React).
The roadblock : I can't seem to figure out how to run the chained functions that I call in my original mounted() function. I've tried running it from inside the render function with no success.
Big Question
How can I find and dynamically import the component I'm targeting before I pass that component to the createElement function (or within my single file <template functional><template/>)?
Thanks all you Vue-heads! ✌️
Update: I stumbled across this solution for using the h() render function and randomly loading a component, but I'm not sure how to make it work to accept the name prop...
Late to the party, but I was in a similar situation, where I had a component in charge of conditionally render one of 11 different child components:
<template>
<v-row>
<v-col>
<custom-title v-if="type === 'title'" :data="data" />
<custom-paragraph v-else-if="type === 'paragraph'" :data="data" />
<custom-text v-else-if="type === 'text'" :data="data" />
... 8 more times
</v-col>
</v-row>
</template>
<script>
export default {
name: 'ProjectDynamicFormFieldDetail',
components: {
CustomTitle: () => import('#/modules/path/to/CustomTitle'),
CustomParagraph: () => import('#/modules/path/to/CustomParagraph'),
CustomText: () => import('#/modules/path/to/CustomText'),
... 8 more times
},
props: {
type: {
type: String,
required: true,
},
data: {
type: Object,
default: null,
}
},
}
</script>
which of course is not ideal and pretty ugly.
The functional equivalent I came up with is the following
import Vue from 'vue'
export default {
functional: true,
props: { type: { type: String, required: true }, data: { type: Object, default: null } },
render(createElement, { props: { type, data } } ) {
// prop 'type' === ['Title', 'Paragraph', 'Text', etc]
const element = `Custom${type}`
// register the custom component globally
Vue.component(element, require(`#/modules/path/to/${element}`).default)
return createElement(element, { props: { data } })
}
}
Couple of things:
lazy imports don't seem to work inside Vue.component, hence require().default is the way to go
in this case the prop 'type' needs to be formatted, either in the parent component or right here
I'm trying to show a text preview for a list of posts. I have a list of posts that are rendered in the home blog page, each post only shows title, author and an anchor tag as a link to a view that shows a single post with all the data that contains the selected post. Now in the home blog page I need to show a text preview of the post in addition to title and author but I don't have any idea of how to grab a part of the post content and show it as a preview.
I'm a beginner with Meteor and React. If someone could give me some advise, it would be great. Here is the code:
Collections.jsx
export const Posts = new Mongo.Collection('Posts');
export const PostSchema = Astro.Class({
name: 'PostSchema',
collection: Posts,
fields: {
title: {
validator: Validators.and([
// more validators...
]),
},
content: { // I need to show a preview of this content
validator : Validators.and([
Validators.required(),
Validators.string(),
Validators.minLength(3)
])
},
author: {
validator: Validators.and([
// more validators
])
},
category: {
validator: Validators.and([
// more validators
])
},
tags: {
validator: Validators.and([
// more validators
])
},
}
});
Here is where I show the posts list.
Blog.jsx
// other imports
import { Posts } from '../../lib/collections.jsx';
class Blog extends Component {
render() {
let content = (
<div>
<h1>Posts</h1>
{this.props.posts.map((post, idx) => (
<Post key={idx} post={post} />
))}
</div>);
if (!this.props.ready) {
content = <p>LOADING...</p>;
}
return content;
}
}
Blog.propTypes = {
ready: React.PropTypes.bool.isRequired,
posts: React.PropTypes.array.isRequired,
};
export default createContainer((params) => {
const handle = Meteor.subscribe('posts');
let posts = Posts.find({}, { sort: { createdAt: -1 } }).fetch();
if (params.category) {
posts = Posts.find({ category: params.category },
{ sort: { createdAt: -1 } }).fetch();
}
return {
ready: handle.ready(),
posts,
};
}, Blog);
/**
* POST
* Here is where I don't know how to show the text preview
*/
const Post = (props) => {
const {
title,
author,
slug,
category,
content,
} = props.post;
return (
<div>
<article className="new-resolution">
<p>Title:{title}</p>
<p>Author:{author}</p>
<p>{content}</p>
<a href={`/blog/kategorie/${category}/${slug}`}>
Weiter lesen...
</a>
</article>
</div>
);
};
Post.propTypes = {
post: PropTypes.object.isRequired,
};
Since I din't have to much time to resolve this in a much fancy way, I just decide to add a new field in my collection named "preview". In the form post I added a new text area with a limit of characters and this would be displayed in the Post.jsx <p>Preview:{preview}</p>.