Styling Rich Text with TailwindCSS
In this tutorial, we’ll cover using the JSON representation from the RTE to create custom elements for each text-based element of rich text.
While using the HTML that comes from Hygraph’s Rich Text Editor (RTE) is an easy shortcut when prototyping. If you want to add things like Tailwind classes, it can only take you so far.
In this tutorial, we’ll cover using the JSON representation from the RTE to create custom elements for each text-based element of rich text. This will allow us to add granular Tailwind classes to each.
RequirementsAnchor
This demo will be using 11ty for its base to work in pure JavaScript, but the methods described work in any framework. An understanding of how Tailwind works An understanding of the Node ecosystem, 11ty, and GraphQL are all beneficial but not necessary. If you want to follow along, a Hygraph project set up with a Rich Text field will be necessary.
Getting data from HygraphAnchor
To start, we need the data for each post in Hygraph. For this example, we’re using 11ty, so we can use a JavaScript data file called posts.js
to get all posts from our Hygraph project.
// Install with `npm install graphql-request`const GraphQLClient = require('graphql-request').GraphQLClient// Get Hygraph posts for 11ty dataconst getHygraphPosts = async () => {const client = new GraphQLClient('https://api-us-east-1.hygraph.com/v2/cl8vzs0jm7fb201ukbf4ahe92/master')const response = await client.request(`query MyQuery {posts {slugtitlecontent {html}}}` )return response.posts}module.exports = async () => {const posts = await getHygraphPosts()return posts}
We can then use this content in an 11ty template by using the built-in Pagination feature to render a single page for each post, and for completeness' sake, you’ll need a base.html
included to use as the overall template for the site.
---pagination:data: postssize: 1alias: postpermalink: "/post/{{ post.slug | slug }}/index.html"layout: "base.html"# You'll need a base.html with a simple HTML wrapper---<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">{{ post.title }}</h1>{{ post.content.html }}
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script src="https://cdn.tailwindcss.com"></script></head><body><div class="container mx-auto my-10 px-6"><a href="/">Home</a>{{ content }}</div></body></html>
Note in these files, we’re already using Tailwind classes and are including the Tailwind “Play” CDN link. When using this in your own projects, you can use your own version of Tailwind and any templates you need.
While there’s content on the page, it’s not ideal. Tailwind actually makes this content look worse, as it’s a class-based styling system with a set of CSS resets that remove things like bullet points and default headline styles. To fix that, we need classes on the HTML body.
Converting the data to use JSON instead of HTMLAnchor
Now that we have data coming in and pages being built, we need to switch out post.content.html
from our post body to be rendered HTML from JSON.
To do this, in the posts.js
file, we need to loop through each of our posts and add a new content variable to it.
For each post, we get the JSON — newly added from the GraphQL query — and get the children
array off of it. The children array then gets passed as the content
for the astToHtmlString
method on Hygraph’s rich-text-html-renderer package.
Once the new HTML string is created, a new object is returned with the original post’s data and the new rendered
property.
const GraphQLClient = require('graphql-request').GraphQLClient// Install using npm install @graphcms/rich-text-html-rendererconst { astToHtmlString } = require('@graphcms/rich-text-html-renderer')// Get Hygraph posts for 11ty dataconst getHygraphPosts = async () => {const client = new GraphQLClient('https://api-us-east-1.hygraph.com/v2/cl8vzs0jm7fb201ukbf4ahe92/master')const response = await client.request(`query MyQuery {posts {slugtitlecontent {htmljson}}}` )return response.posts}async function addContent(post) {// Isolates the content array from the JSONconst content = post.content.json.children// Passes the content to the astToHtmlString methodconst rendered = await astToHtmlString({ content: content })return {...post, rendered}}module.exports = async () => {const posts = await getHygraphPosts()// Loops through each posts and runs the addContent functionconst postsWithContent = posts.map(addContent)// Returns the new data to the 11ty templatereturn postsWithContent}
After making this change, the rendered
variable is now accessible by our post template.
---pagination:data: postssize: 1alias: postpermalink: "/post/{{ post.slug | slug }}/index.html"layout: "base.html"---<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">{{ post.title }}</h1>{{ post.rendered }}
Just because we’re rendering this differently doesn’t mean we’ve solved our problem yet. Our site should look exactly as it did before. That’s because we’re using the default renderers. We can change that by providing new ways to render each content type.
Creating custom HTML for each content typeAnchor
The astToHtmlString method accepts another parameter: renderers.
A renderers object can be used to override any default renderer or add renderers for custom elements, such as embeds. Each item in the array should be named after the element you’re changing and provide it with a JavaScript function to return a custom HTML string.
// Basic syntax// <tag name>: (node) => `custom string`const renderers = {h1: ({ children }) => `<h1 class="mb-4 text-4xl text-gray-900 md:text-5xl lg:text-6xl ${sharedClasses}">${children}</h1>`,}
The argument of each custom function is the data from each content type node. The main need we have here will be for the children
data off that object. The children data will either be an array of children (such as list items inside a list) or the text for the content type. This makes it easier to handle things like ordered and unordered lists.
Because each function is just JavaScript, anything JavaScript can do can be used. Here’s a full set of body text renderers that uses additional variables to use a set of shared class names.
const sharedClasses = "dark:text-white"const bodyClasses = "text-lg text-gray-700"const renderers = {h1: ({ children }) => `<h1 class="mb-4 text-4xl text-gray-900 md:text-5xl lg:text-6xl ${sharedClasses}">${children}</h1>`,h2: ({ children }) => `<h1 class="mb-4 text-3xl text-gray-900 md:text-5xl lg:text-6xl ${sharedClasses}">${children}</h1>`,h3: ({ children }) => `<h3 class="text-3xl ${sharedClasses}">${children}</h3>`,h4: ({ children }) => `<h4 class="text-2xl ${sharedClasses}">${children}</h3>`,h5: ({ children }) => `<h5 class="text-xl ${sharedClasses}">${children}</h3>`,h6: ({ children }) => `<h6 class="text-large ${sharedClasses}">${children}</h3>`,p: ({ children }) => `<p class="my-4 text-lg ${bodyClasses} ${sharedClasses}">${children}</p>`,ul: ({ children }) => `<ul class="list-disc list-inside my-4 text-lg ${bodyClasses} ${sharedClasses}">${children}</ul>`,ol: ({ children }) => `<ol class="list-decimal list-inside my-4 text-lg ${bodyClasses} ${sharedClasses}">${children}</ol>`,li: ({ children }) => `<li class="my-2 text-lg ${bodyClasses} ${sharedClasses}">${children}</li>`,code: ({ children }) => `<code class="bg-gray-100 dark:bg-gray-800 rounded-md p-2 text-sm ${sharedClasses}">${children}</code>`,code_block: ({ children }) => `<pre class="bg-gray-100 dark:bg-gray-800 overflow-y-scroll rounded-md p-2 text-sm ${sharedClasses}">${children}</pre>`,}
Once those renderers are defined, you can add that as an argument for the astToHtmlString
in the addContent
function.
async function addContent(post) {const content = post.content.json.childrenconst rendered = await astToHtmlString({ content: content, renderers })return {...post, rendered}}
With that change saved, all the new classes will take effect on the post pages. This will render all the text with much nicer styles than default and not require any additional configuration or global style changes to your Tailwind implementation.
Taking this furtherAnchor
This is just scratching the surface of what you can do with the JSON representation of RTE. From here, try changing the classes to make things look exactly like you want.
If you’re up for a bigger challenge use something like Prism.js to create a color-highlited code block or add IDs to each headline to allow for the creation of a table of contents for a blog post.
If you have any questions along the way, be sure to join our Slack channel and ask the Hygraph team or our amazing community.