Skip to content

Instantly share code, notes, and snippets.

@sarahbethfederman
Last active May 12, 2021 17:56
Show Gist options
  • Save sarahbethfederman/04613d376188f71a1995228f33c38328 to your computer and use it in GitHub Desktop.
Save sarahbethfederman/04613d376188f71a1995228f33c38328 to your computer and use it in GitHub Desktop.
Using contentful with gatsby and react
import escape from 'escape-html';
import React from 'react';
import {
Document,
Mark,
Text,
BLOCKS,
MARKS,
INLINES,
Block,
Inline,
helpers,
} from '@contentful/rich-text-types';
let NODE_KEY_I = 0;
const defaultNodeRenderers = {
[BLOCKS.PARAGRAPH]: (node, next) => <p>{next(node.content)}</p>,
[BLOCKS.HEADING_1]: (node, next) => <h1>{next(node.content)}</h1>,
[BLOCKS.HEADING_2]: (node, next) => <h2>{next(node.content)}</h2>,
[BLOCKS.HEADING_3]: (node, next) => <h3>{next(node.content)}</h3>,
[BLOCKS.HEADING_4]: (node, next) => <h4>{next(node.content)}</h4>,
[BLOCKS.HEADING_5]: (node, next) => <h5>{next(node.content)}</h5>,
[BLOCKS.HEADING_6]: (node, next) => <h6>{next(node.content)}</h6>,
[BLOCKS.EMBEDDED_ENTRY]: (node, next) => <div>{next(node.content)}</div>,
[BLOCKS.EMBEDDED_ASSET]: (node, next) => <div>{next(node.content)}</div>,
[BLOCKS.UL_LIST]: (node, next) => <ul>{next(node.content)}</ul>,
[BLOCKS.OL_LIST]: (node, next) => <ol>{next(node.content)}</ol>,
[BLOCKS.LIST_ITEM]: (node, next) => <li>{next(node.content)}</li>,
[BLOCKS.QUOTE]: (node, next) => <blockquote>{next(node.content)}</blockquote>,
[BLOCKS.HR]: () => <hr />,
[INLINES.ASSET_HYPERLINK]: node =>
defaultInline(INLINES.ASSET_HYPERLINK, node),
[INLINES.ENTRY_HYPERLINK]: (node, next) => (
<a href={node.data.target.sys.id}>{next(node.content)}</a>
),
[INLINES.EMBEDDED_ENTRY]: node => defaultInline(INLINES.EMBEDDED_ENTRY, node),
[INLINES.HYPERLINK]: (node, next) => (
<a href={node.data.uri}>{next(node.content)}</a>
),
};
const defaultMarkRenderers = {
[MARKS.BOLD]: text => <b>{text}</b>,
[MARKS.ITALIC]: text => <i>{text}</i>,
[MARKS.UNDERLINE]: text => <u>{text}</u>,
[MARKS.CODE]: text => <code>{text}</code>,
};
const defaultInline = (type, node) => (
<span>
type: {type} id: {node.data.target.sys.id}
</span>
);
/**
* Serialize a Contentful Rich Text `document` to JSX.
*/
export function documentToJSX(richTextDocument, options = {}) {
NODE_KEY_I = 0;
return nodeListToJSX(richTextDocument.content, {
renderNode: {
...defaultNodeRenderers,
...options.renderNode,
},
renderMark: {
...defaultMarkRenderers,
...options.renderMark,
},
});
}
function nodeListToJSX(nodes, { renderNode, renderMark }) {
return nodes.map(node => nodeToJSX(node, { renderNode, renderMark }));
}
function nodeToJSX(node, { renderNode, renderMark }) {
if (helpers.isText(node)) {
const nodeValue = escape(node.value);
if (node.marks.length > 0) {
return node.marks.reduce((value, mark) => {
if (!renderMark[mark.type]) {
return value;
}
return {
...renderMark[mark.type](value),
key: NODE_KEY_I++
};
}, nodeValue);
}
return nodeValue;
} else {
console.log(node)
const nextNode = nodes => nodeListToJSX(nodes, { renderMark, renderNode });
if (!node.nodeType || !renderNode[node.nodeType]) {
return <span />;
}
return {
...renderNode[node.nodeType](node, nextNode),
key: NODE_KEY_I++
};
}
}
import React from 'react';
import { Link as GatsbyLink } from 'gatsby';
import * as deepmerge from 'deepmerge';
import { INLINES, BLOCKS, MARKS } from '@contentful/rich-text-types';
import { documentToJSX } from './../scripts/documentToJSX';
import { getEntry } from './../scripts/getContentfulEntry';
const Link = ({ children, to, activeClassName, ...other }) => {
const internal = /^\/(?!\/)/.test(to);
// To do: add test for public path (for absolute links that are still internal)
// Use Gatsby Link for internal links, and <a> for others
if (internal) {
return (
<GatsbyLink to={to} activeClassName={activeClassName} {...other}>
{children}
</GatsbyLink>
)
}
return (
<a href={to} {...other}>
{children}
</a>
)
}
const renderComponent = (node, next) => {
const {
data: {
target: {
fields,
sys: {
contentType: {
sys: { id },
},
},
},
},
} = node;
// Add custom components here
switch (id) {
case 'column':
return <div {...fields} />;
default:
return <span />;
}
};
const renderAsset = (node, next) => {
const {
data: {
target: {
fields: {
file,
title
}
},
},
} = node;
if (file['en-US'].contentType.includes('image')) {
return <img src={file['en-US'].url} alt={title} />
}
return <span />
};
// Override-able via options prop
const options = {
renderNode: {
[BLOCKS.PARAGRAPH]: (node, next) => (
<p className="spectrum-Body3">{next(node.content)}</p>
),
[BLOCKS.EMBEDDED_ASSET]: (node, next) => renderAsset(node, next),
[BLOCKS.EMBEDDED_ENTRY]: (node, next) => renderComponent(node, next),
[INLINES.ENTRY_HYPERLINK]: (node, next) => (
<GatsbyLink
to={node.data.target.fields ? node.data.target.fields.slug['en-US'] : '/404'}>
{next(node.content)}
</GatsbyLink>
),
[INLINES.HYPERLINK]: (node, next) => {
return (
<Link className="spectrum-Link" to={node.data.uri}>
{next(node.content)}
</Link>
);
}
},
};
class RichTextRenderer extends React.Component {
getOptions = () => {
if (this.props.options) {
return deepmerge(options, this.props.options);
}
return options;
};
render() {
const { content } = this.props;
const JSONContent = JSON.parse(content);
const renderOpts = this.getOptions();
const JSX = documentToJSX(JSONContent, renderOpts);
return JSX;
}
}
export default RichTextRenderer;
@sarahbethfederman
Copy link
Author

