Will Browar https://wbrowar.com/ https://wbrowar.com//theme/logo.png Will Browar https://wbrowar.com/ RSS Feed for all articles on wbrowar.com en-US Fri, 20 Dec 2024 20:56:38 -0500 Fri, 20 Dec 2024 20:56:38 -0500 Hello, Admin Bar PRO https://wbrowar.com/article/code/admin-bar-pro Fri, 20 Dec 2024 18:39:00 -0500 Will https://wbrowar.com/article/code/admin-bar-pro Last year I rewrote part of the Craft CMS plugin, Admin Bar, to solve some major CSS clashing issues and to make it easier to use the toolbar portion of Admin Bar across headless front-end frameworks. I published this blog post that outlined the problem and my solution and in writing that post I did a short retrospective on where the plugin began. I mentioned a feature, called Admin Bar Widgets, that never took off due to it relying on getting other plugin developers to add hooks and templates to their plugins.

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 2.0

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:

Admin bar settings 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:

New Entry

Admin bar widget new entry

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.

Guide

Admin bar widget guide

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.

SEOmatic

Admin bar widget seomatic

The SEOmatic widget shows you a preview of SEO meta data for the current page. This shows these SEO values for the entry:

  • The schema.org Main Entity
  • og:image
  • Page Title
  • Page Description
  • Canonical URL

Sites

Admin bar widget sites

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.

Blitz

Admin bar widget blitz

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.

View Count

Admin bar widget view count

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.

Admin bar settings widgets labels

Editions

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.

Technical Details

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.

When Updates Happen

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.

Anchoring Widgets Along the Bleeding Edge

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:

Admin bar widget seomatic

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:

Admin bar widget popover fallback

Once anchor positioning support is brought to these browsers the affected widgets will automatically change their behavior to match the first screenshot.

There’s More to Come

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.

🥃 ]]>
Craftentries Podcast: Episode 19 https://wbrowar.com/article/note/craftentries-podcast-episode-19 Fri, 29 Nov 2024 20:54:00 -0500 Will https://wbrowar.com/article/note/craftentries-podcast-episode-19 You can probably tell from listening that I was excited to join Thomas on the latest episode of the Craftentries Podcast. It was a lot of fun talking with him and I really appreciate him carrying the torch from the Craft CMS community podcasts of the past.

We talked about my experience as a web developer and then it turned into an impromptu review of the 2024 Dot One event in Toronto.

]]>
Dot One Toronto 2024 Photos https://wbrowar.com/article/note/dot-one-toronto-2024-photos Wed, 30 Oct 2024 07:30:00 -0400 Will https://wbrowar.com/article/note/dot-one-toronto-2024-photos BZ28881
BZ28884
BZ28886
BZ28890
BZ28891
BZ28898
BZ28900
BZ28902
BZ28921
BZ28927
BZ28931
BZ28935
BZ28939
BZ28842
BZ28865
BZ28872
BZ28874
BZ28941
BZ28944
BZ28945
BZ28942
BZ28950
]]>
Thank You for Using Guide https://wbrowar.com/article/maker/thank-you-for-using-guide Tue, 29 Oct 2024 17:00:00 -0400 Will https://wbrowar.com/article/maker/thank-you-for-using-guide I’ve been working on this plugin for Craft CMS since 2017, called Guide. The idea behind the plugin is to make it easy to add pages and other custom things to the CMS Control Panel without needing to use PHP or write your own plugin. The whole thing started as a way to replace printed CMS user manuals that we used to hand out to clients during training meetings, so the logo for the plugin was meant to look like a little notebook.

Over the years I’ve looked for ways to make the plugin unique and to maybe add a little joy for its users. When I released version 3 in 2021 I included a little easter egg for the developer installing the plugin:

When you used the CMS to install the plugin you were redirected to a page where I had this 3D-animated notebook slide in from the left side. Once the camera slides over the book for the first time, the cover would pop open and reveal a page inside with a thank you message on the inside sheet. From there an animation plays on an endless loop until you click a button to take you to the settings page.

In order to make this animation happen I used Blender to create the 3D model of the logo and then exported that out into a format that lets me display it into the browser. Since I already had this 3D model created, I rendered it out and used it in things like the header on the plugin store page:

Guide 3 header

As I was recently updating the Guide plugin to version 5 I decided to keep this animation and the 3D logo around in the plugin and I moved it out of this easter egg screen and into a place where more folks might see it. While I was making changes to update the animation I got this idea...

The folks behind Craft CMS put on single-day and multi-day conferences and I was planning on attending the one closest to me, Dot One Toronto 2024. I thought it would be cool to have something to give to the folks who have helped me promote my plugin, or to just hand out to anybody who wants some extra SWAG.

What I wound up creating took some of the things I’ve learned in 3D printing and woodworking and I think it came out pretty cool.

Design

My original idea for this project was to buy a handful of Field Notes, print a 3D cover that was maybe 1mm thick and glue it onto the cover of the Field Notes top cover. I sort of played around with this idea with a 3D print I whipped up and realized that it would feel uneven having a plastic cover on just the front. Another issue is printing such a thin sheet isn't a good idea since it can warp or break easily.

I looked at my 3D model and decided to thicken up the front cover and use my printer’s multi-color print feature to make the white part of the logo stand out a little more. I wanted to essentially create a hard cover notebook out of the Field Notes by printing a back and front and when you open it up it would look just like it does in the animation.

I placed an order for a bunch of Field Notes to get the size right and to start testing this out.

BZ2 8500
For reference, I landed on 89mm x 140mm x 3.5mm

I got the dimensions of a Field Notes book and used that to create a 3D model of that size to use as a reference.

BZ2 8501

It took me a few tries to get the thickness right as when you look at a Field Notes they are not exactly a rectangle. The depth is higher at the side near the staples and while you can squash that down, I didn't want to mess with the notebook itself to make this piece work. So after some trial and error I got the size from the thickest portion and moved on from there.

With these 3D-printed notebook stand ins I started to think about how to model up the cover and how it might work if I glued it onto the Field Notes books. What I realized is that this original design wouldn't work. If you look at the binding of a hard-cover book you’ll see that it’s a little more complicated than just two sheets of cardboard stuck together—that there are places where the cover needs to contract to allow the book to open up.

I tried thinking about ways to make this work with 3D-printed parts and couldn’t come up with a good way that didn’t leave a weird gap or that made it feel natural to open and close the book. Looking online, some folks have created 3D models with hinges and ways to do something similar to how a hardcover book works, but they require books that are thicker than Field Notes.

I decided to scrap the idea of 3D printing the hard covers and I picked up some cardboard to think of some other ideas.

BZ2 8508

One of the ideas I had was to wrap the Field Notes notebook in U-shaped 3D print where it would sort of act as a protective case around the notebook. I used some tape to hold down the top and bottom and tested this out with one of the 3D-printed stand ins.

BZ2 8506
BZ2 8513

I cut a notch out of the back side of the cardboard to make it so you could push the notebook out when it’s inside. With that I had a good idea where to take it from here.

3D Modeling

One of the things I have learned in 3D printing is that there are some tricks you can use to make things look cooler. I didn’t want to sand or paint these pieces and I thought it was important that the letters in the logo were crisp and readable and that they fit together nicely. The best way I know to achieve this is to design your model to lay down onto the bottom layer of the 3D print—right on the printing plate.

I modeled out the first version in several parts based on how I wanted it to look and how I planned on assembling it. Thinking about things like printing supports and how to glue it together helped me decide on portions of the design.

I used the Field Notes stand in piece as a guide and built around that, giving me the right width and the thickness for things like these little sides that were meant to look like paper.

For the white letters that spelled out the word, GUIDE, I used the shape of the letters and the picture icon to cut out the shape on the top cover and I added a small kerf to make sure I had some room to slide them together once they were printed.

Guide parts
Build plates for each part of the print. Top row, left to right: Field Notes stand in, GUIDE letters, "paper" sides. Bottom row: top and bottom covers, parts inlayed into GUIDE letters.
Guide parts upside down
This screenshot is from below since the GUIDE letters are facing down so they can be printed directly on the build plate.
Guide covers in slicer
One of the earlier versions of the top and bottom pieces.

I wanted to test out the fit of the cover first, so that's where I started printing.

BZ2 8518

The fit of the letters and the top cover worked out great so I printed the rest of the pieces to see how they would fit together.

BZ2 8520

As I did a dry run I noticed that the depth wasn’t a great fit. I made a few iterations on the top portion to make it big enough so you could easily slide a Field Notes notebook in without too much resistance, but tight enough to hold a Field Notes in place without it falling out.

BZ2 8523

The top cover had the top face and then it also included the spine portion in sort of an L shape. One of the ways I could have put this together was to make the width of the bottom cover thinner to fit the thickness of the spine, then glue that bottom piece to the bottom of the spine. My concern there was that it might be hard to do this accurately and it might make a gluey mess.

One of the tricks I have learned in woodworking is that for a really strong hold, and to give you more control over how pieces fit together, you can use a little cutout, called a rabbet, that sort of acts like a groove to rest one piece into another. So instead of making the back cover less wide, I made the spine piece less tall by about 1mm, then I added a groove onto the back cover and offset it to the same height as the original spine piece. In a dry run this worked out great.

BZ2 8528

With the functionality working nicely, I spent the next few days dialing in the aesthetics. I tweaked the height of the GUIDE text to make it feel like the right height (mostly pushing it out more until it seemed tall enough).

I showed this to my wife to get her feedback. She had a great idea to add a little gap between the inner wall of the spine and the edge of the Field Notes book, giving you a little more room to slip your fingers behind the back edge of the notebook to help slide it out. I tweaked the height and width of everything slightly and added a few millimeters to the spine area on the bottom cover—everywhere except where the notch is. This change not only helped with the gap for your finger, but it also gave me more surface area to use when gluing the top and bottom parts together.

At this stage, I had a working prototype and I was ready to begin production.

BZ2 8551
BZ2 8532
BZ2 8529
BZ2 8533
BZ2 8538

Material Change

I ordered the Field Notes notebooks first because they were a prerequisite for the design, and while waiting for them to arrive I also ordered 3D-printing filament. I already had some white PLA in stock (Bambu PLA Basic), and while I liked the Cyan color that I used for the prototype (also Bambu PLA Basic), I wanted to see if I could find a filament that matched the Guide logo color. Because my printer is a Bambu P1P I wanted to find one of their filaments, however, they didn’t have anything closer to the color, in a material I was comfortable using for the project.

I like using PLA for this kind of thing because it's easy to print and I have the most experience with it over other materials. I found a filament on Prusa’s store, in a color called Chalky Blue, however, it was a PETG material. I know that PETG can be a little more finicky and dialing in the temperature was important, however, PETG can be stronger than PLA and I know a lot of folks who print only using PETG when they can. So I placed an order and waited for it to arrive.

BZ2 8721
I had to re-spool the Prusa PETG to fit within my Bambu AMS. I used a drill, a bar from a dumbbell, and lots of patience to move the chalky blue filament to an AMS compatible spool.

I added the chalky blue PETG to my printer as a custom filament and set it up according to the heat ratings as written on the filament box. My first print with it was close but had some issues filling in some of the gaps on smaller areas. This affected the G and the U the most.

IMG 4697

After spending some time dialing in the right temperatures and slowing down the speed of my printer, I finally got to a point where I thought it was good enough to start printing the final pieces.

BZ2 8718

Production

I made an inventory list of all of the pieces that need to be printed, so I can keep track of how many I needed. In a project this small it wasn’t too hard to keep track, but I did it out of habit.

I started off with the parts printed in white first. The little "paper" side walls were easy enough to print in one sheet. The GUIDE letters were also pretty straightforward to print, and I did those in only a couple of batches.

BZ2 8702
BZ2 8703
BZ2 8705
BZ2 8712
Even the supports for the GUIDE letters looked cool.
BZ2 8707
As everybody with a 3D printer knows, failed prints happen. At one point the printer head popped off from a print that wasn’t sticking correctly to the bed.
BZ2 8708

Next up were the parts inlayed into the GUIDE layer. This included the inside of the picture icon and the inner area on the letter D.

BZ2 8723

I cleaned up these prints and got them ready to glue into the GUIDE letter pieces. I followed this order because I knew that I had to have the GUIDE letters completed before I could glue them into the top cover. Then the paper sides and the bottom cover would be glued on last.

BZ2 8727

Over the course of the next few days I had printed out the rest of the top and bottom pieces. The bottom pieces were no problem, but some of the top pieces still had issues with small gaps. Everything was flat, but sometimes there were holes missing and I wouldn’t find out there was an issue until I pulled the piece off of the plate (this is sort of the downside to printing face down).

My goal was to hand out 10 of these, so I made a couple extras—just in case. I eventually got 12 tops that I liked and moved onto the next step.

BZ2 8733

I used some things around my workshop to get the GUIDE letters to stay in place when gluing them into the top cover. I wanted to make sure these were fully cured before I started gluing the top and bottom pieces together.

BZ2 8736
These metal blocks are called 1-2-3 blocks and the are just heavy enough to provide some weight without bending the top pieces.
BZ2 8737
I used one of these tool cases to distribute out a 15lb weight and I stacked the glued pieces into two layers.
BZ2 8739

Once the tops were all set, I began gluing the rest of it together in batches. I used woodworking clamps to hold things together in two spots where I thought they needed them the most. These quick clamps are perfect in that they aren’t too strong and the pads that touch the 3D printed parts didn't leave any marks.

BZ2 8744

I gave everything a good 24 hours to cure and then started inspecting them, making fixes, and cleaning them up where I felt like it was needed.

Fin

BZ2 8769
BZ2 8771
BZ2 8772
BZ2 8761

This project was sort of a good way to use all of the things I’ve learned about 3D printing so far. I didn’t feel like I had run into any major, unforeseen issues at any point. Most of the refinements were based on having the final thing in hand and taking some time to get a feel for what works and doesn’t work.

The only thing I would try to do better next time is work on figuring out a cleaner way to glue the 3D parts together. I used a two-part epoxy and I was sometimes more worried about the epoxy hardening before I used it that sometimes I put on too much and had to come back and clean it up later. Just like a woodworking project, each one has one or two minor things that make it not perfect, but I'm okay with that.

I’ve been doing a lot of promoting for the Guide plugin in Discord, Mastodon, and on my blog and doing that sort of thing doesn’t feel natural for me to do, so putting my energy into a little thank you piece felt like a good change of pace.

📘 ]]>
Slow-Toasted Pumpkin Seeds https://wbrowar.com/article/maker/slow-toasted-pumpkin-seeds Sat, 12 Oct 2024 21:40:00 -0400 Will https://wbrowar.com/article/maker/slow-toasted-pumpkin-seeds As a kid we always cut into our pumpkins, separated the gunk, washed the seeds, salted them and threw them in the toaster oven on high for 15 minutes and that’s how we toasted pumpkins seeds. Sometimes they would burn, sometimes they would cook the outer shell and then the inner seed was still raw. We still loved them.

Around the time my wife and I moved in together I wanted to share pumpkin seeds with her and I found this recipe online for slow-toasted seeds and I thought I would give it a shot. Over the years I've tinkered with the method and at this point I think I've got it down to a science. If you can take the time, give it a try and let me know if it was worth the wait.

Preparation

Cut a hole into your pupkin and pull all of the seeds from the inside. I like to keep the seeds from each pumpkin separate to make it easier to cook them later. Clean off all of the excess flesh, and use a strainer to wash the seeds, then place them in a container with a lid (a take-out soup container works well). Pour enough water into the container to cover the seeds twice. If you’d like, add about 1 tablespoon of salt for each cup of seeds. Close the lid and leave them overnight.

DSC 8699
If the water is orange like this give it another rinse. Rinsing them through a colander helps if you’re finding little bits of pumpkin sticking to the seeds.

The next day, strain the seeds through a strainer to get off as much excess watar as possible. Take a cookie sheet and spread the seeds out to one, flat layer and leave them out to dry (this can take several hours, but using a salad spinner, a fan, or dehydrator can help speed this along).

After letting the pumpkin seeds dry, move them to a small bowl and toss them in olive oil. About a teaspoon of oil for every cup of seeds should do.

Adding Flavor

Add a flavoring of your choice. Some that have worked well in the past:

  • Cayenne pepper + sugar (applied at the 60 minute turn)
  • Sweet chili sauce + Pappy's Hottest Ride hot sauce
  • Tonkatsu Sauce + brown sugar (applied at the 60 minute turn)
  • Sweet Baby Ray’s BBQ sauce (or something with a similar texture) mixed into a little olive oil
  • Worcestershire Sauce
  • Cinnamon + Maple Syrup (put it on at the 60 minute turn and cook until seeds are crispy)
BWR 3214
How much seasoning you use is completely to taste and since the amount you get from each pumpkin can vary, it’s hard to pinpoint exactly how much to use.

Strain seeds to remove excess oil and flavoring, then place seeds onto baking sheet.

Toasting Time

DSC 6217

Preheat the oven to 250°F (121.11°C) and bake for 80 minutes (breaking them up and stirring them around every 20 min). If you are using sugar or something similar, add it to the last 20 minutes so it doesn’t burn.

Before taking the seeds out, check to see if they are crunchy (sometimes the bigger seeds take longer). When cooked to your preference, let them cool down on baking sheet and store in a container or ziploc bag.

BZ2 9172
You can use a regular oven for this or a toaster oven, but make sure you can set the temperature to 250°F.

Fin

Every year I make at least one batch of the cayenne pepper + sugar seeds and then depending on how many other pumpkins we have I’ll experiment with different flavors. Usually I try to use different sauces or rubs that we have laying around and sometimes they are hits and sometimes we have misses. Generally I shoot for a combo of heat and sweetness and sometimes simplicity is where it’s at.

If you wind up using this method, please let me know what spices you wound up liking (or even which ones you think should be avoided). Enjoy!

BZ2 9179
DSC 6262
DSC 8694

Bonus Tip

Okay, so while we’re talking pumpkins I have another thing to suggest. I learned this during a pumpkin carving contest from a friend who is an artist.

A lot of folks cut a hole in the top of their pumpkin, grab the stem and pull off the top like a lid. It gives you good access to put a candle or a light inside, but as the pumpkin gets older and softer, or if you don’t cut it perfectly, you might see some light peaking out in a way that might not fit the rest of your design. A few years ago I learned that you could get a cleaner look by cutting the hole in the bottom of the pumpkin instead.

The hole in the bottom makes it easy to get all the seeds and gunk out and if you use a candle or LED tea light you now have a little base to put it on, then you put the shell of the pumpkin over the base.

WBZ 2669
The big hole is at the bottom of the pumpkin and the little hole is the size of a light socket.

I also learned to ditch the candles and instead put a light bulb inside your pumpkin! If you have an outlet within reach of where you display your jack-o-lanterns you can pick up an outdoor light socket and feed that into your pumpkin with an LED at the end of it. I usually cut a hole somewhere in the back of the pumpkin, place the socket into the hole, then go up through the big hole in the bottom of the pumpkin to screw the bulb in.

If you want to go for a really cool look you can find standard-size LED bulbs that have a built-in flame effect. Since we do multiple pumpkins per year, I picked up this outdoor work light string and took off the little cages. We use the flame-effect bulbs for the inside of the pumpkins and then I also picked up some ultraviolet bulbs to flood the background behind the pumpkins.

WBZ 6876

Happy pumpkining and enjoy the slow-cooked seeds!

🎃

Update 2024

This year I made a batch of Cayenne Pepper + Sugar seeds and a batch of Curry Madras (from a local spice shop, Stuart’s Spices.com. A. During this cook, the house smelled amazing. B. I went back to using the big oven to spread the seeds out better and I think it helped bring out the toasted flavor a bit more than last year’s in the toaster oven.

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

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

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

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

Navigating the CMS

Dashboard

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

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

Admin bar

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

Posts, Pages, and Entries

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

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

Entries list

Menus

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

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

Publishing Entries

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

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

Fields publishing

Editing Entries

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

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

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

Managing User Uploads (AKA Assets)

Image assets

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

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

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

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

SEO

Seo

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

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

Theming, Templates, and the Front End

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

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

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

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

Forms

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

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

Ecommerce

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

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

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

Technical Requirements

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

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

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

Hosting

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

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

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

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

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

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

Plugins, Modules, and PHP

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

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

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

Fin

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

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

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

Happy Crafting!

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

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

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

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

Getting Plugged In

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

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

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

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

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

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

There’s a Plugin for That

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

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

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

The code for this entire thing looks like this:

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

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

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

These images are missing alt text.

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

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

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

All images in this volume have alt values!

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

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

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

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

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

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

Okay, Sometimes You Still Need a Plugin or Module

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

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

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

Using Guide to Replace Other Plugins

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

Craft 4 abandoned plugins

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

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

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

📕 ]]>
Apple Hearing https://wbrowar.com/article/note/apple-hearing Mon, 16 Sep 2024 21:00:00 -0400 Will https://wbrowar.com/article/note/apple-hearing Anybody who knew me when I was a teenager knew that I played drums at church, went to band practice every week, and on any given Friday or Saturday night went out to either see a band play or would be on stage myself. My mom—who was a musician herself—always stressed that earplugs were important and your hearing was something you can't get back once it's gone.

Dumb teenager me doing dumb things, I often kept a few pairs of earplugs in my stick bag but almost never put them on when I was playing. On top of that, the problem with being a band full of poor teenagers is that things like extra speakers and in-ear monitors weren't something we'd invest our part-time paychecks on. So if you’re the drummer and you want to hear the vocals and whatever the guitar is playing you’d put the main speaker and amps behind the kit and listen to them from there. You're also sitting at one of the loudest instruments in the band that has a huge dynamic range between the bass and all the cymbals.

Anyway, somewhere in my 20s I started noticing that I had a hard time hearing in my right ear. I went to the doctor and got my ears cleaned and checked out. While I was having a few issues with hearing a few different sounds it wasn’t bad enough to diagnose me as hard-of-hearing or anywhere worse than that. But I knew that something was off.

Now that I'm 40 I know for sure that my early drumming days did some serious damage. I often find myself having a hard time hearing people in crowded areas where a lot of talking is going on and I find myself asking people to repeat what they said from time to time.

It’s all my fault and while the ship has sailed I have found that being a remote employee has really helped me deal with it at work (because every conversation can be turned up with a volume knob). Elsewhere I find myself using headphones a lot to consume media and for basic hearing protection. I keep a pair of over-the-ear monitors at my drum kit for when I play at home, I have a pair of noise cancelling, over-the-ear headphones for when I mow the lawn or do anything in the wood shop, a pair of headphones for solo watching TV (which is also about not disturbing everyone in the house), and as of the last couple months I've started doing the thing I learned on ATP where you can wear AirPods Pros at concerts to kill off sounds above a certain decibel range.

AirPods Pros as Hearing Aids

At its 2024 iPhone event, Apple announced a software update coming to all AirPods Pros (2nd generation) that turn them into clinical-grade hearing aids. I can’t wait for this to roll out officially because A. I already have a pair of AirPods Pro that will work with this feature so it's just a software upgrade for me, and B. at this point I always have my AirPods Pros on me anyway.

My understanding of the feature is that you use an iPhone on iOS 18 to do a hearing test, then it helps you create a profile that syncs to all of your Apple devices. This means my iPhone, iPad, Mac, and Apple TV would all use this hearing profile to make audio sound better for me. Then as part of the AirPods themselves I can essentially put them in a hearing aid mode where it not only pumps noise from around me into the earbuds but it'll use that profile to help enhance the right sounds that work better for my ears.

While I don’t love being in this situation to begin with, at least I hope this new AirPods feature will help me out a little bit.

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

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

Slidouts

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

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

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

Relying Too Much on a Vue 3 Feature

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

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

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

Vue Teleport-Driven Development

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

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

Making Messes in My Code Base

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

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

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

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

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

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

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

Introducing Guide 5.0 (Beta)

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

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

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

It Was a Good Idea at the Time

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

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

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

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

Using the Platform

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

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

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

Guide component

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

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

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

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

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

Customer Research-Driven Development

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

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

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

Guide organizer
Guide list
Guide cms guide
Guide settings

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

Guide 5 is Now Live

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

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

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

Slideouts 🎉

Guide slideout
Guide ui element

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

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

Markdown Rendering

Guide editor markdown

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

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

New Organization

Guide cms guide

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

Guide list

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

Guide organizer

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

Guide Components

Guide editor components

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

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

CSS and JavaScript Customization

Guide widget

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

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

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

Settling All Family Business

Guide component

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

Guide 5 is Alive

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

📘 ]]>
Frostapalooza Photo Roundup https://wbrowar.com/article/note/frostapalooza-photo-roundup Fri, 23 Aug 2024 17:30:00 -0400 Will https://wbrowar.com/article/note/frostapalooza-photo-roundup I wasn’t the only person shooting photos at Frostapalooza and a week later I'm starting to see tons of great photos and videos from the show pop up! The best place to get an idea what it felt like to be at this show can be found in the Frostapalooza! Google Photos album.

BZ2 7973
A special shout out to Anne-Laure Gaté—who I think is in the photo above—for nailing some really incredible closeups.

I also noticed Brian Kordell was shooting a lot of video from the crowd and has posted a bunch of shorts and videos on his YouTube channel, (including this clip of one of my favorite tunes of the night, Sledgehammer).

Over the past few days I’ve seen a few blog posts popping up that share some really nice insight on what it was like to participate in the show:

Finally, while I believe it’s currently in editing, there is a multi-camera video that is definitely worth looking forward to. I got to see a behind-the-scenes look at the process to capturing all of the clips from all of the cameras placed around the venue and it’s an amazing, yet complicated setup that got all the best angles of the night.

]]>
Photographing Frostapalooza https://wbrowar.com/article/photo/photographing-frostapalooza Tue, 20 Aug 2024 07:00:00 -0400 Will https://wbrowar.com/article/photo/photographing-frostapalooza Frostapalooza was an amazing night! If you haven’t heard of it, Brad Frost turned 40 this year and he invited 40 of his friends, family, and fellow web development-related folks down to put on a charity concert in Pittsburgh. It was a bunch of folks from around the US (and from across the pond) who were coming into town and playing together for the first time.

I had heard about it on episode 601 of the Shop Talk Show and bought my ticket right away. Putting together all of these people who’s books and blogs I’ve been reading, and podcasts I’ve listened to for almost two decades, in one place, only a few hours away, was an incredible thing. I can’t remember anything else like this in the web development community and I don’t know the next time something this big will happen again.

Having spent some time photographing events for The A Cappella Blog I thought it might be fun to bring my camera gear down to capture this event for posterity. From my experience I know that some bands have rules around when and how you can shoot their shows, as well as venues sometimes have rules around where you can go to take photos or what you can and cannot include in your photos. So I reached out to Brad via email asking if it would be okay to bring my gear down and if I could have access to places like the backstage and the balconies. While I thought that worst case I would just go to the show and enjoy it as part of the audience, I was surprised and thrilled to get an enthusiastic “YES!” in response.

With that I made it a goal to capture some of the fun moments of the night and to get at least one good shot of each person performing. I'm happy with how the photos turned out and throughout this page you’ll see some of my favorite shots from the night—along with a few photos from the night before.

BZ2 7394
BZ2 7397
BZ2 7398
BZ2 7470
BZ2 7471
BZ2 7469

Preparing for the Gig

Before heading down to Pittsburgh I put together a solid set of lenses and gear (backup batteries, memory cards, lens cleaning kits, etc...) as well as a few backup items that I wasn’t sure I'd need, like battery-powered LEDs and a tripod.

It’s been a while since I've shot photos during an event like this so I wanted to do a little YouTube research on concert photography and I picked up on a couple of tricks and camera settings that I tried throughout the event. One of them was around metering modes and using spot metering. When I've shot events like this in the past I usually will leave metering on the matrix metering mode (in Nikon parlance), but this can sometimes lead to lots of extra noise and an increased auto-ISO from the huge differences between stage lights and the dark background behind the performers.

BZ2 7483

I moved the metering selector to one of my camera’s function buttons so I could quickly swap between matrix metering and spot metering. My other function button was set to focus modes and throughout the entire show I wound up switching both of these settings frequently as I moved around the venue and changed lenses.

BZ2 7488

Another tip I got was to use Manual mode and set the aperture wide open, set the camera to auto-ISO, and the shutter speed to 1/200 or 1/250. I've done something like this for past shows, but I would try to stick to a shutter speed of 1/160 or something slower to get more light in. I moved my setting to 1/250 for almost the whole show and I'm glad I did. Details like hair moving, drum sticks swinging, and other movement were mostly frozen and I think it made a huge difference in some shots. The tradeoff for this was higher ISO and more noise, but I think spot metering helped keep that down where it could.

BZ2 7552

Finally, I capped the auto-ISO at either 8000 or 6400 throughout the night. There are a few shots where this was an issue and the noise is very obvious, but for the most part exposures were where I wanted them to be and those that were underexposed still got enough detail to balance things out.

BZ2 7501
BZ2 7508
BZ2 7521
BZ2 7546
BZ2 7600
BZ2 7663
BZ2 7668
BZ2 7688
BZ2 7665
BZ2 7680
BZ2 7713
BZ2 7724
BZ2 7719
BZ2 7734
BZ2 7755
BZ2 7764
BZ2 7756
BZ2 7769
BZ2 7779
BZ2 7793
BZ2 7798
BZ2 7811
BZ2 7835
BZ2 7849
BZ2 7853
BZ2 7874
BZ2 7885
BZ2 7896
BZ2 7905
BZ2 7911
BZ2 7912
BZ2 7936
BZ2 7943
BZ2 7944
BZ2 7946
BZ2 7979
BZ2 7980
BZ2 7981
BZ2 7983
BZ2 7984
BZ2 7991
BZ2 8026
BZ2 8028
BZ2 8045
BZ2 8043
BZ2 8055
BZ2 8066
BZ2 8070
BZ2 8085
BZ2 8093
BZ2 8097
BZ2 8103
BZ2 8143
BZ2 7527
BZ2 7760
BZ2 7960
BZ2 7964
BZ2 7967
BZ2 7969
BZ2 7972
BZ2 7974
BZ2 7992
BZ2 8081
BZ2 8082
BZ2 7975
BZ2 8144
BZ2 8153

Fin

I came to this show as a fan of the web developers that I've learned from and that have made a huge impact on my career and how I approach my work. But for one weekend these folks weren’t developers talking shop; they were musicians and a group of people looking to celebrate being together and to share their talents with the crowd. Brad and all of the folks I met were kind and welcoming throughout the entire weekend and Brad answered all of my questions that came up along the way—making it really easy to just show up and go with the flow.

One thing that’s not to be overlooked is that this whole event was for the benefit of two different charities, Project Healthy Minds and NextStep Pittsburgh. Right before hitting the road one of my friends was managing a mental health crisis in his life, so that made this event hit home even more for me. Please take care of each other and—as Brad started off the concert—we all need to take some time to notice the negative and get it out.

To Brad and all of the other folks I met this weekend, thank you and keep rocking!

🍄🎸 ]]>
Gamifying Vacation https://wbrowar.com/article/maker/gamifying-vacation Thu, 08 Aug 2024 17:25:00 -0400 Will https://wbrowar.com/article/maker/gamifying-vacation I recently went on a vacation with my family that took us across the globe. It was exciting for all of us but my kids, in particular, were absolutely stoked. It would mean a lot of new things for them, including seeing the sights, eating new foods, and interacting with new people. While these are all the reasons why I wanted to go on this trip, I can see why my kids might have a little anxiety about getting catapulted way outside of their comfort zone.

We—a family from north-eastern USA—were traveling to Malaysia, Singapore, and Japan, and while my kids are familiar with Malaysian and Japanese food, we knew that there would be a few foods we wanted them to experience, we wanted them to get to know the language and the cultures, and we wanted them to be motivated to be in their best behavior.

Tag Ceremonies

During summer break our kids go to a summer camp that has a reward system for kids that show kindness and maturity or push themselves out of their comfort zones. They give kids a lanyard with a keyring on it at the beginning of the summer and as the kids earn their rewards they are presented with a colorful keyring tag that has a unique label and an illustration for each reward. For example, the kids will get tags for their first time swimming, hitting the target in archery, and doing the ropes course. They also have tags for behaviors, like sharing, being honest, and winning competitions. The tags are presented to the kids in a weekly “Tag Ceremony” put on by the counselors.

My kids are proud when they receive a tag. It motivates them to do their best to receive more tags and they are psyched to let us know what new tags they've earned each week.

A week or so before our trip, my kids were sharing with us the news about the tags they just received and I just threw the idea out there to my kids that wouldn’t it be fun to get a tag for trying durian or sushi on the trip? They both got excited about the idea and I told them that—no promises but—I would look for some tags I could write on during the plane ride and we’d come up with some that they could earn.

Right on, Write-on Tags

With just a few days to spare I went with the quickest route to getting some blank tags that would work for our purpose. I wound up placing an order for a box of translucent tags on Amazon. To be honest, I went with the translucent color because I thought it would look cool, but I think the white or one of the other colors would have worked better. The problem with the translucent background is it made a stack of tags on a keyring harder to read.

These tags were a little smaller than the tags the kids get from camp, but they were very similar in shape and size. We found a couple of key rings around the house that fit the tiny holes in the tags. I would up using a fine-point Sharpie and wrote on the tags by hand—sloppy handwriting be damned.

To organize myself ahead of time I came up with a list of all of the rewards that could be earned on the trip. I used Affinity Publisher to whip together a page that I could print out, cut in half, then put in my kid’s travel backpacks. This page would outline what is required to earn a particular tag as both a reminder to myself and to help make sure the kids knew the rules up front.

Tag descriptions affinity designer
I have two kids so I made two sets of each tag, and I doubled up on a couple of the tags I thought the kids could earn more than once, like the Spicy Adventurer tag.

The blank tags came in a little ziplock bag so after I wrote up all the tags I put them all in that bag and put it in my travel bag.

Throughout the vacation I would give the kids a heads up, like letting them know we’ll be going to a seafood restaurant and they had a good chance of getting a shrimp or crab tag. At the end of the day for each day of our vacation we did our own tag ceremony and I presented the kids with their new tags.

The Ultimate Reward

My kids each have a hand-me-down iPad that we brought on the trip to give them something to do on the long flights. Kids being kids, instead of looking for fun ways to entertain themselves in our homestays and hotels they constantly asked to play games on their iPads whenever we seemed to have a moment of downtime. Instead of constantly telling them “no”, I eventually told them that if they can go the rest of the trip—outside of plane rides—without turning on their iPads I would give them a special, golden tag as a reward.

I can’t say I expected anything to come out of this, but it worked (with some really minor exceptions that we let go by). For the most part they stayed off their devices without us having to tell them. The only thing they used their iPads for were to take videos and photos, and that was something we encouraged them to do.

So anyway, we got back home and now I needed to come up with whatever this golden tag would look like 🤔

I used Fusion 360 to create a blank tag. To do this I measured the current tags (at 5mm x 3mm) and measured the distance from the keyring hole from the edge. In Fusion I put together a sketch and modeled out the based of the tag, added a border, and room for the keyring hole.

Modeling tag fusion 360

Fusion 360 lets you add text, but the slicer software I use for my 3D printer, Bambu Slicer, also lets you add text to a model. It was simpler to create the tags as blank in Fusion, bring the STL file from that into the slicer, add the text to one of the tiles, then duplicate that one tile to modify the text from there.

I created an iPad Dust Collector tag and let the printer do its thing. I used Prusa’s awesomely named Oh My Gold PLA filament with Bambu’s default print settings (along with the PLA profile I added to the slicer for the non-Bambu PLA).

Adding text bambu slicer

While I had done most of the work anyway, I put a couple of colors on my AMS and created tags with my kids names on them just for fun. The slicer lets you start with one color and then swap out the color at a certain layer, so I picked the layer after the base was printed in gold to start the color for the text and the border.

Fin

Keyring tags
Keyring tags closeup

I uploaded the model for the blank tags onto Prusa’s Printables website. There are other similar tags on the website, but if you have a 3D printer and wanted to follow along my process here, the model is free to use.

My kids didn’t earn all of the available tags but they earned most of them and they truly tried their best. I’m so damn proud of them for trying different foods (and actually liking them!), for sticking with us on some long hikes, and for learning some words in the local language and speaking them to strangers (like telling the cook ”thank you“ in Malay on the way out of a restaurant).

I don’t know that we'll use this whole tag setup on future trips, but my kids are at the right age right now where this got them excited for this one vacation and it added a lot to the memories for them and for me.

🏝️ ]]>
Embracing Noise https://wbrowar.com/article/photo/embracing-noise Wed, 19 Jun 2024 22:54:00 -0400 Will https://wbrowar.com/article/photo/embracing-noise I was looking back at some photos from a past vacation—one where I rented a Nikon D500 to bring along with me—and I noticed that I had taken some photos set to really high ISO numbers. For example, this photo of these lanterns was shot with an ISO just over 17000!

Screenshot 2024 06 19 at 10 16 43 PM
This is a screenshot of a crop of the original photo, so there’s a little more in the frame than shown here.

Unless it's very specific for the look of the photo, I typically set the ISO to use AUTO-ISO, which essentially lets the camera decide what ISO setting is best for the shot. Nikon lets you set a base setting and a maximum setting, however, for the D500 I don't think I had set it to a specific maximum ISO, because in my library I saw shots that went all the way up to 40000.

When I got my current camera, the Nikon Z 7ii, I had left on a lot of the noise reduction settings (that blur out the noise in camera) and I had set the maximum AUTO-ISO for each of my three user settings to go no higher than 2000 or 4000.

I've noticed that when taking photos in situations where I'd expect the ISO to go way up that one of two things happen. Either the noise reduction does its thing and the image looks a little blurry, or if in a situation where I'm shooting Aperture Priority (where the shutter speed is automatically set to work with a fixed aperture setting) the shutter speed goes so low that the image is blurry or at least some detail is lost.

Wherever possible I've been trying to avoid any noticeable noise, but looking at my past vacation photos I think letting there be noise was the reason why I got a decent shot in the first place.

As I've heard from other photographers, you can use technology to remove noise and you can spend all day tweaking exposure, but removing motion blur is a much tricker thing to do later on.

So I decided to take my one user setting that I typically use to take photos of my kids or other moving subjects and I cranked the maximum AUTO-ISO to 25000 and turned off the in-camera noise reduction, then took a few random shots around the house.

