Complex query variables in GraphQL (via Gatsby) - graphql

I am building a localized static website using Gatsby, with the help of gatsby-plugin-intl. This plugin adds a context variable named intl to pages (including template-based pages), which is an object: https://github.com/wiziple/gatsby-plugin-intl/blob/master/src/gatsby-node.js#L27-L34
I would like to access the intl.language variable from the context within a page query. This is my (failing) code at this stage:
query($slug: String!, $intl: String) {
contentfulPerson(slug: {eq: $slug}, node_locale: {eq: $intl.language}) {
name
}
}
Contentful is the headless CMS I use and from which I would like to fetch data in the correct locale.
Obviously this code has two problems: $intl is not a string, and $intl.language is not syntactically correct. But I don't know how to fix either problem.
I guess I could either fork the plugin or do something in my own gatsby-node.js to make the language available as a top-level variable in the context, but I'm interested to know if there is a way to do it directly.
The Gatsby docs say that query variables can be complex (https://www.gatsbyjs.org/docs/graphql-reference/#query-variables) but in the example they provide, they don't show how the types are defined or how to access a property within these variables.
EDIT : I tried moving the language to a top-level context variable in my gatsby-node.js using this code:
exports.onCreatePage = ({page, actions}) => {
const { createPage, deletePage } = actions
deletePage(page)
createPage({
...page,
context: {
...page.context,
language: page.context.intl.language
}
})
}
but the program runs out of memory (even when increasing max_old_space_size)

You can move the language field to a top-level context by doing this:
exports.onCreatePage = ({ page, actions }) => {
const { createPage, deletePage } = actions
const oldPage = Object.assign({}, page)
page.context.language = page.context.intl.language;
if (page.context.language !== oldPage.context.language) {
// Replace new page with old page
deletePage(oldPage)
createPage(page)
}
}
Checking if the field has changed avoids the infinity loop.

Related

Any way to split up multiple Fragment expansions for a GraphQL query into multiple calls?

Context
This problem is likely predicated on certain choices, some of which are changeable and some of which are not. We are using the following technologies and frameworks:
Relay / React / TypeScript
ContentStack (CMS)
Problem
I'm attempting to create a highly customizable page that can be built from multiple kinds of UI components based on the data presented to it (to allow pages to be built using a CMS using prefab UI in an unpredictable order).
My first attempt at this was to create a set of fragments for the potential UI components that may be referenced in an array:
query CustomPageQuery {
title
description
customContentConnection {
edges {
node {
... HeroFragment
... TweetBlockFragment
... EmbeddedVideoFragment
"""
Further fragments are added here as we add more kinds of UI
"""
}
}
}
}
In the CMS we're using (ContentStack), the complexity of this query has grown to the point that it is rejected because it requires too many calls to the database in a single query. For that reason, I'm hoping there's a way I can split up the calls for the fragments so that they are not part of the initial query, or some similar solution that results in splitting up this query into multiple pieces.
I was hoping the #defer directive would solve this for me, but it's not supported by relay-compiler.
Any ideas?
Sadly #defer is still not a standard so it is not supported by most implementation (since you would also need the server to support it).
I am not sure if I understand the problem correctly, but you might want to look more toward using #skip or #include to only fetch the fragment you need depending on the type of the thing. But it would require the frontend to know what it wants to query beforehand.
query CustomPageQuery($hero: Boolean, $tweet: Boolean, $video: Boolean) {
title
description
customContentConnection {
edges {
node {
... HeroFragment #include(if: $hero)
... TweetBlockFragment #include(if: $tweet)
... EmbeddedVideoFragment #include(if: $video)
}
}
}
}
Generally you want to be able to discriminate the type without having to do a database query. So say:
type Hero {
id: ID
name: String
}
type Tweet {
id: ID
content: String
}
union Content = Hero | Tweet
{
Content: {
__resolveType: (parent, ctx) => {
// That should be able to resolve the type without a DB query
},
}
}
Once that is passed, each fragment is then resolved, making more database queries. If those are not properly batched with dataloaders then you have a N+1 problem. I am not sure how much control (if at all) you have on the backend but there is no silver bullet for your problem.
If you can't make optimizations on the backend then I would suggest trying to limit the connection. They seem to be using cursor based pagination, so you start with say first: 10 and once the first batch is returned, you can query the next elements by setting the after to the last cursor of the previous batch:
query CustomPageQuery($after: String) {
customContentConnection(first: 10, after: $after) {
edges {
cursor
node {
... HeroFragment
... TweetBlockFragment
... EmbeddedVideoFragment
}
}
pageInfo {
hasNextPage
}
}
}
As a last resort, you could try to first fetch all the IDs and then do subsequent queries to the CMS for each id (using aliases I guess) or type (if you can filter on the connection field). But I feel dirty just writing it so avoid it if you can.
{
one: node(id: "UUID1") {
... HeroFragment
... TweetBlockFragment
... EmbeddedVideoFragment
}
two: node(id: "UUID2") {
... HeroFragment
... TweetBlockFragment
... EmbeddedVideoFragment
}
}

Gatsby: How to handle undefined fields in siteMetadata?

I want to build a site header based on having defined a logo field or not (if it is not defined it will be used the site title instead).
gatsby-config.js:
module.exports = {
siteMetadata: {
title: 'Hello Web',
logo: '/images/logo.png'
}
}
query:
const query = graphql`
{
site {
siteMetadata {
title
logo
}
}
}
The above works fine, but if I remove the logo field the build breaks.
I read the Schema Customization section on Gatsby documentation, but I didn't find anything that apply (am I missing something?).
My current solution is to set logo to some "dummy content". Although it works, it's clearly not an elegant solution and have some drawbacks as the project grows.
Screenshot:
Check out create types in this blog post. and fixing fields in the the docs.
createTypes can be used to define, fix, or extend a Gatsby GraphQL type that represents some of your app’s data. Think of it like an escape hatch to politely inform Gatsby of your data’s shape.
Gatsby infers the schema based on the data it has available at build time. If you try to query for a field that doesn't exist, you will get a build error.
With createTypes you can tell gatsby that the logo field is a nullable String on the SiteSiteMetadata type. Now if the field is not found in your data source, you will get a null value for it, but gatsby can successfully build.
// gatsby-node.js
exports.sourceNodes = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type SiteSiteMetadata implements Node {
logo: String
}
`
createTypes(typeDefs)
}
Note that the rest of the fields (title, description, author) don’t have to be provided, they will still be handled by Gatsby’s type inference.