sarahbethfederman commented Jan 9, 2019

To run with correctly resolved entries, I'm using build:watch. Does not currently work with gatsby develop due to embedded entries bug.

  "scripts": {
    "build": "npx gatsby build",
    "build:watch": "nodemon --watch src --exec 'npm run deploy'",
    "serve": "npx gatsby serve",
    "deploy": "npm run build && npm run serve",
  }

To use:

In your graphQL:

[FIELDNAME] {
  childContentfulRichText {
    internal {
      content
    }
  }
}

In your JSX:

// override a component for this instance
const options = {
  renderNode: {
    [BLOCKS.UL_LIST]: (node, next) => <CustomComponent>{next(node.content)}</CustomComponent>,
  },
};
<RichTextRenderer options={options} content={[FIELDNAME].childContentfulRichText.internal.content} />

@MaralS
Copy link

MaralS commented Mar 5, 2019

To run with correctly resolved entries, I'm using build:watch. Does not currently work with gatsby develop due to embedded entries bug.

  "scripts": {
    "build": "npx gatsby build",
    "build:watch": "nodemon --watch src --exec 'npm run deploy'",
    "serve": "npx gatsby serve",
    "deploy": "npm run build && npm run serve",
  }

To use:

In your graphQL:

[FIELDNAME] {
  childContentfulRichText {
    internal {
      content
    }
  }
}

In your JSX:

// override a component for this instance
const options = {
  renderNode: {
    [BLOCKS.UL_LIST]: (node, next) => <CustomComponent>{next(node.content)}</CustomComponent>,
  },
};
<RichTextRenderer options={options} content={[FIELDNAME].childContentfulRichText.internal.content} />

Hi @sarah
I was wondering if you had a repository where you implemented your scripts. I'm a newbie with contentful, and I have some troubles to display my entries and assets
Thanks in advance
Maral

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment