Skip to main content
Back to blog Samuel
Mortenson

Building my site with Tome and Single File Components

I've just finished re-building my site using Tome and Single File Components (SFC), two Drupal projects I maintain and wanted to test out on a real site. If you're reading this post, you're already on my new website! Hope it's working OK so far.

I wanted to write a post going over the build from start to finish, to discuss the process I took and the challenges I ran into. If you want to skip the write up and go straight to the code, the repository for this site is public.

Motivation

My old site was built using a combination of hand-written HTML files and a separate Jekyll site for my blog. Since each HTML file was unique they all looked different, had different markup, and had accessibility and SEO issues. But, the frontpage did have a 3D shark, which I'm pretty sad to lose:

The frontpage of the previous site.

The Jekyll blog worked nicely since I was using the default theme, but editing markdown was a huge pain. Getting consistency on the site was important to me, so I decided to use Drupal and Tome, two projects I was very familiar with. Tome is a static site generator I wrote that shouldn't really affect the Drupal build, it's more about how content is stored (in the repository) and how HTML is generated. I also wanted to use SFC for the Drupal frontend, since it's new and I hadn't really given it a stress test.

Design

I wanted the design of the new site to look clean, but not without some character. I also wanted each content type - blog, work, and gallery - to have a unique look and feel. I'm not a designer by trade, but I know the basics and already had some inspiration going in the form of a collection of 70s-80s technical manuals.

The design only went through two major iterations, with the second (and final) being very grid-focused, with lots of elements offset and stacked in creative ways. My regret is that the listing pages and blocks in the design ended up using standard one or two column layouts, although the gallery page does a bit of creative stacking.

The software I used for the design was Affinity Designer, a vector illustration app that I was already familiar with. It's a nice affordable alternative to Adobe Illustrator and is perfect for making SVGs.

Initial setup

After deleting the original site's codebase, I used the Tome project template to create a new empty Tome site. I was happy to see that the old documentation for Tome still worked without any issues, and jumped straight into the site build. I started with the minimal profile, which results in faster Tome builds and is easier for me to work with. I also created the content types and fields early, since I knew they were simple and wanted to get to design as soon as possible. Next I installed SFC and got to work on the components.

Trying something new with components

I wanted almost all of the markup on the site to be inside a component, which got me thinking about a few paradigms in Drupal. Looking at the design, the homepage was very unique, which is typical. The header and the blocks highlighting recent blogs/work/gallery items were only used on the homepage, and were never re-used. They also didn't contain markup that needed to be changed often.

For a larger site with ambitions for multiple landing pages, I would probably use Paragraphs or Layout Builder, but that seemed like overkill for this site. I started to think about page-level components, and wondered what it would take to make this happen with SFC. I created a homepage component, mc_home.sfc, and built a new feature in SFC that would allow modules to quickly create routes that display components. The code for mc_home.sfc is:

<template>   {% include 'sfc--mc-about' %}   {% include 'sfc--mc-blog-list' %}   {% include 'sfc--mc-work-list' %}   {% include 'sfc--mc-gallery-list' %} </template>

And the route definition for the homepage is:

mortenson_components.home:   path: '/home'   defaults:     _controller: '\Drupal\sfc\Controller\ComponentController::build'     component_id: 'mc_home'     _title: 'Home'   requirements:     _access: 'TRUE'

Now I had a pattern for defining arbitrary pages with components, which freed me from a lot of Drupal's complexity.

Next I had to define the list components, and another problem with owning all the markup came into play. Typically lists in Drupal are made with views, which makes it easier to assemble SQL queries from the admin interface. The problem with views is that they come with a ton of markup that's hard to modify. Making one Twig template that contains everything in the view isn't really possible, so you can't have one component replace one view.

I decided to make components that had their own entity queries, like what you would do with GraphQL in Gatsby. For my use case this was great - my site is simple and views didn't really provide me any value. On a larger site you would have to think about how to re-implement paging, relationships, translations, and more things that views gives you for free, which may make this approach unviable.

When I said that views didn't provide me any value I am missing one benefit of doing things the Drupal way - correct caching. If I control all of the markup on the site, that means I'm also responsible for all of the caching. This led me to my next SFC feature, the {{ sfc_cache() }} Twig function. This function makes caching in components a lot easier - you can pass it cache tags, contexts, max-ages, or any cacheable object, like an entity. For example, the component that lists blogs uses the function like this:

{% for node in nodes %} {% include 'sfc--mc-blog-teaser' with { 'title': node.label(), 'link': path('entity.node.canonical', {'node': node.id}), 'text': node.body|view({ 'label': 'hidden', 'type': 'text_summary_or_trimmed', 'settings': {'trim_length': 350} }), 'title_element': 'h3', }%} {% endfor %} {{ sfc_cache(nodes) }} {{ sfc_cache('node_list') }}

Which ensures that the page that uses this component is cached directly.

With these two new SFC features, I was unblocked to have all the markup on my site live in components controlled by me, which is the first time I've had that happen in Drupal.

Theme and component architecture

I don't work with component based design a lot, which is odd given that I maintain SFC, but did make a few architecture decisions on this site that felt right to me.