Gatsby: load i18n content dynamically from contentful

We have a static site using Gatsby and contentful. Now we want to support multi-languages, with localized content from contentful. I am able to populate a graghql query:
query frontpageTeaser($lang: String) {
contentfulFrontpage(node_locale: { eq: "zh-CN" }) {
myArticalContent
...
}
}
This query is able to load the Chinese content from contentful, and English if changed to node_locale: { eq: "en-US" }.
Now the issue is: we want to support a language switch, so that when switching language, the graphql loads corresponding localized content.
We are using gatsby-plugin-react-i18next, which has this great feature:
Support multi-language url routes in a single page component. You don’t have to create separate pages such as pages/en/index.js or pages/es/index.js.
Pages like http://localhost:8000/zh-CN/ does load Chinese from local /locales/zh-CN/translation.json, but how to load localized content when switching language?
Graphql seems providing page query, so i added gatsby-node.js:
exports.createPages = async function ({ actions, graphql }) {
actions.createPage({
path: '/zh-CN/',
component: require.resolve(`./src/pages/index.js`),
context: { lang: 'zh-CN' },
})
}
And use this on page:
export const query = graphql`
query frontpageTeaser($lang: String) {
contentfulFrontpage(node_locale: { eq: $lang }) {
myArticalContent
}
}
`
But it always returns en. Please kindly help :). Thanks.
This can be a complex switch. There is an example project that has smoothly done it with another CMS + Gatsby, here.
Specific places to point out in the codebase:
Configuration of which locales you use, here
A dynamic link depending on the active locale, here
The context for your whole app to know what the active locale is, here
Actually implementing the locale context provider in the higher order component Layout, here
There is also some magic inside of the gatsby-node.js which updates what you've already been working on! You can find that, here.
Hope that helps :)
Before getting a better solution, this one works:
// #todo gatsby plugin https://www.gatsbyjs.org/packages/gatsby-plugin-react-i18next/
// this plugin provides current language `context.i18n.language`, which not know how to pass it to graphql page query.
// This snippet moves it one-level up to `context.locale`.
// #todo need to explore a better solution.
exports.onCreatePage = ({ page, actions }) => {
const { createPage, deletePage } = actions
if (!page.context.locale) {
const language = page.context.i18n.language
const locale = language === 'en' ? 'en-US' : language
deletePage(page)
createPage({
...page,
context: {
...page.context,
locale,
}
})
}
}

