I’ve always liked the idea around the feature, but that implementation wasn’t right. On top of asking developers to add and maintain something in their plugins, there remained the issue of CSS from your front-end templates overriding the widget CSS and making a mess of them.
As we’re coming up on 10 years of Admin Bar being available as a free Craft CMS plugin, I’ve decided to introduce a PRO edition that brings back Admin Bar Widgets in—what I think—is the right way to do it. Read on to find out more.
Admin Bar Widgets integrate Craft CMS and other Craft CMS plugins to display links and statuses related to the current page you are on in your Craft CMS site. For example, there’s an SEOmatic widget that shows you the meta information for the current page, and another widget that tells you how many times the page has been visited—as collected by the View Count plugin.
Just like everything else Admin Bar, widgets are all about flexibility. Widgets are all opt-in by default, so you can head over to the Admin Bar Plugin Settings page on an environment that lets you make admin changes. From there you’ll see a new section, called Admin Bar Widgets:
If you haven’t picked up the PRO edition yet, this section will be disabled but you’ll still be able to see what widgets will be available to your site. If a widget is an integration for a Craft CMS plugin, it would show up in the list of available widgets when the plugin is both installed and enabled for your project.
If Admin Bar supports a plugin that you don’t have installed on your site, it will appear in a list of available widgets in the right-hand column.
Enabling a widget and then hitting the Save button will update the Admin Bar in the Preview section at the top of the page. The Preview area will try to find an entry on your site that has a URI and it will use that entry to show you what the Admin Bar would look like on that entry’s page. A random entry is loaded every time the Settings page loads to show a variety of Admin Bar scenarios.
Now your enabled widgets will appear and you can get a feel for how they will work on your site. Here is a rundown of all of the widgets that will be available at the launch of Admin Bar PRO:
The New Entry widget is a quick way to create a new entry. The logged-in content author will see a list of entry sections that they have permission to create, and by clicking on one of the links they will be brought back into the CMS and onto the New Entry edit page for that entry type.
The Guide widget displays a list of guides that have been assigned to that entry in the Guide Organizer. Clicking on one of these links will bring you to the page for that guide in the Control Panel.
Below the list of guides, there is a link to the CMS Guide page in the Guide Control Panel section.
The SEOmatic widget shows you a preview of SEO meta data for the current page. This shows these SEO values for the entry:
og:image
The Sites widget shows you the current Craft CMS Site. If the page you are on is propagated to other Sites, a menu will appear with links to that page on all related sites.
This can be an easy way to jump between your project’s Sites.
The Blitz widget shows the Blitz cache status for the current page you are on. If the page is cached, clicking on the status will show the date and time the page was cached.
The View Count widget displays the amount of times the current page has been viewed—as collected by View Count.
All of these widgets have a default view that’s meant to keep them compact on the Admin Bar toolbar. There's also another setting on the Admin Bar Settings page that enables more verbose labels that show the title of the widget.
The core of Admin Bar will continue to live on in the free LITE edition. The PRO edition will be focused mostly on the Admin Bar Widgets feature and integrations with third-party plugins.
The PRO edition will be introduced at $19 and you can upgrade to it on the Craft CMS Plugin Store. Moving back down to the LITE edition can be done at any time.
What is different about Admin Bar where it makes sense to re-introduce the Admin Bar Widgets feature?
Aside from managing integrations from within Admin Bar now, the biggest change is how Admin Bar Widgets utilize the Shadow DOM in Admin Bar Component to isolate them from the CSS on your front-end templates. While the widgets are themeable via Admin Bar’s CSS Custom Properties, you should no longer see your CSS making a big impact on the rest of the widgets’ content.
Speaking of integrations, Admin Bar PRO is based around working well with third-party plugins, so it’s important that when a plugin gets updated it will not break Admin Bar or any other portion of your site’s front-end. Admin Bar Widgets will check plugins for their installed version and use that info to decide which version of its widget should be shown.
If a plugin is updated to a major SEMVER version (for example, from 1.0.0
to 2.0.0
), the widget that requires it is then disabled by default. Once Admin Bar has been tested with the new version then an update to Admin Bar will re-enable the updated plugin’s widget.
Admin Bar Widgets use a feature that was recently added to the Admin Bar Component, utilizing HTML popovers and CSS anchor positioning. As of this writing, popover support is widely available across evergreen browsers, however, anchor positioning support is limited to Chrome and Chromium-based browsers.
So in Google Chrome, when you use the SEOmatic widget you should see something like this:
In browser that don’t yet support anchor positioning, the popover will be displayed in the center of the screen and a mask will blur out the page behind it. So in Safari or Firefox, you may see the SEOmatic widget look like this:
Once anchor positioning support is brought to these browsers the affected widgets will automatically change their behavior to match the first screenshot.
More widgets are planned for future releases, and I’d love to hear from other plugin developers who would like to integrate Admin Bar with their plugin. If you have an idea for a new widget, please start a new post on the Admin Bar GitHub Discussions page or contact me.
Thanks to everyone who has enjoyed the last 10 years of Admin Bar and I hope the next 10 have just gotten much more useful for you and your clients.
🥃 ]]>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.
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.
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.
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 😊
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.
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.
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.
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).
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.
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.
<p>My static text</p>
to <p>{{ entry.textField }}</p>
.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.
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.
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.
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:
For a full list of language and hosting requirements, see this page in the Craft CMS docs.
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:
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).
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.
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:
Happy Crafting!
💻 ]]>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.
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:
{% css %}
tags to write CSS in your Twig document.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:
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 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:
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.
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!
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.
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).
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.
📕 ]]>What was released in Craft CMS 3.7 was one of the best and worst features for my newly minted plugin.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
📓 ]]>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.
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.
The Guide home page is now called CMS Guide—for those who want all of their guides to appear in one place.
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.
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’s Twig components have all been modernized with a few additions and quality-of-life improvements:
{% set %}
block lets you fill in tables with HTML so you can add image previews, links, and styles to your data.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.
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 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.
📘 ]]>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.
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.
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.
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.
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
.
Using the Field Layout field, I added my Layout
field:
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.
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.
If you are new to Guide, the typical process for creating a guide goes something like this:
+ New Guide
button to be taken to the Guide Editor.Save
—taking you back to the Organizer.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 }}
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.
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.
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.
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:
templates/_guide/
by default).Page Template
. This will display the Template field.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.
🍱📘 ]]>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:
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 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.
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.
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
component.<pre><code>
block that enables highlighting for a specific language, using highlight.js.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.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.
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:
include
or embed
tags.pages
directory.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).
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:
.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.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.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
.[ 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.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
.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;
}
}
}
}
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.
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:
&
selectors) doesn’t add nearly as much extra file size, and a lot of the same benefits that I like are there.clamp()
and container queries made this possible.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).!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 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.
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.
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.
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.
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.
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.
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.
To render an Admin Bar Component to look like the one in this screenshot, the code looks like this:
Hello, MateEdit Settings Craft CMS Docs
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;
}
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);
}
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);
}
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.
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.
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"
},
}
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.
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.
🥃 ]]>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.
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:
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.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.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:
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.
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.
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);
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:
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.
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.
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.
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
`;
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
]]>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:
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.
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.
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
.
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.
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.
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.
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.
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()
})
})
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
]]>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.
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.
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.
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.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.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(``)
}
})
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(`
First, an array is set as articleBody
. All of my feed content will get added to this array, string by string, then the array will get joined together into one big string HTML markup.
This file iterates through each matrix block and it uses the __typename
field to figure out what kind of matrix block is being rendered. Based on the type of block some other logic might be needed and the result is more string data getting pushed into the articleBody
array.
A couple of things to note here:
__typename
I am able to make sure each matrix field type is matched up with the generated types.Array.map
on my matrix blocks. This way I don’t necessarily need to render out each of my block types if there's not a good reason to in an RSS feed.So far, I’ve got my main RSS feed all set up and outputting all of the content for all of my articles. In case someone wanted to only subscribe to one of the article categories I'll need to make a few changes to make that happen.
Currently, I have my main feed in a file at ./server/routes/rss/index.ts
. Because Nuxt Server Route files can use variables in their names, I only need to add one more file that takes care of both of my article categories. I added a file at ./server/routes/rss/[articleCategory].ts
and the server directory now looks like this:
| server/
--| routes/
----| rss/
------| index.ts
------| [articleCategory].ts
This file is almost the same as index.ts
:
import RSS from 'rss'
import { ofetch } from 'ofetch'
import { rssArticleCategoryGql } from '~/gql/rssGql'
import { articleBodyBlocks, feedOptions } from '~/utils/rss'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const articleCategory = event.context.params.articleCategory
const category = {
sectionHandle: ''
}
if (articleCategory === 'code') {
category.sectionHandle = 'codeArticle'
} else if (articleCategory === 'maker') {
category.sectionHandle = 'makerArticle'
}
const feed = new RSS(feedOptions({ uri: `/${articleCategory}/` }))
const entries = await ofetch(config.graphqlEndpoint, {
body: { query: rssArticleCategoryGql(category.sectionHandle) },
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)
})
event.context.params.articleCategory
gets the category from the URL.rssArticleCategoryGql
method, instead of the previously used rssIndexGql
.To query the entries for only one article category I added a new method, called rssArticleCategoryGql
, to my ./gql/rssGql.ts
file:
export function rssArticleCategoryGql (handle = 'codeArticle') {
return `{
entries(section: ["codeArticle"]) {
dateCreated
title
uri
${elementGql({ handle })}
}
}`.replace(/codeArticle/g, handle)
}
This is almost the same as rssIndexGql
, but this also does the regex replacement for the section handle.
Just like Michael Hoffmann recommends, I added all of these RSS feed URLs to my list of pages to prerender.
I also added my feed URLs to my app.vue
file so they can be linked across all of the pages in my site:
Now I have the following RSS feeds:
https://wbrowar.com/rss/https://wbrowar.com/rss/code/https://wbrowar.com/rss/maker/
With this setup, adding more article categories in the future should be pretty painless. This setup also makes it an easy update when I decide to add a new matrix field to my Article Body field.
My TODO list: ☑️ RSS Feeds
]]>My kids got old enough where words like chores and allowance started to get thrown around in our house. My oldest started wanting to buy things on his own, but being a grade-schooler wasn't able to scrap up the funds for pretty much anything on his shortlist. We also wanted to start to instill in them that working earned you money and saving that money let you buy the things you want, as opposed to spending your allowance as soon as you got it.
This led to the kids keeping coins in their piggy banks and depositing their paper cash into their "bank accounts". By "bank accounts" I mean money that I held onto and kept note of, adjusting the amount on record as we spent or "deposited" it.
The fist place where this info was kept was in the trusty iOS and macOS app, Notes. What I liked the most about storing it in Notes was that it was synced via iCloud so I had the ability to check in on the balance across devices.
This worked fine for keeping the total amounts for each kid, but I wanted a better way to show the kids how saving money (or spending it) affected their balances. I was also on an iOS Shortcuts kick so I started to think through things like making it easy to deposit money and to keep a ledger in another cloud-synced format.
I learned that Shortcuts had an integration with Apple Numbers, and although limited, it did let me construct and add a row of data to the top or bottom of a worksheet. For my initial needs this worked and I was able to put together a shortcut that let you add balance adjustments for each kid and use one button to add their set allowance to the balance.
Then I came across this tweet by Val Katayev which introduced me to the concept that in lieu of an allowance that is likely to be spent as soon as it's given, you could give your kids compounded interest that encourages them to save money and be more decisive about when and what they spend their money on.
I thought that if I could add this as an option to my shortcut I could try it out and see how it goes. This wound up being even more challenging than I expected. I used some ideas from Val’s spreadsheets, but wound up coming up with my own formulas in a way that allowed my shortcut to add a new adjustment row within the existing setup.
Then I hit a bug within Shortcuts and Numbers. For some reason it would get confused and duplicate data and spread them across incorrect columns in a row. I tried all of the workaround I could think of. I also found it very hard to Google terms like "Numbers" and "Shortcuts" to get any meaningful results. When I did find results, there weren’t enough people discussing the particular issue to guide me or make me aware exactly what the issue was.
At the point of giving up on this idea I thought that I could probably take a few hours to put together a web app that accomplished all of this in a platform that has all the flexibility. So I got started on a personal app I call Kids Money.
As of last year I moved on to work as a front-end engineer in a company that had already worked out the tech stack we use on a daily basis. While I’ve been able to keep up with some my favorite frameworks, like Vue and Tailwind, I haven’t had a practical use for Nuxt in a little while. In November 2022 the team behind Nuxt announced the stable release of Nuxt 3 and as a fan of all of their work, I really wanted to give it a try. My first thought was to upgrade the front-end for this blog, as well as upgrade my CMS from Craft 3 to Craft 4, however, there are so many new concepts in Nuxt 3 that I wanted to wait until I knew more about using Nuxt 3 before I started working with it.
While going through the Mastering Nuxt 3 course and casually reading the docs, I started to get familiar enough that I thought that this banking web app could be an easy place to start diving in. I decided that I'd start by doing the app in the way I'd do it if I were working in Nuxt 2, while keeping going with learning Nuxt 3 and refining the app as I learned more about the framework.
In the past I've only ever worked with Nuxt for its static generation. This made sense on some of the marketing sites I’ve worked on but it’s not the best tool for a web app that is all about real-time interactivity. Along with learning Nuxt 3 I wanted to dive into SSR—which Nuxt makes easy right out of the box.
So the front-end framework was decided, but I also needed to think about data storage. Without access to iCloud I had to think about what options I had. I was looking to put together something that was easy to set up and independent, so I didn’t want to tie it to my website’s CMS or database. I could look into creating Lambda Functions (or more likely Netlify Functions) to securely interface with a database on my VPS, but realizing that we already need to use Node on the SSR end, I wanted to see what I could do in the same environment that runs the SSR server. A friend of mine reminded me of SQLite, a technology I used back in my Flash days, and that felt like the best balance in between a static JSON file and a MySQL/Postgres database.
Using SQLite locally was the fastest option, but it also made hosting a little more complicated. Hosting on the edge at places like Netlify meant that local persistent storage wasn’t going to be a thing. If using Netlify was a requirement I would have changed the DB setup, but in the case where I already have a VPS handy, I decided to stick with the SQLite route on the VPS, but make it easy to point the app to a different data storage place if I ever wanted to move i.
As expected, the installer for Nuxt 3 made getting things up and running quickly in a smooth and painless process. Within maybe an hour I had nuxi
(the Nuxt 3 CLI) scaffolding out my layouts, pages, and some composables. I had also created an error.vue
page and I started down the road of using useFetch
to test out the endpoints I had created in server/api
. Here are a few other notes about working with Nuxt 3:
<Head>
and meta tags that made it so a lot of the page-specific stuff I could keep in the template.nuxt dev
a lot, but thanks to Vite, getting back to development is almost instantaneous. Also, Nuxt is very cool about what things need a restart and what don’t. Lots of minor config changes can happen and you can keep moving..eslintrc.cjs
file to extends: ["@nuxtjs/eslint-config-typescript"]
I don’t recall really having to tweak anything. Getting used to semi-colon-less JavaScript took about a day and I got over it (again, to my surprise).runtimeConfig
and app.config
options to be a bit confusing at first. Just like a lot of things in Nuxt, knowing what works on the server versus the client is a gotcha the takes some getting used to. Eventually I figured all of that out.useFetch
in a composable to make it easy to share data across components, but I found it was easier to repeat similar useFetch
calls in each page template—despite the duplication.git
history to find a lot of mistakes that I made. I guess the lesson here is that I should just stop making mistakes 😆As local storage was an important part of the app, I went to test out the SQLite implementation before getting too far into development.
I started things out by trying out an NPM package, called node-sqlite. I figured I’d use Nuxt’s server API endpoints to work with SQLite via Node, and I had gotten things working quickly enough by following along with the docs. Where I got held up is that, just like with using SQLite, I haven't written a SQL query in at least a decade and had a lot to remember. I have a friend who is a master at SQL so I was ready to tap into his experience, until I came across Prisma.
Prisma is exactly what I was looking for and it is the perfect tool for someone like me who has had experience working with databases, but more on the DevOps level. Just like with Nuxt 3, I learned a few things along the way:
utils/prisma.ts
to define a PrismaClient for use across the entire app.server/api
endpoints or from specific node scripts that you could run via SSH. It all just worked, even in both contexts.CREATE TABLE
queries (which I don’t remember ever being good at)..env
file. Because I was using pm2
on my VPS I was able to add the variable there and it was picked up by Prisma.It took a little bit of learning, but overall I was able to throw together the MVP for this app within a couple of weekends. I’d still consider this a work in progress and I plan on iterating on it over time, but for now it's working the way I want it to and I think I have a good grasp on how Nuxt 3 works.
I knew I wanted to create the ability to have both admin and non-admin users, so I needed some sort of login page for the app. I spent the least amount of time I could on the admin page and thanks to Tailwind UI and HeroIcons, all of the work on the page went to the actual process of submitting the form and logging the user in.
One important note here is that this is absolutely not an example of what you’d do in a real application. Here I am passing login credentials along via a POST request, but I didn’t take the time to set up any advanced security. The stakes of this app are so low and I figure that if my kids learn enough to hack the login page or update their users to become admins... then maybe they deserve a little extra allowance 😜
When you first log into the app there’s not much you can do but by using the navigation in the top-right you can visit the Settings page to add your kid’s information into the system. At the very least a Name and Slug are required. From there adding a Photo URL or by changing the color you can customize the display for each kid. Finally, you can set amounts for an allowance or interest, which can be added to the kid’s balance either by pushing a button or by using the CLI to trigger a node script.
The home page in the app is an overview of all of the kids added to the system, their current balances, and a graph of their past end-of-the-month totals. The graph uses Chart.js to show the data for the past 12 months.
I got to play around with Tailwind’s Container Queries plugin for the kid summary cards and I’m only more excited about the fact that container queries are coming to CSS. For component-based development, it’s an absolute game changer and Tailwind’s current implementation makes things easy to use and to understand.
I also made it really easy to log in and add or remove money from the account right from the homepage. As an admin you can click on the total and an input field and a series of buttons let you either add or subtract a specified dollar amount, add an allowance, or add interest.
Clicking on the name of a kid takes you to their adjustments page. This shows you all of the adjustments made in the past year and when the adjustment was added. When logged in, kids can go to this page and see what made their balance rise (or fall). Admins can also use the same controls from the home page to add adjustments and see them appear right away.
I added a few node scripts to manually make changes to users and the kids’ balances via the command line. Setting up the CLI was a matter of creating node scripts that checked for and used arguments that interact with the database via Prisma.
Here are a few examples:
node scripts/create-user.mjs -u=my-username -p=my-password -admin
node scripts/add-adjustment.mjs -kid=kid-slug -dollar=5
node scripts/add-adjustment.mjs -kid=kid-slug -interest
With all of this, I was able to set up a cron job on my VPS that adds daily compound interest for my kids. I set them up with non-admin accounts so they can log in and check to see what their savings looks like after interest has been added.
I hope to add a few more features, but for the most part the MVP meets all of my personal criteria. At this point we need to live with it to see if anything needs adjusting.
The repo for this project is open source so other folks can take a look and maybe even try it out for themselves:
https://github.com/wbrowar/kids-money
It was a fun learning experience for me and I hope it will provide a good example for other folks getting into Nuxt or Prisma. 💸
]]>Guide 1 included three widgets that got split out into their own plugin, called Communicator, when Guide 2 was released. The idea behind these widgets were that you could edit them in one place and all user Dashboards that included them would get updated immediately—as opposed to going user by user and updating widget content in each widget’s settings.
Guide 3 covers this functionality in that you could create a guide, add it to the Widgets CP area, and a user could add it to their Dashboard. Guide 3’s version is a little more advanced in that you can create the whole widget using Twig and you could also turn the widget content into it’s own page in the CP (Craft’s Control Panel).
One of the three widgets in Communicator was a Changelog that followed Craft’s plugin format, showing features that were added, changed, and deleted. The idea is that as you maintain your Craft project—doing things like changing out fields and sections—you could update the changelog with info that’s relevant for content authors and other developers on the project.
Let’s see if we can create a changelog Guide widget and CP page that is easy to keep up to date.
To start, go to the Guide Organizer page and click on "+ New Guide" to head to the Guide Editor. Here we can give it a name and let that generate the slug.
NOTE: To make it easy to see your changes as you go, save the guide and then in the Organizer click the View button to go to the guide’s standalone CP page. From there you can hit Edit and Save, as needed, to see your page come together as you code it.
The game plan will be that we’ll be creating a changelog note, store it into an array, then depending on whether you’re viewing it on the Dashboard or the page we'll show the appropriate amount of notes.
To start, let’s create a div
and give it a class, called version
. This will be the wrapper for each update.
The first thing we want to display is the version number and the date. Feel free to play around with your own styling here, but if we want to do something similar to the Communicator setup, we’ll add this, <h2 class="g-mb-6"><span>1.0.0</span> Aug 27, 2021</h2>
, so our code will look like this:
1.0.0 Aug 25, 2021
This is simply an h2
with a span tag around the version number. The class on the h2
, g-mb-6
, is a Tailwind CSS utility class built into Guide. If you’re unfamiliar with Tailwind, this is can be read as: g-
(a prefix to avoid collisions) mb
(margin-bottom) -6
(a Tailwind spacing unit similar to Craft’s default spacing)
The version number and date will start out the same size, but we can add a CSS block to style that to match the Communicator styles:
1.0.0 Aug 25, 2021
{% css %}
.guide-{{ guide.slug }} .version h2 span {
font-size: 2rem;
}
{% endcss %}
This is a standard Craft css
Twig tag. In it we’re starting off with .guide-
then the slug of this guide. By doing it this way the slug will stay up to date if you decide to change the slug at a later point. Also, by included the slug here, we can make sure other guides aren’t affected by our custom styling.
Guide’s utility classes are meant for layout and box model properties, so we’re using this CSS block to change the font size of the span
and then letting the date get styled by Craft’s default h2
style.
From here we have a header and now we need some content. This is where a Markdown component can help. In the Guide Editor, click on the "+ Add" button for the Markdown component to add a markdown block after our header:
1.0.0 Aug 25, 2021
{% filter markdown('gfm') %}
## Heading
Content
{% endfilter %}
{% css %}
.guide-{{ guide.slug }} .version h2 span {
font-size: 2rem;
}
{% endcss %}
At first this might look wrong, but because Markdown is sensitive to spacing we need to use shift+tab
to outdent the Markdown content all the way to the left.
In here we can start to add our headers and unordered lists that describe our changes. The Communicator Changelog used emojis alongside its headers, so we’ll use the Unicode symbol for the 🚀 rocket, 🔧 wrench, 🚧 construction, and 🔥 fire emoji so they can safely be saved in our database:
1.0.0 Aug 25, 2021
{% filter markdown('gfm') %}
### 🚀 Added
- Features that have been added.
### 🔧 Changed
- Things that have changed.
### 🚧 Fixed
- Bugs that have been fixed.
### 🔥 Deleted
- Features that have been removed.
{% endfilter %}
{% css %}
.guide-{{ guide.slug }} .version h2 span {
font-size: 2rem;
}
{% endcss %}
If you were to save this and take a look you’d see that you could just copy and paste this to create your changelog page and call it a day. That might be fine for a CP page, but we wouldn’t want to show all of your updates on our Dashboard, right?
So we need to make this repeatable and iterable. To do that lets wrap all of this in a set
tag, along with some delineating comments, so we can set it to a Twig variable then push that variable into an array:
{# VERSION START – For each version copy this block and fill it out. #}
{% set version %}
1.0.0 Aug 27, 2021
{% filter markdown('gfm') %}
### 🚀 Added
- Features that have been added.
### 🔧 Changed
- Things that have changed.
### 🚧 Fixed
- Bugs that have been fixed.
### 🔥 Deleted
- Features that have been removed.
{% endfilter %}
{% endset %}
{% set versions = versions is defined ? versions|merge([version]) : [version] %}
{# VERSION END #}
{% css %}
.guide-{{ guide.slug }} .version h2 span {
font-size: 2rem;
}
{% endcss %}
You might be questioning the version is defined
ternary operator here. You could set a blank versions
array at the top and just do a merge after each note, but it’s then something you need to avoid when copying and pasting to create a new version note. By doing it this way we can update our changelog by copying everything within the latest VERSION START
and VERSION END
comments, then pasting that at the top of our guide as self contained units.
Whichever method you decide to go with is totally fine as long as the concept of wrapping everything in a variable and pushing it into a versions
array remains.
So far we have a way to create version notes, now let’s display them. To make it easy to see where the data ends, let’s add a comment with a bunch of hyphens in it—to say, "everything above this line can be edited, but leave the stuff below it alone, bub".
Next we’ll add a plain div
wrapper for our version note elements so we can avoid running into Guide’s default spacing between top-level elements.
In here we can go ahead and loop through our version notes and output them all.
{# ... (version blocks up here) ... #}
{# -------------------------------------------------------------------------- #}
{# Display changelog versions #}
{% for version in versions %}
{{ version }}
{% endfor %}
{% css %}
.guide-{{ guide.slug }} .version h2 span {
font-size: 2rem;
}
{% endcss %}
Now we have all of our version notes showing and we might want to add a little space between them. Back down in our css
tag we can add some CSS to put margin, padding, and a border between each version element—as long as more than one version is shown:
{# ... (version blocks up here) ... #}
{# -------------------------------------------------------------------------- #}
{# Display changelog versions #}
{% for version in versions %}
{{ version }}
{% endfor %}
{% css %}
.guide-{{ guide.slug }} .version + .version {
margin-top: 3rem;
padding-top: 3rem;
border-top: 1px solid rgba(0,0,0,0.2);
}
.guide-{{ guide.slug }} .version h2 span {
font-size: 2rem;
}
{% endcss %}
If you save this again and check out the CP page, we’re all set there. So the last thing to do is make it so when this guide is added to a Dashboard widget we only see the top-most version note.
Guide comes with a helper variable, guideDisplayArea
, that’s value will change based on where the guide was placed in the Organizer. So in our case, we can check to see if guideDisplayArea
is set to "widget"
and then display just the one version post. In addition to that, we can add a Guide button component that creates a link from our widget to our changelog CP page.
After adding that, the final code for our guide looks like this:
{# VERSION START – For each version, make a copy of this block and place it at the top of this guide. #}
{% set version %}
1.0.0 Aug 27, 2021
{% filter markdown('gfm') %}
### 🚀 Added
- Features that have been added.
### 🔧 Changed
- Things that have changed.
### 🚧 Fixed
- Bugs that have been fixed.
### 🔥 Deleted
- Features that have been removed.
{% endfilter %}
{% endset %}
{% set versions = versions is defined ? versions|merge([version]) : [version] %}
{# VERSION END #}
{# -------------------------------------------------------------------------- #}
{# Display changelog versions #}
{% if guideDisplayArea == 'widget' %}
{{ versions[0] }}
{% else %}
{% for version in versions %}
{{ version }}
{% endfor %}
{% endif %}
{% if guideDisplayArea == 'widget' %}
{{ craft.guide.component('button', { label: 'See all changes', url: url('guide/page/' ~ guide.slug) }) }}
{% endif %}
{% css %}
.guide-{{ guide.slug }} .version + .version {
margin-top: 3rem;
padding-top: 3rem;
border-top: 1px solid rgba(0,0,0,0.2);
}
.guide-{{ guide.slug }} .version h2 span {
font-size: 2rem;
}
{% endcss %}
Our final page and widget should look something like this. This changelog setup will become a Snippet in an upcoming version of Guide 3, but in the mean time hopefully this article showed you a few things that you can use to create other useful tools within your Craft CMS projects.
Craft’s Plugin Store is chock full of great plugins these days, but quantity isn’t a great metric if plugins aren’t supported and kept up to date (ask our friends in Craft’s Discord and on Twitter how they feel about lapses in plugin support). Over the course of the past year I've had a hard time keeping up with many things—leaving side-project plugins at a very low spot in my priority list. Both plugins currently work, but they weren’t getting proper updates and timely TLC.
Since there’s so much overlap between Guide 3, Communicator, and Content Stats, I decided that the better thing to do is put my time into making Guide flexible enough to cover Content Stats and Communicator’s use cases. As a side benefit, the Snippets feature in Guide makes it crazy quick to create a content stats or changelog widget that has the added benefit of letting you tweak its output to you liking:
Farewell, little plugins.
]]>Guide 1 was built to replace your inkjet-printed CMS manual with Twig templates displayed in Craft CMS’s Control Panel (CP). Guide 2 let you edit guides in a code editor and place them in Dashboard widgets and on edit pages. Guide 3 sets out to help you help your content authors by making it easy to put useful information anywhere throughout the CP. Anywhere.
Guide has been rebuilt to be less about that single place to get your CMS manual content (although the main Guide CP section is still there). Guide 3 lets you log in, create a new guide, use it to create a widget and a CP page, as well as display it on pages like the Entries listing page or on a plugin settings page. Guide 3 includes snippets that can be used as a starter to create an internal entries search or to look for images that don't have a focal point set on them, then place that content on your entry and asset edit pages.
Guide 3 is a re-focusing on making every part of the plugin faster and easier—fixing a few Guide 2 workflow issues and removing features that I've heard don’t offer as much value as I had originally hoped. I've been using it over the past couple of weeks and here are a few of the changes that I hope you'll enjoy using.
In Guide 3, the Organizer has been completely rewritten. Instead of moving a guide from the Available Guides column to its new home in the Craft CP, the Organizer in Guide 3 keeps your guide where it is and lets you drag-and-drop it to as many areas around the Craft CP that you’d like.
Here are a few other changes that the new Organizer brings:
Each Guide automatically gets its own page in the CP where you can link directly to it and treat it like a standalone CP page. If you would like to create a traditional CMS manual that contains a selection of guide pages, you can drop multiple guides into the Guide CP section homepage.
A navigation will appear in the top-right when more than one guide is dropped into the same area in the Organizer, so your CMS manual is easy to navigate.
The modals and sidebar buttons on Entry, Asset, Category, User edit pages have been removed and guides are now located at the top of the page* so content authors don't miss out on important information and instructions. On Edit pages, guides include an element
(or user
) variable, letting you tailor the content of your guide to the specific entry or asset your are editing. Guides can now be dropped onto Global Set edit pages, too.
*If it feels like it’s in the way, a Guide plugin setting can move them to the bottom of edit pages, instead.
Instead of Guide 2’s setup where you selected a guide within a UI element and saved that selection in the Project Config, you can use the Field Layout Designer to place a Guide UI element as a placeholder. When you visit the edit page where the UI placeholder lives you can now select the guide that you would like to be displayed there.
Changing the selected guide is as easy as removing the current selection and then picking another.
In addition to widgets and these other CP areas, you can add guide content to any other CP page. You can set the URI of a specific CP page and use a CSS selector to move your guide content into unique areas around the CP. With a little bit of CSS—added in your guide’s Twig template—you can make your content fit in and find unique ways to help your content authors.
In a Ted Lasso voice:If you wanted to go ahead and put a guide about writing guides on the Guide settings page. Well, I guess you can go ahead and do that.
The Guide Editor has been updated with more components so a new set of tabs sits in the top-left to make it easier to find and embed images and other guide content into a guide.
The Guide Editor now includes documentation for each component to describe its use and to let you know what arguments can be passed in to each component.
A new addition to the Editor is a tab, called Snippets. Here you can find dynamic and functional templates that you can use as starters for useful widgets, CP pages, and other guide content.
The Twig code here is meant to be customized and updated so it's written in a way that is meant to be understandable and easy to cut and move pieces around.
For example, there’s a snippet, called Low-Res Image Check, that’s meant to help you find and update images that aren’t big enough or else they may get upscaled or skipped during image transforms. The code it outputs looks something like this:
{# Set the asset volume you would like to check for images in. #}
{% set volume = null %}
{# Set the width to the smallest size that an image should be uploaded. #}
{% set width = 500 %}
{# Display a list of invalid images and instruct authors on what size is recommended. #}
{% cache %}
{# Find all images within the targeted asset volume that are not wider than the "width" value. #}
{% set assets = craft.assets.volume(volume ?? null).width('< ' ~ width).kind('image').all() %}
{% if assets|length %}
{% filter markdown('gfm') %}
## Images that are too small (less than {{ width }}px wide)
These images should be replaced with a .jpg that is at least {{ width }}px wide.
{% endfilter %}
{% for asset in assets %}
{{ craft.guide.component('button', { attrs: { class: ['submit'] }, label: 'Edit Image', url: asset.cpEditUrl }) }}
Title
{{ asset.title }}
File name
{{ asset.filename }}
Width
{{ asset.width }}px
{% endfor %}
{% endif %}
{% endcache %}
You might want to put this on a standalone CP page, or put this in a widget on the homepage of the person most likely to jump in and replace the images.
While there is a setting to restrict this query to a specific volume, you can see that it's just a regular craft.assets
query, so you could tweak that to be even more specific.
If you want to help content authors understand the context of these assets, you could even add a craft.entries
query with a relation back to each image so you can display links to the pages they belong on.
For all of the times I’ve spun up Plugin Factory to create a one-off custom module, I hope to be able to whip up things like this and publish them just as easy as it it to create new entries in Craft. Hopefully this makes creating something like this more accessible to folks who prefer not to dive into the PHP inner workings of Craft and Yii modules.
Guides are based in Twig and you could get away with doing everything in Twig and CSS (using the {% css %}
tag). But if you want to get a little fancier, a small subset of Tailwind CSS is baked into Guide that focuses mainly on layout classes (flex, grid, position, box properties). It includes a few responsive variants to make it easier to create guides that are both mobile and desktop friendly. The code editor in Guide is set up to autocomplete for all of the prefixed utility classes, so start typing g-
will automatically fill in the supported classes.
Vue.js is also used included so if you have a need for some extra interactivity you can set and retrieve variables in Vue’s template syntax. There are a handful of number, string, and boolean variables available to give you a little state to work with when you need it.
All of the features above are available in the PRO edition of Guide. The LITE edition uses some of the new Guide UI, but otherwise it’s unchanged from Guide 2—meaning you can create a CMS user manual in the Guide CP section using templets in your templates directory for free.
The PRO edition can be purchased in the Craft CMS Plugin Store and for the first 3 weeks, it'll cost $33.
I hope that folks who already use Guide find these new features make their experience much nicer and I am hoping that even if you don't need a traditional user manual for your projects, that Guide will offer other ways to connect your Craft users with helpful information.
]]>The other day my friend, Mike, threw out an idea: knowing that he and I were going to eventually 1. find out the vaccine is available to us, which would cause each of us to 2. spend all day checking the appointment availability in our areas. He had the idea of creating some sort of bot to check the local vaccine eligibility page throughout the day and send out an email or some sort of notification when appointments were available in our area.
Mike has a background in databases and PHP, so he had a good grasp on what had to get done, but asked me for some advice on how to put it all together. Together, we went from this idea to programming our own personal appointment trackers using our iPhones and a couple of free apps.
Mike and I both live in Western New York, so our main resource for vaccine availability was this page on the New York State website. While doing a little digging, Mike found a JSON endpoint that is used by this page to provide locations and their appointment statuses. Scraping this page would be possible, but reading from a JSON feed makes things much easier.
While thinking of all of the ways this bot could be accomplished, it dawned on me that we could use a brilliant iOS app, called Scriptable, and skip the whole server and app setup altogether. In a nut shell, Scriptable lets you use JavaScript syntax and some built-in APIs to pull off some really impressive automation that could otherwise be impossible via iOS’s Shortcuts app. It even integrates with Shortcuts and has lots of other really nice ways to make the scripts you write available throughout the OS.
I wrote the initial script in JavaScript and Mike and I refined it along the way. The end result displays something like this:
It’s nothing super fancy, but it does the trick. The script makes a request to the JSON feed, filters the results down to a list of specific locations, determines if there are available appointments in one of those locations, then sends a local notification with the results.
When the notification pops up, tapping on it takes you to the NYS eligibility page so you can begin the process of setting up your appointment.
The script we used here is specific to NYS and their eligibility website, however, the process here could be replicated for other locations, provided that you have the data available in some form that can be read and parsed in JavaScript. Should you want to try to set something like this up(* see my caveats below), here’s how we did it:
First, you'll need to download Scriptable from the App Store. It’s free, but if this works out for you and you want to send the developer a tip, you can do so right within the app.
Once you have it installed, create a new script and copy and paste this code in:
/*
* If true, send notification even when no
* locations have appointments.
*/
const alwaysNotify = true;
/*
* Log steps to debug in Scriptable
*/
const debug = false;
/*
* Limit results to only specific locations
* when onlyMyLocations is true.
*/
const myLocations = [
"Henrietta, NY",
"Buffalo, NY",
"Syracuse, NY",
"Utica, NY",
];
/*
* Filter locations to the list in myLocations.
*/
const onlyMyLocations = true;
/*
* Request appointment data from the website.
*/
const request = new Request("https://am-i-eligible.covid19vaccine.health.ny.gov/api/list-providers");
const response = await request.loadJSON();
if (debug) {
log(response.providerList);
}
/*
* If onlyMyLocations is true, filter all of the
* data down to only the addresses in myLocations,
* otherwise use all of the locations.
*/
const myLocationsData = onlyMyLocations ? response.providerList.filter((location) => {
return myLocations.includes(location.address);
}) : response.providerList;
if (debug) {
log(myLocationsData);
}
/*
* From the results of myLocations, find
* the locations where appointments are available.
*/
const availableLocations = myLocationsData.filter((location) => {
return location.availableAppointments === "Y";
});
if (debug) {
log(availableLocations);
}
/*
* Set a notification body based on whether
* or not there are results.
* If alwaysNotify is true, a fallback message
* will be sent when no appointments are
* available.
*/
let body;
if (availableLocations.length) {
body = "Locations:";
for (let i=0; i
At the top of this script you’ll see a few variables that are set to some sane defaults and they give you a quick way to customize the experience. The one you will want to edit is myLocations
. This lets you narrow down the list to specific areas where you would go to get the vaccine. In my case, I left a few extras in there for testing.
When updating the items in myLocations
it's important that the spelling and case match that of the address
values in the JSON feed:
{
"providerList": [
{
"providerName": "SUNY Polytechnic Institute - Wildcat Field House",
"address": "Utica, NY",
"availableAppointments": "NAC"
},
{
"providerName": "University at Buffalo South Campus - Harriman Hall",
"address": "Buffalo, NY",
"availableAppointments": "NAC"
},
{
"providerName": "Rochester Dome Arena",
"address": "Henrietta, NY",
"availableAppointments": "NAC"
}
... other locations
],
"lastUpdated": "2/16/2021, 8:03:33 PM"
}
Once the script is customized to your liking, you can pick an icon and give it a name in your script’s settings. It can be added to your Home Screen or ran within Scriptable, however, Mike and I opted to automate it. More on that after these fine disclaimers:
*Disclaimer: you many use this script, provided as-is, however you’d like, using your own discretion. Ideally it can help in alerting you when and where vaccine appointments are available, but it doesn’t replace getting appointment information from an official source. Also, I realize there is no error handling or sanitization going on here, but for the scope and the life of this script, I’m not too worried about that.
If your iOS device is up to date, you should have the Shortcuts app installed by default. In the app, you can set it up to interact with your Scriptable script on demand or via Siri, but to automate running this script you can follow these directions:
If all goes well you should see the script run at the time you chose. If you were thinking you’d check the eligibility website several times a day, you can repeat this process to set up multiple automations to run throughout the day.
During this process I've observed two things:
First, there’s irony for me in that with the lack of free time I’ve had due to COVID-19 meant that I haven’t taken the time to really do any recreational coding. It was nice to collaborate and solve a problem with code, without it being a huge undertaking or time commitment.
Second, having gone to some extreme lengths to secure the purchase of a PS5, I’ve learned that it helps to use technology to your advantage. In this case we’re not talking about getting an alert the moment the appointment page gets updated, but by automating this task I can carry on with my day and let my script do the work of checking for me.
If you wind up using this script, improving on it, or developing a version for an area outside of New York, I'd love to hear how you found it helpful. Either way, let’s get vaccinated so we can all get back out there. 💉🦠
]]>My role in the site started off as the friend who traveled around the country with the crew, occasionally taking photos for events we covered, to taking over design and development of the site so the Mikes could focus on creating content and organizing events and other initiatives. A few years ago we transitioned the site into a Craft 2 website during our final front-end redesign.
As posting came to an end, and as spammers became the main users for the site’s contact form, we decided that the CMS was no longer needed and a good way to keep the site alive would be to transition the articles on the site into an archive using a static site generator. While there are a lot of options out there, a new setup came along that fit the sites needs in a way that was quick to get up and running.
Nuxt Content is an extension to Nuxt.js that allows you to fold file-based content into a Nuxt.js site. It makes it easy to create a blog via Markdown, or to drive content using JSON files (as well as with a couple other file types).
After playing around with the Content module, it seemed like the right plan of action looked like this:
static
directoryA typical article included a title, a category, the category description, the article body itself, and a byline.
While some articles included a featured image, the masthead of the site usually displayed a random image from a selection of photos shot for the site at various events.
The rest of the page included a main navigation, search box, and a footer that linked to a few secondary pages.
In thinking ahead for SEO purposes, I would need to include content driven by SEOmatic for the main page image and description.
{% spaceless %}
{% if entry.body.type('image').first() ?? false %}
{% set seoImage = entry.body.type('image').first().file.first() %}
{% else %}
{% set seoImage = craft.assets.source('headerPhotos').assetEnabled('1').order('RAND()').first() %}
{% endif %}
{% set data = {
title: entry.title,
category: craft.request.getSegment(1),
slug: entry.slug,
uri: entry.uri,
description: seomaticMeta.seoDescription,
image: seoImage.url|replace('http://acb-craft-archiver.test:80', ''),
postDate: entry.postDate|date('Y-m-d H:i:s'),
formattedDate: entry.postDate|date('F j, Y'),
body: include('partials/post', { categorySlug: craft.request.getSegment(1), entry: entry })|replace(siteUrl, '/'),
} %}
{{ data|json_encode|raw }}
{% endspaceless %}
The page template that all posts were pointing to now looked something like this. Here I'm generating an object from the CMS via Twig then encoding it to JSON and printing it onto the page. Since there is no <html>
or <body>
tag here, the only text on the page will be the valid JSON data.
The value of the body
was made up of a string that included all of the rendered HTML for the article using a Twig partial I had already had in place.
In a couple of spots I used a Twig replace
filter to change URLs from absolute URLs to relative URLs that worked with my Nuxt router patterns.
With the page template modified, I now had to get the content out of the CMS.
The next step was to come up with a way to get all of this JSON from each page and save it to files in the new Nuxt content
directory.
This was one of those situations where I could have taken the time to research the popular Node-based scrapers or I could have used this as an opportunity to try out something like Deno, but I knew exactly what I needed to do and I knew how to build it.
I threw together a tiny Node module that takes a list of URLs from a JSON file, pulls the rendered JSON templates for each URL, then writes it out to a local .json
file.
To get the list of URLs, I went back to my local version of the Craft 2 site and created another template. This time it didn't matter where the template was as I was just dropping in some Twig code to loop through all of the entries on the site and generate the list of source URLs and destination URIs for each entry.
[
{% set entries = craft.entries.section('article').limit(null) %}{% for entry in entries %}{
"dest": "posts/{{ entry.uri }}",
"url": "{{ entry.url }}"
}{% if not loop.last %},{% endif %}{% endfor %}
]
The resulting code looked something like this:
[
{
"dest": "posts/open-letters/farewell-from-the-a-cappella-blog",
"url": "http://acb-craft-archiver.test:80/open-letters/farewell-from-the-a-cappella-blog"
},{
"dest": "posts/tuesday-tubin/icca-champions",
"url": "http://acb-craft-archiver.test:80/tuesday-tubin/icca-champions"
},{
"dest": "posts/recording-recommendations/legalities-and-referrals",
"url": "http://acb-craft-archiver.test:80/recording-recommendations/legalities-and-referrals"
},{
"dest": "posts/tuesday-tubin/house-of-memories-and-safe-and-sound",
"url": "http://acb-craft-archiver.test:80/tuesday-tubin/house-of-memories-and-safe-and-sound"
}
]
After a little trial and error I used the scraper to pull the content for all of the URLs and placed the resulting JSON files into the content
folder of my Nuxt site.
Pulling the JSON content into my Nuxt page was straightforward from here. I set up a dynamic route to match the /category/slug
pattern and passed in the resulting category
and slug
route parameters into my Nuxt Content lookup.
For the header image, I created a HeaderImage
component that randomly picks a srcset from a collection of images.
I create an ArticleBody
component that handles styling the legacy markup wherever it is used. This was also used for the one-off pages, like About and the Book page.
Finally, a helper function is used as a template for my SEO meta data, based on those fields brought in with the single page JSON.
After being rendered by Nuxt.js, an unstyled page looked something like this:
Styling the site could have been as easy as copying and updating the rendered CSS and including it in my Nuxt site, but the design of this site is so simple that I grabbed the the color palette, custom fonts, and SVGs from the old site and worked them into my Tailwind-based CSS scaffolding. Without making too many configuration changes, I had most of the styles in place within a matter of hours.
I used this as an opportunity to simplify the UI in some places, so things like the main navigation went from a hamburger-based overlay to a horizontal scrolling list on mobile devices.
One of my favorite design features from the site was the categories menu, so I found it very easy to recreate it in Tailwind, along with just a few extra lines from the old CSS.
While it wasn't something I was worried about during the original build of this site, my CSS scaffold has a real easy way to enable dark mode so I applied it here:
When designing the site back in 2016 filter: blur();
was a relatively new property and I used it for an effect that was meant to make the main navigation sidebar look like it was sand blasted and semi-transparent in front of the header image at the top of the page. To do this back then it looked something like this:
background-postition
(this made it so you could focus on a person’s face as the responsive design cropped the image in different ways).background-position
based on the position of the main navigation and on the height of the viewport and make sure it didn't shrink or get distorted so it matched the header image next to it.filter: blur();
to the image for the blur effect.This looked something like this:
To do this using modern CSS, I simply added backdrop-filter: blur(6px) saturate(150%);
to the main navigation sidebar:
When you look closely at the two images, the new way even looks better (in my opinion) in the way that the browser blurs and apply the effect to the elements behind the navigation sidebar. There is no halo effect because filter: blur();
had to be done on the element that contained the background image instead of on the background image itself.
Because I was using Tailwind, I created a set of utilities so I could enable the backdrop-filter
using the class, backdrop-blur-6
:
function({ addUtilities }) {
const newUtilities = {};
for (let i = 0; i < 11; i++) {
newUtilities[`.backdrop-blur-${i}`] = {
backdropFilter: `blur(${i}px) saturate(150%)`,
};
}
addUtilities(newUtilities, { variants: ['responsive'] });
The archive site is now live and can be seen at acappellablog.com. In the future it could be nice to clean up some of the old HTML for errors and to make some changes for better accessibility (something that I wasn’t as attuned to during the original development). Thinking about it, the more I introduce Javascript into my front-end rendering workflow, and the more sites are interactive and dynamic, the harder it may become to simple scrape a site’s HTML and do what I've done here.
At least it’s a good reminder to keep simplicity and well-crafted markup in mind as I’m working on new projects.
]]>Version 2.13.0 brings full static generation, which makes it easier to host a Nuxt.js app on static hosts, like Netlify or Vercel. Preview Mode was added so you can use features like Craft’s Live Preview with your statically generated Nuxt.js app. Also, a new way to handle environment variables was added to the Nuxt.js config that allows for better security.
My personal website is very much a work in progress and while there's certainly much more to do, I was able to get Live Preview working so I could write articles like this one using Craft and Nuxt.js together. Here's my current solution for live preview, but I’d happily welcome suggestion and improvements.
In general, Nuxt.js has made setting up Live Preview very easy, but during the process of figuring it out, I got caught up on the issue of hiding my API secrets. When running nuxt generate
environment variables are consumed server side but they aren't available client side unless you make them public (more on that below). This meant that during a live preview update, the environment variables that connect Nuxt.js to my GraphQL endpoint were coming up blank and API requests couldn’t be made.
I found a workaround for this but this led me to setting up two front-ends: this production site and a separate preview site. It would be great to live preview from the live site, so if I can figure out a more elegant solution in the future I'll update this post. Luckily all I had to do was set up an extra environment variable and add a new branch to git to make deploying to both sites very easy.
Speaking of environment variables, let’s set one up and get started.
# Enable Live Preview
# Turn this off in Production
LIVE_PREVIEW=true
I set up a LIVE_PREVIEW
variable and set it to true
on my preview site and false
on my production site. This variable comes into play when using Nuxt.js’ new runtime config settings in nuxt.config.js
:
privateRuntimeConfig: {
craftApiUrl: process.env.CRAFT_API_URL,
craftAuthToken: process.env.CRAFT_AUTH_TOKEN,
},
publicRuntimeConfig: {
livePreview: process.env.LIVE_PREVIEW === 'true',
craftApiUrl: process.env.LIVE_PREVIEW === 'true' ? process.env.CRAFT_API_URL : '',
craftAuthToken: process.env.LIVE_PREVIEW === 'true' ? process.env.CRAFT_AUTH_TOKEN : '',
},
In privateRuntimeConfig
my Craft API URL and GraphQL authorization token are set. Variables set in privateRuntimeConfig
are only available during build time, and they’ll be used to statically generate my site’s pages during nuxt generate
.
In publicRuntimeConfig
I'm setting a livePreview
boolean that will be used at runtime to determine if live preview should be enabled. I'm also setting my Craft API URL and GraphQL token again, but only when LIVE_PREVIEW
is set to 'true'
. This allows Nuxt.js to use these variables client side when live preview needs to re-fetch the latest draft content.
Now it’s important to note that this setup works if you plan to lock down your preview site so it’s not available to the public. If you can’t restrict access to your preview site, you may be better off setting up a different way to retrieve your access token via JWT or using a custom controller in a Craft CMS module.
There are several different ways to set up HTTP requests through Nuxt.js, but the HTTP module is straightforward and a good fit for my site.
To make a request with the HTTP module, I've set up a Nuxt.js plugin that sets up my Craft API endpoint, adds my GraphQL token, then injects a $craft
function into all of my components:
export default function ({ $config, $http, query }, inject) {
// Create $craft and inject it into Vue components
// Usage: `this.$craft({ query: gqlQuery, variables: { uri: `code/${this.$route.params.slug}` } })`
// Create KY instance
const $craft = $http.create({
prefixUrl: $config.craftApiUrl,
});
// Add GraphQL authorization token for non-public schemas
if ($config.craftAuthToken !== '') {
$craft.setToken($config.craftAuthToken, 'Bearer');
}
// If `token` is set in query parameters, pass it along to Craft API endpoint
inject('craft', $craft.$post.bind($craft, query.token ? `?token=${query.token}` : ''));
}
The last line of this plugin appends a token
query param to your Craft API URL. You’ll get the value for this token when Craft’s Live Preview loads your page and here we’re passing it back to the Craft GraphQL endpoint to close the loop. When a request to your Craft GraphQL endpoint includes a valid token, any request for an entry will return the corresponding draft instead of the currently published version.
The plugin just needs to be added to the nuxt.config.js
file to be used by your Nuxt.js app:
plugins: [
'~/plugins/craft.js',
],
The inject()
function puts a $craft
variable into all of my Vue components, so any time I need to make a request to the Craft CMS GraphQL API, I can use this.$craft({})
and include my query and any variable I want to send along with the request.
For articles like this one, my page template looks something like this:
Using this.$craft()
in the fetch()
method, gets the result of my GraphQL query and then passes that data into data variables that I then pass into my template.
NOTE: In this case I'm importing a method, called articleGql()
, but all that does is a simple string replacement and returns my query as a string.
When running nuxt generate
on my production server, the fetch()
method will render all of my article content in HTML and then fetch()
will no longer be used. On the preview server fetch()
will re-send the API call on every load, getting fresh data every time.
By default, Sections in Craft set preview targets to {url}?CraftPreviewSlug={slug}
. This loads the URL of your page into Live Preview based on how your URI pattern is set up. Because I'm using a separate front-end for my preview URLs, I've updated my default preview targets to {alias('@livePreviewUrl')}/{uri}?CraftPreviewSlug={slug}
.
NOTE: {url}
is changed to {uri}
here.
While I could have hard coded the live preview site’s domain here, I set up an alias in my general.php
config so I could use a Craft environment variable to change where Live Preview is pointing during local development.
To let Nuxt.js know that I want Live Preview to pull my draft content from Craft when the page refreshes, I can use another plugin to turn on preview mode when the CraftPreviewSlug
query param is present (that param is part of my preview target pattern above, but you could change this to anything you’d like):
export default async function ({ $config, enablePreview, query }) {
if (query.CraftPreviewSlug && $config.livePreview) {
await enablePreview();
}
}
This plugin only needs to be available when Craft refreshes the URL to update Live Preview, so by appending '.client'
to the filename, Nuxt will make sure that the plugin is only used client side. :
plugins: [
'~/plugins/craft.js',
'~/plugins/preview.client.js',
],
Both Craft and Nuxt.js should be set up, so after running nuxt generate
you should see your Nuxt.js front-end in your Craft Live Preview.
Depending on how your Nuxt.js page components are set up, Live Preview should work with existing entries as well as new entries.
In my case, I set up dynamic routes to match my URI patterns set up in Craft. I pass my slug into my GraphQL queries during fetch()
to get the right content for the page.
When Live Preview reloads the page it takes you back to the top because the content on the page needs to be fetched again before it’s rendered. To fix this I'm keeping track of the scroll position on the page, storing that in to sessionStorage
, then scrolling the page after the content has been updated.
You can do this in your layout, your page, or in a component that is rendered by the page. You could also create another plugin and call it after your fetched data has been retrieved. I already had code set up to handle anchor tags, so I combined these two things there:
data() {
return {
scrollPosition: 0,
ticking: false,
};
},
mounted() {
// Scroll down to anchor
if (this.$route.hash) {
const el = document.querySelector(this.$route.hash);
el &&
el.scrollIntoView({
behavior: 'smooth',
});
} else if (this.$config.livePreview) {
const storageKey = `scrollPosition:${this.$route.path}`;
// If scroll position is set, scroll to it
if (sessionStorage.getItem(storageKey)) {
window.scrollTo(0, parseInt(sessionStorage.getItem(storageKey)));
}
// Record scroll position in session storage to retain scroll position in Live Preview
setTimeout(() => {
window.addEventListener('scroll', () => {
this.scrollPosition = window.scrollY;
if (!this.ticking) {
window.requestAnimationFrame(() => {
sessionStorage.setItem(storageKey, this.scrollPosition);
this.ticking = false;
});
this.ticking = true;
}
});
}, 1000);
}
},
The result is Live Preview that works just like a Twig-based Craft site:
Clients have told me how much they like Live Preview in Craft CMS and both Craft and Nuxt.js have solved one of the biggest challenges I was running into while diving into Jamstack tools and frameworks.
I like using Craft because it’s a CMS that is designed to be both performant and flexible. The more I explore Nuxt.js, the more I like its flexibility and thoughtful design. Both teams have created an awesome set of tools that compliment each other really well for Jamstack sites. 🥃+⛰
]]>Okay, client training is today. Open "InDesign". Click "Print". Wait.
Pick up stack of papers. Collate. Find cover sheets. Bind.
Hand out. Flip through. Throw into bag.
Put on desk. Put other things on top. Move to drawer.
HELP! Out of date. Call developer.
Download PDF attachment. Click "Print". Staple. Put on desk.
A CMS Manual’s Journey (2015)
The Help plugin for Craft 2 revolutionized the idea—for me anyway—that the CMS manual should live right in the CMS. All of the issues we ran into with printed documents seemed to go away and clients were always impressed that the CMS guide was always available and up to date.
Guide came along during the Craft 3 beta and its goal was to make it easy for a developer to create CMS guide templates and display them in their own Control Panel section. Shortly after its 1.0 release, a fellow developer suggested that one of our clients would love the ability to update the guide herself. So the ability to create guides in the CP came along.
A year and a half later Guide 2 was put together by learning what worked and what didn’t always work in Guide 1. Here are some of the new features you’ll find when you start using Guide 2.
The focus of Guide 2 is on making it easier for clients and project managers to contribute to their website’s CMS guide. This resulted in re-thinking the way guides got written and the way guides were organized and managed. That led to a re-write on how guides are stored into the CMS and how they are displayed within the CP.
The first thing that got an upgrade in Guide 2 was the Guide Editor. Instead of using a basic text area to write your guides, Guide 2’s editor includes a Javascript-based code editor. This offers syntax color-coding and a few other things that make it easier to look at the code and Markdown that you are writing.
Components in Guide 1, were a way to standardize HTML and CSS code for things like buttons, grids, and code snippets. To add a component in Guide 1, you would visit the Guide Components page, copy the code, return to your guide and paste the code in.
In Guide 2, you drag the component from the Components sidebar onto the editor—and that’s it.
Components have been created for both Markdown and Twig formats and they take advantage of each language’s strengths.
{{ craft.guide.component('image', { asset: craft.assets.filename('assets-field.png').one() }) }}
To be completely honest, navigation settings in Guide 1 was added quickly with the intention of improving it at a later point. For it’s purposes a table-based setup worked, but it only showed guides found in the Guide CP section and columns like, "User Guide ID", were not user friendly.
In Guide 2, the Organizer section lets you see all of the guides in the CMS. Managing guides is based on drag-and-drop from one area to another. The Guide CP navigation is set here, and this makes it easy to see what guides can be found on edit pages and which guides are available for Dashboard widgets.
A big part of using Guide is the ability to create a guide for one site, then create your own boilerplate template that can be used on future projects to come.
Craft Guide Templates was created as an open source repo that’s templates could be downloaded to your templates
directory and used as a starting point for your CMS manual.
To make the process of getting these templates into your CMS easier, importing templates, assets, and guides can be done right within the Guide settings page.
From there you may edit the templates just like you would in Guide 1.
Guide 2 now allows more of Twig’s features to be used. You may now {% include %}
or {% import %}
guides from your selected template
directory.
Guide 2 also includes other author-friendly enhancements.
Guide widgets now pull from guides in the CMS. When a content editor adds a Guide widget to their Dashboard, they can choose from a list of guides that are available to them. This allows for updating Guide widget content once and having it distributed to everyone.
Guides can now appear on entry edit pages, category edit pages, and user edit pages. Multiple guides can be added to each section, allowing better organization and more help for content editors.
Colors and layout options are now set as fields in the Guide settings page. A custom logo can now be uploaded for guide headers.
External documentation for plugins, Craft’s documentation, or any page on the internet can be used for a guide’s content.
Grid components are now resized based on the width of the guide’s content, so guides are formatted better in modals, dashboard widgets, and on smaller mobile devices.
When printing a Guide CP page, a new @print
style sheet will remove Craft’s UI so the only thing on the page is the Guide content.
The lite
edition of Guide is free to use and it’s intended for developers who want to write their own guide templates and display them in the Guide CP section.
The pro
edition of Guide includes all of Guide’s features and can be used to allow users to collaborate on the CMS guide, to style the guide to match the brand of the website or the developer, and to add guides to other areas around the CP.
Guide 2 requires Craft 3.4+. The pro
edition costs $49 with a $25 (optional) annual renewal.
I hope it allows for more collaboration from developers, project managers, and clients and I hope it helps those who use your CMS get the answers to their questions as effortlessly as possible.
]]>