At Hex, we believe in competing on what we build, not how we build it. That's why we regularly share open source code and deep dives into how we're developing with Sanity, so others can learn from it and help make it even better.
Our Head of Engineering, Jamie, spoke recently at Sanity's developer deep dive on a new way to think about Modular Content, using inner and outer blocks. The focus was on increasing editor flexibility and autonomy, and how it can be implemented successfully.
In case you missed it, you can catch the video below:
Transcription
Hey, I’m Jamie, I’m the Head of Engineering at Hex Digital.
As an Experience Design agency, we’re really focused on how our websites and products are used, to ensure that both our Customers, and our Customer’s Users get a fantastic experience with our products.
Naturally, Sanity lends itself superbly to that mission. The customisability and openness allow us to really explore and improve on what makes a great experience within a CMS.
So let's talk about Page Building
So let’s talk about Page Building and what this might look like if we really focus in on that experience and try and dial it up.
I think we’re pretty familiar with the Page Builder or Modular Section approach now, creating an array of sections that can be added to a page, with set fields for data and a component to render it.
You can add sections to a page, change their order, edit their content, and have some flexibility around what your pages look like.
And this is great. Compared to Site Builders, this lets editors focus on content and not design. There’s fewer controls and very little training needed.
Compared to Page Templates, it gives editors more flexibility to present their content in the best way.
And then the sections themselves, they help ensure brand consistency. So again compared to Site Builders, they’re ensuring your content fits together well and is presented well.
But often, when using modular sections like this, our customer’s, our editors find themselves with content that doesn’t fit into a section.
Say we have a Hero, and it has a Heading, Tagline, a Background Image, but let’s say the client has signed a new partnership deal, and they need to put in some partner logos in that Hero.
There aren’t fields in the Hero section that allow editors to add partnership logos.
They need to get in touch with us and ask us to add the ability to have partner logos in that hero block.
So we talk through what they need, do a design for the hero section, add that functionality in, we QA, we deploy.
But now they need partner logos on another section somewhere else, and they also need more content in other sections that don’t support that yet.
So we add more controls to more sections to help them support that content, maybe we add controls for the ordering of some of that content as well, as they need variations.
All of this incurs additional costs for design, development, QA, and has an impact on editor’s autonomy and content velocity.
It also has an impact on the usability of the CMS, increasing the complexity of each block, and making them more difficult to use. And this can get worse over time as more controls are added to different blocks.
A different approach - Inner + Outer Blocks
I want to present a different approach to the Modular Sections. At Hex we call these Modular Blocks.
At its core, it starts very similarly: an array of blocks that can be added to the page. But these blocks are known as Outer blocks, and they’re usually
focused on the layout of the content that will go inside of them.
They then usually have another array of blocks, for Inner blocks. And Inner blocks can be thought of as more base types of content, like a Title, Rich Text, a button group, partner logos, article feed.
So rather than building one section and saying this is the content that can go in that section, we build an outer layout, and then let the editor choose what content goes in its inner area.
So let’s look at the earlier example now using Outer and Inner blocks.
Here we have a Hero block with a Heading, Tagline and a Button Group. But this time, the individual content items can be added and re-ordered, as they’re instead composed of Inner Blocks. If we then build a Ticker Inner Block, the editor can add this to their Hero Outer Block, and re-order it to where they need it.
They can then add these Partner names to any other Outer Block too.
Say they add it to this Steps block. You can see here it’s shown in a different colour, as that’s controlled by the Outer Block.
And they can now use this ticker anywhere across their site.
This Outer and Inner Block approach provides even greater flexibility for editors, without any extra controls on the block cluttering up the interface.
And despite that increased flexibility, editor’s still don’t need to think about design. They’re not adding rows and columns, changing widths or vertical spacing, or trying to design anything.
They’re simply adding the content blocks they need to display their content.
Making this even easier - Reusable Content
Now I’ve been talking about trying to make editing easier, but perhaps you’re looking at this and thinking that this maybe makes things a little bit harder.
There’s more flexibility for editors but there’s also more complexity to build a section from scratch. Editor’s have to think more about which blocks to use
So we’ve taken this a few steps further to really improve this experience.
The first thing we’ve done is to build out the ability to reuse blocks and compositions of blocks.
Imagine as an editor, you’re building landing pages, content pages, product detail pages, anything like that, and there’s a type of section you need over and over again.
Maybe it’s an image on the left, with Title, Text and Buttons on the right, and then a reversed version.
You can build this once, then save it as a reusable block, and easily include it in any page later on. It imports the individual Outer and Inner blocks just as if you’d built it yourself.
In terms of ease of use, this gives us parity with the Modular Section approach, as now editors can choose from a predefined list of pre-built Sections,
but it then also provides the flexibility on top of that to re-order content within that section, and add other types of content to it as well.
We have a section in the Structure for viewing and creating reusable blocks, and you can also save one or more blocks directly from a page as a new reusable block.
And you can take this as far as you like. For example, we have entire Landing Page templates set up as reusable blocks.
So you create a new page, pick your landing page template, change what copy needs changing, and push it live.
It’s super fast. And our editors are completely autonomous to create more landing page templates and re-use them themselves.
But again, choosing from a list of blocks and reusable blocks may not be that easy unless you know what those blocks are and what they look like.
Improving visual clarity with the Block Picker + Previews
So a further addition we’ve made, and something Sanity has also very recently built a version of into its core, is a more visual Block picker.
You’ve likely seen this in some of the screenshots I’ve shown already.
When an editor adds an Outer or Inner block, they’re gonna see a dialog open with names and images of all the available blocks.
These images help clarify what a block looks like and helps editors to choose the right block they need straight away.
The blocks are also split into categories, such as Hero, Section, Reusable, Synchronised, and Niche for ones we don’t use very often, and also have Tags and a Search input to further filter by.
And lastly, when editing pages created from Modular Content, the default UI would just show you the Preview of the Outer Block.So there’s two things we do here:
the first is to ensure all of our Outer and Inner blocks have a good preview set in the schema.
The second is to decorate the Outer Block array component to display any Inner Blocks on it as well.
This provides a complete overview of the page content, without any controls getting in the way.
Editors can see all of their content, add, re-arrange and delete Inner blocks without opening the Outer block first, and click straight-through to the Inner block they want to edit.
Minimising chores: Syncable Blocks and Block Transformations
So we’ve looked at increasing our editor flexibility and autonomy through Outer and Inner blocks. We’ve looked at improving content turnaround time through reusable blocks. And we’ve seen how to make using them much easier through a visual blocker picker.
And yet there’s still more we can do to improve this editorial experience.
When editing content, we saw a number of chore tasks come up. Things that needed to be done to allow the editor to do their real work, and which just consume time.
Reducing or eliminating these chores would make for a more pleasing experience, with less effort and less time used.
One chore was keeping certain content blocks in-sync across pages.
Imagine a signpost at the end of certain pages, which has a specific call to action based on the page content.
In the past, we may create a Signpost document for the content, and a Signpost Section for the page, where the user can select the appropriate Signpost.
But this is scoped to just signposts. What if there were other content sections editors want to synchronise?
So instead, we created Synchronised Blocks. These can be created using all the same tools as Modular Blocks, and then they appear in the Block Picker under
the Synchronised section.
Editor’s can then select a Synchronised block to include it in their page.
When added, they would appear in the Block content as a Synchronised block, with its name, and a note about what a synchronised block is.
It also says if you want to edit it, click here, or to de-synchronise it for just this page, click here.
Then they can make edits to all versions of a synchronised block from a single place.
They can also de-sync a block within a page, which turns it into regular blocks again, ready to be edited.
Another chore we recently started looking to tackle was changing the Outer block but keeping the Inner blocks the same.
This required deleting the block, adding a new one, and re-creating the Inner blocks inside.
We haven’t built a solution for this yet, but we have decided on creating the ability to define “Transform” functions on Outer or Inner blocks,
allowing them to be changed to other available blocks.
For a given block, its transform functions will be defined in its schema, and they will describe which blocks it can be transformed into, and how to map the data to the new fields.
For instance, a Hero could be turned into a Section, with its content kept intact. Or a Title could be turned into a Rich Text, or some other similar field.
Code Demonstration of Implementation
Now that I’ve talked about what I think is the platinum standard for page editing, I wanna dive into some of the code and talk through how we achieve this.
We don’t have time to go through everything, so I’ll focus on the Outer and Inner blocks aspect for now.
We take a monorepo approach to our builds, to help separate concerns.
So we have a modular-content-blocks package here, with a lib and a blocks directory.
First I’ll show you how a developer adds a block, and then I’ll show you how it’s all automatically hooked up.
This is designed to be as easy as possible for developers to add and edit blocks: they just need to add the appropriate files into a new directory in blocks, and the rest is taken care of for them. We aim to co-locate code by function rather than by type, so everything our block needs lives in a single folder, which makes it really easy to add and edit.
So let’s say we’re creating a section outer block. We need 5 files:
- The block’s Sanity schema
- The block config, which contains any data that is shared between Sanity and the web app, in this case just its identifying name
- The block Component that will be shown on the web app when this block is used
- A web file which provides a single place to export everything the web needs: the config, the component and the groq query for this block
- An optional image to display in the block picker for this block
Once that’s been created, the block is immediately ready for use in the Outer or Inner block section of the blocks picker.
Under the hood, we’re doing a few things to load this block.
On the Sanity side of things, we’re registering two object fields: an Inner and an Outer modular content field. These can then be used in any other document or block schema.
This is done by using vite to automatically glob all the schema files from the Outer and Inner directories,
add them all to our “objects” in Sanity schema,
and then define the outer ones as array members of our Outer Blocks field, and the inner ones as array members of our Inner Blocks field.
We also have helper methods here that allow us to define other modular content fields in future, for example if an Outer block only wants to allow specific Inner blocks, or wants to prevent certain inner blocks, it can do that easily in its own schema.
Each block schema also uses a defineOuterBlock helper method, which sets the item component we need. This component is how we render the Inner Block fields on the Outer Block component.
We take the children fields and filter out those that aren’t modular content, then return it and display it on the component.
We also define a fieldset in this helper, which is our standardised place for block configuration, like an anchor ID, etc.
As for the Web side of things, there’s fairly little that’s involved to make this work.
There’s two main aspects: a groq query containing all the combined groq queries from each Inner and Outer block, and a Modular Content Renderer component
We add the groq query into our initial page query, so that it can be loaded in a server component. Then the Modular Content Renderer component is rendered on the page, and the query data is fed into it.
It then renders out each block’s component, passing in the data to the block that it received from the page query. It also adds some classes to each block based on the blocks name.
There were two main challenges for this part: vertical spacing and passing through encoded data for Visual Preview, for use on images and other non-text fields.
For Block Spacing, we define a default spacing between any two outer blocks and any two inner blocks.
We then customise this spacing using the classes for specific cases. E.G. two outer sections with the same tone may have a smaller spacing between them than ones with different tones.
Rich Text immediately after a title may have less spacing, and a Title immediately after anything may have more spacing.
Working closely with your design team from the start will make this part much easier, and if your designer is already used to modular sections then they will likely already be taking the right steps to think in terms of components, in terms of standardised spacing and rules governing the design that allow you to translate them easily to code.
As for the encoded data, while all text-based content still works out of the box the main challenge here is in adding
encoded data to images and other non-text content. Specifically in calling the `encodeDataAttribute` function with the correct path for the data.
This is because the data is nested without the components knowing about the parent nesting. We don’t want to prop drill that data
as it makes our components API more verbose and harder to change.
We created SanityDataEncodeProvider and a SanityEncodeAttribute component to help with this.
Conclusion
And that’s how we do Outer and Inner modular content.
There’s lots more I could cover here but we don’t have the time to go through it. So I’ll probably be releasing more demos and talks on this, and if you have any questions you can get me on twitter, LinkedIn or in the Sanity Slack.
I hope you’ve enjoyed the talk, and I’d love to hear from anyone that gives this approach a try.
I think it opens up a world of flexibility and autonomy for editors, and I think it’s quite a cool approach.
Thank you so much