How to pass variable to GraphQL in a non-template based page (part of src/pages)

While I'm able to pass variable via pageContext to GraphQL on all template based pages ( which use createPage function), I'm not able to pass/modify the context for pages which are part of src/page.
Consider this home page : src/pages/index.js
//want to pass dynamic value to $today
const IndexPage = ( {data, pageContext} ) => {
console.log(pageContext); // prints "isCreatedByStatefulCreatePages: true"
return (
<Layout />
)};
export default IndexPage;
export const Query = graphql`
offers( where : { day: $today})
{
..fields
}
`;
In order to modify automatically created pages you will need to use the onCreatePage node API. You basically recreate (delete/create) pages while modifying the page object. Just follow instructions in the docs Creating and Modifying Pages

Access JSON chunk exported from Gatsby Static Query

I have a React Component in a Gatsby app that is using the useStaticQuery hook to pull in data from the GraphQL layer. This component gets used in my application, but it also gets used as part of a JavaScript embed/widget that is created in a separate Webpack configuration.
I don't want the widget to depend on Gatsby, so I've shimmed the relevant bits of Gatsby, but I still need to pass in data to the shim I've created for useStaticQuery. I found that my Gatsby app is generating a file at public/static/d/2250905522.json that contains a perfect representation of the query data, and I'd like to use it like so:
// This file gets substituted when importing from `gatsby`
import queryResult from "../public/static/d/2250905522.json"
export const useStaticQuery = () => queryResult.data
export const graphql = () => {}
This works, but I haven't figured out where this is coming from or how to determine the file name in a way that is deterministic/stable. How is Gatsby determining this file name, and what internals might I use to do the same?
Edit: I found this routine in the Gatsby codebase that appears to be using staticQueryComponent.hash to determine the number. staticQueryComponent is being destructured from store.getState() where store is associated with Redux, but I'm still not sure where the hash is being determined yet.
Edit 2: Found another mention of this in the documentation here. It sounds like hash is a hash of the query itself, so this will change over time if the query changes (which is likely), so I'm still looking for the routine used to compute the hash.
Due to changes in the babel-plugin-remove-graphql-queries, coreyward's (awesome) answer should be updated to:
const { stripIgnoredCharacters } = require('graphql/utilities/stripIgnoredCharacters');
const murmurModule = require('babel-plugin-remove-graphql-queries/murmur');
const murmurhash = typeof murmurModule === 'function' ? murmurModule : murmurModule.murmurhash;
const GATSBY_HASH_SEED = 'abc';
function hashQuery(query) {
const result = murmurhash(stripIgnoredCharacters(query), GATSBY_HASH_SEED).toString();
return result;
}
module.exports = hashQuery;
The changes are:
fix the way murmurhash is imported. Credit to github user veloce, see: https://github.com/birkir/gatsby-source-graphql-universal/pull/16/files
Change to using stripIgnoredCharacters in order to match the updated way that gatsby internally hashes queries by first stripping whitespace and comment lines for efficiency.
Gatsby is using murmurhash with a seed of "abc" to calculate the hash of the full text of the query (including whitespace). This occurs in babel-plugin-remove-graphql-queries.
Since the reused components are isolated from Gatsby, the graphql tagged template literal can be shimmed in order to get the original query for hashing:
// webpack.config.js
module.exports = {
resolve: {
alias: {
gatsby: path.resolve(__dirname, "gatsby-shim.js"),
},
},
}
// gatsby-shim.js
import { murmurhash } from "babel-plugin-remove-graphql-queries/murmur"
import {
stripIgnoredCharacters,
} from "graphql/utilities/stripIgnoredCharacters"
const GATSBY_HASH_SEED = "abc"
const hashQuery = (query) =>
murmurhash(
stripIgnoredCharacters(query),
GATSBY_HASH_SEED
).toString()
export const graphql = query => hashQuery(query.raw[0])
This results in the query hash being passed into useStaticQuery, which can be shimmed similarly to retrieve the cached query from disk.
Also worth noting, newer versions of Gatsby store the StaticQuery result data in public/page-data/sq/d/[query hash].json.
If you're looking to do something similar, I've written up a much longer blog post about the details of this process and the solution I arrived at here.

Resources