├── .all-contributorsrc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── .nvmrc ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── graphcms-fragments │ ├── Asset.graphql │ ├── Category.graphql │ ├── Product.graphql │ ├── ScheduledOperation.graphql │ ├── ScheduledRelease.graphql │ └── User.graphql ├── package.json ├── postcss.config.js ├── src │ ├── components │ │ ├── layout.js │ │ └── paragraph.js │ ├── pages │ │ └── index.js │ ├── styles │ │ └── main.css │ └── templates │ │ └── product-page.js └── tailwind.config.js ├── gatsby-source-graphcms ├── .babelrc ├── README.md ├── index.js ├── package.json └── src │ ├── gatsby-node.js │ └── util │ ├── constants.js │ ├── getDominantColor.js │ ├── getImageBase64.js │ ├── getTracedSVG.js │ └── reportPanic.js ├── package.json ├── prettier.config.js ├── renovate.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "gatsby-source-graphcms/README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "badgeTemplate": "\"Contributors\"", 8 | "contributors": [ 9 | { 10 | "login": "ynnoj", 11 | "name": "Jonathan Steele", 12 | "avatar_url": "https://avatars.githubusercontent.com/u/3578709?v=4", 13 | "profile": "http://jonathan.steele.pro", 14 | "contributions": [ 15 | "code", 16 | "blog", 17 | "example", 18 | "ideas", 19 | "maintenance", 20 | "projectManagement" 21 | ] 22 | }, 23 | { 24 | "login": "jpedroschmitz", 25 | "name": "João Pedro Schmitz", 26 | "avatar_url": "https://avatars.githubusercontent.com/u/26466516?v=4", 27 | "profile": "http://joaopedro.dev", 28 | "contributions": [ 29 | "code", 30 | "example", 31 | "ideas" 32 | ] 33 | }, 34 | { 35 | "login": "notrab", 36 | "name": "Jamie Barton", 37 | "avatar_url": "https://avatars.githubusercontent.com/u/950181?v=4", 38 | "profile": "https://graphql.wtf", 39 | "contributions": [ 40 | "code", 41 | "bug", 42 | "maintenance", 43 | "doc" 44 | ] 45 | } 46 | ], 47 | "contributorsPerLine": 7, 48 | "projectName": "gatsby-source-graphcms", 49 | "projectOwner": "hygraph", 50 | "repoType": "github", 51 | "repoHost": "https://github.com", 52 | "skipCi": true 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - next 7 | jobs: 8 | release: 9 | name: Build & Release 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 14 18 | - name: Install dependencies 19 | run: yarn 20 | - name: Build 21 | working-directory: ./gatsby-source-graphcms 22 | run: yarn build 23 | - name: Release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | run: npx semantic-release 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | .cache 3 | public 4 | 5 | # Dependency directories 6 | node_modules 7 | 8 | # Logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Output 14 | gatsby-source-graphcms/gatsby-* 15 | gatsby-source-graphcms/util/ 16 | !gatsby-source-graphcms/src/** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GraphCMS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gatsby-source-graphcms/README.md -------------------------------------------------------------------------------- /demo/.nvmrc: -------------------------------------------------------------------------------- 1 | 14.15 2 | -------------------------------------------------------------------------------- /demo/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MDXProvider } from '@mdx-js/react' 3 | 4 | import Layout from './src/components/layout' 5 | import Parargraph from './src/components/paragraph' 6 | 7 | import './src/styles/main.css' 8 | 9 | const wrapPageElement = ({ element, props }) => { 10 | return {element} 11 | } 12 | 13 | const wrapRootElement = ({ element }) => { 14 | return {element} 15 | } 16 | 17 | export { wrapPageElement, wrapRootElement } 18 | -------------------------------------------------------------------------------- /demo/gatsby-config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | module.exports = { 4 | plugins: [ 5 | 'gatsby-plugin-image', 6 | 'gatsby-plugin-sharp', 7 | 'gatsby-plugin-mdx', 8 | 'gatsby-plugin-postcss', 9 | { 10 | resolve: 'gatsby-source-graphcms', 11 | options: { 12 | buildMarkdownNodes: true, 13 | endpoint: 14 | process.env.HYGRAPH_ENDPOINT || 15 | 'https://api-eu-central-1.hygraph.com/v2/ckclvjtet0f0901z69og3f3gm/master', 16 | locales: ['en', 'de'], 17 | stages: ['DRAFT', 'PUBLISHED'], 18 | }, 19 | }, 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /demo/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.createPages = async ({ actions: { createPage }, graphql }) => { 4 | const { data } = await graphql(` 5 | { 6 | products: allGraphCmsProduct( 7 | filter: { locale: { eq: en }, stage: { eq: PUBLISHED } } 8 | ) { 9 | nodes { 10 | description { 11 | markdownNode { 12 | childMdx { 13 | body 14 | } 15 | } 16 | } 17 | formattedPrice 18 | id 19 | locale 20 | name 21 | slug 22 | } 23 | } 24 | } 25 | `) 26 | 27 | data.products.nodes.forEach((product) => { 28 | createPage({ 29 | component: path.resolve(`src/templates/product-page.js`), 30 | context: { 31 | id: product.id, 32 | product, 33 | }, 34 | path: `/${product.locale}/products/${product.slug}`, 35 | }) 36 | }) 37 | } 38 | 39 | exports.createResolvers = ({ createResolvers }) => { 40 | const resolvers = { 41 | GraphCMS_Product: { 42 | formattedPrice: { 43 | type: 'String', 44 | resolve: (source) => { 45 | return new Intl.NumberFormat('en-US', { 46 | currency: 'USD', 47 | style: 'currency', 48 | }).format(source.price) 49 | }, 50 | }, 51 | }, 52 | } 53 | 54 | createResolvers(resolvers) 55 | } 56 | -------------------------------------------------------------------------------- /demo/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | export { wrapPageElement, wrapRootElement } from './gatsby-browser' 2 | -------------------------------------------------------------------------------- /demo/graphcms-fragments/Asset.graphql: -------------------------------------------------------------------------------- 1 | fragment Asset on Asset { 2 | stage 3 | locale 4 | remoteId: id 5 | createdAt(variation: COMBINED) 6 | updatedAt(variation: COMBINED) 7 | publishedAt(variation: COMBINED) 8 | handle 9 | fileName 10 | height 11 | width 12 | size 13 | mimeType 14 | createdBy { 15 | ... on User { 16 | remoteTypeName: __typename 17 | remoteId: id 18 | stage 19 | } 20 | } 21 | updatedBy { 22 | ... on User { 23 | remoteTypeName: __typename 24 | remoteId: id 25 | stage 26 | } 27 | } 28 | publishedBy { 29 | ... on User { 30 | remoteTypeName: __typename 31 | remoteId: id 32 | stage 33 | } 34 | } 35 | productImages { 36 | ... on Product { 37 | remoteTypeName: __typename 38 | remoteId: id 39 | locale 40 | stage 41 | } 42 | } 43 | scheduledIn { 44 | ... on ScheduledOperation { 45 | remoteTypeName: __typename 46 | remoteId: id 47 | stage 48 | } 49 | } 50 | url 51 | } -------------------------------------------------------------------------------- /demo/graphcms-fragments/Category.graphql: -------------------------------------------------------------------------------- 1 | fragment Category on Category { 2 | stage 3 | locale 4 | remoteId: id 5 | createdAt(variation: COMBINED) 6 | updatedAt(variation: COMBINED) 7 | publishedAt(variation: COMBINED) 8 | name 9 | createdBy { 10 | ... on User { 11 | remoteTypeName: __typename 12 | remoteId: id 13 | stage 14 | } 15 | } 16 | updatedBy { 17 | ... on User { 18 | remoteTypeName: __typename 19 | remoteId: id 20 | stage 21 | } 22 | } 23 | publishedBy { 24 | ... on User { 25 | remoteTypeName: __typename 26 | remoteId: id 27 | stage 28 | } 29 | } 30 | products { 31 | ... on Product { 32 | remoteTypeName: __typename 33 | remoteId: id 34 | locale 35 | stage 36 | } 37 | } 38 | scheduledIn { 39 | ... on ScheduledOperation { 40 | remoteTypeName: __typename 41 | remoteId: id 42 | stage 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /demo/graphcms-fragments/Product.graphql: -------------------------------------------------------------------------------- 1 | fragment Product on Product { 2 | stage 3 | locale 4 | remoteId: id 5 | createdAt(variation: COMBINED) 6 | updatedAt(variation: COMBINED) 7 | publishedAt(variation: COMBINED) 8 | name 9 | slug 10 | description { 11 | ... on ProductDescriptionRichText { 12 | raw 13 | json 14 | html 15 | markdown 16 | text 17 | references { 18 | ... on Asset { 19 | remoteTypeName: __typename 20 | remoteId: id 21 | locale 22 | stage 23 | } 24 | ... on Category { 25 | remoteTypeName: __typename 26 | remoteId: id 27 | locale 28 | stage 29 | } 30 | ... on Product { 31 | remoteTypeName: __typename 32 | remoteId: id 33 | locale 34 | stage 35 | } 36 | } 37 | } 38 | } 39 | price 40 | createdBy { 41 | ... on User { 42 | remoteTypeName: __typename 43 | remoteId: id 44 | stage 45 | } 46 | } 47 | updatedBy { 48 | ... on User { 49 | remoteTypeName: __typename 50 | remoteId: id 51 | stage 52 | } 53 | } 54 | publishedBy { 55 | ... on User { 56 | remoteTypeName: __typename 57 | remoteId: id 58 | stage 59 | } 60 | } 61 | images { 62 | ... on Asset { 63 | remoteTypeName: __typename 64 | remoteId: id 65 | locale 66 | stage 67 | } 68 | } 69 | categories { 70 | ... on Category { 71 | remoteTypeName: __typename 72 | remoteId: id 73 | locale 74 | stage 75 | } 76 | } 77 | scheduledIn { 78 | ... on ScheduledOperation { 79 | remoteTypeName: __typename 80 | remoteId: id 81 | stage 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /demo/graphcms-fragments/ScheduledOperation.graphql: -------------------------------------------------------------------------------- 1 | fragment ScheduledOperation on ScheduledOperation { 2 | stage 3 | remoteId: id 4 | createdAt 5 | updatedAt 6 | publishedAt 7 | description 8 | errorMessage 9 | rawPayload 10 | createdBy { 11 | ... on User { 12 | remoteTypeName: __typename 13 | remoteId: id 14 | stage 15 | } 16 | } 17 | updatedBy { 18 | ... on User { 19 | remoteTypeName: __typename 20 | remoteId: id 21 | stage 22 | } 23 | } 24 | publishedBy { 25 | ... on User { 26 | remoteTypeName: __typename 27 | remoteId: id 28 | stage 29 | } 30 | } 31 | release { 32 | ... on ScheduledRelease { 33 | remoteTypeName: __typename 34 | remoteId: id 35 | stage 36 | } 37 | } 38 | status 39 | affectedDocuments { 40 | ... on Asset { 41 | remoteTypeName: __typename 42 | remoteId: id 43 | locale 44 | stage 45 | } 46 | ... on Category { 47 | remoteTypeName: __typename 48 | remoteId: id 49 | locale 50 | stage 51 | } 52 | ... on Product { 53 | remoteTypeName: __typename 54 | remoteId: id 55 | locale 56 | stage 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /demo/graphcms-fragments/ScheduledRelease.graphql: -------------------------------------------------------------------------------- 1 | fragment ScheduledRelease on ScheduledRelease { 2 | stage 3 | remoteId: id 4 | createdAt 5 | updatedAt 6 | publishedAt 7 | title 8 | description 9 | errorMessage 10 | isActive 11 | isImplicit 12 | releaseAt 13 | createdBy { 14 | ... on User { 15 | remoteTypeName: __typename 16 | remoteId: id 17 | stage 18 | } 19 | } 20 | updatedBy { 21 | ... on User { 22 | remoteTypeName: __typename 23 | remoteId: id 24 | stage 25 | } 26 | } 27 | publishedBy { 28 | ... on User { 29 | remoteTypeName: __typename 30 | remoteId: id 31 | stage 32 | } 33 | } 34 | operations { 35 | ... on ScheduledOperation { 36 | remoteTypeName: __typename 37 | remoteId: id 38 | stage 39 | } 40 | } 41 | status 42 | } -------------------------------------------------------------------------------- /demo/graphcms-fragments/User.graphql: -------------------------------------------------------------------------------- 1 | fragment User on User { 2 | stage 3 | remoteId: id 4 | createdAt 5 | updatedAt 6 | publishedAt 7 | name 8 | picture 9 | isActive 10 | kind 11 | } -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build": "gatsby build", 7 | "clean": "gatsby clean", 8 | "preinstall": "cd ../gatsby-source-graphcms && yarn build", 9 | "dev": "gatsby develop", 10 | "serve": "gatsby serve" 11 | }, 12 | "dependencies": { 13 | "@mdx-js/mdx": "1.6.22", 14 | "@mdx-js/react": "1.6.22", 15 | "gatsby": "4.4.0", 16 | "gatsby-plugin-image": "2.4.0", 17 | "gatsby-plugin-mdx": "3.4.0", 18 | "gatsby-plugin-postcss": "5.4.0", 19 | "gatsby-plugin-sharp": "4.4.0", 20 | "gatsby-source-graphcms": "2.0.0", 21 | "react": "17.0.2", 22 | "react-dom": "17.0.2" 23 | }, 24 | "devDependencies": { 25 | "babel-preset-gatsby": "^2.4.0", 26 | "postcss-preset-env": "7.2.0", 27 | "tailwindcss": "3.0.12" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /demo/src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Layout({ children }) { 4 | return
{children}
5 | } 6 | 7 | export default Layout 8 | -------------------------------------------------------------------------------- /demo/src/components/paragraph.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Paragraph(props) { 4 | return

5 | } 6 | 7 | export default Paragraph 8 | -------------------------------------------------------------------------------- /demo/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql, Link } from 'gatsby' 3 | import { GatsbyImage, getImage } from 'gatsby-plugin-image' 4 | 5 | const IndexPage = ({ data: { products } }) => { 6 | return ( 7 |

30 | ) 31 | } 32 | 33 | export const query = graphql` 34 | query PageQuery { 35 | products: allGraphCmsProduct( 36 | filter: { locale: { eq: en }, stage: { eq: PUBLISHED } } 37 | ) { 38 | nodes { 39 | formattedPrice 40 | id 41 | images { 42 | gatsbyImageData(layout: FULL_WIDTH, placeholder: BLURRED) 43 | } 44 | locale 45 | name 46 | slug 47 | } 48 | } 49 | } 50 | ` 51 | 52 | export default IndexPage 53 | -------------------------------------------------------------------------------- /demo/src/styles/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply bg-gray-100; 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/templates/product-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import { GatsbyImage, getImage } from 'gatsby-plugin-image' 4 | import { MDXRenderer } from 'gatsby-plugin-mdx' 5 | 6 | const ProductPage = ({ data: { productImages }, pageContext: { product } }) => { 7 | const [mainImage] = productImages.nodes 8 | 9 | return ( 10 |
11 |
12 |

13 | {product.name} 14 |

15 |

16 | {product.formattedPrice} 17 |

18 | {product.description && ( 19 | 20 |
21 | 22 | {product.description.markdownNode.childMdx.body} 23 | 24 |
25 | )} 26 |
27 | {mainImage && ( 28 |
29 | 30 |
31 | )} 32 |
33 | ) 34 | } 35 | 36 | export const query = graphql` 37 | query ProductImageQuery($id: String!) { 38 | productImages: allGraphCmsAsset( 39 | filter: { productImages: { elemMatch: { id: { eq: $id } } } } 40 | ) { 41 | nodes { 42 | gatsbyImageData(layout: FULL_WIDTH) 43 | } 44 | } 45 | } 46 | ` 47 | 48 | export default ProductPage 49 | -------------------------------------------------------------------------------- /demo/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './src/components/**/*.js', 4 | './src/pages/**/*.js', 5 | './src/templates/**/*.js', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | variants: {}, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": true 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": ["@babel/plugin-transform-runtime"], 13 | "only": ["src/", "test/"] 14 | } 15 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/README.md: -------------------------------------------------------------------------------- 1 |

gatsby-source-graphcms

2 |

The official Gatsby source plugin for Hygraph projects

3 | 4 |

5 | 6 | Version 7 | 8 | 9 | Downloads/week 10 | 11 | 12 | License 13 | 14 | 15 | Forks on GitHub 16 | 17 | minified + gzip size 18 | 19 | Contributors 20 | 21 |
22 | Demogatsby-starter-hygraph-blogJoin us on SlackLogin to Hygraph@hygraphcom 23 |

24 | 25 | ## Installation 26 | 27 | ```shell 28 | yarn add gatsby-source-graphcms gatsby-plugin-image 29 | ``` 30 | 31 | > Note: Gatsby v4 requires Node.js >= 14.15. 32 | 33 | ## Configuration 34 | 35 | > We recommend using environment variables with your Hygraph `token` and `endpoint` values. You can learn more about using environment variables with Gatsby [here](https://www.gatsbyjs.org/docs/environment-variables). 36 | 37 | ### Basic 38 | 39 | ```js 40 | // gatsby-config.js 41 | module.exports = { 42 | plugins: [ 43 | { 44 | resolve: 'gatsby-source-graphcms', 45 | options: { 46 | endpoint: process.env.HYGRAPH_ENDPOINT, 47 | }, 48 | }, 49 | ], 50 | } 51 | ``` 52 | 53 | ### Authorization 54 | 55 | You can also provide an auth token using the `token` configuration key. This is necessary if your Hygraph project is **not** publicly available, or you want to scope access to a specific content stage (i.e. draft content). 56 | 57 | ```js 58 | // gatsby-config.js 59 | module.exports = { 60 | plugins: [ 61 | { 62 | resolve: 'gatsby-source-graphcms', 63 | options: { 64 | endpoint: process.env.HYGRAPH_ENDPOINT, 65 | token: process.env.HYGRAPH_TOKEN, 66 | }, 67 | }, 68 | ], 69 | } 70 | ``` 71 | 72 | ### Options 73 | 74 | | Key | Type | Description | 75 | | --------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 76 | | `endpoint` | String (**required**) | The endpoint URL for the Hygraph project. This can be found in the [project settings UI](https://hygraph.com/docs/guides/concepts/apis#working-with-apis). | 77 | | `token` | String | If your Hygraph project is **not** publicly accessible, you will need to provide a [Permanent Auth Token](https://hygraph.com/docs/reference/authorization) to correctly authorize with the API. You can learn more about creating and managing API tokens [here](https://hygraph.com/docs/guides/concepts/apis#working-with-apis). | 78 | | `typePrefix` | String _(Default: `GraphCMS_`)\_ | The string by which every generated type name is prefixed with. For example, a type of `Post` in Hygraph would become `GraphCMS_Post` by default. If using multiple instances of the source plugin, you **must** provide a value here to prevent type conflicts. | 79 | | `downloadLocalImages` | Boolean _(Default: `false`)_ | Download and cache Hygraph image assets in your Gatsby project. [Learn more](#downloading-local-image-assets). | 80 | | `buildMarkdownNodes` | Boolean _(Default: `false`)_ | Build markdown nodes for all [`RichText`](https://hygraph.com/docs/reference/fields/rich-text) fields in your Hygraph schema. [Learn more](#using-markdown-nodes). | 81 | | `fragmentsPath` | String _(Default: `graphcms-fragments`)_ | The local project path where generated query fragments are saved. This is relative to your current working directory. If using multiple instances of the source plugin, you **must** provide a value here to prevent type and/or fragment conflicts. | 82 | | `locales` | String _(Default: `['en']`)_ | An array of locale key strings from your Hygraph project. [Learn more](#querying-localised-nodes). You can read more about working with localisation in Hygraph [here](https://hygraph.com/docs/guides/concepts/i18n). | 83 | | `stages` | String _(Default: `['PUBLISHED']`)_ | An array of Content Stages from your Hygraph project. [Learn more](#querying-from-content-stages). You can read more about using Content Stages [here](https://hygraph.com/guides/working-with-content-stages). | 84 | | `queryConcurrency` | Integer _(Default: 10)_ | The number of promises ran at once when executing queries. | 85 | 86 | ## Features 87 | 88 | - [Installation](#installation) 89 | - [Configuration](#configuration) 90 | - [Basic](#basic) 91 | - [Authorization](#authorization) 92 | - [Options](#options) 93 | - [Features](#features) 94 | - [Querying localised nodes](#querying-localised-nodes) 95 | - [Querying from content stages](#querying-from-content-stages) 96 | - [Usage with `gatsby-plugin-image`](#usage-with-gatsby-plugin-image) 97 | - [`gatsbyImageData` resolver arguments](#gatsbyimagedata-resolver-arguments) 98 | - [Downloading local image assets](#downloading-local-image-assets) 99 | - [Using markdown nodes](#using-markdown-nodes) 100 | - [Usage with `gatsby-plugin-mdx`](#usage-with-gatsby-plugin-mdx) 101 | - [Working with query fragments](#working-with-query-fragments) 102 | - [Modifying query fragments](#modifying-query-fragments) 103 | - [Contributors](#contributors) 104 | - [FAQs](#faqs) 105 | 106 | ### Querying localised nodes 107 | 108 | If using Hygraph localisation, this plugin provides support to build nodes for all provided locales. 109 | 110 | Update your plugin configuration to include the `locales` key. 111 | 112 | ```js 113 | // gatsby-config.js 114 | module.exports = { 115 | plugins: [ 116 | { 117 | resolve: 'gatsby-source-graphcms', 118 | options: { 119 | endpoint: process.env.HYGRAPH_ENDPOINT, 120 | locales: ['en', 'de'], 121 | }, 122 | }, 123 | ], 124 | } 125 | ``` 126 | 127 | To query for nodes for a specific locale, use the `filter` query argument. 128 | 129 | ```gql 130 | { 131 | enProducts: allGraphCmsProduct(filter: { locale: { eq: en } }) { 132 | nodes { 133 | name 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | Check out the [demo source](https://github.com/hygraph/gatsby-source-graphcms/tree/main/demo) for an example of a localisation implementation. 140 | 141 | ### Querying from content stages 142 | 143 | This plugin provides support to build nodes for entries from multiple Content Stages. 144 | 145 | The provided Content Stages **must** be accessible according to the configuration of your project's [API access](https://hygraph.com/docs/authorization). If providing a `token`, then that [Permanent Auth Token](https://hygraph.com/docs/authorization#permanent-auth-tokens) must have permission to query data from all provided Content Stages. 146 | 147 | The example below assumes that both the `DRAFT` and `PUBLISHED` stages are publicly accessible. 148 | 149 | ```js 150 | // gatsby-config.js 151 | module.exports = { 152 | plugins: [ 153 | { 154 | resolve: 'gatsby-source-graphcms', 155 | options: { 156 | endpoint: process.env.HYGRAPH_ENDPOINT, 157 | stages: ['DRAFT', 'PUBLISHED'], 158 | }, 159 | }, 160 | ], 161 | } 162 | ``` 163 | 164 | To query for nodes from a specific Content Stage, use the `filter` query argument. 165 | 166 | ```gql 167 | { 168 | allGraphCmsProduct(filter: { stage: { eq: DRAFT } }) { 169 | nodes { 170 | name 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | ### Usage with `gatsby-plugin-image` 177 | 178 | > Requires [`gatsby-plugin-image`](https://www.gatsbyjs.com/plugins/gatsby-plugin-image) as a project dependency. 179 | 180 | This source plugin supports `gatsby-plugin-image` for responsive, high performance Hygraph images direct from our CDN. 181 | 182 | Use the `gatsbyImageData` resolver on your `GraphCMS_Asset` nodes. 183 | 184 | ```gql 185 | { 186 | allGraphCmsAsset { 187 | nodes { 188 | gatsbyImageData(layout: FULL_WIDTH) 189 | } 190 | } 191 | } 192 | ``` 193 | 194 | #### `gatsbyImageData` resolver arguments 195 | 196 | | Key | Type | Description | 197 | | ---------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 198 | | `aspectRatio` | Float | Force a specific ratio between the image’s width and height. | 199 | | `backgroundColor` | String | Background color applied to the wrapper. | 200 | | `breakpoints` | [Int] | Output widths to generate for full width images. Default is to generate widths for common device resolutions. It will never generate an image larger than the source image. The browser will automatically choose the most appropriate. | 201 | | `height` | Int | Change the size of the image. | 202 | | `layout` | GatsbyImageLayout (`CONSTRAINED`/`FIXED`/`FULL_WIDTH`) | Determines the size of the image and its resizing behavior. | 203 | | `outputPixelDensities` | [Float] | A list of image pixel densities to generate. It will never generate images larger than the source, and will always include a 1x image. The value is multiplied by the image width, to give the generated sizes. For example, a `400` px wide constrained image would generate `100`, `200`, `400` and `800` px wide images by default. Ignored for full width layout images, which use `breakpoints` instead. | 204 | | `quality` | Int | The default image quality generated. This is overridden by any format-specific options. | 205 | | `sizes` | String | [The ` sizes` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attributes), passed to the img tag. This describes the display size of the image, and does not affect generated images. You are only likely to need to change this if your are using full width images that do not span the full width of the screen. | 206 | | `width` | Int | Change the size of the image. | 207 | | `placeholder` | `NONE`/`BLURRED`/`DOMINANT_COLOR`/`TRACED_SVG` | Choose the style of temporary image shown while the full image loads. | 208 | 209 | **NOTE**: `gatsby-plugin-sharp` needs to be listed as a dependency on your project if you plan to use placeholder `TRACED_SVG` or `DOMINANT_COLOR`. 210 | 211 | For more information on using `gatsby-plugin-image`, please see the [documentation](https://www.gatsbyjs.com/plugins/gatsby-plugin-image/). 212 | 213 | ### Downloading local image assets 214 | 215 | If you prefer, the source plugin also provides the option to download and cache Hygraph assets in your Gatsby project. 216 | 217 | To enable this, add `downloadLocalImages: true` to your plugin configuration. 218 | 219 | ```js 220 | // gatsby-config.js 221 | module.exports = { 222 | plugins: [ 223 | { 224 | resolve: 'gatsby-source-graphcms', 225 | options: { 226 | endpoint: process.env.HYGRAPH_ENDPOINT, 227 | downloadLocalImages: true, 228 | }, 229 | }, 230 | ], 231 | } 232 | ``` 233 | 234 | This adds a `localFile` field to the `GraphCMS_Asset` type which resolves to the file node generated at build by [`gatsby-source-filesystem`](https://www.gatsbyjs.org/packages/gatsby-source-filesystem). 235 | 236 | ```gql 237 | { 238 | allGraphCmsAsset { 239 | nodes { 240 | localFile { 241 | childImageSharp { 242 | gatsbyImageData(layout: FULL_WIDTH) 243 | } 244 | } 245 | } 246 | } 247 | } 248 | ``` 249 | 250 | ### Using markdown nodes 251 | 252 | This source plugin provides the option to build markdown nodes for all `RichText` fields in your Hygraph schema, which in turn can be used with [MDX](https://mdxjs.com). 253 | 254 | To enable this, add `buildMarkdownNodes: true` to your plugin configuration. 255 | 256 | ```js 257 | // gatsby-config.js 258 | module.exports = { 259 | plugins: [ 260 | { 261 | resolve: 'gatsby-source-graphcms', 262 | options: { 263 | endpoint: process.env.HYGRAPH_ENDPOINT, 264 | buildMarkdownNodes: true, 265 | }, 266 | }, 267 | ], 268 | } 269 | ``` 270 | 271 | Enabling this option adds a `markdownNode` nested field to all `RichText` fields on the generated Gatsby schema. 272 | 273 | You will need to rebuild your `graphcms-fragments` if you enable embeds on a Rich Text field, or you add/remove additional fields to your Hygraph schema. 274 | 275 | #### Usage with `gatsby-plugin-mdx` 276 | 277 | These newly built nodes can be used with [`gatsby-plugin-mdx`](https://www.gatsbyjs.org/packages/gatsby-plugin-mdx) to render markdown from Hygraph. 278 | 279 | Once installed, you will be able to query for `MDX` fields using a query similar to the one below. 280 | 281 | ```gql 282 | { 283 | allGraphCmsPost { 284 | nodes { 285 | id 286 | content { 287 | markdownNode { 288 | childMdx { 289 | body 290 | } 291 | } 292 | } 293 | } 294 | } 295 | } 296 | ``` 297 | 298 | Check out the [demo source](https://github.com/hygraph/gatsby-source-graphcms/tree/main/demo) for an example of a full MDX implementation. 299 | 300 | ### Working with query fragments 301 | 302 | The source plugin will generate and save GraphQL query fragments for every node type. By default, they will be saved in a `graphcms-fragments` directory at the root of your Gatsby project. This can be configured: 303 | 304 | > If using multiple instances of the source plugin, you **must** provide a value to prevent type and/or fragment conflicts. 305 | 306 | ```js 307 | // gatsby-config.js 308 | module.exports = { 309 | plugins: [ 310 | { 311 | resolve: 'gatsby-source-graphcms', 312 | options: { 313 | endpoint: process.env.HYGRAPH_ENDPOINT, 314 | fragmentsPath: 'my-query-fragments', 315 | }, 316 | }, 317 | ], 318 | } 319 | ``` 320 | 321 | The generated fragments are then read from the project for subsequent builds. It is recommended that they are checked in to version control for your project. 322 | 323 | Should you make any changes or additions to your Hygraph schema, you will need to update the query fragments accrdingly. Alternatively they will be regnerated on a subsequent build after removing the directory from your project. 324 | 325 | #### Modifying query fragments 326 | 327 | In some instances, you may need modify query fragments on a per type basis. This may involve: 328 | 329 | - Removing unrequired fields 330 | - Adding new fields with arguments as an aliased field 331 | 332 | For example, adding a `featuredCaseStudy` field: 333 | 334 | ```graphql 335 | fragment Industry on Industry { 336 | featuredCaseStudy: caseStudies(where: { featured: true }, first: 1) 337 | } 338 | ``` 339 | 340 | Field arguments cannot be read by Gatsby from the Hygraph schema. Instead we must alias any required usages as aliased fields. In this example, the `featuredCaseStudy` field would then be available in our Gatsby queries: 341 | 342 | ```graphql 343 | { 344 | allGraphCmsIndustry { 345 | nodes { 346 | featuredCaseStudy { 347 | ... 348 | } 349 | } 350 | } 351 | } 352 | ``` 353 | 354 | ## Contributors 355 | 356 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 |

Jonathan Steele

💻 📝 💡 🤔 🚧 📆

João Pedro Schmitz

💻 💡 🤔

Jamie Barton

💻 🐛 🚧 📖
368 | 369 | 370 | 371 | 372 | 373 | 374 | If you spot an issue, or bug that you know how to fix, please submit a pull request. 375 | 376 | When working locally you'll need to run `yarn compile` and `yarn dev` using Yarn workspaces that will let you test/develop on the `demo` Gatsby app in this project. You may need to `yarn clean` occcasionally too. 377 | 378 | ## FAQs 379 | 380 |
381 | "endpoint" is required 382 | 383 | If you are using environment variables, make sure to include `require("dotenv").config();` inside your `gatsby-config.js`. 384 | 385 | If it's already included, make sure you have your ENV variable added to `.env`, or `.env.local` without spaces. 386 | 387 |
388 | 389 |
390 | "message": "not allowed" 391 | 392 | This error occurs most likely if your token doesn't have access to the `PUBLISHED` content stage. Configure your token to also access `PUBLISHED`, or specify `stages: ["DRAFT"]` to the options inside `gatsby-config.js`. 393 | 394 |
395 | 396 |
397 | Bad request 398 | 399 | You may need to rebuild your fragments folder when making schema changes. If you change the type of a field, or add/remove any from an existing model you have fragments for, the plugin cannot query for this. 400 | 401 | Simply delete the `graphcms-fragments` (or whatever you named it), and run `gatsby develop` to regenerate. 402 | 403 |
404 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/index.js: -------------------------------------------------------------------------------- 1 | // noop 2 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-source-graphcms", 3 | "version": "2.0.0", 4 | "description": "The official Gatsby source plugin for Hygraph projects", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir ./ --source-maps" 8 | }, 9 | "keywords": [ 10 | "data", 11 | "gatsby", 12 | "gatsby-plugin", 13 | "hygraph", 14 | "graphql", 15 | "source" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "@babel/runtime": "7.16.7", 20 | "gatsby-core-utils": "3.4.0", 21 | "gatsby-graphql-source-toolkit": "^2.0.1", 22 | "gatsby-source-filesystem": "4.4.0", 23 | "he": "1.2.0", 24 | "node-fetch": "2.6.1" 25 | }, 26 | "devDependencies": { 27 | "@babel/cli": "7.16.7", 28 | "@babel/core": "7.16.7", 29 | "@babel/plugin-transform-runtime": "7.16.7", 30 | "@babel/preset-env": "7.16.7" 31 | }, 32 | "peerDependencies": { 33 | "gatsby": "^4.0.0", 34 | "gatsby-plugin-image": "^2.0.0" 35 | }, 36 | "engines": { 37 | "node": ">=14.15.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/src/gatsby-node.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import fs from 'fs' 3 | import { 4 | wrapQueryExecutorWithQueue, 5 | loadSchema, 6 | readOrGenerateDefaultFragments, 7 | compileNodeQueries, 8 | buildNodeDefinitions, 9 | createSchemaCustomization as createToolkitSchemaCustomization, 10 | sourceAllNodes, 11 | sourceNodeChanges, 12 | } from 'gatsby-graphql-source-toolkit' 13 | import { 14 | generateImageData, 15 | getLowResolutionImageURL, 16 | } from 'gatsby-plugin-image' 17 | import { getGatsbyImageResolver } from 'gatsby-plugin-image/graphql-utils' 18 | import { createRemoteFileNode } from 'gatsby-source-filesystem' 19 | import he from 'he' 20 | import fetch from 'node-fetch' 21 | 22 | import { PLUGIN_NAME } from './util/constants' 23 | import { getImageBase64, getBase64DataURI } from './util/getImageBase64' 24 | import { getImageDominantColor } from './util/getDominantColor' 25 | import { getTracedSVG } from './util/getTracedSVG' 26 | import { reportPanic } from './util/reportPanic' 27 | 28 | export function pluginOptionsSchema({ Joi }) { 29 | return Joi.object({ 30 | buildMarkdownNodes: Joi.boolean() 31 | .description( 32 | `Build markdown nodes for all [RichText](https://hygraph.com/docs/reference/fields/rich-text) fields in your Hygraph schema` 33 | ) 34 | .default(false), 35 | downloadLocalImages: Joi.boolean() 36 | .description( 37 | `Download and cache Hygraph image assets in your Gatsby project` 38 | ) 39 | .default(false), 40 | endpoint: Joi.string() 41 | .description( 42 | `The endpoint URL for the Hygraph project. This can be found in the [project settings UI](https://hygraph.com/docs/guides/concepts/apis#working-with-apis)` 43 | ) 44 | .required(), 45 | fragmentsPath: Joi.string() 46 | .description( 47 | `The local project path where generated query fragments are saved. This is relative to your current working directory. If using multiple instances of the source plugin, you **must** provide a value here to prevent type and/or fragment conflicts.` 48 | ) 49 | .default(`graphcms-fragments`), 50 | locales: Joi.array() 51 | .description( 52 | `An array of locale key strings from your Hygraph project. You can read more about working with localisation in Hygraph [here](https://hygraph.com/docs/guides/concepts/i18n).` 53 | ) 54 | .items(Joi.string()) 55 | .min(1) 56 | .default(['en']), 57 | stages: Joi.array() 58 | .description( 59 | `An array of Content Stages from your Hygraph project. You can read more about using Content Stages [here](https://hygraph.com/guides/working-with-content-stages).` 60 | ) 61 | .items(Joi.string()) 62 | .min(1) 63 | .default(['PUBLISHED']), 64 | token: Joi.string().description( 65 | `If your Hygraph project is **not** publicly accessible, you will need to provide a [Permanent Auth Token](https://hygraph.com/docs/reference/authorization) to correctly authorize with the API. You can learn more about creating and managing API tokens [here](https://hygraph.com/docs/guides/concepts/apis#working-with-apis)` 66 | ), 67 | typePrefix: Joi.string() 68 | .description( 69 | `The string by which every generated type name is prefixed with. For example, a type of Post in Hygraph would become GraphCMS_Post by default. If using multiple instances of the source plugin, you **must** provide a value here to prevent type conflicts` 70 | ) 71 | .default(`GraphCMS_`), 72 | queryConcurrency: Joi.number() 73 | .integer() 74 | .min(1) 75 | .default(10) 76 | .description(`The number of promises to run at one time.`), 77 | }) 78 | } 79 | 80 | const createSourcingConfig = async ( 81 | gatsbyApi, 82 | { 83 | endpoint, 84 | fragmentsPath, 85 | locales, 86 | stages, 87 | token, 88 | typePrefix, 89 | queryConcurrency, 90 | } 91 | ) => { 92 | const execute = async ({ operationName, query, variables = {} }) => { 93 | const { reporter } = gatsbyApi 94 | 95 | return await fetch(endpoint, { 96 | method: 'POST', 97 | body: JSON.stringify({ query, variables, operationName }), 98 | headers: { 99 | 'Content-Type': 'application/json', 100 | ...(token && { Authorization: `Bearer ${token}` }), 101 | }, 102 | }) 103 | .then((response) => { 104 | if (!response.ok) { 105 | return reportPanic( 106 | 1, 107 | 'Problem building Hygraph nodes', 108 | response.statusText, 109 | reporter 110 | ) 111 | } 112 | 113 | return response.json() 114 | }) 115 | .then((response) => { 116 | if (response.errors) { 117 | return reportPanic( 118 | 2, 119 | 'Problem building Hygraph nodes', 120 | JSON.stringify(response.errors, null, 2), 121 | reporter 122 | ) 123 | } 124 | 125 | return response 126 | }) 127 | .catch((error) => { 128 | return reportPanic( 129 | 3, 130 | 'Problem building Hygraph nodes', 131 | JSON.stringify(error, null, 2), 132 | reporter 133 | ) 134 | }) 135 | } 136 | const schema = await loadSchema(execute) 137 | 138 | const nodeInterface = schema.getType('Node') 139 | const query = schema.getType('Query') 140 | const queryFields = query.getFields() 141 | const possibleTypes = schema.getPossibleTypes(nodeInterface) 142 | const typeMap = schema.getTypeMap() 143 | 144 | const richTextTypes = Object.keys(typeMap) 145 | .filter((typeName) => typeName.endsWith('RichText')) 146 | .map((value) => value.replace('RichText', '')) 147 | .filter(Boolean) 148 | 149 | const singularRootFieldName = (type) => 150 | Object.keys(queryFields).find( 151 | (fieldName) => queryFields[fieldName].type === type 152 | ) 153 | 154 | const pluralRootFieldName = (type) => 155 | Object.keys(queryFields).find( 156 | (fieldName) => String(queryFields[fieldName].type) === `[${type.name}!]!` 157 | ) 158 | 159 | const hasLocaleField = (type) => type.getFields().locale 160 | 161 | const gatsbyNodeTypes = possibleTypes.map((type) => ({ 162 | remoteTypeName: type.name, 163 | queries: [ 164 | ...locales.map((locale) => 165 | stages.map( 166 | (stage) => ` 167 | query LIST_${pluralRootFieldName( 168 | type 169 | )}_${locale}_${stage} { ${pluralRootFieldName(type)}(first: $limit, ${ 170 | hasLocaleField(type) ? `locales: [${locale}]` : '' 171 | }, skip: $offset, stage: ${stage}) { 172 | ..._${type.name}Id_ 173 | } 174 | }` 175 | ) 176 | ), 177 | `query NODE_${singularRootFieldName(type)}{ ${singularRootFieldName( 178 | type 179 | )}(where: $where, ${hasLocaleField(type) ? `locales: $locales` : ''}) { 180 | ..._${type.name}Id_ 181 | } 182 | } 183 | fragment _${type.name}Id_ on ${type.name} { 184 | __typename 185 | id 186 | ${hasLocaleField(type) ? `locale` : ''} 187 | stage 188 | }`, 189 | ].join('\n'), 190 | nodeQueryVariables: ({ id, locale, stage }) => ({ 191 | where: { id }, 192 | locales: [locale], 193 | stage, 194 | }), 195 | })) 196 | 197 | const fragmentsDir = `${process.cwd()}/${fragmentsPath}` 198 | 199 | if (!fs.existsSync(fragmentsDir)) fs.mkdirSync(fragmentsDir) 200 | 201 | const addSystemFieldArguments = (field) => { 202 | if (['createdAt', 'publishedAt', 'updatedAt'].includes(field.name)) 203 | return { variation: `COMBINED` } 204 | } 205 | 206 | const fragments = await readOrGenerateDefaultFragments(fragmentsDir, { 207 | schema, 208 | gatsbyNodeTypes, 209 | defaultArgumentValues: [addSystemFieldArguments], 210 | }) 211 | 212 | const documents = compileNodeQueries({ 213 | schema, 214 | gatsbyNodeTypes, 215 | customFragments: fragments, 216 | }) 217 | 218 | return { 219 | gatsbyApi, 220 | schema, 221 | execute: wrapQueryExecutorWithQueue(execute, { 222 | concurrency: queryConcurrency, 223 | }), 224 | gatsbyTypePrefix: typePrefix, 225 | gatsbyNodeDefs: buildNodeDefinitions({ gatsbyNodeTypes, documents }), 226 | richTextTypes, 227 | } 228 | } 229 | 230 | export async function createSchemaCustomization(gatsbyApi, pluginOptions) { 231 | const { 232 | webhookBody, 233 | actions: { createTypes }, 234 | } = gatsbyApi 235 | const { 236 | buildMarkdownNodes = false, 237 | downloadLocalImages = false, 238 | typePrefix = 'GraphCMS_', 239 | } = pluginOptions 240 | 241 | const config = await createSourcingConfig(gatsbyApi, pluginOptions) 242 | 243 | const { richTextTypes } = config 244 | 245 | await createToolkitSchemaCustomization(config) 246 | 247 | if (webhookBody && Object.keys(webhookBody).length) { 248 | const { operation, data } = webhookBody 249 | 250 | const nodeEvent = (operation, { __typename, locale, id }) => { 251 | switch (operation) { 252 | case 'delete': 253 | case 'unpublish': 254 | return { 255 | eventName: 'DELETE', 256 | remoteTypeName: __typename, 257 | remoteId: { __typename, locale, id }, 258 | } 259 | case 'create': 260 | case 'publish': 261 | case 'update': 262 | return { 263 | eventName: 'UPDATE', 264 | remoteTypeName: __typename, 265 | remoteId: { __typename, locale, id }, 266 | } 267 | } 268 | } 269 | 270 | const { localizations = [{ locale: 'en' }] } = data 271 | 272 | await sourceNodeChanges(config, { 273 | nodeEvents: localizations.map(({ locale }) => 274 | nodeEvent(operation, { locale, ...data }) 275 | ), 276 | }) 277 | } else { 278 | await sourceAllNodes(config) 279 | } 280 | 281 | if (downloadLocalImages) 282 | createTypes(` 283 | type ${typePrefix}Asset { 284 | localFile: File @link(from: "fields.localFile") 285 | } 286 | `) 287 | 288 | if (buildMarkdownNodes) 289 | createTypes(` 290 | type ${typePrefix}MarkdownNode implements Node { 291 | id: ID! 292 | } 293 | type ${typePrefix}RichText { 294 | markdownNode: ${typePrefix}MarkdownNode @link 295 | } 296 | ${richTextTypes.map( 297 | (typeName) => ` 298 | type ${typePrefix}${typeName}RichText implements Node { 299 | markdownNode: ${typePrefix}MarkdownNode @link 300 | } 301 | ` 302 | )} 303 | `) 304 | } 305 | 306 | export async function onCreateNode( 307 | { 308 | node, 309 | actions: { createNode, createNodeField }, 310 | createNodeId, 311 | getCache, 312 | cache, 313 | }, 314 | { 315 | buildMarkdownNodes = false, 316 | downloadLocalImages = false, 317 | typePrefix = 'GraphCMS_', 318 | } 319 | ) { 320 | if ( 321 | downloadLocalImages && 322 | node.remoteTypeName === 'Asset' && 323 | [ 324 | 'image/png', 325 | 'image/jpg', 326 | 'image/jpeg', 327 | 'image/tiff', 328 | 'image/webp', 329 | ].includes(node.mimeType) 330 | ) { 331 | try { 332 | const fileNode = await createRemoteFileNode({ 333 | url: node.url, 334 | parentNodeId: node.id, 335 | createNode, 336 | createNodeId, 337 | cache, 338 | getCache, 339 | ...(node.fileName && { name: node.fileName }), 340 | }) 341 | 342 | if (fileNode) { 343 | createNodeField({ node, name: 'localFile', value: fileNode.id }) 344 | } 345 | } catch (e) { 346 | console.error(`[${PLUGIN_NAME}]`, e) 347 | } 348 | } 349 | 350 | if (buildMarkdownNodes) { 351 | const fields = Object.entries(node) 352 | .map(([key, value]) => ({ key, value })) 353 | .filter( 354 | ({ value }) => 355 | value && 356 | value.remoteTypeName && 357 | value.remoteTypeName.endsWith('RichText') 358 | ) 359 | 360 | if (fields.length) { 361 | fields.forEach((field) => { 362 | const decodedMarkdown = he.decode(field.value.markdown) 363 | 364 | const markdownNode = { 365 | id: `MarkdownNode:${createNodeId(`${node.id}-${field.key}`)}`, 366 | parent: node.id, 367 | internal: { 368 | type: `${typePrefix}MarkdownNode`, 369 | mediaType: 'text/markdown', 370 | content: decodedMarkdown, 371 | contentDigest: crypto 372 | .createHash(`md5`) 373 | .update(decodedMarkdown) 374 | .digest(`hex`), 375 | }, 376 | } 377 | 378 | createNode(markdownNode) 379 | 380 | field.value.markdownNode = markdownNode.id 381 | }) 382 | } 383 | } 384 | } 385 | 386 | const generateImageSource = ( 387 | baseURL, 388 | width, 389 | height, 390 | format, 391 | fit = 'clip', 392 | { quality = 100 } 393 | ) => { 394 | const src = `https://media.graphcms.com/resize=width:${width},height:${height},fit:${fit}/output=quality:${quality}/${baseURL}` 395 | 396 | return { src, width, height, format } 397 | } 398 | 399 | function makeResolveGatsbyImageData(cache) { 400 | return async function resolveGatsbyImageData( 401 | { handle: filename, height, mimeType, width, url, internal }, 402 | options 403 | ) { 404 | if ( 405 | ![ 406 | 'image/png', 407 | 'image/jpg', 408 | 'image/jpeg', 409 | 'image/tiff', 410 | 'image/webp', 411 | ].includes(mimeType) 412 | ) { 413 | return null 414 | } 415 | 416 | const imageDataArgs = { 417 | ...options, 418 | pluginName: PLUGIN_NAME, 419 | sourceMetadata: { format: mimeType.split('/')[1], height, width }, 420 | filename, 421 | generateImageSource, 422 | options, 423 | } 424 | 425 | if (options?.placeholder === `BLURRED`) { 426 | const lowResImageURL = getLowResolutionImageURL(imageDataArgs) 427 | 428 | const imageBase64 = await getImageBase64({ 429 | url: lowResImageURL, 430 | cache, 431 | }) 432 | 433 | imageDataArgs.placeholderURL = getBase64DataURI({ 434 | imageBase64, 435 | }) 436 | } 437 | 438 | if (options?.placeholder === `DOMINANT_COLOR`) { 439 | const lowResImageURL = getLowResolutionImageURL(imageDataArgs) 440 | 441 | imageDataArgs.backgroundColor = await getImageDominantColor({ 442 | url: lowResImageURL, 443 | cache, 444 | }) 445 | } 446 | 447 | if (options?.placeholder === `TRACED_SVG`) { 448 | imageDataArgs.placeholderURL = await getTracedSVG({ 449 | url, 450 | internal, 451 | filename, 452 | cache, 453 | }) 454 | } 455 | 456 | return generateImageData(imageDataArgs) 457 | } 458 | } 459 | 460 | export function createResolvers( 461 | { createResolvers, cache }, 462 | { typePrefix = 'GraphCMS_', downloadLocalImages = false } 463 | ) { 464 | const args = { 465 | quality: { 466 | type: `Int`, 467 | description: `The default image quality generated. This is overridden by any format-specific options.`, 468 | }, 469 | placeholder: { 470 | type: `enum GraphCMSImagePlaceholder { NONE, BLURRED, DOMINANT_COLOR, TRACED_SVG }`, 471 | description: `The style of temporary image shown while the full image loads. 472 | BLURRED: generates a very low-resolution version of the image and displays it as a blurred background (default). 473 | DOMINANT_COLOR: the dominant color of the image used as a solid background color. 474 | TRACED_SVG: generates a simplified, flat SVG version of the source image, which it displays as a placeholder. 475 | NONE: No placeholder. Use the backgroundColor option to set a static background if you wish. 476 | `, 477 | }, 478 | } 479 | 480 | const resolvers = { 481 | [`${typePrefix}Asset`]: { 482 | gatsbyImageData: { 483 | ...getGatsbyImageResolver(makeResolveGatsbyImageData(cache), args), 484 | type: 'JSON', 485 | }, 486 | }, 487 | ...(downloadLocalImages && { 488 | File: { 489 | gatsbyImageData: { 490 | ...getGatsbyImageResolver(makeResolveGatsbyImageData(cache), args), 491 | type: 'JSON', 492 | }, 493 | }, 494 | }), 495 | } 496 | 497 | createResolvers(resolvers) 498 | } 499 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/src/util/constants.js: -------------------------------------------------------------------------------- 1 | export const PLUGIN_NAME = `gatsby-source-graphcms` 2 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/src/util/getDominantColor.js: -------------------------------------------------------------------------------- 1 | import { fetchRemoteFile } from 'gatsby-core-utils' 2 | 3 | import { PLUGIN_NAME } from './constants' 4 | 5 | export async function getImageDominantColor({ url, cache }) { 6 | try { 7 | const { getDominantColor } = require(`gatsby-plugin-sharp`) 8 | 9 | const filePath = await fetchRemoteFile({ 10 | url, 11 | cache, 12 | }) 13 | 14 | const backgroundColor = await getDominantColor(filePath) 15 | 16 | return backgroundColor 17 | } catch { 18 | console.error( 19 | `[${PLUGIN_NAME}] In order to use the dominant color placeholder, you need to install gatsby-plugin-sharp` 20 | ) 21 | 22 | return `rgba(0, 0, 0, 0.5)` 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/src/util/getImageBase64.js: -------------------------------------------------------------------------------- 1 | import { fetchRemoteFile } from 'gatsby-core-utils' 2 | import { readFileSync } from 'fs' 3 | 4 | export function getBase64DataURI({ imageBase64 }) { 5 | return `data:image/png;base64,${imageBase64}` 6 | } 7 | 8 | export async function getImageBase64({ url, cache }) { 9 | const filePath = await fetchRemoteFile({ 10 | url, 11 | cache, 12 | }) 13 | 14 | const buffer = readFileSync(filePath) 15 | return buffer.toString(`base64`) 16 | } 17 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/src/util/getTracedSVG.js: -------------------------------------------------------------------------------- 1 | import { extname } from 'path' 2 | import { fetchRemoteFile } from 'gatsby-core-utils' 3 | 4 | import { PLUGIN_NAME } from './constants' 5 | 6 | export async function getTracedSVG({ url, internal, filename, cache }) { 7 | try { 8 | const { traceSVG } = require(`gatsby-plugin-sharp`) 9 | 10 | const filePath = await fetchRemoteFile({ 11 | url, 12 | cache, 13 | }) 14 | 15 | const extension = extname(filePath) 16 | 17 | const image = await traceSVG({ 18 | file: { 19 | internal, 20 | name: filename, 21 | extension, 22 | absolutePath: filePath, 23 | }, 24 | args: { toFormat: `` }, 25 | fileArgs: {}, 26 | }) 27 | 28 | return image 29 | } catch (err) { 30 | console.error( 31 | `[${PLUGIN_NAME}] In order to use the traced svg placeholder, you need to install gatsby-plugin-sharp` 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gatsby-source-graphcms/src/util/reportPanic.js: -------------------------------------------------------------------------------- 1 | import { PLUGIN_NAME } from './constants' 2 | 3 | export function reportPanic(id, message, error, reporter) { 4 | return reporter.panic({ 5 | context: { 6 | id, 7 | sourceMessage: `[${PLUGIN_NAME}]: ${message} \n\n ${new Error(error)}`, 8 | }, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "demo", 5 | "gatsby-source-graphcms" 6 | ], 7 | "scripts": { 8 | "build": "yarn workspace demo build", 9 | "clean": "yarn workspace demo clean", 10 | "compile": "yarn workspace gatsby-source-graphcms build", 11 | "dev": "yarn workspace demo dev" 12 | }, 13 | "release": { 14 | "branches": [ 15 | "main", 16 | { 17 | "name": "next", 18 | "prerelease": true 19 | } 20 | ], 21 | "plugins": [ 22 | "@semantic-release/commit-analyzer", 23 | "@semantic-release/release-notes-generator", 24 | [ 25 | "@semantic-release/npm", 26 | { 27 | "pkgRoot": "gatsby-source-graphcms" 28 | } 29 | ], 30 | "@semantic-release/github" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "prettier": "2.5.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true 4 | } 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "automerge": true, 3 | "extends": ["config:base", "schedule:monthly"], 4 | "ignorePresets": [":semanticPrefixFixDepsChoreOthers"], 5 | "major": { 6 | "automerge": false 7 | }, 8 | "semanticCommits": true 9 | } 10 | --------------------------------------------------------------------------------