Will Browar https://wbrowar.com/ https://wbrowar.com//theme/logo.png Will Browar https://wbrowar.com/ RSS Feed for Code articles on wbrowar.com en-US Sun, 17 Nov 2024 12:28:47 -0500 Sun, 17 Nov 2024 12:28:47 -0500 Making WordPress Users Feel at Home with Craft CMS https://wbrowar.com/article/code/making-wordpress-users-feel-at-home-with-craft-cms Thu, 10 Oct 2024 07:28:00 -0400 Will https://wbrowar.com/article/code/making-wordpress-users-feel-at-home-with-craft-cms Like most web developers I knew when I was starting my professional career, my first content-managed site was built on WordPress. I think if you ask most web developers who do content-driven websites (marketing sites, blogs, etc...) they all have at least one or two WordPress sites under their belt. When I started working at an ad agency in 2007 I switched to using Drupal for most things, then moved onto projects in Joomla, Statamic, Sitecore, Expression Engine, and a bunch of smaller—mostly proprietary—CMS platforms. Thanks to Andrew Welch’s introduction in 2014, I started using Craft CMS for all personal and client projects and I haven’t turned back since.

At least whenever I had the choice of the platform or when I could guide a client into the direction of which platform to use, we almost always wound up with Craft CMS being our choice. Over the years of being the senior web developer I had spent a lot of time discussing CMS platforms with clients and there were many times where we discussed the difference between WordPress and Craft CMS.

I tried to keep things objective because a lot of times clients were asking for what they were comfortable with in WordPress and they were biased coming into the conversation. A lot of times when we looked at what the client actually wanted their website to do it was easy to show how a Craft CMS site could feel familiar to a WordPress user while also letting us take advantage of the way Craft CMS gave us developers flexibility in content modeling and on the front-end.

With all of the WordPress versus WP Engine drama going on in the past few weeks, I've seen several folks asking for alternatives in case they feel like moving off of WordPress for their businesses or blogging platform. While to be completely honest with you I may not be up to date with all of the features in WordPress these days, but here are a few things that I’ve discussed with clients in the past.

Navigating the CMS

Dashboard

When you log into your Craft CMS site the first thing you see is the Dashboard. Just like WordPress, the Craft CMS Dashboard is customizable and each user can set which widgets they want to appear. There are some built-in widgets that come with the CMS and third-party plugins can also provide you with widgets to add to this page.

Next to the Dashboard widgets is the sidebar that follows you throughout the Control Panel. The links in the sidebar can change based on what kind of user role you have (admin users can see all of the links available), what features of the CMS you are using, and if you have any third-party plugins installed. The one you might use the most is the Entries link and that takes you to where most of your site’s content lives.

Admin bar

When you click on the name of your site at the top of the sidebar you will be taken to the homepage of your site. One thing you might notice is that the Admin Toolbar from WordPress isn’t there by default, but you can install a third-party plugin, called Admin Bar, that is similar to the Admin Toolbar in that you can navigate to a page on your website and use an Edit link to go directly to the edit page for that entry.

Posts, Pages, and Entries

In WordPress you have no doubt heard the terms Posts and Pages. Craft uses the term Entries to describe a piece of content, but Craft CMS content isn’t always treated as a single page in your website. An entry could be displayed as a page of content, or it could be a list of categories, a single article, a product, a menu item, etc... It’s up to you how you want to structure your content and Craft CMS gives you flexibility to build a simple blog with a section of blog post entries or a complex tree of product entries and product sub-pages.

On my personal blog, I have a section, called Article, where posts like this one live. I customized the entry list page to show me fields like the summary field so I can make sure I didn’t forget to write a summary for every article; and I hid the author column because it’s just me over here 😊

Entries list

Menus

The Pages section of WordPress has another purpose in that in addition to letting you edit single pages it also can decide the structure of your pages and you can use that to build out a main navigation for the front end of your website. On the one hand, Craft doesn’t have something like this built in, but I think that’s actually a good thing. For one, how you edit content and how you structure menus can cause problems when they are tied closely together. In addition to that, there may be times where you need to link to a page in multiple places or in multiple menus on your website.

I like to manage menus via the tools built into Craft CMS, but for a very WordPress-like experience, there's another plugin I can recommend, simply called Navigation. It makes it easy to manage a main menu in addition to secondary menus like one you may find in the footer.

Publishing Entries

Similar to WordPress, Craft CMS gives you a bunch of options around publishing posts. You can schedule posts to go live on a specific date, you can change the slug of the entry (as part of the entry’s URL), and Craft CMS has a whole drafts and revisions workflow that lets you jump back and forth between previous versions of an article and future drafts that aren’t live yet. Craft CMS also automatically saves your work in drafts as you type so you don’t have to worry about a bad internet connection ruining a long session of writing.

You can use the Live Preview feature to see how your content will look before publishing, and you can create preview links that you can share unpublished drafts with collaborators.

Fields publishing

Editing Entries

Probably my favorite thing about Craft CMS is how flexible the field setup is. If you are used to WordPress and doing everything in the old WYSIWYG text editor you can create that and use that for all of your content. The newly updated CKEditor first-party plugin not only works like a WYSIWYG-style text editor, but it comes with a bunch of new ways to pull in field blocks that you can create and repeat throughout your content.

For myself, I prefer to take those field blocks and use Craft CMS’s Matrix field, which essentially lets you add blocks and mix and match them in whatever order makes sense for your content. If you really need it, you could even nest matrix blocks into other matrix blocks, allowing you to handle some very complicated content needs.

For a simple blog I have a text block type (where I write everything in Markdown), an image block, subhead block, and a few specialty blocks for things like notes or fancy emojis. On the front end of my site, I create HTML and CSS components that take all of the fields from each block type and make them all look good no matter how you mix and match them.

Managing User Uploads (AKA Assets)

Image assets

One of the biggest aha! moments that almost always happened for clients when we were discussing using Craft CMS for content management was around the way that images and other files are managed. I would show an asset page that shows you thumbnails of a bunch of images and I would show how you can upload an image while editing an entry or you can select an image from an existing pool of images, uploaded in the Assets section of Craft CMS.

What I would show is how you can have an image selected for a news post and that image would be used on a thumbnail for the news posts on the homepage, on the header of the news posts page, and in social media images shown on Facebook and other networks that showed link previews. This one image would use a focal point (a feature built into Craft CMS) and that would make it so this image would be cropped and still look great in all of these places. The thing that really knocked people’s socks off was that you can replace an image by uploading a new file in its place and that one image would be optimized and would be replaced in all of those places where it was being used.

Gone would be the days of re-uploading the same image multiple times into the Media Library then trying to figure out which one got used where.

I know things are different in WordPress now, but 10 years ago the idea that you didn't need to upload specific thumbnail and social media images into some bottomless Media Library was a huge win for authors. Nowadays Craft CMS’s asset organization is as comfortable as working within the filesystem on my computer (where you can select multiple files and drag them into subfolders to organize things).

SEO

Seo

Folks who use WordPress might be familiar with many SEO plugins, including one called Yoast SEO. While it takes a very different approach from Yoast, there is a very popular SEO plugin for Craft CMS, called SEOmatic. SEOmatic gives you sort of an onion of SEO layers where you can set global SEO settings (like the title of your site and a default SEO description), you can set a template for each entry section, or if you prefer to go page by page there’s a field you can add to your entries to manually write the SEO.

SEOmatic can pull SEO metadata from your content fields and image fields. You can also specify content types for each entry, so you can give search engines even more meta data to work with. For example you can say that an entry is a recipe and you can pass more information into SEOmatic to let Google know that it's more than just an article page and therefore it can add more content to its search results.

Theming, Templates, and the Front End

Here might be one of the biggest pain points for folks switching from WordPress to Craft CMS. Craft CMS purposely doesn’t manage themes. Just like you have flexibility on what fields you set up and how you structure your content, the idea is that you have full flexibility on the front end of your website, too. Which I can see where this seems like an issue, but in some WordPress themes you are tied to a specific content editor because this is what the theme needs in order to properly manage the content.

Instead of forcing you into a content authoring experience you don’t love, Craft CMS leaves it up to you to develop the front end but they give you a bunch of tools to make that easy.

  • Craft CMS uses the Twig templating language by default. If you’re a WordPress developer and you’ve used Timber, you already know how to use it. If you’ve used Shopify for a store in the past, or worked with HubSpot for mareketing, the syntax of Twig is very similar. You are basically doing everything in HTML until you need something from the CMS, and then from there you’re going from <p>My static text</p> to <p>{{ entry.textField }}</p>.
  • Craft CMS has a full routing setup so you can make pages that aren’t content managed. You can use this to create files that aren’t just HTML documents, which makes creating an RSS feed possible with just a Twig template.
  • For those who want to use Craft CMS to drive a separate, static front-end site you could use the built-in GraphQL setup to power a static site builder in whatever framework you want. All of your field data is included in the GraphQL output (based on what you allow in the GraphQL settings section).

At the ad agency, it was rare that we didn’t program the design from scratch and there were a few times where we wound up downloading HTML-based themes from websites like ThemeForest or Tailwind UI and we used them to create the front end. With most of the HTML structure already done, a lot of the work was swapping out headers for the page title and then matching up HTML with content fields in the CMS.

Forms

If you have a contact form on your site, Craft CMS is set up so you can create a form on your site front end through Twig, however, I’ve found third-party form plugins to be a good way to go if you want to create forms and manage their fields through the CMS. They also handle a lot of the work around displaying the form results and doing things like exporting submissions (for things like contests and newsletter signups).

I have the most experience with the Freeform and Formie plugins, but I think I would recommend looking through the Plugin Store to see which form tool fits what you need.

Ecommerce

The folks who create Craft CMS have created a few first-party plugins to help you sell stuff on your websites. WooCommerce users will feel at home with the Craft Commerce plugin. It lets you create products, provides the full checkout experience, and it gives you a ton of flexibility to display your products around your website.

The Shopify plugin can help you connect to an existing Shopify store and it will sync you products over from Shopify into your Craft CMS website so you can add them to the pages on your site.

Just like all the plugins mentioned above, it’s worth looking at the Plugin Store to see if Craft CMS can integrate with an existing system you already have in place.

Technical Requirements

I could go on and on about the feature of Craft CMS, but there are some prerequisite things you might also want to know about from a web development standpoint. Here are a few:

  • Just like WordPress, it’s built on PHP and MySQL (or PostgreSQL if you prefer).
  • Image transforms require some packages to the installed on your hosting server (but in my experience most hosting servers already have them installed).
  • You don’t need to use Docker for local development, but DDEV has a Craft CMS Quickstart that makes local development a fantastic experience.

For a full list of language and hosting requirements, see this page in the Craft CMS docs.

Hosting

There are some minimum hosting requirements for all Craft CMS sites, but depending in the size of your audience or if you use Craft CMS to run a business you may have some specific things you are looking for in a host.

If you are using a WordPress hosting solution already you might need to move to another service or at least to a plan that isn’t specific to WordPress installs.

I would personally avoid using any sort of shared hosting service, like GoDaddy, but using managed hosting is totally fine. Here are a few recommendations I’d consider if you are looking:

  • Craft Cloud – Craft CMS’s first-party hosting solution isn’t cheap, but it offers a bunch of features and you get the support from the team behind Craft CMS.
  • Servd – Servd is managed hosting that is designed for Craft CMS projects.
  • fortrabbit – fortrabbit is a great choice if you have Craft CMS and non-craft CMSs. They typically host sites built on Craft CMS, WordPress, and other PHP-based CMSs.
  • Linode/Akamai VPS – If you are comfortable doing your own DevOps work you can host Craft CMS sites on a cheaper VPS as long as it meets Craft CMS’s hosting requirements.

My blog is hosted on a $5/month Linode (recently purchased by Akamai) and I use Laravel Forge to manage things like SSL Certificates and the server config. I also use an S3-style bucket for image storage through Linode, which is $5/month. My hosting cost for this site is something around $300 per year.

I don’t have a big audience, but I use a caching plugin, called Blitz, to turn my site into a static site. This is what lets me get away with the $5/month hosting server and it also makes sure that if for some reason I had a spike in traffic the server could handle the load (no getting fireballed here).

Plugins, Modules, and PHP

I mentioned some plugins above and just like with a WordPress site you might find yourself using some third-party plugins in order to get your site to act and work like you need it to. I only use a handful of plugins on this site because they do things that—even as a developer—I’m glad to not have to do, but I would generally recommend only using the plugins you actually need.

If you have written plugins for WordPress sites in the past it shouldn’t be too hard to move over to the way Craft CMS works with project-specific modules and plugins. You can do things like create custom form handlers, extend Twig with your own logic and tags, and integrate your website with other APIs and services that you use.

For most folks, with the right mix of plugins you can get away with not touching PHP at all in the development of a Craft CMS site, with maybe the exception of changing configuration files to get the site running on a server.

Fin

If you’re considering jumping from WordPress to Craft CMS I hope this post gives you an idea as to how you can make that experience a little more relatable.

