Skip to content

Commit

Permalink
feat: configurable transform types #245 (#250)
Browse files Browse the repository at this point in the history
Allows for sourced data with invalid data shape by configuring a map from invalid to valid data shape.
  • Loading branch information
raae authored Feb 13, 2024
1 parent 77ae882 commit 202ed12
Show file tree
Hide file tree
Showing 16 changed files with 1,680 additions and 126 deletions.
175 changes: 155 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ With `gatsby-transformer-cloudinary` you may:
- [Install Packages](#install-packages)
- [Configure Plugins](#configure-plugins)
- [Example usage](#example-usage)
- [Sanity.io Configuration](#sanityio-configuration)
- [Contentful Configuration](#contentful-configuration)
- [📤 Upload local images and add Gatsby Image Support](#📤-upload-local-images-and-add-gatsby-image-support)
- [Install Packages](#install-packages-1)
- [Configure Plugins](#configure-plugins-1)
Expand All @@ -32,14 +34,11 @@ With `gatsby-transformer-cloudinary` you may:

## 🖼️ Add Gatsby Image Support to Existing Cloudinary Assets

Use assets hosted by Cloudinary together with Gatsby's Image component:
Information about Existing Cloudinary Assets is sourced into the Gatsby data layer using a plugin like [gatsby-source-contentful](https://www.gatsbyjs.com/plugins/gatsby-source-contentful/), [gatsby-source-sanity](https://www.gatsbyjs.com/plugins/gatsby-source-sanity/) etc. or a custom source plugin.

- The plugin adds the `gatsbyImageData` resolver to each GraphQLType configured.
- The plugin adds the `gatsbyImageData` resolver to the sourced GraphQL Types configured.


This configuration and example assumes your Gatsby Data Layer has at least one node of type `BlogPost` with a `heroImage` field describing an already uploaded Cloudinary asset.

👉 More details in [Transform Type Requierments](#transform-type-requierments).
The GraphQL Type must describe an existing Cloudinary asset and [contain the following data](#transform-type-requierments).

### Install Packages

Expand All @@ -60,11 +59,16 @@ yarn add gatsby-transformer-cloudinary gatsby-plugin-image

module.exports = {
plugins: [
// Some source plugin creating a GraphQL Type named `BlogPostHeroImage`
{
resolve: `gatsby-transformer-cloudinary`,
options: {
// Add the `gatsbyImageData` resolver to `BlogPostHeroImage`
transformTypes: [`BlogPostHeroImage`],
transformTypes: [
// Assumes a GraphQL Type named `BlogPostHeroImage`
// Could be a `BlogPost` node with a `heroImage` field
// with the required data shape
`BlogPostHeroImage`,
],
// Optional transformation option
defaultTransformations: ['c_fill', 'g_auto', 'q_auto'],
},
Expand Down Expand Up @@ -114,30 +118,162 @@ export const query = graphql`
export default BlogPost;
```

### Transform Type Requierments
### Transform Type Requirements

You may add Gatsby Image support to any GraphQL Type describing a Cloudinary assets with this data shape:
Gatsby Image support may be added to any GraphQL Type describing a Cloudinary asset with the following information:

```js
{
// Required
cloudName: "my-amazing-blog",
publicId: "blue-blue-blue",
cloud_name: "my-amazing-blog",
public_id: "blue-blue-blue",
// Optional: Saves a network request for size/format data per image queried if all are added
originalHeight: 360,
originalWidth: 820,
originalFormat: "jpg",
heigh: 360,
width: 820,
format: "jpg",
// Optional: Saves a Cloudinary transformation per image queried with `placeholder=BLURRED` as this value will be used instead
defaultBase64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMMXG/8HwAEwAI0Bj1bnwAAAABJRU5ErkJggg==",
base64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMMXG/8HwAEwAI0Bj1bnwAAAABJRU5ErkJggg==",
// Optional: Saves a Cloudinary transformation per image queried with `placeholder=TRACED_SVG` as this value will be used instead
defaultTracedSVG: "data:image/svg+xml,%3Csvg%20height%3D%229999%22%20viewBox%3D%220%200%209999%209999%22%20width%3D%229999%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m0%200h9999v9999h-9999z%22%20fill%3D%22%23f9fafb%22%2F%3E%3C%2Fsvg%3E",
tracedSVG: "data:image/svg+xml,%3Csvg%20height%3D%229999%22%20viewBox%3D%220%200%209999%209999%22%20width%3D%229999%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m0%200h9999v9999h-9999z%22%20fill%3D%22%23f9fafb%22%2F%3E%3C%2Fsvg%3E",
}
```

To find the GraphQL Type describing your Cloudinary assets use the built in [GraphiQL exlorer](https://www.gatsbyjs.com/docs/how-to/querying-data/running-queries-with-graphiql/). Either hover over the field describing the asset, or look in the "Documentation Explorer".
If the GraphQL Type does not have the required data shape, you may use the `mapping` option to map the data from the sourced data shape to the required data shape:

```js
// File: ./gatsby-config.js

module.exports = {
plugins: [
// Some source plugin creating a GraphQL Type named `BlogPostHeroImage`
{
resolve: `gatsby-transformer-cloudinary`,
options: {
transformTypes: [
{
type: `CustomType`,
mapping: {
// Use a static value
cloudName: () => 'my-cloud',
// Use a differnt key than the default
publicId: 'thePublicId', // default for publicId is `public_id`
// Resolve a value using a function
height: (data) => data.dimensions?.height,
width: (data) => data.dimentions?.width,
},
},
],
// Optional transformation option
defaultTransformations: ['c_fill', 'g_auto', 'q_auto'],
},
},
`gatsby-plugin-image`,
],
};
```
To find the GraphQL Type describing your Cloudinary assets use the built-in [GraphiQL exlorer](https://www.gatsbyjs.com/docs/how-to/querying-data/running-queries-with-graphiql/)](https://www.gatsbyjs.com/docs/how-to/querying-data/running-queries-with-graphiql/). Either hover over the field describing the asset or look in the "Documentation Explorer".
`defaultBase64` and `defaultTracedSVG` is the base64 URI of the placeholder image, it must comply with [RFC 2397](https://tools.ietf.org/html/rfc2397).
### Sanity.io Configuration
If you are using [Sanity.io](https://www.sanity.io/) and the [gatsby-source-sanity](https://www.gatsbyjs.com/plugins/gatsby-source-sanity/) plugin use the following configuration to add the `gatsbyImageData` resolver to the sourced Sanity Cloudinary assets:
```js
// File: ./gatsby-config.js

module.exports = {
plugins: [
{
resolve: `gatsby-source-sanity`,
options: {
projectId: process.env.SANITY_PROJECT_ID,
dataset: process.env.SANITY_DATASET,
token: process.env.SANITY_TOKEN,
},
},
{
resolve: `gatsby-transformer-cloudinary`,
options: {
transformTypes: [
{
type: 'SanityCloudinaryAsset',
mapping: {
// Dynamically get the cloud name
// from SanityCloudinaryAsset.url
cloudName: (data) => {
const findCloudName = new RegExp(
'(cloudinary.com/)([^/]+)',
'i'
);
const result = data.url.match(findCloudName);
return result[1];
},
// Or set it statically if all assets are from the same cloud
// cloudName: () => "my-cloud",
},
},
],
// Optional transformation option
defaultTransformations: ['c_fill', 'g_auto', 'q_auto'],
},
},
`gatsby-plugin-image`,
],
};
```
### Contentful Configuration
If you are using [Contentful](https://www.contentful.com/) and the [gatsby-source-contentful](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-source-contentful) plugin use the following configuration to add the `gatsbyImageData` resolver to the sourced Cloudinary assets:
```js
// File: ./gatsby-config.js

module.exports = {
plugins: [
{
resolve: `gatsby-source-contentful`,
options: {
spaceId: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
},
},
{
resolve: `gatsby-transformer-cloudinary`,
options: {
transformTypes: [
{
// ❗❕ Replace `contentfulBlogPostFeaturedImageJsonNode`
// with the name of the GraphQL Type describing your Cloudinary assets
// will always start with `contentful` and end with `JsonNode`
type: 'contentfulBlogPostFeaturedImageJsonNode',
mapping: {
// Dynamically get the cloud name
// from SanityCloudinaryAsset.url
cloudName: (data) => {
const findCloudName = new RegExp(
'(cloudinary.com/)([^/]+)',
'i'
);
const result = data.url.match(findCloudName);
return result[1];
},
// Or set it statically if all assets are from the same cloud
// cloudName: () => "my-cloud",
},
},
],
// Optional transformation option
defaultTransformations: ['c_fill', 'g_auto', 'q_auto'],
},
},
`gatsby-plugin-image`,
],
};
```
 
## 📤 Upload Local Images and Add Gatsby Image Support
Expand Down Expand Up @@ -439,7 +575,7 @@ An optional array limiting uploads to file nodes with a matching `sourceInstance
An optional array of GraphQL Types to add the `gatsbyImageData` resolver for Gatsby Image support.
**Type:** `[String]`\
**Type:** `[String] | [Object]`\
**Default:** `['CloudinaryAsset']`
### `overwriteExisting`
Expand Down Expand Up @@ -533,7 +669,6 @@ Read the [Gatsby Plugin Image Docs](https://www.gatsbyjs.com/docs/reference/buil
- [Using Cloudinary image service for media optimization](https://www.gatsbyjs.org/docs/using-cloudinary-image-service/)
- [Watch Jason Lengstorf build this plugin's first version](https://www.learnwithjason.dev/build-a-gatsby-transformer-plugin-for-cloudinary)

 
## 🏴‍☠️ Contribute
Expand Down
27 changes: 26 additions & 1 deletion demo/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ module.exports = {
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
path: `${__dirname}/src/content/images`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/content/manual-tests`,
},
},
`gatsby-transformer-remark`,
{
resolve: `gatsby-plugin-image`,
},
Expand All @@ -32,6 +40,23 @@ module.exports = {
'BlogPostHeroImage',
'VariedData',
'EmptyDataCloudinary',
'MarkdownRemarkFrontmatterHeroImage',
// {
// name: 'MarkdownRemarkFrontmatterHeroImage',
// mapping: {
// cloudName: `cloudName`,
// publicId: `publicId`,
// }
// },
{
type: 'MarkdownRemarkFrontmatterHeroImageWithUnconformingShape',
mapping: {
cloudName: `a_cloud_name`,
publicId: (data) => {
return data['a_public_id'];
},
},
},
],
},
},
Expand Down
1 change: 1 addition & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"gatsby-plugin-image": "3.3.2",
"gatsby-source-filesystem": "5.3.1",
"gatsby-transformer-cloudinary": "*",
"gatsby-transformer-remark": "^6.13.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
Expand Down
File renamed without changes
File renamed without changes
File renamed without changes
12 changes: 12 additions & 0 deletions demo/src/content/manual-tests/test1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Data Shape Tests
slug: shape-tests
heroImage:
cloudName: lilly-labs-consulting
publicId: gatsby-transformer-cloudinary-demo/photo-1631462685412-80a75dd611bc
alt: A fine hero image from Cloudinary
heroImageWithUnconformingShape:
a_cloud_name: lilly-labs-consulting
a_public_id: gatsby-transformer-cloudinary-demo/photo-1631462685412-80a75dd611bc
alt: A fine hero image from Cloudinary
---
55 changes: 55 additions & 0 deletions demo/src/pages/manual-tests/{markdownRemark.frontmatter__slug}.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react';
import { graphql } from 'gatsby';
import { GatsbyImage } from 'gatsby-plugin-image';

export default function BlogPostTemplate({ data }) {
const { markdownRemark } = data;
const { frontmatter, html } = markdownRemark;
const { title, heroImage, heroImageWithUnconformingShape } = frontmatter;
return (
<>
<h2>{title}</h2>

{heroImage?.gatsbyImageData ? (
<GatsbyImage image={heroImage.gatsbyImageData} alt={heroImage.alt} />
) : (
<p>
No gatsby image for <code>heroImage</code>
</p>
)}

{heroImageWithUnconformingShape?.gatsbyImageData ? (
<GatsbyImage
image={heroImageWithUnconformingShape.gatsbyImageData}
alt={heroImageWithUnconformingShape.alt}
/>
) : (
<p>
No gatsby image for <code>heroImageWithUnconformingShape</code>
</p>
)}

<div dangerouslySetInnerHTML={{ __html: html }} />
</>
);
}

export const pageQuery = graphql`
query ($id: String!) {
markdownRemark(id: { eq: $id }) {
html
frontmatter {
slug
title
heroImage {
gatsbyImageData(height: 200, layout: FIXED)
alt
}
heroImageWithUnconformingShape {
gatsbyImageData(height: 200, layout: FIXED)
alt
}
}
}
}
`;
30 changes: 29 additions & 1 deletion plugin/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,35 @@ exports.pluginOptionsSchema = ({ Joi }) => {
uploadFolder: Joi.string(),
uploadSourceInstanceNames: Joi.array().items(Joi.string()),
transformTypes: Joi.array()
.items(Joi.string())
.items(
Joi.string(),
Joi.object({
type: Joi.string().required(),
mapping: Joi.object({
cloudName: Joi.alternatives()
.try(Joi.function(), Joi.string())
.default('cloud_name'),
publicId: Joi.alternatives()
.try(Joi.function(), Joi.string())
.default('public_id'),
height: Joi.alternatives()
.try(Joi.function(), Joi.string())
.default('height'),
width: Joi.alternatives()
.try(Joi.function(), Joi.string())
.default('width'),
format: Joi.alternatives()
.try(Joi.function(), Joi.string())
.default('format'),
base64: Joi.alternatives()
.try(Joi.function(), Joi.string())
.default('base64'),
tracedSVG: Joi.alternatives()
.try(Joi.function(), Joi.string())
.default('tracedSVG'),
}),
})
)
.default(['CloudinaryAsset']),
overwriteExisting: Joi.boolean().default(false),
defaultTransformations: Joi.array()
Expand Down
Loading

0 comments on commit 202ed12

Please sign in to comment.