diff --git a/packages/gatsby-source-contentful/README.md b/packages/gatsby-source-contentful/README.md index 970346f2a869f..634a9d19e4543 100644 --- a/packages/gatsby-source-contentful/README.md +++ b/packages/gatsby-source-contentful/README.md @@ -181,10 +181,6 @@ Number of entries to retrieve from Contentful at a time. Due to some technical l Number of workers to use when downloading contentful assets. Due to technical limitations, opening too many concurrent requests can cause stalled downloads. If you encounter this issue you can set this param to a lower number than 50, e.g 25. -**`richText.resolveFieldLocales`** [boolean][optional] [default: `false`] - -If you want to resolve the locales in fields of assets and entries that are referenced by rich text (e.g., via embedded entries or entry hyperlinks), set this to `true`. Otherwise, fields of referenced assets or entries will be objects keyed by locale. - ## Notes on Contentful Content Models There are currently some things to keep in mind when building your content models at Contentful. @@ -364,13 +360,31 @@ It is strongly recommended that you take a look at how data flows in a real Cont Rich Text feature is supported in this source plugin, you can use the following query to get the json output: +### Query Rich Text content and references + ```graphql { allContentfulBlogPost { edges { node { bodyRichText { - json + raw + references { + ... on ContentfulAsset { + contentful_id + fixed(width: 1600) { + width + height + src + srcSet + } + } + ... on ContentfulBlogPost { + contentful_id + title + slug + } + } } } } @@ -378,11 +392,11 @@ Rich Text feature is supported in this source plugin, you can use the following } ``` -To define a way Rich Text document is rendered, you can use `@contentful/rich-text-react-renderer` package: +### Rendering ```jsx import { BLOCKS, MARKS } from "@contentful/rich-text-types" -import { documentToReactComponents } from "@contentful/rich-text-react-renderer" +import { renderRichText } from "gatsby-source-contentful/rich-text" const Bold = ({ children }) => {children} const Text = ({ children }) =>

{children}

@@ -393,10 +407,24 @@ const options = { }, renderNode: { [BLOCKS.PARAGRAPH]: (node, children) => {children}, + [BLOCKS.EMBEDDED_ASSET]: node => { + return ( + <> +

Embedded Asset

+
+            {JSON.stringify(node, null, 2)}
+          
+ + ) + }, }, } -documentToReactComponents(node.bodyRichText.json, options) +function BlogPostTemplate({ data, pageContext }) { + const { bodyRichText } = data.contentfulBlogPost + + return
{bodyRichText && renderRichText(richTextField, options)}
+} ``` Check out the examples at [@contentful/rich-text-react-renderer](https://github.com/contentful/rich-text/tree/master/packages/rich-text-react-renderer). diff --git a/packages/gatsby-source-contentful/index.d.ts b/packages/gatsby-source-contentful/index.d.ts new file mode 100644 index 0000000000000..542b1609adf03 --- /dev/null +++ b/packages/gatsby-source-contentful/index.d.ts @@ -0,0 +1 @@ +declare module "gatsby-source-contentful" {} diff --git a/packages/gatsby-source-contentful/package.json b/packages/gatsby-source-contentful/package.json index 4f029207e3c7c..e36f9c110528c 100644 --- a/packages/gatsby-source-contentful/package.json +++ b/packages/gatsby-source-contentful/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@babel/runtime": "^7.11.2", + "@contentful/rich-text-react-renderer": "^14.1.1", "@contentful/rich-text-types": "^14.1.1", "@hapi/joi": "^15.1.1", "axios": "^0.21.0", @@ -55,5 +56,6 @@ }, "engines": { "node": ">=10.13.0" - } + }, + "types": "index.d.ts" } diff --git a/packages/gatsby-source-contentful/rich-text.d.ts b/packages/gatsby-source-contentful/rich-text.d.ts new file mode 100644 index 0000000000000..fb43b376ecbcd --- /dev/null +++ b/packages/gatsby-source-contentful/rich-text.d.ts @@ -0,0 +1,19 @@ +import { ReactNode } from "react" +import { Options } from "@contentful/rich-text-react-renderer" + +interface ContentfulRichTextGatsbyReference { + /** + * Either ContentfulAsset for assets or ContentfulYourContentTypeName for content types + */ + __typename: string + contentful_id: string +} + +interface RenderRichTextData { + raw: string + references: T[] +} + +export function renderRichText< + TReference extends ContentfulRichTextGatsbyReference +>(data: RenderRichTextData, options?: Options): ReactNode diff --git a/packages/gatsby-source-contentful/scripts/generate-fetch-fixtures.js b/packages/gatsby-source-contentful/scripts/generate-fetch-fixtures.js new file mode 100644 index 0000000000000..9bbbea88b4c5d --- /dev/null +++ b/packages/gatsby-source-contentful/scripts/generate-fetch-fixtures.js @@ -0,0 +1,35 @@ +// Helper script to generate the test fixtures for the contentfulFetch call + +import { writeFileSync } from "fs" +import { resolve } from "path" + +import fetch from "gatsby-source-contentful/src/fetch" + +let syncToken = `` +const reporter = { + verbose: console.log, + info: console.info, + panic: console.error, +} + +const generateFixutures = async () => { + const pluginConfig = new Map([ + [`spaceId`, `ahntqop9oi7x`], + [`accessToken`, `JW5Rm2ZoQl6eoqxcTC1b0J_4eUmXoBljtY9aLGRhRYw`], + [`environment`, `master`], + ]) + pluginConfig.getOriginalPluginOptions = () => { + return {} + } + + const result = await fetch({ syncToken, reporter, pluginConfig }) + + writeFileSync( + resolve(__dirname, `fetch-result.json`), + JSON.stringify(result, null, 2) + ) + + console.log(`Updated result json`) +} + +generateFixutures() diff --git a/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js b/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js new file mode 100644 index 0000000000000..9d23588b59277 --- /dev/null +++ b/packages/gatsby-source-contentful/src/__fixtures__/rich-text-data.js @@ -0,0 +1,926 @@ +exports.initialSync = () => { + return { + currentSyncData: { + entries: [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + id: `6KpLS2NZyB3KAvDzWf4Ukh`, + type: `Entry`, + createdAt: `2020-10-16T11:50:44.939Z`, + updatedAt: `2020-10-16T11:50:44.939Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + contentType: { + sys: { + type: `Link`, + linkType: `ContentType`, + id: `page`, + }, + }, + }, + fields: { + title: { + "en-US": `Home`, + }, + slug: { + "en-US": `home`, + }, + content: { + "en-US": { + nodeType: `document`, + data: {}, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `This is the homepage`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-1`, + content: [ + { + nodeType: `text`, + value: `Heading 1`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-2`, + content: [ + { + nodeType: `text`, + value: `Heading 2`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-3`, + content: [ + { + nodeType: `text`, + value: `Heading 3`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-4`, + content: [ + { + nodeType: `text`, + value: `Heading 4`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-5`, + content: [ + { + nodeType: `text`, + value: `Heading 5`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-6`, + content: [ + { + nodeType: `text`, + value: `Heading 6`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `This is `, + marks: [], + data: {}, + }, + { + nodeType: `text`, + value: `bold `, + marks: [ + { + type: `bold`, + }, + ], + data: {}, + }, + { + nodeType: `text`, + value: `and `, + marks: [], + data: {}, + }, + { + nodeType: `text`, + value: `italic`, + marks: [ + { + type: `italic`, + }, + ], + data: {}, + }, + { + nodeType: `text`, + value: ` and `, + marks: [], + data: {}, + }, + { + nodeType: `text`, + value: `both`, + marks: [ + { + type: `bold`, + }, + { + type: `italic`, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `unordered-list`, + content: [ + { + nodeType: `list-item`, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Very`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `list-item`, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `useful`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `list-item`, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `list`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `blockquote`, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `This is a quote`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-2`, + content: [ + { + nodeType: `text`, + value: `Reference tests:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Inline Link: `, + marks: [], + data: {}, + }, + { + nodeType: `embedded-entry-inline`, + content: [], + data: { + target: { + sys: { + id: `7oHxo6bs0us9wIkq27qdyK`, + type: `Link`, + linkType: `Entry`, + }, + }, + }, + }, + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Link in list:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `ordered-list`, + content: [ + { + nodeType: `list-item`, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + { + nodeType: `embedded-entry-inline`, + content: [], + data: { + target: { + sys: { + id: `6KpLS2NZyB3KAvDzWf4Ukh`, + type: `Link`, + linkType: `Entry`, + }, + }, + }, + }, + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Embedded Entity:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `embedded-entry-block`, + content: [], + data: { + target: { + sys: { + id: `7oHxo6bs0us9wIkq27qdyK`, + type: `Link`, + linkType: `Entry`, + }, + }, + }, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-2`, + content: [ + { + nodeType: `text`, + value: `Embedded Asset:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `embedded-asset-block`, + content: [], + data: { + target: { + sys: { + id: `4ZQrqcrTunWiuNaavhGYNT`, + type: `Link`, + linkType: `Asset`, + }, + }, + }, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + }, + }, + }, + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + id: `7oHxo6bs0us9wIkq27qdyK`, + type: `Entry`, + createdAt: `2020-10-16T11:50:44.929Z`, + updatedAt: `2020-10-16T11:50:44.929Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + contentType: { + sys: { + type: `Link`, + linkType: `ContentType`, + id: `page`, + }, + }, + }, + fields: { + title: { + "en-US": `Second Page`, + }, + slug: { + "en-US": `second-page`, + }, + content: { + "en-US": { + nodeType: `document`, + data: {}, + content: [ + { + nodeType: `heading-1`, + content: [ + { + nodeType: `text`, + value: `Welcome to page #2`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Lets create a loop and go home: `, + marks: [], + data: {}, + }, + { + nodeType: `embedded-entry-inline`, + content: [], + data: { + target: { + sys: { + id: `6KpLS2NZyB3KAvDzWf4Ukh`, + type: `Link`, + linkType: `Entry`, + }, + }, + }, + }, + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-2`, + content: [ + { + nodeType: `text`, + value: `Asset Embed:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `embedded-asset-block`, + content: [], + data: { + target: { + sys: { + id: `5yedy6m6500gVbtpNh8bEm`, + type: `Link`, + linkType: `Asset`, + }, + }, + }, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + }, + }, + }, + }, + ], + assets: [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + id: `4ZQrqcrTunWiuNaavhGYNT`, + type: `Asset`, + createdAt: `2020-10-16T11:47:54.157Z`, + updatedAt: `2020-10-16T11:47:54.158Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + }, + fields: { + title: { + "en-US": `Contentful Logo`, + }, + file: { + "en-US": { + url: `//images.ctfassets.net/ahntqop9oi7x/4ZQrqcrTunWiuNaavhGYNT/acadab6d2776e8b87e03707a71d026d9/347966-contentful-logo-wordmark-dark__1_-4cd185-original-1582664935.png`, + details: { + size: 25053, + image: { + width: 1646, + height: 338, + }, + }, + fileName: `347966-contentful-logo-wordmark-dark (1)-4cd185-original-1582664935.png`, + contentType: `image/png`, + }, + }, + }, + }, + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + id: `5yedy6m6500gVbtpNh8bEm`, + type: `Asset`, + createdAt: `2020-10-16T11:47:43.597Z`, + updatedAt: `2020-10-16T11:47:43.597Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + }, + fields: { + title: { + "en-US": `Gatsby Logo`, + }, + file: { + "en-US": { + url: `//images.ctfassets.net/ahntqop9oi7x/5yedy6m6500gVbtpNh8bEm/b44e93247450ea74f38d6f641f419550/Gatsby_Logo.png`, + details: { + size: 37313, + image: { + width: 2000, + height: 555, + }, + }, + fileName: `Gatsby_Logo.png`, + contentType: `image/png`, + }, + }, + }, + }, + ], + deletedEntries: [], + deletedAssets: [], + nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw3CmxolIsOxF10EMMOGCXM-IFrCrhc0LUPDvkjDkms7w5gLw4sqw4_CvxsiZMOFFsOawpM8R8OVPAhMJ8O1w6zCmg`, + }, + contentTypeItems: [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + id: `page`, + type: `ContentType`, + createdAt: `2020-10-16T11:43:48.221Z`, + updatedAt: `2020-10-16T11:44:25.392Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 2, + }, + displayField: `title`, + name: `Page`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `slug`, + name: `Slug`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `content`, + name: `Content`, + type: `RichText`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + ], + defaultLocale: `en-US`, + locales: [ + { + code: `en-US`, + name: `English (United States)`, + default: true, + fallbackCode: null, + sys: { + id: `1uSElBQA68GRKF30tpTxxT`, + type: `Locale`, + version: 1, + }, + }, + { + code: `nl`, + name: `Dutch`, + default: false, + fallbackCode: `en-US`, + sys: { + id: `2T7M2OzIrvE8cOCOF1HMuY`, + type: `Locale`, + version: 1, + }, + }, + ], + space: { + sys: { + type: `Space`, + id: `ahntqop9oi7x`, + }, + name: `Rich Text`, + locales: [ + { + code: `en-US`, + default: true, + name: `English (United States)`, + fallbackCode: null, + }, + ], + }, + } +} +exports.deleteLinkedPage = () => { + return { + currentSyncData: { + entries: [], + assets: [], + deletedEntries: [ + { + sys: { + type: `DeletedEntry`, + id: `7oHxo6bs0us9wIkq27qdyK`, + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 1, + createdAt: `2020-10-16T12:29:38.094Z`, + updatedAt: `2020-10-16T12:29:38.094Z`, + deletedAt: `2020-10-16T12:29:38.094Z`, + }, + }, + ], + deletedAssets: [], + nextSyncToken: `FEnChMOBwr1Yw4TCqsK2LcKpCH3CjsORIyLDrGbDtgozw6xreMKCwpjCtlxATw3CqcO3w6XCrMKuITDDiEoQSMKvIMOYwrzCn3sHPH3CvsK3w4A9w6LCjsOVwrjCjGwbw4rCl0fDl8OhU8Oqw67DhMOCwozDmxrChsOtRD4`, + }, + contentTypeItems: [ + { + sys: { + space: { + sys: { + type: `Link`, + linkType: `Space`, + id: `ahntqop9oi7x`, + }, + }, + id: `page`, + type: `ContentType`, + createdAt: `2020-10-16T11:43:48.221Z`, + updatedAt: `2020-10-16T11:44:25.392Z`, + environment: { + sys: { + id: `master`, + type: `Link`, + linkType: `Environment`, + }, + }, + revision: 2, + }, + displayField: `title`, + name: `Page`, + description: ``, + fields: [ + { + id: `title`, + name: `Title`, + type: `Symbol`, + localized: false, + required: false, + disabled: false, + omitted: false, + }, + { + id: `slug`, + name: `Slug`, + type: `Symbol`, + localized: false, + required: true, + disabled: false, + omitted: false, + }, + { + id: `content`, + name: `Content`, + type: `RichText`, + localized: true, + required: false, + disabled: false, + omitted: false, + }, + ], + }, + ], + defaultLocale: `en-US`, + locales: [ + { + code: `en-US`, + name: `English (United States)`, + default: true, + fallbackCode: null, + sys: { + id: `1uSElBQA68GRKF30tpTxxT`, + type: `Locale`, + version: 1, + }, + }, + { + code: `nl`, + name: `Dutch`, + default: false, + fallbackCode: `en-US`, + sys: { + id: `2T7M2OzIrvE8cOCOF1HMuY`, + type: `Locale`, + version: 1, + }, + }, + ], + space: { + sys: { + type: `Space`, + id: `ahntqop9oi7x`, + }, + name: `Rich Text`, + locales: [ + { + code: `en-US`, + default: true, + name: `English (United States)`, + fallbackCode: null, + }, + ], + }, + } +} diff --git a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap index da116a5ba1719..52f9b00ec29fc 100644 --- a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap +++ b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/gatsby-node.js.snap @@ -445,3 +445,19 @@ Object { "updatedAt": "2020-06-03T14:17:31.246Z", } `; + +exports[`gatsby-node stores rich text as raw with references attached 1`] = ` +Array [ + "ahntqop9oi7x___7oHxo6bs0us9wIkq27qdyK___Entry", + "ahntqop9oi7x___6KpLS2NZyB3KAvDzWf4Ukh___Entry", + "ahntqop9oi7x___4ZQrqcrTunWiuNaavhGYNT___Asset", +] +`; + +exports[`gatsby-node stores rich text as raw with references attached 2`] = ` +Array [ + "ahntqop9oi7x___7oHxo6bs0us9wIkq27qdyK___Entry___nl", + "ahntqop9oi7x___6KpLS2NZyB3KAvDzWf4Ukh___Entry___nl", + "ahntqop9oi7x___4ZQrqcrTunWiuNaavhGYNT___Asset___nl", +] +`; diff --git a/packages/gatsby-source-contentful/src/__tests__/__snapshots__/rich-text.js.snap b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/rich-text.js.snap new file mode 100644 index 0000000000000..6960289f3fd26 --- /dev/null +++ b/packages/gatsby-source-contentful/src/__tests__/__snapshots__/rich-text.js.snap @@ -0,0 +1,226 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`rich text renders with custom options 1`] = ` +
+

+ This is the homepage +

+

+ Heading 1 +

+

+ Heading 2 +

+

+ Heading 3 +

+

+ Heading 4 +

+
+ Heading 5 +
+
+ Heading 6 +
+

+ This is + + bold + + and + + italic + + and + + + both + + +

+
    +
  • +

    + Very +

    +
  • +
  • +

    + useful +

    +
  • +
  • +

    + list +

    +
  • +
+
+

+ This is a quote +

+
+

+ Reference tests: +

+

+ Inline Link: + + Resolved inline Entry ( + 7oHxo6bs0us9wIkq27qdyK + ) + + +

+

+ Link in list: +

+
    +
  1. +

    + + + Resolved inline Entry ( + 6KpLS2NZyB3KAvDzWf4Ukh + ) + + +

    +
  2. +
+

+ Embedded Entity: +

+

+ Resolved embedded Entry: + Second Page + ( + 7oHxo6bs0us9wIkq27qdyK + ) +

+

+ +

+

+ Embedded Asset: +

+

+ Resolved embedded Asset: + Contentful Logo + ( + 4ZQrqcrTunWiuNaavhGYNT + ) +

+

+ +

+
+`; + +exports[`rich text renders with default options 1`] = ` +
+

+ This is the homepage +

+

+ Heading 1 +

+

+ Heading 2 +

+

+ Heading 3 +

+

+ Heading 4 +

+
+ Heading 5 +
+
+ Heading 6 +
+

+ This is + + bold + + and + + italic + + and + + + both + + +

+
    +
  • +

    + Very +

    +
  • +
  • +

    + useful +

    +
  • +
  • +

    + list +

    +
  • +
+
+

+ This is a quote +

+
+

+ Reference tests: +

+

+ Inline Link: + + type: + embedded-entry-inline + id: + 7oHxo6bs0us9wIkq27qdyK + + +

+

+ Link in list: +

+
    +
  1. +

    + + + type: + embedded-entry-inline + id: + 6KpLS2NZyB3KAvDzWf4Ukh + + +

    +
  2. +
+

+ Embedded Entity: +

+
+

+ +

+

+ Embedded Asset: +

+

+ +

+
+`; diff --git a/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js b/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js index 36abe44e039f1..97f8f194ae00b 100644 --- a/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/__tests__/gatsby-node.js @@ -12,6 +12,7 @@ const fetch = require(`../fetch`) const normalize = require(`../normalize`) const startersBlogFixture = require(`../__fixtures__/starter-blog-data`) +const richTextFixture = require(`../__fixtures__/rich-text-data`) const pluginOptions = { spaceId: `testSpaceId` } @@ -669,4 +670,36 @@ describe(`gatsby-node`, () => { locales ) }) + + it(`stores rich text as raw with references attached`, async () => { + fetch.mockImplementationOnce(richTextFixture.initialSync) + + // initial sync + await gatsbyNode.sourceNodes( + { + actions, + store, + getNodes, + getNode, + reporter, + createNodeId, + cache, + getCache, + schema, + }, + pluginOptions + ) + + const initNodes = getNodes() + + const homeNodes = initNodes.filter( + ({ contentful_id }) => contentful_id === `6KpLS2NZyB3KAvDzWf4Ukh` + ) + homeNodes.forEach(homeNode => { + expect(homeNode.content.references___NODE).toStrictEqual([ + ...new Set(homeNode.content.references___NODE), + ]) + expect(homeNode.content.references___NODE).toMatchSnapshot() + }) + }) }) diff --git a/packages/gatsby-source-contentful/src/__tests__/rich-text.js b/packages/gatsby-source-contentful/src/__tests__/rich-text.js index 295566b0bc0fd..a7798736c7f79 100644 --- a/packages/gatsby-source-contentful/src/__tests__/rich-text.js +++ b/packages/gatsby-source-contentful/src/__tests__/rich-text.js @@ -1,355 +1,516 @@ -const { getNormalizedRichTextField } = require(`../rich-text`) - -const entryFactory = () => { - return { - sys: { - id: `abc123`, - contentType: { sys: { id: `article` } }, - type: `Entry`, +import React from "react" +import { render } from "@testing-library/react" +import { renderRichText } from "gatsby-source-contentful/rich-text" +import { BLOCKS, INLINES } from "@contentful/rich-text-types" +import { initialSync } from "../__fixtures__/rich-text-data" +import { cloneDeep } from "lodash" + +const raw = JSON.stringify({ + nodeType: `document`, + data: {}, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `This is the homepage`, + marks: [], + data: {}, + }, + ], + data: {}, }, - fields: { - title: { en: `Title`, de: `Titel` }, - relatedArticle: { - en: { - sys: { - contentType: { sys: { id: `article` } }, - type: `Entry`, - }, - fields: { - title: { en: `Title two`, de: `Titel zwei` }, - }, + { + nodeType: `heading-1`, + content: [ + { + nodeType: `text`, + value: `Heading 1`, + marks: [], + data: {}, }, - }, + ], + data: {}, }, - } -} - -const assetFactory = () => { - return { - sys: { - type: `Asset`, + { + nodeType: `heading-2`, + content: [ + { + nodeType: `text`, + value: `Heading 2`, + marks: [], + data: {}, + }, + ], + data: {}, }, - fields: { - file: { - en: { - url: `//images.ctfassets.net/asset.jpg`, + { + nodeType: `heading-3`, + content: [ + { + nodeType: `text`, + value: `Heading 3`, + marks: [], + data: {}, }, - }, + ], + data: {}, }, - } -} - -describe(`getNormalizedRichTextField()`, () => { - let contentTypesById - let currentLocale - let defaultLocale - let getField - - beforeEach(() => { - contentTypesById = new Map() - contentTypesById.set(`article`, { - sys: { id: `article` }, - fields: [ - { id: `title`, localized: true }, - { id: `relatedArticle`, localized: false }, + { + nodeType: `heading-4`, + content: [ + { + nodeType: `text`, + value: `Heading 4`, + marks: [], + data: {}, + }, ], - }) - currentLocale = `en` - defaultLocale = `en` - getField = field => field[currentLocale] - }) - - describe(`when the rich-text object has no entry references`, () => { - it(`returns the object as-is`, () => { - const field = { - nodeType: `document`, - data: {}, - content: [ - { - nodeType: `text`, - data: {}, - content: `This is a test`, - }, - ], - } - expect( - getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }) - ).toEqual(field) - }) - }) - - describe(`when a rich-text node contains an entry reference`, () => { - describe(`a localized field`, () => { - describe(`when the current locale is \`en\``, () => { - beforeEach(() => (currentLocale = `en`)) - - it(`resolves the locale for the field`, () => { - const field = { - nodeType: `document`, - data: {}, - content: [ - { - nodeType: `embedded-entry-inline`, - data: { target: entryFactory() }, - }, - ], - } - - const expectedTitle = `Title` - const actualTitle = getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }).content[0].data.target.fields.title - - expect(actualTitle).toBe(expectedTitle) - }) - }) - - describe(`when the current locale is \`de\``, () => { - beforeEach(() => (currentLocale = `de`)) - - it(`resolves the locale for the field`, () => { - const field = { - nodeType: `document`, - data: {}, - content: [ - { - nodeType: `embedded-entry-inline`, - data: { target: entryFactory() }, - }, - ], - } - - const expectedTitle = `Titel` - const actualTitle = getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }).content[0].data.target.fields.title - - expect(actualTitle).toBe(expectedTitle) - }) - }) - }) - - describe(`a nested localized field`, () => { - beforeEach(() => { - currentLocale = `de` - }) - - it(`resolves the locale for the nested field`, () => { - const field = { - nodeType: `document`, + data: {}, + }, + { + nodeType: `heading-5`, + content: [ + { + nodeType: `text`, + value: `Heading 5`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-6`, + content: [ + { + nodeType: `text`, + value: `Heading 6`, + marks: [], data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `This is `, + marks: [], + data: {}, + }, + { + nodeType: `text`, + value: `bold `, + marks: [ + { + type: `bold`, + }, + ], + data: {}, + }, + { + nodeType: `text`, + value: `and `, + marks: [], + data: {}, + }, + { + nodeType: `text`, + value: `italic`, + marks: [ + { + type: `italic`, + }, + ], + data: {}, + }, + { + nodeType: `text`, + value: ` and `, + marks: [], + data: {}, + }, + { + nodeType: `text`, + value: `both`, + marks: [ + { + type: `bold`, + }, + { + type: `italic`, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `unordered-list`, + content: [ + { + nodeType: `list-item`, content: [ { - nodeType: `embedded-entry-inline`, - data: { target: entryFactory() }, + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Very`, + marks: [], + data: {}, + }, + ], + data: {}, }, ], - } - - const expectedTitle = `Titel zwei` - const actualTitle = getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }).content[0].data.target.fields.relatedArticle.fields.title - - expect(actualTitle).toBe(expectedTitle) - }) - }) - }) - - describe(`when a rich-text node contains an asset reference`, () => { - describe(`when the current locale is \`en\``, () => { - beforeEach(() => (currentLocale = `en`)) - - it(`resolves the locale for the field`, () => { - const field = { - nodeType: `document`, data: {}, + }, + { + nodeType: `list-item`, content: [ { - nodeType: `embedded-asset-block`, - data: { target: assetFactory() }, + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `useful`, + marks: [], + data: {}, + }, + ], + data: {}, }, ], - } - - const expectedURL = `//images.ctfassets.net/asset.jpg` - const actualURL = getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }).content[0].data.target.fields.file.url - - expect(actualURL).toBe(expectedURL) - }) - }) - }) - - describe(`when a referenced entry contains an asset field`, () => { - describe(`when the current locale is \`en\``, () => { - beforeEach(() => { - contentTypesById.get(`article`).fields.push({ - id: `assetReference`, - localized: true, - }) - currentLocale = `en` - }) - - it(`resolves the locale for the asset's own fields`, () => { - const field = { - nodeType: `document`, data: {}, + }, + { + nodeType: `list-item`, content: [ { - nodeType: `embedded-entry-block`, - data: { target: entryFactory() }, + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `list`, + marks: [], + data: {}, + }, + ], + data: {}, }, ], - } - - field.content[0].data.target.fields.assetReference = { - en: assetFactory(), - } - - const expectedURL = `//images.ctfassets.net/asset.jpg` - const actualURL = getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }).content[0].data.target.fields.assetReference.fields.file.url - - expect(actualURL).toBe(expectedURL) - }) - }) - }) - - describe(`when an entry/asset reference field is an array`, () => { - beforeEach(() => { - contentTypesById.get(`article`).fields.push({ - id: `relatedArticles`, - localized: false, - }) - contentTypesById.get(`article`).fields.push({ - id: `relatedAssets`, - localized: false, - }) - }) - - it(`resolves the locales of each entry in the array`, () => { - const field = { - nodeType: `document`, - data: {}, - content: [ - { - nodeType: `embedded-entry-block`, - data: { target: entryFactory() }, + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `blockquote`, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `This is a quote`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-2`, + content: [ + { + nodeType: `text`, + value: `Reference tests:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Inline Link: `, + marks: [], + data: {}, + }, + { + nodeType: `embedded-entry-inline`, + content: [], + data: { + target: { + sys: { + id: `7oHxo6bs0us9wIkq27qdyK`, + type: `Link`, + linkType: `Entry`, + }, + }, }, - ], - } - - const relatedArticle = entryFactory() - relatedArticle.fields.title = { en: `Related article #1` } - - field.content[0].data.target.fields.relatedArticles = { - en: [relatedArticle], - } - - const expectedTitle = `Related article #1` - const actualTitle = getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }).content[0].data.target.fields.relatedArticles[0].fields.title - - expect(actualTitle).toBe(expectedTitle) - }) - - it(`resolves the locales of each asset in the array`, () => { - const field = { - nodeType: `document`, - data: {}, - content: [ - { - nodeType: `embedded-entry-block`, - data: { target: entryFactory() }, + }, + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Link in list:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `ordered-list`, + content: [ + { + nodeType: `list-item`, + content: [ + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + { + nodeType: `embedded-entry-inline`, + content: [], + data: { + target: { + sys: { + id: `6KpLS2NZyB3KAvDzWf4Ukh`, + type: `Link`, + linkType: `Entry`, + }, + }, + }, + }, + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: `Embedded Entity:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `embedded-entry-block`, + content: [], + data: { + target: { + sys: { + id: `7oHxo6bs0us9wIkq27qdyK`, + type: `Link`, + linkType: `Entry`, }, - ], - } - - const relatedAsset = assetFactory() - relatedAsset.fields.file = { - en: { - url: `//images.ctfassets.net/related-asset.jpg`, }, - } - - field.content[0].data.target.fields.relatedAssets = { - en: [relatedAsset], - } - - const expectedURL = `//images.ctfassets.net/related-asset.jpg` - const actualURL = getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }).content[0].data.target.fields.relatedAssets[0].fields.file.url + }, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `heading-2`, + content: [ + { + nodeType: `text`, + value: `Embedded Asset:`, + marks: [], + data: {}, + }, + ], + data: {}, + }, + { + nodeType: `embedded-asset-block`, + content: [], + data: { + target: { + sys: { + id: `4ZQrqcrTunWiuNaavhGYNT`, + type: `Link`, + linkType: `Asset`, + }, + }, + }, + }, + { + nodeType: `paragraph`, + content: [ + { + nodeType: `text`, + value: ``, + marks: [], + data: {}, + }, + ], + data: {}, + }, + ], +}) - expect(actualURL).toBe(expectedURL) - }) +const fixtures = initialSync().currentSyncData + +const references = [ + ...fixtures.entries.map(entity => { + return { + sys: entity.sys, + contentful_id: entity.sys.id, + __typename: `ContentfulContent`, + ...entity.fields, + } + }), + ...fixtures.assets.map(entity => { + return { + sys: entity.sys, + contentful_id: entity.sys.id, + __typename: `ContentfulAsset`, + ...entity.fields, + } + }), +] + +describe(`rich text`, () => { + test(`renders with default options`, () => { + const { container } = render( + renderRichText({ raw: cloneDeep(raw), references: cloneDeep(references) }) + ) + expect(container).toMatchSnapshot() }) - describe(`circular references`, () => { - it(`prevents infinite loops when two entries reference each other`, () => { - const entry1 = entryFactory() - entry1.sys.id = `entry1-id` - const entry2 = entryFactory() - entry2.sys.id = `entry2-id` - - entry2.fields.title = { - en: `Related article`, - de: `German for "related article" ;)`, - } - - // Link the two to each other - entry1.fields.relatedArticle.en = entry2 - entry2.fields.relatedArticle.en = entry1 - - const field = { - nodeType: `document`, - data: {}, - content: [ - { - nodeType: `embedded-entry-inline`, - data: { target: entry1 }, - }, - ], - } - - expect(() => { - getNormalizedRichTextField({ - field, - contentTypesById, - getField, - defaultLocale, - }) - }).not.toThrowError() - }) + test(`renders with custom options`, () => { + const options = { + renderNode: { + [INLINES.EMBEDDED_ENTRY]: node => { + if (!node.data.target) { + return ( + + Unresolved INLINE ENTRY: {JSON.stringify(node, null, 2)} + + ) + } + return ( + + Resolved inline Entry ({node.data.target.contentful_id}) + + ) + }, + [INLINES.ENTRY_HYPERLINK]: node => { + if (!node.data.target) { + return ( + + Unresolved ENTRY HYPERLINK: {JSON.stringify(node, null, 2)} + + ) + } + return ( + + Resolved entry Hyperlink ({node.data.target.contentful_id}) + + ) + }, + [INLINES.ASSET_HYPERLINK]: node => { + if (!node.data.target) { + return ( + + Unresolved ASSET HYPERLINK: {JSON.stringify(node, null, 2)} + + ) + } + return ( + + Resolved asset Hyperlink ({node.data.target.contentful_id}) + + ) + }, + [BLOCKS.EMBEDDED_ENTRY]: node => { + if (!node.data.target) { + return ( +
Unresolved ENTRY !!!!": {JSON.stringify(node, null, 2)}
+ ) + } + return ( +

+ Resolved embedded Entry: {node.data.target.title[`en-US`]} ( + {node.data.target.contentful_id}) +

+ ) + }, + [BLOCKS.EMBEDDED_ASSET]: node => { + if (!node.data.target) { + return ( +
Unresolved ASSET !!!!": {JSON.stringify(node, null, 2)}
+ ) + } + return ( +

+ Resolved embedded Asset: {node.data.target.title[`en-US`]} ( + {node.data.target.contentful_id}) +

+ ) + }, + }, + } + const { container } = render( + renderRichText( + { raw: cloneDeep(raw), references: cloneDeep(references) }, + options + ) + ) + expect(container).toMatchSnapshot() }) }) diff --git a/packages/gatsby-source-contentful/src/extend-node-type.js b/packages/gatsby-source-contentful/src/extend-node-type.js index 4c5bf5625101e..0756c204e4810 100644 --- a/packages/gatsby-source-contentful/src/extend-node-type.js +++ b/packages/gatsby-source-contentful/src/extend-node-type.js @@ -9,7 +9,6 @@ const { GraphQLString, GraphQLInt, GraphQLFloat, - GraphQLJSON, GraphQLNonNull, } = require(`gatsby/graphql`) const qs = require(`qs`) @@ -553,23 +552,7 @@ const fluidNodeType = ({ name, getTracedSVG }) => { } } -exports.extendNodeType = ({ type, store }) => { - if (type.name.match(/contentful.*RichTextNode/)) { - return { - nodeType: { - type: GraphQLString, - deprecationReason: `This field is deprecated, please use 'json' instead.`, - }, - json: { - type: GraphQLJSON, - resolve: (source, fieldArgs) => { - const contentJSON = JSON.parse(source.internal.content) - return contentJSON - }, - }, - } - } - +exports.extendNodeType = ({ type, store, cache, getNodesByType }) => { if (type.name !== `ContentfulAsset`) { return {} } diff --git a/packages/gatsby-source-contentful/src/gatsby-node.js b/packages/gatsby-source-contentful/src/gatsby-node.js index ac4f95eb187ea..446b6eb79712b 100644 --- a/packages/gatsby-source-contentful/src/gatsby-node.js +++ b/packages/gatsby-source-contentful/src/gatsby-node.js @@ -129,15 +129,6 @@ List of locales and their codes can be found in Contentful app -> Settings -> Lo .default(true), // default plugins passed by gatsby plugins: Joi.array(), - richText: Joi.object() - .keys({ - resolveFieldLocales: Joi.boolean() - .description( - `If you want to resolve the locales in fields of assets and entries that are referenced by rich text (e.g., via embedded entries or entry hyperlinks), set this to \`true\`. Otherwise, fields of referenced assets or entries will be objects keyed by locale.` - ) - .default(false), - }) - .default({}), }) .external(validateContentfulAccess) @@ -170,7 +161,7 @@ exports.sourceNodes = async ( }, pluginOptions ) => { - const { createNode, deleteNode, touchNode } = actions + const { createNode, deleteNode, touchNode, createTypes } = actions let currentSyncData, contentTypeItems, defaultLocale, locales, space if (process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE) { @@ -262,46 +253,86 @@ exports.sourceNodes = async ( `Note: \`GATSBY_CONTENTFUL_OFFLINE\` was set but it either was not \`true\`, we _are_ online, or we are in production mode, so the flag is ignored.` ) } + } - const fetchActivity = reporter.activityTimer( - `Contentful: Fetch data (${sourceId})`, - { - parentSpan, - } - ) - - fetchActivity.start() - ;({ - currentSyncData, - contentTypeItems, - defaultLocale, - locales, - space, - } = await fetchData({ - syncToken, - reporter, - pluginConfig, + const fetchActivity = reporter.activityTimer( + `Contentful: Fetch data (${sourceId})`, + { parentSpan, - })) - - if (process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE) { - reporter.info( - `GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE was set. Writing v8 serialized glob of remote data to: ` + - process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE - ) - fs.writeFileSync( - process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE, - v8.serialize({ - currentSyncData, - contentTypeItems, - defaultLocale, - locales, - space, - }) - ) } - fetchActivity.end() + ) + + fetchActivity.start() + ;({ + currentSyncData, + contentTypeItems, + defaultLocale, + locales, + space, + } = await fetchData({ + syncToken, + reporter, + pluginConfig, + parentSpan, + })) + + createTypes(` + interface ContentfulEntry @nodeInterface { + contentful_id: String! + id: ID! + node_locale: String! + } +`) + + createTypes(` + interface ContentfulReference { + contentful_id: String! + id: ID! } +`) + + createTypes( + schema.buildObjectType({ + name: `ContentfulAsset`, + fields: { + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + }, + interfaces: [`ContentfulReference`, `Node`], + }) + ) + + const gqlTypes = contentTypeItems.map(contentTypeItem => + schema.buildObjectType({ + name: _.upperFirst(_.camelCase(`Contentful ${contentTypeItem.name}`)), + fields: { + contentful_id: { type: `String!` }, + id: { type: `ID!` }, + node_locale: { type: `String!` }, + }, + interfaces: [`ContentfulReference`, `ContentfulEntry`, `Node`], + }) + ) + + createTypes(gqlTypes) + + if (process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE) { + reporter.info( + `GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE was set. Writing v8 serialized glob of remote data to: ` + + process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE + ) + fs.writeFileSync( + process.env.GATSBY_CONTENTFUL_EXPERIMENTAL_FORCE_CACHE, + v8.serialize({ + currentSyncData, + contentTypeItems, + defaultLocale, + locales, + space, + }) + ) + } + fetchActivity.end() const processingActivity = reporter.activityTimer( `Contentful: Proccess data (${sourceId})`, @@ -351,6 +382,37 @@ exports.sourceNodes = async ( mergedSyncData.entries = res.items + // Inject raw API output to rich text fields + const richTextFieldMap = new Map() + contentTypeItems.forEach(contentType => { + richTextFieldMap.set( + contentType.sys.id, + contentType.fields + .filter(field => field.type === `RichText`) + .map(field => field.id) + ) + }) + + const rawEntries = new Map() + mergedSyncDataRaw.entries.forEach(rawEntry => + rawEntries.set(rawEntry.sys.id, rawEntry) + ) + + mergedSyncData.entries.forEach(entry => { + const contentTypeId = entry.sys.contentType.sys.id + const richTextFieldIds = richTextFieldMap.get(contentTypeId) + if (richTextFieldIds) { + richTextFieldIds.forEach(richTextFieldId => { + if (!entry.fields[richTextFieldId]) { + return + } + entry.fields[richTextFieldId] = rawEntries.get(entry.sys.id).fields[ + richTextFieldId + ] + }) + } + }) + const entryList = normalize.buildEntryList({ mergedSyncData, contentTypeItems, @@ -507,7 +569,6 @@ exports.sourceNodes = async ( locales, space, useNameForId: pluginConfig.get(`useNameForId`), - richTextOptions: pluginConfig.get(`richText`), }) ) } @@ -553,7 +614,7 @@ exports.sourceNodes = async ( // Check if there are any ContentfulAsset nodes and if gatsby-image is installed. If so, // add fragments for ContentfulAsset and gatsby-image. The fragment will cause an error // if there's not ContentfulAsset nodes and without gatsby-image, the fragment is useless. -exports.onPreExtractQueries = async ({ store, getNodesByType }) => { +exports.onPreExtractQueries = async ({ store }) => { const program = store.getState().program const CACHE_DIR = path.resolve( diff --git a/packages/gatsby-source-contentful/src/normalize.js b/packages/gatsby-source-contentful/src/normalize.js index 90d3a46142cee..1346cad57db89 100644 --- a/packages/gatsby-source-contentful/src/normalize.js +++ b/packages/gatsby-source-contentful/src/normalize.js @@ -3,7 +3,6 @@ const stringify = require(`json-stringify-safe`) const { createContentDigest } = require(`gatsby-core-utils`) const digest = str => createContentDigest(str) -const { getNormalizedRichTextField } = require(`./rich-text`) const typePrefix = `Contentful` const makeTypeName = type => _.upperFirst(_.camelCase(`${typePrefix} ${type}`)) @@ -206,29 +205,6 @@ function prepareTextNode(node, key, text, createNodeId) { return textNode } -function prepareRichTextNode(node, key, content, createNodeId) { - const str = stringify(content) - const richTextNode = { - ...content, - id: createNodeId(`${node.id}${key}RichTextNode`), - parent: node.id, - children: [], - [key]: str, - internal: { - type: _.camelCase(`${node.internal.type} ${key} RichTextNode`), - mediaType: `text/richtext`, - content: str, - contentDigest: digest(str), - }, - sys: { - type: node.sys.type, - }, - } - - node.children = node.children.concat([richTextNode.id]) - - return richTextNode -} function prepareJSONNode(node, key, content, createNodeId, i = ``) { const str = JSON.stringify(content) const JSONNode = { @@ -266,7 +242,6 @@ exports.createNodesForContentType = ({ locales, space, useNameForId, - richTextOptions, }) => { // Establish identifier for content type // Use `name` if specified, otherwise, use internal id (usually a natural-language constant, @@ -315,25 +290,6 @@ exports.createNodesForContentType = ({ ? getField(v) : v[defaultLocale] - if ( - fieldProps.type === `RichText` && - richTextOptions && - richTextOptions.resolveFieldLocales - ) { - const contentTypesById = new Map() - contentTypeItems.forEach(contentTypeItem => - contentTypesById.set(contentTypeItem.sys.id, contentTypeItem) - ) - - return getNormalizedRichTextField({ - field: localizedField, - fieldProps, - contentTypesById, - getField, - defaultLocale, - }) - } - return localizedField }) @@ -495,17 +451,42 @@ exports.createNodesForContentType = ({ fieldType === `RichText` && _.isPlainObject(entryItemFields[entryItemFieldKey]) ) { - const richTextNode = prepareRichTextNode( - entryNode, - entryItemFieldKey, - entryItemFields[entryItemFieldKey], - createNodeId - ) + const fieldValue = entryItemFields[entryItemFieldKey] + + const rawReferences = [] + + // Locate all Contentful Links within the rich text data + const traverse = obj => { + for (let k in obj) { + const v = obj[k] + if (v && v.sys && v.sys.type === `Link`) { + rawReferences.push(v) + } else if (v && typeof v === `object`) { + traverse(v) + } + } + } - childrenNodes.push(richTextNode) - entryItemFields[`${entryItemFieldKey}___NODE`] = richTextNode.id + traverse(fieldValue) - delete entryItemFields[entryItemFieldKey] + // Build up resolvable reference list + const resolvableReferenceIds = new Set() + rawReferences + .filter(function (v) { + return resolvable.has( + `${v.sys.id}___${v.sys.linkType || v.sys.type}` + ) + }) + .forEach(function (v) { + resolvableReferenceIds.add( + mId(space.sys.id, v.sys.id, v.sys.linkType || v.sys.type) + ) + }) + + entryItemFields[entryItemFieldKey] = { + raw: stringify(fieldValue), + references___NODE: [...resolvableReferenceIds], + } } else if ( fieldType === `Object` && _.isPlainObject(entryItemFields[entryItemFieldKey]) diff --git a/packages/gatsby-source-contentful/src/rich-text.js b/packages/gatsby-source-contentful/src/rich-text.js index 77636c29599ca..2ed4f1a27bc2d 100644 --- a/packages/gatsby-source-contentful/src/rich-text.js +++ b/packages/gatsby-source-contentful/src/rich-text.js @@ -1,185 +1,47 @@ -const _ = require(`lodash`) -const { BLOCKS, INLINES } = require(`@contentful/rich-text-types`) +import { documentToReactComponents } from "@contentful/rich-text-react-renderer" +import resolveResponse from "contentful-resolve-response" -const isEntryReferenceNode = node => - [ - BLOCKS.EMBEDDED_ENTRY, - INLINES.ENTRY_HYPERLINK, - INLINES.EMBEDDED_ENTRY, - ].indexOf(node.nodeType) >= 0 +function renderRichText({ raw, references }, options = {}) { + const richText = JSON.parse(raw) -const isAssetReferenceNode = node => - [BLOCKS.EMBEDDED_ASSET, INLINES.ASSET_HYPERLINK].indexOf(node.nodeType) >= 0 - -const isEntryReferenceField = field => _.get(field, `sys.type`) === `Entry` -const isAssetReferenceField = field => _.get(field, `sys.type`) === `Asset` - -const getFieldProps = (contentType, fieldName) => - contentType.fields.find(({ id }) => id === fieldName) - -const getAssetWithFieldLocalesResolved = ({ asset, getField }) => { - return { - ...asset, - fields: _.mapValues(asset.fields, getField), - } -} - -const getFieldWithLocaleResolved = ({ - field, - contentTypesById, - getField, - defaultLocale, - resolvedEntryIDs, -}) => { - // If the field is itself a reference to another entry, recursively resolve - // that entry's field locales too. - if (isEntryReferenceField(field)) { - const key = `${field.sys.id}___${field.sys.type}` - if (resolvedEntryIDs.has(key)) { - return field - } - - return getEntryWithFieldLocalesResolved({ - entry: field, - contentTypesById, - getField, - defaultLocale, - resolvedEntryIDs: resolvedEntryIDs.add(key), - }) - } - - if (isAssetReferenceField(field)) { - return getAssetWithFieldLocalesResolved({ - asset: field, - getField, - }) - } - - if (Array.isArray(field)) { - return field.map(fieldItem => - getFieldWithLocaleResolved({ - field: fieldItem, - contentTypesById, - getField, - defaultLocale, - resolvedEntryIDs, - }) - ) - } - - return field -} - -const getEntryWithFieldLocalesResolved = ({ - entry, - contentTypesById, - getField, - defaultLocale, - - /** - * Keep track of entries we've already resolved, in case two or more entries - * have circular references (so as to prevent an infinite loop). - */ - resolvedEntryIDs = new Set(), -}) => { - const contentType = contentTypesById.get(entry.sys.contentType.sys.id) - - return { - ...entry, - fields: _.mapValues(entry.fields, (field, fieldName) => { - const fieldProps = getFieldProps(contentType, fieldName) - - const fieldValue = fieldProps.localized - ? getField(field) - : field[defaultLocale] - - return getFieldWithLocaleResolved({ - field: fieldValue, - contentTypesById, - getField, - defaultLocale, - resolvedEntryIDs, - }) - }), + // If no references are given, there is no need to resolve them + if (!references || !references.length) { + return documentToReactComponents(richText, options) } -} -const getNormalizedRichTextNode = ({ - node, - contentTypesById, - getField, - defaultLocale, -}) => { - if (isEntryReferenceNode(node)) { - return { - ...node, - data: { - ...node.data, - target: getEntryWithFieldLocalesResolved({ - entry: node.data.target, - contentTypesById, - getField, - defaultLocale, - }), + // Create dummy response so we can use official libraries for resolving the entries + const dummyResponse = { + items: [ + { + sys: { type: `Entry` }, + richText, }, - } - } - - if (isAssetReferenceNode(node)) { - return { - ...node, - data: { - ...node.data, - target: getAssetWithFieldLocalesResolved({ - asset: node.data.target, - getField, + ], + includes: { + Entry: references + .filter(({ __typename }) => __typename !== `ContentfulAsset`) + .map(reference => { + return { + ...reference, + sys: { type: `Entry`, id: reference.contentful_id }, + } }), - }, - } - } - - if (Array.isArray(node.content)) { - return { - ...node, - content: node.content.map(childNode => - getNormalizedRichTextNode({ - node: childNode, - contentTypesById, - getField, - defaultLocale, - }) - ), - } + Asset: references + .filter(({ __typename }) => __typename === `ContentfulAsset`) + .map(reference => { + return { + ...reference, + sys: { type: `Asset`, id: reference.contentful_id }, + } + }), + }, } - return node -} - -/** - * Walk through the rich-text object, resolving locales on referenced entries - * (and on entries they've referenced, etc.). - */ -const getNormalizedRichTextField = ({ - field, - contentTypesById, - getField, - defaultLocale, -}) => { - if (field && field.content) { - return { - ...field, - content: field.content.map(node => - getNormalizedRichTextNode({ - node, - contentTypesById, - getField, - defaultLocale, - }) - ), - } - } + const resolved = resolveResponse(dummyResponse, { + removeUnresolved: true, + }) - return field + return documentToReactComponents(resolved[0].richText, options) } -exports.getNormalizedRichTextField = getNormalizedRichTextField +exports.renderRichText = renderRichText diff --git a/yarn.lock b/yarn.lock index e4a06d4c3a5ba..63f0fed5182d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,8 +974,6 @@ version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-runtime@^7.11.5": version "7.11.5" @@ -1248,6 +1246,13 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@contentful/rich-text-react-renderer@^14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@contentful/rich-text-react-renderer/-/rich-text-react-renderer-14.1.1.tgz#7a205c74c76bdf3ff7e6f315be2aadd3b8df8409" + integrity sha512-J9NhI3OeYt/ivj4uXj0874A1IhBADEcMSkyfNmU2XiKTwLfEab577+Tma5pe7mp/4NF4xGrq5YTU146vKufNHA== + dependencies: + "@contentful/rich-text-types" "^14.1.1" + "@contentful/rich-text-types@^14.1.1": version "14.1.1" resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-14.1.1.tgz#2b105c8fdb36cd1f8ed06ce077a853f4ab5e3770"