gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.22k stars 10.32k forks source link

Change the main layout according to its path (retrieving data from graphQL) #12073

Closed fsgreco closed 5 years ago

fsgreco commented 5 years ago

EDIT:

I created a demo on github if you want to take a look (now all the config resides on springhero.js component): https://github.com/anonimoconiglio/gatsby-for-debugging

Summary

Sorry if this question seems noob, I tried everything and feel like there's something missing but I don't understand how to do it.

I need to change my hero-image (that is part of the main layout) according to the page (its slug). Every post has a hero-image, setted in the frontmatter info. So I need to tell Gatsby to use the image of the post that has loaded, instead of the main hero-image.

I used a conditional statement that works well to distinguish the homepage that anything else, then iterate allMarkdownRemark and set the alternative image with a key prop. Now, as far as I have understand using a "key" prop isn't enought because it shows me all the images of the posts instead of filter the right one according to the slug.

It seems that I need to allow Gatsby to undestand the context of the page (like with my singlePagePost component), so it can filter the right image according to the page loaded.

But I can't use a Page Query with variables on the main layout, because it's not a page, so I run out of ideas...

Relevant information

I'm using gatsby-plugin-layout in order to make the layout "static" (otherwise my Spring effect on hero image flickered, because it loaded the layout everytime).

My Changes

This is the last attempt on my main layout component, this is the conditional statement:

[...]
{location.pathname === '/' 
  ? (<Img fluid={data.file.childImageSharp.fluid} />) 
  : (data.allMarkdownRemark.edges.map(({ node }) => (
     <Img key={node.frontmatter.slug} fluid={node.frontmatter.hero.childImageSharp.fluid} /> ))
    )
}
[...]

Here is the full Layout Component:

import React from "react";
import PropTypes from "prop-types";
import Helmet from "react-helmet";
import { Spring } from 'react-spring/renderprops';
import styled from "styled-components";
import { StaticQuery, graphql } from "gatsby";
import Img from 'gatsby-image';

import Header from "./header";
import Archive from "./archive";
import "./layout.css";

const MainLayout = styled.main` [...] `;

const Layout = ({ children, location }) => (
<StaticQuery
  query={graphql`
    query SiteTitleQuery {
      site {
        siteMetadata {
          title
          description
        }
      }
      file(relativePath: {regex: "/my-main-img/"}) {
        childImageSharp {
          fluid (maxWidth: 1900, quality: 90) {
            ...GatsbyImageSharpFluid_tracedSVG
          } 
        }
      }
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              slug
              hero {
                childImageSharp{
                  fluid(maxWidth: 800, quality: 100) {
                    ...GatsbyImageSharpFluid
                  }
                }
              }
            }
          }
        }
      }
    }
  `}
  render={data => (
    <>
      <Helmet [...]>
      [...]
      </Helmet>
      <Header siteTitle={data.site.siteMetadata.title} />
      <Spring 
        from={{ height: location.pathname === '/' ? 200 : 450 }} 
        to={{ height: location.pathname === '/' ? 450 : 200 }}>
        {({ height }) => 
          <div style={{ overflow: 'hidden', height }}>
            {location.pathname === '/' 
              ? (<Img fluid={data.file.childImageSharp.fluid} />) 
              : (data.allMarkdownRemark.edges.map(({ node }) => (
                 <Img key={node.frontmatter.slug} fluid={node.frontmatter.hero.childImageSharp.fluid} /> ))
                )
            }
          </div>
        }
      </Spring>
      <MainLayout>
        <div>{children}</div>
        <Archive />
      </MainLayout>
    </>
    )}
  />
);

Layout.propTypes = {
children: PropTypes.node.isRequired
};

export default Layout;

Environment (if relevant)