I didn’t touch on many other features and approaches you can use in a more advanced Craft CMS website. If you’re thinking of diving in, here is what I would recommend in order to get you started:

  1. Check out CraftQuest if you like to learn from video courses. It covers some tips from getting started to advanced ways to optimize your project.
  2. Poke around the Craft Documentation to find up-to-date installation instructions, details instructions for setting up fields and entries, and the full list of things you can do in your Twig templates.
  3. Once you’ve gotten your site started, read some of the articles on NYStudio107 to find out how to optimize things and to deep dive into both Craft CMS and general web development topics.
  4. Say “Hi” and get your questions answered in the Craft CMS Discord or on the Craft CMS StackExchange.

Happy Crafting!

💻 ]]>
Guide 5: Don’t Write That Craft CMS Module! https://wbrowar.com/article/code/guide-5-dont-write-that-craft-cms-module Sat, 21 Sep 2024 11:30:00 -0400 Will https://wbrowar.com/article/code/guide-5-dont-write-that-craft-cms-module Sorry for the hyperbole. If you want to write that module, go right ahead.

This may come as a surprise, but as its author, a long time ago I stopped thinking about the Guide plugin as a place to write a traditional CMS Manual. This is still a fine use case for the plugin, but my theory since the Guide 3 days was that unless you are going through a proper training session where you walk them through page by page, clients and content authors are unlikely to take the time to read through every detail about their website in a book or wiki format.

Even when I wrote CMS manuals it was often a jumping off point. One quick paragraph about how to update news articles and where their related posts will show up, then a link off to /admin/entries/news-article to get them right into editing. The best place for those details you were sweating should be right on the News Article edit page itself. So putting documentation above the edit fields and in UI Elements—and now in slideouts—seems like a good way to make sure content authors feel comfortable with the edits they’re making.

Guide slideout
Adding content to slideouts is a new feature in Guide 5.

Getting Plugged In

Putting documentation and useful authoring tips around the edit page is one thing, but sometimes clients ask for something more specific. Maybe you have a client that runs a news site and they have a pool of images that they are able to pull from that are organized in different folders in an asset volume. The client wants to make sure that they have alt text on all their images, across all of their folders in the asset volume. As the site’s developer, how do you approach that?

You could make a page on the front end and password protect it (or just hope that nobody comes across it). This way you could use Twig and use craft.assets.volume('newsArticlePhotos').all() to pull the images that are missing alt text and display them in a list.

But you're probably better off putting this into the CMS somewhere and that gets you into plugin or custom module territory. Say you go the module route and you do the following:

  • Create a new module PHP file.
  • Register it to the app.
  • Create a template route in the module file.
  • Create a Twig page to render at the route.
  • Extend the Craft CP template and follow along with all of the Twig blocks and variables available within that context.
  • Create a way to get CSS into the page, or use {% css %} tags to write CSS in your Twig document.
  • Write your asset queries and create your template from there.

If you’re experienced with PHP and with Yii modules this isn’t all that hard once you’ve got the hang of it, but then there are other concerns to think about:

  • Updating it on production means deploying a change to your website’s code base.
  • Locally you’re working with old data or you’re pulling down assets and the database from time to time (which is common in a lot of development environments).
  • Modules and plugins require maintenance. While uncommon within a major version of Craft CMS, changes in PHP and Craft CMS’s classes mean that you should keep track of updates and be prepared to make tweaks to your module as part of major updates to Craft CMS—including changes to Craft CMS’s underlying frameworks.

There’s a Plugin for That

I’m here today to convince you that using Guide is a better solution than both of those options above. Here are a few reasons why:

  • In Guide you are writing Twig code just like you could on your site’s front end. This includes Twig functions and tags provided by third-party plugins.
  • Guide comes with a CSS editor that uses nice-to-have features, like color pickers and basic autocompletion. There's also a JavaScript editor if you want to add some more advanced interaction to your guides.
  • When you make changes to guide content, CSS, and JavaScript, it’s happening on the server, meaning no build process or deployment is required.
  • If you want, you can use permissions to open up authoring of guides to clients and content authors.
  • There are no PHP classes to learn. If you want to add content to a widget, CP page, UI Element, or a slideout panel you can do that through the Guide Organizer in 2-3 simple steps.
Guide organizer

In that example above—the one about the alt text checker—you could create a guide, use Twig to query for your assets, use a Guidetable component to lay out the data, and then use the Guide Organizer to add this checker to its own CP page, as well as a widget on the Dashboard, and as a slideout on the entry edit pages where you add these images.

The code for this entire thing looks like this:

{# Set the asset volume you would like to check for images in. #}
{% set volume = 'newsArticlePhotos' %}

{# Display a list of invalid images that are missing alt text. #}
{% cache %}
  {% set assets = craft.assets.volume(volume ?? null).hasAlt(false).kind('image').all() %}

  {% if assets|length %}
{% apply markdown('gfm') %}

These images are missing alt text.

{% endapply %}
    {% set rows = [] %}

    {% for asset in assets %}
      {% set cell1 %}{{ craft.guide.component('image', { modal: true, url: asset.url }) }}{% endset %}
      {% set cell2 %}{{ asset.title }}{% endset %}
      {% set cell3 %}{{ asset.filename }}{% endset %}
      {% set row = [cell1, cell2, cell3] %}
      {% set rows = rows|merge([row]) %}
    {% endfor %}

    {{ craft.guide.component('table', {
      headers: ['Preview', 'Title', 'File name'],
      rows,
    }) }}
  {% else %}
{% apply markdown('gfm') %}

All images in this volume have alt values!

{% endapply %}
  {% endif %}
{% endcache %}

When rendered in a slideout, this guide looks like this:

Guide images missing alt text
Clicking on an image will show the image in a modal so you can verify that you are looking at the right image. Clicking on the title of the image will take you to the edit page for the image asset.

If you have a lot of images, you could take this a little further. There’s a Snippet in Guide, titled Contact Sheet – Images, that you can use as a starting point. You can replace the code above with the Snippet’s Twig and CSS code. By default any image shown that has an empty alt text field is highlighted in fuchsia so it stands out.

Guide image contact sheet
Just like the first example, clicking on the image opens the full image in a lightbox and clicking on the title takes you to the edit page for the image asset.

If you wanted to, you could change the asset query to include only assets that had a blank alt field. Then your client could work through the images in the guide until they’ve emptied the list. Sort of like asset inbox zero!

Okay, Sometimes You Still Need a Plugin or Module

Using Guide to build out tools like the example above is a perfect use case for the plugin, but there are some things that Guide can’t—or shouldn’t—do. For example, Guide doesn’t provide a way to hook into PHP like you can in a module. This means making changes to the Craft CMS CP in Guide are based around things you can do with HTML/Twig, CSS, and JavaScript.

Guide also doesn't do anything to change your content or augment the content editing process. So doing things like trying to add form inputs to entry edit pages might cause some errors to occur in the content authoring process. In that case creating a custom module with a custom field in it might be the best way to go.

What you can do is use forms and controller actions in Twig to do some basic content editing within the scope of the controllers built into Craft CMS. If you are feeling really advanced, you could use JavaScript in Guide to call some of Craft CMS’s built-in JavaScript methods, like window.Craft.postActionRequest() and perform similar actions without using a page refresh.

Using Guide to Replace Other Plugins

Part of the reason I wanted to write this post was to help change the perception that Guide is just another wiki tool, but the thing that got me to write it is that I noticed that there’s been a big drop in plugins that are supported across both Craft CMS 4 and 5. Some folks have noticed that there have been plugins that have been marked as abandoned, but when you go to the Craft CMS Plugin Store and switch from Craft CMS version 4 to 5 you can also see that some plugins either have not yet been updated or perhaps the developer behind the plugin has decided to move on from its development (and didn't mark the plugin as abandoned yet).

Craft 4 abandoned plugins

I know from experience that updating and maintaining plugins takes time and on the flip side of that, upgrading a Craft CMS site from 3 to 4, or 4 to 5 can come to a halt when the plugins you use aren’t supported across both versions. Craft makes it easy to see which plugins are updated with their upgrade Utility, and in some cases you need to decide on whether or not you’ll replace a plugin or drop it from the site. My guess is that in this situation most Author Experience-focused plugins are easy to let go as they may be seen as nice-to-haves (Guide included!).

My goal with Guide is to reduce the amount of plugins and custom modules you need to maintain in a Craft CMS project. While I can’t promise anything in terms of long-term support, I can say that Guide PRO is popular enough that I plan on continuing to support it for the foreseeable future. I also feel like the plugin is finally at a point where its found its scope and while it might continue to improve over time, it’s unlikely that I’ll make big changes like the change from Guide 2 to 3. So while when Craft CMS and Guide major versions are updated my goal is that you will only need to make a few minor Twig updates to support new Twig features and CSS updates to match the control panel CSS—if any changes are needed at all.

So next time you’re looking at installing a new plugin or writing a module, try checking out Guide from the Plugin Store. And if you come up with a great use for Guide as a module, consider using Guide's built-in export utility to export out your guide and post it as a Gist or in the Guide Discussion section on GitHub.

📕 ]]>
Guide 5 BTS: Slideouts, Vue v. Web Components, and Plugin Design https://wbrowar.com/article/code/guide-5-bts-slideouts-vue-and-web-components-and-plugin-design Wed, 11 Sep 2024 12:30:00 -0400 Will https://wbrowar.com/article/code/guide-5-bts-slideouts-vue-and-web-components-and-plugin-design The summer of 2021 resulted in two releases in the world of Craft CMS—one big one for me and one big one for everyone else! In July there was the release of Craft CMS 3.7 and about a month later I released version 3 of the Craft CMS plugin, Guide. Regarding the Guide release, I had been planning on its release for months, so I put out what I felt was a good GA release, but I had plenty of plans for improvements and features that I planned on working on soon after.

What was released in Craft CMS 3.7 was one of the best and worst features for my newly minted plugin.

Slidouts

Along with a bunch of other author-experience features, Craft CMS 3.7 introduced the slidout UI for making it easier for authors to make changes to related entries, image assets, and other related elements. You could double-click just about any entry element in the UI and it would open up a full editor in the slideout panel—letting you make changes to the entry, save them, and then when the slidout panel closed you’d be right back to the original entry you had begun with.

It was a really impressive feature and since one of the things I really wanted to promote in Guide 3 was the idea of putting important authoring information right on entry edit pages, having some sort of button in the sidebar that could open up related guides (content entries in the Guide plugin) in a slideout was an absolute no-brainer. I added slideouts to my backlog and planned on working on it for a 3.1 release.

It had been a little bit before I could make the time to work on the 3.1 branch and I remember having a little trouble figuring out how to work with the Craft.Slidout API at first. I found Leevi Graham’s excellent walkthrough on working with the Craft.CpScreenSlidout API and that helped me get a working prototype going. At least I could hack some of the way Guide moved content around the Craft CMS CP (Control Panel) to get guide content to appear on a Craft.Slideout panel. From there, that's where a bunch of problems became obvious around one of the basis on Guide’s architecture.

Relying Too Much on a Vue 3 Feature

The approach for Guide 3 came from what I was seeing in my own personal use of the Guide 2 plugin. The company I had worked for was still creating CMS guides (a lot of time we had a template and we brought it from site to site), but we were also doing lots of custom Craft CMS module work. Clients wanted a page in the CMS to keep track of things like image alt text or to have a widget on the Dashboard page that showed very specific data. Guide 2 let you do some of this and by Guide 3 I had re-engineered it so you could take one guide and spread it around several of the predefined locations in the CP, as well as on any page in the CP—and even on elements outside of the main #content area.

We were making custom alert banners and dropping buttons in the sidebar on element list pages. We'd do things that would make Brandon Kelly and the UI folks at Pixel & Tonic cry (probably), but these customizations were helpful for our clients and how they wanted to use their CMS.

All of this was driven by how Guide would let you put in any CP page URI (like seomatic/settings) and then any CSS selector you could find in the CMS’s HTML markup, and Guide would figure out how to get your page content to show up there.

Vue Teleport-Driven Development

This ability to move guides into various elements drove the main way that all guides were displayed on the page. While you may have seen a guide appear above the fields on an entry edit page, what was really happening is that Guide would render all of the guides at the bottom of the page from Twig, then use Vue 3’s Teleport component to take the guide content and throw it up to the correct element above. If you had some guides appear above the entry edit fields and then some guide content appear in another element, Guide would still render all of the guides at the bottom of the page, but I had mapped out all of the guides and where they were going and then I had Vue dispatch them upon mounting Vue.

I was pretty happy with this setup but there were a few thorns on that rose. For one, Vue Teleport required an element to be rendered at the point that Vue was mounted, or it would sort of say "hey, I want to teleport this thing to this element but the element doesn't exist yet... I guess we're done here...". At that point Vue would give up and your guide content wouldn't show up anywhere. I worked around this for some situations, but this was one of the bigger issues with trying to get guides into slideouts.

Making Messes in My Code Base

When I started the slideout prototype I had already started refactoring some things in my code base to migrate my Vue components from TypeScript with Vue’s Options API to using TypeScript and Vue’s Composition API. Anybody who has worked with both knows that the Options API with TypeScript wasn't nearly as straightforward as the Composition API approach. At one point I realized I had made a mess of things and that derailed things for a while. Some of it was my TypeScript implementation and some of it was me getting used to how to convert my existing code to Vue 3’s Composition API.

Also, rendering all guide content in Vue had another downside that I couldn't come up with an answer for. Vue likes it when Vue knows about everything it’s rendering. If done with coding guard rails in place you can get plain JavaScript to work with Vue, but it was really limiting with what you could do with JavaScript to customize guide content without completely breaking things. Craft has a couple of handy Twig tags, such as the {% js %} tag, that allow you to write custom JavaScript and while Guide would let you use them in your guide content (anything Craft CMS’s Twig could do you could do in Guide), but if you tried doing something like using vanilla JavaScript to sort rows in a table of data you'd be in trouble.

There were other problems with my implementation and I would create a branch, get stuck, and would scrap it for another attempt. I had thought about how to move forward within my code base for many months and finally came to the conclusion that some big rewrites would be needed.

Dropping Frameworks to Get (a Little Bit) Closer to HTML and Twig

Okay, fast forward to early 2024 when Craft 5 was entering beta. I had taken a break from the Guide problem and let that one sit on the back burner so I could show some love to my other two plugins, Admin Bar and Little Layout. Admin Bar had some of its own issues and the solution I went with for working with it across Twig and JavaScript SPA-like sites was to break it out into a standalone Web Component.

Little Layout also got the Web Component treatment as I converted the field and its settings from Vue 3 to using Web Components that wrapped around HTML elements that were rendered by Twig. The difference between waiting for DOM elements to be ready so you could vue.mount() an instance onto it versus registering a Web Component and having it automatically take care of kicking off when the custom element gets added onto the page was a big eye opener for me.

This is the thing that made Guide click for me and I got to thinking about how a refactor from Vue to Lit (my current Web Component framework of choice) might look.

Introducing Guide 5.0 (Beta)

This introductory post goes over the details around what’s new in Guide 5 at a high level.

What is new is that guides can now be shown in slideouts 🎉, and there are no hacks used to get them there. There's also a new guides list page that I hope will fix some of the confusion that the old Guide Organizer page created for some folks. I’ve re-written the entire UI side of things and even moved the documentation out of its own Nuxt-based docs website and I've put it right on the pages in Guide (you can also read them as markdown pages on GitHub if you prefer to read the docs that way).

More details around what has changed can be found on the plugin CHANGELOG on GitHub.

It Was a Good Idea at the Time

Guide 5 drops Tailwind and Vue 3 as dependencies. I don’t have a problem with either technology, but they no longer benefitted the project in the way I had hoped when I introduced them.

With Tailwind I had used it for a lot of the Vue component templates and I had offered a very small subset of Tailwind classes to guide authors as "Guide utility classes". It worked fine for my Vue work, but after thinking about it for a while I thought that it's not fair to offer Tailwind but to make customers do two things: they A. have to look up whether a class is available before using it, and B. they had to prefix everything with g- (so display: grid would be g-grid). I looked into ways of running guide content through some sort of Tailwind compiler, but I couldn’t find a good way to do that without a Node server or adding some pretty big dependencies to get things working in PHP.

Instead of using Tailwind I made it easier to write CSS for Guides by adding a code editor field that has some basic code completion and CSS syntax highlighting. For authors who just want to use Markdown I added a few more styles to cover common Markdown elements. For folks who do want to use CSS most of the default styles now come from Craft CMS itself and many of my overrides include CSS Custom Properties so it should be easy to override styles and spacing.

I think with all of the new features CSS has to offer, it is much easier to work with CSS directly. Features like :has() can be a huge help and I added a container to guide displays so you can use @container queries to make adjustments as guides appear in widgets and on full pages, alike.

Using the Platform

Regarding Vue, I was really invested in getting Vue Teleport to work and I thought at one point that if I needed to use Vue for some of it I may as well write everything in Vue. The problem with this is that if I wanted to do something with a native Craft CMS element, like form inputs, I either had to play a game of JavaScript tag between Vue and the native HTML components or I had to create my own components. I wound up creating Vue components that looked like the native text input and select components and they worked very similar by the time I was done.

The big problem, though, is that the version I created got out of date very quickly. To their credit, Pixel & Tonic have put a bunch of effort into making accessibility improvements and iterations on the design of the elements in the CP and my component clones weren’t getting any of it. By removing Vue’s rendering I wound up going back to using Craft CMS form elements (via their Twig macro) and now when you submit the form for something like editing a guide it's back to using a good old fashioned HTML form. Any reactive bits come in the form of adding event listeners and then maybe doing something within a Web Component and using slots to swap out content or visually hiding irrelevant input fields.

Because of the way Web Components are registered to the page and then initialized when they are added to the DOM, I got around the way I was using Vue Teleport to move guides around. In some cases guides are just rendered in place, but for guides that are added to places outside of the presets I am now using native JavaScript to take a handful of guide elements and then distributing them where they need to go. There's a Web Component that is just a wrapper around individual guide content that handles things like adding a menu when multiple guides are displayed in one spot. Otherwise guides are now just HTML rendered from Twig.

Guide component

Some of the Guide Twig components use Web Components, too, but those are added by guide authors using Twig, and the Web Components just enhance their functionality. For example, the image component will just display an optimized version of the image if you just pass in an asset parameter. Because it’s transformed to a smaller size it can be hard to see the details in something like a screenshot, so now there’s an parameter in the component function to make it so the image can be clicked on and the original image can be displayed in a modal.

{# Adds an image from an uploaded asset and does an image transform to reduce the size and optimize it. #}
{{ craft.guide.component('image', {
    asset: craft.assets.filename('guide-widget.png').volume('guide').one()
}) }}

{# Adding the `modal` param wraps the output of this image in a web component that handles opening up the full size image URL in a modal when you click on it. #}
{{ craft.guide.component('image', {
    asset: craft.assets.filename('guide-widget.png').volume('guide').one(),
    modal: true
}) }}

The output is a <picture> tag with srcset used to handle multiple resolutions. If you add the modal option a Web Component wraps the <picture> element to add the modal functionality but it doesn't do anything to the image inside of it.

I really like this kind of thing. In working with Web Components for a third time now, I am starting to understand where Shadow DOM and Light DOM (non-shadow) have their own benefits. I also like the idea that I don’t have to worry about big framework shifts because Lit seems to be closely in tune with the web standards behind Web Components. Maybe someday these web components will simply follow web standards and I can remove Lit, but for now it includes a lot of great tools that makes working with Web Components very pleasant.

Customer Research-Driven Development

At one point all of the trouble with Guide’s source, and what looked like a big dip in sales, got me to think about how I wanted to proceed with the plugin. The trickiest thing is I have GitHub Issues and Discussions as my main point of getting feedback and while that helps show the holes, I wanted to know what people using the plugin actually thought and to know how they used it. In early 2024 I put out a survey in the form of a Google Form and while I got a few responses from that I also got some suggestions by folks via the Craft CMS Discord.

What I had learned was that Guide 3 was confusing. I had put together what I thought were clever redirects when you didn’t add certain settings, but other than in the docs somewhere, I hadn’t explained why you were getting redirected. From a UI design standpoint I tried to cram a lot of things into the Organizer page and doing things like trying to restrict the height and forcing page scrolling within page scrolling made the UI even harder to work with.

With Guide 5 I hope I have corrected a lot of that. Splitting up the Organizer so you now create and manage guides on one page, then having a separate page used to map out guides will hopefully help make things make more sense. I got rid of all of those redirects and even got rid of some settings being required in the first place.

Guide organizer
Guide list
Guide cms guide
Guide settings

Doing a little dogfooding, I also added my own Guide buttons around the pages in Guide where I thought documentation could be helpful. These buttons show the documentation in slideouts—of course 🎉

Guide 5 is Now Live

Guide 5 is now available and can be installed via composer or from the Craft CMS Plugin Store. The features above are available in the paid PRO edition, and a LITE edition is available for folks who just need a simple CMS Guide.

If you run into any bugs please add an issue to the Guide GitHub issue page. If you have questions or feature requests, please head over to the Guide GitHub Discussions page or drop my a line.

📓 ]]>
Guide 5 https://wbrowar.com/article/code/guide-5 Tue, 10 Sep 2024 22:30:00 -0400 Will https://wbrowar.com/article/code/guide-5 Guide 5 is here with support for its number-one requested feature: putting guides in Craft CMS slideouts! This release also brings a lot of quality-of-life features that have been suggested by the Craft CMS community. It has been re-structured to make it easier to manage guides and then distribute them around the Craft CMS CP (Control Panel).

Slideouts 🎉

Guide slideout
Guide ui element

Now when you are on an element edit page—for entries, assets, globals, categories, and users—you’ll see a new Guide button next to the Live Preview button. When clicked, this button will open up a slideout with all of the guides relevant for that element. This could contain how-tos, important authoring workflow steps, stats, a changelog, and other content-aware information—whatever you can think of writing in your guides. When you are done you can close the slideout panel and get right back to editing the element on the page.

Guide also comes with a new guide-button Twig component so you can put your own slideout button in any of your guides.

Markdown Rendering

Guide editor markdown

In previous versions of Guide you could wrap text in a markdown Twig filter to render a few lines of text in Markdown at a time. In Guide 5 there is an option on every guide that lets you render the whole guide in Markdown. This can be great for pages of documentation and for content written by authors who prefer not to mess with HTML markup and Twig tags.

If you did want to drop in some HTML, Markdown leaves HTML tags as-is when rendering. Twig tags are also welcomed and they’ll be rendered on the page before Markdown processes the content—letting you do things like use {% for %} loops to fill out Markdown lists.

New Organization

Guide cms guide

The Guide home page is now called CMS Guide—for those who want all of their guides to appear in one place.

Guide list

The new Guides page lets you manage the guides in your Craft CMS project. From here you can edit and delete guides, as well as preview guides to see what they'll look like in a slideout. Clicking on a guide title takes you into a standalone page in the CP with your guide content on it.

Guide organizer

The re-designed Guide Organizer lets you enabled guides for Widgets, UI Elements, and lets you distribute guides on element edit pages or a specific URI (you could create a guide with some documentation around how you had set up your favorite SEO plugin and put it right on the plugin’s settings page).

Guide Components

Guide editor components

Guide’s Twig components have all been modernized with a few additions and quality-of-life improvements:

  • Images can now be zoomed in by clicking on the image and showing the full-size image in a lightbox.
  • A new Details component lets you simplify your guides by hiding detailed instructions and documentation until your reader needs to see them.
  • The Table component is now a Twig component that lets you programmatically fill in table cells. Combining it with Twig’s {% set %} block lets you fill in tables with HTML so you can add image previews, links, and styles to your data.

CSS and JavaScript Customization

Guide widget

New fields can appear—based on your settings—in the Guide Editor that let you add custom CSS and JavaScript to your guides.

With all of the powerful tools CSS has to offer you can use things like :has() and Container Queries to make your guides fit—wherever they are added. Guide makes use of the CSS Custom Properties built into the Craft CMS and provides several CSS Custom Properties of its own—allowing you more customization in your guides.

The JavaScript field adds a callback onto the page that is fired whenever the guide is displayed. Using the Craft Code Editor field by nystudio107, you can author JavaScript in a code editor that has JavaScript-specific code highlighting, autocomplete, and multi-cursor editing.

Settling All Family Business

Guide component

This release makes Guide a better plugin citizen. All static text can be translated into other languages, the source code has more-detailed documentation, and many issues and features requests have been incorporated.

Guide 5 is Alive

Guide 5 is now available and can be installed via composer or from the Craft CMS Plugin Store. The features above are available in the paid PRO edition, and a LITE edition is available for folks who just need a simple CMS Guide.

📘 ]]>
Testing Craft CMS Fields with Guide UI Elements https://wbrowar.com/article/code/testing-craft-cms-fields-with-guide-ui-elements Sun, 31 Mar 2024 16:53:00 -0400 Will https://wbrowar.com/article/code/testing-craft-cms-fields-with-guide-ui-elements I just made some updates to the field plugin, Little Layout, to give it support for Craft 5. Of the plugins I release on the Craft Plugin Store, this one I test out the most because it affects author content, and bugs on a custom field can have some very unwanted results.

I like to dogfood my plugins for yet another touch point to make sure things are working as expected, so last year I put together a way to use Guide to test out all of the different ways you can output data from a Little Layout field into a Twig document. I used this setup when refactoring the front-end portion of the field, and when experimenting with new features.

With other developers working on updating their plugins for Craft 5, I thought this would be a good time to share, in case anybody else finds this setup useful.

Entry field layout field and guide

Setting Thing Up

The first thing you'll need to get this working in your development environment is install Guide from the Plugin Store.

Next you'll need to change the edition of Guide from LITE to PRO. While I would really appreciate it if you'd buy a PRO license, this setup doesn't require you do pay anything, and I wouldn’t ask you to pay for it for just this purpose.

You can set Guide to the PRO edition, by hitting the Try button on the Guide Plugin Store page.

Try guide pro
Once you've installed and activate the trail for Guide PRO, the button will change its label.

With Guide installed, visit the Guide Settings page to confirm that a Template Path and Asset Volume are set up. While you might not need these for your layout tests, they are required for Guides to work with images and Twig templates.

Field Settings

To test your field, you can create a new field in Craft’s Fields settings page. At this point you can set it up with whatever options you want to use as a default, or you could even create multiple fields from your field type. I will typically start with the default settings and then go through the process of updating each setting I want to test out, to also make sure I get the expected results for that field setting.

So I have created a Little Layout field, called Layout, and I’ve adjusted a few settings that I want to start from in my test layout.