Screenshot 2024 06 19 at 9 58 38 PM
This photo is also zoomed in and cropped from a portrait orientation.

In this low-light shot of my cat you can see a lot of noise, but also things like her whiskers or the fur on her ears aren't blurry from movement. I might continue to tweak these settings, but I'm leaning towards keeping this look. Over the next couple of weeks I'll focus more on low-light shooting to see how well this pans out. I’ll bet some photos in otherwise decent light don’t change, but I'll also keep an eye on that too. For now I’m willing to embrace the noise.

]]>
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.

🍱📘 ]]>
Wall-Mounted Standby https://wbrowar.com/article/maker/wall-mounted-standby Sun, 28 Jan 2024 09:51:00 -0500 Will https://wbrowar.com/article/maker/wall-mounted-standby In 2023, Apple introduced a feature to their MagSafe-compatible iPhones, called Standby. This feature lets you use the widgets from your apps when your phone is:

  • Magnetically attached to a MagSafe charger or plugged in with a charging cable
  • The phone is positioned upwards and in the horizontal position
  • The device’s Lock Screen is on

I currently have 2 MagSafe stands by TwelveSouth that I use on a nightstand while I sleep and on my desk while I'm working. With these stands in places here are some ways I typically use Standby:

  • On my nightstand I display a digital clock alongside either the weather widget, a charging widget, or my calendar (via Fantastical).
  • While I'm working I typically leave my phone on the World Clock, showing points that represent the timezones of some of the international teammates I work with.
  • In both places, I’ll turn on the Now Playing screen for audio books and podcasts that I listen to (usually air playing to speakers or AirPods Pros).

I really love how simple this feature is to use and now I want to put MagSafe stands all around my house. After upgrading my nightstand MagSafe stand to the HiRise 3 Deluxe, I gave the Forté I was previously using to my wife, which—replacing her MagSafe charger—left us with an extra MagSafe puck to spare.

Finding a Use Case

My first thought was to pick up another Forté and find a place to put it in the kitchen, but then—as part of a customer journey—marketing for a different brand had another idea in mind.

I got a marketing email by a company I bought iPhone stands and AirTag cases from, Elevation Lab, for a new product that lets you pin an AirTag to your kids or a piece of luggage. While I'm not in the market for that, it got me to visit their website and check it out. While I was there, I came across something they call, MagBase.

MagBase is essentially a little silicon holder for a MagSafe puck that is meant to live on your desk or another flat surface. It’s designed so you can pick up your phone by sliding it off the magnet, but just as easily, you can pick up both your phone and your MagSafe puck to do things like check your email or respond to messages.

I like that the MagBase is very minimal, and even though it’s designed to sit on a flat surface it gave me an idea on how I can solve a very minor problem, while also giving me another place to use Standby.

A few years ago I put together this little couch-side shelf, and above that I mounted a shelf for more vertical space. Last year, I unmounted the shelf for a few minutes to add a headphone hook to it. I use this shelf while watching TV or playing video games and I usually place things like my iPad and AirPods Pro case on it.

As far as my phone goes, I usually put that on the arm of the couch because I had no other particular place to put it. This extra MagSafe puck helped me find a new place for it, and it looked cool to boot.

BZ2 6166

The headphone hook sits in the middle of the shelf, so when sitting on the couch there is some room on the wall to the left and to the right of the headphones. The space to the right seemed like a really good place to put a MagSafe puck. Before ordering a MagBase I thought that maybe I would 3D print my own MagSafe mount. After taking a look at some of the 3D printing model sites I found that the idea of mounting a MagSafe puck on the wall wasn't a new idea, and there were several free, really nice models to choose from.

One thing the MagBase had going for it, though, was that it also comes with a USB-C extender. This would be important because the 1-meter distance from the nearest outlet to the place I wanted to mount the MagSafe puck was too short. That got me to go for the MagBase, so I placed an order and it showed up a couple of days later.

I thought for a little bit about how I would run the MagSafe cable around the shelf. I didn't want to drape the cable over the shelf, but if I added a little gap between the shelf and the wall I could run the cable behind the shelf. This wasn’t really my favorite option, so I unmounted the shelf and brought it down to the workshop to see if I could come up with something else.

Channeling a Solution

I took a look at where the shelf mounting hardware fits into the shelf and noticed that the area I routed out for the mounting hardware gave me about 5mm to work with. With the cable plugged into the wall on the left side, I could have ran the cable from the left mount to the right, leaving the cable just under the shelf.

Maybe it was where I was standing, but something else came to mind. Did you know that the width of the kerf on a table saw blade is exactly the same thickness as a MagSafe cable? At least this is true for the blade that came with my DeWalt table saw.

I moved the table saw fence close enough that the blade lined up with the routed area for the mounting hardware. I raised the blade to just about 3-4mm, picked up one side of the shelf, and did a plunge cut from one side of the mounting hardware slot to the other.

BZ2 0265

I did a quick test of the cable to find that it was a perfect fit!

BZ2 0269

I realized I had to create an exit point for the cable on both sides, so I grabbed a chisel and diagonally cut out a notch for the cable on both sides.

BZ2 0277

While doing a dry run with the cable, I also realized that I would need to let some of the cable sit within the routed out area that the bottom of the mounting hardware sits. The problem with this is that a lot of the stability for the shelf is based around clamping down on that area and I didn't want to crush the MagSafe cable to get this to work.

I found a washer big enough to fill up that area and found that if I stack up two washers it was just about as high as the MagSafe cable. I used some hot glue to attach the washers tougher, then onto the shelf. I didn’t really need to do this but this would make it so that when I'm mounting the shelf the washers wouldn’t move around and fall out of place.

BZ2 0281

After another quick test I was all set with routing the cable from one end of the shelf to the other.

BZ2 0283

The final step was to re-mount the shelf and then attach the MagBase to the wall. I took one more pass at running the cable through the channel I just made. I made it as flush as I could and made sure the MagSafe puck was the right distance from the shelf to where I wanted to put it.

BZ2 0286

Fin

This is a DIY situation I really like being in. This is the 4th time I’ve iterated on this shelf area and each time it becomes more useful for my needs. I also like that everything in this project is reversible. As with all technology, a new things comes along every few years and sometimes things just break. If I decide I no longer want the MagSafe puck in this spot, I can unmount it and remove it from the shelf, leaving only the channel and the notch cut out, but hidden in the back of the shelf. Removing the washers should be pretty easy, too.

BZ2 0300
BZ2 0301

As I was going through these pictures on my laptop, I placed my phone up on the wall, put on a podcast, and got to enjoy this new setup right away.

🖼️ ]]>
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".
]]>
Unboxing the Birdfy Nest https://wbrowar.com/article/ee/testing-the-birdfy-nest Wed, 06 Dec 2023 12:30:00 -0500 Will https://wbrowar.com/article/ee/testing-the-birdfy-nest Intro

Earlier this year I built two bird houses and put cameras in them as gifts for family members. At the end of that post I mentioned that the company I bough the cameras from, Netvue, invited me into their testing program to give feedback on a product, called Birdfy Nest. I received my Nest a couple of weeks ago and I set it up in my back yard. I wanted to write this post as sort of an unboxing and to show some detailed photos of what to expect from the Birdfy Nest.

Unboxing

BZ2 9258

On first impression, the box the Nest ships in was a little heavier than I expected, but it turns out there's a lot in the box. The retail box for the nest fits nicely into its shipping box, then inside the retail box there is the Nest birdhouse and a series of little boxes for the solar panel and the mounting accessories. There were also a couple of other cardboard boxes used to protect the gap between the top of the birdhouse and the vented roof, as well as the outer camera.

BZ2 9259
BZ2 9260

One thing I really appreciated was that they included a ribbon as sort of a temporary handle to help you pull all of this out of the box, instead of having to turn the box upside down and hoping things don't go all over the place.

The box also comes with two instruction booklets. One is an installation guide that walks you through the installation options and how to use the gear that comes with the Nest. The other booklet is the user manual and it includes things like the parts list and some other standard user manual info. One of the things I really liked is that the manuals included information around how to change the predator guard (the doorway with the hole that the bird enters the box) and they give some suggestions on good places to mount the Nest.

Setup

The first thing the instructions tell you to do is to charge the camera unit up. The Nest comes with a USB-A-to-USB-C cable so as long as you have a cell phone charging brick around you’re all set (no need to pull the solar panel out just yet). To access the camera unit you can pull down the main door from the side of the Nest.

BZ2 9274
BZ2 9263

If you want, you can plug the charging cable right into the camera unit, or you can unplug the cables going into it and slide the whole camera unit out of the Nest.

BZ2 9279
BZ2 9281
The camera is split into three parts: the inside camera (shown here), an outside camera, and the box that stores the camera battery and the WiFi transmitter.

With the camera charging, the next step is to set up the outer camera. In order to do this there's a piece of tape you need to remove, then there's a cover that you need to slide off. The instructions make this clear with some really helpful illustrations.

BZ2 9287

For someone with big fingers, installing the outer camera was a tiny bit tricky. The first step is to turn the camera 180 degrees so the lens is facing the front entrance of the birdhouse. There are guide hole that make this easy to do, but the tricky part is that there are four screws that need to be lined up and screwed into the wooden side. There are pre-drilled holes there already but I found my self having a hard time keeping the screws from falling into the hole used for the camera wire. The screws rolled right out, but it took me a few extra attempts to get it right.

Once the outer camera is fastened to the side you can slip the cover back on. Another neat trick is that you can tilt the camera down. To do this you can pull the camera forward a bit, rotate it, then let it snap into place. I tilted it down a notch to point the lens right at the entrance hole.

BZ2 9290

Mounting Accessories

The three accessory boxes were split up like this:

Wall and pole mounting hardware and extra predator guards

BZ2 9265

This box included a mixed bag of an accessories. In this box you’ll find the a charging cable you can use the first time you set up the camera. There's also a screwdriver you can use to mount the Nest and install the outer camera.

The Nest also comes with two alternate predator guards. One has a hole that is larger than the standard one and the other has a hole that is smaller. The smaller hole provides more protection from predators, but it means only smaller birds can fit comfortably through the hole and reside in your Nest. I chose to keep the standard guard on, but I may consider moving to the larger hole to attract larger birds in the future. The user manual shows you how you can access and then replace the guard.

One of the most important pieces in this box is a mounting plate. This plate attaches to another mounting piece on the back of the Nest. You can choose to screw the mounting plate directly onto a flat surface, using the screws included in this box. There is a paper template that you can use to help mark where the screw holes will go.

If you want to mount your Nest onto a pole there are two smaller hose clamps and two that are slightly larger. You can unscrew the clamp all the way and feed it into the mounting plate, then tighten it around the pole you are mounting it to.

Finally, there is a third options, using the tree-mounting strap found in another box.

Tree-mounting strap

BZ2 9267

You can feed the tree-mounting strap through the mounting plate, wrap it around a tree or a larger post, then tighten it up and lock it into place.

I really like all of the mounting options. I planned on mounting my Nest to a pole, but I’m going to keep the other accessories around so in case I need to move the Nest someday I’ll have the flexibility to try one of the other mounting options.

Solar panel

BZ2 9268

The solar panel is an awesome accessory. It saves you from having to remove the camera to charge it up—disturbing the birds residing in your birdhouse. It comes with two mounting options. There’s a screw-on wall mount that can be rotated to face the sun. The solar panel has a really long USB-C cable that means you can mount the birdhouse where you'd like and then mount the solar panel in an area where you'll get optimal sunlight.

The other option is a posable mount that screws into the ¼ inch standard mount on the back of the solar panel. This mount can be bent and wrapped around a pole or a fencepost. Just note that the posable mount can be found in a different box than the solar panel.

Installation

To mount my Nest onto a pole I fed two of the smaller hose clamps into the mounting plate. I also used the posable mount and attached that to the solar panel so I can mount that onto the same pole.

BZ2 9293

Before putting the mount on the pole I checked the orientation with the mount on the back of the Nest.

BZ2 9282

I see that Netvue just announced a poll that would work in this situation, but I had picked this pole up at a local garden center. I also picked up a squirrel baffle, just like I did for my bird feeder. Finally, I 3D-printed the topper to keep rain out of the top of the pole.

BZ2 9296
BZ2 9294

I added the solar panel in a way where I could hide it when you look straight on, but I might move this again after seeing how much sun we get over the next couple of weeks.

BZ2 9297
BZ2 9299

Software

I had already gotten familiar with the Netvue app from my previous experience with the Birdfy Cameras I purchased. Recently Netvue came out with a new app that’s dedicated to their Birdfy line of cameras. It seems more focused on sharing the story of the birds that reside in your Nest, as well as using AI features to help you identify the birds that visit your Nest.

The setup process is quite painless and I had the app connected to my camera in just a couple of minutes. The only thing I haven’t tried yet is the bird recognition AI feature, but I’ll likely turn that on in the spring, when we have some more traffic around the Nest. Within my first week of setting up the camera I had already had a visitor (my guess is it was maybe a House Sparrow).

IMG 2864

One of the features that stood out to me is that when your camera detects motion and captures a clip, the app gives you the option to "Collect" the bird in the clip. When you hit the "Collect" button, it saves the clip and tags it with the species of the bird. My kids—who are huge fans of Pokémon—really like this feature and the idea of building out a collection of the birds around our house.

Fin

The folks behind the Birdfy Nest really went all out to design this birdhouse. When I was doing research for my homemade birdhouses a lot of features that were suggested by other makers included things like adding a screen at the bottom, adding ventilation, adding notches below the entrance hole to help birds climb out, using a specific diameter for the predator guard and entrance hole, having a door that opens up so you can clean it every year, etc...

When I was building the birdhouses I did some of these things, but I didn't get around to adding all of these features (specifically the mesh screen). This birdhouse just comes with all of those things built into it. I can’t think of anything I would have done differently, feature wise.

There is something to be said for learning how, and then building your own birdhouse, but for most folks I think this is the way to go.

BZ2 9303
BZ2 9305

The Birdfy Nest is currently on pre-order at the Netvue Birdfy website and—as of this writing—there’s even a special Christmas edition that’s on sale.

🪺 ]]>
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

]]>
Chest Organizer https://wbrowar.com/article/maker/chest-organizer Wed, 10 May 2023 12:30:00 -0400 Will https://wbrowar.com/article/maker/chest-organizer We wanted to get a storage chest for our back yard because our kids were spending more time with friends out there and we wanted to make it easier for them to access their toys. Along with the better playing-outside weather, we were doing more grilling and I was looking for a place to store grates, charcoal, and other grill-related tools.

At first I planned on getting a separate storage cabinet for the grilling stuff, but we found a storage chest that was big enough to house everything. The tricky part was keeping everything organized and separating the toys from the fire-related tools in one big, empty box.

This project was a quick one and it helped clean out some of the leftover wood I had laying around. It also let me try out something new on the 3D printer.

Design

The basic idea was to cut the interior of the box in half, based on the size of some of the grilling components. Our grill was a 22-inch kettle grill, so the widest we'd need to go is around 22 inches. The grill we have has a grate with a 12-inch hole that is meant for accessories, like a cast iron plate or wok. It also came with a warming shelf that we used on occasion. We also had some cleaning tongs and matches that needed a place to stay.

On the toy side of things, we have various balls, bats, and catching devices. My kids’ reach is still pretty short, so we wanted to elevate all of the toys so they could easily reach in and grab what they need.

BZ2 7746

I put together a rough plan for everything so I knew what sizes I was working with, but along the way I changed things up based on where we wound up putting the chest.

As far as materials go, I had a bunch of leftover plywood and pine 2x4s, so my plan was to whip them all together using some glue and some brad nails, then use some oil or paint to seal them.

Grilling Dividers

I had plywood ripped at about 2 feet wide, which was perfect for the height of the chest. I left the height as-is and worked out the measurements along the length to cut it down into the lengths I needed it to be for each piece.

BZ2 7752

I used some foam packing pieces as a base and a couple of clamps and a long piece of scrap wood as a guide to cut the pieces with the hand saw.

By the time I was done I had cut all of the walls with the hand saw and used the miter saw to cut the smaller pieces down to the right size.

BZ2 7767

I planned on attaching them all together using glue and nails and I didn't go through the trouble to plan out any fancy joinery as the pieces should be plenty supported for their use. I did cut the smallest divider a little wider and then I use a router to cut a channel into its connecting pieces. While maybe unnecessary, it offers a little more support in the middle.

BZ2 7771

I went ahead and nailed these pieces together.

BZ2 7773

Next up I measured out the length and width of what would be the floor of the raised up portion of the grilling side. I cut that down with the hand saw and miter saw and put it aside.

BZ2 7775

At this point I tested the fit inside the chest and everything was looking good.

BZ2 7780

I wanted to add a little support to the raised floor, so I made a frame with a 2x4 and nailed that into the floor piece.

BZ2 7794

With that all set, I nailed the floor into place. Although not shown in this picture, I also added one small 2x4 piece in the corner for a little extra support.

BZ2 7795

I flipped the piece over and used a rounding bit along the tops of all of the dividing pieces to smooth them down a bit.

Making a Toy Table

Once I had the final size of the grilling side set, I used the remaining room inside the chest to create a platform for the toys.

I cut a leftover sheathing panel down the the right width and length to cover the toy side, then I used some 2x4s to create four legs and then some supports.

BZ2 7796

I used some 1-2-3 blocks and clamps to hold them up while I nailed the supports into place.

From there I thought about how to get the table in and out and went over to the drill press—which still had the 1½ inch hole saw bit in it from my birdhouse project—and cut a whole on one side of the middle of the table.

I used the same rounding bit on the router to smooth out the hole so it no longer had any sharp edges.

BZ2 7800

Quick Finish

Just like everything in this project so far where I was using scraps and leftovers I also chose to finish everything with some leftover spray paint. I had a couple of cans of red and black paint, but the black had primer built in so I decided to go that route.

BZ2 7802
BZ2 7803

A couple of coats later, I let the pieces dry and then I placed them into the storage chest.

BZ2 7816

Functional Labels

In the grill side of the chest I wanted to hang a couple of tools, so I needed some sort of hook in the portion that was the full height of the chest. I decided to design and print the hooks with my 3D printer.

I used Fusion360 to create a model that would wrap around the top of the plywood edges with a hook protruding out one side.

BZ2 7830

The fit came out great and the hook seemed strong enough for the things I would hang off of it.

I also wanted to test out some debossed lettering with my 3D printer, so I made an SVG label, imported it into Fusion360, and cut it into the base of the hook. I figured that if the icon was printed on the 3D printer’s plate it would come out clearer.

BZ2 7820

There was one part that got a little messed up in the print, but I figured that this was good enough to proceed with, so I made labels for some of the different sections and I printed those out using PLA.

BZ2 7824

It took a few tries to get the print right and I wound up settling with a couple of pieces that had some errors on them.

The thing I learned here was not to fill up the printing plate with too many pieces. Also, I will still need some more practice printing out text and next time I might consider printing it on a different side or rotating the print 90˚ so the text isn't printed on the plate.

Fin

BZ2 7825
BZ2 7828

If I were making a storage chest from scratch I might consider taking more time to focus on stability and finish, but I'm happy with the amount of effort I put towards making this work.