npx gatsby info --clipboard

  System:
    OS: Linux 4.20 Antergos Linux undefined
    CPU: (4) x64 Intel(R) Core(TM) i3 CPU         540  @ 3.07GHz
    Shell: 5.7.1 - /usr/bin/zsh
  Binaries:
    Node: 11.9.0 - /usr/bin/node
    npm: 6.7.0 - /usr/bin/npm
  npmPackages:
    gatsby: ^2.0.50 => 2.0.50 
    gatsby-image: ^2.0.29 => 2.0.29 
    gatsby-plugin-layout: ^1.0.12 => 1.0.12 
    gatsby-plugin-manifest: ^2.0.9 => 2.0.9 
    gatsby-plugin-offline: ^2.0.15 => 2.0.15 
    gatsby-plugin-react-helmet: ^3.0.2 => 3.0.2 
    gatsby-plugin-sharp: ^2.0.20 => 2.0.20 
    gatsby-plugin-styled-components: ^3.0.4 => 3.0.4 
    gatsby-plugin-typography: ^2.2.2 => 2.2.2 
    gatsby-remark-images: ^3.0.4 => 3.0.4 
    gatsby-remark-reading-time: ^1.0.1 => 1.0.1 
    gatsby-source-filesystem: ^2.0.8 => 2.0.8 
    gatsby-transformer-remark: ^2.1.12 => 2.1.12 
    gatsby-transformer-sharp: ^2.1.13 => 2.1.13 
  npmGlobalPackages:
    gatsby-cli: 2.4.10

File contents (if changed)

Other files:

gatsby-config.js:

module.exports = {
  siteMetadata: {
    title: 'This is a blog title',
    description: 'This is the description',
  },
  plugins: [
    'gatsby-plugin-react-helmet',
    'gatsby-plugin-styled-components',
    'gatsby-plugin-sharp',
    'gatsby-transformer-sharp',
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: 'my blog',
        short_name: 'santiago',
        start_url: '/',
        background_color: '#663399',
        theme_color: '#663399',
        display: 'minimal-ui',
        icon: 'src/images/rabbit.png', // This path is relative to the root of the site.
      },
    },
    'gatsby-plugin-offline',
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/posts`,
        name: 'posts'
      }
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/images`,
        name: 'images'
      }
    },
    {
      resolve: `gatsby-plugin-layout`,
      options: {
        component: require.resolve(`./src/components/layout.js`),
      },
    },
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          `gatsby-remark-reading-time`,
            {
                resolve: `gatsby-remark-images`,
                options: {
                    maxWidth: 800,
                },
            }
        ],
      },
    }
  ],
}

package.json: N/A gatsby-node.js:

const path = require('path');
exports.createPages = ({ graphql, actions}) => {
    const {createPage} = actions;
    return new Promise ((resolve, reject) => {
        graphql(`
            {
                allMarkdownRemark{
                    edges {
                        node {
                            frontmatter {
                                slug
                            }
                        }
                    }
                }
            }
        `).then(results => {
            results.data.allMarkdownRemark.edges.forEach(({node}) =>{
                createPage({
                    path: `/posts${node.frontmatter.slug}`,
                    component: path.resolve('./src/components/postLayout.js'),
                    context: {
                        slug: node.frontmatter.slug,
                    }
                });
            })
            resolve();
        })
    });
}`

gatsby-browser.js: N/A gatsby-ssr.js: N/A

arturhenryy commented 5 years ago

@yogeshkotadiya i think the easiest and cleanest solution here is to not embed the hero component in the layout and rather move it in the page templates / components. there you have the page specific context. just imagine you would have single-blog pages where you would have to check all the slugs manually and output the right hero image in the layout component. would be pretty messy haha. so i think best approach is here it to remove every non generic component which differs per page from the layout to the designated pages.

arturhenryy commented 5 years ago

another option is you could leave the hero image in layout and pass the image data as a prop to Layout component on every page you need it. i hope this is clear if not feel free to ask.

arturhenryy commented 5 years ago

both approaches have pros and cons. i you leave the hero in layout you can have no page specific changes to it in case you would need for example an extra title in it for some pages e. g. but as a pro you only need to embed the code once in layout. so if you dont need page specific changes in this component (except the different images) this approach would be ok.

fsgreco commented 5 years ago

@arturhenryy Hi, thanks for the suggestion. I removed the condition inside Spring, leaving only the Img component of the main-hero that already worked in the homepage. This to be sure that it wasn't that "Spring" component.

Then I created imghero.js component and placed it within a condition in the main layout like this:

import ImgHero from "./imgHero"
[...]
</Spring>

    {location.pathname !== '/' &&  <ImgHero /> }

<MainLayout>
[...]

This is the content of ImgHero component:

import React from 'react'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'

export const query = graphql`
    query ImgHeroQuery($slug: String!) {
        markdownRemark(frontmatter: {slug: {eq: $slug}}) {
            frontmatter {
                slug
                hero {
                    id
                childImageSharp {
                    fluid(maxWidth: 1900, quality: 100) {
                    ...GatsbyImageSharpFluid
                    }
                }
                }
            }
        }
    }
` 
const ImgHero = ({ data, location }) => (
<div location={location}>
    <Img 
        key={data.markdownRemark.frontmatter.hero.id} 
        fluid={data.markdownRemark.frontmatter.hero.childImageSharp.fluid} 
    />
</div>
    )

export default ImgHero;

It gives me some warnings during compiling (although it compiles well):

warning The GraphQL query in the non-page component "/home/santiago/..../src/components/imgHero.js" will not be run.
Exported queries are only executed for Page components. Instead of an exported
query, either co-locate a GraphQL fragment and compose that fragment into the
query (or other fragment) of the top-level page that renders this component, or
use a <StaticQuery> in this component. For more info on fragments and
composition, see http://graphql.org/learn/queries/#fragments and for more
information on <StaticQuery>, see https://gatsbyjs.org/docs/static-query

Homepage works as espected but now all the other posts results in a completely white screen on the browser, this are the errors of the console:

Uncaught TypeError: Cannot read property 'markdownRemark' of undefined
[...]
The above error occurred in the <ImgHero> component:
[...]
React will try to recreate this component tree from scratch using the error boundary you provided, LocationProvider.

The above error occurred in the <LocationProvider> component:
[...]

I think the problem is in the location, but I'm already passing that location property also in the ImgHero component...

another option is you could leave the hero image in layout and pass the image data as a prop to Layout component on every page you need it. i hope this is clear if not feel free to ask.

I'm a bit confused with this option you pointed out. I thought that props can only be passed from top to bottom. I mean it's possible to tell the main layout to take fluid={data.markdownRemark.frontmatter.hero.childImageSharp.fluid} from the post component?

t2ca commented 5 years ago

You are getting this error because in a non page component you need to use static query https://www.gatsbyjs.org/docs/static-query/

warning The GraphQL query in the non-page component

fsgreco commented 5 years ago

You are getting this error because in a non page component you need to use static query https://www.gatsbyjs.org/docs/static-query/

warning The GraphQL query in the non-page component

I convert the query on a StaticQuery. Now the image on the post is always the same. Its the hero image of my first post. It's like Gatsby don't filter the right hero image, it only take the one that appears first (I think this happens because I can't give a variable on a static query, in order to tell Gatsby to filter according to the slug).

This is my new ImgHero.js component:

import React from 'react'
import { graphql, StaticQuery } from 'gatsby'
import Img from 'gatsby-image'

const ImgHero = ({ location }) => (
    <StaticQuery 
    query={graphql`
        query ImgHeroQuery {
        markdownRemark {
            frontmatter {
                slug
                hero {
                id
                childImageSharp {
                    fluid(maxWidth: 1900, quality: 100) {
                    ...GatsbyImageSharpFluid
                    }
                }
                }
            }
        }
    }
    `}
    render={data => (
    <div location={location}>
        <Img 
            key={data.markdownRemark.frontmatter.hero.id} 
            fluid={data.markdownRemark.frontmatter.hero.childImageSharp.fluid}/>
    </div>
    )
    }
    />
    )

export default ImgHero;
t2ca commented 5 years ago

Thats correct static query does not accept variables.

fsgreco commented 5 years ago

Thats correct static query does not accept variables.

That's my problem :/ Is there any way to tell gatsby to evaluate the context here? Even if this is not a page query?

t2ca commented 5 years ago

Typically this would be accomplished as its shown in this tutorial. https://www.gatsbyjs.org/tutorial/part-seven/#creating-pages

You would have a template file such as: component: path.resolve(./src/templates/blog-post.js),

And this file would contain your query.

fsgreco commented 5 years ago

Typically this would be accomplished as its shown in this tutorial. https://www.gatsbyjs.org/tutorial/part-seven/#creating-pages

You would have a template file such as: component: path.resolve(./src/templates/blog-post.js),

And this file would contain your query.

I already do that, indeed I have a postLayout.js where there is a Page Query that is used to create the posts. But I can't replicate that query on my imghero.js because it's not a page. So I'm stucked.

fsgreco commented 5 years ago

Now I followed the warming that Gatsby gives me when was compiled that second page query. So I put a fragment on my postLayout.js:

import React  from 'react'
import { graphql } from 'gatsby'
import Transition from "../components/transition"
import styled from 'styled-components'

export const query = graphql`
    query PostQuery($slug: String!) {
        markdownRemark(frontmatter: {
            slug: {
                eq: $slug
            }
            }) {
            html
            frontmatter {
                ...imghero
            }
        }
    }