Field settings

Entry Types

Next up you’ll need to create an element or use an existing one where you don't mind editing the field layout. You can do this by adding an Element Type and a new Entry Section.

In this case, I created one called, Homepage.

Set up entry type

Using the Field Layout field, I added my Layout field:

Field layout

Next, click on the UI Elements tab and drag a Guide UI Element over to your field layout. I set it to span 50% width for both the field and the UI Element so I can see them both side-by-side.

Ui element layout

When you are ready, click Save to confirm your field layout.

At this point you could go to that new entry type and create a new entry to see what your field layout looks like.

Entry field layout no guide
If you have any guides created in Guide, you'll see a select field that lets you pick a guide to use here. If not you’ll see a message indicating that there are no guides created yet.

Creating Your Field Test Guide

If you are new to Guide, the typical process for creating a guide goes something like this:

  1. Click on the Guide link in the Craft CP sidebar menu.
  2. If you aren’t already there, click on the Organizer submenu link.
  3. Click on the + New Guide button to be taken to the Guide Editor.
  4. Write your guide content and hit Save—taking you back to the Organizer.
  5. Typically you can then use the Organizer to distribute this guide across the different CP pages, but we won’t need to do that today.

So making our way through that list, let’s start with the Guide Editor in step 3. Create the new guide, give it a Title and Slug and leave the Content Source as Code Editor for now.

This guide will go on an entry edit page, so in the code editor field we want to get data from the entry that we are currently editing. Guide lets you do this by using an element Twig variable that works just like using entry in a front-end Twig template.

So to get the current element of the entry edit page, you can add something like this to your guide code, then hit Save:

{{ element.layout }}
Guide editor blank

If we jump back to our entry edit page, we should now see the new guide appear in the Guide select field. Select that, hit the Save button, and then reload the page.

We should see our Guide UI Element populated with the current value of the field we are testing—it will probably show the field’s default value at this point.

Entry field layout error

In my case, an error is thrown. Guide captures errors so you can debug the issue back in your guide without throwing an error on the entire page.

The reason for this error is that the API for the Little Layout field doesn't return anything from the default field handle. I can add any of these properties to see what the value of my field would be.

When I set my guide content to display this:

Selected column: {{ element.layout.selectedColumns[0] }}

... I can see the selected Little Layout column on my entry edit page.

Entry field layout column value

Fin

At this point you can go to town and update your guide to see all of the things your custom field can do. This is a good way to test things like the empty state of your field and what your field looks like when run through Twig filters. It’s a good way to check that your field doesn’t break when you develop new features for it or run content migrations (although unit tests are great for this, too).

While my main focus is on the Little Layout field, I have written custom fields for clients in custom modules for their site, so this sort of thing has been handy for me in that situation, too. If you wind up wanting to test a field across several different entry types or with different entry content, you could consider breaking this out into its own Guide page, or use the one guide in both contexts.

Hopefully this gives you some ideas and you find this helpful. If you do use it in this way, I’d love to hear more about it.


Epilogue: Using a Twig Template Across Craft Testing Environments

At one point, I wound up having to re-create my local plugin testing environment and that meant setting all of this up again. While Guide comes with a utility that lets you export guides out and then import them into another site that uses Guide, I wound up making a GitHub gist to store the template code I use to test Little Layout in Guide UI Elements.

In this gist, I made it easy to test different Little Layout fields by setting the field handle at the top:

{# Set the hande of the little layout field you want to test. #}
{% set fieldHandle = 'layout' %}

{# Set a variable to point to the field when used on the current element #}
{% set littleLayoutField = element[fieldHandle] %}

{# Use the new variable to test out the field’s API #}
{% set totalColumns = littleLayoutField.totalColumns %}
{% set totalRows = littleLayoutField.totalRows %}
{# ... more testing code ... #}

Setting up Guide to use this file follows this process:

  1. Create a new Twig template and save it in the same directory you selected on the Guide Settings page (this is usually set to templates/_guide/ by default).
  2. Open up your field testing guide from the Organizer.
  3. Change the Content Source to Page Template. This will display the Template field.
  4. Select your new Twig file from the list in the Template field.
Guide editor template

At this point, Guide will now render the contents of that file, allowing you to make changes to the file and then see them appear in your UI Element. This can be helpful in keeping both developing the field and testing the field in your code editor.

🍱📘 ]]>
CSS for Me, in 2023 https://wbrowar.com/article/code/css-for-me-in-2023 Thu, 04 Jan 2024 18:00:00 -0500 Will https://wbrowar.com/article/code/css-for-me-in-2023 Jamstacked

In late 2019 the agency I worked for went all in on headless and Jamstack sites. At that time we explored ways to pair Craft CMS with front-end frameworks, like Gridsome, Gatsby, and Nuxt. I used my personal website, wbrowar.com, as my guinea pig, giving me a playground to connect all the puzzle pieces involved in creating a headless and static Jamstack website, such as:

  • Organizing the repos and CI/CD for the back-end and front-end source code.
  • Rendering a static front-end that can dynamically pull from the GraphQL API when using Craft’s Live Preview.
  • Working out incremental builds to reduce time taken during static generation.

I also used my site to explore using Tailwind CSS as the primary method for working with CSS. This worked great when paired with the Vue-based components in the Nuxt-based front-end I decided to go with.

2023 CSS Challenge

2023 has been an epic year for CSS features and web components seem to be all over the front-end zeitgeist right now. I keep reading about all of the new tools us front-end folks have at our disposal and I decided to refactor my personal site to give me a place to try them out. This got me thinking about how I'll do this with the Tailwind-based setup.

First off, Tailwind is not an either use it or don’t use it technology. What I like about Tailwind is that you could decide to style every part of a fully custom site with it, or simply use Tailwind for a few utility classes here and there—mixed in with the rest of your CSS. Yes, there is the part that includes the build step, but with my experience—and with build tools like Vite around—I'm not adverse to post processing my CSS.

But with this refactor, I decided that I wanted to go back to the days before I started using Tailwind, and even earlier in the days before I used SCSS for most of my projects. I wanted to see what I could come up with, architecting a front-end using modern CSS practices. While I'm not shooting for complete Vanilla CSS, I wanted to get back to thinking through a system instead of working within a framework.

Markup Templating

In my day job I use TypeScript, Vue.js, and REST/GraphQL APIs all day, so while that makes maintaining a Jamstack site within my wheelhouse, I no longer felt like I was getting the same level of new learning out of my Jamstack setup as I did back in 2020.

The back end of the site is built in Craft CMS and one of the things I like about Craft is that it’s focused on content management and it doesn't care about what you do on the front end. You can use Craft’s built-in routing and Twig templating setup, you can use the built-in GraphQL setup to feed a headless front end, or you can use something like the Element API plugin to expose your content via JSON API to a headless front end. As the developer, it’s your choice on whether or not you use a front-end framework, and if so, which framework you prefer (with a few caveats around things like working with Live Preview).

Nuxt is really great for static sites, and for a time I also looked into using Astro, but in this new refactor I wanted to get back to defining the markup I need, instead of working within a component-based framework. Part of this goes back to wanting to write CSS for my content and another side of it came from looking at what I need my site to do and what kind of content I was creating. I decided to go back to a non-headless site, built in Twig, with the use of the Craft plugin, Blitz, to handle static caching and cache invalidation.

JavaScript Framework is _undefined_

With my HTML markup approach decided, next up I wanted to think about how I’ll work with JavaScript for the places where it’s needed. Looking at my current needs, I really only used JavaScript for highlighting code blocks and some minor functionality around image carousels (which is an enhancement on a CSS scroll-snap-based carousel). Programming these couple of bits in vanilla JS wouldn’t be too much work, but rendering the whole page in something like Vue.js is probably overkill.

I like the general setup of Vue components, and in my recent Craft CMS plugin work I got some experience working in Lit to build out web components. So I decided to use web components where it made sense, then go to vanilla JS for more global interactions. SPOILER: So far I haven’t had the need for any global JavaScript and I wound up using just 4 web components for all of the JS on this site:

  • bokeh-box: a component used to generate bokeh-style bubbles for the background of the site.
  • bokeh-controls: a small set of buttons used to define the color scheme of the site and the color of the bokeh bubbles in the bokeh-box component.
  • code-highlight: a wrapper around a <pre><code> block that enables highlighting for a specific language, using highlight.js.
  • element-carousel: takes all of the child elements within the component and turns them into a CSS flex and scroll-snap carousel. The main JavaScript portion is in observing the element currently scrolled in the middle of the page, then indicating that with some dots at the bottom.

My 2023 CSS Solution

I took some time to think about how I’d structure my CSS. This post by Dave Rupert was helpful in introducing me to other approaches and I wound up with a mixture of CUBE principles, organization like Vue’s Single File Component (SFC), and some inspiration from Tailwind.

The sections below describe the problems and areas of my current CSS solution.

Keeping the CSS With the Twig Pieces

One of the features of SCSS that I’ve utilized a lot in the past was that you could break out CSS into a file structure that mirrored the structure of your templates, instead of creating one long CSS file, broken up with comments and other wayfinding tricks. Vue’s SFCs took this even further in that you could write the CSS relevant for a component directly within the component itself.

The value in either approach is in the time saving of being able to open a template file and then work right within its corresponding CSS that is limited to just the scope that the template file is responsible for.

Since I'm working in Twig files, I don't have an easy way to store my CSS in the Twig template files then extract them into a cacheable .css files that can be served on my front-end. I know there is probably a solution here around using PHP or using something like Node to extract the CSS from the files in the filesystem, but the benefit of writing CSS into a Twig file isn’t worth adding a complicated setup into the mix.

I looked for ways to organize CSS natively, but the closest I could get was to use CSS @import, however, the action of importing happens in the browser, so that would A. lead to some performance woes, and B. wouldn’t work because my Twig templates are not stored in a directory that is exposed to my server’s web root.

There are lots of ways to concatenate and minify CSS with build tools, so at this point I decided that I'd use Vite to handle those tasks.

Vite comes with PostCSS, along with the postcss-import package, that lets you inline files imported via CSS @import. Vite also handles minification during the build step, so between the stock setup I would be all set. The only other package I might consider adding is Autoprefixer, but before doing that, I found that Vite 5 comes with Lightning CSS (included as an experimental feature).

With a solution for bundling CSS in place, I looked at my Twig setup and came up with a structure for my CSS files. My templates directory looked something like this:

templates
├── components
│   ├── article-body.twig
│   └── article-thumb.twig
├── includes
│   └── admin-bar.twig
├── layouts
│   └── global-layout.twig
├── macros
│   └── icons.twig
└── pages
    ├── about.twig
    ├── article.twig
    └── home.twig

My Twig structure could be defined like this:

  • components: self-contained, reusable files included via the Twig include or embed tags.
  • includes: one-off includes that don’t belong in any other directory.
  • layouts: page layouts that are extended by templates in the pages directory.
  • macros: Twig macros that are imported and used throughout the project.
  • pages: templates for a specific page or entry section.

Each of these files could require their own CSS, so my idea was to add a CSS file alongside the Twig template as needed. Here's what I wound up with in this first pass:

templates
├── components
│   ├── article-body.css
│   ├── article-body.twig
│   ├── article-thumb.css
│   └── article-thumb.twig
├── includes
│   └── admin-bar.twig
├── layouts
│   ├── global-layout.css
│   └── global-layout.twig
├── macros
│   └── icons.twig
└── pages
    ├── about.css
    ├── about.twig
    ├── article.css
    ├── article.twig
    ├── home.css
    └── home.twig

The name of each CSS file matches the name of its corresponding Twig file.

In the root directory where all of my other front-end source files are stored I have a CSS file that imports all of these files so that these styles are bundled alongside all of the other global styles for the site. With the exception of some web component styles, all of my styles for the site live in one of these files:

front-end
└── src
    ├── css
    │   └── reset.css
    └── wbrowar.css
templates
├── components
│   ├── article-body.css
│   ├── article-body.twig
│   ├── article-thumb.css
│   └── article-thumb.twig
├── includes
│   └── admin-bar.twig
├── layouts
│   ├── global-layout.css
│   └── global-layout.twig
├── macros
│   └── icons.twig
└── pages
    ├── about.css
    ├── about.twig
    ├── article.css
    ├── article.twig
    ├── home.css
    └── home.twig

When bundled, all of these files are imported and minimized, then served on the front end as wbrowar.css (along with a unique content hash for cache busting upon changes).

CUBEish CSS, Nesting, and the Freedom of Cascade Layers

Early into my career in writing CSS I had latched on to the pattern of OOCSS. As I started working with teams of developers we gravitated towards BEM and eventually stayed there for a while. Our rule for writing CSS was if you were going to style something you’d use a class, and with BEM you’d be descriptive and you’d name everything in a way that was unique and described within the context the element is used.

So we'd wind up with markup that looked a lot like this:

In this case .main_menu was sort of a class that gave you context for which <nav> element this was (amongst maybe a .footer_menu or a .secondary_menu). Then everything else gets prefixed with that context along with what the element is (in regards to how it will be used or styled), then any modifiers that describe something unique about the element.

In our minds, this was descriptive and it solved problems like clashing styles and being able to use ⌘+F to hunt for this style amongst all of our stylesheets in our _scss directory. Over time this also led to some other issues that made us less efficient:

  • Our BEM setup was based around very unique class names, but it also meant that we either had a lot of duplicate CSS code to cover common patterns in unique contexts, or it meant a rat king of selectors and modifiers everywhere the minute you moved something or tried to re-use CSS.
  • Naming took a while to get right. Some things are easy when there are only one of them on the page, but when you had common things, like headers, you have to do more to define them into sections like, .content_header, .sidebar_header, .news_card_header. Sometimes designers would make a decision after seeing something in the browser, so maybe the news cards got moved into the sidebar... Now you are either renaming a bunch of classes, or you have some no-longer-obvious element names.
  • As much as some folks may find a bunch of Tailwind classes hard to skim and quickly parse, reading BEM code—sometimes compiled via includes and partials—can also look like a wall of underscores and dashes amongst a lot of repetitive names. Indenting source code helps, but some extra mental processing time is required.
  • Gzipping CSS files is great when working with BEM, but when you don't have the ability to set that up where your static assets are served, you wind up with extra file bloat in both your CSS and in your template files.

Instead of following my old BEM rules, I’ve combined Cascade Layers, nesting, child selectors, and parts of CUBE CSS to come up with my new CSS setup.

To start, I’ve created a set of layers and I've imported all of my CSS files into their designated layers:

@layer reset, admin-bar, globals, components, theme, layout, page;

/*
 * reset: normalize browser defaults, remove margins, set box-sizing: border-box on everything 
 * admin-bar: layers the styles that come with Admin Bar here so they can be overwritten later
 * globals: set project-specific HTML defaults and global classes
 * components: include styles specific to re-usable Twig components
 * theme: define custom properties for colors, fonts, animation, and spacing
 * layout: styles specific to the global page layout
 * page: page-specific styles
 */
 
@import './css/reset.css' layer(reset);

@import '../../templates/components/article-body.css' layer(components);
@import '../../templates/components/article-thumb.css' layer(components);

@import '../../templates/layouts/global-layout.css' layer(layout);

@import '../../templates/pages/about.css' layer(page);
@import '../../templates/pages/article.css' layer(page);
@import '../../templates/pages/home.css' layer(page);

Adding these layers make it so I don’t need to worry as much about clashing styles or using hyper-specific class naming. This lets me go back to using more global classes, like a .button class, knowing that I can overwrite it on the components or page layers later.

There is still the problem of selecting elements within the same layer, and that's where CUBE and child selectors come in. The things I took most from the CUBE were the composition (C) and the block (B) principles, along with the visual benefit of grouping with brackets (as seen in the CUBE documentation).

Take a look at the markup for my About page for example:

{# Intro text ... #}
{# Links to external URLs ... #}

The first thing you might notice are the bracket groupings here. When you look at the source code it makes it really easy to see where the different sections of code are. I set up sort of a framework for thinking about how these blocks work together with the elements on the page. Here’s a general outline for how I’ve organized this:

  • page is a root level starting point and it is the singular name of the folder that this Twig file lives in.
  • Where page describes the directory this file is in, about describes the file name. All of the styles in the about.css file are nested within .page.about.
  • Any child bracket, like [ content ], gets selected using a child selector, .page.about > .content. Then later, .page.about > .content > .grid. This is done via native CSS nesting.
  • .content-container and .prose are global classes that are defined on the globals cascade layer. Because the CSS in about.css is on the page layer, it’s a lot easier to overwrite properties defined on the globals layer.
  • The grid element contains a few <a> elements, so instead of naming those elements with a class like we would have in BEM, this setup makes me confident that I can set properties using this selector, .page.about > .content > .grid > a.
  • I haven’t done anything too complicated yet, but my goal is to limit the amount of child brackets to avoid nesting too much. If needed, I could break things out in to components, but so far that hasn’t been necessary.

Here’s what the full CSS for the About page looks like:

.page.about {
  container-name: page-article;
  container-type: inline-size;

  & > .content {
    margin: 0 auto;
    padding-top: 100px;
    padding-bottom: 100px;
    max-width: 1024px;

    & h1 {
      font-family: var(--font-futura);
      font-size: 2rem;
      font-weight: var(--font-normal);
      color: var(--color-orange);
    }

    & > .grid {
      --svg-width: 1.5em;
      display: grid;
      grid-template-columns: var(--grid-template-columns, 1fr);
      align-items: center;
      gap: var(--xl);
      margin-top: 50px;
      font-size: 2em;

      & a {
        display: grid;
        grid-template-columns: max-content 1fr;
        align-items: center;
        gap: var(--md);
        text-decoration: none;
        color: var(--color-blue);
        transition: color var(--duration) ease-out;

        &:hover {
          color: var(--color-orange);
        }
      }

      @container page-article (width > 600px) {
        --grid-template-columns: 1fr 1fr;
      }
      @container page-article (width > 900px) {
        --grid-template-columns: 1fr 1fr 1fr 1fr;
      }
    }
  }
}

Global Styles and Custom Properties

My main CSS file, wbrowar.css, imports all of the CSS from my templates directories, and it also includes the CSS for the globals and themes layers. My globals layer contains commonly used classes, like an .article-grid class that is used to display article thumbnails across several pages. It also defines basic element styles, like the styles for <hr> elements.

Because I use Markdown for a lot of the content in my site, the .prose class handles styling some commonly used HTML elements rendered by Markdown:

.prose {
  & > * + *:not(hr) {
    margin-block-start: var(--md-rem);
  }

  & :is(blockquote, dd, dl, h1, h2, h3, h4, h5, h6, hr, p, li, pre) {
    max-width: var(--prose-max-width, 70ch);
    font-size: var(--prose-font-size, 1.25rem);
    line-height: var(--prose-line-height, 2);
    text-wrap: pretty;
    color: var(--prose-color, var(--color-black));

    & a {
      color: var(--color-orange);

      &:hover {
        color: var(--color-blue);
      }
    }
    & code {
      padding: 0.4em;
      background-color: color-mix(in srgb, var(--color-blue), transparent 50%);
      border-radius: 0.3em;
      font-family: monospace;
      font-size: 0.9em;
      user-select: all;
      color: var(--prose-code-color, var(--color-blue-800));

      @media (prefers-color-scheme: dark) {
        & {
          color: var(--prose-code-color, var(--color-blue-300));
        }
      }
    }
  }
  & :is(ol, ul) {
    & li {
      & + li {
      	margin-block-start: var(--sm-rem);
      }
      
      &::marker {
        color: var(--color-red);
      }
    }
  }

  @media (prefers-color-scheme: dark) {
    & {
      --prose-color: var(--color-gray-200);
    }
  }
}

The great things about the layer setup is that if I wanted to overwrite a <ul> on a specific page, I won’t need to specify .prose in the selector, but instead I would follow the rules for the file I’m working in. I did include a few CSS Custom Properties here just in case I want to overwrite something and then use that variable to compute something else in all .prose elements.

The name "prose" might seem familiar to someone who uses Tailwind CSS, and specifically the Tailwind Typography plugin. I’m sure other folks have used the term "prose" before Tailwind came along, but that’s where I first heard of the concept and the name for it.

I got a lot of inspiration for my theme layer from Tailwind CSS. Although I wound up not adding any utility classes (yet), I took a lot of naming from Tailwind.

@layer theme {
  :root {
    /* Colors */
    --color-black: oklch(27.68% 0 0);

    --color-gray: oklch(57.51% 0 0);
    --color-gray-100: oklch(91.58% 0 0);
    --color-gray-200: oklch(82.3% 0 0);
    --color-gray-300: oklch(73.49% 0 0);
    --color-gray-400: oklch(66.06% 0 0);
    --color-gray-500: oklch(57.51% 0 0);
    --color-gray-600: oklch(48.37% 0 0);
    --color-gray-700: oklch(30.9% 0 0);
    --color-gray-800: oklch(27.68% 0 0);
    --color-gray-900: oklch(16% 0 0);

    /* OTHER COLORS ... */
  }
}

For starters I kept the concept of color shades. Part of this was a carry-over from the old Tailwind setup, but I also really appreciate the idea of limiting your color palette to a fixed number of shades. While I understand we could use color-mix() to automate shades now, I specifically like going through and defining each shade for myself (although automating it might be a good place to start).

@layer theme {
  :root {
    /* Font families */
    --font-apple: system-ui, sans-serif;
    --font-avinir: 'Avinir Next', 'Avinir', Helvetica, Arial, sans-serif;
    --font-futura: Futura, 'Trebuchet MS', Arial, sans-serif;

    /* Font weight */
    --font-thin: 100;
    --font-extralight: 200;
    --font-light: 300;
    --font-normal: 400;
    --font-medium: 500;
    --font-semibold: 600;
    --font-bold: 700;
    --font-extrabold: 800;
    --font-black: 900;
  }
}

The fonts setup also comes from Tailwind, at least in the weights naming. I'm not opposed to using the numbers directly in my CSS properties, but the naming is just a tad easier for me to look at and immediately put an image in my head as to what name equates to which weight.

@layer theme {
  :root {
    /* Border radius */
    --rounded-sm: .25rem;
  }
}

As time goes on I’ll add more things, like --rounded-sm, to define specific sizes of things I want to use throughout multiple contexts. Using -sm here is a bit of a risk in that it’s possible I won’t want to be limited to sm, md, and lg naming. When that happens I’ll judiciously employ find and replace where I have to 🤓

@layer theme {
  :root {
    /* Animations */
    --duration: 0.3s;
  }

  @media (prefers-reduced-motion) {
    :root {
      --duration: 0.01ms;
    }
  }
}

The --duration property is a little different in that I'm using that as a base for animations everywhere. I shouldn't ever set a new value for it, but instead I should use something like transition-duration: calc(var(--duration) * 1.5). This way animations can be cut down to almost 0 when prefers-reduced-motion is turned on. I thought about doing something like this for opacity and prefers-reduced-transparency, but opacity isn't always set the same way and it changes more often that I handle prefers-reduced-transparency in the situation where it needs overriding.

@layer theme {
  :root {
    /* Sizes for layout spacing */
    --xl: 32px;
    --lg: 24px;
    --md: 16px;
    --sm: 8px;
    --xs: 4px;

    --xl-em: 2em;
    --lg-em: 1.5em;
    --md-em: 1em;
    --sm-em: 025em;
    --xs-em: 0.25em;

    --xl-rem: 2rem;
    --lg-rem: 1.5rem;
    --md-rem: 1rem;
    --sm-rem: 0.5rem;
    --xs-rem: 0.25rem;
  }
}

In my opinion, one of the best uses of utility classes is to work with the 4px-based spacing system that Tailwind and other utility design systems tend to use. I didn’t bring over utility classes because so far I haven’t needed to, but I wanted to maintain some consistency in spacing for things like container padding and grid/flex gaps.

I picked up the idea of using really simple size naming from looking at the CSS found in the Craft CMS Control Panel (CP). While there are places where I use one-off values, these sizes are used for most of the padding and spacing across the site.

I added -em and -rem variants to each size so that I could get the same general spacing size at 100%, but to let those spaces change as the base font size changes.

Fin

I’m still living with this new CSS setup, and I’m not sure this is scalable or the right approach for other types of projects, but so far I’ve achieved my goals of being able to write less CSS, to find CSS quickly when making changes, and to write CSS for the needs of my content.

Along the way I’ve made notes about other side effects that I feel like have occurred for me since making this transition:

  • This is the first time in maybe a decade or so where I’ve used things other than classes for CSS selectors (when I am creating the markup). I really like not naming everything, but I also wonder how much more work this will introduce during times where I move things around.
  • My biggest concerns are updates and maintainability—two of the main reasons why I started using Tailwind in the past. We'll see how nesting and using cascade layers works out when it comes time to update or remove sections. Because I am the only developer for my site, I’m not too worried, but I wonder how this sort of system holds up in a team setting.
  • Nesting with BEM led to a lot of extra CSS, but native CSS nesting (even when using & selectors) doesn’t add nearly as much extra file size, and a lot of the same benefits that I like are there.
  • I went all in on size container queries and it has been great. I’m not opposed to using viewport media queries, but I just didn’t wind up needing any. Things like clamp() and container queries made this possible.
  • I have a dark mode theme, but instead of defaulting to inverting all of the color shades, I went through and manually added prefers-color-scheme: dark queries as needed.
  • color-mix() is a CSS dream. It works so well and I think it will help in so many situations where you don’t have full control over the colors you’re working with (such as colors brought in from third-party libraries).
  • I wrapped all of the styles for Admin Bar in a layer so it’s easier to work with a custom cascade layer setup. I really hope UI libraries and other third-party packages start to do this. Imagine a company that provides a third-party form wrapping all of their CSS in a layer instead of using !important everywhere. In fact, @scope, cascade layers, and container queries makes me glad for all of the developers for marketing sites who currently cringe when getting a message saying "just drop this JavaScript tag onto the page".
]]>
Admin Bar for Everyone https://wbrowar.com/article/code/admin-bar-for-everyone Sat, 25 Nov 2023 00:30:00 -0500 Will https://wbrowar.com/article/code/admin-bar-for-everyone Screenshot

Admin Bar was my first Craft CMS plugin—dating all the way back to 2015! I used Admin Bar to learn my way through plugin development on Craft 2. This was before there was a Plugin Store and before Composer and Packagist came into the mix.

The reason why I created Admin Bar was because the agency I worked for had just transitioned off from making all of our projects with Drupal 7 and we had started using Craft CMS 2 for our default CMS of choice. Craft provided a much better authoring experience on the CP side of things (for the way we approached websites), but it was missing the toolbar that we were used to in Drupal and on Wordpress sites. As a developer who also made content updates, those toolbars were super helpful, and it was always fun to show a client that you can find a page on your site that needs updating and then use the Edit link in the toolbar to jump right to the edit page.

So on my free time I put together Admin Bar and "released" it via updates to its GitHub repo.

Screenshot edit
Screenshot edit note
Screenshot edit outline

I wound up looking for more uses to expand Admin Bar so I added the Edit Link feature. This was more like an edit button for those thumbnails and summaries of news stories you might put on a news homepage.

Implementing Edit Links was trickier than the regular Admin Bar. For one, there would be multiple links on the page, which meant instead of loading a bunch of CSS and JavaScript multiple times, I had to find a way to load them once, find all of the edit links on the page, then get them to render. I eventually got this all working, but sometimes there were issues.

Around this time I had started using Vue.js on a lot of my projects. There seemed to be a wave of folks in the Craft world using Vue or similar frameworks to add interactivity to their pages, on top of their Twig-based sites. For the most part things still worked, but sometimes race conditions between my Edit Links code and Vue’s rendering would cause some issues.

Screenshot guide entry
Screenshot widget edit links

Along with Edit Links I also experimented with a concept, called Admin Bar Widgets. The idea here was that a plugin, like Guide, could inject some HTML into Admin Bar using a custom event on the PHP side of things. The HTML here could give you some info based on the current page you’re on.

I also tried setting up a widget that would find all of your Edit Links on the page and made it so you could click on the link to jump down to that section on the page.

I hadn’t heard of anybody really using the Admin Bar Widgets feature, and even in my own use I found that I’d run into some CSS issues due to the stylesheet on the page clashing with my widget CSS.

THE Problem

So some of the features in Admin Bar had some issues, but they were all things that could be addressed with some CSS or a little extra JS work. The biggest issue that Admin Bar ran into was more about the shift into Jamstack, Headless, and static-cached sites.

On a Twig-based project, Admin Bar would use the session information in your browser to figure out if you are logged in and what permissions your account had. It would also rely on you passing in an entry argument, or it would try to use Craft’s built-in route matching to figure out what page you were on. Then it used all of your config options and plugin settings to customize the look and the links on the Admin Bar on the page.

At some point Admin Bar needs to get information from your CMS database and PHP files to render itself onto the page. In the world of headless sites, where your front-end may even be hosted on a completely different domain, this surfaced a whole bunch of problems.

I spent a lot of time thinking about this. Could you use GraphQL to render out the Admin Bar for the page, then use some JavaScript to pull that in and display it on the page? Could you hit a plugin controller that returns a blob of HTML, or at least the info you need to pass into Admin Bar on the front-end? How do I create something in the Admin Bar plugin that can do all of this securely? Maybe I could put together some lambda functions and service workers to help with this stuff?

Spoiler for the rest of this article: I don’t have answers to these questions, but I have come up with a way to get Admin Bar onto more projects.

Admin Bar 4.0

My conclusion came down to deciding where the line would be for Admin Bar going forward. It’s not feasible for me to try to solve auth and rendering on static sites across all of the different server setups and front-end frameworks out there, but I could provide a way where if you can get through those barriers in your specific site setup, I can provide the front-end component that is a consistent experience across projects.

After spending some time learning some new front-end tricks I converted Admin Bar’s front-end to a standalone NPM package that can be used on Craft and non-Craft websites. I set up Admin Bar Component as a web component, built on Lit.

Screenshot bar

The web component can be installed, using npm install admin-bar-component --save, and you can bundle it in with your existing JavaScript, or you can use a <script type="module"> tag to add it to the page.

The way web components work is that there's the JavaScript that registers the component and you need this loaded onto the page once. Then you can add an <admin-bar> element and the JavaScript will kick in and render the Admin Bar Component based on the attributes and slot content you pass into it.

So in a headless or static site, you could have a script do a fetch to your Craft CMS back-end and when that returns you can use logic on your front end to add the <admin-bar> element.

Props and Slots

The README file in the repo goes over the options you can pass into an Admin Bar Component to add things like a logout button, or your links to various parts of your Craft CMS instance. Everything is opt-in, so you can choose what the greeting looks like or what buttons show up and in what order.

Admin bar example

To render an Admin Bar Component to look like the one in this screenshot, the code looks like this:

Hello, Mate
EditSettingsCraft CMS Docs

Styling an Admin Bar Component

There’s a lot of talk about web components right now and one approach I really like is to take existing HTML and wrap it in a web component to bring interactions to it that you can’t do without some custom scripting or with native HTML elements. The alternate approach is using the Shadow DOM for your component, which gives you the ability to use slots, but it puts a wall between your web component’s HTML and the CSS stylesheet in your project.

There are pros for rendering web components in the "Light DOM", but the Shadow DOM solves a major issue for Admin Bar in that we wouldn’t want to make a change to our website’s CSS only to find that it screwed up the front-end toolbar, requiring us to fix it again.

Admin Bar Component’s look won’t be affected by your stylesheet, but there are plenty of CSS Custom Properties that give you control to adjust things to your liking.

For example, say you’ve added the environment warning to let your content author know they are editing on the QA site and you wanted to make that warning a little more obvious. You could make the height a little taller:

admin-bar {
  --admin-bar-environment-height: 10px;
}
Admin bar environment height

Oh, wait. You’re actually on DEV. So you change the warning color:

admin-bar {
  --admin-bar-environment-height: 10px;
  --admin-bar-environment-bg-color: rgb(255, 50, 24);
}
Admin bar environment color

Okay, so that’s good, but maybe you don’t like the striped look. That’s cool. You can replace the background CSS to a plain, red line:

admin-bar {
  --admin-bar-environment-height: 10px;
  --admin-bar-environment-bg: rgb(255, 50, 24);
}
Admin bar environment color solid

Admin Bar for Craft CMS

The web component’s superpower is that it’s not build just for React, Vue, or any other specific framework. This also means that it can be used for front ends for non-Craft CMS projects. But for the folks who have used Admin Bar in the past, I wanted to keep the same experience (or as close to it as possible).

Admin Bar 4.0 is the same plugin as the 3.x version, with most of the same config options, but the output has been changed to load Admin Bar as the web component, instead of the old Vue component it previously used.

Screenshot bar

Currently the plugin is in beta, but when it is released you’ll be able to upgrade by changing the version in composer, or by installing it from the Craft CMS Plugin store.

There are a couple of new config settings and a few that have been removed, but for the most part things should work as they currently do. Check out the plugin’s README for all of the updated settings.

Admin bar settings

If you had styled the previous version using the Custom CSS setting, check out the new CSS Custom Property defaults. Most of them are similar to the previous version, but there are name changes, a couple of removed variables, and some new additions.

If you'd like to give the new version of Admin Bar a try, you can install it for a Craft 4 site by updating your composer file with the following:

{
  "require": {
    "wbrowar/craft-admin-bar": "^4.0.0"
  },
}

Fin

Admin Bar continues to teach me new things. This time around the Craft CMS portion was more about cleaning things up, where the web component portion gave me a chance to learn a little more about authoring and using web components.

Admin bar all four

I hope the work here will make Admin Bar more useful for more types of projects. If you're one of the folks who’ve been a part of its 60k+ installs over the years, thanks for helping me catch bugs and for helping me to continue to improve it. This project will continue to iterate, and I’m looking forward to seeing what else can be done in this next phase.

🥃 ]]>
LITtle Layout: Converting a Craft CMS Field from Vue to a Web Component https://wbrowar.com/article/code/little-layout-converting-a-craft-cms-field-from-vue-to-a-web-component Mon, 06 Nov 2023 11:59:00 -0500 Will https://wbrowar.com/article/code/little-layout-converting-a-craft-cms-field-from-vue-to-a-web-component Little Layout is a plugin for Craft CMS that gives content authors the ability to visually adjust layout options for a given page, content section, matrix block, or for whatever else the developers sets it up to do.