Eventually I plan on taking the platform out of the toy section and I left a little extra room on the grilling side for future accessories. For now this simple project will help keep things tidy for both play time and grilling time.

🍔 ]]>
Solar-Powered Bird House https://wbrowar.com/article/maker/solar-powered-bird-house Tue, 02 May 2023 07:00:00 -0400 Will https://wbrowar.com/article/maker/solar-powered-bird-house A few months ago I created this Mid-Century Bird Feeder and showed the finished photos to some family members. Upon checking it out, my mom made a request: she asked for a birdhouse made specifically for North American bluebirds. My sister was with us and asked for one, too.

I had a bunch of ideas for some cool and colorful bird houses, but after doing some research I landed on a lot of parameters and pointers that make for a successful bluebird house. So I dialed my ideas back a bit and—despite sticking to some standard birdhouse plans—this project led to a new tool and some cool experiences.

Design

In my research I found several great sites that shared the basic plans for bluebird houses. I made note of the optimal dimensions for the inside of the birdhouse and the height of the entrance hole. I also read about a few features you can add to help keep the entrance hole safer from predators, as well as a way to help newborn babies leave the nest for the first time.

One thing that caught my eye was that a lot of makers had added cameras to their birdhouses and bird feeders so they can see their avian guests up close. This was a whole entire rabbit hole that included lots of options, ranging from all-in-one camera kits to some how-to articles for creating your own DIY camera feed. Since this was a gift, I was leaning towards the camera kits that had software that was simple to set up and use.

With that in mind, I left the design of the bird house a little open-ended until I was sure as to what the camera hardware setup would look like.

BZ2 7599

Technology Testing

I did some shopping around when looking for a good camera kit to use. A lot of makers had something similar to a Wireless Bird Box Camera, so I started my search there. These looked very compact and there were lots of options to choose from.

I liked some of the AI bird feeders, like the Bird Buddy, that include things like bird recognition and activity notifications. However, Bird Buddy didn't sell a stand-alone camera kit and being a bird house, the AI recognition feature would go unused.

I eventually stumbled upon Netvue’s Birdfy Cam and it seemed to be exactly what I was looking for. I liked the idea of using a solar panel as a way to keep the camera battery charged up, so I bought the bundle that included both the camera and the panel.

IMG 2082
IMG 2081

The camera used a standard ¼ inch mount, so I used a couple of flexible lighting mounts to clip the camera and the solar panel to my bird feeder. I wanted to test the overall experience of capturing clips, streaming, and getting notifications via the app. Here are my general notes:

  • The software made things really easy to set the camera up and to change options. I really liked that you could flip the camera upside down and set the recording quality.
  • I liked that you could quickly save clips and that on iOS it created an album in Photos for storing the clips you saved.
  • The solar panel did its job just fine. While I don’t live in the sunniest area, we still got plenty enough to charge the camera and to keep it charged for the entire time I tested it.
  • The clips are only 1080p, and I hope one day they offer a camera that lets you record in 4k (even if it only streams in 1080p), however, recording at HD quality was still pretty good and I think most people would prefer better battery life over image quality.

Throughout the day it was fun to see the variety of birds that visited my feeder.

Next up I wanted to make sure the camera would work within the rough dimension of the birdhouse and I wanted to figure out the best viewing distance from the bottom of the bird house to figure out the final measurements.

I used my sketch to cut a cardboard box into a 5-by-5 inch tunnel and placed the camera at the top. With my phone streaming the camera feed, I marked the height that I thought worked the best.

BZ2 6986

I cut a hole in the cardboard and stacked up a couple of 1-2-3 blocks at the bottom of the model to get an understanding of what the height of a nest or some eggs might look like.

IMG 2096

From there I had finished up my plans and started gathering materials to build two birdhouses.

Building the Base

I wanted to use cedar for the birdhouses as it holds up well without any finish. I know the sun will eventually fade the color a bit, but I still picked pieces that had nice grain patterns and as few knots as possible.

BZ2 6993

I picked out lumber that was close to the width in my plans, and I trimmed down the rest of the pieces as needed.

BZ2 6999

I had figured out the height for the sides and the front and the back, so I used the miter saw to cut two piles—one for each birdhouse.

BZ2 6998

There was a slight color difference between each pile of wood so I decided that throughout the whole project I would try to keep the wood from getting mixed between each pile. This way each birdhouse would look more cohesive than if there was one or two pieces that had a very different grain pattern.

BZ2 7001

Once I had the walls done, I set my miter saw to a 30 degree angle and cut the diagonal top of each of the side walls.

BZ2 7004

I set my table saw to a matching angle and cut the tops of the front and the back down to match the side walls.

BZ2 7010

Because I was planning on using wood glue and brad nails to join the walls together, I wanted to give the bird feeder some extra support by cutting some rabbets into the bottom piece. To do this I first cut a deep cut onto the sides of the piece, then flipped the piece onto its side, adjusted the fence, and did a very shallow cut to trim off the hanging slice of wood.

BZ2 7012

I did another cross cut to add a trench into the front and back walls at the height to match the bottom piece.

BZ2 7015

I didn’t get it quite right the first time through, so I wound up trimming things down a little bit to fill in some of the gaps that showed up when I first put it together.

BZ2 7023

Once I got things to fit, I tested out the bottom and things lined up nicely.

BZ2 7022

WiFi-Enabled Roofing

With the walls and floor cut to size, I did another test of the camera position.

IMG 2101

This time I had to factor in where the camera’s antenna would need to go, as well as to make sure I had room to plug in the cable for the solar panel.

BZ2 7024

I grabbed the back wall and used my router to create a channel for the cable to to live in, as the camera plus the cable wound up being about a ¼ inch too tall.

BZ2 7026

At the top of the channel, I used a slightly larger cutting bit to create space for the antenna to stick out the back of the birdhouse.

BZ2 7027

My plan was to mount the camera onto the roof and make it so the roof could be pulled out without disturbing anything going on in the bottom of the birdhouse. I don’t think any bird would tolerate any disturbance to their nest, so this gave better access to the camera in case some technical support was needed.

I still had to figure out exactly how to mount the camera to the roof, but I did know the dimensions I’d be using for the top piece.

BZ2 7029

The lumber I had wasn’t wide enough to allow for an overhang along the sides the birdhouse, but luckily I had a really long and narrow piece of scrap cedar laying around. I cut the strip down to the same length of the roof piece and then glued two strips onto either side of the main piece.

BZ2 7031

After cleaning up some glue drips I ran the roof pieces through the planer to smooth them down and to get them to the thickness I wanted.

BZ2 7035

Once again I tilted the blade on my table saw and did a cross cut to match up the angle on the top and bottom ends of the roof pieces.

BZ2 7043

Speaking of Access

One of the things I remember from the birdhouse we had when I was growing up is that every year we would clean out the inside of the birdhouse to get it ready for the next bird who would come and make it their home. To do this we had a hinge on the front panel of the birdhouse and we would lift that up for access to the inside.

I wanted to add some hinges to the left side panel so you could open it up like a cabinet door. I wanted to avoid a big latch on the front of the birdhouse, so I decided to try using a magnet to keep the door shut throughout the year.

BZ2 7045
BZ2 7048

At first I thought I had picked out a strong magnet and a metal plate that would do the trick, but after actually getting it screwed into place I realized that the screws made it so the magnet wouldn't get close enough to the metal plate to provide a strong enough connection to hold the door on securely.

I wound up finding better hardware with a bigger magnet, more surface area, and recessed screw holes that make the screws flush with their surface.

BZ2 7050

I used a chisel to widen up the holes used for the original magnet.

BZ2 7052

Door Groove

To make it a little easier to open up the door, I wanted to use my router to create a groove along the bottom-front of the side door. While I didn't have a router table, I did have a guide attachment to my router that got me pretty close to a router table fence.

BZ2 7058

I lined up fence, turned the router on, and then slowly dropped a test piece onto the bit and pulled it across the guide.

BZ2 7060

To my delight, this worked as planned! I grabbed the side door pieces and carefully repeated my movement until I created the groove at a few inches in length.

BZ2 7063

Ventilation

While I was working on the sides, I wanted to add some air holes to the top of each side for some extra ventilation. I wanted to drill holes in the shape of a circle using my drill press. While I know you could use a compass and some lines to map out the coordinate for each hole, I chose to go the visual route using a drawing app on my computer to add points to a circle and I printed that out a few times across a piece of paper.

BZ2 7054

I taped down the paper to the wood and used a center hole punch to mark where each hole would go.

BZ2 7055

I pulled the paper off of the wood and drilled each hole where the marks were made.

BZ2 7056

I cleaned up the holes a little bit, but I knew that I would eventually be sanding these pieces so I could come back and fix them along with that step.

BZ2 7057

Extra Support

At this point, because of the door, the base of the birdhouse was in the shape of a U. I wanted to add just a little extra support but I didn't want to get it in the way of the roof or the camera. I found some scraps from my previous cuts and created a mortise and tenon joint to hold them into place.

BZ2 7457
BZ2 7458

This was my first time doing this kind of joint and while it wasn't perfect, using cedar did make things easier because the wood is so soft and easy to cut with a chisel.

BZ2 7459

I used a vice to help cut the tops of the tenon side of the joint and then used a drill press and chisel for the mortise.

BZ2 7460

Hello, Benchy

At this point in the project I had been thinking about different ways to mount the camera onto the roof piece. I originally planned something out with a series of wooden pieces, but I wanted to make it easy to take the camera out if for some reason it could no longer be of use.

This was one of many little problems I wanted to solve in projects like this and it just so happened that I work with lots of folks who are into 3D printing. Our discussions over the years have had me interested in getting one, but they always seemed like they require a lot of work to get it up and running and maintain it over time.

I discussed the current state of 3D printers with a few people who have had experience and started to look into Prusa’s MK3 kit, but a friend of mine redirected me to the relatively new Bambu P1P. The P1P was a little bit cheaper than the MK3, but it promised faster printing speed and it seemed to be a little more accessible to 3D printing newbies like myself.

I took a chance and ordered the P1P—on the day before the similar Prusa MK4 was announced—along with a bunch of random PLA Basic and PLA Matte colors. In about a week the printer had arrived and with very little effort I had it set up and in about 20 minutes I had printed my first Benchy.

IMG 2133

While I was waiting on the printer to arrive I looked into a couple of apps, Bambu Slicer and Fusion360. Fusion360 has been in the maker community for years and I had first heard about it for making woodworking plans. On the one hand it is very accurate and using things like User Parameters sounded like a great way to plan out woodworking projects, however, I found that I could get by with some simple sketches for now.

For 3D printing, however, that accuracy is important and Fusion360 is an amazing tool. If you search for Fusion360 tutorials on YouTube, this video is likely to come up—for good reason because it does a really great job of going over the basics:

Following along with what I had just learned, I created a bracket in Fusion360, then sent that over to Bambu Slicer. to prepare the print.

Screenshot 2023 04 08 at 2 19 38 PM
Screenshot 2023 04 08 at 2 20 09 PM

Since I hadn’t spent too much time with the printer yet, I had stuck to a lot of the default print settings. I printed my first design and tested the fit with the camera.

BZ2 7482

I had picked up a few ¼ inch screws and had to use a little pressure to get it to fit into the bracket, but for the most part it had fit and confirmed the size of the bracket would work.

BZ2 7483

I jotted down a few notes to make revisions, then I decided to test the strength of the bracket. Within seconds the side of the bracket snapped and right away I realized where support was weak.

Screenshot 2023 04 08 at 6 12 15 PM

I went back into Fusion 360, made the adjustments I had written down, and then added some support along the length of the bracket.

BZ2 7491

After printing out this revision I could tell right away that this was more solid. I had a few more minor tweaks to make and printed out a new revision. This time I switched to a beige-colored PLA that I got for woodworking projects.

BZ2 7487

I used the camera to figure out where the bracket would need to go onto the pieces of wood that would be attached to the roof.

BZ2 7489

I drilled a few pilot holes and then used wood screws to screw the bracket into place.

BZ2 7490

Assembly

At this point all of the wooden pieces were cut and ready for sanding. I knew I wouldn't be applying a finish to the wood so I went with 120 grit paper to smooth things down. I left the inside a little rough, as that might help the birds move around inside, and then I used a 220 grit for the final sanding on the outside pieces.

BZ2 7492

I grabbed my wood glue and used my brad nailer to assemble the floor and the walls. Keeping in mind the width of the door, I used glue and a clamp to assemble the mortise and tenon support.

After this dried I did one more round of sanding around the outside where needed.

BZ2 7494

For the roof, I lined things up and clamped down a piece of wood to use as a guide while I measured out the location of the roof pieces.

BZ2 7495

I used some wood glue to hold the roof pieces together, then carefully drove a couple of nails in from the inner part of the roof. While the glue may be enough to hold it together, the nails add a little extra support.

BZ2 7497

I re-added the magnet hardware to the door and the front wall, then use clamps to hold the door onto the side of the box. I mounted a couple of brass hinges along the back of the birdhouse. I used brass because it's weather resistant.

BZ2 7499

While the hinge and the magnet worked well to keep the door shut, I decided to add a couple of eyelet hooks to the bottom of the birdhouse. These could act like a lock by using a string or a small zip tie to hold them together.

IMG 2217
IMG 2218

One of the things that is highly recommended for bluebird houses is to provide young birds with something to grip their talons on as they try to climb out of the entrance for the first time. I used a rotary cutting bit to create some grooves beneath the entrance.

BZ2 7507

Another recommendation is to strengthen the entrance hole to avoid wear and to keep predators from scratching their way in. I found some 1½-inch plates and screwed those over the entrance holes.

With that, both bird houses were assembled and ready for delivery.

BZ2 7513

My mom lives nearby so her birdhouse could be hand-delivered, but my sister’s birdhouse would need to be shipped out of state. I reused some packing materials and did my best to secure the birdhouse in place. Because of the battery and extra weight, I had ordered her camera to be delivered directly to her as a separate package.

BZ2 7525

Fin

BZ2 7594
BZ2 7592
BZ2 7595
BZ2 7597

This project was fun in that this is the first time I was building two copies of the same thing. For the most part they came out identical, but there are a couple of places where the second time I did something I felt like I did a better job with it. I think that all comes with learning new things and making small improvements as you go.

This project also got me into 3D printing and since getting the printer up and running I've printed a couple dozen models off of sites like Thingiverse and Printables. I also started creating some more custom things in Fusion360 and I'll write about those in a future article.

As far as birdhouses go these are by the book, with a little added technology included. I learned that building a birdhouse can be super simple and scrappy, or super complicated and detail rich if you want them to be. As long as birds like them and can safely raise their family and send them on their first flight, then it’s a win.

🪺

Epologue

Speaking of winning, the research for this project also led to a really cool opportunity.

Because I had recently purchased a camera from Netvue, I received an email about a birdhouse product that they were working on that just happened to have its own camera situation. The product, called Birdfy Nest, had a lot of similarities to the general measurements in my birdhouse design, but they also include a camera on the outside so you can see birds as they enter and leave the birdhouse. It's also got a few nice features like a wire mesh floor and some nice ventilation holes on the roof.

The email was a call for testers and after applying and waiting a few days I was selected to join the testing program! This means that I’ll be getting my own Birdfy Nest to set up and try out, in exchange for providing feedback to the team behind the product. I’m really looking forward to setting it up and seeing if I get some birds of my own. After building my own birdhouses, it’ll also be cool to see how they’ve done things differently and I think it’ll help in providing feedback based around my own experience.

🐦 ]]>
Making More Headphone Hooks https://wbrowar.com/article/maker/making-more-headphone-hooks Sun, 02 Apr 2023 07:00:00 -0400 Will https://wbrowar.com/article/maker/making-more-headphone-hooks In late 2020 I made my own holder for the pair of headphones that I was using in all of my WFH Zoom and Teams meetings. This design fit the curve of the headphones and the spacing between the wall made it easy to return the headphones to their home, just as easy as it is to remove them for use.

Since then, I designated a couple more pairs of headphones that each serve specific purposes around the house. I bought a pair of wireless gaming headphones that stay in the living room and don't stray too far from the couch. Also, as a Christmas gift for the family, I bought an electric piano and reused a pair of wired headphones as monitors so we can learn and practice on the piano without disturbing everybody in the house.

Both headphones are resizable and are generally around the same size as my office pair, so I wanted to reuse the design of the original hook, but change the mounting setup to fit each location.

Design

In 2018 I added a thin shelf next to our living room couch for a place to store my iPad and charging cables. I wanted to keep the gaming headphones in the same general area, so my thought was to hang the new hook off that shelf. I also had some scrap left over from the shelf so I could match the wood to make it a cohesive piece.

I originally wanted to use a piece of wire or some sort of metal connected to the shelf and driven into the back of the hook to hold the headphones up. It had to be strong enough so the headphones wouldn’t move or fall off.

Because I knew the thickness of the shelf, I could get a way with making this hook a little deeper than my previous hook design.


The piano lived in another room in the house and next to it was a three-shelf, rolling utility cart made of metal. We had an extra hook with a strong magnet on it that would be perfect to attach to the cart, so I thought I could bend the hook on it and use that to hold up the wooden hook for the piano headphones.

Being near a shiny black electric piano, I thought this would be a good place to use a black polyurethane finish that I had used on my side table. At that point it didn’t exactly matter what wood I used, but I quickly got an idea for that.

Revisiting a Classic

So the wood for the gaming headphones was already selected, but for the piano I thought this would be a great use of scraps from an actual piano, since I had a bunch of that laying around already. I pulled out a piece that was around the right thickness, but I would need to cut it down to a workable size.

The wood used for the piano felt like it was generally strong but not too dense. It was easy to cut and shape. I wasn’t sure what type of wood it was made out of, but it would make since if it was made from fir or birch as that’s what a lot of pianos were made of at that time.

I used the original headphone hook to figure out the size and then cut down the scrap based on the direction of the wood grain.

BZ2 6088

On the gaming hook I wanted to cut the wood down to create the least amount of wasted material, so I marked and cut it down to the length of the original hook, with the plan to double it up with a glueup.

BZ2 6092
BZ2 6093

I cut the gaming hook again in the middle, then turned it around so that the wood grain wood match up when I laminated it. I wanted the seam to live where the headphones sit so when you look at the front face of the hook you wouldn’t see where the pieces were glued together. This all worked out well and the dimensions fit the wider band of the gaming headphones.

BZ2 6095

With the glue up done, I traced the original hook to draw the outline onto each piece.

BZ2 6106

I used the table saw to cut them down to just about the height of the final hook—leaving a little extra material knowing that I would be sanding it down to the curve shape.

BZ2 6107

Following the process of the original hook, I cut down the pieces and used a belt sander to get a rough shape. Then I used a handheld sander and 220 sandpaper to smooth out the curve.

BZ2 6110

I used a drill press to create the mounting hole for the piano hook, based on the hook on the magnet.

BZ2 6112

For the gaming hook I tried experimenting with a few different materials. I started with a thick piece of sculpting wire, but found that to be too flexible. I tried bending a wire hanger but found that to also not be a great fit. As I was looking around my workbench I realized I had collected a ton of allen wrenches over the years, so I selected one of the shiny ones and cut it down with a hacksaw.

I used the drill press to drill a hole at the size of the allen wrench into the back of the wood piece. I took the shelf off of my wall and drilled a hole into the center-rear of it.

