@@ -64,7 +64,7 @@ const options = {
)
},
},
-}
+})
const RichTextPage = ({ data }) => {
const entries = data.allContentfulRichText.nodes
@@ -75,7 +75,7 @@ const RichTextPage = ({ data }) => {
return (
{title}
- {renderRichText(richText, options)}
+ {renderRichText(richText, makeOptions)}
)
@@ -93,77 +93,94 @@ export const pageQuery = graphql`
id
title
richText {
- raw
- references {
- __typename
- sys {
- id
- }
- ... on ContentfulAsset {
- fluid(maxWidth: 200) {
- ...GatsbyContentfulFluid
- }
- }
- ... on ContentfulText {
- title
- short
- }
- ... on ContentfulLocation {
- location {
- lat
- lon
+ json
+ links {
+ assets {
+ block {
+ sys {
+ id
+ }
+ fluid(maxWidth: 200) {
+ ...GatsbyContentfulFluid
+ }
}
}
- ... on ContentfulContentReference {
- title
- one {
+ entries {
+ block {
__typename
sys {
id
+ type
}
... on ContentfulText {
title
short
}
+ ... on ContentfulLocation {
+ location {
+ lat
+ lon
+ }
+ }
... on ContentfulContentReference {
title
one {
+ __typename
+ sys {
+ id
+ }
+ ... on ContentfulText {
+ title
+ short
+ }
... on ContentfulContentReference {
title
+ one {
+ ... on ContentfulContentReference {
+ title
+ }
+ }
+ many {
+ ... on ContentfulContentReference {
+ title
+ }
+ }
}
}
many {
+ __typename
+ sys {
+ id
+ }
+ ... on ContentfulText {
+ title
+ short
+ }
+ ... on ContentfulNumber {
+ title
+ integer
+ }
... on ContentfulContentReference {
title
+ one {
+ ... on ContentfulContentReference {
+ title
+ }
+ }
+ many {
+ ... on ContentfulContentReference {
+ title
+ }
+ }
}
}
}
}
- many {
+ inline {
__typename
sys {
id
- }
- ... on ContentfulText {
- title
- short
- }
- ... on ContentfulNumber {
- title
- integer
- }
- ... on ContentfulContentReference {
- title
- one {
- ... on ContentfulContentReference {
- title
- }
- }
- many {
- ... on ContentfulContentReference {
- title
- }
- }
+ type
}
}
}
diff --git a/packages/gatsby-source-contentful/package.json b/packages/gatsby-source-contentful/package.json
index 8f86bb274d827..e5872d55f0535 100644
--- a/packages/gatsby-source-contentful/package.json
+++ b/packages/gatsby-source-contentful/package.json
@@ -8,6 +8,7 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
+ "@contentful/rich-text-links": "^14.1.2",
"@contentful/rich-text-react-renderer": "^14.1.2",
"@contentful/rich-text-types": "^14.1.2",
"@hapi/joi": "^15.1.1",
diff --git a/packages/gatsby-source-contentful/src/__tests__/rich-text.js b/packages/gatsby-source-contentful/src/__tests__/rich-text.js
index c8f558c4c00b4..cbcd8c13f2463 100644
--- a/packages/gatsby-source-contentful/src/__tests__/rich-text.js
+++ b/packages/gatsby-source-contentful/src/__tests__/rich-text.js
@@ -2,10 +2,11 @@ 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 { getRichTextEntityLinks } from "@contentful/rich-text-links"
import { initialSync } from "../__fixtures__/rich-text-data"
import { cloneDeep } from "lodash"
-const raw = {
+const json = {
nodeType: `document`,
data: {},
content: [
@@ -404,101 +405,128 @@ const raw = {
}
const fixtures = initialSync().currentSyncData
+const fixturesEntriesMap = new Map()
+const fixturesAssetsMap = new Map()
-const references = [
- ...fixtures.entries.map(entity => {
- return {
- sys: entity.sys,
- __typename: `ContentfulContent`,
- ...entity.fields,
- }
- }),
- ...fixtures.assets.map(entity => {
- return {
- sys: entity.sys,
- __typename: `ContentfulAsset`,
- ...entity.fields,
- }
- }),
-]
+fixtures.entries.forEach(entity =>
+ fixturesEntriesMap.set(entity.sys.id, { sys: entity.sys, ...entity.fields })
+)
+fixtures.assets.forEach(entity =>
+ fixturesAssetsMap.set(entity.sys.id, { sys: entity.sys, ...entity.fields })
+)
+
+const links = {
+ assets: {
+ block: getRichTextEntityLinks(json, `embedded-asset-block`)[
+ `Asset`
+ ].map(entity => fixturesAssetsMap.get(entity.id)),
+ hyperlink: getRichTextEntityLinks(json, `asset-hyperlink`)[
+ `Asset`
+ ].map(entity => fixturesAssetsMap.get(entity.id)),
+ },
+ entries: {
+ inline: getRichTextEntityLinks(json, `embedded-entry-inline`)[
+ `Entry`
+ ].map(entity => fixturesEntriesMap.get(entity.id)),
+ block: getRichTextEntityLinks(json, `embedded-entry-block`)[
+ `Entry`
+ ].map(entity => fixturesEntriesMap.get(entity.id)),
+ hyperlink: getRichTextEntityLinks(json, `entry-hyperlink`)[
+ `Entry`
+ ].map(entity => fixturesEntriesMap.get(entity.id)),
+ },
+}
describe(`rich text`, () => {
test(`renders with default options`, () => {
const { container } = render(
- renderRichText({ raw: cloneDeep(raw), references: cloneDeep(references) })
+ renderRichText({ json: cloneDeep(json), links: cloneDeep(links) })
)
expect(container).toMatchSnapshot()
})
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.sys.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.sys.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.sys.id})
- )
- },
- [BLOCKS.EMBEDDED_ENTRY]: node => {
- if (!node.data.target) {
+ const makeOptions = ({
+ assetBlockMap,
+ assetHyperlinkMap,
+ entryBlockMap,
+ entryInlineMap,
+ entryHyperlinkMap,
+ }) => {
+ return {
+ renderNode: {
+ [INLINES.EMBEDDED_ENTRY]: node => {
+ const entry = entryInlineMap.get(node?.data?.target?.sys.id)
+ if (!entry) {
+ return (
+
+ Unresolved INLINE ENTRY:{` `}
+ {JSON.stringify({ node, entryInlineMap }, null, 2)}
+
+ )
+ }
+ return Resolved inline Entry ({entry.sys.id})
+ },
+ [INLINES.ENTRY_HYPERLINK]: node => {
+ const entry = entryHyperlinkMap.get(node?.data?.target?.sys.id)
+ if (!entry) {
+ return (
+
+ Unresolved ENTRY HYPERLINK: {JSON.stringify(node, null, 2)}
+
+ )
+ }
+ return Resolved entry Hyperlink ({entry.sys.id})
+ },
+ [INLINES.ASSET_HYPERLINK]: node => {
+ const entry = assetHyperlinkMap.get(node?.data?.target?.sys.id)
+ if (!entry) {
+ return (
+
+ Unresolved ASSET HYPERLINK: {JSON.stringify(node, null, 2)}
+
+ )
+ }
+ return Resolved asset Hyperlink ({entry.sys.id})
+ },
+ [BLOCKS.EMBEDDED_ENTRY]: node => {
+ const entry = entryBlockMap.get(node?.data?.target?.sys.id)
+ if (!entry) {
+ return (
+
+ Unresolved ENTRY !!!!": {JSON.stringify(node, null, 2)}
+
+ )
+ }
return (
- Unresolved ENTRY !!!!": {JSON.stringify(node, null, 2)}
+
+ Resolved embedded Entry: {entry.title[`en-US`]} ({entry.sys.id})
+
)
- }
- return (
-
- Resolved embedded Entry: {node.data.target.title[`en-US`]} (
- {node.data.target.sys.id})
-
- )
- },
- [BLOCKS.EMBEDDED_ASSET]: node => {
- if (!node.data.target) {
+ },
+ [BLOCKS.EMBEDDED_ASSET]: node => {
+ const entry = assetBlockMap.get(node?.data?.target?.sys.id)
+ if (!entry) {
+ return (
+
+ Unresolved ASSET !!!!": {JSON.stringify(node, null, 2)}
+
+ )
+ }
return (
- Unresolved ASSET !!!!": {JSON.stringify(node, null, 2)}
+
+ Resolved embedded Asset: {entry.title[`en-US`]} ({entry.sys.id})
+
)
- }
- return (
-
- Resolved embedded Asset: {node.data.target.title[`en-US`]} (
- {node.data.target.sys.id})
-
- )
+ },
},
- },
+ }
}
+
const { container } = render(
renderRichText(
- { raw: cloneDeep(raw), references: cloneDeep(references) },
- options
+ { json: cloneDeep(json), links: cloneDeep(links) },
+ makeOptions
)
)
expect(container).toMatchSnapshot()
diff --git a/packages/gatsby-source-contentful/src/generate-schema.js b/packages/gatsby-source-contentful/src/generate-schema.js
index c946e82fe2165..9e2b8895ac86f 100644
--- a/packages/gatsby-source-contentful/src/generate-schema.js
+++ b/packages/gatsby-source-contentful/src/generate-schema.js
@@ -1,3 +1,5 @@
+import { getRichTextEntityLinks } from "@contentful/rich-text-links"
+
import { makeTypeName } from "./normalize"
// Contentful content type schemas
@@ -143,6 +145,7 @@ export function generateSchema({
pluginConfig,
contentTypeItems,
}) {
+ // Generic Types
createTypes(`
interface ContentfulInternalReference implements Node {
id: ID!
@@ -180,59 +183,100 @@ export function generateSchema({
}
`)
+ // Assets
generateAssetTypes({ createTypes })
+ // Rich Text
+ const makeRichTextLinksResolver = (nodeType, entityType) => (
+ source,
+ args,
+ context
+ ) => {
+ const links = getRichTextEntityLinks(source, nodeType)[entityType].map(
+ ({ id }) => id
+ )
+
+ return context.nodeModel.getAllNodes().filter(
+ node =>
+ node.internal.owner === `gatsby-source-contentful` &&
+ node?.sys?.id &&
+ node?.sys?.type === entityType &&
+ links.includes(node.sys.id)
+ // @todo how can we check for correct space and environment? We need to access the sys field of the fields parent entry.
+ )
+ }
+
// Contentful specific types
+ createTypes(
+ schema.buildObjectType({
+ name: `ContentfulNodeTypeRichTextAssets`,
+ fields: {
+ block: {
+ type: `[ContentfulAsset]!`,
+ resolve: makeRichTextLinksResolver(`embedded-asset-block`, `Asset`),
+ },
+ hyperlink: {
+ type: `[ContentfulAsset]!`,
+ resolve: makeRichTextLinksResolver(`asset-hyperlink`, `Asset`),
+ },
+ },
+ })
+ )
+
+ createTypes(
+ schema.buildObjectType({
+ name: `ContentfulNodeTypeRichTextEntries`,
+ fields: {
+ inline: {
+ type: `[ContentfulEntry]!`,
+ resolve: makeRichTextLinksResolver(`embedded-entry-inline`, `Entry`),
+ },
+ block: {
+ type: `[ContentfulEntry]!`,
+ resolve: makeRichTextLinksResolver(`embedded-entry-block`, `Entry`),
+ },
+ hyperlink: {
+ type: `[ContentfulEntry]!`,
+ resolve: makeRichTextLinksResolver(`entry-hyperlink`, `Entry`),
+ },
+ },
+ })
+ )
+
+ createTypes(
+ schema.buildObjectType({
+ name: `ContentfulNodeTypeRichTextLinks`,
+ fields: {
+ assets: {
+ type: `ContentfulNodeTypeRichTextAssets`,
+ resolve(source) {
+ return source
+ },
+ },
+ entries: {
+ type: `ContentfulNodeTypeRichTextEntries`,
+ resolve(source) {
+ return source
+ },
+ },
+ },
+ })
+ )
+
createTypes(
schema.buildObjectType({
name: `ContentfulNodeTypeRichText`,
fields: {
- raw: {
+ json: {
type: `JSON`,
resolve(source) {
return source
},
},
- references: {
- type: `[ContentfulInternalReference]`,
- resolve(source, args, context) {
- const referencedEntries = new Set()
- const referencedAssets = new Set()
-
- // Locate all Contentful Links within the rich text data
- // Traverse logic based on https://github.com/contentful/contentful-resolve-response
- const traverse = obj => {
- // eslint-disable-next-line guard-for-in
- for (const k in obj) {
- const v = obj[k]
- if (v && v.sys && v.sys.type === `Link`) {
- if (v.sys.linkType === `Asset`) {
- referencedAssets.add(v.sys.id)
- }
- if (v.sys.linkType === `Entry`) {
- referencedEntries.add(v.sys.id)
- }
- } else if (v && typeof v === `object`) {
- traverse(v)
- }
- }
- }
- traverse(source)
-
- // Get all nodes and return all that got referenced in the rich text
- return context.nodeModel.getAllNodes().filter(node => {
- if (
- !(
- node.internal.owner === `gatsby-source-contentful` &&
- node?.sys?.id
- )
- ) {
- return false
- }
- return node.internal.type === `ContentfulAsset`
- ? referencedAssets.has(node.sys.id)
- : referencedEntries.has(node.sys.id)
- })
+ links: {
+ type: `ContentfulNodeTypeRichTextLinks`,
+ resolve(source) {
+ return source
},
},
},
@@ -240,6 +284,7 @@ export function generateSchema({
})
)
+ // Location
createTypes(
schema.buildObjectType({
name: `ContentfulNodeTypeLocation`,
@@ -253,6 +298,7 @@ export function generateSchema({
})
)
+ // Text
// @todo Is there a way to have this as string and let transformer-remark replace it with an object?
createTypes(
schema.buildObjectType({
@@ -267,6 +313,7 @@ export function generateSchema({
})
)
+ // Content types
for (const contentTypeItem of contentTypeItems) {
try {
const fields = {}
diff --git a/packages/gatsby-source-contentful/src/rich-text.js b/packages/gatsby-source-contentful/src/rich-text.js
index 31506a29996ff..890f3ac910144 100644
--- a/packages/gatsby-source-contentful/src/rich-text.js
+++ b/packages/gatsby-source-contentful/src/rich-text.js
@@ -1,47 +1,51 @@
import { documentToReactComponents } from "@contentful/rich-text-react-renderer"
-import resolveResponse from "contentful-resolve-response"
-function renderRichText({ raw, references }, options = {}) {
- const richText = raw
+function renderRichText({ json, links }, makeOptions = {}) {
+ const options =
+ typeof makeOptions === `function`
+ ? makeOptions(generateLinkMaps(links))
+ : makeOptions
- // If no references are given, there is no need to resolve them
- if (!references || !references.length) {
- return documentToReactComponents(richText, options)
+ return documentToReactComponents(json, options)
+}
+
+exports.renderRichText = renderRichText
+
+/**
+ * Helper function to simplify Rich Text rendering. Based on:
+ * https://www.contentful.com/blog/2021/04/14/rendering-linked-assets-entries-in-contentful/
+ */
+function generateLinkMaps(links) {
+ const assetBlockMap = new Map()
+ for (const asset of links.assets.block || []) {
+ assetBlockMap.set(asset.sys.id, asset)
}
- // Create dummy response so we can use official libraries for resolving the entries
- const dummyResponse = {
- items: [
- {
- sys: { type: `Entry` },
- richText,
- },
- ],
- includes: {
- Entry: references
- .filter(({ __typename }) => __typename !== `ContentfulAsset`)
- .map(reference => {
- return {
- ...reference,
- sys: { type: `Entry`, id: reference.sys.id },
- }
- }),
- Asset: references
- .filter(({ __typename }) => __typename === `ContentfulAsset`)
- .map(reference => {
- return {
- ...reference,
- sys: { type: `Asset`, id: reference.sys.id },
- }
- }),
- },
+ const assetHyperlinkMap = new Map()
+ for (const asset of links.assets.hyperlink || []) {
+ assetHyperlinkMap.set(asset.sys.id, asset)
}
- const resolved = resolveResponse(dummyResponse, {
- removeUnresolved: true,
- })
+ const entryBlockMap = new Map()
+ for (const entry of links.entries.block || []) {
+ entryBlockMap.set(entry.sys.id, entry)
+ }
- return documentToReactComponents(resolved[0].richText, options)
-}
+ const entryInlineMap = new Map()
+ for (const entry of links.entries.inline || []) {
+ entryInlineMap.set(entry.sys.id, entry)
+ }
-exports.renderRichText = renderRichText
+ const entryHyperlinkMap = new Map()
+ for (const entry of links.entries.hyperlink || []) {
+ entryHyperlinkMap.set(entry.sys.id, entry)
+ }
+
+ return {
+ assetBlockMap,
+ assetHyperlinkMap,
+ entryBlockMap,
+ entryInlineMap,
+ entryHyperlinkMap,
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 37b8922beee49..b5b2626c40d15 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1428,6 +1428,13 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
+"@contentful/rich-text-links@^14.1.2":
+ version "14.1.2"
+ resolved "https://registry.yarnpkg.com/@contentful/rich-text-links/-/rich-text-links-14.1.2.tgz#993cd086d55af11f5d31b76060c02a9866c93a01"
+ integrity sha512-oK+y/c42fOJOdRM6XDKNLqw7uwVHZUIRGKzk9fJLDaOt5tygDm0pvgJ9bkvadaBwbAioxlQ0hHS0i5JP+UHkvA==
+ dependencies:
+ "@contentful/rich-text-types" "^14.1.2"
+
"@contentful/rich-text-react-renderer@^14.1.2":
version "14.1.2"
resolved "https://registry.yarnpkg.com/@contentful/rich-text-react-renderer/-/rich-text-react-renderer-14.1.2.tgz#b7fff19faa0512f034f1717774a0d9b348bb07fc"