Will Browar https://wbrowar.com/ https://wbrowar.com//theme/logo.png Will Browar https://wbrowar.com/ RSS Feed for Code articles on wbrowar.com en-US Mon, 01 Apr 2024 10:56:52 -0400 Mon, 01 Apr 2024 10:56:52 -0400 Testing Craft CMS Fields with Guide UI Elements https://wbrowar.com/article/code/testing-craft-cms-fields-with-guide-ui-elements Sun, 31 Mar 2024 16:53:00 -0400 Will https://wbrowar.com/article/code/testing-craft-cms-fields-with-guide-ui-elements I just made some updates to the field plugin, Little Layout, to give it support for Craft 5. Of the plugins I release on the Craft Plugin Store, this one I test out the most because it affects author content, and bugs on a custom field can have some very unwanted results.

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

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

Entry field layout field and guide

Setting Thing Up

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

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

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

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

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

Field Settings

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

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

Field settings

Entry Types

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

In this case, I created one called, Homepage.

Set up entry type

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

Field layout

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

Ui element layout

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

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

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

Creating Your Field Test Guide

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

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

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

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

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

{{ element.layout }}
Guide editor blank

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

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

Entry field layout error

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

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

When I set my guide content to display this:

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

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

Entry field layout column value

Fin

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

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

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


Epilogue: Using a Twig Template Across Craft Testing Environments

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

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

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

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

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

Setting up Guide to use this file follows this process:

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

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

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

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

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

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

2023 CSS Challenge

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

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

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

Markup Templating

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

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

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

JavaScript Framework is _undefined_

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

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

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

My 2023 CSS Solution

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

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

Keeping the CSS With the Twig Pieces

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

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

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

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

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

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

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

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

My Twig structure could be defined like this:

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

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

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

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

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

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

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

CUBEish CSS, Nesting, and the Freedom of Cascade Layers

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Global Styles and Custom Properties

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

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

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

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

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

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

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

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

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

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

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

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

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

    /* OTHER COLORS ... */
  }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fin

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

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

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

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

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

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

Screenshot edit
Screenshot edit note
Screenshot edit outline

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

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

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

Screenshot guide entry
Screenshot widget edit links

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

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

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

THE Problem

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

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

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

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

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

Admin Bar 4.0

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

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

Screenshot bar

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

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

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

Props and Slots

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

Admin bar example

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

Hello, Mate
EditSettingsCraft CMS Docs

Styling an Admin Bar Component

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

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

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

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

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

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

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

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

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

Admin Bar for Craft CMS

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

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

Screenshot bar

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

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

Admin bar settings

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

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

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

Fin

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

Admin bar all four

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

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

Little layout example center column

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

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

The Problem

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

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

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

Solutions

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

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

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

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

Why Lit?

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

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

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

Register Once, Use Everywhere

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

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

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

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

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

Light vs Shadow

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

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

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

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

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

The resulting markup looks like this:

Little layout rendered markup

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

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

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

Matching the Craft CP UI

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

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

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

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

Little layout field with clearable
Little layout field with clearable hover

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

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

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

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

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

The parent looks like this:

protected render() {
    return html`
      
    `;
}

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

Speaking of CSS

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

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

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

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

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

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

  // ... more styles
`;

Fin

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

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

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

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

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

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

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

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

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

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

The Gist of It

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

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

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

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

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

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

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

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

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

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

Setting Up a Separate Live Preview Target in Craft

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

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

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

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

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

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

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

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

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

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

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

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

Matching Environments Up in Nuxt

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

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

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

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

GraphQL Queries in Nuxt

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

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

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

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

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

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

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

  return response
})

Let’s break this down.

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

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

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

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

Server Route Usage

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

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

const route = useRoute()

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

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

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

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

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

File-Based Preview Routing

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

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

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

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

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

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

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

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

Route Rules Rule

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

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

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

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

Keeping the Scroll Position

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

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

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

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

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

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

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

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

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

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

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

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

Fin

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

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

Craft ❤️ Nuxt

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

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

Article Structure

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

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

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

These handles and URL segments will be important later.

Nuxt Approach

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

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

Working in Nuxt

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

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

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

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

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

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

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

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

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

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

Index GraphQL Query

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

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

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

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

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

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

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

Utils

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

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

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

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

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

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

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

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

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

${block.subheader ?? ''}

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

${block.subheader ?? ''}

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