Matching Finishes

I still had the same wipe-on poly that I used on the original slim shelf, so I mounted the hook without any glue so it would stand up freely, then I wiped on the finish. I let that dry and added a second coat later.

BZ2 6118
BZ2 6116

I also did a dry mount of the piano hook onto the magnet so I could wipe on the black poly mix.

BZ2 6135

I attached the magnet to the top of a jar and repositioned it based on what side I was finishing. I started with the back and did just one coat there, then made sure to eliminate bubbles as I covered the front and the sides. I really liked how you can see the grain of the original piano wood behind the shiny black polyurethane.

BZ2 6127
BZ2 6130

Mounting in Place

I mixed up some strong, thick epoxy so that once they are mounted the hooks wouldn't move or reposition themselves. This was especially important for the gaming hook because it would be attached to the bottom of the shelf and there was no other support to hold it into place.

BZ2 6139

I used the point of a wooden skewer to guide the epoxy into the holes, then after putting the metal mount in I wiped off the excess epoxy while it was still viscous.

I wanted to make sure it was straight so I measured and aligned it so the hook was flat in both directions. Once I had that down I moved it aside to a place where it wouldn’t get bumped or moved while it was drying.

BZ2 6142
BZ2 6136

While some accuracy was important for the piano hook, I just had to do a little bending of the magnet’s metal hook to get it lined up. The metal was pliable but strong enough to hold up the pair of headphones that would go on the hook.

I dropped some epoxy into the back of the piano hook and then connected the magnet to it.

After it dried I moved it over to the utility cart to make sure that the angle would be flat when attached to the cart.

BZ2 6153

Fin

The design for the original headphone hook works well and I use it every day. Reusing that design made it just as easy to remove and store the new headphones. During the process of this project I realized that I have a ton of pairs of single-purpose headphones (one for mowing the lawn, one for playing drums, etc ...). So far these are the only ones that warrant creating hooks, but if I ever need to get a new pair for something, I know which design I’m coming back to. 🎧

BZ2 6156
BZ2 6155
BZ2 6166
BZ2 6170
]]>
How to Build an Upright Grand Piano https://wbrowar.com/article/maker/how-to-build-an-upright-grand-piano Sat, 01 Apr 2023 07:41:00 -0400 Will https://wbrowar.com/article/maker/how-to-build-an-upright-grand-piano When I work on a maker project I usually like to take a lot of photos and write up a blog post to document the process. This is usually done within the weeks following the project because at that point a lot of details are fresh in my head and I can recall the new things I’ve learned along the way.

Today I noticed a folder full of images on my computer from a few years ago. It contained photos of a piano, and with this being a maker blog, I can only guess that at some point I had made my own upright grand piano 🤷‍♂️

I don't recall much, so here's my best guess at how it all went down:

Getting Framed

BWR 2733

Pardon my dust. My first step in this process was to get the back portion of the piano all done. As you can see here it looks like I started with a frame and some beams for support.

BWR 2732
BWR 2731
BWR 2730
BWR 2729
BWR 2728
BWR 2726
BWR 2723
BWR 2721

The frame looked easy enough to manage. Just needed to hammer a few things into place and we’re all good.

BWR 2720
BWR 2719
BWR 2718

Piano Heavy Metal

Okay so I am still fuzzy on what exactly happened here, but I definitely remember this piece—and so does my back.

BWR 2717

One thing about pianos is that you need this heavy plate of iron to hold up to the intense amount pressure the strings put on it. This is like the skeleton of the piano, providing the structure that everything else is built around.

BWR 2713

So I flipped the backing frame over and set in some guides to support the iron plate.

BWR 2712

A perfect fit.

Next up came adding the tuning pegs. The pegs went through the metal plate and into a piece of wood that was predrilled with holes for each peg.

BWR 2711

I remember starting off screwing these by hand, but I found the right drill bit to use and that sped things along greatly.

BWR 2710

I guess you can order just about anything online these days because I got this box full of perfectly sized piano strings.

BWR 2709

One important thing to note here is that messing with piano strings on a piano is no joke. Although the metal plate is strong, you can feel the tension change as you loosen and tighten sections of strings. For this next part I made sure to wear eye protection, gloves, and long sleeve clothes for protection.

BWR 2708
BWR 2707
IMG 0991

With the strings attached to the pegs, all I had to do next was lift the piano up onto its feet. Up you go!

BWR 2705
BWR 2704

To make it easier to move the piano around, a pair of handles were added to the back.

IMG 0990
BWR 2701

The Key Takeaway

So what’s a grand piano without a set of 88 black and white key? And what’s a set of keys without a place to put them?

First things first, a tray and support structures are added to to the frame of the piano.

IMG 0989

Within the tray go a bunch of pegs that are used to hold up the keys. The keys sort of do a balancing act on these pegs.

BWR 2700

Just like the strings, I got a box of keys over here. Shipping must be great around here because these are perfectly organized!

BWR 2698
BWR 2697
BWR 2696
BWR 2695

Next up came the module that contained all of the hammers and the connections to the keys.

BWR 2694

That is set in place and a keylid is installed to protect the keys when not in use.

BWR 2693

From there, a cover with some sweet carvings on it is added. I’m just going to assume that I did these carvings. Like freeform sketching, but in wood.

BWR 2692

And from there I guess we're done...?

Fin

So making a piano probably wasn’t all that hard to do. If you have a free afternoon to spare you can probably follow this guide and throw one together for yourself. Happy pianoing 🎹

Okay, so here’s the thing. When we bought our house it came with this piano. While my wife and I thought this was a bonus, that we’d fix it up and hire a tuner to come in and help us get it working, it turned out to be a much larger undertaking than we thought.

The piano was unplayable and several of the keys were misaligned. I took apart the piano enough to find that a lot of the leather straps that connected the keys to the hammers were so old that many of them had disintegrated. A few key pieces of wood were cracked—maybe damage caused by moving it at one point. We didn't know the history behind this piano but it seemed like it was moved into the house by the previous owner but that it hand't been used in decades.

First we tried to figure out what it would take to fix it. Lots of parts would need to be sourced or custom made. There were so many parts that were worn down that it seemed like we'd be replacing a lot of the internals. The cost and the time commitment were a lot for a couple that just gave birth to their second kid.

Our next step was to find a new home for the piano. I posted on Facebook groups and online marketplaces and found that not only did our piano not gain any interest, but there were also many other folks struggling to offload pianos that were in even better shape than this one. Local organizations that resold donated furniture only took working pianos and the local music-related organizations I checked with didn’t want to fund the repairs or tuning needed to get the piano back into working condition.

We decided to get the piano out of the house, but the only way to remove it would be to tear it down into pieces the garbage collection would take. I found a friend who just happened to be looking for a set of piano keys for a project so I gave those to her, and the heavy metal iron plate went to a guy who takes metal scrap to the junkyard. As far as the wood in the piano goes, I still have a lot of it and have used some of the support frame as part of my work bench and for a few other things.

It would have been cool to get the piano working again, but during the teardown process I got to learn a lot about the inner workings and the details put into a piano like this. I can imagine the people who originally put it together took several weeks to get all the precise mechanics in the right place. Here are a few more shots I got before demolition to appreciate some of the piano’s details:

BWR 2666
BWR 2667
BWR 2670
BWR 2668
BWR 2681
BWR 2672
BWR 2675
BWR 2677
BWR 2680
BWR 2656
BWR 2687
BWR 2655
]]>
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(`

This was my first time cutting glass, so of course I watched about a dozen videos on YouTube on how to do it. While most of the videos said the same thing, one tip from this video, in particular, was very helpful: After you score the glass with the cutter, placing a dowel right below you break line helps to create a clean cut.

I forgot about this tip and in my first attempt I had to break off the glass in a couple of places, making the cut uneven. When I did use the dowel the breaks were perfectly flat and precise.

With the glass cut out I placed it into the hopper. As cool as this looked, after I took these photos I realized that I needed a bigger gap at the bottom of the hopper so I wound up with a gap of about 3/4 inch.

BZ2 3450
BZ2 3451
BZ2 3453

The final step was to attach the mounting hardware to the base. I got this mount at a garden store along with a long pole and a base that you screw into the ground. I used a post leveler that I got for putting in a mailbox and used that to level out the pole.

BZ2 3454

Fin

BZ2 3480
BZ2 3478
BZ2 3493
BZ2 3485

This was a fun project and it gave me and the kids a chance to spend some time together in the wood shop. While the kids role was mostly to watch and learn, they helped contribute to some of the design choices along the way. Both the birds and I agree that these kids have great taste. 🦜

]]> Creating a Changelog in Guide 3 https://wbrowar.com/article/code/creating-a-changelog-in-guide-3 Fri, 27 Aug 2021 22:44:00 -0400 Will https://wbrowar.com/article/code/creating-a-changelog-in-guide-3 A Plugin Feature Returns Home

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.

Communicator widgets

Making Data

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.

Guide editor blank

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 cssTwig 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.

Displaying Our Changelog

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 %}

Fin

Changelog widget
Changelog page

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.

P.S. On Abandoning Communicator (and Content Stats)

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 3 https://wbrowar.com/article/code/guide-3 Wed, 25 Aug 2021 05:52:00 -0400 Will https://wbrowar.com/article/code/guide-3

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.

Organizer

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.

Guide organizer grid

Here are a few other changes that the new Organizer brings:

Grouping Guides

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.

Guide cp section

Element Edit Pages

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.

Guide edit pages

UI Element Picker

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.

Guide ui display

Everywhere Else

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.

Guide area uri

Editor

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.

Guide editor snippets

Snippet Components

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.

Utility Classes and Vue.js

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.

Editions

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.

FIN

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 A Cappella Book https://wbrowar.com/article/maker/the-a-cappella-book Sun, 07 Mar 2021 09:54:00 -0500 Will https://wbrowar.com/article/maker/the-a-cappella-book My history with The A Cappella Blog went from being the friend who hangs out with the crew, to being the unofficial photographer (for the shows I could make it to), to the designer/web developer who established the look and feel of the site and its printed materials. My friends Mike Chin and Mike Scalise handled the things they were good at—writing, coordinating people and events, reviewing a cappella shows—and they trusted me with the brand and other visual things.

In the early 2010’s my day job consisted of developing interactive Flash components and client websites, and while I got to contribute to the direction of the designs I was working with, I had almost entirely made the shift over to development and the production side of things. Working with the Mikes on ACB gave me a place to use my background in graphic design and to dabble in the things I was excited to see my design colleagues do on a daily basis. When the opportunity to lay out and design The A Cappella book came along, I was excited to be a part of it.

Cover Design

To be honest, I don’t think I had given enough thought into what the cover should look like. The Mikes and I had thrown around a few concept ideas based on things that we liked, but our favorites ideas looked a lot like what other blogs or writers were doing. I specifically remember liking the idea of creating a microphone sculpture out of paper and shooting that, but the choice to do a single microphone alongside the title had been played out.

Lone microphone
The banner image for The Lone Microphone, another music blog that was active in 2013.

At some point we settled on the idea that since this book was tied so closely to the website that we could design the book to match the website’s current look and feel. At this point, we had some nice photography to work with and had established this look where the image at the top of the page would get cropped over a gray background and the colors that highlighted the image would be blurred and faded into the content below. It was all based on some fancy javascript and canvas work we were doing on the website at the time.

While we didn’t exactly know what the photo at the top of the cover would be yet, we had started down the road on a concept showing the Mikes in the audience at a performance or somewhere on stage like they would be when hosting an event. With our goal to create a cover that would get the concept of the book across to potentially Kickstarter backers, we found an empty theater and did a quick photo shoot.

After trying a few options, we laid out what the cover could look like and prepared to make a few mockups.

Cover flat
Cover ipad ereader

Because the iPad had been around for a couple of years I put together an e-book mockup, and since we expected to distribute more of the printed version of the book I wanted to create a printed mockup and photograph that as the main Kickstarter image. Because we only needed to show enough of the book to get the idea across, I had designed a cover and spine in the proportions that we planned the printed book to be.

I had found a book that I had read and no longer needed and thought that we could glue the cover design onto it for the mockup.

Cover proof print

I printed out the cover design and used the physical book to guide my scoring marks and trim cuts. I used some mounting paper to adhere the design over the book’s existing paperback cover. At this point we had a good enough representation of the final product to begin advertising the campaign on Kickstarter.

Book mockup
Book mockup 2

Kickstarted

Just like the work on the website, the book was a labor of love for me and the Mikes. We didn’t expect to make a lot of money on the book, but we did want to cover a few costs to help with the book’s production. We turned to Kickstarter and set up a campaign that would get advertised to readers on the website.

Kickstarter sidebar

During the campaign we sort of hit readers over the head with a Kickstarter campaign widget in the sidebar of the website. This led readers to the campaign page that included a video that we had shot at the time we had done the cover photo shoot. Just like working on the mockup of the book cover, I had learned a lot about shooting and editing video from the people I had worked with.

After some promotion on Facebook by the Mikes, the campaign was successful with just a couple of days left to go.

Kickstarter page

This had meant that the work on the real book was ready to begin. As we made progress with the book, we kept Kickstarter backers and the site’s readers up to date by replacing the campaign widget one that linked to the latest news about the book.

Kickstarter update

Book Photography

While the Mikes were writing the content for the book I worked on the design of the cover. The layout didn’t change too much from the mockup design, but we decided not to go with a photo of Mike and Mike on the cover. I shot a few photos of microphones or other stage elements and we took some time to think through ideas while going to shows and working on content for the site and the book.

On the site we had taken photographs of collegiate groups performing and we had a collection of photos that we had begun using on the site or in promotional material.

Acb group photo 1
Acb group photo 3
Acb group photo 2

I don’t think we ever considered using any of these as we wanted the book to feel like its own thing, but in these photos there is a common look that you get from the type of stage lighting used in college theaters and auditoriums.

Since we had connections to a local college theater, we decided to get some friends together to do our own photo shoot that would be set to look like a live performance. I had purchased some LEDs and work lights and set them around the stage, we had asked our friends to come dressed in black and white dresses and slacks, and I directed and shot the cover using my Nikon 300s.

The thing I was going for in this photo was to capture the moment just before a song starts where someone in the group has just played a note into a pitch pipe and was just dropping it back into their pocket. Because the book was for students and performers working on their craft, the idea was that they might recognize this moment of calibrating the group, even if the audience wasn’t fully aware it was happening.

Book cover photo final

It was intended that we focused on the hand and the pitch pipe, and all of the folks in the group would be cropped out or blurry. We avoided using any specific school colors and tried to make it feel like we could have been capturing any group in the collegiate a cappella world.

The one thing I wish we had done was get more diversity into the photo. One of the great things about seeing groups from around the country—and sometimes around the world—is that the members come in all shapes and sizes. We were lucky to gather some close friends who gave up their time to help us out, but given the chance to do it again, I would have tried harder to get folks who better represent the people we were meeting.

One little Easter egg that I don't know too many people caught is that to fill in the crowd of people on stage, Mike Scalise—alongside his then girlfriend, now wife, Amy—made it onto the cover.

Designing the Content

For the entire run of The A Cappella Blog the articles were set in a serif font and san-serif was used for things like captions or sidebar content. We kept that going for the book by picking from a print typeface that we felt was easy to read over longer chapters. Each chapter had a title and some included black and white photos to illustrate the topic. The book spanned about 275 pages and the last 60 pages were made up of a snapshot of the website’s Group Directory section, detailing the names and makeup of all of the groups we had been keeping tabs on over the years.

Page layout

The book was laid out in InDesign and it was created for the size that we would be printing it in. Using master pages, inline images, and paragraph styles made it easy to update the content of the book or the Group Directory pages as changes would be made.

During this process I had printed a few pages on my printer, but to really test its readability we had ordered copies of the printed book and waited to get those before making design revisions.

Book text

On Demand Printing: The Good, The Bad, The Weirdly Cut

From the get-go we determined that we'd have a digital edition of the book and a printed edition. Because we planned on self-publishing the book, we would have to do all of the work to print, store, and distribute copies. To make this easier, we decided to go with Amazon’s print-on-demand service. It would digitally print your book and ship it to the buyer on a per-book basis, or in small runs.

Using Amazon meant that the profit on the book was slim, but we had decided that the goal was to distribute the book and that making money on it was secondary. This on-demand setup allowed us to set it all up and run itself while we focused on going to shows and creating other content for the site.

This also meant that the quality of the printing was in Amazon’s hands, so we took some time to do some test runs and iterated design tweaks until we landed on the quality that we were happy with.

Book versions

The first thing we did was send the first draft of the book along with the mockup cover to get a feeling for the size and print quality. The quality of the interior pages was great and the cover was something like what we had expected from digital printing at the time.

After that we had taken the final cover photo and made some updates as we dialed in the final layout. At one point we created a version of the cover that let us see what the main photo looked like at a few different brightness settings.

Book test printing

After one last proof, we finalized the design and focused the rest of our time using the mockups to proof the book and focus on content updates.

One thing that was a result of using Amazon’s on-demand printing service was that when we went to order the final copies of our books we noticed that some of the books were cut incorrectly. While we had bleeds in place to accommodate variations in cuts on the front and back faces of the cover, the issue was that it looked like the book was settled on a slant when the book was cut so the whole book was crooked if you looked at it from its spine. When working with a local printer or book binding company, I'd expect their standards to be high enough that if you asked they would have corrected this. Being Amazon we had given up on the idea that they would reprint and resend the book to us and we had let it go.

Fin

Book website photo

Working on the A Cappella Book was another random design-related thing I’m glad I got to do. While I'm happy creating websites, it was a fun exercise to work on a tangible item that occupies real space. Just like everything I did for The A Cappella Blog over the years, I appreciate the Mikes giving me the chance to be a part of it.

The A Cappella Book had gone on sale for the past 5 years, and as of this writing we’ve decided that we accomplished the goal we wanted with the book and it’s now made available as a downloadable PDF. You can read it by downloading the PDF on The A Cappella Blog website.

]]>
Our Own Personal Vaccine Checker https://wbrowar.com/article/code/our-own-personal-vaccine-checker Tue, 16 Feb 2021 19:33:00 -0500 Will https://wbrowar.com/article/code/our-own-personal-vaccine-checker I’m done with COVID. It’s the worst and I can’t wait for it to go away. While I can understand a bit of the hesitation some people have with the COVID-19 vaccine, I’m on team "get it in me", and I’m counting down the days until we collectively kick this thing in the capsid.

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.

Approach

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:

Notifications

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.

Scriptable Code

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.

Automating with Shortcuts

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:

  1. Tap on the Automation tab and tap the plus icon to create a new automation.
  2. Tap on Create Personal Automation to run this script on your device.
  3. Select Time of Day and choose when and how often the script should run. Click Next when you’re done.
  4. This step may be different depending on what Siri suggestions are shown, but to be sure you can add your script, tap on Add Action, tap Apps, scroll down to Scriptable, then select your script from the list. NOTE: depending on how complicated your script is, you can set Shortcuts to run your script within the Scriptable app in this action’s settings.
  5. After tapping Next and getting to the New Automation summary screen, turn Ask Before Running off and then hit Done to finish set up.

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.

Shortcuts automations

Fin

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. 💉🦠

]]> Headphone Hook https://wbrowar.com/article/maker/headphone-hook Sun, 01 Nov 2020 18:23:00 -0500 Will https://wbrowar.com/article/maker/headphone-hook Working in 2020 means making whatever space you have available your base of operations. When buying our house, we knew that the extra bedroom on the first floor was to become some sort of office, but after moving all of our stuff in it became the room with all of the unpacked stuff, and a couple of desks. Since we've been working from home so much this year, we’ve taken some time to spruce our little office up and make some quality-of-live improvements.

One of those updates was to add in a set of metal pegboards, by a company called Wall Control, for some easy-to-get-to storage. This gave me the goal of getting some stuff off the desk, including my everyday headphones.

WBZ4473

Design

The pegboard works with standard pegboard hooks, but you can also buy flat hooks that go into slots along the surface of the pegboard. I like these because they are easy to move, but once you get them on they stay in place. The thing about these hooks is that if I were to just hang my headphones directly on the hooks they would absolutely ruin the material of the headband part of my headphones over time.

WBZ4443

As far as things you can buy go, I've seen some hooks that go under the desk and stands that sit on top and most of them have some sort of flat or curved hook that contours to the shape of your headphones. I decided to take a block of wood, create a rounded edge to rest the top of the headphones on, and attach them to the flat pegboard pegs.

The Things You Learn in Wood Shop

After cutting the wood down to the height I wanted, I used a 6 in. bit as my guide and drew the cut line onto the wood. I then walked around my basement shop and tried to figure out what tool would work the best to take a rectangle and cut it down to a smooth arch. I recently got a drill press, so I thought that maybe I’d drill a bunch of holes close to the line and then cut away and sand it down from there. Looking at my table and miter saws, I thought no way is that happening.

WBZ4445

I didn’t actually have wood shop at my high school, but in college I had a 3D design course in RIT’s well-equipped wood shop. There we had access to a band saw—which would have been perfect here—but I also learned how to use a belt and disc sander. While I know it would be cheaper to just buy a headphone stand, I used this as an excuse to pick up a belt sander that I had been thinking about getting for a couple of years.

WBZ4447

It didn’t take long to get dust flying and for the piece to begin to take shape.

WBZ4449

Next up I got a couple of the pegboard pegs and measured and cut them to the depth I wanted them to be. I used a vice to hold them still and slowly cut them down with a hack saw.

WBZ4450

Going back to the wood piece, I measured out and drew guides for the pegs to where they would be inserted into the back. I figured the drill press would give me a straight cut down into the wood, so I got a clamp and fired it up.

WBZ4454

I drilled one hole at the top and one at the bottom of the guide, then I drilled through the middle and then used a screwdriver as a chisel to cut away the middle portion. After a quick dry run, the pegs fit perfectly.

WBZ4459

Sanding Surprises

As I was sanding down the wood block with a finer grid sand paper, I noticed that the color of the wood started to change. As if there was a dye or stain already on the wood. I bought it from a lumber store that often reclaims wood, so I wasn’t super surprised, and in the case of this piece I sort of liked the change in appearance right where the headphones would lie.

The wood was already on the dark side, but I still wanted to finish it, so—after a touch of stain conditioner—I reached for one of my two favorite finishes: wipe-on polyurethane.

WBZ4465
WBZ4472

The red color and the pattern had a sort of grilled salmon sort of look.

WBZ4467

After a little epoxy in the holes, the pegs were secured and all that was left to do was wait for it all to dry and cure.

Fin

This was a super simple project and it’s about all I could get to do these days. It’ll be a nice addition to our newly organized office space.

WBZ4483
WBZ4485
WBZ4490
]]>
Cornhole Boards to Reduce Boredom https://wbrowar.com/article/maker/cornhole-boards-to-reduce-boredom Sun, 19 Jul 2020 16:37:00 -0400 Will https://wbrowar.com/article/maker/cornhole-boards-to-reduce-boredom These days we're spending a lot of time at home. A lot. Pretty much all of it really.

To give the kids a reason to get outside and have a little fun, I put together a cornhole set using some 2x4s, plywood, and spray paint. It's a pretty simple build and there are lots of plans available online, so I found one and got to work.

The Hole in Cornhole

It starts with two 2x4 foot plywood boards with a six inch hole centered nine inches from the top edge. I picked up a six inch hole saw bit and drilled a hole into each board.

WBZ 3872
WBZ 3873
WBZ 3874

The hole saw bit did a good job at creating the holes but the edges felt a little rigid, so I used a rounding bit and routed out the top and bottom edges. I also routed the top of the plywood to soften the edges and smooth down the corners.

WBZ 3877

The frame was made up of 4 pieces of 2x4. I screwed the frames together and then screwed pilot holes into the plywood tops.

WBZ 3880
WBZ 3881

I countersunk the screws to keep them from poking out and affecting gameplay.

WBZ 3882

Feeted

The feet are where I strayed from the instructions a little bit. I didn’t have bolts on hand to create the foldable feet many of the guides included, and while a ten minute drive to a nearby hardware store is usually how I would remedy this situation, I wasn’t going to throw on a mask and make a trip in this case. Thanks, SARS-CoV-2.

Now worries. I cut the feet down and screwed them in without using any glue. This way I could replace them when I have the bolts handy.

I know I could use geometry to figure out the correct angles needed to cut the feet so they'll lay flat on the ground. Several plans noted the correct angle, as well, but I used this method that involve a spool of twine and a pencil:

  1. Flip the board upside down and place one of the 2x4s that will be cut for the feet into one of the corners of the board.
  2. In this case, measure 12 inches from the bench and mark the side of the 2x4 closer to the corner of the frame.
  3. On the same side of the frame, but at the other end, place one end of the string under the board and pull the spool up and over to the mark on the 2x4.
  4. Mark where the string intersects with 2x4. From here you can put the string away.
  5. If you have a miter saw that’s angle can be adjusted, you can use the markings to adjust the saw to make the cut.
WBZ 3885

After cutting the feet down to size I screwed them into place. I flipped the frames over and sanded them down to remove any rough spots on the top and the sides.

WBZ 3888

Paint, My Nemesis (but Maybe Not So Much This Time)

I wanted to paint the boards but didn’t want to go with the triangle pattern I’ve seen in many of the plans. I got the idea to do a racing stripe from some Matchbox cars the kids have around the house. I had some red and black spray paint around along with some leftover white paint and primer.

WBZ 3890

I painted the top and sides of each of the boards with the white paint/primer mix. I had just enough for one coat so I didn't do the bottom or the feet.

For the racing stripe I wanted to alternate black and red on each board, so I taped up all of the sides and taped off the areas where the black paint would eventually go.

WBZ 3892

I started with the red paint because I thought that Black paint would do a better job at covering up any stray red paint.

WBZ 3894

After a couple of coats with the red paint, I peeled the tape off of the tops and took a look at the results.

WBZ 3896
WBZ 3897

After the red paint was dry I covered it up with masking tape in preparation for the black paint. This was an easy thing to do because I was working with straight lines. I can see needing to take a different approach if you were to create more complicated shapes or curves.

WBZ 3898

I sprayed the boards with two coats of black paint and waited for them to dry.

WBZ 3899

Shortly after the second coat, I took the tape off and took a look at the result.

WBZ 3900
WBZ 3907
WBZ 3913

I’m not always a fan of working with paint, but I’m happy with how these turned out. There are a couple of places where the lines didn’t match up correctly, but for the most part they achieved the look I was going for.

Now the black and red paint wasn’t an accident. Thanks to the Wirecutter, I found a set of eight cornhole bags in red and black. These are pretty simple bags but they seem durable enough to withstand the wear and tear my kids will surly bring to them.

WBZ 3920

After letting the boards dry over night we gave them a quick play test.

WBZ 3938
WBZ 3952

Fin

Being such a simple build it was nice to work on a quick project in these times where my free time is at an a minimum. This is the first project I’ve done this year and it was nice to take some time away from programming and thinking about work to build something for my family. I also got some help from my older son and it was awesome to spend some time showing him a thing or two about paint and woodworking.

I’m really looking forward to spending more time in the shop and I have several projects in mind that I want to tackle when I can get to them. For now I can’t wait until our lives can go back to normal and when the risk of getting COVID-19 is behind us. Until then we have a new outdoor game to help us pass the time.

WBZ 3956
WBZ 3957
]]>
Archiving the A Cappella Blog with Nuxt Content https://wbrowar.com/article/code/archiving-the-a-cappella-blog-with-nuxt-content Sat, 11 Jul 2020 00:33:00 -0400 Will https://wbrowar.com/article/code/archiving-the-a-cappella-blog-with-nuxt-content My friends, Mike and Mike, created a website in 2007 revolving around the world of collegiate a cappella. Over years of posting advice, event reviews, and sharing community YouTube videos, the site had accumulated about 1,800 articles over its 12 year run. As life events happened the Mikes decided to say farewell to the blog, but as a service to their passionate, niche audience they decided to keep the site up and running so it can be used as a resource for people just getting started in the a cappella world.

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.

Enter Nuxt Content

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:

  1. Pull down the current A Cappella Blog Craft site and update the templates to generate JSON files for each article
  2. Find a tool to scrape the JSON content into files that would be parsed by Nuxt Content
  3. Move all file uploads into the Nuxt static directory
  4. Re-create or tweak the site’s CSS it to match the branding of the current site
  5. Statically generate the site and upload it to the hosting environment
Acb article

Setting up the JSON templates

A 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.

Little Scraper

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.

Nuxt Pages

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:

Modern CSS for Old Markup

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:

Light mode
Dark mode

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:

  1. Load the randomized header image in place, using a focal point field in the CMS to determine the background-postition (this made it so you could focus on a person’s face as the responsive design cropped the image in different ways).
  2. Load the header image again as a background image in an element behind the main nav and calculate the 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.
  3. Apply filter: blur(); to the image for the blur effect.

This looked something like this:

Backdrop blur old

To do this using modern CSS, I simply added backdrop-filter: blur(6px) saturate(150%); to the main navigation sidebar:

Backdrop blur new

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'] });

Fin

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.

]]>
Side Table Slim https://wbrowar.com/article/maker/side-table-slim Sun, 28 Jun 2020 22:54:00 -0400 Will https://wbrowar.com/article/maker/side-table-slim Sometimes you just need a little extra room to put things. In my case, I needed a place to put a drink, some remote controls, and my phone while relaxing on the couch. Based on the dimensions of our living room, purchasing a side table wouldn't leave us with much room to walk through the gap between our couch and chair, and while a coffee table may do, with small children running around, we didn't want to introduce another obstacle into our living room.

I had the idea to sort of build a shelf next to the couch, it would be about 5-6 in wide and the top would sit just below the arm of the couch. I've never built furniture with a drawer before, but one would be really handy to hide the collection of device-specific remotes that are rarely ever used.

Being against the wall, you would see the back, top, and front so the design of the sides wasn't important. I wanted this shelf to fit between the wall and couch without any gaps, so I made sure to cut out space at the bottom so I didn't have to worry about running into the molding that runs along floor.

BWR 0556

Cutting and Constructing

There didn't need to be much to the interior, but I had to consider the height of the drawer and how that would affect where I put support pieces. One things that I wanted to make sure I got right was that the drawer looked like it was tight and flush with the front piece, but not too tight that it was hard to open and close, so I cut out the lower front piece and the drawer face, and confirmed the rest of the dimensions still worked.

BWR 0557

As far as material goes, I wanted to make sure the top was still solid, even though I planned on painting everything, so I used some left over maple from my stool project. I didn't have enough for the back and front, so I used plywood for those and the interior.

BWR 0559

After cutting everything out and doing a quick dry run, I started gluing and screwing everything into place. I added two pieces of wood to the inside for support, but I cut their width to be a little narrower than the top, front, and back. This let me inset ¼ in. plywood for the sides.

BWR 0560
BWR 0565
BWR 0562

The upper support also became a container for the drawer to sit in, so I put everything together based around the height and depth that I planned for the drawer.

BWR 0569

Before securing the top, I decided to add another feature to the shelf to accommodate some electronics I tend to use on the couch. I knew I wanted to add a qi charger and found one that was small and minimal. I've seen some really nice pieces of furniture where the charger is embedded into the bottom of a thick piece of wood and the idea is that you would route out a very thin layer of wood and the charger would charge your device through the wood.

While I would love to figure this out someday, I wasn't confident that I wouldn’t screw up this shelf trying to make that happen. I wound up routing a space along the top and embedding the charger into it. I ran a hole through the top and ran the cord down into the interior.

I also popped a couple of hole in the back where I could run a laptop charger and a Lightning cable to charge an iPad.

BWR 0570

To get everything right, I did a dry run of the electronics. This let me decide where to pop a hole in the back to run power out to a nearby outlet.

BWR 0577
BWR 0573

Fingers in the Saw Blade

Up until this project, I had never made a drawer or attempted to create something with more advanced joinery, but I have had enough wooden furniture to know that dovetail and finger joints are true methods for creating solid drawers. I went with finger joints as a way of dipping my toe into the water.

BWR 0580

After measuring and cutting enough pieces for 2.5 drawers, I took all of the pieces over to my friend, Ian’s, place where he had used his Glowforge to make a finger joint jig that helped make sure we could accurately—and safely—cut the finger joints out on a table saw. This helped to make sure all of the joints were correctly spaced and our setup avoided tear-out when cutting with a dado blade.

BWR 0596
BWR 0593
BWR 0589

After getting all of the sides home, I cut out a bottom and glued everything together. I made two drawers just in case one of them didn’t work out, but they both turned out great. The second drawer currently houses a mix of LEGO bricks, crayons, and play dough in our kids’ play room.

BWR 0605
BWR 0607
BWR 0608

I wanted to personalize the drawer pull since this was a piece of furniture that was tailored specifically for me, so Ian used the Glowforge again to cut out and engraved a few faces for me that I inserted into a metal pull base.

BWR 0611

My first attempt was to round the edge of the face down and to dip it into a clear epoxy to give it a glazed look. I bet I could figure out the right way to do this now, but back then the edges were sanded a little uneven and air bubbles took away from the look I was going for.

BWR 1248

I eventually wound up using one of the other faces that were cut out and engraved from some walnut. After using some epoxy to secure it into the metal base, I screwed the pull into the drawer.

IMG 0360

I painted everything, ran all of the wires, and installed the shelf into our living room. I knew that someday I would upgrade some of the electronics, so I made it easy to unscrew the sides in case I needed to pull the cables out.

Fin: Part I

This side table serves its purpose while taking up as little space as possible. As always, there are things I might consider doing differently (like embedding the qi charger into the top in a different way), but I learned a lot in making the drawer and it was really nice to design and build something so simple that I use almost every single day.

BWR 1268
BWR 1291
BWR 1450

I Heard You Like Shelves so I Made a Shelf to Put Above Your Shelf

A few weeks after completing the side table, I realized that I could use a little more room for things like my iPad or laptop when they weren’t in use. While they fit just fine on the side table, I realized I could add a small shelf to make use of the space on the wall above.

To find a material that fit my style I went to my local lumber store and found a beautiful piece of bocote hardwood. I didn’t know the history and rarity of bocote at the time, but it's color and grain pattern were exactly what I was looking for.

BWR 1451

I knew from my experience using ipe that hardwood like this requires careful cutting and routing, so I took my time and marked and double-checked the measurements of each of my cuts.

BWR 1453

I planned on making a shelf that could hold my iPad, so I used a ¼ in. round nose router bit to cut out a groove that would hold the iPad in place. I made the groove long enough so if I ever got a bigger iPad-like device in the future, I’d be covered. I also cut out a second groove that could fit an Apple Pencil or another thin device.

BWR 1458

I picked out some mounting hardware that could hold the shelf flush to the wall and used a cross cut sled to cut the back edge of the shelf to fit around the mount.

BWR 1459
BWR 1460

Since I planned on using the shelf mainly for my iPad, I used the rounding bit and the cross cut sled to cut out a place for a Lightning cable to rest when it wasn’t charging my iPad.

BWR 1463
BWR 1464

All that was left was to do some sanding and apply a finish. I didn’t want to change the color of the Bocote too much, so instead of using something like boiled linseed oil, I used a clear wipe-on polyurethane. It got a little darker, but it only helped to bring out the wood grain even more.

BWR 1474

Fin: Part II

This shelf is very simple, but every time I take the time to look at it I appreciate the look and the feel of the material.

IMG 0355
IMG 0356
]]>
Craft CMS Live Preview with Nuxt.js https://wbrowar.com/article/code/craft-cms-live-preview-with-nuxt-js Thu, 25 Jun 2020 14:00:00 -0400 Will https://wbrowar.com/article/code/craft-cms-live-preview-with-nuxt-js I've been trying to find the right Vue-based pairing to Craft CMS ever since my introduction to the concept of Jamstack at the 2019 Dotall conference. Since that conference, the team I work with has created headless Craft sites using Gridsome as well as Put Your Lights On’s fantastic Blitz plugin. The next framework on my list to check out was Nuxt.js and—coincidently—Nuxt.js 2.13 was already in the works.

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.

Secret Secrets

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.

Setting up Nuxt.js

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.

Talking to Craft

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.

Pointing Craft to the Preview Site

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.

Turning on Preview Mode in Nuxt.js

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',
],

Build & Test

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.

Keeping Live Preview Scroll Position

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:

Fin

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. 🥃+⛰

]]>
Guide 2 https://wbrowar.com/article/code/guide-2 Wed, 03 Jul 2019 07:45:00 -0400 Will https://wbrowar.com/article/code/guide-2 Guide 2

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.

Features That Got the AX

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.

Guide Editor

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.

Guide editor old
The Guide Editor in Guide 1.
Guide editor new
In Guide 2, the editor is syntax highlighted.

Guide Components

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.

Assets field
{{ craft.guide.component('image', { asset: craft.assets.filename('assets-field.png').one() }) }}

Guide Organizer

Guide navigation

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.

Guide organizer

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.

Guide Templates

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.

Other Improvements

Guide 2 also includes other author-friendly enhancements.

Improved Guide Widgets

Guide multiple widgets

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.

Multiple Guides on Edit Pages

Guide sidebar

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.

Better Rebranding Options

Guide rebrand

Colors and layout options are now set as fields in the Guide settings page. A custom logo can now be uploaded for guide headers.

Iframe Docs

Guide iframe

External documentation for plugins, Craft’s documentation, or any page on the internet can be used for a guide’s content.

Size-Aware Grids

Guide grids

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.

Print Stylesheet

Guide print

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.

Editions

Lite

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.

Pro

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.

Release

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.

]]>
The Letters https://wbrowar.com/article/maker/the-letters Wed, 27 Jun 2018 20:47:00 -0400 Will https://wbrowar.com/article/maker/the-letters When our first son was born we decorated his room with the letters of his name by ordering a set of modern, wall-mounted letters online. When we were expecting our second son, we also planned to put his name up on the wall, but instead of buying the letters, we decided to make them ourselves.

My initial idea was to use a laser to cut the letters out of layers of wood and acrylic. The wood would be against the wall, painted white, with keyholes hidden and cut out of them so they could be hung on the wall. A thin piece of translucent acrylic would be adhered in front of 2 or 3 layers of ¼ inch sheets of wood. The acrylic would give the front surface a glassy look just like the old white iMacs, or EVE from WALL-E.

I really wanted the edge of the acrylic to be cut flush to the edge of the wood, but I realized the challenge would be to get the acrylic to stick to the painted wood. I don't have much experience using acrylic and my guess is that even with something like acrylic cement, getting it to stick to the paint in a clean and attractive way might be an issue.

I discussed this idea with my friend, Ian from Roc City Laser, and we came to the idea of using epoxy as an alternative to acrylic. I hadn't cast epoxy before so I really liked the idea of testing it out for this and future projects.

Part I: Material Test

Easy Casting

My first step was to get some experience mixing and working with a two-part epoxy. Because epoxy is very expensive I picked up a small kit of EasyCast for my first test.

The two-part kits usually contain the epoxy in one bottle and a hardener in the other. When the two combine, the chemical reaction hardens them and cures into a solid piece.

BWR 1943

I remember hearing a tip about reducing wasted epoxy by measuring out the volume of your mould using water and marking your mixing container for both parts. I was planning on using a silicone mould of some LEGO-like figures, so I started by filling it in with water, then I poured that water into a measuring cup. It came to about 3.5 oz., so I rounded up and measured out 2 oz. of water and poured it into a mixing cup, marked the cup with permanent marker, measured out another 2 oz., added that to the cup, then marked the cup at the new level.

BWR 1939

I followed the mixing instructions to fully combine the epoxy and poured it into the mould.

BWR 1946

Because I've seen makers embed things in epoxy, I grabbed a couple of actual LEGO pieces and used a bamboo skewer to lower them into the mould. What I found was that the pieces did not float or stay suspended, but they did stay put once they sank to the bottom of the mould. I'd still like to figure out if there's a way to hold the items in place, but this wasn't an important part of this particular test.

BWR 1945

I picked up a heat gun and used it to pull up as many bubbles as I could. It was difficult to get the bubbles from the bottom of the mould, but I wonder if it would help to only pour a thin layer, use heat to pull out the bubbles, fill in the rest of the mould, then remove the remaining bubbles.

BWR 1941
I had to take a moment to appreciate the glow of the UI on the heat gun. This looked just like a light-up effect from a movie.

The epoxy took about 24 hours to harden enough where we could pop them out of the mould. I popped out the first one, then had my assistant pick out the rest.

BWR 1974
BWR 1981
BWR 1985
BWR 1989
BWR 1995
BWR 1996
The figures standing on top of a performance of Radio, Radio by Elvis Costello and the Beastie Boys.

My first impression is that these looked great. I can see where I had poured too much or too little and how the meniscus from each affected the backs of the figures. The EasyCast did not seem to completely harden, but instead it's almost like a very hard rubber. The bubbles in some of the corners were noticeable on the larger figure, but not as much on the smaller figures.

At this point I was convinced that the epoxy would work, so the plan was this:

  1. Use the laser to cut three layers of letters out of the wood
  2. Glue up and paint the three inner pieces for each letter
  3. Glue up and seal the outer pieces to create moulds to pour the epoxy into
  4. When the epoxy would dry, slide the epoxy out and wet sand it to a perfect, shiny finish
BWR 1998

Ian used his Glowforge to cut out the letters from ¼ inch plywood. We created two variations of the letter "W", and one rectangle that might represent the thickness of one of the letters.

BWR 2001

I glued up the letters into stacks of three, and glued the moulds into stacks of two. After a quick sanding I painted the letters with spray paint and let them dry.

BWR 2278

I used silicone to seal everything up. At this point it might have been a good idea to test the moulds by pouring water through, but at the time I didn't think about doing that.

BWR 2284

I ordered a big kit of two-part marine epoxy and hardener. I figured I would use it all for this project or if I had extra I could use it for future projects.

I also ordered a blue, glow-in-the-dark pigment for fun. I wasn't planning on using it for the letters project, but since I'd be testing out the new epoxy I thought I would play around for some other projects I have in mind.

BWR 2281

I mixed the epoxy to test my moulds, but I also wanted to do a test for sanding the epoxy, so I poured another round of the LEGO-like figures.

IMG 0805

I planned to wait about 24 hours for the epoxy to harden, but they were dry enough to take out around 18 hours later.

IMG 0806

I took out the figures and had some fun testing out the glow-in-the-dark look. The pigment worked really well!

IMG 0808

The larger figure looked awesome and after a good charge in the sunlight or with an LED, it can glow for hours.

IMG 0809

Unfortunately the moulds were another story.

A lot of epoxy leaked out of the letter moulds and they became very messy. Also, I wasn't confident I would be able to wet sand the epoxy without messing up the layers of paint.

My plan relied on the pour to go smoothly as it would be very hard to repair issues with the paint after the fact. I could have kept working towards a solution, but we were getting close to our due date so I decided this wouldn't be the way we would do the final build.

This still might not be a terrible idea, but if I were to try it again I would look into making my own rubber mould from the laser-cut letters, pouring the epoxy down into the mould, then finding a way to place the letters on top of the epoxy in the moulds.

Part II: Pivot

With time running out, I decided to go much simpler for the final solution.

Design

The idea of using the Glowforge to cut layers of plywood into the letters was still the plan, but I wanted the do something unique for the top surface. Ian had suggested doing epoxy inlays and I really liked that idea, but it still had its own set of challenges that would need to be tested out.

BWR 2424

I was looking through some of my old photos for ideas and came across the surface of an object that had a halftone pattern cut into it. It's super simple, but I really like the textured look and since we were planning on using the Glowforge for the cuts, adding an engrave step to the top layers would be super easy to do.

I designed the files and sent them along to Ian with one cut layer and one engrave layer.

BWR 2427

Production

I picked up a large, 4x8 sheet of ¼ inch plywood and got it to Ian. He brought me back a couple of test letters so we can see how deep the engrave should be and to see if the size worked. They looked fantastic.

BWR 2428

With a few minor adjustments, the plan was moving forward and Ian put together a quick test piece to see how everything would come together. I used this to test out the spray paint to make sure the engraved pieces would fill in correctly.

BWR 2480

Ian cut out and engraved all of the letters and dropped them off. The engrave looked great and the letters all stacked nicely.

BWR 2442

Between Ian's suggestion and hearing a tip about buying wood glue in bulk, I bought a GlüBot and a gallon of wood glue. Ian also recommended silicon brushes to apply the glue. The cool thing about these is that you can just let the glue dry and peel it off very easily.

BWR 2444

I did a quick dry run to make sure all of the letters lined up and that I had them all right side up.

BWR 2446
BWR 2465

My original plan for the keyholes was to use a keyhole router bit to cut into the back of each of the letters, but Ian had the idea to use the laser to cut the keyholes into the bottom two layers. This design worked perfectly when it came time to hang the letters.

BWR 2468
BWR 2469

Most of the letters were made up of flat edges and corners so lining them up was very easy. The “O” on the other ̦ was a little harder to line up and this would have been an issue when getting to the glue up step.

IMG 0893

When cutting out the “O” the laser left a small mark on the inside piece in the same spot for each layer. This was super helpful lining up the layers in the dry run.

BWR 2472

I used a technique I see in a lot of tabletop glue up where I took a pencil and made a squiggly line on the outer edges. Between the squiggly line and the marks on the inner cut I was able to put the letter back together quickly when it came time to gluing the layers together.

BWR 2477

After gluing all of the layers of the letters together and waiting for them to dry, I sanded down all of the edges. While the Glowforge cut the edges very flat and accurately, I wanted to remove a little bit of the burn mark to make it easier to apply white paint later on.

BWR 2486
You can see the burn mark coming off when sanding with 150 grit paper.

I applied a thin layer of wood filler around all of the edges. This filled in a couple of gaps that the plywood had, but it also flattened out the very small gaps between each layers.

BWR 2490

After sanding down the wood filler, I painted the letters with spray paint and let them dry over night.

Hang Time

I wanted to make sure the spacing and alignment of the letters were correct before putting them up onto the wall, so I found a large piece of paper and laid the letters out on top of it.

BWR 2491

I started by drawing both the ascender line and baseline guides onto the paper.

BWR 2492

My wife, who is the resident designer, kerned the letters and helped me line up the keyholes on the backs of each letter. We did this by tracing the letters, holding the paper up, and popping a hole into the top of each keyhole.

BWR 2506

With the keyhole markers in place, I leveled and taped the paper onto the wall. I marked the wall and used a screwdriver to create some pilot holes. With screws in the wall, the letters were ready to hang.

BWR 2504

Fin

BWR 2534
BWR 2540
BWR 2542

Even though the original plan for this piece turned out to be very different from where I started, I'm happy with the final outcome.

Working with the epoxy really sparked some ideas. With all of the extra epoxy I now have, I'll probably get started on them sooner than later.

The Glowforge really helped make this happen in that it allowed me to experiment and come up with several options and quickly see how they would work before spending hours committing to one solution.

I just hope my son enjoys these letters as much as I enjoyed making them for him.

]]>
Shoe Wrangler https://wbrowar.com/article/maker/shoe-wrangler Fri, 01 Jun 2018 00:00:00 -0400 Will https://wbrowar.com/article/maker/shoe-wrangler A few years ago my wife and I went to IKEA and bought TJUSIG: a modest shoe shelf and organizer. Back then it was just the two of us and—while we had plenty of shoes to fill the two-level rack—it served us well for years.

Then we had our first child and his shoes required space on the shoe rack. Now we're just about to have our second child and the limited space of this shelf is officially a problem.

BWR 2012
TJUSIG

A new shoe organizer would help us better use the space that TJUSIG occupied, and building our own set of shelves would allow us to tailor the organizer to fit the particular sizes of shoes we typically wear.

Design

We broke the design process down based on a few important factors.

  • First, the space we had to work within was just the height from the floor to the light switch on the wall, and between a door and a wall outlet. Ideally we would stay within this size when considering how things will fit on top and at the sides of the cabinet.
  • We wanted to utilize the metal bars from TJUSIG, since those provide a way to allow snow and water to drop down into a drip tray, below. To avoid having to cut down the bars, the inner width of the cabinet would be fit to their length.
  • We have a few common dimensions in our shoes, so planning out the vertical space between the shelves was pretty straightforward.
  • My wife has a lot of flats, so we decided to build in some cubbies to pack them in even more.
  • Our kids‘ shoes will only get bigger over time, so instead of designing the shelves for toddler shoes, providing a few different sizes would allow us to use this organizer for years to come.
BWR 2029

While the primary use of the organizer was to store our shoes, I wanted to use this opportunity to solve another storage problem we had. I often leave my messenger bag and my son’s book bag on a nearby chair or on the floor because we had no specific place to put them. Since we’re no longer restricted to two shelves, I wanted to use the top of the organizer as a dedicated place for our bags.

Making Plank

BWR 2018

I went to my local lumber store to look for wood that would be visible at the top of the organizer. Normally I would look for planks that just need to be cut to length and glued up, but I found two great pieces of walnut in the bargain bin that look like old scraps or maybe they were unfit to sell due to the uneven surface.

BWR 2026

The first thing I did was even out the rough edges by using a table saw method I learned from a Jimmy Diresta video.

  1. First, you find a flat piece of wood with a flat edge on at least one side.
  2. Straighten up and attach your board to the flat piece so that it hangs over the edge that’s opposite the flat edge.
  3. Keeping the flat piece up against the table saw fence, slide the fence over to move the rough edge past the blade to the size you would like to make your first cut.
  4. Cut through and make your first flat edge on your rough board.
  5. Detach the board and move your guide piece aside.
  6. Flip or turn your rough board around so that the new, cut edge sits up against your fence.
  7. Slide the fence over and trim off the other rough side.

From here you could cross-cut off the ends to flatten them out, but I didn't need to do that just yet.

IMG 0733

Next up, my friend, Ian, gave me a hand in planing the boards to remove the rough surfaces. I didn’t mind having the bottom remain rough, but I really wanted a smooth, even top and I had to make sure the remaining wood was thick enough to glue the boards together.

BWR 2027

Once the boards were planed, I used biscuits and glue to join the boards together.

BWR 2041

For my first time doing this, I'm thrilled with the results.

IMG 0747

Going forward, I think I'll find myself using this method more often. I might have passed on the chance to use walnut—to use a cheaper wood—if I had to buy these boards at full price.

BWR 2043

When the glue dried I measured out the length that I needed for the top of the organizer and used my circular saw to cut the boards to length.

BWR 2073
BWR 2033

While shaping the top, I also did a quick test to see which finish I would like to use. I have some wipe on poly and boiled linseed oil handy, so I tested them both. In the end I went with the linseed because it looked like a more even finish when it seeped in. I only used one coat of linseed oil and I like how it looked.

BWR 2079

I set the top board aside and started working on the rest of the structure.

Production

Cutting the Sides

I took the techniques I learned from my previous project for routing out grooves for shelves and applied them here.

BWR 2031

I started by measuring out the shelves and the placement of the metal bars onto the inside piece of plywood.

BWR 2034

The front and back bars on TJUSIG had inserts at each end to attach bolts to the base, so I drilled through the front and back placements on my inside piece.

BWR 2035

I then used my ½ inch cutting bit to route out about ⅛ inch deep circles for each metal bar.

BWR 2038

With the same depth setting in place, I used the ½ inch cutting bit to route out grooves for each of the three shelves.

BWR 2039

I flipped both boards over and countersunk the four holes for the metal bars.

BWR 2045
A quick test of the countersunk holes showed the bolts would be flush when the bars are attached

Cubby Grooves

BWR 2053

I had cut the boards for the shelves and measured out the placement of all of the cubby dividers.

BWR 2049

I attached my new ¼ inch cutting bit and used that to route all of the slots for the cubbies.

BWR 2048

At first, I was doing things like trying to measure the distance between the round edge of my router to the edge of the bit to calculate the distance of the guide to the cut. But I wound up making this quicker by holding my guide piece up against my speed square, lining up the router bit to the cut, then moving the guide in until it touched the edge of the router. Normally I would think this would be inaccurate, but in practice it worked pretty well.

BWR 2087
I did a quick check to make sure all of my grooves lined up
BWR 2090

I wanted to make the sides of the organizer the thickness of two pieces of plywood, so I planned to glue an outer piece to each of the inner boards. For extra support I predrilled some countersunk holes and matched them up into the outer pieces.

BWR 2092

I did a dry run of the shelves and once I had confirmed that everything lined up I installed the metal bars by lining them up and screwing the bolts into the inner side boards.

I then glued up the two sides with their outer boards.

BWR 2096

After letting the sides dry for a bit, I glued all of the shelves into place and ran some long clamps in the front and the back to hold everything into place.

BWR 2108

Because the cubbies weren't as deep as the sides, I screwed a thin piece of ¼ inch plywood onto the back of the shelves.

BWR 2106

Cubby Dividers

Okay, here's where I made my first mistake. I knew that I would be painting the cubbies at some point, but my plan was to put all of the wooden pieces together, then paint the entire organizer at once. In retrospect, I could have taken the time to paint the cubby dividers separately, then glued them in pre-painted. I don't know if this would have caused a mismatch in the painted finish, but I have a feeling it would have sped up the paint job immensely.

BWR 2109

Such as it is, I had cut all of the dividers and glued them into place. I clamped the shelves vertically to lock the cubbies into place.

BWR 2113

Trim

Since I was planning to paint the organizer white, the wood that would be painted didn't need to match, exactly. This was great because I had a bunch of extra maple from a failed glue up that I really didn't want to see go to waste.

BWR 2117

I roughly measured out how much I would need to cover the top and front faces of the organizer, then cut them to width.

BWR 2118

I then raised my table saw blade and used my push sticks to carefully cut each of the pieces down to a consistent ¼ inch.

BWR 2120

To make sure every piece fit as perfectly as they could, I measured and cut each piece down as I went.

BWR 2124

This allowed me to go slowly and clamp as I went. Speaking of Diresta, I might need to follow his advice and pick up a couple more clamps each time I have a big project. The pace of this part of the project could have been greatly sped up if I had maybe four or six more long clamps.

BWR 2136
Creatively clamping to distribute the force as much as possible
BWR 2141

Another thing I did to make sure the sides were as strait as I could get them was to let the trim hang over the sides just a bit. I then took an edging bit and used my router to cut the trim right to the edge of the side board.

BWR 2140
If you look closely at the shadow, the trim is uneven over the side board

Paint‘s Double-Edge

Other than the walnut boards for the top, some new rollers, and the paint, I didn‘t need to purchase anything to make this project. I had a ton of scrap or wood set aside for future projects that could easily be replaced. Since I planned on covering everything in paint, I didn't have to go to the trouble buying new wood that would match.

However, I'm learning that paint is messy to work with and—in this case—takes the finely crafted details and makes them look chunky and unrefined. Granted I wouldn‘t claim to be good at working with paint and the paint I‘m using may not be the right choice for this kind of project (for example, I probably should use several passes of spray paint instead of brushing on acrylic).

BWR 2157

Anyway, I did my best to hide drips and to cleanly paint each cubby and shelf. I used a roller for the larger areas and I tried using foam brushes for the corners and smaller areas. Being my first time using the foam brushes, I was happy with how well they worked, but I can‘t say I prefer them over brushes, just yet.

BWR 2165

I took the back off of the organizer and painted that separately. This worked out well because with so many corners in the shelf and cubby areas there were many drips that I had to tend to and clean up. Painting the back separately turned out smooth and fit nicely back into place.

BWR 2170

After putting the back on, I took a look at the painted piece and realized that with so much white, the organizer looked a bit chunky. I wanted to put in a pop of color that wouldn't clash with the orange wall that the organizer would sit in front of. Because our orange wall was already an accent wall that was meant to pop on its own, I thought that using a lighter, yellower color would hold up well.

BWR 2178

I rolled on two coats of paint and once the back was dry I screwed it back into place.

Metal Work

While waiting for the paint to fully dry, I took some time to clean everything up and put on some final details.

First, I had to clean up a few smudges of paint off of the metal bars. I tried doing this in a few ways, but the material that worked the best was some steel wool that I had laying around.

BWR 2175

Speaking of IKEA ... Around the same time as our trip to buy TJUSIG, we picked up a few sets of folding wall hooks—also known as BJÄRNUM. We‘ve used these hooks in other areas of the house and decided these would work well for hanging book bags onto the bare sides of the organizer. With those in place, the piece was complete.

BWR 2186
BWR 2185

Fin

BWR 2211
BWR 2230
BWR 2238
BWR 2245
BWR 2233

This piece is pure function over form, but it solves a problem in a way that‘s tailored specifically to the needs of my family, in our home, at a price that you just can‘t beat.

With a new baby on the way, finding time to work on project like this may become scarcer and scarcer, so before I take a break from these larger projects, I‘m glad I got a chance to put the learning I‘ve done over the past couple of years into good use.

My favorite thing was realizing the ability to take rough wood and turn it in to a smooth, seamless surface. This opens up a new approach to deciding on materials to use and I‘m looking forward to seeing what I can do with this in the future.

]]>
The Closet: A Series of Photos https://wbrowar.com/article/maker/the-closet-a-series-of-photos Mon, 23 Apr 2018 11:48:00 -0400 Will https://wbrowar.com/article/maker/the-closet-a-series-of-photos BWR 4999
BWR 5000
BWR 5001
BWR 5002
BWR 5003
BWR 5004
BWR 5005
BWR 5006
BWR 5007
BWR 5008
BWR 5009
BWR 5010
BWR 5011
BWR 5012
BWR 5013
BWR 5014
BWR 5015
BWR 5016
BWR 5017
BWR 5023
BWR 5024
BWR 5029
BWR 5030
BWR 5031
BWR 5033
BWR 5034
BWR 5035
BWR 5036
BWR 5038
BWR 5039
BWR 5040
BWR 5041
BWR 5042
BWR 5043
BWR 5044
BWR 5045
BWR 5046
BWR 5053
BWR 5054
BWR 5055
BWR 5056
BWR 5057
]]>
Bed Lights https://wbrowar.com/article/maker/bed-lights Sun, 15 Apr 2018 11:38:00 -0400 Will https://wbrowar.com/article/maker/bed-lights My wife had a solution to a problem that arose when we upgraded my son from a crib to a twin-sized bed. The issue was that the headboard we had bought sat too low and leaned up against the wall. It was possible to mount the headboard onto the wall, but we wanted to have the ability to move the furniture in the future. She suggested rising the headboard up, adding some shelves along the side, and adding a light strip to use as a nightlight.

Design

BWR 1295

The design started with a frame made up of 2x4 beams. Plywood would be used for the top, the sides, and the shelves. The length was based on the shelves being inset from the headboard so they would be seen only from the sides of the bed. The height of the top shelf was set a few inches from the top of the headboard so toys and books could be placed up there without worrying about them falling over.

Routing for Shelving

BWR 1296

The tops and sides were pretty easy to cut down on the table saw. I cut the sides in an "L" shape as part of the support that holds up the headboard.

BWR 1299

I haven't added shelves to a piece before so I watched a few YouTube videos for suggestions and decided to route out a groove for each of the lower shelves. To avoid tear-out from the plywood I clamped both sides together and used "sacrificial pieces" on the edges.

BWR 1300
I used a scrap piece of wood to figure out the width of the shelves.
BWR 1302
A longer piece of scrap was set up as a guide for the router.
BWR 1307
Guiding the router.

Having the extra pieces also helped in letting me test the thickness of the route before making an actual cut. I could adjust the guides if the cut was too tight or too wide.

BWR 1308

Building the Frame

BWR 1309

Because the frame didn't need to be created first, I was able to decide the length of it after seeing how the top and the sides fit together. I used a couple of scrap 2x6s to add to the support that holds up the headboard.

BWR 1314

The Shelves

BWR 1328

The shelves were cut to be flush with the top shelf so now that the frame was in place, I could measure and cut the shelf pieces down. For a little bit of detail, I routed out the bottom edges of the shelves.

BWR 1329

The grooves I cut for the shelves fit perfectly, so I dropped in some glue and clamped the shelves into place. I made sure they were squared off as I adjusted the clamps.

BWR 1336

When the glue dried I tested the placement on the headboard and everything was starting to look good.

Light It Up!

BWR 1340

While I had the shelves sitting on top of the headboard, I marked where the light strip would go. I wound up using a Philips Hue LightStrip because I already had other Hue products in the house. This allows me to turn the lights on and off automatically around bedtime.

BWR 1344

I routed out the back of the headboard to the length of the light strip. The light strip has its own adhesive on the back, so I cleaned out the groove and slowly glued the light strip into place.

BWR 1345

To help make sure the light strip stays into place, I lightly clamped a board on top of the light strip and let it sit for a few hours.

BWR 1355
BWR 1356
BWR 1357
BWR 1358
BWR 1359

Paint Job

BWR 1363

I used the same glossy white paint that I used on my stool project, so I followed the same process as I did before.

BWR 1364

I used a basic primer/paint combination as the foundation for the paint job. I did a couple of light coats to make sure I had a smooth surface before the final coat.

BWR 1368

Putting the final coat shined and looked exactly like I hoped it would. This paint takes a little longer to dry but it did a good job at smoothing itself out.

Assembly

BWR 1376

While watching the paint dry, I prepared for the final assembly. This included one large lug at the top, four more around the corners, and a eye bolt anchor used to help keep the headboard from falling over in case the bed gets moved.

BWR 1379

I moved everything up into my son's room to put it all together there. I found that the headboard wasn't quite flat so I found there were some small gaps between the shelves and the headboard. This could have been mitigated through some sanding of the shelves earlier on.

I didn't grab a photo but I also screwed some supports into the headboard just below each of the lower shelves to help hold them up in case they get leaned on. The bond provided by the glue is very strong, but this will help hold the shelves up just in case.

Fin

BWR 1392
BWR 1380
]]>
Homemade Push Sticks https://wbrowar.com/article/maker/homemade-push-sticks Tue, 26 Sep 2017 11:34:00 -0400 Will https://wbrowar.com/article/maker/homemade-push-sticks We didn’t have shop class in high school, but when I got to college we had a 3D design class that gave me exposure to tools like the bandsaw, table saw, and a few other things that are happy to take your fingers off. In that class, we learned a few tricks to keeping yourself safe, such as using two pieces of scrap to hold the wood you’re pushing through a band saw or table saw, to give you a safe distance from the blade.

One of the things that got me started in making with wood is the bevy or YouTube videos and maker blogs out there. People in this community are willing to share their ideas and it’s easy to find several approaches to the same problem. The topic of push sticks was no exception.

I learned about a few different styles, but being most comfortable with the two sticks approach, I found this video by Matthias Wendal to a be a great guide in making my own push sticks.

What I like about this design is that it’s ergonomic and it keeps your hands far away from the blade, but you get the control of two hands.

Design

Raw wood

I started with some leftover oak I had tested for another project. The oak seemed hard enough that I could rely on its sturdiness, and it wasn’t too heavy either. One of the things I don’t like about it was how grainy it is, but in this case that could be a good thing by offering more grip on the handles.

I didn’t realize until later that Matthias included his design on his website, so I found a frame on his video of when he was working with the design and kind of drew it freehand onto some scrap cardboard.

BWR 8725

I used an exacto and box cutter to cut the shape out and matched it up to the wood that I had already cut down to size. This allowed me to kind of preview the handle and to confirm that this design was what I liked. I traced the cardboard onto the first piece and made a few slight adjustments.

BWR 8726

This project was a great excuse to pick up my first jigsaw. A bandsaw might be even better for this kind of project, but the jigsaw worked just fine.

I cut out the first stick.

BWR 8729

This gave me an even better idea as to the final weight and balance of the push sticks. With a few more adjustments, I outlined the first piece onto the second and cut it out with the jigsaw.

What I learned by doing it this way was that even though both sticks came out similar in sizes they’re off by just a little bit. I think it was just because it was like taking two passes at the same design. It doesn’t bother me, but if I were making these for someone else, I would redo them. I made these push sticks before my step stool project, but if I were to make these again, I might try clamping the wood together and cut both pieces at the same time as I did with the sides of the stool.

BWR 8732
BWR 8731

The next step was to route off the hard edges. I routed everywhere except the front where the sticks make contact with the wood.

Finally, I put a hole in each handle in case I wanted to hang these up, then I sanded them down.

Fin

BWR 8734

These push sticks work really well and I keep them right on the side of my table saw, so they’re easy to grab when needed.

I’ve seen many more videos by Matthias since then and this is by far one of the tamer projects he’s done. He takes a very engineer-like approach to a lot of his work and his YouTube channel is chock full of useful tools and woodworking techniques.

]]>
L https://wbrowar.com/article/maker/l Mon, 25 Sep 2017 11:32:00 -0400 Will https://wbrowar.com/article/maker/l To commemorate the 50th anniversary of my aunt and uncle, I asked Ian from Roc City Laser to put together a simple piece that could either stand up on a desk or be hung up on the wall.

Design

Ian had some leftover ipe that he had gotten from scrap day at Jimmy DiResta’s shop. It was a beautiful piece of hardwood that had some heft to it.

This time I started right in Adobe Illustrator and planned everything around a 5 in. square. The design is simple but the risky part was that we planned to engrave the wood first, then cut it and route out the corners. To help with the cutting, we put a score line around the piece, so I had a guide to cut from.

I planned to use boiled linseed oil again, since I liked using it so much it in my last project. I knew it would get darker, but it really changes the color on this wood to a redder, richer brown.

L stain
Testing out boiled linseed oil on ipe.

Cutting, Sanding, and All of That Stuff

After I received the engraved piece from Roc City Laser, I took it down to my shop and made the cuts around the sides on the chop saw.

NOTE: While the Glowforge is an awesome cutting device for thinner wood, we didn't have the time or stomach to attempt to cut through this piece with the laser.

Wow—this wood is dense. I barely made it through the first side and the surge protector connected to my shop tripped from the electricity my saw was pulling. It happened two more times before I could make it to the other three sides.

This wood is very sharp when cut. It could seriously slice your finger if you move it along a freshly cut edge. Because this wood is so dense, sanding by hand takes a while, but it's amazing how little you take off compared to sanding softer woods. I felt more like I was buffing or polishing the wood as I made my way through different grits of sandpaper.

Routing wasn't a huge issue. I decided to round out the corners of the piece instead of creating a rounded edge along the sides. Again, because the wood is so dense routing seemed to work better when it was done slowly.

The last step of the process was to apply the boiled linseed oil. I applied three coats on this in total and I was amazed at how much richer it looked when the oil seeped in.

The Cover Up. Dun Dun Dunnnn

Stepping back for a second: I took it slow and steady and the cuts all looked good, until I noticed a small chip in the top. I've seen a few tricks to avoid chipping when cutting across the grain, but because the test cuts turned out so well, I didn't think I needed to take precautions. Rookie mistake? I think so.

You'll notice in the photos below that the chip doesn't show up. This is because I Photoshopped them out when prepping to send these photos to Roc City Laser to share on social media. I'm not afraid to admit to a mistake that I've made and there's no avoiding it if you look at the finished piece, but I didn't want my error to distract anyone looking at the fantastic work that Roc City Laser did on this piece.

Next time I’ll try a sawdust/glue mix to see if that will work as a filler, or maybe see if I can salvage the tiny piece from the chop saw. Another lesson learned.

Fin

L front edited
L back edited
]]>
One Small Step https://wbrowar.com/article/maker/one-small-step Wed, 06 Sep 2017 11:15:00 -0400 Will https://wbrowar.com/article/maker/one-small-step Two things came together around the same time: my son was learning about washing his hands and needed a way to reach the bathroom sink, and my friend, Ian Auch, started up a local laser engraving shop, Roc City Laser. I planned on making a basic step stool out of 3/4 in. plywood and just painting it white, but the idea of engraving the steps took this project into a whole new direction.

First: Finish

This occurred around the time when I started learning and playing around with various finishes. Ian—who is always learning and seeking out new ways to craft things—told me about some success he had using boiled linseed oil as a finish.

Linseed oil plywood test
Boiled linseed oil on maple (left) and sanded plywood (right)

With linseed oil in mind, I started doing some tests to see which wood looked the best with it. Although linseed oil made the plywood shine and it brought out the grain and the edges, the surfaces had this yellowish look that I wasn't a fan of. I wound up testing it on a scrap piece of maple, and I knew right away that was what I was looking for.

From there I decided the steps would be made of maple and the color of the sides would be of some sort of dark stain. In the past, I used some brush-on stain/poly mixes that look great, but clean up is a big hassle. It made testing stains on smaller pieces take more time than I wanted to spend.

I picked up a couple of cans of rub-on stains and some disposable rags (think extra thick paper towels). I really liked the color of Miniwax's Jacobean on the plywood. It has this nice chocolaty brown hue.

Linseed oil next to stain
Pairing maple with boiled linseed oil with Miniwax Jacobean stain

Hiding Screws

Ian had also introduced me to the idea of using wood plugs to hide the screws used to support the steps. This made a lot of sense for this project because I wanted the strength of wood glue and screws to hold the stool together, but I didn't want any little fingers getting cut or stuck anywhere. As with most purchases, I started researching reviews on The Sweethome and picked up their top pick for a Japanese-style pull saw, the Shark 15 in. Carpentry Saw.

Shark pull saw
First attempt at plugging a hole using a dowel

To get the plugs to work, I picked up a dowel that matched my 3/8 in. forstner bit and followed these steps:

  1. Punch and drill a pilot hole where the screw will be placed
  2. Use the fostner bit to drill a hole about 1/4 in. into the drill hole
  3. Countersink the inside of the hole a bit so the screw will be flush
  4. Drive the screw in to attach the pieces of wood together
  5. Cut a small piece of the dowel down to at least 1/2 in.
  6. Put some wood glue into the hole (especially on the inner walls of the hole)
  7. Place the dowel into the hole and wipe off any excess glue
  8. Wait for the glue to dry, then use the saw to cut the dowel as flush to the wood as possible
  9. Hit the dowel with some sand paper or a hand sander to flatten it to match the side of the wood
  10. if you find any gaps around the cut, use some wood filler, let it dry, and sand it down to smooth out the area
Wood plug sanded
After cutting and sanding the plug was smooth with the surface

Design

Earlier this year I picked up a 10.5 in. iPad Pro and an Apple Pencil with the intention of using it for projects like this. I found a great vector-based drawing app, called Graphic, to lay out the design of the stool. At this point I had already decided on some sort of space/rocket theme, so I decided to make the front of the sides of the stool rounded to follow the curves of a rocket ship. As for the measurements, I looked up a few step stool plans online to get an idea for what works and what is safe.

Graphic ipad pro
Step spacing
Placing the steps into position
Ruler bent curve
Using a flexible ruler to draw out the curve

The design of the ship blasting off and the footprints at the top were a mix of inspiration and practicality. I had just finished listening to the audio book of The Martian and I loved the detail Andy Weir used to be so descriptive about the equipment and the suits that his characters used on their mission.

I designed the top step to include the kind of footprints you might make in the soil of Mars while walking around. When it gets engraved it provides some tread on the top step to make it a little safer to stand on, and it also indicates to my toddler where he's supposed to stand on the step. Spoiler alert: he totally uses the footprints to guide where he steps.

Rocket sketch (square)
A sketch of the rocket blasting off

The idea for the rocket blasting off and the starry night scene followed and at that point I had decided to keep the natural wood color on the steps but to go to a spaceship-white finish for the rest of the stool. Not only would that give me a chance to try out a glossy, oil-based finish, but it would contrast better with the dark floors in our house.

I worked with Ian to figure out how to design the engraving art for the Glowforge and wound up doing the majority of the design in Graphic, on the iPad. Then I exported the designs over to Illustrator to clean up some pathfinder issues and to finalize everything.

Steps art
The final art sent to Roc City Laser: black = deep engrave, blue = shallow

Because the laser is precise and can handle multiple engraving depths, I made sure the engrave of the rocket and the footprints were deep enough to add some tread to the steps, but the cracks and some of the stars got a shallower depth to give a little variation to the engrave.

Laser depth test
Roc City Laser dropped off a depth sample to give me a couple of options to choose from
Depth sample with white paint
Testing out the final finishes: boiled linseed oil and oil-based white paint (ugh ... those brush strokes)

Once we had tested out the engrave, I cut, routed, and sanded the steps and delivered them to Ian. A few short hours later the engrave was done and he had sent me this shot.

Steps after laser
Hot off the Glowforge

I was super excited to get these home and I got right to finishing. First, I used a conditioner to prep the boards. Then I applied two coats of boiled linseed oil. I know linseed oil should be mostly water resistant, but I also applied two coats of rub-on polyurethane to add some extra protection—hitting the boards with 1000 grit sandpaper in between coats.

Before and after linseed oil
The test piece with boiled linseed oil applied, next to the raw, newly-engraved board
Linseed oil applied
The steps after the first coat of boiled linseed oil

Constructing the Rest

Putting together the sides of the stool were pretty straightforward. Once I did the initial cuts on the table saw, it helped to clamp together the two sides and move the clamps around as I cut out and sanded the edges. I wound up cutting everything out with a jigsaw, but this made me really wish I had a bandsaw in the shop.

I drilled out the holes for the plugs and routed the sides to smooth off the sharp edges. It really helped to lay out the boards earlier because I used the outline to route out the sides while stopping where the steps would go. This would leave a flat edge in the right spots.

Sides clamped
Cutting the bottom legs
Cutting edges
Screw holes
Routed and sanded
Steps attached

Once the steps were screwed into place, I had glued the sides of the board between the steps and used a couple of pocket holes to secure it to the top step. I also used a few screws to fix it to the back of the bottom step, as well.

I glued in one more piece of wood towards the bottom back area to help add a little more support between the sides.

Stool assembled
Just before putting in the plugs, all of the wood has been assembled

Painting: My Nemesis

I did several tests with the oil-based paint that I picked up. Each time I had some brush strokes or some areas where the paint would look droopy, so I wasn't super confident going into the painting step. Regardless, I taped up the steps and clamped the stool to a board and put on the first coat.

Paint 1
Paint 2
Paint 3

In my tests I found that if I started with a coat of primer/latex mix, it stuck better with the oil-based paint. This is something that I wasn't sure about, based on people online stating that you should start with an oil-based primer and never mix the two. I tried it anyway and found that this primer sanded really well and helped to fill in the gaps on the edges of the plywood.

The final coat was done with a gel-based, glossy white paint by Glidden. I don't know how to compare this mix with regular oil-based paint, but it did a good job at smoothing itself out and I really like the bright white surface it makes when it cures.

Paint finished

It took about two days to fully dry, but to my surprise one coat did the job. I'll confess that if you look really closely there are some small areas that aren't perfectly smooth, and one brush stroke that didn't smooth out completely. It kills me to get to the very end of a piece to make these kinds of mistakes, but I'm otherwise happy with how the paint turned out.

Fin

Will browar step stool 1
Will browar step stool 2
Will browar step stool 3
Will browar step stool 4
Will browar step stool 5

I learned a ton of small things with this project. People on YouTube and sites like Tested are a constant resource that I keep going back to for inspiration and to learn new techniques. I hope by making this blog, I can help add to the resources that other people use for their inspiration.

This step stool changed completely from my original plan thanks to the Glowforge and the expertise of my friend, Ian Auch. When he gets Roc City Laser up and running, I'm hoping to work with him some more to create projects like this.

Just to throw this out there, a few people have asked if they can purchase a stool with this design, but I'm reluctant to build something that's so tied to the safety of other people's children (I'd hate it if a child fell off of the stool because it wasn't balanced or engineered for safety). On the other hand, if there's any interest is buying steps engraved with this design so you can build your own stool, I'd be happy to talk about it.

]]>
Luggage Highlights https://wbrowar.com/article/maker/luggage-highlight Fri, 14 Jul 2017 14:10:00 -0400 Will https://wbrowar.com/article/maker/luggage-highlight When you're waiting at baggage claim, you see all sorts of ribbons, flags, and stickers that are used to help travelers find their luggage amongst the ever growing pile on the conveyor belt. While preparing for a family vacation, my wife and I wanted to make a new set of luggage tags that stood out. Our friend, Ian from Roc City Laser, just received his Glowforge and a bunch of their Proofgrade materials. He offered to make us some tags out of their orange acrylic, so we quickly put something together.

We came up with a very simple, color swatch-like style. They're big enough to be noticeable (especially with the bright orange material), but small enough that they never got in our way.

I'm happy with how these came out, but if I ever did a second version I think rounding the corners a bit would be a nice touch that we didn't think about in the first run.

Luggage tags
Luggage tags stacked
]]>