Building custom web pages made flexible with Craft CMS: Our story
At Pipedrive, we used Craft CMS, an open-source system, to build our public Pipedrive site. We had a lineup of solid components, such as our Hero block, Testimonial, Rich text, CTA block, and Pricing block, to name a few. With Craft CMS, content managers could easily stack these fixed components to create a page in no time. For example, with the hero block, they could simply choose an image, add some text — and voila, it’s good to go! This setup worked well, but soon we discovered that trouble was brewing.
The problem
As the components are inflexible, making even slight modifications could be time-consuming. Something as seemingly simple as moving an image from left to right in the hero block could take longer than a week, sometimes even longer depending on how busy the development pipeline was. To accommodate the various versions, we often found ourselves creating new components. Needless to say, this was not an efficient workflow, especially as our company expanded. Not only did it add unnecessary complexity and inefficiencies, but it also set the number of columns in our content table skyrocketing toward the limits of MySQL’s tolerance. It was becoming clear that we would soon be unable to accommodate the requirements of our content managers. The situation was like a ticking timebomb, ready to explode.
At Pipedrive, we aim to offer a fully customizable solution for our content managers, enabling them to quickly and easily build new web pages without any dependencies. So, we started looking for alternative solutions to overcome this limitation.
We had clear requirements for our setup:
- Able to accommodate web pages of different layouts, such as blog articles, landing pages, knowledge base articles, and more;
- Give content managers the freedom to come up with any creative layout and design they want;
- There must be a way to maintain design consistency across the system;
- Site localization to support our global presence;
- The system must be able to integrate with third-party services;
- Customizable yet feature-rich asset management; and
- Easily scalable, customizable, and capable of handling heavy traffic.
There were plenty of other niches yet essential requirements. The above is a selection of the most relevant ones.
We spent a significant amount of time reviewing various popular CMS systems. They were all amazing and solved many problems, but they still needed to check all of our boxes regarding flexibility, customization, ease of use, and seamless integration between our systems. The flexibility that Craft CMS provided was quite unmatched. Another crucial factor that influenced our decision was our existing familiarity with Craft CMS. So, we returned to our existing, inefficient setup and asked ourselves: did we set up our current system the right way? The answer was staring right at us for the past few years: no. In a happy coincidence, this was around the same time Craft CMS released version 3.5 RC, a significant overhaul. Among other cool changes, the one that caught our attention was the new field layout designer.
With this new version at hand, we set out to dive more deeply into Craft CMS, enabling content managers to easily create and modify web pages without generating an incomprehensible maze of fields. Although Craft CMS offered robust field types, we needed something more. That’s when we discovered the craft-neo plugin. It was like a superhero swooping in to save the day and open up a whole new world of possibilities. At this point, we were confident about rebuilding a public web architecture that checked all our requirements and was also simple to use.
Revamping the CMS and its structure effectively invalidates the UI component that we built over time, so we were looking at rearchitecting the whole stack, not just the CMS.
The plan
We needed a few parts for the system to work:
- The design library;
- Setting up the CMS;
- Post processor: Translations and other entry enhancements are done here; and
- The renderer.
The design library
A seamless yet cohesive integration between the CMS, renderer and design library was essential. During the research phase, we strongly considered going entirely with the atomic design methodology, a creative design approach inspired by chemistry. But ultimately, the modular composition model allowed for more granular control over individual components, making mixing and matching them easier to create custom interfaces. In tandem with Craft CMS, this gave us a high degree of customization. Additionally, modular component libraries can be more efficient in terms of development time and maintenance.
One potential downside of this approach is that it is time-consuming and requires significant upfront planning, which might not fit smaller projects well. Fortunately, that didn’t apply to us.
After extensive research and planning, we divided our designs into smaller pieces. Once we had a solid plan, we rolled up our sleeves and built these components. We chose to build it with React.js for a couple of reasons. Firstly, React was widely used and recommended within Pipedrive. Additionally, the frontend framework we selected, Next.js, is built on top of React, making it a natural fit for our project.
We’ve separated the library pieces into five main parts:
- Core: The foundation of everything that’s built in our public web. This is necessary to build other components;
- Elements: simple building blocks for the web. They are either an implementation of the core or a combination of other elements;
- Components: building blocks that need a wrapper around them and are meaningful. These can consist of a core, elements and other components; and
- Patterns: Big building blocks that are arranged or connected in a specific way.
As we built this library, ensuring it was not exclusive to this specific CMS configuration was always top of mind. It had to be friendly to other potential consumers.
Setting up the CMS: let’s Craft!
Throughout the development process, we focused on various important aspects, such as field layout, reusable containers, global variables, tags, localized assets, page preview, element-API endpoints, webhooks, health-check, logger and more. Later in this article, I will focus on the field layout aspect and detail exactly how we tackled it.
Landing pages needed most of our effort since they cannot be contained in a singular layout. We created a Section called “Landing” for that, and the field layout looked like this:
- Meta: self-descriptive, high-level, describes the page itself;
- Content: contains all the fun! Components are built here; and
- SEO: Overriding default SEO settings and adding additional related information.
Wondering how we designed the structure of the page content? To begin with, there are top-level containers (layout components) in place. Along with Container, there are CTA blocks and a Comparison table, which are premade containers we frequently use, so they made it onto the list.
We built the entire Content builder with a new field, which allows you to create blocks with a separate field setup. These blocks can also have children, which is perfect for the composition model we were trying to achieve.
Here’s a simple example of adding a two-column layout with a button and text. The Container is a top-level component that holds the grid. The grid is formed with two layout components called Row and Column. It should be easy to understand that most design requirements are achievable with row and column configuration.
We faced a challenge when displaying the grids between different screens. We solved this by splitting the viewport into a max number of columns for each type: desktop, tablet and mobile. The maximum number of columns is 12, 6, and 3.
The Size property determines the width(columns) of the column relative to the screen. Selecting the maximum number means it always occupies the full width of the viewport.
Every component in the CMS has at least three pieces of data attached to it: Name, Properties and Children. Properties values are passed to its UI component counterpart, while Children are the other components it encompasses (remember the composition model we mentioned before?).
Once the entry is saved, Craft sends a webhook notification to a processor with the below data (I have trimmed this down to make it easier to understand the structure). Webhook invokes a custom serializer that queries Craft and converts it into a well-structured JSON format that’s easily understandable by other systems and the front-end renderer.
{
"id": 123,
"title": "Test entry title",
"slug": "test-entry-title",
"enabled": true,
"structure": [{
"name": "container",
"properties": {
"theme": "default"
},
"children": [{
"name": "flexRow",
"properties": {
"horizontalAlign": {
"xs": "center",
"s": "center",
"m": "center"
},
"column_verticalAlign": {
"xs": "start",
"s": "start",
"m": "start"
},
"verticalGap": {
"xs": "none",
"s": "none",
"m": "none"
},
"direction": "row"
},
"children": [{
"name": "column",
"properties": {
"size": {
"xs": "3/3",
"s": "6/6",
"m": "12/12"
}
},
"children": [{
"name": "button",
"properties": {
"type": "primary",
"color": "default",
"size": "m",
"width": "auto"
},
"children": [
"Try it free"
]
}]
},
{
"name": "column",
"properties": {
"size": {
"xs": "3/3",
"s": "6/6",
"m": "12/12"
}
},
"children": [{
"name": "text",
"properties": {
"textAlignment": {
"xs": "left",
"s": "left",
"m": "left"
}
},
"children": [
"<p>Create the stages of your sales funnel or use an existing template. Add your deals or import them automatically from a spreadsheet or CRM.</p>"
]
}]
}
]
}]
}]
}
Notice that the structure has a list of components and each component has a name, properties and children (components), recursively.
The entry processor translates this JSON, runs some post-processing steps on top of it, and then stores it in a database. Next is the final part of the puzzle: the renderer.
The renderer
We have arrived at the final piece of the puzzle, and our task is to connect everything. We chose Next.js for the job. Next.js’s pre-built tools and features have been truly remarkable. Its focus on performance right from the start was particularly noteworthy, and we also appreciated the added convenience of accessing the real-time core web vitals scores through an API.
The renderer is a bridge connecting the UI library and component structure. It houses some complex components that don’t fit the design library philosophy. Such components are exceptions, and we try to minimize the number of such components as much as possible.
Challenges
- The greater the level of customization, the more complicated it becomes to comprehend. It demands a deeper understanding of the system and imposes a learning curve for new content managers. Therefore, it’s crucial to involve the content team closely in the implementation, gather their feedback, conduct demos, and iterate the process.
To simplify the process, we created reusable components, containers, and blocks that can be built once and utilized across multiple pages. This eliminates the frustration of repeatedly constructing the same thing. - The system quickly became bulky and resource-consuming. We have addressed this by vertically scaling the system’s infrastructure, while also exploring the option of horizontal scaling through the utilization of a centralized cache and the session storage mechanism that Craft supports.
- There’s one piece of the puzzle that’s been eluding us: a reusable layout. While Craft CMS provides vast flexibility, we were unable to find a magical field type that would allow us to create a layout once and reuse it across multiple entries by selecting the desired components.
Conclusion
Finding the right CMS to accommodate the requirements of your business can be challenging. While the current system may not be perfect, it is being actively modified and updated to accommodate our ever-changing design and business requirements. Our journey to find the right setup for the public site proved to be a valuable learning experience, and we hope our experience will help other businesses facing similar challenges.