Little layout example center column

The field is designed to be simple to use and flexible enough that you could use it for all sorts of things, like text alignment or CSS flex and grid alignment. On this blog—run on Craft CMS—I use it to align text and other content across a six-column CSS Grid layout.

So if you’re looking on a tablet or desktop-sized screen, you might see this text laid out in a two-column layout—with this text off to the right.

The Problem

While dogfooding my own plugin, I've found what I think is a performance bottleneck. I use several Little Layout fields for each matrix block of content, and each article may include dozens of blocks, therefore the amount of Little Layout fields instantiated and active on the page grows pretty quickly. What I've found is that the more matrix blocks I add, the more I start to see a slow down in performance around the rest of the page. Typing in a textarea text box starts to lag and adding images and links to other relational items takes a few seconds.

I’ve used Craft CMS for a while and it tends to be very performant at its core, so based on the few plugins I use on this personal blog, I have to suspect that my Little Layout fields could be the culprit. I started thinking of potential theories for what seemed to be a memory or CPU leak:

  • For one, I used Vue 3 to create Little Layout’s field and settings page. Each field instance calls Vue’s createApp.mount() method to create a new Vue instance for each field. On a page with maybe 10 Little Layout fields you may not notice any issues with this, but as you create a new matrix block a new field instance is added, and that means a new Vue instance is created and running on the page.
  • Another potential problem could have been a ResizeObserver I had on each field, watching to see if the field would need to horizontally scroll. If it did, this put up a message to the author letting them know they could scroll to the right to access the other fields.

Solutions

For the ResizeObserver issue, I was thinking of coming up with a CSS-based solution. I thought maybe the new :has() selector or some of the new scroll-related CSS features would help, but I haven't I haven’t found the solution for this yet. Because the feature was more of a helper, I removed the observer for now. You can still horizontally scroll, but there won’t be a message that pops up.

Regarding the Vue issue, I spent some time thinking through some potential solutions:

  • Instead of creating a new Vue app for each field, I could find a way to create one Vue app and see if I could share the instance via Vue’s Teleport feature. I sill haven't thought through how this could actually work but I have a feeling the performance gain won’t be very big.
  • Another solution could be to try to switch to the Craft CMS internal Vue asset bundle instead of bundling in my own version of Vue in my plugin JS. I don’t think it will help because I think while it might reduce the JS bundle size it won’t reduce the amount of CPU and RAM used for multiple Vue instances.
  • Finally, after learning a little more about web components, I thought maybe this is a good use case for a set of web components to replace my Vue components. The change of framework shouldn’t affect the content author as long as the way the field works and its output remain the same, but if I can get a little closer to the metal with web components I thought I could at least explore this option by refactoring the Control Panel (CP) field first.

I went with the web component approach first, refactoring from a set of Vue single-file components (SFC) to a set of Lit-based web components. I've pushed this up to Packagist and as I type this blog post I'm going to see if I notice any improvement over the Vue version of the field.

Why Lit?

The short answer is because Dave told me to use it.. I wanted to start with vanilla web standards first, but I quickly found that things like reactive properties and render loops involve a lot of the work that web component frameworks are great at mitigating. Templating would also make a big difference since rendering this field requires loops and conditionals based on user input.

I think it’s still good to learn these standards, but since this isn't a web component I'm sharing as a library I felt like I had a little more freedom to work with dependencies that provide some extra helpers—as long as they are an improvement over my existing Vue setup.

As someone who’s spent time in Vue more than any other front-end framework (other than maybe jQuery), I am very fond of the things a Vue SFC gets you. I was really happy to find some equivalents in Lit that made porting the components over easier.

Register Once, Use Everywhere

So in the Vue version of the Little Layout field I needed to detect when the field was rendered onto the page (via Twig) and then I needed to call a JavaScript function that kicks off initializing Vue and attaching it to that field’s markup. This had to work both when the page initially loads, and whenever a field is created within something like a Matrix or Super Table field, so it was set up to get the name of the newly created field, match that up to an element on the page, then use a JS function to mount the component via Vue.

Web components are much more elegant here. When the page loads I can load a JavaScript file that registers my web component as a custom element on the page, based on the name of my choosing. So I have a script that defines little-layout-field as a custom element and whenever that element shows up on the page the web component will be used to render it. This means when new matrix block creates a new little-layout-field element I can pass in some props to define the settings for the field and rendering happens automatically.

import { LittleLayoutField } from './components/LittleLayoutField'
import { LittleLayoutFieldControl } from './components/LittleLayoutFieldControl';

// Register light DOM field component that includes input element for the page.
customElements.define('little-layout-field', LittleLayoutField);

// Register shared layout component (including layout boxes).
customElements.define('little-layout-field-control', LittleLayoutFieldControl);

Light vs Shadow

As you can see in the snippet above, I’m actually registering two web components to the page. This follow a pattern from the Vue setup where I have a shared component that displays the layout boxes and handles the majority of the author’s interactions, then I have a wrapper component for the content author’s field and another wrapper that is used in the field settings page.

So far with this refactor I've only ported over the content author field, so refactoring the settings field component is to come—if this experiment winds up being the solution to go with.

This pattern also helps with something I was struggling to figure out around web components. By default, Lit renders your template into the Shadow DOM. This is great for scoping your markup and your styles, but I wanted the input field to be rendered outside of the Shadow DOM so it can be accessible if Craft’s built-in JavaScript needs access to it.

In my LittleLayoutField component, I switched the rendering mode to target light DOM with the createRenderRoot() method:

/**
 * Changes the render mode for this component from shadow DOM to light DOM.
 */
protected createRenderRoot() {
    return this;
}

The resulting markup looks like this:

Little layout rendered markup

You can see here where Shadow Content starts within the little-layout-field-control element, but everything else is rendered on the page as normal elements.

In this setup, the content author interacts with elements within the little-layout-field-control element, the field value is computed there, then it's passed up to the little-layout-field component using a custom event (similar to how you could use emit to pass data up in Vue), then little-layout-field renders the input field.

From the Craft CMS standpoint, this input field is part of the overall edit page form and it gets submitted with the computed value. Then the developer can continue to work with that value as they would like in their GraphQL or Twig template.

Matching the Craft CP UI

One of the other challenges I ran into here is making sure my field continued to feel like it was part of the Craft CP design language. I know the layout boxes are sort of unique, but Craft CP has a defined color palette and things like icons that I wanted to continue to use.

The tricky thing with rendering in the Shadow DOM is that because its CSS is scoped, you can’t simply drop in a class that matches up to an existing style in the global stylesheet. For example, you wouldn't be able to get button styles, like the ones you’d get from the btn class. You also wouldn't inherit things like the text colors or global spacing properties. Imagine that there's a wall around everything in the Shadow DOM and you need to find a way to pass things through, or duplicate them within your component.

Luckily the Craft CP includes a whole lot of CSS Custom Properties. Most of the colors used in the CP are defined on the :root as custom properties, so pulling in colors was no issue at all. I had a few places where I wanted to slightly adjust a color and the new CSS color-mix() method came in handy.

The one thing that I did have an issue with is that in the Little Layout field there is a setting that lets you clear out the selected field data and this uses a button with the classes, delete icon on it. The icon class adds a global icon pulled in from a global stylesheet, and the delete icon selects the correct icon and then does some work to make sure it’s centered and positioned correctly.

Little layout field with clearable
Little layout field with clearable hover

This icon and its interactions wouldn’t be hard to re-create, but because the Craft CP styles could change at any point, I wanted to utilize the original styles from the original stylesheet. The way that Craft’s CP resources are loaded includes a unique hash in the file name, so in order to load the stylesheet, I'd need to pull it in via PHP first and then pass the path to it into the web component. While I think the web component could load that CSS file, it would mean a double download penalty for the user.

Again, Lit has a solution that worked in my light DOM/Shadow DOM setup. Lit uses the web standard for using slots to let you pass elements into web components as child elements.

The button in my little-layout-field-control component is rendered like this:

protected render() {
    return html`
        
${this.clearable && this.editable && this._hasSelected ? html`` : nothing}
`; }

Notice that the button doesn’t include the delete icon classes, because this component wouldn’t be able to use them to render the icon in the Shadow DOM. To fix this, a slot, called clear-icon, has been added. The button can be clicked on to handle clearing out the field data, and the contents of the clear-icon will come in from the parent web component.

The parent looks like this:

protected render() {
    return html`
      
    `;
}

Here we are rendering the little-layout-field-control component (as a child component) and passing a plain old span with the delete icon classes on it as the clear-icon slot. Because this file is rendered in the light DOM we'll get any styles added onto the page, even though it gets nested into the markup of the Shadow DOM.

Speaking of CSS

In the Vue version of Little Layout I had used Tailwind CSS to handle everything CSS. I was using Tailwind with a prefix to help namespace my classes but otherwise I did things like copied color values from the Craft CP color palette into my config (this was before Craft’s CP colors were changed to CSS Custom Properties).

While I am generally fine with using Tailwind where it helps you out, using any framework CSS in Shadow DOM-scoped styles is a pain and it requires compromises and jumping through hoops. This is especially true for Tailwind.

So I bit the bullet and removed Tailwind from the web component, in favor of refactoring my template to use vanilla CSS. I was essentially re-creating my HTML markup anyway, so I used my old design as a guide, but wound up re-writing all of the CSS based on the new markup.

Lit lets you add CSS to the Shadow DOM-rendered elements via a styles property, using a css template function, similar to the html template function you use in the render() method. CSS for the light DOM elements can be styled by the page itself.

static styles = css`
  .container {
    overflow: auto;
    -webkit-overflow-scrolling: touch;
  }
  .field {
    --layout-box-size: 22px;
    --layout-boxes-gap: .25rem;

    display: grid;
    grid-template-columns: min-content minmax(0, 1fr);
    gap: .3rem;
    align-items: center;
  }

  // ... more styles
`;

Fin

So while I'm still testing out this approach and learning my way through the technology behind web components, I’m pretty happy with the results. I’d like to reach out to the Craft CMS community to see if anybody there can help beta test this new version of the field before releasing it as an update. Based on how that goes, I will likely revisit the settings Vue component and then fully remove Vue and Tailwind as dependencies on the front-end of my plugin.

If you have a Craft CMS project and you would like to try this out, you can change the version number of wbrowar/craft-little-layout in your composer.json file to use the version in the current development branch:

{
  "require": {
    "wbrowar/craft-little-layout": "dev-main"
  },
}

If you run into any issues, please go ahead and create a ticket in the Little Layout repo: https://github.com/wbrowar/craft-little-layout/issues

]]>
Craft CMS Live Preview with Nuxt 3 https://wbrowar.com/article/code/craft-cms-live-preview-with-nuxt-3 Sat, 18 Feb 2023 14:00:00 -0500 Will https://wbrowar.com/article/code/craft-cms-live-preview-with-nuxt-3 I recently upgraded my website from Craft CMS 3 to Craft 4 and from Nuxt 2 to Nuxt 3. It’s still a work in progress, but I'd like to share my learnings along the way.

In 2020 I wrote about how to set up Live Preview between Craft CMS and Nuxt 2. At that time, headless, static, Jamstack sites were a new thing to me and the main issue I was tackling was around wanting to serve the content pages of my site in a fully static way, while allowing me to see the changes I make to new articles before they are published.

Nuxt 2 has a preview mode that lets you take a static page and replace its content by re-rendering the page as an SPA, but in the way Nuxt 2 worked, you had to set up serverless functions to avoid exposing your API endpoint and authorization tokens. My solution in that article was to take your one front-end codebase and build it twice (one as a fully static public site, and the other as a hidden SPA that always re-requests your page content from the API on every page).

As Nuxt 3 was going stable, I began the work to upgrade my site and I found that the new Hybrid Rendering feature gets me closer to the same sort of setup as my Nuxt 2 site, but it both simplifies and complicates things. On the one hand, I was able to drop the second build of my site (along with the separate infrastructure) because Nuxt could now pre-render some routes while leaving other routes dynamic. On the other hand, this meant running Nuxt on a Node server and using SSR as part of rendering my content. Using SSR means the back-end API needs to stay connected, and this means the site is no longer fully headless.

These are trade-offs I can live with and, technically, I could have just left it on the duel-build setup. I think the hybrid rendering setup would have been a much better fit for clients that I’ve worked with in the past, so some of taking this approach was to help figure all of that out.

Anyway, if you’re building a site with both Nuxt 3 and Craft CMS 4, here's an approach you can take to make Live Preview work:

The Gist of It

Okay, so before getting into the technical stuff, what we want to do is create a route in both Craft and Nuxt that is a dynamic version of our page content. Using my blog as an example, the URI for this article is:

https://wbrowar.com/article/code/craft-cms-live-preview-with-nuxt-3

When someone visits this page, they will be served the full pre-rendered article, as built by my CI/CD process.

In order to preview this article, I am going to prefix my URI with a unique URI segment and tell Nuxt, “Hey Nuxt, any route that starts with this segment should not be pre-rendered, so keep hitting that API every time you load the page”. Nuxt doesn't talk back, but I assume it would say “Hey, I gotchu”.

So I'm going to add an additional URI pattern in both Craft and Nuxt so the preview URL will look like this:

https://wbrowar.com/live-preview/article/code/craft-cms-live-preview-with-nuxt-3

Now, when Nuxt sees live-preview in the first route segment, it will reach for the same content as the static page, but re-fetch it on every page request.

In addition to adding the separate route segment, I also want to add a query parameter that is unique and can be changed via environment variables, to add just a little extra obscurity to my live preview pattern. So now my Live Preview URLs will looks something like this:

https://wbrowar.com/live-preview/article/code/craft-cms-live-preview-with-nuxt-3?has-live-preview=yes

So now Nuxt is looking for both the live-preview route segment, and has-live-preview query param to determine if it should show Live Preview content.

Setting Up a Separate Live Preview Target in Craft

Craft gives you flexibility to choose the URL pattern that is used during Live Preview, and I want to get this route segment and query parameter into my Live Preview URLs. We can start by adding the following variables in my Craft CMS .env file.

# Live Preview settings
# Set preview tarrgets to something like this:
#   {alias('@livePreviewUrl')}/{uri}?{alias('@livePreviewParam')}={slug}
LIVE_PREVIEW_PARAM=has-live-preview
LIVE_PREVIEW_URL=https://wbrowar.com/live-preview

In order to concatenate my URI pattern, I added a couple of URL aliases in my Craft CMS config file.

aliases([
        '@livePreviewParam' => getenv('LIVE_PREVIEW_PARAM'),
        '@livePreviewUrl' => getenv('LIVE_PREVIEW_URL'),
    ]);

With these in place I can now update the sections of my site that I would like to enable Live Preview. For my articles and for most pages in my site, I’ll set my Preview Targets → URL Pattern to this:

{alias('@livePreviewUrl')}/{uri}?{alias('@livePreviewParam')}=yes

The first portion adds in live-preview, then we add the full URI for the page, as resolved from the Entry URI Format setting. Then, we tack on the has-live-preview query parameter.

One thing to note here is that if you wanted to set up your home page for live preview, you should remove the {uri} portion and use a pattern like this:

{alias('@livePreviewUrl')}/?{alias('@livePreviewParam')}=yes

If you want to make sure this worked, go to an entry page in the CMS, click the Preview button and use your browser DevTools to inspect the right side of the page. Look for the iframe that loads your page content and check that the src attribute looks like this:

https://wbrowar.com/live-preview/article/code/craft-cms-live-preview-with-nuxt-3?has-live-preview=yes&x-craft-live-preview=ABC12345&token=ABC12345

Now if you aren't familiar with how Live Preview works, you might notice the x-craft-live-preview and token query parameters have been added here. These will be important later, but for now if you see something like this you should be all set in the Craft side of things.

Matching Environments Up in Nuxt

Just like in the Craft setup, let’s tackle the settings stuff first. In my Nuxt config I'll add the following to my runtimeConfig:

export default defineNuxtConfig({
  runtimeConfig: {
    graphqlAccessToken: '',
    graphqlEndpoint: '',
    livePreviewParam: 'has-live-preview'
  }
})

In order to communicate with my Craft CMS GraphQL endpoint, I already had the graphqlAccessToken and graphqlEndpoint properties set and I am using Environment Variables to update those.

To match up my LIVE_PREVIEW_PARAM in my Craft .env file, I am setting the same string, has-live-preview here. Note that you could also update this to be unique to each server environment by adding LIVE_PREVIEW_PARAM=has-live-preview to your Nuxt .env.

GraphQL Queries in Nuxt

These variables have to be used somewhere, and for the sake of keeping things DRY, I am going to create a Nuxt server route that will be used to make all of my requests out to my GraphQL API.

In server/api I'll create a new TypeScript file, called query.ts and that will look like this:

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  console.log('API: query', body)
  const runtimeConfig = useRuntimeConfig()

  async function runQuery () {
    const requestBody: {
      query: string;
      variables?: object
    } = {
      query: body.query
    }
    if (body.variables) {
      requestBody.variables = body.variables
    }

    const endpointUrl = body.route?.query?.[runtimeConfig.livePreviewParam] && body.route?.query?.token ? `${runtimeConfig.graphqlEndpoint}?token=${body.route.query.token}` : runtimeConfig.graphqlEndpoint

    const data: any = await $fetch(endpointUrl, {
      body: JSON.stringify(requestBody),
      headers: {
        Authorization: `Bearer ${runtimeConfig.graphqlAccessToken}`,
        'Content-Type': 'application/json'
      },
      method: 'POST'
    })
    if (data) {
      return data.data
    }
  }

  const response = await runQuery()
    .then((response) => {
      return response
    })
    .catch((e) => {
      console.error('GraphQL API error', e)
      throw e
    })

  return response
})

Let’s break this down.

First, I am going to use the POST method to use this file, and I'll be passing in parameters via a post body. The body variable gets the parameters for the request and the runtimeConfig variables gets those settings from my Nuxt config file.

In the runQuery() method I pull out my GraphQL query and variables from the request body.

Then I set endpointUrl to the full endpoint that I’ll use to hit Craft CMS with my request. There is some logic here that checks to see if there is a parameter that matches my livePreviewParam as well as a token parameter—from the <iframe> URL mentioned above. If both of those parameters are there, we’ll pass that token variable back to Craft as a Live Preview request. If one of those variables are missing, we'll assume this is not a Live Preview request and that we just need the GraphQL endpoint so we can query for the published page content.

There's a lot going on in that line, but from here we're going to use Nuxt’s built-in $fetch to make our request and either get back the live page content or dynamic Live Preview content.

Server Route Usage

In my article page components I use <script setup> so in order to use this server route, I do something like this:

import { articleGql } from '@/gql/articleGql'

const route = useRoute()

const articleCategory = ref(route.params.articleCategory)
const articleSlug = ref(route.params.articleSlug)

const { data: page, pending } = await useFetch('/api/query', {
  body: {
    query: articleGql,
    route,
    variables: {
      uri: `article/${articleCategory.value}/${articleSlug.value}`
    }
  },
  method: 'POST'
})

First, I keep my GraphQL queries in a separate directory, so I import in my query as a string. Then I need context from the Nuxt route for the current page in order to match this up with the article’s route in Craft CMS.

From there I use Nuxt’s useFetch composable to hit my custom server route and make the request for my content. In my request body I'm passing in my GraphQL query, the current page route, and the URI that will be passed along as a GraphQL variable.

Because I'm passing in my route, my server route will know if the has-live-preview param is present or not, however, up until this point we are still not prepending live-preview to the front of the route. That comes next.

File-Based Preview Routing

The components that render my articles live in this directory structure:

| pages/
--| articles/
----| [articleCategory]/
------| [articleSlug].vue

To prepend live-preview to the front of my Live Preview-specific routes, I can add a live-preview directory to my pages folder. Within that pages/live-preview directory, I want to match my current directory structure so the rest of the URI stays the same. Here's what that would look like:

| pages/
--| articles/
----| [articleCategory]/
------| [articleSlug].vue
--| live-preview/
----| articles/
------| [articleCategory]/
--------| [articleSlug].vue

The [articleSlug].vue component in that pages/live-preview directory could be a duplicate of the original page component, but the downside would be that if I ever wanted to make changes to one, I’d have to make the same change to both.

Luckily, Vue and Nuxt are just a whole bunch of JavaScript files, so we can do things like this:

In my preview version of [articleSlug].vue I am importing the original component file, then as my <template> content I am using a Vue dynamic component as the root element.

By doing this, I get all of the refs and functionality from my original component’s <script setup>, but when I pass the route parameter into my server route, it’ll now be based on the page/live-preview route. That will complete the requirements in my server route to show my Live Preview content.

Route Rules Rule

In my Nuxt setup I have some Nitro route rules configured, so to make sure my Live Preview routes are not pre-rendered, I can add the following rules to my Nuxt config file:

export default defineNuxtConfig({
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: routeUrls,
      ignore: ['/live-preview', '/live-preview/**']
    },
    routeRules: {
      '/live-preview': { ssr: false },
      '/live-preview/**': { ssr: false }
    }
  },
  runtimeConfig: {
    graphqlAccessToken: '',
    graphqlEndpoint: '',
    livePreviewParam: 'has-live-preview'
  }
})

I can’t say that I've fully figured out how to use Nitro yet, but this seems to work for now. In nitro.prerender I am telling Nitro to ignore all routes that begin with /live-preview when crawling my site. In nitro.routeRules I’m letting Nitro know that when Nitro does get to one of those routes, that I don't want to use SSR at all. This treats those routes like an SPA, fetching all of the data every time the page is hit.

Note: I would like to keep working on this to optimize the pre-rendered routes, so this might change in the future.

Keeping the Scroll Position

Oh, just like in my previous article, keeping scroll position during Live Preview can be solved in Nuxt 3. This time, I'm using a composable to help:

export const useScrollPosition = () => {
  const route = useRoute()

  const scrollPosition = ref(0)
  const ticking = ref(false)

  if (route.path.startsWith('/live-preview')) {
    const storageKey = `scrollPosition:${route.path}`

    // If scroll position is set, scroll to it
    if (sessionStorage.getItem(storageKey)) {
      window.scrollTo(0, parseInt(sessionStorage.getItem(storageKey) ?? '0'))
    }

    // Record scroll position in session storage to retain scroll position in Live Preview
    setTimeout(() => {
      window.addEventListener('scroll', () => {
        scrollPosition.value = window.scrollY

        if (!ticking.value) {
          window.requestAnimationFrame(() => {
            sessionStorage.setItem(storageKey, scrollPosition.value.toString())
            ticking.value = false
          })

          ticking.value = true
        }
      })
    }, 1000)
  }
}

This composable uses sessionStorage to store the scrollY value of the page in Live Preview and then it sets the page to scroll down to that value whenever it refreshes.

Unlike a plugin, this composable needs to be added to each page that you want it to be active for, so you can add this to your page components:

import { useScrollPosition } from '~/composables/useScrollPosition'

onMounted(() => {
  nextTick(() => {
    useScrollPosition()
  })
})

Fin

If everything is set up correctly, when I visit the article’s URL I should see the fully rendered version of this article. When I go into Craft CMS and hit Preview, I should also see my changes update as Craft saves my content drafts.

I'd still like to figure out how to set all of this up in a headless way, but for now this setup works for me. Just as it was in Nuxt 2, Nuxt 3 and Craft CMS remain a very good pair in my tech stack.

Craft ❤️ Nuxt

]]>
RSS Feeds in Nuxt 3 and Craft CMS https://wbrowar.com/article/code/rss-feeds-in-nuxt-3-and-craft-cms Fri, 13 Jan 2023 21:30:00 -0500 Will https://wbrowar.com/article/code/rss-feeds-in-nuxt-3-and-craft-cms Recently I upgraded my website from Nuxt 2 to Nuxt 3 and I've been crossing a few things off my list of features and content to add. Specifically, I’ve been wanting to set up a proper RSS feed for my blog articles, but hadn’t taken the time to figure out how to do it with Node or Nuxt. I finally got around to rectifying this and I got a chance to play around with another feature in Nuxt 3 in the process: Nuxt Server Routes.

My site is currently driven by Craft 4 as my CMS with Nuxt 3 on the front end. While this article is Nuxt 3-specific, a lot of the concepts around Craft CMS can be applied to any headless CMS or GraphQL data provider.

Article Structure

Articles on my site, like the one you are currently reading, are grouped by category in Craft via different entry sections. Each section determines the article’s URL and gives me the flexibility to create templates for SEO or field sets based on each section.

Right now I have a “code” section and a “maker” section. As you can see on this post, the URL contains the segment, /code/, that is used to match up my front-end routes with my article categories.

In Craft’s GraphQL implementation each section handle—in this case “codeArticle” and “makerArticle”—is used as part of the schema definition for all of the fields that make up my article content.

These handles and URL segments will be important later.

Nuxt Approach

In Nuxt 2, my typical approach around adding things like XML files to the project would normally be to use some sort of Node script that pulls in my articles, generates the file, then writes it out to disk. I wanted to see if Nuxt 3 had a built-in tool that would work and I came across this article, Create an RSS Feed With Nuxt 3 and Nuxt Content. For a general RSS feed, this article is all you need (especially if you’re using Nuxt Content).

This article pretty much lays out the approach I went with, including pulling in the NPM package, rss, as the main tool used to generate RSS XML content. I wanted to create a main RSS feed, as well as individual feeds for each category, so I abstracted this out a bit and made a few changes to work with my CMS fields.

Working in Nuxt

So in following Michael’s article, I added in my default RSS feed as a TypeScript file at ./server/routes/rss/index.ts. Unlike Michael’s setup I wanted my feed to appear at https://wbrowar.com/rss/, so I removed the .xml file extension from my filename.

import RSS from 'rss'
import { ofetch } from 'ofetch'
import { rssIndexGql } from '~/gql/rssGql'
import { articleBodyBlocks, feedOptions } from '~/utils/rss'

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig()

  const feed = new RSS(feedOptions({ uri: '/' }))

  const entries = await ofetch(config.graphqlEndpoint, {
    body: { query: rssIndexGql },
    headers: { Authorization: `Bearer ${config.graphqlAccessToken}` },
    method: 'POST'
  })

  for (const entry of entries?.data?.entries) {
    feed.item({
      title: entry.title ?? '-',
      url: `https://wbrowar.com/${entry.uri}`,
      date: entry.dateCreated,
      description: articleBodyBlocks({ blocks: entry.articleBody ?? [] })
    })
  }

  const feedString = feed.xml({ indent: true })
  event.res.setHeader('content-type', 'text/xml')
  event.res.end(feedString)
})

In this file, I am creating a server route, using defineEventHandler, setting up the feed’s global settings, querying all of my articles, populating the feed, then writing out the XML file contents using event.res.end().

There are a few things in here that are abstracted out to other files:

  • rssIndexGql is the GraphQL query that pulls in all of my article content for all of my article sections.
  • feedOptions sets up globals for my RSS feed. Things like the site URL, copyright, etc...
  • articleBodyBlocks generates the content for each article in the feed, based on the Craft CMS matrix field that I use to build out each article.

Index GraphQL Query

As a habit, I keep all of my GraphQL queries stored as files in a gql directory at the root of my Nuxt project. I keep these as TypeScript files so I can manipulate the query based on the particular situation. These files export out my queries as plain strings that are passed along via my API request.

For my RSS feed, I copied the GraphQL query that I use for my Nuxt templates and stripped out a lot of the layout fields or other fields that aren’t needed in an RSS reader. From there I created a file, called rssGql.ts:

/*
 * Test using `codeArticle`, then swap the section handle out
 */

const elementGql = ({ handle = 'codeArticle' }) => {
  return `
... on codeArticle_codeArticle_Entry {
  articleBody {
    __typename
    ... on articleBody_code_BlockType {
      fileName
      code
      languageName
      caption
    }
    ... on articleBody_image_BlockType {
      file {
        title
        ... on codeArticleImages_Asset {
          caption
          ioArticleBodyUnconstrained {
            srcset
          }
        }
      }
    }
    ... on articleBody_markdown_BlockType {
      subheader
      text @markdown(flavor: "gfm-comment")
    }
    ... on articleBody_spacer_BlockType {
      hr
    }
    ... on articleBody_subheader_BlockType {
      subheader
    }
    ... on articleBody_video_BlockType {
      youtubeId
      vimeoId
      file {
        ... on codeArticleImages_Asset {
          url
        }
      }
    }
  }
}`.replace(/codeArticle/g, handle)
}

export const rssIndexGql = `{
  entries(section: ["codeArticle", "makerArticle"]) {
    dateCreated
    title
    uri
    ${elementGql({ handle: 'codeArticle' })}
    ${elementGql({ handle: 'makerArticle' })}
  }
}`

In this file, I’ve broken my query into two parts:

  • elementGql is a method that returns a portion of the query for all of the fields for each section. It uses a simple string replacement regex to swap out the section handle. If I didn’t do it this way, I’d repeat all of the same fields for each section, but that would require a little extra work any time I wanted to make adjustments. Part of what makes this work is to be consistent in my field handle naming.
  • rssIndexGql sets up an entries query, gets the title and other general entry data, and inserts the elementGql partial for each article section.

Utils

To help reduce duplication in my sever route files, I created a utils file at ./utils/rss.ts to house a couple of functions that help generate my feed markup.

Some global options I want to include in all of my feeds are set in the feedOptions method:

/*
 * Fill in feed options and adjust based on URL
 */
export function feedOptions ({ uri = '/' }) {
  const now = new Date()

  return {
    copyright: `©️${now.getFullYear()} William Browar`,
    feed_url: `https://wbrowar.com/rss${uri}`,
    language: 'en',
    managingEditor: 'Will Browar',
    webMaster: 'Will Browar',
    image_url: 'https://wbrowar.com/logo.png',
    site_url: 'https://wbrowar.com',
    title: 'Will Browar'
  }
}

Doing this here will make it easier if I decide to make a change to any of these options. The only thing that needs to be passed in is the URI of the feed. The result of this method is used when creating a new rss instance with new RSS(feedOptions({ uri: '/' })).

For each article in my feed, I need to pull out all of the matrix blocks that make up each article’s content. To do this, I created the articleBodyBlocks method.

import escape from 'lodash-es/escape.js'
import { ArticleBody_MatrixField, MakerArticleImages_Asset } from '~/types/automated/generated-craft'

/*
 * Generate markup for all articleBody blocks
 */
export function articleBodyBlocks ({ blocks = [] }: {blocks: ArticleBody_MatrixField[]}) {
  const articleBody = ['']

  blocks.forEach((block) => {
    switch (block.__typename) {
      case 'articleBody_code_BlockType':
        articleBody.push(`
${escape(block.code ?? '')}
`) break case 'articleBody_image_BlockType': (block.file as MakerArticleImages_Asset[]).forEach((image) => { if (image) { const caption = image.caption ? `
${image.caption}
` : '' articleBody.push(`
${image.title ?? ''}${caption}
`) } }) break case 'articleBody_markdown_BlockType': if (block.subheader ?? false) { articleBody.push(`

${block.subheader ?? ''}

`) } articleBody.push(block.text ?? '') break case 'articleBody_spacer_BlockType': if (block.hr ?? false) { articleBody.push('
') } break case 'articleBody_subheader_BlockType': articleBody.push(`

${block.subheader ?? ''}

`) break case 'articleBody_video_BlockType': if (block.youtubeId) { articleBody.push(`