fragment imghero on frontmatter_2 {
    slug
    date
    title
    hero {
        id
        childImageSharp {
        fluid {
            src
            ...GatsbyImageSharpFluid_tracedSVG
        }
        }
    }
}
` 

const PostWrapper = styled.div`
    max-width: 42rem;
    margin: 0 auto;
`

const postLayout = ({ data, location }) => (
    // <Layout location={location}>
    <Transition location={location}>
        <PostWrapper>
            <h1>Questo è: { data.markdownRemark.frontmatter.title} </h1>
            <div  dangerouslySetInnerHTML={{
                __html: data.markdownRemark.html
            }}/>
        </PostWrapper>
    </Transition>
    //</Layout>
)

export default postLayout;

And this is my imgHero.js component:

import React from 'react'
import { graphql, StaticQuery } from 'gatsby'
import Img from 'gatsby-image'

const ImgHero = ({ location }) => (
    <StaticQuery 
    query={graphql`
        query ImgHeroQuery {
        markdownRemark {
            frontmatter {
                slug
                ...imghero
            }
        }
    }
    `}
    render={data => (
    <div location={location}>
        <Img 
            key={data.markdownRemark.frontmatter.hero.id} 
            fluid={data.markdownRemark.frontmatter.hero.childImageSharp.fluid}/>
    </div>
    )
    }
    />
    )

export default ImgHero;

It gives me no errors at all, but the posts have all the same image :( I have run out of ideas here

arturhenryy commented 5 years ago

@anonimoconiglio as i said before, please move the ImgHero component inside your postLayout and pass the image data as a prop. you can also remove the key from the Img inside ImgHero because you dont iterate

t2ca commented 5 years ago

I think you want something like this:

import React  from 'react'
import { graphql } from 'gatsby'
import Transition from "../components/transition"
import styled from 'styled-components'
import Img from 'gatsby-image'

export const query = graphql`
    query($slug: String!) {
        markdownRemark(frontmatter: {slug: {eq: $slug}}) {
              frontmatter {
                title
                hero {
                    id
                childImageSharp {
                    fluid(maxWidth: 1900, quality: 100) {
                    ...GatsbyImageSharpFluid
                    }
                }
                }
            }
        }
    }
`

const PostWrapper = styled.div`
    max-width: 42rem;
    margin: 0 auto;