Components either represent full pages (like the homepage), unique page elements (like the header), or re-usable elements (like titles and teasers). I don't see a reason for making a component out of something super small, like the title of a teaser, unless it's going to be re-used. There are still ~22 components on this site, but each one has a clear purpose and name, which lets me know which one to edit without thinking too hard.

Templates for components are abstracted from their data model at the lowest level. So teasers and full node pages have no references to entities or fields. That means that higher level components, like lists, are responsible for mapping fields to template variables. For example, here's the content of node--blog--full.html.twig:

{% include 'sfc--mc-blog' with {   'title': node.label(),   'time': node.created.value,   'text': node.body|view({     'label': 'hidden',     'type': 'text_default',   }), } %} {{ sfc_cache(node) }}

Note that the view filter there is provided by Twig Tweak, which is an awesome module for component work.

Keeping the lowest level of components data model agnostic let me work on styling before I had real content, which is a good workflow for larger teams.

For component styles, I ended up using BEM (or at least my understanding of BEM) to keep selectors small and make scoping easier to understand. Since the site uses vanilla CSS (no build step!), I also went with CSS variables for everything that could be shared. The Drupal theme then acts more like a skin on top of the components, and defines the CSS variables they use. Here's what that definition looks like:

body { --font-family: 'Source Sans Pro', sans-serif; --font-color: #1E1E1E; --font-color-light: #e3e3e3; --font-color-link: #1F7DB5; --font-color-link-hover: #175680; --font-color-link-light: #238CCA; --font-color-link-light-hover: #23a5e4; --font-weight-regular: 400; --font-weight-semibold: 600; --font-weight-bold: 700; --font-weight-black: 800; --accent-color-1: #E3919A; --accent-color-2: #AADFFF; --background-color: white; --background-color-dark: #1E1E1E; --spacer-1: 10px; --spacer-2: 25px; --spacer-3: 50px; --spacer-4: 75px; font-family: var(--font-family); font-weight: var(--font-weight-regular); color: var(--font-color); margin: var(--spacer-3) 0; background: var(--background-color); }

With this in place, my theme became a component consumer - it provides no unique markup within the main content of the site and has no component-specific CSS. I'm fairly happy with how this turned out, but if I made the components re-usable outside of this site I would probably need to prefix their CSS variables, which could get messy. Imagine using three component libraries and having to copy your font variables three times, that's a lot of CSS!

Some Tome trouble

For the most part Tome worked great, but I did run into one bug I had to fix. In Tome Sync (the thing that syncs content to JSON), a lot of effort has been put into removing IDs and replacing them with UUIDs. This is done to prevent merge conflicts and data loss, which is possible with multiple editors or even with entity deletions. It turns out that exported path aliases could contain entity paths like /node/1, which would lead to the same ID/UUID problem with other entity references. That led me down a rabbit hole with path aliases, pathauto, and the import process that ate up days of my time. I got one fix in, and one issue left to fix that wasn't blocking the site launch.

Search

Search on the site is done using the Lunr module, which I also maintain. Lunr worked out of the box, and the only tweaks I did were to styling. It felt good to have at least one project work seamlessly!

Other projects used

Here are all the other projects used for this site:

  • Admin Toolbar - basically essentially for me to use Drupal without going insane, it adds dropdown support to the toolbar and a quick way to rebuild cache.
  • Metatag - To improve SEO I installed Metatag and set up a very basic Twitter/Facebook integration.
  • Codetag - I needed a way to embed <code> tags in CKEditor, and this module did the trick.
  • Pathauto - This automatically generates path aliases for work, blog, and gallery item nodes.
  • Redirect - I used redirect to make sure old blog paths redirected to the current alias. If path aliases change in the future, this module will automatically create redirects.
  • Simple XML Sitemap - I wanted my site to have a sitemap for SEO reasons, and this provided that.
  • Claro - I'm using the new Claro theme, which feels a lot better than Seven so far.
  • highlight.php - I wrote a custom module to automatically highlight code snippets as a part of a text filter. This used the scrivo/highlight.php project, which is a PHP implementation of highlight.js. It's working pretty nice so far, even if it misses the language detection every now and then.

GitHub Actions

To build the site, I tried out GitHub Actions, which builds the static site and commits it to a staging branch for GitHub Pages. I used my existing Tome Docker image which saved some time, but trying to configure existing actions (I tried three) for deploying to GitHub pages took a lot of work.

Now that it's set up - when I commit changes from Drupal locally, GitHub Actions will install Tome, run cron (to generate the sitemap), index Lunr search, run a static build, and push the static build to a staging branch. I then review the staging branch locally (no branch previews for GitHub pages), make a PR to master, and merge to deploy to production.

Netlify is a bit easier in my experience, but I haven't used GitHub Actions before this so it was good practice for me. You can view the GitHub Actions configuration here.

Conclusion

The site is live now, and I'm glad that it turned out as well as it did. I have some changes I'd already like to make - more content for the gallery (I need to draw more), better summaries for existing blog posts, more Tome bug fixes - but it feels good to see so many of my projects come together and deliver a site and editing experience that is functional and easy to use.

Thanks for reading, and make sure to check out the GitHub repository to build the site locally and make sure to check out the components I built!

Edit 2/9/2020: I've added a style guide page for the components used on this site, check it out at /components!