I'm using Strapi as a Headless CMS and building my frontend with Gatsby + Graphql. I have a "blocks renderer" component that is rendering any of the dynamic zones in strapi.
import React from "react"
import { graphql } from "gatsby"
import BlockHero from "./block-hero"
import BlockParagraph from "./block-paragraph"
import BlockSplitFeature from "./block-split-feature"
const componentsMap = {
// STRAPI__COMPONENT_LAYOUT_ELEMENTS_MULTIPLE_CALLOUT: blockMultipleCallout,
STRAPI__COMPONENT_LAYOUT_ELEMENTS_SIMPLE_PARAGRAPH: BlockParagraph,
STRAPI__COMPONENT_LAYOUT_ELEMENTS_SPLIT_FEATURE: BlockSplitFeature,
STRAPI__COMPONENT_MEDIA_ELEMENT_HERO: BlockHero,
// STRAPI__COMPONENT_META_DATA_DEFAULT_SEO: blockSeo
}
const Block = ({ block }) => {
const Component = componentsMap[block.__typename]
if(!Component) {
return null
}
return <Component data={block} />
}
const BlocksRenderer = ({ blocks }) => {
return (
<div>
{blocks.map((block, index) => (
<Block key={`${index}${block.__typename}`} block={block} />
))}
</div>
)
}
export const query = graphql`
fragment Blocks on STRAPI__COMPONENT_LAYOUT_ELEMENTS_CTASTRAPI__COMPONENT_LAYOUT_ELEMENTS_MULTIPLE_CALLOUTSTRAPI__COMPONENT_LAYOUT_ELEMENTS_SIMPLE_PARAGRAPHSTRAPI__COMPONENT_LAYOUT_ELEMENTS_SPLIT_FEATURESTRAPI__COMPONENT_MEDIA_ELEMENT_HEROUnion {
__typename
... on STRAPI__COMPONENT_LAYOUT_ELEMENTS_MULTIPLE_CALLOUT {
id
MultipleCalloutItem {
id
Heading
Description
}
}
... on STRAPI__COMPONENT_LAYOUT_ELEMENTS_SIMPLE_PARAGRAPH {
id
Text
}
... on STRAPI__COMPONENT_LAYOUT_ELEMENTS_SPLIT_FEATURE {
id
Heading
Description
mediaAlignment
Media {
id
mime
localFile {
childImageSharp {
gatsbyImageData
}
}
alternativeText
}
}
... on STRAPI__COMPONENT_MEDIA_ELEMENT_HERO {
id
Heading
Description
Media {
id
mime
alternativeText
localFile {
url
}
alternativeText
}
}
}
`
export default BlocksRenderer
Then I have my page layout file to generate a page layout (side note, the "Layout" element is just for the navigation & footer. This will be rewritten once I have this page layout file issue fixed)>
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"
import BlocksRenderer from "../components/blocks-renderer"
const PageLayout = () => {
const { allStrapiPage } = useStaticQuery(graphql`
query {
allStrapiPage {
edges {
node {
id
Name
Slug
Blocks {
...Blocks
}
}
}
}
}
`)
const { Blocks } = allStrapiPage
return (
<Layout>
<div>{allStrapiPage.id}</div>
<h1>{allStrapiPage.Name}</h1>
<BlocksRenderer blocks={allStrapiPage.Blocks} />
</Layout>
)
}
export default PageLayout
I'm dynamically creating pages with a gatsby-node.js file. When I try to access one of the dynamically created slugs, I get an error in the blocks-renderer file that says can't access property "map", blocks is undefined. Anyone have any ideas?
EDIT: Added the additional files mentioned.
gatsby-config.js file below:
/**
* Configure your Gatsby site with this file.
*
* See: https://www.gatsbyjs.com/docs/gatsby-config/
*/
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
})
module.exports = {
/* Your site config here */
plugins: [
"gatsby-plugin-gatsby-cloud",
"gatsby-plugin-postcss",
"gatsby-plugin-sass",
"gatsby-plugin-image",
"gatsby-plugin-sharp",
"gatsby-transformer-sharp",
"gatsby-transformer-remark",
{
resolve: "gatsby-source-strapi",
options: {
apiURL: process.env.STRAPI_API_URL || "http://localhost:1337",
accessToken: process.env.STRAPI_TOKEN,
collectionTypes: [
"drink",
"category",
{
singularName: "page",
queryParams: {
populate: {
Blocks: {
populate: "*",
MultipleCalloutItem: {
populate: "*",
},
},
PageMeta: {
populate: "*",
},
ParentPage: {
populate: "*",
},
},
},
},
],
singleTypes: [
{
singularName: "global",
queryParams: {
populate: {
DefaultSeo: {
populate: "*",
},
Favicon: {
populate: "*",
},
},
},
},
{
singularName: "homepage",
queryParams: {
populate: {
Blocks: {
populate: "*",
},
},
},
},
],
queryLimit: 1000,
}
},
],
}
home.js (which works as intended).
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"
import BlocksRenderer from "../components/blocks-renderer"
const HomePage = () => {
const { strapiHomepage } = useStaticQuery(graphql`
query {
strapiHomepage {
Blocks {
...Blocks
}
}
}
`)
const { Blocks } = strapiHomepage
// const seo = {
// metaTitle: title,
// metaDescription: title
// }
return (
<Layout>
<BlocksRenderer blocks={Blocks} />
</Layout>
)
}
export default HomePage
This is the gatsby-node.js file I'm using to generate the pages with the page-layout.js file. Note that I can generate the pages and content, minus the Blocks query.
const path = require('path')
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const result = await graphql(
`
query {
allStrapiPage {
edges {
node {
Slug
Name
ParentPage {
Slug
}
}
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL Query`)
return
}
const pageTemplate = path.resolve(`./src/layouts/page-layout.js`)
result.data.allStrapiPage.edges.forEach(({ node }) => {
const path = node.Slug
createPage({
path,
component: pageTemplate,
context: {
pagePath: path
},
})
})
}
The problem is here:
<BlocksRenderer blocks={allStrapiPage.Blocks} />
You can't access directly to Blocks because you node is an array inside edges property. From what I see, the loop is done in BlocksRenderer hence you need to provide it an array of blocks. Without knowing exactly your data structure and what returns it's difficult to guess but try something like:
<BlocksRenderer blocks={allStrapiPage.edges.node[0].Blocks} />
I have a Home.js file that is using a different property
(allStrapiHomepage) and BlockRenderer is working as expected
If your Home.js query is using a page query instead of a static query they cane be triggered and hydrated in different runtimes and build times, so one can fail if the other doesn't. This leads me to think that maybe the query is ok, but the logic isn't. You can easily check it by adding a simple condition like:
<BlocksRenderer blocks={allStrapiPage?.Blocks} />
Or:
{allStrapiPage.Blocks && <BlocksRenderer blocks={allStrapiPage.Blocks} />}
Thanks to #Ferran I was pointed in the right direction and solved this issue.
Two changes needed to be made for this to work properly. First, I needed to be passing pageContext from the gatsby-node.js file. Here I'm passing the page slug to the template
const { resolve } = require('path')
const path = require('path')
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const result = await graphql(
`
query {
allStrapiPage {
edges {
node {
Slug
Name
ParentPage {
Slug
}
}
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL Query`)
return
}
const pageTemplate = path.resolve('./src/layouts/page-layout.js')
result.data.allStrapiPage.edges.forEach(edge => {
const path = `${edge.node.Slug}`
const parentPath = `${edge.node.ParentPage.Slug}`
createPage({
path,
component: pageTemplate,
context: {
Slug: edge.node.Slug
},
})
resolve()
})
}
Then in the page-layout.js file, I needed to get the pageContext from gatsby-node.js, map all of my page nodes, in the graphql query, and pass the page Slug from gatsby-node.js as a variable in the graphql query.
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"
import BlocksRenderer from "../components/blocks-renderer"
const PageLayout = ({ data, pageContext }) => {
const { Slug } = pageContext
console.log(Slug)
return (
<Layout>
{
data.allStrapiPage.nodes.map(node => {
return (
<div key={node.id}>
<h1>{node.Name}</h1>
{node.Blocks &&
<BlocksRenderer blocks={node.Blocks} />
}
</div>
)
})
}
</Layout>
)
}
export const query = graphql`
query GetPage($Slug: String) {
allStrapiPage(filter: { Slug: {in: [$Slug]} }) {
nodes {
id
Name
Blocks {
...Blocks
}
}
}
}
`
export default PageLayout
Now I can dynamically create pages with Strapi and the "blocks" I made using dynamic zones.
Related
I'm building a nextjs-application and I crossed an issue with getStaticPaths. Inside the pages-folder, I have a file called [slug].tsx which contains this code:
import { Image } from "react-datocms";
import { request } from "../lib/datocms";
import { GetStaticProps, GetStaticPaths } from "next";
export default function Page({ pageData }) {
return (
<div>
<h1>{pageData.title}</h1>
</div>
);
}
const PATHS_QUERY = `
query MyQuery {
allPages {
slug
}
}
`;
export const getStaticPaths: GetStaticPaths = async (context) => {
const slugQuery = await request({
query: PATHS_QUERY,
preview: context.preview,
});
let paths = [];
slugQuery.allPages.map((path) => paths.push(`/${path.slug}`));
return {
paths,
fallback: false,
};
};
const PAGE_QUERY = `
query MyQuery($slug: String) {
page(filter: {slug: {eq: $slug}}) {
title
slug
id
}
}
`;
export const getStaticProps: GetStaticProps = async ({ params }) => {
const page = {
query: PAGE_QUERY,
variables: { slug: params.slug },
};
return {
props: {
pageData: page,
}
};
};
This gives me the error: Objects are not valid as a React child (found: object with keys {children}). If you meant to render a collection of children, use an array instead.
I have no clue what this means, so can anyone help me out?
****** UPDATE ******
I suspect my Navbar could have something to do with this. In my components folfer, I have a nav folder with a Navbar.tsx-file which looks like this:
const Navbar = ({ topNav }) => {
const menu_items = topNav.menuItems[0];
return (
<nav>
{menu_items.topNavigationItems.map((navitem, idx) => (
<div key={navitem.text}>
<NavItem {...navitem} />
</div>
))}
</nav>
)
}
export default Navbar;
the NavItem looks like this:
const NavItem = ({ text, path, active }) => {
return (
<Link href={path.slug}>
<a>
{text}
</a>
</Link>
);
};
export default NavItem;
The way you are building your paths array inside getStaticPaths is not quite right according to the new standards. You have to "push" an object with a key of params, which then contains an object with your slug.
Rewriting your getStaticPaths function would result in the following.
export const getStaticPaths: GetStaticPaths = async (context) => {
const slugQuery = await request({
query: PATHS_QUERY,
preview: context.preview,
});
const paths = slugQuery.allPages.map(path => {params: {slug: path.slug} });
return {
paths,
fallback: false,
};
};
You can read more about the getStaticPaths function in the official documentation.
EDIT: To be more specific on the error you're getting, you are trying to render an object as a JSX element, thus generating an error. Try and find the source of that error and fix it this way.
I am trying to get a dynamic title for useMeta with composition API but it does not work.
<script setup>
import { computed } from 'vue'
import { POST } from '#/constants/blog'
import { useQuery, useResult } from "#vue/apollo-composable";
import { useRoute } from 'vue-router'
import { useMeta } from "vue-meta";
const route = useRoute();
const variables = computed(() => ({
slug: route.params.slug,
}));
const { result, loading, error } = useQuery(
POST, variables
);
const post = useResult(result, null, data => data.post.data );
const metaTitle = computed(() => ({
title: post.attributes.title,
}));
useMeta(metaTitle);
</script>
here is the response
{
"data": {
"post": {
"data": {
"id": 4,
"attributes": {
"title": "This is the post title"
}
}
}
}
}
Please help me understand what is wrong here!
Maybe It's too late to answer this question.
This module is for vue2. After many searches, I found version 3 of this module, But It's at the alpha stage, now.
I found an alternative solution that doesn't need any dependency.
Create a new file somewhere in your project directory(utils.js) and put the below code in it:
const changeMetaTags = (meta) => {
document.title = `${meta.title} - YOUR PROJECT NAME`;
// document.querySelector('meta[name="og:title"]').setAttribute("content", meta['og:title']);
// document.querySelector('meta[name="description"]').setAttribute("content", meta.description);
// document.querySelector('meta[name="og:description"]').setAttribute("content", meta['og:description']);
// document.querySelector('meta[name="keywords"]').setAttribute("content", meta.keywords);
}
export { changeMetaTags }
Caution: You have to have the above code on your index.html file.
and for your use case just import it and use:
<script setup>
import { computed } from 'vue'
import { POST } from '#/constants/blog'
import { useQuery, useResult } from "#vue/apollo-composable";
import { useRoute } from 'vue-router'
import { useMeta } from "vue-meta";
import { changeMetaTags } from "#/infrastructures/seo/utils"; // <----- this
const route = useRoute();
const variables = computed(() => ({
slug: route.params.slug,
}));
const { result, loading, error } = useQuery(
POST, variables
);
const post = useResult(result, null, data => data.post.data );
const metaTitle = computed(() => ({
title: post.attributes.title,
}));
changeMetaTags(metaTitle.value); // <---- this
</script>
I use it in the router file (router/index.js) as well
const router = createRouter({
routes: [
{
path: "/",
component: () => import("#/layouts/MainLayout.vue"),
children: [
{
path: "",
name: "Home",
meta: { // <-------- add this
title: "Home",
description:
"your description",
"og:title": `YOUR PROJECT NAME home page`,
"og:description":
"og description",
keywords:
`your, keywords`,
},
component: () => import("#/views/HomeView.vue"),
},
...
]
})
router.beforeEach((to, from) => {
changeMetaTags(to.meta); // <----- and this
...
})
Get post by ID ( slug ) from prisma when getStaticProps() before page build
So the issue is that I cannot use React hook in getStaticProps. I was going to get slug names with useRouter, then query for post by using the slug (postID), but I learned that I cannot run prisma inside of body components. Then I learned that I can use getStaticProps and getStaticPaths to query the post by its ID before build time.
How do I get N levels of slug names in getStaticProps?.
Code
/post/[...slugs].tsx
My url looks like: localhost:3000/post/postID/PostTitle
such as localhost:3000/post/b513-ad29e3cc67d9/Post%20Title
import { Post, PrismaClient } from '#prisma/client';
import { GetStaticPaths, GetStaticProps } from 'next';
import { useRouter } from 'next/router';
type postByIdProps = {
postById: Post
}
export default function PostDetail({postById}: postByIdProps) {
return (
<>
<div>
{postById.title}
</div>
</>
);
}
export const getStaticProps = async(context: any)=>{
// I can't ues React Hook here, but I don't know how to get slug name without the hook.
const router = useRouter();
const slugs: any = router.query.slugs;
const postId = slugs?.[0].toString()
//Prisma
const prisma = new PrismaClient()
const postById = prisma.post.findUnique({
where: {
postId: postId,
},
})
return postById
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
return {
paths: [], //indicates that no page needs be created at build time
fallback: 'blocking' //indicates the type of fallback
}
}
This worked fro me, but if someone can improve this code, more than welcome.
How to Build a Fullstack App with Next.js, Prisma, and PostgreSQL
code
import { Post } from '#prisma/client';
import { GetStaticPaths, GetStaticProps } from 'next';
import prisma from '../api/prisma';
type postByIdProps = {
post: Post
}
export default function PostDetail({post}: postByIdProps) {
console.log("Post here,", post)
return (
<>
<div>
{post.title}
</div>
</>
);
}
export const getStaticProps = async({params}: any)=>{
const postId = params.slugs[0] //gets post's ID
const post = await prisma.post.findUnique({
where:{
postId: String(postId)
},
})
return {
props:{
post
}
}
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
return {
paths: [], //indicates that no page needs be created at build time
fallback: 'blocking' //indicates the type of fallback
}
}
I tried to implement a SSG application in Amplify with Next.js following this tutorial
pages/index.tsx worked well. But pages/posts/[id].tsx doesn't work and showed the following error.
Server Error
Error: No current user
This error happened while generating the page. Any console logs will be displayed in the terminal window.
And also the console showed this error.
GET http://localhost:3000/posts/da53d1f3-775f-49c4-a311-d311f2e5623f 500 (Internal Server Error)
My code in pages/posts/[id].tsx is following.
May I have your idea how to resolve this error?
import { Amplify, API, withSSRContext } from "aws-amplify";
import Head from "next/head";
import { useRouter } from "next/router";
import awsExports from "../../aws-exports";
import { getPost, listPosts } from "../../graphql/queries";
Amplify.configure({ ...awsExports, ssr: true });
export async function getStaticPaths() {
const SSR = withSSRContext();
const { data } = await SSR.API.graphql({ query: listPosts });
const paths = data.listPosts.items.map((post: any) => ({
params: { id: post.id },
}));
return {
fallback: true,
paths,
};
}
export async function getStaticProps({ params }: { params: any }) {
const SSR = withSSRContext();
const { data } = await SSR.API.graphql({
query: getPost,
variables: {
id: params.id,
},
});
return {
props: {
post: data.getPost,
},
};
}
export default function Post({ post }: { post: any }) {
const router = useRouter();
if (router.isFallback) {
return (
<div>
<h1>Loading…</h1>
</div>
);
}
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
I'm trying to generate Gatsby pages based on data in my CMS (Sanity).
I have created three pages in my CMS under the umbrella term "Second page", I use createPage in gatsby-node to generetate pages with the correct slugs.
All the pages are generated according to their slug from the CMS, but in my template component I cannot filter out data. I get the result for all three pages when I only need the result for the one page that matches the slug. My console.log in secondPage.js shows three arrays corresponding to the three items in my CMS
gatsby-node.js
// Create pages for docs
exports.createPages = ({ actions, graphql }) => {
const path = require(`path`);
const { createPage } = actions;
//const docTemplate = path.resolve("src/templates/docTemplate.js");
const secondPageTemplate = path.resolve("src/templates/secondPage.js");
return graphql(`
{
allSanitySecondPage {
edges {
node {
slug
}
}
}
}
`).then((result) => {
if (result.errors) {
Promise.reject(result.errors);
}
result.data.allSanitySecondPage.edges.forEach(({ node }) => {
createPage({
path: node.slug,
component: secondPageTemplate,
context: {
slug: node.slug,
},
});
});
});
};
secondPage.js (template)
import React from "react";
import { graphql } from "gatsby";
import Layout from "../components/layout";
const BlockContent = require("#sanity/block-content-to-react");
const secondPage = ({ data }) => {
// const pageData = data.sanitySecondPage.edges.node;
return (
<Layout>
<h1>Hello from the second page!</h1>
{console.log(data.sanitySecondPage)}
{/* <BlockContent blocks={pageData._rawBlockContent} /> */}
</Layout>
);
};
export const query = graphql`
query($slug: String!) {
sanitySecondPage(slug: { eq: $slug }) {
_rawBlockContent
}
}
`;
export default secondPage;
It turns out that all I needed was to write a stack overflow post to solve my own issue. Everything was correct, I miss understood the return of block content, it was supposed to be three array elements.