Meet Bookish, an install profile for static Drupal blogs
For the last four years I’ve been working on a static site generator for Drupal called Tome. Unlike other generators Tome uses “vanilla” Drupal, which means that if you know how to build a Drupal site, you know how to build a Tome site! One downside of this is that when comparing a default install of Drupal with a default install of something like Gatsby, Drupal looks pretty outdated. I wanted to show Tome off but couldn’t do it well with core, so I decided to focus my energy on a new install profile for static blogs - Bookish.
Bookish is similar to core’s standard profile, but every feature has been tweaked to provide the best out of the box experience possible. I’ll go through those features in this blog post, but if you want to skip ahead and make a static blog right now, you can follow the instructions on GitHub or view a demo Bookish site.
Frontend
Bookish’s default theme was designed to look modern while still being simple enough to be easily extensible. It uses vanilla CSS and JavaScript, but makes use of newer features like CSS grid, CSS variables, and dark mode. It’s mobile friendly and I’ve worked to make it accessible.
By using vanilla CSS grid and CSS variables, users could accomplish a lot by only providing a custom stylesheet. Not having a build step also means that no tooling is required to make changes, which removes another barrier to entry for users.
Practically all of the HTML, CSS, and JS in theme come from Single File Components (SFC), which will feel odd for a lot of users. SFC is a Drupal module I wrote that lets you write CSS, JS, Twig, and PHP in one file that represents a “component”. Once created, SFCs can be used like any other Twig template, and if you look at the source code for the theme you’ll see that every “normal” template in the theme just passes Twig context down to a SFC.
I’ve also made an intentional choice in Bookish to not use configurable view modes or Views. Instead, I do everything in SFCs using Twig Tweak functions and entity queries. As a frontend developer I want to have complete control over all the HTML in my site, and Drupal’s site builder features can often feel like footguns. I have a much easier time theming Drupal using SFCs, but their usage here may actually steer people away from using Bookish. I’m not really sure yet, but I hope that it shows how useful they can be!
End users don’t really see any of what I just talked about, but they will notice that the theme also performs a “blur-up” effect for all images. A blur-up effect is when a blurred placeholder for an image is displayed while the full size image loads. To accomplish this, I created a field formatter for images that creates a 42px wide version of the image style and displays that as a temporary background image using a data URI. The blur effect is added using CSS, JS is only used to add classes that help smoothly fade in the full size image.
My version of the blur-up effect does not rely on JS and has no third party dependencies, which I’m proud of. I’ve also found that the blurs look better than alternatives, but that’s mostly because of the thumbnail resolution. 42px doesn’t sound like a lot, but it does increase the page’s response size.
Metadata / SEO
Bookish includes pre-configured support for metatags, RSS feeds, and sitemaps.
For social sharing, metatags are important for making sure social cards look nice and clickable. Page and Blog nodes include an image field to show for the social card, however not every path in Bookish is a node. For that reason I’ve also included support for a default “social node”, which is used as a fallback for every other path so that you always get a nice image and description even if you’re sharing something like the 404 page. This was a trick I learned when I built sites professionally, and should probably be contributed back to the Metatag module at some point.
RSS feeds and sitemaps may seem a little old school, but are still important for syndication and for search engines. Especially for smaller sites, every path on your site isn’t guaranteed to be crawled, so a sitemap can help to force a search engine to index your entire site.
Editing experience
Image editing
The image widget built into Drupal is pretty basic, and I’ve always been jealous of newer site builders like Wix and Squarespace that have image editing built into the CMS. As a result, in what must have been a fever dream, I created an image widget and image effect that allows users to edit the color and crop of uploaded images right in Drupal.
To accomplish this, I added a new base field to file entities that stores information about the image’s color and crop. Then, I used the AJAX API to create a widget that allows you to edit the image, only committing those edits on node save. The design of this feature means that color/crop edits are stored alongside the image, not the field, but this doesn’t have many downsides as images aren’t re-usable in Bookish.
Performance-wise, it feels a little slow to wait on a server response to see the results of your tweaks, but since most Tome users run Drupal locally the lag is negligible. If I could replicate the image effect perfectly client-side I would, but from my testing it’s hard to replicate how GD works.
Cropping is done using my own version of focal point, which I created both to have one streamlined widget and to remove a dependency on the Crop API module. With Crop API, new entities are created for every crop you make. When using Tome, this can lead to a lot of noise as you now have two entities for every image you upload instead of one. I didn’t find the focal point logic too hard to replicate, but I’m sure my implementation has some rough edges.
Trying out CKEditor 5
CKEditor 5 is coming to core to replace CKEditor 4 (hey that rhymes) so I figured that I should give it a try here to “future proof” Bookish. From an end user perspective it feels pretty similar, but I’ve done some work to make it nice for technical blogs.
Code snippets are supported via the normal Code Blocks plugin, but are highlighted through a module I wrote called highlight_php. This module uses the composer package scrivo/highlight.php to add syntax highlighting server-side. I’ve made sure to include light and dark theme styling as well into the theme.
Heading links - those little 🔗 or “#” symbols you see next to titles in blogs - are also added with a server-side text filter. I don’t even have those on this site, so I’m already a little jealous of Bookish users.
oEmbed is supported using the standard Media Embed plugin. The rendering of oEmbed content is mostly done client-side using the “previewable” feature that CKEditor wrote, but I’ve also added a text filter to add additional support for Twitter and Flickr. It’s quite easy to use - any YouTube, Vimeo, Spotify, Dailymotion, YouTube, Twitter, or Flickr URL can be copy+pasted into the editor and get auto-magically rendered.
The name “Media Embed” is a bit misleading - Bookish doesn’t actually use core’s Media module at all. While I have been a Media contributor in the past, I don’t think it’s a fit for every site and don’t think it should fully replace files and images. The fact that oEmbed in core is reliant on the Media module is odd to me also - how common is it to re-use an embedded YouTube video when all you have to do is paste the URL again? Media is a good fit for more complex sites, but for single-user blogs it seems like overkill.
Lastly, I made sure my image color/crop editing magic also works with images uploaded to CKEditor5. You can also select an image style when embedding an image, which again to me is much easier than using Media view modes.
Improving core tagging
Core’s entity reference widget for tagging feels very odd - almost all other sites I use represent tags as “pills” or “chips”, not a comma separated list with the entity ID leaking out into user space. To address this I made a quick widget that uses Tagify, a JavaScript library that has great UX for tagging. Tagify syncs changes to and from JSON, which means the field widget just has to parse that data, lookup the tags based on their title, and save that in the field.
Simplifying the toolbar
The toolbar that ships with Drupal has always frustrated me - it doesn’t remember when it’s collapsed (yes I know I could file an issue to fix this), and I never use the sidebar view. The menu links also aren’t completely user configurable - only the “Shortcuts” section is. To calm my toolbar rage, I whipped together a new toolbar that has no nested menus and only displays the default shortcuts.
I also installed Coffee and Coffee Extras, which to me are much faster ways of navigating Drupal’s admin area than any toolbar, including Admin Toolbar. The keyboard shortcut takes some getting used to, but I’ve found it to work really well.
Providing in-site help with Help Topics
Help Topics is a newcomer to core, and allows you to provide multiple help pages for your modules. I’ve used it in Bookish to document things extensively - so once users log in they can just click “Help” in the toolbar and browse through the documentation without going off-site. This was a breeze to implement, so if you haven’t tried it out for your projects I recommend it!
Static faceted search
For search, the Lunr module is used to index blogs and pages which allows them to be searched client-side. As a part of integrating Lunr into Bookish, I made a few updates to make faceted search a bit easier. This work allows you to filter search results by tag, but could be used for any field that uses a toggle.
An optional contact form with Netlify
When you use Tome there is no Drupal backend, so any module like Webform will not be able to store submissions server-side. If Bookish users need a contact form and are hosting on Netlify, they can enable the “Bookish Contact” module, which uses Netlify forms to store form submissions and filter out spam using reCaptcha. While this feature is Netlify-specific, it isn’t that complicated and I think doing something similar for any SaaS form provider should be easy.
Speeding things up with client-side navigation
As I was wrapping up Bookish’s development, I started noticing some “Flash of Unstyled Content” (FOUC) issues on my demo site which ran on Netlify. This only happened in Chrome, and appeared to be a problem related to a combination of dark mode, blocking JS, and Netlify’s lack of cache headers. I could have let this slide, but I wanted to show my work off and having the page flash every time you clicked a link was making me really upset.
So, like any normal person would, I created a new module called “Bookish Speed” that performs client-side navigation to replace just the <main> content of the page when links are clicked. This fixes the FOUC issue as the old CSS and JS isn’t reloaded on navigation.
Without going into too much detail, when a link is clicked the new page is requested with fetch(). Then, the response HTML is parsed to pull out the drupalSettings, which now includes references to all the CSS and JS files that are on the new page. The current assets are then diff’d with the new assets, and anything that needs to be added to the current page is added. Then once the assets are ready, the content of <main> is replaced and Drupal behaviors are re-attached. For accessibility reasons, I also announce when the new content arrives.
The disadvantage of using Bookish Speed is that sometimes normal browser navigation would be faster than client side navigation - for instance if a stylesheet takes a long time to load. I actually think it may be more useful for Drupal sites that work more like web apps than blogs, which is why I made it an optional module.
How to use Bookish
That’s about all the features I designed for Bookish - if you’re interested in trying it out yourself, you can follow the instructions in GitHub. Also, while Bookish was designed for Tome, you can always uninstall Tome Static and Tome Sync after installing Bookish to use it with traditional Drupal hosting.
If you want to use some of the features of Bookish without making a new site, I’ve published all the modules to Drupal.org too in the “bookish_admin” module. This module is automatically synced from GitHub using a GitHub Action, so it should always have whatever new features I push to the main Bookish repository.
Some reflections
I really liked working on this project - I don’t make Drupal sites for work anymore, and haven’t worked on an install profile in a long time. In the past I’ve contributed to Demo Framework, Lightning, and Bene which heavily influenced Bookish in terms of the feature set. This is my way of sharing how I would make a Drupal site today with all of you, so even if you’re not interested in Bookish maybe you can take the parts you like and use them in your own site!