`

const postLayout = ({ data, location }) => (
    // <Layout location={location}>

    <Img 
        fluid={data.markdownRemark.frontmatter.hero.childImageSharp.fluid} 
    />

    <Transition location={location}>
        <PostWrapper>
            <h1>Questo è: { data.markdownRemark.frontmatter.title} </h1>
            <div  dangerouslySetInnerHTML={{
                __html: data.markdownRemark.html
            }}/>
        </PostWrapper>
    </Transition>
    //</Layout>
)

export default postLayout;
fsgreco commented 5 years ago

@arturhenryy @t2ca I already tried but unfortunately that's not a solution, since I want that the image takes the place of the main-hero, with this solution the image resides on it's relative post but there is a sidebar and a hero that still remain in their place.

Here's an example of what I mean. This is the homepage:

screenshot_home

And this is the result if I put the Img component inside postLayout.js:

screenshot_result

t2ca commented 5 years ago

I think you can do something like

const postLayout = ({ data, location }) => (
    <Layout hero={data.markdownRemark.frontmatter.hero.childImageSharp.fluid}>
      <Transition location={location}>
        <PostWrapper>
            <h1>Questo è: { data.markdownRemark.frontmatter.title} </h1>
            <div  dangerouslySetInnerHTML={{
                __html: data.markdownRemark.html
            }}/>
        </PostWrapper>
      </Transition>
    </Layout>
)

export default postLayout;

Inside your Layout component you will have a hero prop

const Layout = props => (
[...]
{location.pathname === '/' 
  ? (<Img fluid={data.file.childImageSharp.fluid} />) 
  :  <Img fluid={props.hero} /> ))
    )
}
[...]
fsgreco commented 5 years ago

@t2ca Thanks, I tried with a div instead of "Layout" (I don't use Layout because I'm using gatsby-plugin-layout to make the layout "static" or "fixed". So it automatically wraps my postLayout component (its children))

const postLayout = ({ data, location }) => (
    <div hero={data.markdownRemark.frontmatter.hero.childImageSharp.fluid}>
    <Transition location={location} >
        <PostWrapper >
            <h1>Questo è: { data.markdownRemark.frontmatter.title} </h1>
            <div  dangerouslySetInnerHTML={{
                __html: data.markdownRemark.html
            }}/>
        </PostWrapper>
    </Transition>
    </div>
)

and then this (to simplify things for the moment):

const Layout = ({ children, location, props }) => (
  <StaticQuery
    query={QUERY_MAIN_LAYOUT}
    render={data => (
      <>
      [...]

     {location.pathname !== '/' &&  <Img fluid={props.hero} /> }

Compilation goes fine but post pages are white again, with this errors:

Uncaught TypeError: Cannot read property 'hero' of undefined
[...]
The above error occurred in the <Context.Consumer> component:
[...]
The above error occurred in the <LocationProvider> component:
[...] 
t2ca commented 5 years ago

Sorry i'm not familiar with using gatsby-plugin-layout

fsgreco commented 5 years ago

Sorry i'm not familiar with using gatsby-plugin-layout

do you think that's generating the problem? I thought it only wrapped the layout in its children.

t2ca commented 5 years ago

Well this code below is incorrect which is causing the undefined error.

const Layout = ({ children, location, props }) => (
  <StaticQuery
    query={QUERY_MAIN_LAYOUT}
    render={data => (
      <>
      [...]

     {location.pathname !== '/' &&  <Img fluid={props.hero} /> }

It would be something like

const Layout = ({ children, location, hero }) => (
  <StaticQuery
    query={QUERY_MAIN_LAYOUT}
    render={data => (
      <>
      [...]

     {location.pathname !== '/' &&  <Img fluid={hero} /> }
fsgreco commented 5 years ago

@t2ca mm ok, now there is only one error (always in the browser, it compiles well):

Uncaught TypeError: Cannot read property 'src' of undefined
[...]
The above error occurred in the <Image> component:
[...]
The above error occurred in the <LocationProvider> component:
t2ca commented 5 years ago

Im sorry, i'm not sure, i don't any experience with gatsby-plugin-layout

fsgreco commented 5 years ago

Im sorry, i'm not sure, i don't any experience with gatsby-plugin-layout

It's ok, thanks for your help anyway :)

fsgreco commented 5 years ago

I created a demo on github if you want to take a look (now all the config resides on springhero.js component): https://github.com/anonimoconiglio/gatsby-for-debugging

Thanks for the help. cc/ @t2ca

gatsbot[bot] commented 5 years ago

Hiya!

This issue has gone quiet. Spooky quiet. 👻

We get a lot of issues, so we currently close issues after 30 days of inactivity. It’s been at least 20 days since the last update here.

If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not stale" to keep this issue open!

Thanks for being a part of the Gatsby community! 💪💜

gatsbot[bot] commented 5 years ago

Hey again!

It’s been 30 days since anything happened on this issue, so our friendly neighborhood robot (that’s me!) is going to close it.

Please keep in mind that I’m only a robot, so if I’ve closed this issue in error, I’m HUMAN_EMOTION_SORRY. Please feel free to reopen this issue or create a new one if you need anything else.

Thanks again for being part of the Gatsby community!