<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>brianperry.dev</title><description>A serious and professional website</description><link>https://brianperry.dev/</link><language>en</language><item><title>Initial Impressions of Claude Design</title><link>https://brianperry.dev/posts/2026/initial-impressions-claude-design/</link><guid isPermaLink="true">https://brianperry.dev/posts/2026/initial-impressions-claude-design/</guid><description>&lt;p&gt;I&apos;ve been poking at &lt;a href=&quot;https://design.claude.ai&quot;&gt;Claude Design&lt;/a&gt; over the past few days, using it to explore a redesign for an app I&apos;m working on. In general I&apos;m impressed, but it is still very much an early unstable release as the Anthropic &apos;labs&apos; label implies.&lt;/p&gt;
&lt;h3&gt;The output is surprisingly good&lt;/h3&gt;
&lt;p&gt;This is the headline. I&apos;ve tried other AI design tools (Google&apos;s &lt;a href=&quot;https://stitch.withgoogle.com/&quot;&gt;Stitch&lt;/a&gt;, Banani, UX Pilot) and Claude Design is miles ahead. It produced a design revision that I looked at and thought &quot;I&apos;d actually build that.&quot; And then I did. I&apos;m currently porting the concept into real code, which is about the strongest endorsement I can give a design tool like this.&lt;/p&gt;
&lt;p&gt;That&apos;s a first for me with this category of tool. Usually the output is either too generic to be useful or too far from what I actually want to be worth iterating on. Claude Design landed in the sweet spot where the design felt opinionated enough to be interesting but connected enough to the current state of the app that I could run with it.&lt;/p&gt;
&lt;h3&gt;Still very much experimental&lt;/h3&gt;
&lt;p&gt;There are rough edges. The tool made minor, unprompted changes to my preferred design that I couldn&apos;t figure out how to revert. I&apos;ve seen people on social media complaining about designs vanishing entirely (I haven&apos;t hit that myself, but I ran into the same issue frequently with Stitch, so I believe it). General navigation within the app feels a little awkward too. None of this is surprising for a brand new tool, but it&apos;s worth noting.&lt;/p&gt;
&lt;h3&gt;Token hungry&lt;/h3&gt;
&lt;p&gt;I&apos;ve been working on one design, on and off, over a weekend and I hit my weekly Claude Design limit with five days to go. Either it&apos;s burning through tokens fast or the limits are set conservatively right now (probably both). This also might be a side effect of using default of Opus 4.7. For casual exploration this is fine. For sustained design work across multiple projects, you&apos;ll feel the ceiling quickly.&lt;/p&gt;
&lt;h3&gt;The Claude Code handoff needs work&lt;/h3&gt;
&lt;p&gt;There&apos;s a &quot;hand off to Claude Code&quot; option, which in theory is the killer feature. In practice, at least for my use case of redesigning an existing app, it needed a lot of follow-up refinement. The handoff got me partway there but I spent a significant chunk of time cleaning things up afterward.&lt;/p&gt;
&lt;p&gt;For comparison, I find &lt;a href=&quot;https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server&quot;&gt;Figma&apos;s MCP&lt;/a&gt; much more useful for getting designs into code right now. If Claude Design&apos;s handoff eventually reaches that level, this becomes a must-use tool. It&apos;s not there yet.&lt;/p&gt;
&lt;h3&gt;Bottom line&lt;/h3&gt;
&lt;p&gt;Even with the rough edges, I&apos;m genuinely impressed. The core output quality is strong enough that I&apos;m actively building from it, and that puts it ahead of everything else I&apos;ve tried. I&apos;m looking forward to watching it improve over time.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This post was co-authored with my agent assistant, Ryan Terry.&lt;/em&gt;&lt;/p&gt;
</description><pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate></item><item><title>Porting My OpenClaw Assistant to Claude Cowork</title><link>https://brianperry.dev/posts/2026/porting-open-claw-to-cowork/</link><guid isPermaLink="true">https://brianperry.dev/posts/2026/porting-open-claw-to-cowork/</guid><description>&lt;p&gt;In early January I saw a number of people I follow exploring the concept of building stateful agent assistants. What really caught my attention was &lt;a href=&quot;https://timkellogg.me/blog/2025/12/15/strix&quot;&gt;Tim Kellogg&apos;s post about Strix the Stateful Agent&lt;/a&gt;. Inspired, I started building my own. The goal was that it would be my daily driver for keeping track of what I was working on, prepping for standups, and generally offloading the mental overhead of maintaining a work log.&lt;/p&gt;
&lt;p&gt;And then, as things go these days, &lt;a href=&quot;https://openclaw.ai/&quot;&gt;OpenClaw&lt;/a&gt; (Clawdbot at the time) came along and upended everything. Out of the box OpenClaw could do everything I was trying to build and a lot more. So I switched over. It mostly worked - but it required real effort to maintain, and I found myself spending more time managing bugs and infrastructure than actually using the assistant. That friction is what eventually pushed me toward Cowork: I wanted to see how much of the same workflow I could rebuild without worrying about the infrastructure at all.&lt;/p&gt;
&lt;p&gt;This post walks through what I built, how I built it, and what the migration looked like in practice.&lt;/p&gt;
&lt;h3&gt;What OpenClaw Did&lt;/h3&gt;
&lt;p&gt;If given the permissions, OpenClaw can do just about anything. But my initial goals were pretty simple. At its core, my OpenClaw was an assistant hosted on a VPS (I&apos;m not currently willing to let it run wild on a machine on my network) and wired up to a few key workflows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Work logging&lt;/strong&gt; - I could tell it what I was working on and it would keep a running, timestamped log&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standup summaries&lt;/strong&gt; - It would pull from the log and generate a concise summary for my morning standup automatically 15 minutes before the start of the meeting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Periodic check-ins&lt;/strong&gt; - If I&apos;d gone quiet for a while, it would nudge me for an update&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blog post brainstorming&lt;/strong&gt; - It could review my recent work log and suggest angles for posts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also needed it to be available across all of my devices. For this setup, this was accomplished by communicating with OpenClaw via Discord.&lt;/p&gt;
&lt;p&gt;The pain points were mostly operational: keeping the self-hosted model updated, managing the infrastructure, and dealing with the occasional reliability quirk. I also ran into Anthropic rate limits off and on that I had to debug.&lt;/p&gt;
&lt;p&gt;Since this was meant to make my life easier, even small hiccups and debugging were pretty much a non-starter. I do enough debugging every day, the last thing I want is to have to maintain what is essentially a fancy journal that keeps breaking in new and interesting ways.&lt;/p&gt;
&lt;p&gt;I have no doubt that the workflow introduced by OpenClaw is here to stay. I&apos;m just going to have to revisit it when it is ready for more than just the earliest of early adopters.&lt;/p&gt;
&lt;h3&gt;Why Cowork&lt;/h3&gt;
&lt;p&gt;Cowork solved the specific pain points I was hitting with OpenClaw. No VPS to maintain, no updates to stay on top of, no rate limits to debug - just a desktop app from Anthropic with built-in scheduled tasks and persistent file access to a folder on my machine.&lt;/p&gt;
&lt;p&gt;The tradeoffs are real. Cowork is much more locked down than OpenClaw and runs in a sandbox. Going from the freewheeling nature of OpenClaw to the workflow-oriented nature of Cowork meant giving some things up - no out of the box agent memory, no Discord bot I could ping from my phone, and no ability to wire up arbitrary integrations on a whim. But I felt confident that most of the process I was aiming for would be possible in either system, just with different mechanics. Worst case, I&apos;d learn about the limits of Cowork and keep looking.&lt;/p&gt;
&lt;h3&gt;Building the Basic Workflow&lt;/h3&gt;
&lt;h4&gt;The Work Log&lt;/h4&gt;
&lt;p&gt;The first thing I asked Cowork to set up was &lt;code&gt;worklog.md&lt;/code&gt; — a plain markdown file that serves as the running log of everything I work on. The format is simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 2026-03-14

**16:30 UTC** — Reviewed and merged an update to the More Later browser plugin...

**16:47 UTC** — Did initial setup of the Claude Cowork system...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tell Claude what I&apos;m working on in plain language, and it handles the timestamp (via bash) and appends the entry under the correct date heading. No special syntax required on my end.&lt;/p&gt;
&lt;p&gt;As I started adding work log entries, I realized that I needed light categorization. Essentially to keep noise out of my daily standup updates I needed to determine what was business and what was personal. I can provide a category when I update, but if I don&apos;t the agent will assign one for me. If it doesn&apos;t have enough information to know for sure, it asks.&lt;/p&gt;
&lt;h4&gt;Scheduled Check-ins&lt;/h4&gt;
&lt;p&gt;This system is really only useful if I stay current with updates. It is extremely easy to get heads-down on something and forgot to log it, and then completely forget what I have been working on for the past five hours. As a result, I often need a poke to be reminded to update the work log.&lt;/p&gt;
&lt;p&gt;In Cowork, I set this up as a scheduled task that runs at 9am, 11am, 1pm, and 3pm on weekdays. Each time it fires, it reads &lt;code&gt;worklog.md&lt;/code&gt;, checks the timestamp of the most recent entry, and — if it&apos;s been more than two hours — asks me for an update. If there&apos;s a recent entry, it stays quiet.&lt;/p&gt;
&lt;h4&gt;Daily Standup Summary&lt;/h4&gt;
&lt;p&gt;At 10:45am every weekday, a second scheduled task reads the last 24 hours of &lt;code&gt;worklog.md&lt;/code&gt; and generates a concise standup summary. By the time my standup rolls around, I&apos;ve got a tight summary of what I worked on the day before, ready to go.&lt;/p&gt;
&lt;h4&gt;Blog Post Brainstorming and Publishing&lt;/h4&gt;
&lt;p&gt;This one is on-demand rather than scheduled. When I want to brainstorm blog ideas, I ask Claude to review recent log entries and surface anything worth writing about — interesting technical decisions, workflows, tools. It suggests angles and titles, then helps me draft the post when I&apos;ve picked one. (Meta note: this post was kicked off exactly that way.)&lt;/p&gt;
&lt;p&gt;Drafts get saved to a &lt;code&gt;blog/&lt;/code&gt; subfolder in the Cowork workspace. When I&apos;m ready to publish, Claude handles the conversion to site format — transforming the draft into the frontmatter and markdown structure my Astro site expects, writing it directly into the repo at &lt;code&gt;src/content/posts/YEAR/filename.md&lt;/code&gt;, and wiring up any images alongside the post file.&lt;/p&gt;
&lt;p&gt;The one thing Cowork can&apos;t easily do is run the dev server for a live preview. For that step I hand off to Claude Code, which can run &lt;code&gt;pnpm dev&lt;/code&gt; locally without any issues. It&apos;s a minor seam in an otherwise smooth workflow.&lt;/p&gt;
&lt;h3&gt;Persisting the Workflow&lt;/h3&gt;
&lt;h4&gt;CLAUDE.md as the System&apos;s Source of Truth&lt;/h4&gt;
&lt;p&gt;One of the things that makes OpenClaw feel like magic is its memory system. Cowork does not have this out of the box. But since it is Claude at the end of the day, it is easy to add.&lt;/p&gt;
&lt;p&gt;The glue holding all of this together is &lt;code&gt;CLAUDE.md&lt;/code&gt;, a markdown file that Cowork reads at the start of every session. It documents the entire system: how to add log entries, how to generate standup summaries, what the scheduled tasks do, and an instruction to keep itself updated whenever the system changes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Claude Instructions

## Keeping This File Up To Date

Whenever changes are made to this system — such as adding new scheduled tasks,
modifying workflows, or updating logging behavior — this file should be updated
immediately to reflect those changes. CLAUDE.md is the source of truth for how
this setup works, so it should always stay current.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I want to establish a repeatable process like the daily standup summary, I&apos;ll ask Cowork to document it in Claude.md. It typically does a great job writing rules for itself to follow. I wish I was as good at that. And if Cowork needs another nudge, I can always modify CLAUDE.md manually.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;### Daily standup summary

When Brian asks for a standup summary, read `worklog.md` and summarize all entries from the last 24 hours in a concise standup format:

- Group entries into **Actual AI** and **Personal** sections
- What was worked on / completed
- Keep it brief and scannable — this is for a standup, not a report
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Sync&lt;/h4&gt;
&lt;p&gt;In order to use this on more than one device, we need to sync these files. This was harder than I expected using OpenClaw on a VPS - logging into the server to access the files is inconvenient, and committing them using git could work but was not as lightweight as I wanted this process to be.&lt;/p&gt;
&lt;p&gt;For this new setup, I just took a lo-fi approach and configured iCloud sync for the folder that Cowork operates in. This allows me to use this assistant on any Mac that I have the Claude app installed on. The one manual thing I have to do when using Cowork on a new machine is ask the agent to create the scheduled tasks referenced in CLAUDE.md. Those do not get created automatically.&lt;/p&gt;
&lt;p&gt;For mobile, I can use the dispatch feature to use Cowork remotely when my machine is on. That only gets me some of the way there as it requires my machine to be powered on and awake, and isn&apos;t quite the same interface as Cowork on desktop. For cases where dispatch won&apos;t work, I also have this folder configured as an Obsidian vault and can edit the necessary markdown files directly.&lt;/p&gt;
&lt;h3&gt;Takeaways&lt;/h3&gt;
&lt;p&gt;I&apos;m happy with this workflow on Cowork so far. I know I&apos;m missing out on the magic of having a fully autonomous OpenClaw agent, but I gain quite a bit in return. Not having to maintain a VPS means I&apos;m not responsible for securing the box it runs on, and Cowork&apos;s sandbox means I&apos;m not lying awake wondering what my agent decided to do at 2am. On the reliability front, I&apos;ve never really had to &quot;fix&quot; anything about this workflow since I established it. No mysterious rate limits, no debugging infrastructure - just a reliable worklog that, for once, doesn&apos;t keep breaking. (This post, for what it&apos;s worth, was brainstormed, drafted, and published using the very workflow it describes. So at least the system works well enough to write about itself.)&lt;/p&gt;
&lt;p&gt;The biggest concrete loss is cross-device availability. OpenClaw via Discord meant I could log updates from anywhere. Cowork is Mac-only, and while dispatch and Obsidian fill some of the gap, it&apos;s not the same seamless experience. That&apos;s a real tradeoff worth knowing about upfront.&lt;/p&gt;
&lt;p&gt;Perhaps over time I&apos;ll run into more limitations as I try to do more with it, but I also suspect that Cowork&apos;s feature set will continue to expand. Either way, if your assistant needs its own assistant, something&apos;s probably off.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This post was co-authored with my agent assistant, Ryan Terry.&lt;/em&gt;&lt;/p&gt;
</description><pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate></item><item><title>Heading Back to DrupalCon</title><link>https://brianperry.dev/posts/2026/heading-back-to-drupalcon/</link><guid isPermaLink="true">https://brianperry.dev/posts/2026/heading-back-to-drupalcon/</guid><description>&lt;p&gt;I&apos;ll be at DrupalCon Chicago this year. It&apos;s right in my back yard, so I really have no excuse not to show up.&lt;/p&gt;
&lt;p&gt;I&apos;ve been away from the Drupal world for about a year now. Most of my time lately has been focused on building &lt;a href=&quot;https://actual.ai&quot;&gt;Actual AI&lt;/a&gt;, working with AI tooling, and spending time in the broader JS ecosystem. All good stuff, but it&apos;s meant that aside from continuing to maintain the &lt;a href=&quot;https://www.drupal.org/project/api_client&quot;&gt;API Client&lt;/a&gt;, Drupal has been in my periphery rather than front and center. I wrote a bit about how that happened in my &lt;a href=&quot;/posts/2026/its-been-a-while&quot;&gt;last post&lt;/a&gt;, and honestly it still feels a little weird to have drifted from something that was such a huge part of my professional life for so long.&lt;/p&gt;
&lt;p&gt;DrupalCon is a good moment to change that — to check back in with a project and community I care about, see where things have headed, and reconnect with people I haven&apos;t seen in a while.&lt;/p&gt;
&lt;p&gt;A few things I&apos;m particularly curious about:&lt;/p&gt;
&lt;h3&gt;How the project is thinking about AI&lt;/h3&gt;
&lt;p&gt;This is the thing I&apos;m most eager to dig into. I&apos;ve spent a lot of the past year building with LLMs — building agents, wiring AI into personal workflows, working on &lt;a href=&quot;https://actual.ai&quot;&gt;Actual AI&lt;/a&gt;. I&apos;m curious how Drupal is approaching AI integration, what&apos;s landed, what&apos;s still being figured out, and where the interesting open problems are.&lt;/p&gt;
&lt;h3&gt;Getting hands on with Drupal Canvas and code components&lt;/h3&gt;
&lt;p&gt;I followed along with early development, but I haven&apos;t actually gotten my hands dirty with the stable release yet. I&apos;m especially interested in experimenting with code components since &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/json-api-client&quot;&gt;the Drupal JSON:API Client&lt;/a&gt; is available out of the box for use in Canvas. This is likely what I&apos;ll be focusing on during contribution day if anyone is interested in collaborating (or just wants to hang out.)&lt;/p&gt;
&lt;h3&gt;Sharing what I&apos;ve been working on&lt;/h3&gt;
&lt;p&gt;Part of coming back is bringing back what I&apos;ve learned. Building with AI day-to-day has changed how I think about a lot of things — how I prototype, how I approach problems, what I&apos;m willing to attempt. I&apos;m looking forward to conversations about what that actually looks like in practice, and whether any of it maps back to Drupal in useful ways. At a minimum I&apos;ll have some strong opinions and wild demos to share.&lt;/p&gt;
&lt;p&gt;A big thank you to &lt;a href=&quot;https://www.midcamp.org/&quot;&gt;MidCamp&lt;/a&gt; for providing my ticket. If you&apos;re going to be there, come say hi. I&apos;ll be the one who looks like they have forgotten how the LAMP stack works.&lt;/p&gt;
</description><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate></item><item><title>2026 Oscars Films</title><link>https://brianperry.dev/posts/2026/2026-oscars-films/</link><guid isPermaLink="true">https://brianperry.dev/posts/2026/2026-oscars-films/</guid><description>&lt;p&gt;It&apos;s Oscars time, so here&apos;s a quick rundown of my thoughts on all ten Best Picture nominees.&lt;/p&gt;
&lt;h3&gt;My Favorites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt30144839/&quot;&gt;One Battle After Another&lt;/a&gt; - My favorite film of the year. Paul Thomas Anderson at his most propulsive and crowd-pleasing, even though it&apos;s still plenty strange. Another movie feels really of the time. Benicio del Toro turns in my single favorite performance of the year.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt12300742/&quot;&gt;Bugonia&lt;/a&gt; - I&apos;m a sucker for movies that take sharp turns into sci-fi, and Bugonia delivers on that. The internet conspiracy theorist element felt very of the moment too. My second favorite film of the year.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt1312221/&quot;&gt;Frankenstein&lt;/a&gt; - A take on the classic tale that had me all the way through. Jacob Elordi&apos;s performance was excellent, and it&apos;s my wife&apos;s favorite film of the year. Probably my third favorite of the year as well.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Rest&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt16311594/&quot;&gt;F1&lt;/a&gt; - Entertaining enough, even if Brad Pitt&apos;s character would have been banned from F1 racing ten times over in the course of the film.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14905854/&quot;&gt;Hamnet&lt;/a&gt; - Found most of the film a little too over the top, but the conclusion was extremely effective and moving. Jessie Buckley&apos;s performance is hard to deny.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt32916440/&quot;&gt;Marty Supreme&lt;/a&gt; - A little too over the top for its own good. I was hoping for a bit of a change of pace now that the Safdies are working individually, but it felt more like Uncut Gems with ping-pong than I was expecting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt27847051/&quot;&gt;The Secret Agent&lt;/a&gt; - Didn&apos;t really come together in a way that made sense or was satisfying for me. Has a lot in common with &lt;em&gt;I&apos;m Still Here&lt;/em&gt; from last year, which covers similar ground better.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt27714581/&quot;&gt;Sentimental Value&lt;/a&gt; - Was a little too Oscar-y and distant when I first watched it, but in hindsight it has stuck with me more than I expected.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt31193180/&quot;&gt;Sinners&lt;/a&gt; - Highly entertaining, but made some strange story choices that I didn&apos;t love. Can&apos;t say no to dual Michael B. Jordans though, and even though I prefer &lt;em&gt;One Battle After Another&lt;/em&gt;, a late campaign surge for this film would be fun.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt29768334/&quot;&gt;Train Dreams&lt;/a&gt; - Didn&apos;t love this one. Wonderfully shot, but too traditionally Oscar-y. And frankly, not enough trains. If I had to pick between the vroom vroom movie and the choo-choo movie, I pick the vroom vroom movie.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;My Picks&lt;/h3&gt;
&lt;p&gt;20/24 correct.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;My Pick&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best Picture&lt;/td&gt;
&lt;td&gt;One Battle After Another&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Director&lt;/td&gt;
&lt;td&gt;Paul Thomas Anderson, One Battle After Another&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Actor&lt;/td&gt;
&lt;td&gt;Michael B. Jordan, Sinners&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Actress&lt;/td&gt;
&lt;td&gt;Jessie Buckley, Hamnet&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Supporting Actor&lt;/td&gt;
&lt;td&gt;Sean Penn, One Battle After Another&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Supporting Actress&lt;/td&gt;
&lt;td&gt;Amy Madigan, Weapons&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original Screenplay&lt;/td&gt;
&lt;td&gt;Sinners&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adapted Screenplay&lt;/td&gt;
&lt;td&gt;One Battle After Another&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Casting&lt;/td&gt;
&lt;td&gt;Sinners&lt;/td&gt;
&lt;td&gt;✗ One Battle After Another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editing&lt;/td&gt;
&lt;td&gt;One Battle After Another&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated Feature&lt;/td&gt;
&lt;td&gt;KPop Demon Hunters&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentary Feature&lt;/td&gt;
&lt;td&gt;The Perfect Neighbor&lt;/td&gt;
&lt;td&gt;✗ Mr. Nobody Against Putin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;International Feature&lt;/td&gt;
&lt;td&gt;Sentimental Value (Norway)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production Design&lt;/td&gt;
&lt;td&gt;Frankenstein&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Costume Design&lt;/td&gt;
&lt;td&gt;Frankenstein&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cinematography&lt;/td&gt;
&lt;td&gt;One Battle After Another&lt;/td&gt;
&lt;td&gt;✗ Sinners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Makeup and Hairstyling&lt;/td&gt;
&lt;td&gt;Frankenstein&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sound&lt;/td&gt;
&lt;td&gt;F1&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual Effects&lt;/td&gt;
&lt;td&gt;Avatar: Fire and Ash&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original Score&lt;/td&gt;
&lt;td&gt;Sinners&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Original Song&lt;/td&gt;
&lt;td&gt;&quot;Golden&quot; (KPop Demon Hunters)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated Short&lt;/td&gt;
&lt;td&gt;Butterfly&lt;/td&gt;
&lt;td&gt;✗ The Girl Who Cried Pearls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentary Short&lt;/td&gt;
&lt;td&gt;All the Empty Rooms&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live-Action Short&lt;/td&gt;
&lt;td&gt;Two People Exchanging Saliva&lt;/td&gt;
&lt;td&gt;✓ (tie with The Singers)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Other Thoughts&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Pulling for Amy Madigan in Best Supporting Actress. Aunt Gladys forever.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I&apos;d be fine with Conan hosting the Oscars forever.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I&apos;m going back and forth on if things are really up in the air, or this is actually a surprisingly predictable year. We&apos;ll find out soon enough.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Sun, 15 Mar 2026 00:00:00 GMT</pubDate></item><item><title>I Broke (and Fixed) A Guitar String</title><link>https://brianperry.dev/posts/2026/broken-string/</link><guid isPermaLink="true">https://brianperry.dev/posts/2026/broken-string/</guid><description>&lt;p&gt;I got an electric guitar in late December. And in February I broke a string (strum too hard much?) I knew at some point I would eventually break a string. My assumption was that I would have to take it somewhere to be restrung, or maybe I would just throw my guitar away and quit playing altogether.&lt;/p&gt;
&lt;p&gt;Instead, I tried doing it myself. I knew it would probably be bad. I bought multiple sets of guitar strings assuming that I would do it wrong the first time. I kind of did do it wrong. In the picture below, can you determine which string was the one I restrung? I bet you can.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Ugly, but it works. And it sounds fine, at least to my not very well trained ears. But something about this experience stuck with me. I&apos;ll probably restring this guitar multiple times. But I&apos;m just left with the lingering thought that there&apos;s something special about doing something poorly for the very first time.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</description><pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate></item><item><title>(Aaron Lewis Voice) It&apos;s Been A While</title><link>https://brianperry.dev/posts/2026/its-been-a-while/</link><guid isPermaLink="true">https://brianperry.dev/posts/2026/its-been-a-while/</guid><description>&lt;p&gt;2025 was a strange year. That almost certainly factors in to why I really didn&apos;t post here.&lt;/p&gt;
&lt;h4&gt;Navigating a Layoff&lt;/h4&gt;
&lt;p&gt;On a cold February morning in 2025, I came in from snow-blowing my driveway only to sit down at my laptop and realize I couldn&apos;t log into Slack, and then Google. That was enough to realize I was experiencing the classic tech company goodbye. It was my first time ever being laid off, but with the current market, I couldn&apos;t be shocked. I took a few minutes to collect my thoughts, called my wife to let her know, and then it was off to LinkedIn (gross) to start the process of figuring out where to go from here.&lt;/p&gt;
&lt;p&gt;I had an outpouring of support pretty much instantly once I posted on LinkedIn. My connections in the &lt;a href=&quot;https://drupal.org&quot;&gt;Drupal&lt;/a&gt; community really helped - without them, navigating the layoff would have been much harder. Personal connections made all the difference. If I was chasing unsolicited applications I would have lost my mind.&lt;/p&gt;
&lt;p&gt;As I explored my options, I was quickly able totake on contracting work with a company called &lt;a href=&quot;https://actual.ai&quot;&gt;Actual AI&lt;/a&gt;. This made a huge difference in lessening the blow of my job transition and providing stability as I figured out what&apos;s next. It also unsurprisingly changed the way that I used AI in my development workflow and opened my eyes to the changes that are happening right now.&lt;/p&gt;
&lt;p&gt;I eventually took a full time Founding Engineer role with Actual AI. Working at an early stage startup is not without its challenges, but the work has been really interesting and I&apos;ve taken on many things I probably wouldn&apos;t have been able to do elsewhere (at least not on this timeframe.) Looking forward to what is ahead this year.&lt;/p&gt;
&lt;p&gt;All of these changes have kept me out of the loop with the Drupal community aside from attending &lt;a href=&quot;https://midcamp.org&quot;&gt;Midcamp&lt;/a&gt; and &lt;a href=&quot;https://nedcamp.org/&quot;&gt;NEDCamp&lt;/a&gt;. At times that made me feel a little unmoored since it was such a big part of my professional life. The Drupal API client is a dependency of &lt;a href=&quot;https://www.drupal.org/project/canvas&quot;&gt;Drupal&apos;s next generation page builder&lt;/a&gt;, so keeping that maintained will be a fun way to keep my link back to Drupal alive.&lt;/p&gt;
&lt;h4&gt;Family Fun&lt;/h4&gt;
&lt;p&gt;We went on a Midwest road trip during our summer vacation. The header image from this post is one of the amazing views we took in at Badlands National Park. We didn&apos;t have any major travel mishaps and stayed at &lt;a href=&quot;https://www.circleviewranch.com/&quot;&gt;an amazing bed and breakfast&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My son started high school, which is still wild to think about. He&apos;s been busy playing percussion in the marching band and jazz band in addition to all of the regular high school things. And as an added bonus the football team made a solid run which made the marching band events even more entertaining.&lt;/p&gt;
&lt;h4&gt;Things That Kept Me Sane-ish in 2025&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Switch 2 - had a great time following the lead up and eventual launch of the sequel to my favorite game console of all time. Played a ton of Donkey Kong Bananza as well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Guitar - kept a pretty good pace of learning guitar. So much more to learn, but can actually kind of play a few things at this point. Got an electric for Christmas as well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Snuck out to a few concerts - David Byrne and Bear vs. Shark were (very different) highlights pretty much back to back.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Made pizzas of varying quality - got one of those small wood fired ovens and tried to learn how to make a reliable pizza. Improved along the way, but stil not as consistent as I&apos;d like.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Patriots got good again out of nowhere - thought we&apos;d be looking at years for Vrabel to get us back to the playoffs, but this was way better. Well, as long as you don&apos;t think about the Super Bowl...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Albums - my favorite album of the year was &lt;a href=&quot;https://open.spotify.com/album/52yD51X7yDinwlg6tbCtpP&quot;&gt;Never Enough by Turnstyle&lt;/a&gt; but the best album of the year was &lt;a href=&quot;https://open.spotify.com/album/0eeXb23yMW6EaIgm63xxPC&quot;&gt;Getting Killed by Geese&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Hopes for 2026&lt;/h4&gt;
&lt;p&gt;I&apos;m looking forward to seeing Actual AI evolve and grow.&lt;/p&gt;
&lt;p&gt;I&apos;ve been working on an app on the side as a way to continue exploring what is possible both using AI, and building with AI. I&apos;ll be upset if I don&apos;t launch it in some form this year. Of course, I said that last year as well.&lt;/p&gt;
&lt;p&gt;I have some ideas about how I could share more frequently at this site, but just getting this post out the door was a big milestone. We did it!&lt;/p&gt;
</description><pubDate>Sun, 22 Feb 2026 00:00:00 GMT</pubDate></item><item><title>2025 Oscars Films</title><link>https://brianperry.dev/posts/2025/2025-oscars-films/</link><guid isPermaLink="true">https://brianperry.dev/posts/2025/2025-oscars-films/</guid><description>&lt;p&gt;For the past handful of years I&apos;ve made a point to watch all of the films nominated for Best Picture at the Oscars. Here are my quick thoughts on what I&apos;ve seen this year, along with some bonus films I&apos;ve seen from other nominated categories.&lt;/p&gt;
&lt;h3&gt;My Favorite of the Year&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.imdb.com/title/tt23055660/&quot;&gt;Nickel Boys&lt;/a&gt; - this movie felt like the best kind of magic trick to me. I was initially distracted by the first person viewpoint of the film. Eventually that disorientation vanished, leaving me deeply connected to the story. Once I was all in, the changes in perspective, time and other twists felt so well earned. A crime that this wasn&apos;t nominated for cinematography. I&apos;ll be thinking about this one long after this Oscars season is over.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Best Picture Nominees&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt28607951/&quot;&gt;Anora&lt;/a&gt; - felt like two or three distinct movies in one, but they all worked well as a whole. Mikey Madison&apos;s performance was grounded and believable despite all the surrounding chaos. Would love to see this win Best Picture as is being predicted, but something tells me the Academy will chicken out. Also need to go back and watch some of Sean Baker&apos;s other films.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt17526714/&quot;&gt;The Substance&lt;/a&gt; - I love that a movie this fucked up made the cut. I have some structural problems with it, especially in the third act, but I don&apos;t think a movie like this really cares about structure, logic, or reality. Enjoyed the disorienting way it was shot once I settled in, and boy oh boy those last few scenes. Too many needles though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt20215234/&quot;&gt;Conclave&lt;/a&gt; - enjoyable, but a little to soap opera-y for me. And the twist at the end didn&apos;t feel earned at all. Never gonna say no to some Tucci though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt20221436/&quot;&gt;Emilia Pérez&lt;/a&gt; - speaking of movies that are trying too hard... I don&apos;t think this movie really worked overall, but Zoe Saldaña&apos;s performance was excellent regardless. Really wild that controversy has destroyed the chances of what was once an Oscar front-runner. Also wild that a portion of this movie essentially follows the plot of Mrs. Doubtfire.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt15239678/&quot;&gt;Dune: Part Two&lt;/a&gt; - did nothing for me unfortunately. Couldn&apos;t remember enough of the first one, and generally found most of the movie baffling. Impressive to look at though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt8999762/&quot;&gt;The Brutalist&lt;/a&gt; - a lot to like here, but ultimately it fell apart in the second half for me. Cinematography and score are incredible and Adrien Brody is well deserving of his second best actor trophy. Too many needles though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt11563598/&quot;&gt;A Complete Unknown&lt;/a&gt; - Bob Dylan is great at music and treats women poorly. Got it. Nothing offensive about this movie, but musical biopics like this just end up feeling like people playing dress up to me.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt1262426/&quot;&gt;Wicked&lt;/a&gt; - really enjoyed this when it was firing on all cylinders (which I assume was the stuff more directly sourced from the musical) and could feel it dragging when it was unnecessarily padding things out to support two films.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14961016/&quot;&gt;I&apos;m Still Here&lt;/a&gt; - I was probably just a little burnt out on movies once I got to this one. The meat of the movie was good, but felt that the pacing was off early and late in the film. And I dock this a point for being the only film that didn&apos;t make it to streaming in time. Did get a laugh seeing how many other completists went solo to the 12:45 Sunday showing I was in.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Other Nominated Films&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt21823606/&quot;&gt;A Real Pain&lt;/a&gt; - A funny yet touching look at generational trauma. Kieran Culkin is great, even if he&apos;s squarely in his wheelhouse. And really appreciate that it is a tightly structured 90 minutes that doesn&apos;t outstay its welcome.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt29623480/&quot;&gt;The Wild Robot&lt;/a&gt; - a great adaptation of a book that my son &lt;strong&gt;loved&lt;/strong&gt; when he was younger. The animation is beautiful, but it tried to squeeze in too many competing themes in the third act.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt4772188/&quot;&gt;Flow&lt;/a&gt; - glad I rounded out my list of animated films with this one, but think I might like the underdog story of a small team from Latvia using open source software more than I liked the movie itself.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt22022452/&quot;&gt;Inside Out 2&lt;/a&gt; - Pales in comparison to the first unfortunately. And I&apos;m still mad that &lt;a href=&quot;https://people.com/why-did-mindy-kaling-bill-hader-not-return-inside-out-2-8665675&quot;&gt;they low-balled some of the original voice cast&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt17163970/&quot;&gt;Wallace &amp;amp; Gromit: Vengeance Most Fowl&lt;/a&gt; - Fun! Best use of creepy gnomes for sure. I haven&apos;t seen other Wallace &amp;amp; Gromit films, but the way Wallace was oblivious to Gromit&apos;s feelings bummed me out a little bit. They should break up.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt27990245/&quot;&gt;Incident&lt;/a&gt; - extremely effective short assembled from security and body cam footage covering a 2018 Chicago police shooting. Great use of both silence, and competing multi-angle footage.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt27990245/&quot;&gt;A Lien&lt;/a&gt; - a dramatization of a green card interview interrupted by an ICE raid. Maybe a little too on the nose, but still a tension filled short.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Nominated Films I&apos;d Still Like to See&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Sing Sing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Apprentice&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A Different Man&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;My Picks&lt;/h3&gt;
&lt;p&gt;Taking a bit of a flyer here and going with Conclave for Best Picture. With all of the uncertainty this year it feels like one of those years where the academy (and ranked-choice voting) gets it wrong.&lt;/p&gt;
&lt;p&gt;&amp;lt;table class=&quot;table table-bordered&quot; cellpadding=&quot;4&quot; style=&quot;margin-top:20px; border-collapse:collapse; border-color:#999;&quot; border=&quot;1&quot;&amp;gt;
&amp;lt;tbody&amp;gt;&amp;lt;tr&amp;gt;
&amp;lt;th width=&quot;50%&quot;&amp;gt;Category&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;Winner&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Best Picture&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Conclave&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Actor in a Leading Role&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Adrien Brody in &quot;The Brutalist&quot;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Actor in a Supporting Role&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Kieran Culkin in “A Real Pain”&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Actress in a Leading Role&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Demi Moore in &quot;The Substance&quot;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Actress in a Supporting Role&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Zoe Saldaña in &quot;Emilia Pérez&quot;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Animated Feature Film&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;The Wild Robot&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Production Design&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Wicked&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Cinematography&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;The Brutalist&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Costume Design&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Wicked&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Directing&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Sean Baker, &quot;Anora&quot;&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Documentary (Feature)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Porcelain War&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Documentary (Short Subject)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Incident&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Film Editing&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Conclave&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;International Feature Film&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;I&apos;m Still Here&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Makeup and Hairstyling&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;The Substance&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Music (Original Score)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;The Brutalist&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Music (Original Song)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;&quot;El Mal&quot; from (&quot;Emilia Pérez&quot;)&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Short Film (Animated)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Yuck!&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Short Film (Live Action)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;A Lien&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Sound&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Dune: Part Two&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Visual Effects&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Dune: Part Two&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Writing (Adapted Screenplay)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Conclave&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr class=&quot;&quot;&amp;gt;
&amp;lt;td align=&quot;left&quot;&amp;gt;Writing (Original Screenplay)&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;Anora&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;Tiebreaker: How many minutes will the awards show run (including commercials)?&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;213&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/tbody&amp;gt;&amp;lt;/table&amp;gt;&lt;/p&gt;
</description><pubDate>Sun, 02 Mar 2025 00:00:00 GMT</pubDate></item><item><title>Talking Drupal - Single Directory Components Workflow</title><link>https://brianperry.dev/posts/2025/talking-drupal-sdc/</link><guid isPermaLink="true">https://brianperry.dev/posts/2025/talking-drupal-sdc/</guid><description>&lt;p&gt;I was a guest this week on the &lt;a href=&quot;https://talkingdrupal.com&quot;&gt;Talking Drupal podcast&lt;/a&gt; to discuss my experience working with Single Directory Components in Drupal. I had a chance to use them extensively on a recent client project and developed a workflow that I was pretty happy with.&lt;/p&gt;
&lt;p&gt;It was a great conversation, covering both the basics of Single Directory Components, how I’ve been using them, and how I see Single Directory Components evolving in future versions of Drupal.&lt;/p&gt;
&lt;p&gt;You can watch or listen to the episode below, on &lt;a href=&quot;https://talkingdrupal.com/487&quot;&gt;the episode page on talkingdrupal.com&lt;/a&gt;, or in your podcast player of choice.&lt;/p&gt;
&lt;p&gt;&amp;lt;audio controls=&quot;&quot; style=&quot;margin-bottom: var(--size-7)&quot;&amp;gt;
&amp;lt;source src=&quot;https://traffic.libsyn.com/sacstudio/td-487-libsyn.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
Your browser does not support the audio element.
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/c_5QPZrVCpg&quot; title=&quot;Talking Drupal #487 - Single Directory Components Workflow&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</description><pubDate>Wed, 05 Feb 2025 00:00:00 GMT</pubDate></item><item><title>Using Verbose Output When Applying Drupal Recipes</title><link>https://brianperry.dev/posts/2025/drupal-recipes-verbose/</link><guid isPermaLink="true">https://brianperry.dev/posts/2025/drupal-recipes-verbose/</guid><description>&lt;p&gt;I&apos;ve been slowly chipping away at a side project that includes a set of related &lt;a href=&quot;https://www.drupal.org/docs/extending-drupal/drupal-recipes&quot;&gt;Drupal Recipes&lt;/a&gt;. Given the repeatable nature of recipes, I apply them after a clean install frequently, and have a shell script that automates this process.&lt;/p&gt;
&lt;p&gt;After some recent changes I found that one of my recipes was not applying cleanly. In an effort to debug, I ran the &lt;code&gt;drush recipe&lt;/code&gt; command with the &lt;code&gt;-v&lt;/code&gt; flag to get verbose output. Debugging aside, I found the output to be really useful.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I&apos;d see when applying the &lt;code&gt;sanctuary_graphql&lt;/code&gt; recipe without the verbose flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ddev drush recipe recipes/sanctuary_graphql
12/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░]
Installed Simple OAuth &amp;amp; OpenID Connect module.
25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]
Applied Sanctuary GraphQL recipe.

 [OK] Sanctuary GraphQL applied successfully
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and this is the output with the &lt;code&gt;-v&lt;/code&gt; flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ddev drush recipe recipes/sanctuary_graphql -v
 [info] Drush bootstrap phase: bootstrapDrupalRoot()
 [info] Change working directory to /var/www/html/web
 [info] Initialized Drupal 10.4.1 root directory at /var/www/html/web
 [info] Drush bootstrap phase: bootstrapDrupalSite()
 [info] Initialized Drupal site sanctuary.ddev.site at sites/default
 [info] Drush bootstrap phase: bootstrapDrupalConfiguration()
 [info] Drush bootstrap phase: bootstrapDrupalDatabase()
 [info] Successfully connected to the Drupal database.
 [info] Drush bootstrap phase: bootstrapDrupalFull()
 0/25 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]
Applying recipe
 [info] decoupled_preview_iframe module installed.
 3/25 [▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░]
Installed Config Pages module.
 5/25 [▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░]
Installed Paragraphs module.
 [info] paragraphs_edit module installed.
 8/25 [▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░]
Installed Sanctuary module.
10/25 [▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░]
Installed Iconify Icons module.
 [info] menu_item_extras module installed.
 [info] simple_oauth module installed.
13/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░]
15/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░]
Installed Typed Data module.
 [info] graphql module installed.
18/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░]
Installed Sanctuary GraphQL module.
20/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░]
Installed GraphQL Compose: Routes module.
 [info] graphql_compose_menus module installed.
25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]
Applied Sanctuary GraphQL recipe.

Modules installed
-----------------

 * Config Pages
 * Consumers
 * Decoupled Preview Iframe
 * Entity Reference Revisions
 * Frontend Editing
 * GraphQL
 * GraphQL Compose
 * GraphQL Compose: Edges
 * GraphQL Compose: Menus
 * GraphQL Compose: Preview
 * GraphQL Compose: Routes
 * Iconify Icons
 * Menu Item Extras
 * Paragraphs
 * Paragraphs Edit
 * Sanctuary
 * Sanctuary GraphQL
 * Serialization
 * Simple OAuth &amp;amp; OpenID Connect
 * Token
 * Typed Data

Recipes applied
---------------

 * Sanctuary Core
 * Sanctuary GraphQL


 [OK] Sanctuary GraphQL applied successfully
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is verbose (as advertised) but pretty essential information, especially if you&apos;re applying a recipe for the first time. Without -v all we really know is that the recipe was applied successfully. With -v we get a list of all of the modules installed, and also see that another recipe was applied as a dependency.&lt;/p&gt;
&lt;p&gt;I&apos;d often find myself either reviewing the source code of a recipe, or skimming the module page in the admin UI to understand what was actually included. This verbose output is a much quicker way to get that info.&lt;/p&gt;
&lt;p&gt;From what I can tell, &lt;code&gt;-v&lt;/code&gt; and &lt;code&gt;-vv&lt;/code&gt; provide the same output. &lt;code&gt;-vvv&lt;/code&gt; is more verbose, but I haven&apos;t found the extra info super useful as of yet. None of these options seem to provide debug output for config actions, which is the only thing I could see as a useful addition. (Update: there is an &lt;a href=&quot;https://www.drupal.org/project/distributions_recipes/issues/3459304&quot;&gt;open issue for this&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;I realize that &apos;try running it with debug output&apos; is hardly a groundbreaking tip, but in the case of recipes this additional info is really handy. Going forward, I&apos;ll be using &lt;code&gt;-v&lt;/code&gt; by default when applying new recipes so that I can get a better sense of what&apos;s being added to my site.&lt;/p&gt;
</description><pubDate>Sun, 02 Feb 2025 00:00:00 GMT</pubDate></item><item><title>The Perils of Third-Party Scripts in Next.js</title><link>https://brianperry.dev/posts/2025/nextjs-scripts/</link><guid isPermaLink="true">https://brianperry.dev/posts/2025/nextjs-scripts/</guid><description>&lt;p&gt;In &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt; if you find yourself needing to integrate a script outside of the Node ecosystem (basically something you can&apos;t &lt;code&gt;import&lt;/code&gt;,) you&apos;ve probably already lost. It can sometimes be unavoidable though. I recently faced this with some third-party ad code that needed to be included in a project and it was tricky to get right.&lt;/p&gt;
&lt;h2&gt;So You&apos;ve Got a Script...&lt;/h2&gt;
&lt;p&gt;Your first instinct may be to just toss a traditional &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in the Document component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Html, Head, Main, NextScript } from &quot;next/document&quot;;

export default function Document() {
  return (
    &amp;lt;Html&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;script
          async
          src=&quot;https://securepubads.g.doubleclick.net/tag/js/gpt.js&quot;
        /&amp;gt;
      &amp;lt;/Head&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;Main /&amp;gt;
        &amp;lt;NextScript /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/Html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works, but that affords you very little control over how Next.js loads the script and when it runs in comparison to other scripts. This will be especially problematic if you have other scripts that depend on this one. That was true in my case as I had custom code that needed to run after the third-party ad code had loaded. So I set out to find other alternatives.&lt;/p&gt;
&lt;h2&gt;Wrestle Back Some Control with The &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; Component&lt;/h2&gt;
&lt;p&gt;Capitalizing the first letter in &lt;code&gt;script&lt;/code&gt; can make a big difference. Aren&apos;t computers fun?&lt;/p&gt;
&lt;p&gt;Next.js has a &lt;a href=&quot;https://nextjs.org/docs/pages/api-reference/components/script&quot;&gt;built-in component called &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt;&lt;/a&gt; that allows you to control when and how a script is loaded. Here&apos;s the previous example adapted to use the &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Html, Head, Main, NextScript } from &quot;next/document&quot;;
import Script from &quot;next/script&quot;;

export default function Document() {
  return (
    &amp;lt;Html&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;Script
          src=&quot;https://securepubads.g.doubleclick.net/tag/js/gpt.js&quot;
          strategy=&quot;beforeInteractive&quot;
        /&amp;gt;
      &amp;lt;/Head&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;Main /&amp;gt;
        &amp;lt;NextScript /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/Html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://nextjs.org/docs/pages/api-reference/components/script#strategy&quot;&gt;The &lt;code&gt;strategy&lt;/code&gt; prop&lt;/a&gt; is especially important here. By default the &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; component will use the &lt;code&gt;afterInteractive&lt;/code&gt; strategy, which may be too late for scripts that depend on it. Setting it to &lt;code&gt;beforeInteractive&lt;/code&gt; will load the script before any Next.js code and before page hydration.&lt;/p&gt;
&lt;p&gt;You can technically use the &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; component in any component on your Next.js site, but in order to use the &lt;code&gt;beforeInteractive&lt;/code&gt; strategy, it must be used in the &lt;code&gt;Document&lt;/code&gt; component.&lt;/p&gt;
&lt;h2&gt;If Client Code Depends on These Scripts, My Thoughts Are With You&lt;/h2&gt;
&lt;p&gt;Having the control provided by the &lt;code&gt;loadingStrategy&lt;/code&gt; prop is helpful, but I still found that the loading order was a bit unpredictable. Scripts that use the same loading strategy wouldn&apos;t always load in the order that I referenced them in the source. In my case, this was a problem because I had variable ad slots that depended on this code. I essentially needed to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load the third-party ad script&lt;/li&gt;
&lt;li&gt;Once that loads, run custom ad code that defines ad slots and sets targeting. This code was provided by a another vendor, and was not written with Next.js in mind.&lt;/li&gt;
&lt;li&gt;After the ad slots have been defined, display them based on the current page&apos;s content.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Next &lt;code&gt;&amp;lt;Script&amp;gt;&lt;/code&gt; component offers &lt;a href=&quot;https://nextjs.org/docs/pages/api-reference/components/script#onload&quot;&gt;&lt;code&gt;onLoad&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://nextjs.org/docs/pages/api-reference/components/script#onready&quot;&gt;&lt;code&gt;onReady&lt;/code&gt;&lt;/a&gt; props that can help with this. I initially assumed that &lt;code&gt;onLoad&lt;/code&gt; would be the right choice, but it has some important limitations. It can&apos;t be used with &lt;code&gt;beforeInteractive&lt;/code&gt; and also does not yet work with Server Components. That makes &lt;code&gt;onReady&lt;/code&gt; the only viable option here. Since &lt;code&gt;onReady&lt;/code&gt; only runs on the client, we&apos;d also need to move the script outside of the &lt;code&gt;Document&lt;/code&gt; component as well.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Script from &quot;next/script&quot;;

export default function Ad() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Script
        id=&quot;ad-targeting&quot;
        src=&quot;https://securepubads.g.doubleclick.net/tag/js/gpt.js&quot;
        onReady={() =&amp;gt; {
          console.log(&quot;Run custom ad targeting code here&quot;);
        }}
      /&amp;gt;
      &amp;lt;div id=&quot;ad-slot&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I actually ended up taking a different approach inside of a &lt;code&gt;useEffect&lt;/code&gt; hook that carefully checks for the existence of the window object, specific &lt;code&gt;googletag&lt;/code&gt; methods, and runs only once. If given an opportunity to refactor this, I&apos;d adapt it to use the &lt;code&gt;onReady&lt;/code&gt; prop instead.&lt;/p&gt;
&lt;p&gt;I also also needed to find a way to update targeting during client side routing since the ad targeting code was written assuming a full page refresh. Listening for router events made this pretty straightforward.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// re-set ad targeting on route change
useEffect(() =&amp;gt; {
  router.events.on(&quot;routeChangeComplete&quot;, handleRouteChange);

  // If the component is unmounted, unsubscribe
  // from the event with the `off` method:
  return () =&amp;gt; {
    router.events.off(&quot;routeChangeComplete&quot;, handleRouteChange);
  };
}, [router]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;handleRouteChange&lt;/code&gt; is a function that re-runs the ad targeting code, considering the new route.&lt;/p&gt;
&lt;h2&gt;Always Open to a Cleaner Solution&lt;/h2&gt;
&lt;p&gt;What I have in place works, but still feels hackier than I&apos;d prefer. If you&apos;ve found a better solution for something similar, I&apos;d love to hear it.&lt;/p&gt;
</description><pubDate>Wed, 15 Jan 2025 00:00:00 GMT</pubDate></item><item><title>Things I Enjoyed Over Winter Break</title><link>https://brianperry.dev/posts/2025/winter-break/</link><guid isPermaLink="true">https://brianperry.dev/posts/2025/winter-break/</guid><description>&lt;p&gt;Had a wonderful two weeks off for the holidays this year, and I wanted to take a moment to reflect on some of the things that brought me joy over the break. Here are a few of the highlights:&lt;/p&gt;
&lt;h2&gt;Holiday Celebration Nonsense 🎄&lt;/h2&gt;
&lt;p&gt;I have a habit of taking on elaborate and unnecessary holiday related projects every year. My theory is that I still get as excited for the Christmas as I did when I was a kid, and as an &apos;adult&apos; this is how I channel that energy.&lt;/p&gt;
&lt;p&gt;The largest of these projects is my now 6-year tradition of surprising my family with a light show in my basement on Christmas morning. The first couple of years, the light show itself was the surprise, but the past few years I&apos;ve been finding ways to challenge expectations of what a &apos;light show&apos; could even be. This year that resulted in me secretly renting a U-Haul and pre-loading it with lights. My family awoke to a basement with only one light set up, packages containing winter clothes, and instructions to go outside. Pretty proud of how it all turned out.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/A66dFP40ItU?si=f8GdG3rgiLLZRz7W&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;I also made my first attempt at a &lt;a href=&quot;https://holiday.campperry.fun/&quot;&gt;digital version of our family Christmas card&lt;/a&gt;. I also gave myself the additional challenge of trying to make it from scratch using exclusively AI tools. I&apos;m hoping to write about the experience in more detail at some point, but I primarily used &lt;a href=&quot;https://bolt.new/&quot;&gt;bolt.new&lt;/a&gt;, along with &lt;a href=&quot;https://chatgpt.com/&quot;&gt;ChatGPT&lt;/a&gt; for the video script, and &lt;a href=&quot;https://www.synthesia.io/&quot;&gt;Synesthesia&lt;/a&gt; for the video itself.&lt;/p&gt;
&lt;h2&gt;Blue Man Group&lt;/h2&gt;
&lt;p&gt;It was announced that &lt;a href=&quot;https://blockclubchicago.org/2025/01/07/a-funeral-for-the-blue-man-group-chicago-says-farewell-to-iconic-performance-troupe/&quot;&gt;Blue Man Group&apos;s multi-decade Chicago run was ending&lt;/a&gt; in early January, so we got tickets for a show in the last week. I hadn&apos;t seen it since high school (a friend and I actually did the marshmallow catching thing for a school variety show,) and I especially wanted my son to see it due to his interest in percussion. I could feel the age in the show a little bit, but it was still a lot of fun, and I&apos;m glad we got to see it before the run ended. And Colin seemed to really enjoy it.&lt;/p&gt;
&lt;h2&gt;Games&lt;/h2&gt;
&lt;p&gt;There are always video games on holiday break, and I usually try to start something new on Christmas Day.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nintendo.com/us/store/products/1000xresist-switch/&quot;&gt;1000xRESIST&lt;/a&gt; - heard nothing but good things and have been really blown away by this game so far. If you are open to visual novels and sci-fi games I can&apos;t recommend it enough.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nintendo.com/us/store/products/the-legend-of-zelda-echoes-of-wisdom-switch/?srsltid=AfmBOoofjjHw1sJ2Cm_x17xfGsnjt&quot;&gt;Zelda: Echoes of Wisdom&lt;/a&gt; - this game has some fundamental problems, but it still ended up being my gaming comfort food over the break.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://indianajones.bethesda.net/en-US&quot;&gt;Indiana Jones and the Great Circle&lt;/a&gt; - only fired this up a few times, but it yielded an all time gaming highlight:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/BBkp7hFnUio?si=NJLSo_dofTA5_Fsd&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;TV&lt;/h2&gt;
&lt;p&gt;Is it still TV if I mostly watched it on an iPad?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14044212/&quot;&gt;Mr. And Mrs. Smith&lt;/a&gt; - finally got around to this and really dug it. Went down real easy and I liked how it wrapped up.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.imdb.com/title/tt10919420/episodes/?season=2&quot;&gt;Squid Game Season 2&lt;/a&gt; - Not sure this really needed to exist, but an easy holiday binge like the first season.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Guitar&lt;/h2&gt;
&lt;p&gt;My son got an electric guitar for Christmas, and I threatened that if that happened I&apos;d almost certainly end up getting a guitar for myself. So I did! I splurged on &lt;a href=&quot;https://www.lavamusic.com/lava-me-4-spruce&quot;&gt;this tech enabled electric acoustic&lt;/a&gt;. For a variety of reasons it probably wasn&apos;t the right choice for a complete beginner, but the bells and whistles are fun so I&apos;ve decided to stick with it. Didn&apos;t really get started until the end of break. Related: my hand hurts.&lt;/p&gt;
&lt;h2&gt;Puzzles&lt;/h2&gt;
&lt;p&gt;I put this one together.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Hang in there for the non-holiday season. We can make it to the next one, I swear.&lt;/p&gt;
</description><pubDate>Wed, 08 Jan 2025 00:00:00 GMT</pubDate></item><item><title>Two Modules to Help Tame Large Drupal Menus</title><link>https://brianperry.dev/posts/2024/taming-drupal-menus/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/taming-drupal-menus/</guid><description>&lt;p&gt;Stop me if you&apos;ve heard this one before. At some point in the life of your Drupal site, you have a menu that has gotten out of control. Dragging and dropping is basically a lost cause, your hand hurts from scrolling, and a sense of dread approaches every time you find yourself in the menu administration screen. If it isn&apos;t possible to re-structure the menu to address the root cause, you&apos;ll need to turn to other solutions to make menu administration more manageable.&lt;/p&gt;
&lt;p&gt;I recently used two modules to address this issue for a client. They may not be a huge surprise to those who have run into this problem repeatedly, but it seemed worth documenting for both future me and also our search engine and LLM overlords.&lt;/p&gt;
&lt;h2&gt;Big Menu&lt;/h2&gt;
&lt;p&gt;The first module is &lt;a href=&quot;https://www.drupal.org/project/bigmenu&quot;&gt;Big Menu&lt;/a&gt;. The project page on this one seems to be describing the Drupal 7 implementation of the module, which is quite a bit different. The &apos;modern Drupal&apos; version of the module essentially re-works the menu administration page to focus on a single level of the menu tree at a time. Any menu item that has children will have an &apos;Edit child items&apos; link that you can drill into. This results in more clicks to get to the item you want to edit, but it makes the menu administration page much more manageable and reduces cognitive load quite a bit.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;You can also configure the module to use a different depth for the menu tree, which can be useful if wanted to see more of the menu in a single view. Personally I prefer to go all the way with this one and stick with the single level view that is used by default.&lt;/p&gt;
&lt;h2&gt;Menu Select&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.drupal.org/project/menu_select&quot;&gt;Menu Select&lt;/a&gt; module addresses the experience of selecting a parent menu item in the menu settings for a node or menu item. By default, this is a select list containing the entire menu, which can get very long. Menu Select replaces this with an autocomplete search and a hierarchal collapsible unordered list.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Bonus: Menu Firstchild&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.drupal.org/project/menu_firstchild&quot;&gt;Menu Firstchild&lt;/a&gt; is a little less about the admin experience, but can be useful in cases where a large menu needs some additional grouping but you don&apos;t want to turn to a full mega menu style approach. The module provides an option to have a menu item that doesn&apos;t have it&apos;s own path, but instead links to its first direct child.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Used together, these modules made a substantial difference in addressing the client&apos;s menu administration related feedback.&lt;/p&gt;
&lt;p&gt;This was also a reminder of the impact that the ongoing work on &lt;a href=&quot;https://www.drupal.org/project/drupal_cms&quot;&gt;Drupal CMS&lt;/a&gt; will hopefully have. I&apos;m looking forward to a Drupal CMS future that can theoretically pre-package user experience improvements like these. Or in cases where it might not be the right choice for Drupal CMS, opinionated community developed &lt;a href=&quot;https://www.drupal.org/docs/extending-drupal/drupal-recipes&quot;&gt;recipes&lt;/a&gt; can be created to address common use cases like this one.&lt;/p&gt;
</description><pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate></item><item><title>My Own Personal Bird of the Century</title><link>https://brianperry.dev/posts/2024/bird-of-century/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/bird-of-century/</guid><description>&lt;p&gt;Since I&apos;m currently spending more time outside enjoying the summer weather, I&apos;ve also been able to admire my most obscure yard decoration.&lt;/p&gt;
&lt;p&gt;If you don&apos;t watch &lt;a href=&quot;https://www.hbo.com/last-week-tonight-with-john-oliver&quot;&gt;Last Week Tonight&lt;/a&gt;, you may not be familiar with &lt;a href=&quot;https://www.npr.org/2023/11/15/1213228757/john-oliver-new-zealand-bird-century-contest-puteketeke&quot;&gt;how John Oliver wielded his audience to help the pūteketeke win New Zeland&apos;s Bird of the Century competition&lt;/a&gt;. You also wouldn&apos;t know that the &lt;a href=&quot;https://metalbird.com/&quot;&gt;Metalbird&lt;/a&gt; company went the extra mile and created a &lt;a href=&quot;https://metalbird.com/products/puteketekes-friend&quot;&gt;tiny metal John Oliver&lt;/a&gt; to compliment &lt;a href=&quot;https://metalbird.com/products/puteketeke&quot;&gt;their silhouette of the winning bird&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What you might be less surprised by is the fact that this metal duo ended up under my Christmas tree this year.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;It isn&apos;t the perfect placement - ideally the silhouette would be against the open sky, but I couldn&apos;t find a spot for that where it would still be visible. Still looks pretty cool where it lives, and hopefully it will catch the eye of our back yard guests. I also really love the look of the way the metal is weathering.&lt;/p&gt;
</description><pubDate>Mon, 24 Jun 2024 00:00:00 GMT</pubDate></item><item><title>Talking Drupal API Client on Talking Drupal</title><link>https://brianperry.dev/posts/2024/talking-drupal-api-client/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/talking-drupal-api-client/</guid><description>&lt;p&gt;I was a guest on a recent episode of &lt;a href=&quot;https://talkingdrupal.com/&quot;&gt;Talking Drupal&lt;/a&gt; to discuss the Drupal API Client. Always a great time participating in the show. Thanks to my &lt;a href=&quot;https://talkingdrupal.com/brian-perry&quot;&gt;bio page&lt;/a&gt; on their new website, I see that this is my 8th appearance on the show - four times as a guest, and four times as a guest host.&lt;/p&gt;
&lt;p&gt;We talked about the basics of the API Client, participating in the Pitch-burgh competition, the future of the project, and some of the challenges describing a primarily JavaScript focused project to an audience that typically thinks Drupal first.&lt;/p&gt;
&lt;p&gt;You can watch or listen to the episode below, on &lt;a href=&quot;https://talkingdrupal.com/454&quot;&gt;the episode page on talkingdrupal.com&lt;/a&gt;, or in your podcast player of choice.&lt;/p&gt;
&lt;p&gt;&amp;lt;audio controls=&quot;&quot; style=&quot;margin-bottom: var(--size-7)&quot;&amp;gt;
&amp;lt;source src=&quot;https://traffic.libsyn.com/sacstudio/td-454-libsyn.mp3&quot; type=&quot;audio/mpeg&quot;&amp;gt;
&amp;lt;/audio&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/IybVjiw23Js?si=5iSv0oqJoouYu9oz&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</description><pubDate>Wed, 12 Jun 2024 00:00:00 GMT</pubDate></item><item><title>Matching Drupal’s GitLab CI ESLint Configuration in a Contrib Module</title><link>https://brianperry.dev/posts/2024/matching-drupal-eslint/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/matching-drupal-eslint/</guid><description>&lt;p&gt;The Drupal Association now maintains a &lt;a href=&quot;https://git.drupalcode.org/project/gitlab_templates/-/blob/main/gitlab-ci/template.gitlab-ci.yml&quot;&gt;GitLab CI Template&lt;/a&gt; that can be used for all Drupal contrib projects. It&apos;s an excellent way to quickly take advantage of Drupal.org&apos;s CI system and ensure your project is following code standards and best practices. And using it has the bonus of giving you a sweet green checkmark on your project page!&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;We recently added this template to the &lt;a href=&quot;https://www.drupal.org/project/same_page_preview&quot;&gt;Same Page Preview&lt;/a&gt; module. After doing so, our JavaScript linting was failing. This wasn&apos;t surprising since we hadn&apos;t yet committed a standard &lt;a href=&quot;https://eslint.org&quot;&gt;ESLint&lt;/a&gt; or &lt;a href=&quot;https://prettier.io/&quot;&gt;Prettier&lt;/a&gt; configuration to the codebase. I took a shot at trying to resolve these linting issues, initially turning to the &lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-drupal-contrib&quot;&gt;ESLint Drupal Contrib plugin&lt;/a&gt;. This allowed me to get ESLint up and running quickly and run linting with only within the context of this module. I resolved all of the linting issues, pushed my work up to GitLab, and started thinking about how I&apos;d reward myself for a job well done.&lt;/p&gt;
&lt;h3&gt;Disaster Strikes&lt;/h3&gt;
&lt;p&gt;And as you might expect, the CI build still failed. 🤦‍♂️&lt;/p&gt;
&lt;p&gt;At this point I took a step back. First off, I needed to determine what differed between my ESLint process and the one that was being executed by the Drupal Gitlab CI Template. Secondly, beyond just getting the CI job to pass, I wanted to define the linting use cases I was trying to solve for. I decided to focus on the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Determining how to run the exact same ESLint command that the GitLab CI Template was running, using the same configuration as Drupal Core.&lt;/li&gt;
&lt;li&gt;Developing an ESLint configuration that could be run within the standalone module codebase (with or without an existing instance of Drupal) but matching Drupal Core and GitLab CI&apos;s configuration as closely as possible.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Using the Drupal Core ESLint Configuration&lt;/h3&gt;
&lt;p&gt;Here we literally want to use the same ESLint binary and config used by Drupal Core. Since this is what Drupal&apos;s GitLab CI Template is doing, this is also an opportunity to match the CI linting configuration as closely as possible.&lt;/p&gt;
&lt;p&gt;The CI job is running the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ $CI_PROJECT_DIR/$_WEB_ROOT/core/node_modules/.bin/eslint \
  --no-error-on-unmatched-pattern --ignore-pattern=&quot;*.es6.js&quot; \
  --resolve-plugins-relative-to=$CI_PROJECT_DIR/$_WEB_ROOT/core \
  --ext=.js,.yml \
  --format=junit \
  --output-file=$CI_PROJECT_DIR/junit.xml \
  $_ESLINT_EXTRA . || EXIT_CODE_FILE=$?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And prior to that command, symlinks are also created for some relevant configuration files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/$CI_PROJECT_NAME
$ ln -s $CI_PROJECT_DIR/$_WEB_ROOT/core/.eslintrc.passing.json $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/.eslintrc.json
$ ln -s $CI_PROJECT_DIR/$_WEB_ROOT/core/.eslintrc.jquery.json $CI_PROJECT_DIR/$_WEB_ROOT/modules/custom/.eslintrc.jquery.json
$ test -e .prettierrc.json || ln -s $CI_PROJECT_DIR/$_WEB_ROOT/core/.prettierrc.json .
$ test -e .prettierignore || echo &apos;*.yml&apos; &amp;gt; .prettierignore
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means that we&apos;ll need to run eslint using Core&apos;s &apos;passing&apos; configuration (which itself extends the &apos;jquery&apos; configuration.)&lt;/p&gt;
&lt;p&gt;To match that, I created an &lt;code&gt;eslint:core&lt;/code&gt; script in the module&apos;s &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;eslint:core&quot;: &quot;../../../core/node_modules/.bin/eslint . \
      --no-error-on-unmatched-pattern \
      --ignore-pattern=&apos;*.es6.js&apos; \
      --resolve-plugins-relative-to=../../../core \
      --ext=.js,.yml \
      -c ../../../core/.eslintrc.passing.json&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was surprised to find that even after running this command locally, the CI job was still failing. It turned out that ESLint wasn&apos;t using Core&apos;s Prettier config in this case, resulting in a different set of formatting rules being applied. Copying &lt;code&gt;core/.prettierrc.json&lt;/code&gt; into the module&apos;s root directory resolved this issue.&lt;/p&gt;
&lt;p&gt;Copying Drupal Core&apos;s prettier config wholesale isn&apos;t great. The approaches to referencing and extending a prettier config are clunky, but possible. A more ideal solution would be to have Drupal&apos;s prettier config as a package that could be referenced by both core and contrib modules.&lt;/p&gt;
&lt;h3&gt;Using a Standalone ESLint Configuration&lt;/h3&gt;
&lt;p&gt;Ideally it would also be possible to run this linting outside of the context of a full Drupal instance. This could help speed up things like pre-commit hooks, some CI tasks, and also make quick linting checks easier to run. With the lessons from using Drupal Core&apos;s ESLint configuration fresh in mind, I took another shot at using the &lt;code&gt;eslint-plugin-drupal-contrib&lt;/code&gt; plugin.&lt;/p&gt;
&lt;p&gt;First, I installed it in the module as a dev dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i -D eslint-plugin-drupal-contrib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I created a file &lt;code&gt;.eslintrc.contrib.json&lt;/code&gt; in the module&apos;s root directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;extends&quot;: [&quot;plugin:drupal-contrib/passing&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will result in eslint using the same configuration as Drupal Core&apos;s &apos;passing&apos; configuration, but without needing to reference Core&apos;s configuration files. Finally, you can run this by adding the following &lt;code&gt;eslint&lt;/code&gt; script in the module&apos;s &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;eslint&quot;: &quot;eslint . \
      -c .eslintrc.contrib.json \
      --no-eslintrc&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might be surprised to see the &lt;code&gt;--no-eslintrc&lt;/code&gt; flag above. That prevents ESLint from looking for any other configuration files in the directory tree. Without it, ESLint will find the Drupal Core configuration files if this happens to be run from within a Drupal project. This will result in ESLint attempting to resolve plugins using Drupal Core&apos;s &lt;code&gt;node_modules&lt;/code&gt; directory, which may or may not exist.&lt;/p&gt;
&lt;p&gt;Also note that ESLint 8.x uses the &lt;code&gt;--no-eslintrc&lt;/code&gt; flag, while the ESLint 9.x equivalent is &lt;code&gt;--no-config-lookup&lt;/code&gt;. Drupal core is currently on ESLint 8.x, which is the previous major release.&lt;/p&gt;
&lt;h3&gt;Happy Linting&lt;/h3&gt;
&lt;p&gt;I ran into a few more hiccups than I expected along the way, but now feel confident that I can have consistent linting results between my local environment and the Drupal.org CI system in all of the JavaScript code I write for contrib modules. Hopefully this can help you do the same.&lt;/p&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://git.drupalcode.org/project/gitlab_templates/-/blob/main/gitlab-ci/template.gitlab-ci.yml&quot;&gt;Drupal GitLab CI Template&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eslint.org/&quot;&gt;ESLint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prettier.io/&quot;&gt;Prettier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-drupal-contrib&quot;&gt;ESLint Drupal Contrib plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.drupal.org/docs/develop/standards/javascript-coding-standards/eslint-settings#s-checking-custom-javascript-with-eslint&quot;&gt;Checking custom JavaScript with ESLint&lt;/a&gt; - an alternative approach that can be run from the conext of &lt;code&gt;/core&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Wed, 05 Jun 2024 00:00:00 GMT</pubDate></item><item><title>Drupal API Client 1.0 Release</title><link>https://brianperry.dev/posts/2024/drupal-api-client-1/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/drupal-api-client-1/</guid><description>&lt;p&gt;We&apos;re extremely excited to announce the 1.0 release of the &lt;a href=&quot;https://www.drupal.org/project/api_client&quot;&gt;Drupal API Client&lt;/a&gt;. This release includes a fully functional JSON:API client and completes our commitment as a result of funding from the &lt;a href=&quot;https://www.drupal.org/innovation/pitchburgh-2023&quot;&gt;&apos;Pitch-burgh&apos; innovation contest&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Before diving into the details of some recent updates, let&apos;s recap the state of the project now that it has reached 1.0.&lt;/p&gt;
&lt;h2&gt;What is the Drupal API Client?&lt;/h2&gt;
&lt;p&gt;The Drupal API Client is a set of JavaScript packages that simplify the process of interacting with common Drupal APIs. Most commonly, developers will use our &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/json-api-client&quot;&gt;JSON:API client&lt;/a&gt; to interface with Drupal&apos;s JSON:API endpoints, but we also publish &lt;a href=&quot;/posts/2024/extending-api-client&quot;&gt;a base API Client package that can be extended&lt;/a&gt;, &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/decoupled-router-client&quot;&gt;a client for Decoupled Router&lt;/a&gt;, and &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3373029&quot;&gt;may support other Drupal APIs in the future&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Drupal API Client takes great care to be framework-agnostic and universal. It can be used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://project.pages.drupalcode.org/api_client/with-frameworks/overview/&quot;&gt;with your JavaScript framework of choice&lt;/a&gt;, vanilla JavaScript, or even in Drupal itself.&lt;/li&gt;
&lt;li&gt;with or without TypeScript.&lt;/li&gt;
&lt;li&gt;on the server, or on the client.&lt;/li&gt;
&lt;li&gt;with a bundler, or as a script import from a CDN.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Recent Developments&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;We&apos;ve completed our Pitch-burgh commitment and released JSON:API Client 1.0. Thanks to all who contributed along the way!&lt;/li&gt;
&lt;li&gt;We&apos;ve published &lt;a href=&quot;https://project.pages.drupalcode.org/api_client/&quot;&gt;detailed documentation on GitLab Pages&lt;/a&gt;. The docs include live code examples, and is itself an example of using the API Client with &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;We&apos;ve opened an &lt;a href=&quot;https://www.drupal.org/project/ideas/issues/3440566&quot;&gt;issue proposing that the Drupal API Client packages be promoted to the Drupal namespace on npm&lt;/a&gt;. We&apos;d love your feedback and support on the issue.&lt;/li&gt;
&lt;li&gt;We&apos;re refining &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3440572&quot;&gt;our 1.x roadmap&lt;/a&gt; and soliciting community feedback. One proposed priority will be &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3421478&quot;&gt;TypeScript improvements and automatic type generation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;See you at Stanford WebCamp and DrupalCon!&lt;/h2&gt;
&lt;p&gt;Moving forward, we hope to prioritize additional features for projects that could use our libraries as a dependency. Catch up with us at community events in May to learn more and share your use cases.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Drupal API Client will be featured in &lt;a href=&quot;https://webcamp.stanford.edu/session/the-drupal-api-client&quot;&gt;a session at Stanford WebCamp&lt;/a&gt;. Sessions are free and virtual, so this is a great way to get a more detailed overview of the project.&lt;/li&gt;
&lt;li&gt;We&apos;ll be holding a &lt;a href=&quot;https://events.drupal.org/files/media/documents/DrupalCon2024_Schedules_BoF.pdf&quot;&gt;Birds of a Feather&lt;/a&gt; discussion and participating in contribution events at DrupalCon Portland. The BOF will be on Monday, May 6 in room G129 at 4PM.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Beyond these events, we&apos;re always available in the #api-client channel on &lt;a href=&quot;https://www.drupal.org/community/contributor-guide/reference-information/talk/tools/slack&quot;&gt;Drupal Slack&lt;/a&gt; and monitoring &lt;a href=&quot;https://www.drupal.org/project/issues/api_client&quot;&gt;our issue queue&lt;/a&gt;. Hope to see you there!&lt;/p&gt;
</description><pubDate>Wed, 24 Apr 2024 00:00:00 GMT</pubDate></item><item><title>2024 Best Picture Nominees</title><link>https://brianperry.dev/posts/2024/2024-best-picture/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/2024-best-picture/</guid><description>&lt;p&gt;Ever since streaming made the years Oscar films so much easier to watch, I&apos;ve tried to make sure I see all the best picture nominees before the awards ceremony. I connected with some more than others, but no real duds this year (I&apos;m looking at you Elvis.)&lt;/p&gt;
&lt;h2&gt;My favorite movie of the year&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.imdb.com/title/tt13238346/&quot;&gt;Past Lives&lt;/a&gt; - Past Lives echoes the themes of some of my favorite movies like Eternal Sunshine of the Spotless Mind (which actually appears on screen in this) and Lost in Translation, which might give this a bit of an unfair advantage. Regardless, this was the movie that stuck with me the most. Greta Lee is fantastic, and compared to a number of other bloated nominees not a moment is wasted in its hour and forty-five minute runtime. I see myself revisiting this one again in the future.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Movies I Loved&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt1517268/&quot;&gt;Barbie&lt;/a&gt; - I re-watched this little known indie gem to see if I still felt the same way about it in comparison to &apos;serious&apos; Oscar movies. Turns out Barbie still rules. Still extremely funny, and in a perfect world Gosling would win best supporting actor. Sure, it probably tries to do a little too much, but on re-watch I was surprised how well everything came together in the third act.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14230458/&quot;&gt;Poor Things&lt;/a&gt; - Emma Stone is amazing in this and would be my pick for best actress. Drags a little bit in the second half, but still really loved it. Interested in working through some of Yorgos Lanthimos&apos; other movies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14849194/&quot;&gt;The Holdovers&lt;/a&gt; - it really doesn&apos;t get much better than Paul Giamatti and Da&apos;Vine Joy Randolph together in this. A warm hug of a movie with bonus points for spending time in &apos;70s Boston.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Movies I Liked&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt23561236/&quot;&gt;American Fiction&lt;/a&gt; - enjoyed this quite a bit, but by the end felt like there were a few different movies in conflict with each other. Excellent performances from both Jeffrey Wright and Sterling K. Brown though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt15398776/&quot;&gt;Oppenheimer&lt;/a&gt; - an achievement, but not really what I&apos;m looking for from Nolan. Impressive pacing despite its length. Had a little trouble looking past some of the well known faces that kept popping up, and not as crazy about Robert Downey Jr. in this as others seem to be.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt17009710/&quot;&gt;Anatomy of a Fall&lt;/a&gt; - entertaining and well acted, although my mind will probably always just think of Snoop the dog, and the instrumental cover of P.I.M.P.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt7160372/&quot;&gt;The Zone of Interest&lt;/a&gt; - amazing use of sound, but boy was it a tough watch. Did find the final sequence extremely moving though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt5537002/&quot;&gt;Killers of the Flower Moon&lt;/a&gt; - was just longer than I could handle given the dark subject matter and many extremely unlikable characters. Thought De Niro was truly great in it, not sure why his work didn&apos;t get more attention. Appreciated Lilly Gladstone&apos;s performance as well, but personally found it a little too internal to fully connect with.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.imdb.com/title/tt5535276/&quot;&gt;Maestro&lt;/a&gt; - expected this to be over the top Oscar bait, but I enjoyed it overall. Between this and Flower Moon I did reach my limit on strong women who stood by men who treated them like garbage.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bonus&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.imdb.com/title/tt13651794/&quot;&gt;May December&lt;/a&gt; - kind of mixed on this one but thought Charles Melton had one of the strongest performances of the year in this. A shame he didn&apos;t get nominated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;My Ballot&lt;/h2&gt;
&lt;p&gt;Not a member of the academy (yet,) but here are my predictions this year.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Going to crush the Oscars pool at my wife&apos;s school.&lt;/p&gt;
</description><pubDate>Mon, 11 Mar 2024 00:00:00 GMT</pubDate></item><item><title>Extending The Drupal API Client</title><link>https://brianperry.dev/posts/2024/extending-api-client/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/extending-api-client/</guid><description>&lt;p&gt;import RadCallout from &apos;../../../components/rad/RadCallout.astro&apos;;&lt;/p&gt;
&lt;p&gt;As a result of our &lt;a href=&quot;https://www.drupal.org/innovation/pitchburgh-2023&quot;&gt;Pitch-burgh funding&lt;/a&gt;, the current focus of &lt;a href=&quot;https://www.drupal.org/project/api_client&quot;&gt;the Drupal API Client&lt;/a&gt; is to create a fully featured client for Drupal&apos;s JSON:API implementation. Even with that goal, we&apos;ve focused on making our work extensible for other API formats in the future through the implementation of an &lt;code&gt;ApiClient&lt;/code&gt; base class. Functionality that could apply to any API client is added to the base class, while anything specific to JSON:API is added to the &lt;code&gt;JsonApiClient&lt;/code&gt; class (which extends &lt;code&gt;ApiClient&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;Recently, we have been working on adding &lt;a href=&quot;https://www.drupal.org/project/decoupled_router&quot;&gt;Decoupled Router&lt;/a&gt; support to &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/json-api-client&quot;&gt;our JSON:API Client&lt;/a&gt;. I found this implementation to be a great example of the extensibility of the library, so I wanted elaborate on it in a blog post for those who may want to extend the API Client in the future.&lt;/p&gt;
&lt;p&gt;The existing JsonApiClient has the following method to retrieve data for a resource:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await client.getResource(&apos;node--article&apos;, &apos;3347c400-302d-4f6c-8fcb-3e74beb002c8&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ideally, users of Decoupled Router could also get an identical response by resolving a path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await client.getResource(&apos;/articles/give-it-a-go-and-grow-your-own-herbs&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To achieve this, we first needed to provide a way to reliably get data from Decoupled Router.&lt;/p&gt;
&lt;h2&gt;The Decoupled Router Endpoint&lt;/h2&gt;
&lt;p&gt;With the module enabled, Decoupled Router exposes an endpoint with the following structure:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/router/translate-path?path=&amp;lt;path&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Given a path like &lt;code&gt;/articles/give-it-a-go-and-grow-your-own-herbs&lt;/code&gt; the endpoint could provide a response similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;resolved&quot;: &quot;https://dev-drupal-api-client-poc.pantheonsite.io/en/articles/give-it-a-go-and-grow-your-own-herbs&quot;,
  &quot;isHomePath&quot;: false,
  &quot;entity&quot;: {
    &quot;canonical&quot;: &quot;https://dev-drupal-api-client-poc.pantheonsite.io/en/articles/give-it-a-go-and-grow-your-own-herbs&quot;,
    &quot;type&quot;: &quot;node&quot;,
    &quot;bundle&quot;: &quot;article&quot;,
    &quot;id&quot;: &quot;11&quot;,
    &quot;uuid&quot;: &quot;3347c400-302d-4f6c-8fcb-3e74beb002c8&quot;
  },
  &quot;label&quot;: &quot;Give it a go and grow your own herbs&quot;,
  &quot;jsonapi&quot;: {
    &quot;individual&quot;: &quot;https://dev-drupal-api-client-poc.pantheonsite.io/en/jsonapi/node/article/3347c400-302d-4f6c-8fcb-3e74beb002c8&quot;,
    &quot;resourceName&quot;: &quot;node--article&quot;,
    &quot;pathPrefix&quot;: &quot;jsonapi&quot;,
    &quot;basePath&quot;: &quot;/jsonapi&quot;,
    &quot;entryPoint&quot;: &quot;https://dev-drupal-api-client-poc.pantheonsite.io/en/jsonapi&quot;
  },
  &quot;meta&quot;: {
    &quot;deprecated&quot;: {
      &quot;jsonapi.pathPrefix&quot;: &quot;This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While easy to make sense of, this response technically doesn&apos;t follow the JSON:API spec, which prevents us from using our existing JSON:API Client without modification. We could write a small amount of custom code in JsonApiClient to fetch and handle data from this endpoint, but this case is exactly what our ApiClient base class is intended for. With a similarly small amount of code we can extend the ApiClient class to add only what is unique to the Decoupled Router endpoint, while getting access to all of the features of the base class at the same time.&lt;/p&gt;
&lt;p&gt;So rather than writing code specific to JsonApiClient, we decided to create a new DecoupledRouterClient class that our JsonApiClient could then make use of.&lt;/p&gt;
&lt;h2&gt;Extending ApiClient&lt;/h2&gt;
&lt;p&gt;For the sake of example, a simple Decoupled Router client could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DecoupledRouterClient.ts
import {
  ApiClient,
  type ApiClientOptions,
  type BaseUrl,
} from &quot;@drupal-api-client/api-client&quot;;

export class DecoupledRouterClient extends ApiClient {
  constructor(baseUrl: BaseUrl, options?: ApiClientOptions) {
    super(baseUrl, options);
    const { apiPrefix } = options || {};
    this.apiPrefix = apiPrefix || &quot;router/translate-path&quot;;
  }

  async translatePath(path: string) {
    const apiUrl = `${this.baseUrl}/${this.apiPrefix}?path=${path}`;
    const response = await this.fetch(apiUrl);

    return response.json();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our constructor, the only modification we need to make is the default value for the API prefix. While the base class doesn&apos;t have a default, Decoupled Router uses &lt;code&gt;router/translate-path&lt;/code&gt;. Now when instance of &lt;code&gt;DecoupledRouter&lt;/code&gt; is created without this option, it will use the default.&lt;/p&gt;
&lt;p&gt;We then define a &lt;code&gt;translatePath&lt;/code&gt; method that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Takes a path of type string&lt;/li&gt;
&lt;li&gt;Uses the fetch method provided by the base class to make a request to Decoupled Router&lt;/li&gt;
&lt;li&gt;Returns a promise with the provided json data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using an instance of this class would look something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.ts
import { DecoupledRouterClient } from &quot;./DecoupledRouterClient.ts&quot;;

const decoupledRouterClient = 
  new DecoupledRouterClient(&quot;https://dev-drupal-api-client-poc.pantheonsite.io&quot;);

const translatedPath =
  await decoupledRouterClient.translatePath(
    &quot;/articles/give-it-a-go-and-grow-your-own-herbs&quot;
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;RadCallout&amp;gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/extending-drupal-api-client-32wt3c&quot;&gt;Check out this code sandbox&lt;/a&gt; for a live version of the example above.&amp;lt;/RadCallout&amp;gt;&lt;/p&gt;
&lt;h2&gt;Taking Advantage of Additional ApiClient Features&lt;/h2&gt;
&lt;p&gt;With this example we already have a functional client, but quite a bit more is possible using the features of the ApiClient class we extended. For example, We can already make authenticated requests using any of the supported authentication methods:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.ts
import { DecoupledRouterClient } from &quot;./DecoupledRouterClient.ts&quot;;

const decoupledRouterClient = 
  new DecoupledRouterClient(&quot;https://dev-drupal-api-client-poc.pantheonsite.io&quot;, {
    authentication: {
      type: &quot;OAuth&quot;,
      credentials: {
        clientId: &quot;client-id&quot;,
        clientSecret: &quot;client-secret&quot;
      }
    },
  });

// API requests will now be authenticated
const translatedPath =
  await decoupledRouterClient.translatePath(
    &quot;/articles/give-it-a-go-and-grow-your-own-herbs&quot;
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our example Decoupled Router client could be updated to take advantage of built in caching, logging, or locale support. For example, the following modification would allow us to make use of the &lt;code&gt;defaultLocale&lt;/code&gt; option if our Drupal site supports multiple languages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DecoupledRouterClient.ts
import {
  ApiClient,
  type ApiClientOptions,
  type BaseUrl,
} from &quot;@drupal-api-client/api-client&quot;;

export class DecoupledRouterClient extends ApiClient {
  constructor(baseUrl: BaseUrl, options?: ApiClientOptions) {
    super(baseUrl, options);
    const { apiPrefix } = options || {};
    this.apiPrefix = apiPrefix || &quot;router/translate-path&quot;;
  }

  async translatePath(path: string) {
    // If it exists, incorporate the default locale
    // into the apiUrl
    const apiUrlObject = new URL(
      `${this.defaultLocale ?? &quot;&quot;}/${this.apiPrefix}?path=${path}`,
      this.baseUrl,
    );
    const apiUrl = apiUrlObject.toString();
    const response = await this.fetch(apiUrl);

    return response.json();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Routing is a common problem, so we&apos;ve added a fully featured &lt;a href=&quot;https://project.pages.drupalcode.org/api_client/classes/_drupal_api_client_json_api_client.JsonApiClient.html#getResourceByPath&quot;&gt;getResourceByPath method&lt;/a&gt; to our latest &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/json-api-client&quot;&gt;@drupal-api-client/json-api-client&lt;/a&gt; release. We&apos;ve also published the &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/decoupled-router-client&quot;&gt;Decoupled Router client&lt;/a&gt; as a standalone package for anyone who wants to use it separately.&lt;/p&gt;
&lt;p&gt;While the caching functionality of the client can lessen the impact, getResourceByPath still makes multiple API calls for uncached data, which leaves room for improvement. We could optimize this in the future by providing support for the &lt;a href=&quot;https://www.drupal.org/project/subrequests&quot;&gt;subrequests module&lt;/a&gt;. That is yet another client for a type of Drupal API that could use the ApiClient base class as a starting point.&lt;/p&gt;
&lt;p&gt;We&apos;re closing in on the 1.0 release of &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/json-api-client&quot;&gt;@drupal-api-client/json-api-client&lt;/a&gt;. If you’re interested in contributing, check out our project page on Drupal.org, and join us in the &lt;a href=&quot;https://www.drupal.org/community/contributor-guide/reference-information/talk/tools/slack&quot;&gt;#api-client&lt;/a&gt; channel on Drupal Slack.&lt;/p&gt;
</description><pubDate>Wed, 07 Feb 2024 00:00:00 GMT</pubDate></item><item><title>Winter Builds</title><link>https://brianperry.dev/posts/2024/winter-builds/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/winter-builds/</guid><description>&lt;p&gt;While we&apos;re far from the puzzle and Lego heyday that was the pandemic, things tend to slow down enough during winter break for me to party like it&apos;s 2020.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;I put together this Charlie Brown Christmas puzzle like an advent calendar (hat tip to my Mom for this idea last year.) Each day I had access to 42 (technically 41.6 repeating) new pieces of this 1000 piece puzzle. Things kind of stacked up at the end, and I technically finished this up quite late on Christmas day.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Lego wise I put together the &lt;a href=&quot;https://www.lego.com/en-us/product/piranha-plant-71426&quot;&gt;Lego Piranha Plant&lt;/a&gt;, which was a small but fun build.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The window well in my office features a growing variety of takes on the piranha plant, including an actual venus flytrap that is really struggling during the winter months. When I got the awesome &apos;real life&apos; piranha plant (the nightmare &apos;Little Shop of Horrors&apos; looking one) for Christmas I knew I had to get this Lego version to round out the collection.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Capping things off was our local park district&apos;s &apos;Puzzle Palooza&apos;, an &lt;a href=&quot;https://www.facebook.com/watch/?v=708757353937811&quot;&gt;amusingly serious&lt;/a&gt; two hour puzzle building competition. It was just my wife and I in what could have been a four person team, so we had no shot at winning. Even given that, we fared really poorly and didn&apos;t even complete the border of the puzzle. In our defense, this was a difficult puzzle with many irregular pieces. We got to keep the puzzle, so someday I&apos;ll have to see what those birds in snow really look like. Maybe it can be next year&apos;s advent puzzle.&lt;/p&gt;
</description><pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate></item><item><title>The 2023 New England Patriots</title><link>https://brianperry.dev/posts/2024/2023-patriots/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/2023-patriots/</guid><description>&lt;p&gt;As a Patriots fan, 2023 was quite the rude awakening. It has been rough going since Brady left, but this was different. I&apos;ve been a Patriots fan long enough that I experienced many bad seasons pre-Brady, so I knew it was somewhat inevitable, but I had wondered exactly what it would be like experiencing the basement of the NFL after such a long dynasty. Would it infuriate me? Would I give up and lose my rooting interest entirely? Would I even be able to show my face in the Chicagoland area if the Pats have a worse record than the Bears?&lt;/p&gt;
&lt;p&gt;The good news is that I enjoyed this no good, very bad season much more than I expected. With a few notable exceptions, most games were still close and thus somewhat entertaining. And I still listened to Patriots podcasts and followed the weekly power rankings. The topics were just different this time. Quarterback controversy. Draft rankings (why did you have to win that Christmas Eve game!?!) Will Belichick stay or go?&lt;/p&gt;
&lt;p&gt;I don&apos;t want that to be the nature of my fandom forever, but it&apos;s nice to confirm that I enjoy the process of following this team regardless of what that actually looks like. With &lt;a href=&quot;https://www.espn.com/nfl/story/_/id/39285303/bill-belichick-leaving-patriots-24-seasons-sources-say&quot;&gt;Belichick officially gone&lt;/a&gt;, &lt;a href=&quot;https://www.espn.com/nfl/story/_/id/39293221/patriots-hire-jerod-mayo-replace-bill-belichick-sources-say&quot;&gt;Mayo (almost) officially the successor&lt;/a&gt;, and &lt;a href=&quot;https://www.tankathon.com/nfl&quot;&gt;the 3rd pick in the NFL Draft&lt;/a&gt;, it should be a very different Patriots team next season.&lt;/p&gt;
&lt;p&gt;Despite how it ended, this has been a hell of a run.&lt;/p&gt;
</description><pubDate>Sat, 13 Jan 2024 00:00:00 GMT</pubDate></item><item><title>44 in 2024</title><link>https://brianperry.dev/posts/2024/44-in-2024/</link><guid isPermaLink="true">https://brianperry.dev/posts/2024/44-in-2024/</guid><description>&lt;p&gt;I took an extended holiday break this year to match my family&apos;s school vacations. That means I&apos;m currently bracing for a return to reality after the Christmas holiday, a New Year for the world, and a new year for me (44!) Here&apos;s what has been top of mind as January makes its presence known.&lt;/p&gt;
&lt;h2&gt;2023&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Junior High&lt;/strong&gt; - my son started junior high this year which has been exciting and new for all of us. He&apos;s also been doing well playing drums in the band and running for the cross-country team. It has also turned out to be pretty useful to be married to a school psychologist that works in a junior high school.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;More of my mouth broke&lt;/strong&gt; - I got a root canal after a crown went bad in early 2023. Not my first, but still not fun. Just hoping for no mouth related changes this year.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Big new things around the house!&lt;/strong&gt; After threatening to do so for a number of years, we got a small above ground pool. Led to some fun times with neighborhood friends, and some great Sunday afternoon floats. Also, as an early family Christmas gift we got a &lt;a href=&quot;https://www.lovesac.com/&quot;&gt;Lovesac&lt;/a&gt; to replace our long broken basement couch. Much easier to get all humans and cats in the same place now, which makes a big difference.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ongoing Decoupled CMS work&lt;/strong&gt; - I wasn&apos;t able to attend &lt;a href=&quot;https://events.drupal.org/pittsburgh2023&quot;&gt;DrupalCon Pittsburgh&lt;/a&gt;, but I did submit the &lt;a href=&quot;https://www.drupal.org/project/api_client&quot;&gt;Drupal API Client&lt;/a&gt; Project to the &lt;a href=&quot;https://www.drupal.org/innovation/pitchburgh-2023&quot;&gt;Pitch-burgh Innovation Contest&lt;/a&gt;. We were selected as a finalist by the judging panel, and then funded by a community vote. In my day-to-day I was also continuing to lead the &lt;a href=&quot;https://decoupledkit.pantheon.io/docs&quot;&gt;Decoupled Kit&lt;/a&gt; project and generally helping support &lt;a href=&quot;https://docs.pantheon.io/guides/decoupled/overview&quot;&gt;Front-End Sites at Pantheon&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tech Related Travel&lt;/strong&gt; - scaled down my conference travel last year, but still made it to &lt;a href=&quot;https://www.fldrupal.camp/&quot;&gt;Florida Drupal Camp&lt;/a&gt;, &lt;a href=&quot;https://www.midcamp.org/&quot;&gt;MidCamp&lt;/a&gt;, &lt;a href=&quot;https://2023.tcdrupal.org/&quot;&gt;Twin Cities Drupal Camp&lt;/a&gt;, and &lt;a href=&quot;https://nedcamp.org/&quot;&gt;New England Drupal Camp&lt;/a&gt;. I also visited Pantheon&apos;s San Francisco headquarters for the first time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tons of Tears of the Kingdom&lt;/strong&gt; - I&apos;ve played 150 hours of &lt;a href=&quot;https://zelda.nintendo.com/tears-of-the-kingdom/&quot;&gt;Tears of The Kingdom&lt;/a&gt; and counting. Similar to Breath of the Wild I was obsessed in the beginning, and then went through cycles of playing and taking extended breaks. Crazy how much game there is here. Still a lot that I&apos;m hoping to do - expecting that I&apos;ll wrap up around 200 hours.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A Terrible Patriots Season&lt;/strong&gt; - truly terrible, but still somehow managed to be fun and interesting to follow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Saw Some Fun Shows&lt;/strong&gt; - finally made it to &lt;a href=&quot;https://thefestfl.com/&quot;&gt;The Fest&lt;/a&gt; in Gainesville, FL for the first time. Had a blast, but may have exceeded the limits of what my then 43 year old ass could handle. Huge thanks to &lt;a href=&quot;https://herchel.com/&quot;&gt;Mike Herchel&lt;/a&gt; for hosting me. Also saw the sketch comedy group &lt;a href=&quot;http://localhost:4321/posts/2023/the-state-chicago&quot;&gt;The State reunited&lt;/a&gt;, which was a blast.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2024&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A New Website!&lt;/strong&gt; - made a long gestating migration of this site to &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; in the new year. There is quite a bit more that I&apos;d like to do with the site in 2024 - we&apos;ll see if I can keep the momentum going.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Drupal API Client 1.0&lt;/strong&gt; - I&apos;ll be ramping back up after the holidays for a focused effort on &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3373023&quot;&gt;our 1.0 scope&lt;/a&gt; to close out our commitment to Pitch-burgh funding. The 1.0 release will also open up a number of new possibilities for using the client. I have an idea for a new open source project building on this work that I&apos;m excited about.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Brushing Up on TypeScript&lt;/strong&gt; - both this website and the API Client are written in TypeScript, but I feel like much of my TS learning thus far has been on the job training, which has left some gaps. As a result, I&apos;ve been brushing up over the break with the courses at &lt;a href=&quot;https://www.executeprogram.com/&quot;&gt;Execute Program&lt;/a&gt;, which I think are really great. On top of that, hopefully having multiple ongoing TypeScript projects will help me stay fresh.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Charting New Territory Professionally&lt;/strong&gt; - after multiple years focusing on a specific long-term project at my day job, I&apos;ll be dedicated to something new and different as 2024 starts. Perhaps that will click for me. If not, it might be time for something even newer. Either way, this year is sure to be filled with new professional challenges.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Traditional New Years Fitness Goals&lt;/strong&gt; - need to stay focused fitness wise as it gets harder and harder to keep this ancient body in shape. Related to that, I&apos;m going to be trying out a dry-ish January (-ish because I&apos;m cheating and starting at the end of my vacation and the Patriot&apos;s season.) I&apos;m hoping that cutting out beer will make maintaining my weight after the holidays a little easier.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Steam Deck&lt;/strong&gt; - I broke down on my birthday and ordered myself a &lt;a href=&quot;https://www.steamdeck.com/en/oled&quot;&gt;Steam Deck OLED&lt;/a&gt;. This is partly motivated by hearing endless positive things about the Steam Deck in 2023. But possibly even more motivated by starting &lt;a href=&quot;https://baldursgate3.game/&quot;&gt;Baldur&apos;s Gate 3&lt;/a&gt; on Xbox over break and wanting a reliable way to play it handheld. Could see myself putting Zelda-level hours into that one in 2024.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Golf Lessons&lt;/strong&gt; - I&apos;ve played a little more frequently over the past few years and it&apos;s time to start sucking less. Tempted to get new clubs, but I think the smarter move is to get lessons first, and then reward myself with some new equipment if I make some positive progress.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Mon, 08 Jan 2024 00:00:00 GMT</pubDate></item><item><title>The State Live in Chicago</title><link>https://brianperry.dev/posts/2023/the-state-chicago/</link><guid isPermaLink="true">https://brianperry.dev/posts/2023/the-state-chicago/</guid><description>&lt;p&gt;It isn&apos;t a stretch to say that the 90s sketch comedy group &lt;a href=&quot;https://davidwain.com/thestate&quot;&gt;The State&lt;/a&gt; are one of my all-time biggest influences. Not only did the show lead me to years of performing comedy, but I also learned html in order to create an impressively ugly fan site.&lt;/p&gt;
&lt;p&gt;I previously saw The State live just after their MTV show ended, at the now defunct &lt;a href=&quot;https://www.carolines.com/&quot;&gt;Carolines&lt;/a&gt; in Times Square. If you told me then that I&apos;d bring my wife to see them in Chicago in 2023, I would have had a few questions. But I wouldn&apos;t be suprised to hear that those sketches meant as much to me in 2023 as they did back when MTV actually played music videos. Getting a chance to relive those sketches without having to break out a VHS bootleg was pure joy.&lt;/p&gt;
</description><pubDate>Mon, 27 Nov 2023 00:00:00 GMT</pubDate></item><item><title>An Update on The Drupal API Client</title><link>https://brianperry.dev/posts/2023/drupal-api-client-update/</link><guid isPermaLink="true">https://brianperry.dev/posts/2023/drupal-api-client-update/</guid><description>&lt;p&gt;At &lt;a href=&quot;https://events.drupal.org/pittsburgh2023&quot;&gt;DrupalCon Pittsburgh&lt;/a&gt;, a team of interested community members submitted &lt;a href=&quot;https://www.drupal.org/project/api_client&quot;&gt;The Drupal API Client Project&lt;/a&gt; to be considered for funding as part of the &lt;a href=&quot;https://www.drupal.org/innovation/pitchburgh-2023&quot;&gt;Pitchburgh Innovation Contest&lt;/a&gt;. (If you missed it, check out our pitch video below, along with the &lt;a href=&quot;https://docs.google.com/document/d/1MAUCgxJmSHxA6ozVXp6U49UMPF3sJPrInz1rnl9Wf_4/edit?pli=1#heading=h.9531ycwaflet&quot;&gt;original proposal&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/EdTnrPZUW98?si=qSF4Kml7J9p9HFMh&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;While competing community maintained API clients exist, Drupal does not offer any official JavaScript utilities to simplify the process of consuming data outside of the CMS. Our goal is to assemble a group of contributors to combine the best of existing Drupal API clients into a set of utilities that can both address common use cases with little configuration, and also be extended to support the needs of a diverse JavaScript ecosystem.&lt;/p&gt;
&lt;p&gt;We were &lt;a href=&quot;https://youtu.be/tNa4XKb3zds?si=di_9WNupphYQrnPi&amp;amp;t=4995&quot;&gt;lucky enough to be selected&lt;/a&gt; by a panel of judges and the Drupal community to recieve funding. Since then, we&apos;ve made some exciting progress on making our pitch a reality and wanted to share some updates.&lt;/p&gt;
&lt;h3&gt;Initial Goals and Vertical Slice POC&lt;/h3&gt;
&lt;p&gt;Our initial focus is what we&apos;ve been calling the &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3365506&quot;&gt;vertical slice POC&lt;/a&gt;  for our JSON:API Client. As a checkpoint on the way to our eventual 1.0 release, we saw value in focusing deeply on a narrow portion of the client. Specifically, we decided to focus on the ability to get a collection of resources from the API.&lt;/p&gt;
&lt;p&gt;Alongside that functionality, we also wanted to cover some level  of implementation for all of our planned configuration options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom fetch method &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3376929&quot;&gt;#3376929&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Authentication (basic authentication for the POC) &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3376937&quot;&gt;#3376937&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Local caching &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3377144&quot;&gt;#3377144&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Deserialization of API responses &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3377191&quot;&gt;#3377191&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Basic localization support &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3377803&quot;&gt;#3377803&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Adding query parameters to API requests &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3383578&quot;&gt;#3383578&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Going this deep on our method to get a collection should provide groundwork that will help simplify the remaining implementation on the way to 1.0. And possibly more importantly, having a functional POC early should allow us to engage with the community and gather real world feedback.&lt;/p&gt;
&lt;h3&gt;0.1.0 Release&lt;/h3&gt;
&lt;p&gt;Speaking of the POC, we&apos;re excited to announce that &lt;a href=&quot;https://www.npmjs.com/package/@drupal-api-client/json-api-client&quot;&gt;we&apos;ve published our first release&lt;/a&gt;! You can install it right now using npm (or your package manager of choice):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i @drupal-api-client/json-api-client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://project.pages.drupalcode.org/api_client/modules/_drupal_api_client_json_api_client&quot;&gt;API documentation for the package&lt;/a&gt; is available on GitLab pages.&lt;/p&gt;
&lt;p&gt;Included below is a CodeSandbox including a simple example of using the client to get a collection of nodes from a Drupal site. Feel free to fork it and play around!&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://codesandbox.io/embed/drupal-api-client-json-api-client-basic-example-54t589?fontsize=14&amp;amp;hidenavigation=1&amp;amp;module=%2Fsrc%2Findex.mjs&amp;amp;theme=dark&amp;amp;view=editor&quot;
style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
title=&quot;@drupal-api-client/json-api-client Basic Example&quot;
allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For the more adventurous, here&apos;s a live TypeScript example from our project readme that demonstrates more of the available configuration options:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://codesandbox.io/embed/drupal-api-client-json-api-client-configuration-options-4wyqrw?fontsize=14&amp;amp;hidenavigation=1&amp;amp;module=%2Fsrc%2Findex.ts&amp;amp;theme=dark&amp;amp;view=editor&quot;
style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
title=&quot;@drupal-api-client/json-api-client Configuration Options&quot;
allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This 0.1.0 release represents the initial POC, but we&apos;re planning on having continued 0.x releases as we approach 1.0.&lt;/p&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https://git.drupalcode.org/project/api_client/-/graphs/canary?ref_type=heads&quot;&gt;all who contributed to this release&lt;/a&gt;, including: &lt;a href=&quot;https://www.drupal.org/u/cobysher&quot;&gt;coby.sher&lt;/a&gt;, &lt;a href=&quot;https://www.drupal.org/u/pratik_kamble&quot;&gt;pratik_kamble&lt;/a&gt;, &lt;a href=&quot;https://www.drupal.org/u/mitchellmarkoff&quot;&gt;mitchellmarkoff&lt;/a&gt;, &lt;a href=&quot;https://www.drupal.org/u/shrutishende&quot;&gt;shrutishende&lt;/a&gt;, &lt;a href=&quot;https://www.drupal.org/u/elber&quot;&gt;elber&lt;/a&gt;, &lt;a href=&quot;https://www.drupal.org/u/abhisekmazumdar&quot;&gt;abhisekmazumdar&lt;/a&gt;, &lt;a href=&quot;https://www.drupal.org/u/alexmoreno&quot;&gt;alexmoreno&lt;/a&gt; and the many others who provided feedback in the issue queue or dropped in on one of our Slack meetings.&lt;/p&gt;
&lt;h3&gt;Obligatory Call for Help&lt;/h3&gt;
&lt;p&gt;The Drupal API Client Project is rolling along, but we&apos;re still looking for help from the community to make it a success. With the release of this POC, one of the biggest areas we need help is getting general feedback on our work thus far. The wider the feedback we get from the community, the better chance we have to ship something that meets a wide array of decoupled Drupal use cases.&lt;/p&gt;
&lt;p&gt;We&apos;ve created a &lt;a href=&quot;https://www.drupal.org/project/api_client/issues/3383579&quot;&gt;feedback issue&lt;/a&gt; to help guide the conversation, but we&apos;re also open to feedback in any form. We&apos;re you able to get up and running? Does the current API make sense to you? Is there a particular use case you&apos;re hoping to see supported? Let us know!&lt;/p&gt;
&lt;p&gt;You also may have noted that many of our primary contributors work together at &lt;a href=&quot;https://pantheon.io/&quot;&gt;Pantheon&lt;/a&gt;. While we greatly appreciate Pantheon&apos;s support and sponsorship of this project, we&apos;re also looking for help from the broader community. We&apos;re doing a decent job keeping focused on the generic use cases that this project is intended to solve, but having a more diverse array of contributors can only help the end result.&lt;/p&gt;
&lt;p&gt;If you&apos;re interested in contributing, check out our &lt;a href=&quot;https://www.drupal.org/project/api_client&quot;&gt;project page&lt;/a&gt; on Drupal.org, and join us in the &lt;a href=&quot;https://www.drupal.org/community/contributor-guide/reference-information/talk/tools/slack&quot;&gt;#api-client&lt;/a&gt; channel on Drupal Slack.&lt;/p&gt;
&lt;p&gt;And if you happen to be interested and on the East Coast, I&apos;ll be at &lt;a href=&quot;https://nedcamp.org&quot;&gt;NedCamp&lt;/a&gt; - both at the contribution day, and &lt;a href=&quot;https://nedcamp.org/sessions/2023/drupal-api-client-project&quot;&gt;presenting on the API Client project&lt;/a&gt; during session day. Come say hi!&lt;/p&gt;
</description><pubDate>Thu, 09 Nov 2023 00:00:00 GMT</pubDate></item><item><title>My Holiday Lego Build</title><link>https://brianperry.dev/posts/2023/holiday-lego-build/</link><guid isPermaLink="true">https://brianperry.dev/posts/2023/holiday-lego-build/</guid><description>&lt;p&gt;Stretched a little bit past my holiday break, but enjoyed assembling this Lego Question Block. Had it for over a year, but never got a chance to put it together.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Up next is the &lt;a href=&quot;https://www.lego.com/en-us/product/atari-2600-10306&quot;&gt;Lego Atari 2600&lt;/a&gt;, which hopefully I&apos;ll make quicker work of.&lt;/p&gt;
</description><pubDate>Sun, 08 Jan 2023 00:00:00 GMT</pubDate></item><item><title>Decoupled Menus (and Beyond) at DrupalCon Portland</title><link>https://brianperry.dev/posts/2022/decoupled-menus-and-beyond-at-drupalcon-portland/</link><guid isPermaLink="true">https://brianperry.dev/posts/2022/decoupled-menus-and-beyond-at-drupalcon-portland/</guid><description>&lt;p&gt;DrupalCon Portland is almost here! Getting back to an in-person DrupalCon will take some getting used to, but I&apos;m especially excited for the ability to collaborate face-to-face during contribution events throughout the week. There is quite a bit going on in the world of Decoupled Drupal - here are some of the things I plan on focusing on...&lt;/p&gt;
&lt;h2&gt;Decoupled Menus Initiative&lt;/h2&gt;
&lt;p&gt;We’re aiming to get a point where we can declare this specific initiative complete and could use help in a few specific focus areas.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.drupal.org/project/drupal/issues/3227824&quot;&gt;Migrating the decoupled menus linkset endpoint to Drupal Core&lt;/a&gt; - we&apos;re very close to getting this functionality included in core, but will still likely need some final assistance at DrupalCon.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Documentation improvements - first and foremost, we need to &lt;a href=&quot;https://www.drupal.org/project/decoupled_menus_initiative/issues/3263181&quot;&gt;finalize the documentation specific to the new decoupled menus endpoint&lt;/a&gt;. We have made good progress there and mostly just need to incorporate some proposed revisions. We&apos;ve also &lt;a href=&quot;https://www.drupal.org/project/decoupled_menus_initiative/issues/3265903&quot;&gt;migrated the existing Decoupled Drupal documentation&lt;/a&gt; to &lt;a href=&quot;https://www.drupal.org/docs/develop/decoupled-drupal&quot;&gt;a new home within the main Drupal Developer docs&lt;/a&gt;. There is a huge opportunity to flesh out the overall documentation for Decoupled Drupal, and &lt;a href=&quot;https://www.drupal.org/project/documentation/issues/3276081&quot;&gt;hopefully this will evolve into a focused effort&lt;/a&gt; beyond the Decoupled Menus Initiative.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Decoupled Menu Parser - the &lt;a href=&quot;https://www.drupal.org/project/decoupled_menu_parser&quot;&gt;Decoupled Menus Parser&lt;/a&gt; is a JavaScript library to simplify consuming data from the new decoupled menus endpoint. There are &lt;a href=&quot;https://www.drupal.org/project/issues/decoupled_menu_parser?categories=All&quot;&gt;a few open issues&lt;/a&gt; to update the library due to recent adjustments to the API response.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Drupal State&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://events.drupal.org/portland2022/sessions/drupal-state-and-need-javascript-sdk&quot;&gt;Drupal State and the Need for a JavaScript SDK&lt;/a&gt; - bright and early Thursday morning I&apos;ll be presenting both about the Drupal State project, and perhaps more importantly my belief that an official JavaScript SDK is important for the future of Drupal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Drupal State also has a healthy &lt;a href=&quot;https://www.drupal.org/project/issues/drupal_state?categories=All&quot;&gt;issue queue&lt;/a&gt;, so if you&apos;re interested in contributing or learning more about the project, I&apos;ll be around during the contribution events.&lt;/p&gt;
&lt;h2&gt;Generic Drupal Web Components&lt;/h2&gt;
&lt;p&gt;This project has had a long and winding road. Initially created in support of the Decoupled Menus hackathon at last year&apos;s DrupalCon, &lt;a href=&quot;https://www.drupal.org/project/gdwc&quot;&gt;Generic Drupal Web Components (GDWC)&lt;/a&gt; aims to create a set of web components that can work well with data sourced from Drupal. But as you&apos;ll hear in my talk, finding a better solution to source data from Drupal resulted in the considerable tangent that was the creation of the Drupal State project.&lt;/p&gt;
&lt;p&gt;There is &lt;a href=&quot;https://www.drupal.org/project/gdwc/issues/3276122&quot;&gt;an in-progress issue for GDWC&lt;/a&gt; that introduces two new components that can be used to make Drupal data available to any web components. I think this has the potential to open the floodgates for future components. I&apos;d love to hear your thoughts.&lt;/p&gt;
&lt;h2&gt;Pantheon&lt;/h2&gt;
&lt;p&gt;That&apos;s a lot of stuff! I&apos;ll also be around the Pantheon booth and would be happy to talk about these projects, or anything relevant to decoupled Drupal on &lt;a href=&quot;https://pantheon.io/&quot;&gt;Pantheon&lt;/a&gt;. I&apos;ll also be at &lt;a href=&quot;https://www.eventbrite.com/e/the-unofficial-official-drupalcon22-party-tickets-293497698517&quot;&gt;the Pantheon DrupalCon Party&lt;/a&gt; and almost certainly leaving early to rest up for my session the next morning :)&lt;/p&gt;
</description><pubDate>Tue, 19 Apr 2022 13:21:43 GMT</pubDate></item><item><title>Star Wars: Galaxy&apos;s Edge</title><link>https://brianperry.dev/jams/2022/star-wars-galaxys-edge/</link><guid isPermaLink="true">https://brianperry.dev/jams/2022/star-wars-galaxys-edge/</guid><description>&lt;p&gt;My &lt;em&gt;Star Wars&lt;/em&gt; fandom has waned a bit over the years, but on occasion my memories of watching &lt;em&gt;A New Hope&lt;/em&gt; over, and over come rushing back. For example, I cried (sobbed really) during &lt;em&gt;The Force Awakens&lt;/em&gt; and &lt;em&gt;The Last Jedi&lt;/em&gt;. My eyes stayed dry during &lt;em&gt;The Rise of Skywalker&lt;/em&gt; though. Draw your own conclusions there.&lt;/p&gt;
&lt;p&gt;Over the holiday break I found myself in Walt Disney World for a last chance at a trip that was twice rescheduled due to Covid. The crowds were insane, which was extremely concerning due to the explosion in cases from the Omicron variant. One of the few moments where I was able to be completely transported away from this Covid hellscape was the time I spent in &lt;em&gt;Galaxy&apos;s Edge&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;All of it was amazing, but the somewhat surprising highlight for me was &lt;em&gt;Millennium Falcon: Smuggler&apos;s Run&lt;/em&gt;. I had heard disappointing things about the ride, but it really hit me hard in the nostalgia zone. I was selected to be one of the pilots, so it was like Star Tours except I was in control. And look how faithful the interior of the ride is.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The hand sanitizer dispenser kind of gives it away though. Thanks Covid.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Rise of The Resistance&lt;/em&gt; was amazing as well, but still had nothing on piloting the Millennium Falcon. Even my &lt;em&gt;Star Tours&lt;/em&gt; trip was great this time around (no prequel trilogy nonsense.) Speaking of &lt;em&gt;Star Tours&lt;/em&gt;, I was even able to catch up with an old friend at the Cantina. I sure wish he was willing to wear a mask though.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</description><pubDate>Tue, 04 Jan 2022 13:48:59 GMT</pubDate></item><item><title>Deafheaven: Infinite Granite</title><link>https://brianperry.dev/jams/2021/deafheaven-infinite-granite/</link><guid isPermaLink="true">https://brianperry.dev/jams/2021/deafheaven-infinite-granite/</guid><description>&lt;p&gt;I&apos;ve tried getting into Deafheaven in the past, but was never really able to find my way into their harsh black metal sound. However, my interest was peaked when &lt;a href=&quot;https://open.spotify.com/album/0kCdT4gjYlSxIV7ll3Yd4M&quot;&gt;Infinite Granite&lt;/a&gt; promised &lt;a href=&quot;https://pitchfork.com/thepitch/how-deafheaven-made-their-least-metal-album-yet/&quot;&gt;a mellower version of their sound&lt;/a&gt; with mostly clean singing.&lt;/p&gt;
&lt;p&gt;Yes, it is substantially more melodic than albums like &lt;a href=&quot;https://open.spotify.com/album/2kKXGWaCEl06EKZ4DxBJIT&quot;&gt;Sunbather&lt;/a&gt; and features mostly singing as opposed to growls and screams, but harsh moments still do occasionally make their way through. And because of the lush, swirling guitars that come before, when things get heavy they hit so much harder.&lt;/p&gt;
&lt;p&gt;But more than anything, this is an album I can just get lost in. It will also forever be associated with a time and place as it came out the weekend before I traveled home to the east coast to mourn a friend&apos;s passing.&lt;/p&gt;
&lt;p&gt;Likely to be my favorite album of the year, and one that I expect to revisit for years to come.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/fXTBQQU191Y&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
</description><pubDate>Mon, 11 Oct 2021 00:23:57 GMT</pubDate></item><item><title>How Drupal&apos;s Preview Works</title><link>https://brianperry.dev/til/2021/how-drupals-preview-works/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/how-drupals-preview-works/</guid><description>&lt;p&gt;I&apos;ve been thinking quite a bit recently about Drupal&apos;s options for decoupled preview with other JavaScript front ends. As part of some related experimentation, I found myself needing to understand more about how Drupal&apos;s standard preview functionality works. To be specific here - I&apos;m talking about when you&apos;re editing a node and click on the preview button to see a full rendering of the page you&apos;re currently editing.&lt;/p&gt;
&lt;p&gt;I realized I had never really had any reason to think about how that actually happens. Like many things on the web, it just kind of magically does. My general assumption was that it was some variation of Drupal&apos;s revision functionality. Something along the lines of an unpublished version of the node is created to represent the preview, and then is deleted after some period of inactivity.&lt;/p&gt;
&lt;p&gt;As I dug into the code a bit I found out that my assumption was pretty far off.&lt;/p&gt;
&lt;p&gt;Surprise number one was that the data for this is stored in Drupal&apos;s tempstore. Specifically &lt;a href=&quot;cms/til/how-drupals-preview-works&quot;&gt;Drupal&apos;s private tempstore&lt;/a&gt;. The tempstore is a key/value collection that can make data available across requests and the private tempstore also includes checks to ensure that the data is only available to the current user. Here&apos;s a short post with &lt;a href=&quot;https://alexrayu.com/snippets/drupal-8-tempstore&quot;&gt;a good comparison of the private tempstore and the shared tempstore&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Surprise number two was that the form state of the current node edit form is what is stored in the tempstore, not the node entity itself. As we&apos;ll see in a second, it is possible to derive the node entity from the form state, but technically only the form ends up in the tempstore.&lt;/p&gt;
&lt;p&gt;Let&apos;s take a peek at some of the relevant code.&lt;/p&gt;
&lt;p&gt;When the node edit form is submitted, the following form submit handler is used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// From core/modules/node/src/NodeForm.php

/**
 * Form submission handler for the &apos;preview&apos; action.
 *
 * @param $form
 *   An associative array containing the structure of the form.
 * @param $form_state
 *   The current state of the form.
 */
public function preview(array $form, FormStateInterface $form_state) {
  $store = $this-&amp;gt;tempStoreFactory-&amp;gt;get(&apos;node_preview&apos;);
  $this-&amp;gt;entity-&amp;gt;in_preview = TRUE;
  $store-&amp;gt;set($this-&amp;gt;entity-&amp;gt;uuid(), $form_state);

  $route_parameters = [
    &apos;node_preview&apos; =&amp;gt; $this-&amp;gt;entity-&amp;gt;uuid(),
    &apos;view_mode_id&apos; =&amp;gt; &apos;full&apos;,
  ];

  $options = [];
  $query = $this-&amp;gt;getRequest()-&amp;gt;query;
  if ($query-&amp;gt;has(&apos;destination&apos;)) {
    $options[&apos;query&apos;][&apos;destination&apos;] = $query-&amp;gt;get(&apos;destination&apos;);
    $query-&amp;gt;remove(&apos;destination&apos;);
  }
  $form_state-&amp;gt;setRedirect(&apos;entity.node.preview&apos;, $route_parameters, $options);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here the form is being set in the private tempstore, and the user is redirected to the preview route.&lt;/p&gt;
&lt;p&gt;Before the preview route is rendered, the node preview service will be invoked. Within this service the &lt;code&gt;convert&lt;/code&gt; method gets the form state from the tempstore and then derives the node entity form that.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// core/modules/node/src/ParamConverter/NodePreviewConverter.php

public function convert($value, $definition, $name, array $defaults) {
  $store = $this-&amp;gt;tempStoreFactory-&amp;gt;get(&apos;node_preview&apos;);
  if ($form_state = $store-&amp;gt;get($value)) {
    return $form_state-&amp;gt;getFormObject()-&amp;gt;getEntity();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, within the node preview controller, the &lt;code&gt;view&lt;/code&gt; method assembles a build of the node.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// core/modules/node/src/Controller/NodePreviewController.php

public function view(EntityInterface $node_preview, $view_mode_id = &apos;full&apos;, $langcode = NULL) {
  $node_preview-&amp;gt;preview_view_mode = $view_mode_id;
  $build = parent::view($node_preview, $view_mode_id);

  $build[&apos;#attached&apos;][&apos;library&apos;][] = &apos;node/drupal.node.preview&apos;;

  // Don&apos;t render cache previews.
  unset($build[&apos;#cache&apos;]);

  return $build;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pretty much everyone I ran this by was surprised*, but now that I&apos;ve worked through it a bit this approach makes more sense to me. Many of these previews will be abandoned, so the tempstore actually is an appropriate home for them due to its built in expiration and cleanup. Same for the private tempstore - previews are specific to the current user, and could contain sensitive data. Finally, I&apos;d imagine that not processing this as a full entity/revision saves some overhead considering that this preview could be updated frequently while editing.&lt;/p&gt;
&lt;p&gt;I now understand the &apos;how&apos; and possibly the &apos;why&apos;, but that doesn&apos;t make it any easier to access this data for other preview related purposes. I think that is the reason why this specific use case is covered by few, if any, decoupled Drupal preview solutions. From my initial experimentation it can be solved, but it will take quite a bit of work to do right.&lt;/p&gt;
&lt;p&gt;I&apos;d love to hear more from those managing decoupled Drupal sites. Is previewing in your decoupled front end while editing in Drupal really the holy grail it seems like sometimes? Or are other preview workflows meeting your needs?&lt;/p&gt;
&lt;p&gt;* &lt;em&gt;I&apos;m sure there are some Drupal lifers out there who were well aware of this, and are yelling at their screens right now.&lt;/em&gt;&lt;/p&gt;
</description><pubDate>Fri, 08 Oct 2021 01:28:50 GMT</pubDate></item><item><title>Identifying Circular Dependencies in JavaScript Modules</title><link>https://brianperry.dev/til/2021/identifying-circular-dependencies-in-javascript/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/identifying-circular-dependencies-in-javascript/</guid><description>&lt;p&gt;While recently scouring Stack Overflow to debug a vague error in a JavaScript library I was working on, one of the possible causes I kept running into was a circular dependency introduced in module imports. Seemed plausible that I could have made that mistake, but given that I may have been foolish enough to introduce a circular dependency in the first place, tracking it down myself seemed like a tall order.&lt;/p&gt;
&lt;p&gt;Thankfully there are a handful of NPM packages that can do all the work for you. I first tried &lt;a href=&quot;https://www.npmjs.com/package/madge&quot;&gt;Madge&lt;/a&gt; which seemed to be the most popular solution. It did have an option to identify circular dependencies, but that information wasn&apos;t included in the default output. It also has an impressive dependency visualization option, but it required manually installing a homebrew package.&lt;/p&gt;
&lt;p&gt;I next tried &lt;a href=&quot;https://www.npmjs.com/package/dpdm&quot;&gt;dpdm&lt;/a&gt; which was simpler to use, and covered more of my needs by default. Here&apos;s what the output looked like when run against the main entry point of my library:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;There was in fact a circular dependency between my TypeScript types and interfaces. Real rookie mistake right there. Combining the interfaces and types into a single file eliminated the circular dependency and got dpdm to stop yelling at me.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Ironically after all that, the circular dependency wasn&apos;t even the cause of the error I was seeing. I&apos;m glad I found this handy little utility though, and hopefully working through this will make me less likely to introduce a circular dependency in the future.&lt;/p&gt;
</description><pubDate>Sun, 19 Sep 2021 12:55:31 GMT</pubDate></item><item><title>GitHub Copilot is My Copilot</title><link>https://brianperry.dev/posts/2021/github-copilot-is-my-copilot/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/github-copilot-is-my-copilot/</guid><description>&lt;p&gt;I can’t resist blog post titles that would make a really stupid bumper sticker.&lt;/p&gt;
&lt;p&gt;If you haven’t heard about &lt;a href=&quot;https://copilot.github.com/&quot;&gt;GitHub Copilot&lt;/a&gt;, it is a plugin for Visual Studio Code that uses AI to autosuggest code as you type. It sounds too good to be true, so of course I signed up for the technical preview to find out for myself.&lt;/p&gt;
&lt;p&gt;When I was eventually granted access, I turned it on without any hesitation. After a few minutes that were a mix of amazement and confusion, I turned it off.&lt;/p&gt;
&lt;p&gt;Why? Honestly, I mostly found the suggestions it was making to my code disorienting. I&apos;m obviously familiar with my typical thought process, but Copilot was trying to lead me in different directions. The frequency of suggestions was also distracting. The site describes Copilot as &apos;your AI pair programmer.&apos; I&apos;m not as comfortable with pair programming as I should be, but imagine if you were pair programming but your partner was suggesting something every time you typed.&lt;/p&gt;
&lt;p&gt;The final issue that motivated me to turn it off was accuracy. I found that the things it was suggesting were typically not what I wanted. Following the examples shown on the site, I also tried providing guidance to Copilot via comments. I found that those comments had to be pretty elaborate, and even then it was mainly resulting in large blocks of code that weren&apos;t what I wanted.&lt;/p&gt;
&lt;p&gt;Since it was mainly distracting me and breaking my flow, Copilot had to go.&lt;/p&gt;
&lt;p&gt;A funny thing happened after I turned it off though; I kept thinking about my new friend Copilot.&lt;/p&gt;
&lt;p&gt;I was left thinking about the need to communicate with it via function signatures and clear comments. AI or no, that seemed like a positive habit that I should do more of. And while Copilot&apos;s suggestions were overzealous, I did find myself missing the cases where the situations where it did accurately read my mind.&lt;/p&gt;
&lt;p&gt;Since I kept thinking about it, I eventually gave up and turned copilot back on. Going back into it with my expectations tempered made a big difference. Rather than thinking of it as The Genie from Aladdin (either Robin Williams or Will Smith depending on your personal preference), seeing it as a slightly more intelligent autocomplete helped. I’ve learned to ignore when it is wrong, which is still often. But as it trains it is getting better. Copilot is now pretty good at suggesting complete lines when things are simple, and saving myself the typing still kind of feels like magic.&lt;/p&gt;
&lt;p&gt;I&apos;ve also found Copilot helpful in a few cases I didn&apos;t anticipate. When I&apos;m working in an area I&apos;m somewhat new to (Typescript and Jest these days,) Copilot can often give me enough of a nudge to keep me from getting stuck. And most interestingly I&apos;ve found that it can help me push through adding documentation when I&apos;m not motivated to do so. Your first draft is always garbage, so why not let a robot take care of some of that for you. And having something on the screen that isn&apos;t in your voice is a great motivator to revise it. Much better than staring at a blank README.&lt;/p&gt;
&lt;p&gt;Positives aside, there are also legitimate ethical concerns with open source code being used to power Copilot&apos;s AI. Open source code helping shape proprietary code seems problematic. And if you take things to their logical conclusion you could think of your code being consumed by Copilot, but then used in a software project that you find objectionable. Providing some way for repositories to opt out would help, but it seems like a difficult problem to solve.&lt;/p&gt;
&lt;p&gt;I&apos;ve already stuck with Copilot longer than I expected, but it is still not out of the question that I could just nope back out of it someday. Regardless, it has been a fun little experiment and has made me think about the code I write in some new ways.&lt;/p&gt;
</description><pubDate>Sat, 28 Aug 2021 02:17:25 GMT</pubDate></item><item><title>Using Apollo Client Without React</title><link>https://brianperry.dev/til/2021/using-apollo-without-react/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/using-apollo-without-react/</guid><description>&lt;p&gt;I&apos;ve been experimenting with &lt;a href=&quot;https://www.apollographql.com/docs/react/&quot;&gt;Apollo Client&lt;/a&gt; recently and started work on a new package that will use it as a dependency. After some initial experiments in a Create React App project, I scaffolded a new vanilla JS project using &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt;. Much to my surprise, when I added Apollo my build started failing because React was missing as a dependency. It took me a little longer than I expected to figure out why this was happening.&lt;/p&gt;
&lt;p&gt;As of Apollo 3.0 the import example in the getting started docs will also bring some React hooks along for the ride:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ApolloClient, gql, InMemoryCache } from &quot;@apollo/client&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use Apollo in a vanilla JS project, you instead need to use the entry point &lt;code&gt;@apollo/client/core&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ApolloClient, gql, InMemoryCache } from &quot;@apollo/client/core&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is documented, but was a little hard to find &lt;a href=&quot;https://www.apollographql.com/docs/react/migrating/apollo-client-3-migration/#using-apollo-client-without-react&quot;&gt;within the Apollo 3.0 migration guide&lt;/a&gt;. Hopefully giving this a little more Google juice might save someone a few minutes in the future.&lt;/p&gt;
</description><pubDate>Wed, 04 Aug 2021 02:24:32 GMT</pubDate></item><item><title>Working Around React&apos;s Web Component Limitations</title><link>https://brianperry.dev/posts/2021/working-around-reacts-web-component-limitations/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/working-around-reacts-web-component-limitations/</guid><description>&lt;p&gt;One of the most exciting features of web components is the fact that they can be used in any JavaScript framework. If you scan through the list at &lt;a href=&quot;https://custom-elements-everywhere.com/&quot;&gt;Custom Elements Everywhere&lt;/a&gt; you&apos;ll see that coverage is really solid across the board... until you get to React.&lt;/p&gt;
&lt;p&gt;As &lt;a href=&quot;https://custom-elements-everywhere.com/libraries/react/results/results.html&quot;&gt;the test results&lt;/a&gt; show, the limitations fall into two main categories. The first is that React can&apos;t automatically pass non-string data like arrays and objects into custom element properties.&lt;/p&gt;
&lt;p&gt;Take for example this election results tracker component: &lt;code&gt;&amp;lt;results-tracker&amp;gt;&lt;/code&gt;. All of the properties of the component are strings, except for candidates, which expects an array of objects like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const candidates = [
  {
    name: &quot;Joseph R. Biden Jr.&quot;,
    primary: 253,
    secondary: 73879622,
    color: &quot;#1375b7&quot;
  },
  {
    name: &quot;Donald J. Trump&quot;,
    primary: 214,
    secondary: 69772905,
    color: &quot;#c93135&quot;
  }
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You would assume that you could pass in the &lt;code&gt;candidates&lt;/code&gt; array just as you could in a React component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;results-tracker
  headline=&quot;{headline}&quot;
  race=&quot;{race}&quot;
  total=&quot;{total}&quot;
  candidates=&quot;{candidates}&quot;
&amp;gt;
  &amp;lt;p&amp;gt;{subheadline}&amp;lt;/p&amp;gt;
&amp;lt;/results-tracker&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead, you&apos;ll be met with an error. You can work around it by converting the &lt;code&gt;candidates&lt;/code&gt; array to a JSON string, but it feels a little dirty.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;results-tracker
  headline=&quot;{headline}&quot;
  race=&quot;{race}&quot;
  total=&quot;{total}&quot;
  candidates=&quot;{JSON.stringify(candidates)}&quot;
&amp;gt;
  &amp;lt;p&amp;gt;{subheadline}&amp;lt;/p&amp;gt;
&amp;lt;/results-tracker&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second and larger category of limitations are related to events. React does not automatically listen to events dispatched by a custom element.&lt;/p&gt;
&lt;p&gt;Take for example the &lt;a href=&quot;https://genericcomponents.netlify.app/generic-switch/demo/index.html&quot;&gt;generic-switch component&lt;/a&gt; from &lt;a href=&quot;https://github.com/thepassle/generic-components&quot;&gt;@generic-components/components&lt;/a&gt;. &lt;code&gt;&amp;lt;generic-switch&amp;gt;&lt;/code&gt; emits a &lt;code&gt;checked-changed&lt;/code&gt; event when toggling the switch on or off. You may want to update some application state as a result of this event, but by default React won&apos;t be aware of it.&lt;/p&gt;
&lt;p&gt;To handle this event, you could create a wrapping component that uses a ref to listen to the &lt;code&gt;checked-changed&lt;/code&gt; event. Within the app, you&apos;d use your wrapper component instead of the custom element itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;SwitchProvider checked handleToggle=&quot;{togglePolitics}&quot;&amp;gt;
  Politics:
&amp;lt;/SwitchProvider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The specifics don&apos;t really matter for this example, but the &lt;code&gt;togglePolitics()&lt;/code&gt; function updates the necessary application state when the switch is toggled.&lt;/p&gt;
&lt;p&gt;The wrapping component itself would look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useEffect, useRef } from &quot;react&quot;;

import &quot;@thepassle/generic-components/generic-switch.js&quot;;

const SwitchProvider = ({ checked, handleToggle, children }) =&amp;gt; {
  const switchRef = useRef();

  useEffect(() =&amp;gt; {
    switchRef.current.addEventListener(&quot;checked-changed&quot;, (e) =&amp;gt; {
      handleToggle(switchRef.current.__checked);
    });
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
  // Can&apos;t add handleToggle as a dependency above because it would result in
  // this hook being called multiple times

  return (
    &amp;lt;generic-switch checked={checked} ref={switchRef}&amp;gt;
      {children}
    &amp;lt;/generic-switch&amp;gt;
  );
};

export default SwitchProvider;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gets the job done, but it is quite a bit of additional boilerplate. Almost enough to make you consider just using a React based switch component instead.&lt;/p&gt;
&lt;p&gt;With &lt;a href=&quot;https://lit.dev/blog/2021-04-21-lit-2.0-meet-lit-all-over-again/&quot;&gt;the release of Lit 2.0&lt;/a&gt; the Lit team has created a number of supporting Lit Labs packages, including &lt;a href=&quot;https://www.npmjs.com/package/@lit-labs/react&quot;&gt;@lit-labs/react&lt;/a&gt;. @lit-labs/react provides improved React integration for Web Components, and with the &lt;code&gt;createComponent&lt;/code&gt; utility it just so happens to address both of the issues outlined earlier in this post.&lt;/p&gt;
&lt;p&gt;Going back to our &lt;code&gt;&amp;lt;results-tracker&amp;gt;&lt;/code&gt; component, you&apos;ll recall that in order to pass an array of objects into the candidates property we had to first convert the variable into a string using &lt;code&gt;JSON.stringify()&lt;/code&gt;. Using @lit-labs/react we could create a wrapping component for &lt;code&gt;&amp;lt;results-tracker&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import { ResultsTracker } from &quot;@backlineint/results-tracker&quot;;
import { createComponent } from &quot;@lit-labs/react&quot;;

export const ResultsTrackerWrapper = createComponent(
  React,
  &quot;results-tracker&quot;,
  ResultsTracker
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we then use this wrapping component in our render method, we can pass in an array with no additional conversion necessary.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ResultsTrackerWrapper
  headline=&quot;{headline}&quot;
  race=&quot;{race}&quot;
  total=&quot;{total}&quot;
  candidates=&quot;{candidates}&quot;
&amp;gt;
  &amp;lt;p&amp;gt;{subheadline}&amp;lt;/p&amp;gt;
&amp;lt;/ResultsTrackerWrapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works because &lt;code&gt;createComponent&lt;/code&gt; actually discovers the available properties of the related web component, and then sets any props passed into the component as properties (which can be non-string values) of the web component rather than attributes (which must be strings). Using a wrapping component for this simple example feels a little heavy, but for more complicated components this could save quite a bit of effort.&lt;/p&gt;
&lt;p&gt;Looking back at events emitted by &lt;code&gt;&amp;lt;generic-switch&amp;gt;&lt;/code&gt;, &lt;code&gt;createComponent&lt;/code&gt; can save us even more boilerplate. When using &lt;code&gt;createComponent&lt;/code&gt; to create a wrapping component we can also provide a mapping of React prop names to events fired by the custom element.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &quot;react&quot;;
import { createComponent } from &quot;@lit-labs/react&quot;;
import GenericSwitch from &quot;@thepassle/generic-components/generic-switch.js&quot;;

export const GenericSwitchComponent = createComponent(
  React,
  &quot;generic-switch&quot;,
  GenericSwitch,
  {
    onCheckedChanged: &quot;checked-changed&quot;,
  }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now when &lt;code&gt;&amp;lt;generic-switch&amp;gt;&lt;/code&gt; emits a &lt;code&gt;checked-changed&lt;/code&gt; event, the &lt;code&gt;onCheckedChanged&lt;/code&gt; event prop we specified will fire. If we pass our &lt;code&gt;togglePolitics&lt;/code&gt; function that updates the application state, we&apos;ve wired up everything we need to make React respond to the event within our web component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;GenericSwitchComponent
  checked=&quot;{checked&quot;
  ?
  true
  :
  undefined}
  onCheckedChanged=&quot;{togglePolitics}&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;re still using a wrapping React component here, but this approach feels much leaner.&lt;/p&gt;
&lt;p&gt;@lit-labs/react also has an additional &lt;code&gt;useControler&lt;/code&gt; hook as well which provides support for the &lt;a href=&quot;https://lit.dev/docs/composition/controllers/&quot;&gt;reactive controller concept&lt;/a&gt; introduced in Lit 2. I don&apos;t have experience with reactive controllers as of yet, so I haven&apos;t made use of that hook. But it is nice to know it is there if I do take advantage of that feature in the future.&lt;/p&gt;
&lt;p&gt;In a perfect world, I&apos;d love to see React resolve these issues with Web Components within the framework themselves. My hope is that growing web component adoption will force their hand. But in the meantime, the Lit team taking matters into their own hands seems like the next best thing.&lt;/p&gt;
&lt;p&gt;If you&apos;d like to see the full example in action, I&apos;ve created a couple of code sandboxes.&lt;/p&gt;
&lt;p&gt;The first uses React only:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://codesandbox.io/embed/web-components-with-react-tvnwd?fontsize=14&amp;amp;hidenavigation=1&amp;amp;theme=dark&amp;amp;view=editor&quot;
style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
title=&quot;Web Components With React&quot;
allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And the second refactors to use @lit-labs/react:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://codesandbox.io/embed/lit-react-358ez?fontsize=14&amp;amp;hidenavigation=1&amp;amp;theme=dark&amp;amp;view=editor&quot;
style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
title=&quot;Lit React&quot;
allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description><pubDate>Sun, 01 Aug 2021 01:55:21 GMT</pubDate></item><item><title>Framework Detection with @netlify/framework-info</title><link>https://brianperry.dev/til/2021/framework-detection-with-netlify-framework-info/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/framework-detection-with-netlify-framework-info/</guid><description>&lt;p&gt;I recently needed a way to detect which framework was being used to build a site. Immediately I wondered, &quot;how does Netlify do this?&quot; When setting up a new site, Netlify is able to determine things like your build command and publish directory. I didn&apos;t have to search too long for an answer because it turns out Netlify has open sourced &lt;a href=&quot;https://github.com/netlify/framework-info&quot;&gt;the utility they use for framework detection&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s a simple example that outputs information about the detected framework, and also tests for the existence of a specific framework.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { listFrameworks, hasFramework } = require(&quot;@netlify/framework-info&quot;);

async function detectFramework() {
  // listFrameworks() returns proimise containing tons of framework data
  const frameworkData = await listFrameworks();
  console.log(&quot;Framework Object: &quot;, frameworkData);
  console.log(&quot;Detected Dev Commands: &quot;, frameworkData[0].dev);
  console.log(&quot;Detected Build Commands: &quot;, frameworkData[0].build);

  // hasFramework() can test for a specific framework
  const isVue = await hasFramework(&quot;vue&quot;);
  const isNext = await hasFramework(&quot;next&quot;);
  console.log(&quot;Is this Vue? &quot;, isVue);
  console.log(&quot;Is this Next? &quot;, isNext);
}

detectFramework();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output of above when used in a NextJS project would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sandbox@sse-sandbox-0ngye:/sandbox$ node detect.js
Framework Object:  [
  {
    id: &apos;next&apos;,
    name: &apos;Next.js&apos;,
    category: &apos;static_site_generator&apos;,
    dev: { commands: [Array], port: 3000, pollingStrategies: [Array] },
    build: { commands: [Array], directory: &apos;out&apos; },
    staticAssetsDirectory: undefined,
    env: {},
    plugins: [ &apos;@netlify/plugin-nextjs&apos; ]
  }
]
Detected Dev Commands:  {
  commands: [ &apos;yarn dev&apos;, &apos;yarn start&apos;, &apos;yarn build&apos; ],
  port: 3000,
  pollingStrategies: [ { name: &apos;TCP&apos; } ]
}
Detected Build Commands:  { commands: [ &apos;next build&apos; ], directory: &apos;out&apos; }
Is this Vue?  false
Is this Next?  true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or alternatively, you can &lt;a href=&quot;https://codesandbox.io/s/framework-detection-0ngye?file=/detect.js&quot;&gt;try it out in this CodeSandbox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Detecting a framework is hardly an everyday activity for most (at least I hope it isn&apos;t,) but if you ever need to do it, this package is super handy. You can also run it via the CLI using the related &lt;a href=&quot;https://github.com/netlify/build-info&quot;&gt;build-info project&lt;/a&gt;.&lt;/p&gt;
</description><pubDate>Mon, 12 Jul 2021 07:33:52 GMT</pubDate></item><item><title>Documenting My Mac Setup</title><link>https://brianperry.dev/posts/2021/documenting-mac-setup/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/documenting-mac-setup/</guid><description>&lt;p&gt;Between loaners, replacements, and other surprises, over the past year or so I&apos;ve set up new Mac laptops more than I ever have before. I tend to start fresh rather than migrate in order to get that nice clean digital slate. But what I don&apos;t do, is document the process all that well. You&apos;re in luck future self, because 4th time is the charm.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Make a cup of coffee.&lt;/li&gt;
&lt;li&gt;Make sure you have your 2FA apps handy&lt;/li&gt;
&lt;li&gt;For anything that isn&apos;t in version control or in the cloud (hopefully that list gets smaller every time you do this), copy over files using a USB drive.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;System Preferences&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;If I&apos;m using multiple monitors, configure monitor orientation under System Preferences -&amp;gt; Displays.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;System Preferences -&amp;gt; Trackpad:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uncheck &apos;Look up &amp;amp; data detectors&apos;.&lt;/li&gt;
&lt;li&gt;Enable tap to click.&lt;/li&gt;
&lt;li&gt;Secondary click - click in bottom right corner.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;System Preferences -&amp;gt; Dock: Position on screen left.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finder -&amp;gt; Preferences -&amp;gt; Show Hard Disks on the Desktop&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sign in and enable iCloud&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;System Preferences -&amp;gt; Mission Control&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disable &apos;automatically rearrange spaces based on most recent use&apos;&lt;/li&gt;
&lt;li&gt;Disable &apos;displays have separate spaces&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Install Chrome (yeah, &lt;a href=&quot;https://chromeisbad.com/&quot;&gt;I know&lt;/a&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sync to Google Account if possible (has the added bonus of syncing all extensions.) Otherwise install:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/instapaper/ldjkgaaoikpmhmkelcgkgacicjfbofhh?hl=en&quot;&gt;Instapaper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/zoom-scheduler/kgjfgplpablkjnlkjmjdecgdpfankdle?hl=en-US&quot;&gt;Zoom Scheduler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Last Pass or &lt;a href=&quot;https://chrome.google.com/webstore/detail/1password-%E2%80%93-password-mana/aeblfdkhhhdcdjpifhhbdiojplfjncoa?hl=en&quot;&gt;1Password&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/wappalyzer/gppongmhjkpfnbhagpmjfkannfbllamg?hl=en&quot;&gt;Wappalyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/ghostery-%E2%80%93-privacy-ad-blo/mlomiejdfkolichcflejclcbmpeaniij?hl=en&quot;&gt;Ghostery&lt;/a&gt; (and turn off the stats overlay in settings.)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/xdebug-helper/eadndfjplgieldjbigjakmdgkmoaaaoc&quot;&gt;Xdebug Helper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/dreditor/dhdpoembhlojpmehepeadblhglloobao/related?hl=en-US&quot;&gt;Dreditor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi/related?hl=en&quot;&gt;React Dev Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/evernote-web-clipper/pioclpoplcdbaefihamjohnefbikjilc?hl=en&quot;&gt;Evernote Web Clipper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/momentum/laookkfknpbbblfpciffpaejjkokdgca/related?hl=en&quot;&gt;Momentum&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Log into &lt;a href=&quot;https://chrome.google.com/webstore/detail/lastpass-free-password-ma/hdokiejnpimakedhajhdlcegeplioahd?hl=en-US&quot;&gt;Last Pass extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Preferences -&amp;gt; Appearance - turn off &apos;Show warning before quitting with ⌘Q&apos; (I&apos;ve always been baffled about that being a default)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install Firefox and Edge for cross browser fun.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sign in to Mac App Store and download the following apps and utilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://evernote.com/&quot;&gt;Evernote&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Install &lt;a href=&quot;https://culturedcode.com/things/&quot;&gt;Things&lt;/a&gt; and set up Things Cloud.&lt;/li&gt;
&lt;li&gt;Set up all the &lt;a href=&quot;https://slack.com/&quot;&gt;Slacks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://unclutterapp.com/&quot;&gt;Unclutter&lt;/a&gt; (can&apos;t live without this one) Configure to store files in ~/Downloads&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://app.prntscr.com/en/index.html&quot;&gt;Lightshot Screenshot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pomodoro timer - currently using &lt;a href=&quot;https://flowapp.info/&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://apps.apple.com/us/app/be-focused-pro-focus-timer/id961632517?mt=12&quot;&gt;Be Focused Pro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://giphy.com/apps&quot;&gt;Giphy Capture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set up email. Outlook or Gmail for work depending on company preferences. Log into &lt;a href=&quot;https://hey.com/&quot;&gt;Hey&lt;/a&gt; (or whatever you replaced it with after your subscription lapses - some real nonsense going on at Basecamp).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download and configure &lt;a href=&quot;https://www.macbartender.com/&quot;&gt;Bartender&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download and activate &lt;a href=&quot;https://www.deckset.com/&quot;&gt;Deckset&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;a href=&quot;https://raycast.com/&quot;&gt;Raycast&lt;/a&gt; and &lt;a href=&quot;https://www.notion.so/Hotkey-56103210375b4fc78b63a7c5e7075fb7&quot;&gt;replace the spotlight hotkey&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tunes - Install Sonos and set up account in Apple Music App.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.postman.com/&quot;&gt;Postman&lt;/a&gt; - for API debugging&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Terminal and SSH&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Install developer tools by typing &lt;code&gt;git&lt;/code&gt; in the terminal.&lt;/li&gt;
&lt;li&gt;Follow &lt;a href=&quot;https://kbroman.org/github_tutorial/pages/first_time.html&quot;&gt;First Time Github Guide&lt;/a&gt;, but &lt;a href=&quot;https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&quot;&gt;use a stronger key&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Change git editor to Nano: &lt;code&gt;git config --global core.editor &quot;nano&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&quot;&gt;Add key to ssh-agent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Add new key to Bitbucket, Github, Gitlab, Pantheon and Acquia accounts as needed (Acquia requires an RSA key. Hopefully they will support ed25519 in the future, but in the meantime using RSA will make your life easier.)&lt;/li&gt;
&lt;li&gt;Install &lt;a href=&quot;https://ohmyz.sh/#install&quot;&gt;Oh My Zsh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/powerline/fonts#installation&quot;&gt;Installed Powerline Fonts&lt;/a&gt; via git clone&lt;/li&gt;
&lt;li&gt;Change zsh theme by editing ~/.zshrc and change to agnoster&lt;/li&gt;
&lt;li&gt;Update ZSH permissions &lt;a href=&quot;https://github.com/ohmyzsh/ohmyzsh/issues/6835&quot;&gt;as outlined here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Terminal Preferences - Changed default profile to Pro and font to &apos;Source Code Pro for Powerline&apos; size 11.&lt;/li&gt;
&lt;li&gt;VS Code User Preferences - Changed Terminal › Integrated: Font Family to &apos;Source Code Pro for Powerline&apos;&lt;/li&gt;
&lt;li&gt;Install &lt;a href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IDE(s)&lt;/h2&gt;
&lt;p&gt;Download and install &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;Visual Studio Code&lt;/a&gt; and enable the following extensions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one&quot;&gt;Markdown all in one&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced&quot;&gt;Markdown Preview Enhanced&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker&quot;&gt;Code Spell Checker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=DmitryDorofeev.empty-indent&quot;&gt;Empty Indent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow&quot;&gt;Indent Rainbow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint&quot;&gt;Eslint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens&quot;&gt;Gitlens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ecmel.vscode-html-css&quot;&gt;HTML CSS Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug&quot;&gt;PHP Debug&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=neilbrayfield.php-docblocker&quot;&gt;PHP Doc Blocker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/search?term=php%20doc%20blocker&amp;amp;target=VSCode&amp;amp;category=All%20categories&amp;amp;sortBy=Relevance&quot;&gt;PHP Intelephense&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=persoderlind.vscode-phpcbf&quot;&gt;phpcbf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ikappas.phpcs&quot;&gt;phpcs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode&quot;&gt;Prettier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=whatwedo.twig&quot;&gt;Twig&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome&quot;&gt;Debugger for Chrome&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plus many more that will be triggered by project configuration.&lt;/p&gt;
&lt;p&gt;I also add the following settings in the user version of settings.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;editor.tabSize&quot;: 2,
    &quot;editor.detectIndentation&quot;: false,
    &quot;editor.fontFamily&quot;: &quot;&apos;Source Code Pro for Powerline&apos;, Menlo, Monaco, &apos;Courier New&apos;, monospace&quot;,
    &quot;explorer.confirmDelete&quot;: false,
    &quot;window.zoomLevel&quot;: 1,
    &quot;[javascript]&quot;: {
      &quot;editor.defaultFormatter&quot;: &quot;esbenp.prettier-vscode&quot;
    },
    &quot;files.associations&quot;: {
      &quot;*.theme&quot;: &quot;php&quot;
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Local Development Environment&lt;/h2&gt;
&lt;h3&gt;NodeJS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://formulae.brew.sh/formula/nvm#default&quot;&gt;Install NVM via Homebrew&lt;/a&gt; (following manual steps outlined at end of install process)&lt;/li&gt;
&lt;li&gt;Install latest node LTS via homebrew &lt;code&gt;nvm install --lts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;PHP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Upgrade PHP Version &lt;code&gt;brew install php@7.4&lt;/code&gt;. Follow manual installation steps to add this new php version to your path.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getcomposer.org/download/&quot;&gt;Download composer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Move composer into path &lt;code&gt;mv composer.phar /usr/local/bin/composer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Increase composer memory limit by running &lt;code&gt;echo &apos;export COMPOSER_MEMORY_LIMIT=-1&apos; &amp;gt;&amp;gt; ~/.zshrc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Hoping to stick with composer 2, but if I need to downgrade to Composer 1 &lt;code&gt;composer self-update --1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Docker-y Things&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;a href=&quot;https://www.docker.com/products/docker-desktop&quot;&gt;Docker Desktop&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Docker Desktop preferences, disable starting Docker upon login.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;a href=&quot;https://github.com/lando/lando/releases&quot;&gt;Lando&lt;/a&gt; (assuming that the version packaged with Lando is reasonable, you could install Docker Desktop as part of installing Lando)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;a href=&quot;https://pantheon.io/product/localdev&quot;&gt;Pantheon LocalDev&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Dock&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Rip a bunch of nonsense out of the dock.&lt;/li&gt;
&lt;li&gt;Add: Outlook, Things, Chrome, VS Code, Slack, Terminal, Evernote, Sonos&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Things to install as needed:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://rogueamoeba.com/audiohijack/&quot;&gt;Audio Hijack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Dropbox (hoping to find my way out of Dropbox...)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://encrypt.me/&quot;&gt;EncryptMe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;FileZilla&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getkap.co/&quot;&gt;Kap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mmhmm.app/&quot;&gt;Mmhmm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;XCode&lt;/li&gt;
&lt;li&gt;Sonos&lt;/li&gt;
&lt;li&gt;Descript&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sketch.com/&quot;&gt;Sketch&lt;/a&gt; - because that is what you see most designs in these days.&lt;/li&gt;
&lt;li&gt;Creative Cloud / Photoshop&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jetbrains.com/phpstorm/&quot;&gt;PHP Storm&lt;/a&gt; because their visual merge conflict resolution tool is still way better than VS Code.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://atom.io/&quot;&gt;Atom&lt;/a&gt; for a lightweight editor when you need one.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Vagrant&lt;/h3&gt;
&lt;p&gt;Fingers crossed that my Vagrant days are over, but if needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Download and install &lt;a href=&quot;https://www.virtualbox.org/wiki/Downloads&quot;&gt;VirtualBox&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download and install &lt;a href=&quot;https://www.vagrantup.com/&quot;&gt;Vagrant&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install the following Vagrant plugins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vagrant plugin install vagrant-auto_network&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vagrant plugin install vagrant-hostsupdater&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vagrant plugin install vagrant-vbguest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://formulae.brew.sh/formula/ansible#default&quot;&gt;Install Ansible via Homebrew&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You did it. Take a nap.&lt;/p&gt;
</description><pubDate>Thu, 01 Jul 2021 13:13:53 GMT</pubDate></item><item><title>New Beaches, New Basements, and New Jobs</title><link>https://brianperry.dev/posts/2021/new-beaches-basements-and-jobs/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/new-beaches-basements-and-jobs/</guid><description>&lt;p&gt;It has been an eventful couple of weeks.&lt;/p&gt;
&lt;p&gt;June 17th was my last day at &lt;a href=&quot;https://www.bounteous.com/&quot;&gt;Bounteous&lt;/a&gt;. I enjoyed my nearly five years there immensely, and am proud of the work I was able to within their amazing Drupal practice. I also had the unique opportunity to contribute to the company&apos;s re-branding efforts during a huge period of growth. But most of all I am extremely grateful for all the support I was given to contribute back to the Drupal community, both at events and online in the issue queue.&lt;/p&gt;
&lt;p&gt;I took a little time off in between and was able to squeeze in a family trip to South Haven Michigan. It was our first time there, but we were able to find a nice AirBnb with private beach access. Not quite our typical Rhode Island trip (we were gun shy about air travel with a our son who isn&apos;t yet old enough to be vaccinated,) but it did he trick.&lt;/p&gt;
&lt;p&gt;And then this shit happened:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The day after we got back from South Haven I was stuck in line at the DMV in the pouring rain, and came home to find some standing water coming up through the unfinished portion of our basement, along with wet spots coming up through the carpet in the finished portion of the basement. The next day we got more rain, and the water got worse. Since we were home, we were able to get our possessions out, but now the main floor of our house is cluttered beyond belief. We&apos;ll know more in the next day or so, but it looks like some pretty serious issues and possibly a problem with the foundation at ground level. Yikes!&lt;/p&gt;
&lt;p&gt;Basement be damned, I started my new role at &lt;a href=&quot;https://pantheon.io/&quot;&gt;Pantheon&lt;/a&gt; on Monday. It was a very welcome distraction after a long, wet weekend. I&apos;ll be working within the Professional Services team as a Senior Technology Consultant focused on Decoupled Architectures. I&apos;m really excited about what Pantheon is working on to support Decoupled sites, and happy to have the opportunity to be a part of it.&lt;/p&gt;
&lt;p&gt;Between the new job and what will likely be a torn up basement, I&apos;m probably still in for a few wild weeks. But I&apos;d eventually be fine with things being boring for a bit at some point.&lt;/p&gt;
</description><pubDate>Wed, 30 Jun 2021 12:32:05 GMT</pubDate></item><item><title>Running GitHub Codespaces on an  iPad</title><link>https://brianperry.dev/til/2021/github-codespaces-on-ipad/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/github-codespaces-on-ipad/</guid><description>&lt;p&gt;I&apos;ve been experimenting on and off with &lt;a href=&quot;https://github.com/features/codespaces&quot;&gt;GitHub Codespaces&lt;/a&gt; since gaining access to the beta. My long term goal is to set up an environment for Drupal contribution, similar to Ofer Shaal&apos;s great &lt;a href=&quot;https://github.com/shaal/ddev-gitpod&quot;&gt;ddev Gitpod&lt;/a&gt; project. In the meantime, I&apos;ve used Codespaces for a slightly more frivolous pursuit.&lt;/p&gt;
&lt;p&gt;I occasionally find myself using my iPad and wanting to do a small amount of coding, or a relatively minor update to this site. Codespaces finally makes this practical. You can update and commit code, run commands in a terminal, and even view your development site in Safari with live reload enabled. Paired with the &lt;a href=&quot;https://www.apple.com/shop/product/MXQT2LL/A/magic-keyboard-for-ipad-pro-11-inch-3rd-generation-and-ipad-air-4th-generation-us-english-black&quot;&gt;Magic Keyboard&lt;/a&gt; (speaking of frivolous) this setup can do a surprisingly good job approximating an honest-to-goodness local development setup.&lt;/p&gt;
&lt;p&gt;Here&apos;s an overview of how I configured Codespaces in the repository for this site:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In VSCode, download and enable the &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&quot;&gt;remote containers plugin&lt;/a&gt; for VSCode. (Alternatively you could &lt;a href=&quot;https://github.com/microsoft/vscode-dev-containers/tree/main/containers&quot;&gt;download the appropriate dev container&lt;/a&gt;, but the VSCode integration is really nice.)&lt;/li&gt;
&lt;li&gt;Hit F1 and search for &apos;Add Development Container Configuration Files...&apos;&lt;/li&gt;
&lt;li&gt;Select a container configuration definition. In my case it was Node.&lt;/li&gt;
&lt;li&gt;Select your node version.&lt;/li&gt;
&lt;li&gt;This will create a .devcontainer directory in your project that contains a dockerfile and a configuration file. Commit these files to version control and push to your GitHub repository.&lt;/li&gt;
&lt;li&gt;In the Github Web UI on your iPad you should now be able to select &apos;Open with Codespaces&apos; under the &apos;Code&apos; dropdown button. From there you can create a new Codespace, or later launch one of your existing Codespaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the future I&apos;d like to automate a few things like installing node packages when the container starts up. I&apos;m guessing that can just be added to the dockerfile.&lt;/p&gt;
&lt;p&gt;Now that the iPad is running the same M1 chip as the MacBook, I have a suspicion (hope? dream?) that the line between MacOS and iPadOS will start to blur in some ways that will make web development on an iPad more practical. But in the meantime it is nice to have this option in repos where I might want to make quick updates without dragging out my big boy laptop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; adding the development container that is most appropriate for your project has some advantages, but it turns out there is a much lower effort way to use a codespace on any repository. If you haven&apos;t added a .devcontainer folder to a repository, opening a codespace in the GitHub UI will use &lt;a href=&quot;https://aka.ms/ghcs-default-image&quot;&gt;the default development container&lt;/a&gt;. This container has Python, Node.js, JavaScript, TypeScript, C++, Java, C#, F#, .NET Core, PHP, PowerShell, Go, Ruby, and Rust (phew.) Might be overkill, but probably has the tools you need to get started without any advance configuration.&lt;/p&gt;
</description><pubDate>Sat, 19 Jun 2021 07:16:22 GMT</pubDate></item><item><title>Creating a CodeSandbox from a GitHub Repository</title><link>https://brianperry.dev/til/2021/creating-a-github-codesandbox/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/creating-a-github-codesandbox/</guid><description>&lt;p&gt;I recently had the need to post a live demo of a simple Create React App project that was posted on another user&apos;s GitHub repository. I typically turn to &lt;a href=&quot;https://codesandbox.io&quot;&gt;CodeSandbox&lt;/a&gt; for examples like this, but I didn&apos;t want to recreate the project from scratch since it already was good to go. I found that CodeSandbox has a ton of options to &lt;a href=&quot;https://codesandbox.io/docs/importing&quot;&gt;import a Sandbox&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The easiest one in this case by far was using GitHubBox.com. From &lt;a href=&quot;https://github.com/dferber90/githubbox&quot;&gt;their readme&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Go to a repo in GitHub&amp;lt;br&amp;gt;Replace github.com with githubbox.com&amp;lt;br&amp;gt;There&apos;s no step three&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So all I had to do was take this GitHub URL:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/aroseman/gdwc-react-demo/tree/main/&quot;&gt;https://github.com/aroseman/gdwc-react-demo/tree/main/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;and change it to:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://githubbox.com/aroseman/gdwc-react-demo/tree/main/&quot;&gt;https://githubbox.com/aroseman/gdwc-react-demo/tree/main/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Which magically gave me this sandbox:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://codesandbox.io/embed/github/aroseman/gdwc-react-demo/tree/main/?fontsize=14&amp;amp;hidenavigation=1&amp;amp;theme=dark&quot;
style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
title=&quot;react-component-demo&quot;
allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Super simple and super handy.&lt;/p&gt;
</description><pubDate>Sun, 30 May 2021 09:41:45 GMT</pubDate></item><item><title>Configuring Tugboat Live Previews For Drupal General Projects</title><link>https://brianperry.dev/til/2021/tugboat-previews-on-general-projects/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/tugboat-previews-on-general-projects/</guid><description>&lt;p&gt;&lt;a href=&quot;/posts/2021/were-in-the-golden-age-of-contributing-to-drupal/&quot;&gt;I&apos;ve written previously&lt;/a&gt; about my excitement that Tugboat now offers live previews for core and contrib merge requests on Drupal.org, so I was especially happy to see live previews added to the &lt;a href=&quot;https://www.drupal.org/project/gdwc&quot;&gt;Generic Drupal Web Components (GDWC)&lt;/a&gt; project recently.&lt;/p&gt;
&lt;p&gt;GDWC is a &lt;a href=&quot;https://www.drupal.org/project/project_general&quot;&gt;general project&lt;/a&gt; on Drupal.org and runs using NodeJS rather than PHP. Since Tugboat runs on Docker it seemed likely that we could also run Node, but &lt;a href=&quot;https://www.drupal.org/docs/develop/git/using-git-to-contribute-to-drupal/using-live-previews-on-drupal-core-and-contrib&quot;&gt;the existing documentation&lt;/a&gt; is unsurprisingly focused on Drupal PHP projects.&lt;/p&gt;
&lt;p&gt;With a little experimentation we found that Tugboat Live Previews could in fact be easily configured to run for Node based projects as well. Almost all of the credit here goes to &lt;a href=&quot;https://www.drupal.org/u/joegraduate&quot;&gt;Joe Parsons&lt;/a&gt; who took the lead on &lt;a href=&quot;https://www.drupal.org/project/gdwc/issues/3207881&quot;&gt;this issue&lt;/a&gt;. We ended up adding &lt;code&gt;.tugboat/config.yml&lt;/code&gt; to the project with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  apache:
    image: tugboatqa/httpd:2.4
    default: true
    commands:
      init:
        - curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
        - apt-get install -y nodejs
      build:
        - npm install
        - npm run build-storybook
        - ln -snf &quot;${TUGBOAT_ROOT}/storybook-static&quot; &quot;${DOCROOT}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see above, this first installs Node on Tugboat&apos;s httpd image. We can then install our dependencies and run a static build of &lt;a href=&quot;https://storybook.js.org/docs/web-components/get-started/introduction&quot;&gt;Storybook&lt;/a&gt;, which is the primary development tool used by the project. The last step symlinks our build asset to the document root on the image so that it will display when someone views a live preview.&lt;/p&gt;
&lt;p&gt;It feels like &lt;a href=&quot;https://www.drupal.org/project/gdwc/issues/3208848&quot;&gt;a more lightweight Node image could serve a similar purpose&lt;/a&gt;, but this solution meets our needs for now and builds in a reasonable amount of time.&lt;/p&gt;
&lt;p&gt;Thanks again to all who helped make these live previews possible - they have already simplied the review process for this project.&lt;/p&gt;
</description><pubDate>Tue, 27 Apr 2021 18:23:13 GMT</pubDate></item><item><title>2021 Best Picture Nominees</title><link>https://brianperry.dev/jams/2021/2021-best-picture-nominees/</link><guid isPermaLink="true">https://brianperry.dev/jams/2021/2021-best-picture-nominees/</guid><description>&lt;p&gt;I&apos;ve always wanted to watch all of the best picture nominees, but in past years I&apos;ve never been able to make it through the whole list. Thanks to the pandemic, I was finally able to achieve my goal this year. All of the films were easily streamable, and staying home and watching movies all the time was much more socially acceptable for some reason. Since no one asked, here are some quick thoughts on the films:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Sound of Metal&lt;/strong&gt; - not just my favorite film this year, probably one of my favorite movies in years. The sound design is incredible, but the story it tells about recovery is even better.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Father&lt;/strong&gt; - My second favorite on this list. A creative and well executed depiction of dementia. In another year I think Anthony Hopkins would have had a strong shot at winning best actor for this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Judas and the Black Messiah&lt;/strong&gt; - entertaining, but left me with a lot of questions about how much this story reflected reality. Great performance from Daniel Kaluuya. Also interesting to watch this after Trial of the Chicago 7 given some of the overlap between these movies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mank&lt;/strong&gt; - given all the shit talking about this movie, I liked it a lot more than I expected. Well shot and well crafted, and strange enough to keep me interested.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Minari&lt;/strong&gt; - wasn&apos;t quite ready for how much of a downer this was. The film overall didn&apos;t completely click for me, but Yuh-Jung Youn&apos;s performance really stuck with me. Hoping she wins for best supporting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nomadland&lt;/strong&gt; - feel like the odd man out given all the praise, but this didn&apos;t really do much for me. Still stunningly beautiful and worth watching for that alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Promising Young Woman&lt;/strong&gt; - a super entertaining but flawed film. Carey Mulligan is my favorite of the lead actress performances I saw. Had some real issues with the ending, but on the other hand the movie deserves some kind of &apos;best needle drop&apos; award for its use of a &lt;a href=&quot;https://www.youtube.com/watch?v=7FtHmaudaYE&quot;&gt;cover of Toxic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trial of the Chicago 7&lt;/strong&gt; - I get the shit talking about this one a little more than I did Mank. Felt more like a TV movie, but an admittedly entertaining one. And I found Sacha Baron Cohen&apos;s accent to be distractingly awful.&lt;/p&gt;
&lt;p&gt;Other nominated films I&apos;ve seen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time&lt;/strong&gt; - the juxtaposition of modern and archival footage was really haunting, but I ended up a little confused about the message I was supposed to be taking away. Maybe that was the point?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ma Rainey&apos;s Black Bottom&lt;/strong&gt; - finishing this one up, so not sure on my thoughts on the movie as a whole. Chadwick Boseman&apos;s performance really is incredible. Viola Davis&apos; performance is a little to showy for me, but sure is Oscary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Borat Subsequent Moviefilm&lt;/strong&gt; - give Maria Bakalova the Oscar you cowards!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Soul&lt;/strong&gt; - I think watching this in two parts tanked the emotional effectiveness. Whoops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Films I still want to see: One Night in Miami, Another Round, Collective, If Anything Happens I Love You.&lt;/p&gt;
&lt;p&gt;I feel like I don&apos;t watch many tv shows or movies anymore, but that sure is a lot of movies up above. I hope all the Oscar films continue to be this easily available in the future.&lt;/p&gt;
</description><pubDate>Sun, 25 Apr 2021 13:46:34 GMT</pubDate></item><item><title>What I&apos;m Excited About in Lit 2.0</title><link>https://brianperry.dev/posts/2021/what-im-excited-about-in-lit-2-0/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/what-im-excited-about-in-lit-2-0/</guid><description>&lt;p&gt;I tuned in for the live Lit launch event on YouTube today, which was surprisingly &lt;s&gt;long&lt;/s&gt; detailed. I&apos;ve really enjoyed the developer experience improvements that Lit-element and Lit-html have offered for building web components, and I&apos;m even more excited for these new features in Lit 2.0.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/f1j7b696L-E&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h3&gt;The new Lit branding&lt;/h3&gt;
&lt;p&gt;I&apos;ve always found the Polymer branding and the division between the Lit-element and Lit-html websites confusing. Having both packages live on just as Lit is so much more pleasant. Not to mention the fact that Lit is a super cool name.&lt;/p&gt;
&lt;h3&gt;Server Side Rendering support&lt;/h3&gt;
&lt;p&gt;Currently &lt;a href=&quot;https://github.com/lit/lit/tree/main/packages/labs/ssr&quot;&gt;pre-release under Lit labs&lt;/a&gt;, SSR support is one of the biggest ticket features in Lit 2.0. Much more learning to come here, but this should open up a wide variety of use cases. And it also uses a &lt;a href=&quot;https://www.chromestatus.com/feature/5191745052606464&quot;&gt;proposed declarative Shadow DOM feature&lt;/a&gt;, the impact of which is kind of mindblowing:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This API allows Web Components that use Shadow DOM to also make use of Server-Side Rendering (SSR), to get rendered content onscreen quickly without requiring Javascript for shadow root attachment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;IE 11 support is opt-in&lt;/h3&gt;
&lt;p&gt;When we can all ditch IE, we can ditch the extra dependency weight as well.&lt;/p&gt;
&lt;h3&gt;Refs&lt;/h3&gt;
&lt;p&gt;There are other ways to query and reference an element, but since I&apos;m familiar with the pattern from React, having &lt;a href=&quot;https://lit.dev/docs/api/directives/#createRef&quot;&gt;refs&lt;/a&gt; as an option is nice.&lt;/p&gt;
&lt;h3&gt;React&lt;/h3&gt;
&lt;p&gt;Speaking of React, another exciting Lit Labs project is &lt;a href=&quot;https://github.com/lit/lit/tree/main/packages/labs/react&quot;&gt;improved React integration&lt;/a&gt;. React has some &lt;a href=&quot;https://custom-elements-everywhere.com/#react&quot;&gt;long-standing limitations&lt;/a&gt; related to making use of web components. I&apos;d prefer to see React address this (and hope that as web component adoption grows their hand will be forced,) but Lit taking matters into their own hands is the next best thing.&lt;/p&gt;
&lt;h3&gt;Playground elements&lt;/h3&gt;
&lt;p&gt;Not necessarily a Lit 2.0 specific feature, but &lt;a href=&quot;https://github.com/PolymerLabs/playground-elements/#readme&quot;&gt;playground elements&lt;/a&gt; are used heavily throughout the great new &lt;a href=&quot;https://lit.dev/playground/&quot;&gt;Lit documentation&lt;/a&gt; and I have a long dormant side project that has been begging for this element.&lt;/p&gt;
&lt;h3&gt;Reactive controllers... probably&lt;/h3&gt;
&lt;p&gt;I still need to wrap my head around the impact of &lt;a href=&quot;https://lit.dev/docs/composition/controllers/&quot;&gt;reactive controllers&lt;/a&gt;. My hope is that it will simplify the application level state management story, but need to learn more to make sure I&apos;m not jumping to conclusions here.&lt;/p&gt;
&lt;h3&gt;Simple&lt;/h3&gt;
&lt;p&gt;It feels like there is a lot here, and I hope Lit manages to maintain the simplicity that drew me to it in the first place. Hopefully the new tagline of &apos;Simple. Fast. Web Components.&apos; remains true. Regardless, I&apos;m excited to make this upgrade in &lt;a href=&quot;https://www.drupal.org/project/gdwc&quot;&gt;the web component library I&apos;ve been working on&lt;/a&gt; and start experimenting.&lt;/p&gt;
</description><pubDate>Wed, 21 Apr 2021 17:12:58 GMT</pubDate></item><item><title>We&apos;re In The Golden Age of Contributing to Drupal</title><link>https://brianperry.dev/posts/2021/were-in-the-golden-age-of-contributing-to-drupal/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/were-in-the-golden-age-of-contributing-to-drupal/</guid><description>&lt;p&gt;I spent some time recently looking at the new &lt;a href=&quot;https://www.drupal.org/project/project_general&quot;&gt;general project type on Drupal.org&lt;/a&gt;. As someone who spends quite a bit of time working on Drupal adjacent things, a home for Drupal related projects that aren&apos;t modules or themes opens up some exciting (and important) possibilities. As I thought more about how this could be used for an upcoming project, it dawned on me that this was just latest in a series of developments that vastly improve the experience of contributing to Drupal.&lt;/p&gt;
&lt;p&gt;I&apos;m sure other Drupal lifers can think of older milestones, but my personal turning point goes back about four years ago to &lt;a href=&quot;https://www.drupal.org/project/drupalorg/issues/2666584&quot;&gt;when the project application process was updated&lt;/a&gt; so that any user would have the ability to create a full project on Drupal.org. Prior to that, the process to create a new project felt insurmountable (at least to younger me.) Drupal activity will never reach the 50 NPM packages that were probably created while you read this paragraph, but reducing the barrier to entry makes a big difference.&lt;/p&gt;
&lt;p&gt;Fast forward a few years and the hits just keep on coming.&lt;/p&gt;
&lt;p&gt;Unquestionably the biggest improvement is the introduction of the &lt;a href=&quot;https://www.drupal.org/docs/develop/git/using-git-to-contribute-to-drupal/creating-issue-forks-and-merge-requests&quot;&gt;merge request workflow&lt;/a&gt; on Drupal.org for both Drupal Core and Contributed projects. Rather than generating and uploading patches, users can now create an issue fork and submit a merge request. This process is likely to be much more comfortable to developers who are familiar with creating pull requests on Github. And this workflow wouldn&apos;t have been possible if Drupal hadn&apos;t &lt;a href=&quot;https://about.gitlab.com/blog/2018/08/16/drupal-moves-to-gitlab/&quot;&gt;migrated to a platform like Gitlab&lt;/a&gt; in the first place.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/NIWCXE-aM6Y&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;The process to set up a local environment for Drupal development has improved over this time frame as well. Docker based solutions like &lt;a href=&quot;https://github.com/thinktandem/drupal-contributions&quot;&gt;Lando&lt;/a&gt; and &lt;a href=&quot;https://github.com/drud/quicksprint&quot;&gt;DDEV&lt;/a&gt; have prepackaged environments to automate the process of setting up a local environment to contribute to Drupal. Looking forward, &lt;a href=&quot;https://github.com/shaal/ddev-gitpod&quot;&gt;cloud based environments&lt;/a&gt; could further simplify this set up process.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/ifk5dF6rGy0&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://simplytest.me/&quot;&gt;Simplytest.me&lt;/a&gt; continues to be a great option for contributors to evaluate modules and even test patches, and it has recently been modernized with a backend powered by &lt;a href=&quot;https://www.tugboat.qa/&quot;&gt;Tugboat&lt;/a&gt;. Speaking of Tugboat, it also now provides &lt;a href=&quot;https://www.drupal.org/docs/develop/git/using-git-to-contribute-to-drupal/using-live-previews-on-drupal-core-and-contrib&quot;&gt;live previews for merge requests for Core and Contrib projects&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That brings us all the way back to the recent addition of general projects.&lt;/p&gt;
&lt;p&gt;A ridiculous amount of work went into all of these developer experience improvements. Thanks to the many people who put in the time and effort to make this possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;During his keynote at DrupalCon this year Dries shared a walkthrough from &lt;a href=&quot;https://www.drupal.org/u/grasmash&quot;&gt;Matthew Grasmick&lt;/a&gt; that provided quite the counterpoint. I&apos;ll let the video speak for itself:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/IIxbaT-jmNc?start=4383&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;This is a completely fair reminder that while we&apos;ve come a long way, we also still have a long way to go to modernize our contributor experience.&lt;/p&gt;
&lt;p&gt;While I&apos;ve spent years getting to a point where I can benefit from the improvements that Drupal has made to the contribution workflow, countless others still get turned away at the door. Both things can be true: the contribution process is vastly improved, and it also isn&apos;t good enough. I&apos;m still hopeful though. Based on the improvements outlined in my original post I have faith that the Drupal project will be successful continuing to adopt the features of Gitlab and I&apos;ll be able to collaborate with countless new contributor friends who are able to make it through the proverbial Drupal door.&lt;/p&gt;
</description><pubDate>Wed, 21 Apr 2021 02:06:16 GMT</pubDate></item><item><title>Bluey</title><link>https://brianperry.dev/jams/2021/bluey/</link><guid isPermaLink="true">https://brianperry.dev/jams/2021/bluey/</guid><description>&lt;p&gt;As we&apos;ve been waiting for the weather here in Chicago to fully break in the direction of springtime, I feel like the entire family has been feeling extra cooped up. One thing that we found ourselves turning to as a way to reset at the end of the day is Bluey. If you have a preschool age child, you&apos;re probably already familiar with this show. But if you don&apos;t, I&apos;d encourage you to give it a try anyway. Everyone in my house is older than that target demographic and we all still love it.&lt;/p&gt;
&lt;p&gt;Bluey centers around an Australian family of &lt;a href=&quot;https://en.wikipedia.org/wiki/Australian_Cattle_Dog&quot;&gt;blue heeler dogs&lt;/a&gt;. There are &lt;a href=&quot;https://geekmom.com/2019/09/12-reasons-why-you-will-love-bluey/&quot;&gt;tons of reasons to love the show&lt;/a&gt;, but for me the hook is the &lt;a href=&quot;https://www.the-father-hood.com/article/bluey-how-a-cartoon-dog-became-your-ultimate-guide-to-fatherhood/&quot;&gt;surprisingly realistic depiction of parenting&lt;/a&gt;. They have fun, make mistakes, and sometimes just want their kids to leave them alone. I&apos;ve never seen anything like it on a show for preschoolers.&lt;/p&gt;
&lt;p&gt;To top it all off, &apos;Sleepytime&apos; isn&apos;t just a great episode of a kids show, I&apos;d rank it as one of my all time favorite episodes of television period.&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/hfkyGAQyNvg&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;I&apos;d put Bluey right up there with vaccines on the list of things that have kept us going the past few months. 🐾&lt;/p&gt;
</description><pubDate>Sun, 18 Apr 2021 13:29:14 GMT</pubDate></item><item><title>Help Build a Decoupled Menu Web Component at DrupalCon</title><link>https://brianperry.dev/posts/2021/build-a-web-component-at-drupalcon/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/build-a-web-component-at-drupalcon/</guid><description>&lt;p&gt;Next week at DrupalCon North America as part of the &lt;a href=&quot;https://events.drupal.org/northamerica2021/decoupled-menus-day&quot;&gt;Decoupled Menus Initiative&lt;/a&gt; I&apos;ll be participating in the &lt;a href=&quot;https://events.drupal.org/northamerica2021/news/decoupled-menus-initiative-hosting-contribution-hackathon&quot;&gt;Decoupled Menu Hackathon&lt;/a&gt;. Members of the community will be coming together to build a variety of menu components using different approaches and frameworks as a way to exercise a new menu endpoint provided by the &lt;a href=&quot;https://www.drupal.org/project/decoupled_menus&quot;&gt;Decoupled Menus module&lt;/a&gt;, along with the infrastructure supporting the new general project type on Drupal.org.&lt;/p&gt;
&lt;p&gt;Leading up to DrupalCon I&apos;ve also begun work on the &lt;a href=&quot;https://www.drupal.org/project/gdwc&quot;&gt;Generic Drupal Web Components&lt;/a&gt; project. Starting with a menu component, it aims to create a library of generic web components that are accessible, framework agnostic, possible to style, and easy to use with data provided by Drupal. Here&apos;s a quick video overview:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://www.youtube.com/embed/eWnMEbNLbws&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;p&gt;If you&apos;re interested in getting involved, we could use your help! &lt;a href=&quot;https://events.drupal.org/northamerica2021/sessions/decoupled-menus-initiative&quot;&gt;The Decoupled Menus Initiative Keynote&lt;/a&gt; on Tuesday April 13 is a great opportunity to catch up on the current status of the initiative. Following the keynote there will be related contribution efforts throughout the week of DrupalCon.&lt;/p&gt;
&lt;p&gt;If you&apos;re interested in working with the Generic Drupal Web Components project, there is a &lt;a href=&quot;https://www.drupal.org/project/gdwc/issues/3207329&quot;&gt;meta issue of tasks tagged for DrupalCon&lt;/a&gt;. Many of them involve seeing how far we can extend and re-theme the generic version of this component, which should leave room for a lot of interesting experimentation. Have other ideas? &lt;a href=&quot;https://www.drupal.org/project/gdwc/issues/3207329&quot;&gt;Leave a comment on the issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Web components not your thing? There is a &lt;a href=&quot;https://www.drupal.org/project/react_menu_component/issues/3206416&quot;&gt;similar meta issue&lt;/a&gt; for a &lt;a href=&quot;https://www.drupal.org/project/react_menu_component&quot;&gt;menu component being built using React&lt;/a&gt;. Prefer another framework or have another wild idea? &lt;a href=&quot;https://decoupled-menus.jsonapi.dev/system/menu/main/linkset&quot;&gt;Try out the demo endpoint&lt;/a&gt; and see what you can come up with.&lt;/p&gt;
&lt;p&gt;Looking forward to collaborating with everyone and seeing how many different ways we can represent data from a single menu endpoint. 🚀&lt;/p&gt;
&lt;p&gt;Resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://events.drupal.org/northamerica2021/decoupled-menus-day&quot;&gt;Decoupled Menus Day&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://events.drupal.org/northamerica2021/news/decoupled-menus-initiative-hosting-contribution-hackathon&quot;&gt;The Decoupled Menus Initiative is hosting a contribution hackathon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gdwc.netlify.app/&quot;&gt;Generic Drupal Web Components Storybook and Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.drupal.org/project/gdwc/issues/3207329&quot;&gt;Meta issue for Generic Drupal Web Components tasks for DrupalCon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Tue, 06 Apr 2021 21:43:45 GMT</pubDate></item><item><title>Using Screen Recordings to Identify Page Load Performance Issues</title><link>https://brianperry.dev/til/2021/using-screen-recordings-to-identify-performance-issues/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/using-screen-recordings-to-identify-performance-issues/</guid><description>&lt;p&gt;Sometimes when auditing Lighthouse results the amount of feedback can make it difficult to determine what to focus on improving. In cases like those, a screen recording can provide quite a bit of clarity.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.webpagetest.org/&quot;&gt;Web Page Test&lt;/a&gt; provides a number of easy options to generate a recording of your test runs. When setting up a test, check &apos;capture video&apos; in the advanced options. You can then save the recording as a video or a gif after clicking on the &apos;watch video&apos; link in the related column. Or you can enter the timeline view, configure options like frame size and slow motion, and export your recording.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Here is an example &lt;a href=&quot;/static/img/bpdev.gif&quot;&gt;recording of the homepage of this site being loaded&lt;/a&gt;. You&apos;ll see a pretty obvious problem - the menu is loading as expanded and then collapsing when JavaScript executes, which is likely causing &lt;a href=&quot;https://web.dev/cls/&quot;&gt;cumulative layout shift&lt;/a&gt;. I&apos;ve found that a slow motion video can also make less glaring issues clear, like web fonts shifting during load.&lt;/p&gt;
&lt;p&gt;For a paid alternative, &lt;a href=&quot;https://calibreapp.com/&quot;&gt;Calibre&lt;/a&gt; automatically captures and archives a video recording of each test run. And if you&apos;re specifically trying to eliminate layout shift, this &lt;a href=&quot;https://defaced.dev/tools/layout-shift-gif-generator/&quot;&gt;layout shift gif generator&lt;/a&gt; can be helpful.&lt;/p&gt;
&lt;p&gt;You can get similar insights by scrolling through the timeline view in the performance tab in Chrome, but there is just something about a video that clicks for me a little easier. Grab some 🍿, sit back, and find those page loading bottlenecks.&lt;/p&gt;
</description><pubDate>Fri, 02 Apr 2021 21:50:15 GMT</pubDate></item><item><title>How I Exercised in this Cold Pandemic Wasteland</title><link>https://brianperry.dev/posts/2021/how-i-exercise-in-this-cold-pandemic-wasteland/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/how-i-exercise-in-this-cold-pandemic-wasteland/</guid><description>&lt;p&gt;Temperatures are finally leaning in the direction of spring here in the Chicago area, but the combination of weather and the pandemic made for a rough winter. For exercise, I mainly just run. Since it is the only form of exercise I truly enjoy, I try to do it year round - outside if there is no snow or ice on the ground, and indoors if it is too cold. Indoors wasn&apos;t an option this year, so I had to find some alternatives to stay active.&lt;/p&gt;
&lt;p&gt;(Could I have bought a treadmill? Funny story - we have one, but the ceiling in my basement is low enough that if I ran on it, I&apos;d hit my head.)&lt;/p&gt;
&lt;p&gt;I ended up cycling through a few digital options. I&apos;m not saying this is a very successful plan - at best I&apos;ve maintained where I was at pre-pandemic. Regardless, having a variety of options for some amount of daily activity was helpful as we wait for the world to open back up.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://apps.apple.com/us/app/7-minute-workout/id650762525&quot;&gt;7 Minute Workout App&lt;/a&gt; - short interval training workouts you can do with just body weight. I&apos;ve had this app forever and it is just enough to make me feel like I was active.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.apple.com/apple-fitness-plus/&quot;&gt;Apple Fitness +&lt;/a&gt; - I&apos;ve really enjoyed Apple Fitness +. The trainers are bearable, and the variety in workouts made a big difference while I was stuck in my basement. I mostly did strength training and interval workouts, with core workouts on occasion. I also enjoyed the mindful cooldowns that were suggested after completing a workout. They also had the side effect of getting me back into meditation a bit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.calm.com/&quot;&gt;Calm&lt;/a&gt; - I don&apos;t need guided meditation as much as I used to, but I use Calm when I do. I&apos;m hoping I can keep up with meditation when I get back to outdoor running, but my track record on that isn&apos;t great.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ringfitadventure.nintendo.com/&quot;&gt;Ring Fit Adventure&lt;/a&gt; - The workouts you get from this crazy Nintendo gadget are typically pretty light, but this also falls into the &apos;at least I did something active&apos; category. The game itself is surprisingly fun and deep, and the ring device itself works really well.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now that I can run outside again I still want to stick with Apple Fitness - at least the strength training (I. Am. Weak.) So maybe we can add that just after &apos;puzzles&apos; on the list of good things I picked up during the pandemic.&lt;/p&gt;
</description><pubDate>Thu, 25 Mar 2021 13:08:52 GMT</pubDate></item><item><title>Gearing Up For Midcamp 2021</title><link>https://brianperry.dev/posts/2021/gearing-up-for-midcamp-2021/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/gearing-up-for-midcamp-2021/</guid><description>&lt;p&gt;&lt;a href=&quot;https://www.midcamp.org/&quot;&gt;Midcamp&lt;/a&gt;, my local Drupal Camp, is back March 24 - 27. Midcamp went virtual at the beginning of lockdown last year and while it was a rousing success, we were definitely making it up as we went along. This time around we&apos;re trying a new format with an increased focus on community building which we&apos;re also hoping will help combat the Zoom fatigue we&apos;re all feeling. &lt;a href=&quot;https://www.midcamp.org/2021/schedule&quot;&gt;The schedule&lt;/a&gt; breaks down as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wednesday, March 24: Get Started with Drupal - content focused on introducing attendees to Drupal.&lt;/li&gt;
&lt;li&gt;Thursday, March 25: Meet the Drupal Community - lightly structured community building activities.&lt;/li&gt;
&lt;li&gt;Friday, March 26: Share Your Knowledge - a one day &lt;a href=&quot;http://unconference.net/unconferencing-how-to-prepare-to-attend-an-unconference/&quot;&gt;unconference format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Saturday, March 27: Give Back to the Project - Our traditional contribution day.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Within that schedule, here are a few things I&apos;m extra excited about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In addition to our traditional Zoom rooms, we&apos;ll also have a space in &lt;a href=&quot;https://gather.town/&quot;&gt;Gather.town&lt;/a&gt; for us to virtually socialize. I&apos;ve been spending some time moving around sprites, and expect to have some surprises in store.&lt;/li&gt;
&lt;li&gt;On Thursday, &lt;a href=&quot;https://www.drupal.org/u/mglaman&quot;&gt;Matt Glaman&lt;/a&gt; will be leading a workshop on writing JavaScript tests using &lt;a href=&quot;https://nightwatchjs.org/&quot;&gt;Nightwatch.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;And on contribution day, one of the featured initiatives will be adding additional test coverage to Drupal&apos;s new &lt;a href=&quot;https://www.drupal.org/about/core/strategic-initiatives/olivero&quot;&gt;Olivero theme&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://ti.to/midcamp/2021&quot;&gt;Registration&lt;/a&gt; is pay what you want, and we&apos;d love to see you there.&lt;/p&gt;
&lt;p&gt;With any luck we&apos;ll have an in-person component at Midcamp next year, which will also overlap with St. Patrick&apos;s day. I&apos;m workshopping the following tagline: Midcamp 2022: IRL in IRL.&lt;/p&gt;
</description><pubDate>Mon, 15 Mar 2021 19:56:40 GMT</pubDate></item><item><title>Recent Drupal Podcast Appearances</title><link>https://brianperry.dev/posts/2021/recent-drupal-podcast-appearances/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/recent-drupal-podcast-appearances/</guid><description>&lt;p&gt;I&apos;ve been lucky enough to be a guest on a few Drupal related podcasts recently, continuing my long standing trend of talking to anyone who will listen.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.drupaleasy.com/podcast&quot;&gt;Drupal Easy Podcast&lt;/a&gt;: Back in January, Mike Anello had me on to talk about Front End Components on &lt;a href=&quot;https://www.drupaleasy.com/podcast/2021/01/drupaleasy-podcast-238-front-end-components-beginners-brian-perry&quot;&gt;Episode 238&lt;/a&gt;. We mostly focused on the basics, which was a nice change of pace compared to the component integration talk I had been giving recently.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://talkingdrupal.com/&quot;&gt;Talking Drupal&lt;/a&gt;: the folks at Talking Drupal recently decided to have a rotating co-host seat for four week stretches and were nice enough to invite me to be the first to occupy the seat. During my four week run I was lucky enough to chat about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://talkingdrupal.com/282&quot;&gt;Starting a Drupal Career&lt;/a&gt; with Mike Anello (I swear Mike is following me)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://talkingdrupal.com/283&quot;&gt;Tugboat&lt;/a&gt; with Matt Westgate&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://talkingdrupal.com/284&quot;&gt;Iterative Approach To Decoupling&lt;/a&gt; with me!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://talkingdrupal.com/285&quot;&gt;Decoupled Menus&lt;/a&gt; with Gabe Sullice, Baddy Sonja Breidert, and Liam Hockley.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Decoupled Menus discussion was especially enlightening as it clarified some of my misconceptions about the project (they aren&apos;t shipping a menu component, instead they are making enhancements to Drupal to better support many menu components) and also gave me some ideas about how I might be able to contribute.&lt;/p&gt;
&lt;p&gt;A huge thanks to Stephen, John and Nic for having me - it was a great experience. Looking forward to hearing from their future guest hosts.&lt;/p&gt;
&lt;p&gt;On the topic of podcasts, I&apos;d recommend the following recent non-Drupal podcasts that I thought were especially great:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://shoptalkshow.com/451/&quot;&gt;Shop Talk Show 451&lt;/a&gt;: JavaScript and Web Components with Nolan Lawson&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://syntax.fm/show/330/react-query-more-react-with-tanner-linsley&quot;&gt;Syntax 330&lt;/a&gt;: React Query + More React with Tanner Linsley&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Sat, 13 Mar 2021 22:56:40 GMT</pubDate></item><item><title>Running Lighthouse CI Server Locally</title><link>https://brianperry.dev/til/2021/running-lighthouse-ci-server-locally/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/running-lighthouse-ci-server-locally/</guid><description>&lt;p&gt;I had previously &lt;a href=&quot;/til/2021/configuring-lighthouse-ci-with-github-actions/&quot;&gt;set up Lighthouse CI with Github Actions&lt;/a&gt;, but I still found myself looking longingly at the &lt;a href=&quot;https://twitter.com/_developit/status/1266112451155841024&quot;&gt;reports provided by Lighthouse CI Server&lt;/a&gt; as a way to compare results over time. While the docs are mainly focused on running on a public server with persistent storage (which is clearly the most useful way to do this,) I couldn&apos;t help but wonder if I could run the server locally to capture some quick data comparing a few specific changes. Turns out you absolutely can. The docs provide &lt;a href=&quot;https://twitter.com/_developit/status/1266112451155841024&quot;&gt;an example Docker container to use locally&lt;/a&gt;, but I went with the even lower-fi solution.&lt;/p&gt;
&lt;p&gt;From within the repository containing the code that I&apos;m measuring, I added packages for Lighthouse CI Server, along with sqlite for data storage.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -D @lhci/cli @lhci/server sqlite3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also added &lt;a href=&quot;https://www.npmjs.com/package/concurrently&quot;&gt;Concurrently&lt;/a&gt; as it made it easier to start the server and collect data in one fell swoop:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -D concurrently
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While I was messing around with &lt;code&gt;package.json&lt;/code&gt; I added the following scripts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
  &quot;lhci:audit&quot;: &quot;concurrently \&quot;npm run lhci:server\&quot; \&quot;lhci autorun\&quot;&quot;,
  &quot;lhci:wizard&quot;: &quot;lhci wizard&quot;,
  &quot;lhci:server&quot;: &quot;lhci server --storage.storageMethod=sql --storage.sqlDialect=sqlite --storage.sqlDatabasePath=./db.sql&quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With those scripts in place, I can start the server by running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run lhci:server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then run the wizard with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run lhci:wizard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The wizard will ask you a few questions about your server and your code repository, and then provide your build token and admin token. With that information in hand, I added &lt;code&gt;lighthouserc.js&lt;/code&gt; in the root of my repository containing the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  ci: {
    collect: {
      url: [&apos;https://www.brianperry.dev&apos;],
    },
    upload: {
      target: &apos;lhci&apos;,
      serverBaseUrl: &apos;http://localhost:9001&apos;,
      token: &apos;###-###-###&apos;, // the build token provider by the wizard. Could also use LHCI_TOKEN variable instead
    },
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my case, I added &lt;code&gt;lighthouserc.js&lt;/code&gt; to the &lt;code&gt;.gitignore&lt;/code&gt; for my project because I didn&apos;t want these local settings to override the Lighthouse settings I already had working with Github Actions. You may also want to ignore &lt;code&gt;.lighthouseci/&lt;/code&gt; and &lt;code&gt;db.sql&lt;/code&gt; as well if you are only planning to store this information locally.&lt;/p&gt;
&lt;p&gt;With that in place I can now run&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run lhci:audit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which will start the Lighthouse CI Server and record results for the current commit. If I run this repeatedly as commits are made, I can get a granular comparison of the impact of my changes.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This would certainly be more useful incorporated into your main CI process, but this local approach can still be useful for benchmarking a feature branch or making some other small scale measurement.&lt;/p&gt;
</description><pubDate>Wed, 10 Mar 2021 23:10:55 GMT</pubDate></item><item><title>Embedding a File From Github as a Gist</title><link>https://brianperry.dev/til/2021/embedding-a-file-from-github-as-a-gist/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/embedding-a-file-from-github-as-a-gist/</guid><description>&lt;p&gt;This is probably the first personal site I&apos;ve ever had that has quality &lt;a href=&quot;https://www.11ty.dev/docs/plugins/syntaxhighlight/&quot;&gt;syntax highlighting for code snippets&lt;/a&gt;. But there are still cases where it would be easier to include an example from a github repository wholesale. I&apos;ve found a couple of services that handle this nicely.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gist-it.appspot.com/&quot;&gt;Gist It&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://emgithub.com/&quot;&gt;Embed Like Gist&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;ve been using Gist It, because the styles on this site conflict with Embed Like Gist.&lt;/p&gt;
&lt;p&gt;For either of these services, it is as easy as embedding a script tag within the markdown file for my post.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Markdown Heading
&amp;lt;script src=&quot;https://gist-it.appspot.com/github/robertkrimen/gist-it-example/blob/master/example.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
### More markdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output looks like this:&lt;/p&gt;
&lt;p&gt;&amp;lt;script src=&quot;https://gist-it.appspot.com/github/robertkrimen/gist-it-example/blob/master/example.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;Handy trick, but but a tradeoff - highlighting the code in the post itself will work forever, but this approach will only work as long as the services exist.&lt;/p&gt;
</description><pubDate>Tue, 09 Mar 2021 23:19:12 GMT</pubDate></item><item><title>Adding Simple Pagination to an 11ty Collection</title><link>https://brianperry.dev/til/2021/adding-simple-pagination-to-an-11ty-collection/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/adding-simple-pagination-to-an-11ty-collection/</guid><description>&lt;p&gt;11ty can handle pagination really well, but finding the right subset of the many pagination features can be a little difficult to figure out. In my case, I just wanted something really simple so that I didn&apos;t end up with endlessly scrolling list pages as this site grows. Since this site uses &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind&lt;/a&gt;, I decided to aim for something similar to the &lt;a href=&quot;https://tailwindui.com/components/application-ui/navigation/pagination#component-0797a02a34692167c369d134e7a6f9c5&quot;&gt;simple card footer from Tailwind UI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At its simplest, page data can be enabled by adding the &lt;code&gt;pagination&lt;/code&gt; key to your template&apos;s front matter. The first example from the &lt;a href=&quot;https://www.11ty.dev/docs/pagination/&quot;&gt;pagination docs&lt;/a&gt; shows how to page over a dataset also defined in your front matter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
pagination:
  data: testdata
  size: 2
testdata:
  - item1
  - item2
  - item3
  - item4
---
&amp;lt;ol&amp;gt;
{%- for item in pagination.items %}
&amp;lt;li&amp;gt;{{ item }}&amp;lt;/li&amp;gt;
{% endfor -%}
&amp;lt;/ol&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my case, I was looking to &lt;a href=&quot;https://www.11ty.dev/docs/pagination/#paging-a-collection&quot;&gt;add pagination to a collection&lt;/a&gt;. &lt;a href=&quot;https://github.com/backlineint/bpi-11ty/blob/8ebdd42966106e98ea172781d08a0027586d44bf/src/all/index.html&quot;&gt;My existing template&lt;/a&gt; set a variable for the collection, and then used a filter to reverse it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% set postslist = collections.all %}

{% for post in postslist | reverse %}
{# iterate through posts #}
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can instead use the pagination key in our front matter to prepare this data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
title: &quot;All Updates&quot;
pagination:
data: collections.all
size: 5
alias: postslist
reverse: true

---

{% for post in postslist %}
{# iterate through posts #}
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We specified the collection that should be used as the dataset, the number of posts that should display on each page, and reversed the results. And by using the alias of &lt;code&gt;postslist&lt;/code&gt; I didn&apos;t have to change variable names in any of my existing markup. You could instead use the default &lt;code&gt;pagination.items&lt;/code&gt; if you were starting from scratch.&lt;/p&gt;
&lt;p&gt;Now on to the pager itself. First let&apos;s look at displaying the current page and result set.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% if pagination.pageLinks.length &amp;gt; 1 %}

  &amp;lt;div class=&quot;pb-6 sm:pb-0&quot;&amp;gt;
    &amp;lt;p class=&quot;text-md text-gray-500&quot;&amp;gt;
      Showing
      &amp;lt;span class=&quot;font-medium&quot;&amp;gt;
        {{ (pagination.pageNumber * pagination.size) + 1 }}
      &amp;lt;/span&amp;gt;
      to
      &amp;lt;span class=&quot;font-medium&quot;&amp;gt;
        {% if pagination.nextPageLink %}
          {{ (pagination.pageNumber * pagination.size) + pagination.size }}
        {% else %}
          {{ collections.all.length }}
        {% endif %}
      &amp;lt;/span&amp;gt;
      of
      &amp;lt;span class=&quot;font-medium&quot;&amp;gt;{{ collections.all.length }}&amp;lt;/span&amp;gt;
      results
    &amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
{% endif %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see in that code snippet, 11ty&apos;s gives us a bunch of helpful data in the pagination object. We can use &lt;code&gt;pagination.pageLinks.length&lt;/code&gt; to determine if there is even enough data to page - if there is only one page, we don&apos;t render the pagination at all. We&apos;re also doing a tiny little bit of math to determine the current range of posts that are displayed (I should have warned you that there would be math.) In &lt;code&gt;(pagination.pageNumber * pagination.size) + 1&lt;/code&gt; we&apos;re adding 1 because page number is zero indexed. And we can use &lt;code&gt;collections.all.length&lt;/code&gt; for the overall number of posts.&lt;/p&gt;
&lt;p&gt;Next, let&apos;s add the previous and next links. I based this on &lt;a href=&quot;https://github.com/11ty/eleventy/issues/455#issuecomment-474026138&quot;&gt;an example from an 11ty github issue&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;nav class=&quot;pagination&quot;&amp;gt;
  {% if pagination.previousPageLink %}
    &amp;lt;a class=&quot;pagination__item&quot; href=&quot;{{ pagination.previousPageHref }}&quot;&amp;gt;Previous&amp;lt;/a&amp;gt;
  {% else %}
    &amp;lt;span class=&quot;pagination__item&quot;&amp;gt;Previous&amp;lt;/span&amp;gt;
  {% endif %}
  {% if pagination.nextPageLink %}
    &amp;lt;a class=&quot;pagination__item&quot; href=&quot;{{ pagination.nextPageHref}}&quot;&amp;gt;Next&amp;lt;/a&amp;gt;
  {% else %}
    &amp;lt;span class=&quot;pagination__item&quot;&amp;gt;Next&amp;lt;/span&amp;gt;
  {% endif %}
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If there is a &lt;code&gt;previousPageLink&lt;/code&gt; or &lt;code&gt;nextPageLink&lt;/code&gt; we&apos;re displaying as a link, otherwise we&apos;re displaying as a span to indicate that the option is disabled. And the link to the previous or next page of results is available in &lt;code&gt;pagination.previousPageHref&lt;/code&gt; and &lt;code&gt;pagination.nextPageHref&lt;/code&gt; respectively.&lt;/p&gt;
&lt;p&gt;After layering in some Tailwind classes the end result looked like this:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Putting it all together, here&apos;s the final pagination partial. I&apos;m using a variable &lt;code&gt;length&lt;/code&gt; rather than &lt;code&gt;collections.all.length&lt;/code&gt; so this pagination partial can be used with other collections.&lt;/p&gt;
&lt;p&gt;&amp;lt;script src=&quot;https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fbacklineint%2Fbpi-11ty%2Fblob%2Fmaster%2Fsrc%2F_includes%2Fpartials%2Fpagination.html&amp;amp;style=default&amp;amp;type=code&amp;amp;showBorder=on&amp;amp;showLineNumbers=on&amp;amp;showFileMeta=on&amp;amp;showFullPath=on&amp;amp;showCopy=on&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;Resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.11ty.dev/docs/pagination&quot;&gt;11ty Pagination&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.11ty.dev/docs/pagination/nav/&quot;&gt;11ty Pagination Navigation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/11ty/eleventy/issues/455&quot;&gt;Github issue - Pagination is very confusing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindui.com/components/application-ui/navigation/pagination#component-0797a02a34692167c369d134e7a6f9c5&quot;&gt;Tailwind UI Simple Card Footer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Sun, 07 Mar 2021 17:00:30 GMT</pubDate></item><item><title>Configuring Lighthouse CI with Github Actions</title><link>https://brianperry.dev/til/2021/configuring-lighthouse-ci-with-github-actions/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/configuring-lighthouse-ci-with-github-actions/</guid><description>&lt;p&gt;I&apos;ll be spending a lot of time auditing web performance over the next couple of weeks, and along the way expect to be setting up some tooling that I haven&apos;t used before. First on the list was Lighthouse CI, which I took for a spin in a Github repository. I initially set this up manually following &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/getting-started.md&quot;&gt;the getting started docs&lt;/a&gt;, but eventually learned that the &lt;a href=&quot;https://github.com/marketplace/actions/lighthouse-ci-action&quot;&gt;Lighthouse CI Action&lt;/a&gt; met my needs nicely.&lt;/p&gt;
&lt;p&gt;First, I added a &lt;code&gt;budget.json&lt;/code&gt; file which I will eventually assert against:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  {
    &quot;path&quot;: &quot;/*&quot;,
    &quot;resourceSizes&quot;: [
      {
        &quot;resourceType&quot;: &quot;document&quot;,
        &quot;budget&quot;: 18
      },
      {
        &quot;resourceType&quot;: &quot;total&quot;,
        &quot;budget&quot;: 200
      }
    ]
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is just the default example from the docs, I&apos;ll be defining what the actual budget should be as I get deeper into auditing.&lt;/p&gt;
&lt;p&gt;Next I created &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Lighthouse CI for Netlify sites
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js 14.x
        uses: actions/setup-node@v1
        with:
          node-version: 14.x
      - name: Install &amp;amp; Build
        run: |
          npm install
          npm build
      - name: Wait for the Netlify Preview
        uses: jakepartusch/wait-for-netlify-action@v1
        id: netlify
        with:
          site_name: &apos;my-fun-netlify-name&apos;
      - name: Audit URLs using Lighthouse
        uses: treosh/lighthouse-ci-action@v7
        with:
          urls: |
            ${{ steps.netlify.outputs.url }}
            ${{ steps.netlify.outputs.url }}/about/
            ${{ steps.netlify.outputs.url }}/all/
            ${{ steps.netlify.outputs.url }}/jams/2021/hades/
            ${{ steps.netlify.outputs.url }}/posts/2021/gatsby-source-drupal-only-referenced-images/
          budgetPath: ./budget.json
          temporaryPublicStorage: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This isn&apos;t the default recipe, but it allowed me to wait for Netlify&apos;s deploy preview to complete and then run lighthouse on the actual preview site. In my case I also needed to bump things up to a more recent version of node instead of the default. I also removed &lt;code&gt;uploadArtifacts: true&lt;/code&gt; as I don&apos;t know that I really need the reports to persist long term (I can always re-enable this later).&lt;/p&gt;
&lt;p&gt;At this point, I had the lighthouse reports running, but I had to dig into the build logs to get them. Thankfully it was possible to configure &lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/getting-started.md#github-status-checks&quot;&gt;Github status checks&lt;/a&gt; to see the results right in the Github pull request UI. I had to authorize &lt;a href=&quot;https://github.com/apps/lighthouse-ci&quot;&gt;the Lighthouse CI Github App&lt;/a&gt; which provided me an authorization token. Under the settings for my repository I then added that token as a repository secret called &lt;code&gt;LHCI_GITHUB_APP_TOKEN&lt;/code&gt;. To add the checks to my CI job I had to add two sections to my &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, I had to add a reference to the related git history:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;steps:
  - uses: actions/checkout@v2
    with:
      ref: ${{ github.event.pull_request.head.sha }}
{% endhighlight %}

I missed this part initially. Without it the status checks will be sent, but they won&apos;t be able to be tied to the correct PR.

Lastly, I had to add a the repository secret as an environment variable:

{% highlight yml 11-12 %}
- name: Audit URLs using Lighthouse
  uses: treosh/lighthouse-ci-action@v7
  with:
    urls: |
      ${{ steps.netlify.outputs.url }}
      ${{ steps.netlify.outputs.url }}/about/
      ${{ steps.netlify.outputs.url }}/all/
      ${{ steps.netlify.outputs.url }}/jams/2021/hades/
      ${{ steps.netlify.outputs.url }}/posts/2021/gatsby-source-drupal-only-referenced-images/
    budgetPath: ./budget.json
    temporaryPublicStorage: true
  env:
    LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With all of that in place I now see entries for each lighthouse check, and can click on the details link to launch the report.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This site isn&apos;t actually what I&apos;m auditing, but it still looks like I&apos;ve got some work to do :)&lt;/p&gt;
</description><pubDate>Wed, 03 Mar 2021 20:34:15 GMT</pubDate></item><item><title>Adding Excerpts to an 11ty RSS Feed</title><link>https://brianperry.dev/til/2021/adding-excerpts-11ty-rss/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/adding-excerpts-11ty-rss/</guid><description>&lt;p&gt;I previously wrote about &lt;a href=&quot;/til/2021/creating-multiple-feeds-with-the-11ty-rss-plugin/&quot;&gt;creating multiple feeds with the 11ty RSS Plugin&lt;/a&gt; which I was doing in service of creating a feed for &lt;a href=&quot;https://www.drupal.org/planet&quot;&gt;Planet Drupal&lt;/a&gt;. While creating a second feed was an important first step, I also &lt;a href=&quot;https://www.drupal.org/project/content/issues/3199462&quot;&gt;found that my feed needed to have an excerpt&lt;/a&gt; of a specific maximum length.&lt;/p&gt;
&lt;p&gt;There are a handful of options out there for handling excerpts with 11ty. &lt;a href=&quot;https://www.npmjs.com/package/eleventy-plugin-excerpt&quot;&gt;eleventy-plugin-excerpt&lt;/a&gt; adds a universal shortcode for excerpts. This was close to what I needed, but the shortcode takes the entire template object as an input, which made using additional filters like htmlToAbsoluteUrls from &lt;a href=&quot;https://www.11ty.dev/docs/plugins/rss/&quot;&gt;eleventy-rss&lt;/a&gt; difficult.&lt;/p&gt;
&lt;p&gt;Slightly buried in the docs to customize front matter parsing is an example of &lt;a href=&quot;https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content&quot;&gt;parsing excerpts from content&lt;/a&gt; using additional options from the gray-matter package. I was only able to access the excerpt in my feed if I used an excerpt alias for some reason, so I ended up adding the following to my 11ty config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eleventyConfig.setFrontMatterParsingOptions({
  excerpt: true,
  excerpt_alias: &apos;feed_excerpt&apos;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I can now include a separator in my post markdown as follows:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;This will serve as my excerpt. It is also included in the full post.&amp;lt;br /&amp;gt;
---&amp;lt;br /&amp;gt;
And everything after will also be included in the full post, but not the excerpt.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It is also possible to manually overwrite the excerpt field by specifying it in your markdown front matter. This nice little escape hatch comes in handy if you want the excerpt to combine non sequential parts of the post, or otherwise work around some markup that should be excluded from the excerpt.&lt;/p&gt;
&lt;p&gt;Since these excerpts were being added for an RSS feed, I also needed to take an extra step to ensure that the excerpt was being rendered as markup rather than raw markdown. As a quick workaround, I created a custom &lt;code&gt;feedEncode&lt;/code&gt; filter using 11ty&apos;s default markdown parsing library:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// eleventy.js
const md = require(&apos;markdown-it&apos;)();

// add within module.exports function:
eleventyConfig.addNunjucksFilter(&quot;feedEncode&quot;, function(value) {
  return value ? md.render(value) : &apos;&apos;;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in my feed template, I can pass the excerpt through my filter to ensure it will be rendered as html in feed readers:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;&amp;lt;description&amp;gt;{{ post.data.feed_excerpt | htmlToAbsoluteUrls(absolutePostUrl) | feedEncode }}&amp;lt;/description&amp;gt;&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Due to some confusion related to feed validation, I also created &lt;a href=&quot;https://github.com/11ty/eleventy-plugin-rss/pull/24&quot;&gt;an RSS feed example for the 11ty RSS plugin&lt;/a&gt;. In hindsight, I could have gotten away with the default Atom feed example, but perhaps someone else out there will find this useful.&lt;/p&gt;
</description><pubDate>Tue, 02 Mar 2021 00:00:00 GMT</pubDate></item><item><title>Bartees Strange: Live Forever</title><link>https://brianperry.dev/jams/2021/bartees-strange-live-forever/</link><guid isPermaLink="true">https://brianperry.dev/jams/2021/bartees-strange-live-forever/</guid><description>&lt;p&gt;Hard to pin the sound down on this one, so I&apos;m not even going to try.&lt;/p&gt;
&lt;p&gt;After a mood setting intro the album opens up with two absolutely killer songs:&lt;/p&gt;
&lt;p&gt;&amp;lt;p style=&quot;text-align: center&quot;&amp;gt;&amp;lt;iframe style=&quot;display: inline&quot; width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/FlywGIexOnA&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p style=&quot;text-align: center&quot;&amp;gt;&amp;lt;iframe style=&quot;display: inline&quot; width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/EFaZ43nW28c&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;Having those two massive songs up front initially made things feel really front loaded, but this album has been a real grower for me that has balanced out over time. Looking forward to following his work in the future.&lt;/p&gt;
</description><pubDate>Sun, 21 Feb 2021 00:00:00 GMT</pubDate></item><item><title>Running Lando on an Apple Silicon Mac</title><link>https://brianperry.dev/til/2021/running-lando-on-an-apple-silicon-mac/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/running-lando-on-an-apple-silicon-mac/</guid><description>&lt;p&gt;While I&apos;ve done some NodeJS based development on my M1 MacBook, I had been procrastinating on doing anything Docker based. I had an opportunity to take the plunge today and decided to go for it.&lt;/p&gt;
&lt;p&gt;My understanding is that &lt;a href=&quot;https://github.com/lando/lando/issues/2688&quot;&gt;this config is not officially supported&lt;/a&gt; for a variety of reasons, &lt;s&gt;but for me it pretty much just worked&lt;/s&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I did:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Downloaded and installed the &lt;a href=&quot;https://docs.docker.com/docker-for-mac/apple-m1/&quot;&gt;Docker M1 Tech Preview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Downloaded the latest &lt;a href=&quot;https://github.com/lando/lando/releases&quot;&gt;Lando release&lt;/a&gt; (3.0.25 in my case)&lt;/li&gt;
&lt;li&gt;Installed Lando, being sure to customize the install to not install Docker Desktop over my existing tech preview.&lt;/li&gt;
&lt;li&gt;Created and started a Drupal 9 project &lt;s&gt;that just worked&lt;/s&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I run into any quirks as I put it through the paces a bit more, I&apos;ll update this post.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Quirks already :)&lt;/p&gt;
&lt;p&gt;Upon further restarts things seemed to be getting hung up on the message &lt;code&gt;Scanning to determine which services are ready... Please standby... ?&lt;/code&gt; I was able to get past this by adding the following to &lt;code&gt;.lando.yml&lt;/code&gt; &lt;a href=&quot;https://github.com/lando/lando/issues/2281#issuecomment-632620604&quot;&gt;as mentioned in this issue&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  appserver:
    scanner: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works, but indicates that there is some mystery issue under the covers. So as the Lando installer says when I install using an existing version of Docker: YMMV.&lt;/p&gt;
</description><pubDate>Sat, 20 Feb 2021 20:23:29 GMT</pubDate></item><item><title>Talks: Florida Drupal Camp 2021</title><link>https://brianperry.dev/posts/2021/florida-drupal-camp-2021/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/florida-drupal-camp-2021/</guid><description>&lt;p&gt;&lt;a href=&quot;https://noti.st/brianperry/bdLuuQ/web-components-through-the-eyes-of-a-newcomer&quot;&gt;Slides&lt;/a&gt; / &lt;a href=&quot;https://www.fldrupal.camp/sessions/design-theming-front-end-development/web-components-through-eyes-newcomer&quot;&gt;Session Page&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This was my second time giving this talk, and I kind of forgot how dense of a topic this is. One major update was a re-worked example of approaches to scoped styling:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://codesandbox.io/embed/election-results-tracker-global-styling-options-w0i3e?fontsize=14&amp;amp;hidenavigation=1&amp;amp;theme=dark&quot;
style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
title=&quot;Election Results Tracker (Global Styling Options)&quot;
allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(The sandbox probably doesn&apos;t make a ton of sense without having someone walk through it. Might try to do a quick recording of it someday.) Next time I do this styling demo, it should go later in the presentation. I think people needed more of a foundation on web component concepts before we get this deep into styling specifics.&lt;/p&gt;
&lt;p&gt;I also added an example of using web components with a JS framework - React in this case:&lt;/p&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;https://codesandbox.io/embed/web-components-with-react-tvnwd?fontsize=14&amp;amp;hidenavigation=1&amp;amp;theme=dark&quot;
style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
title=&quot;Web Components With React&quot;
allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I think this example ended up being a little confusing because React and Web Components don&apos;t play all that nice together. I&apos;ll try to work up an example with a more web component friendly framework next time.&lt;/p&gt;
&lt;p&gt;Also saw that I somehow found my way into being listed as a featured presenter on &lt;a href=&quot;https://noti.st/explore&quot;&gt;Noti.st&lt;/a&gt; where I post my slides. Cool! (Less cool is the fact that my wife called me out on using a photo that is too old...)&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Other odds and ends:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The way the camp used &lt;a href=&quot;https://gather.town/&quot;&gt;gather.town&lt;/a&gt; was really fun. Hoping we can borrow some of FLDC&apos;s great ideas for &lt;a href=&quot;https://www.midcamp.org/&quot;&gt;Midcamp&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Found out about Promet Source&apos;s &lt;a href=&quot;https://github.com/promet/provus&quot;&gt;Provus&lt;/a&gt; project which is a component based Drupal stater site. We are working on a similar effort at &lt;a href=&quot;https://www.bounteous.com/&quot;&gt;Bounteous&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Hoping to check in on contribution day for a bit.&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Sat, 20 Feb 2021 00:00:00 GMT</pubDate></item><item><title>Configuring gatsby-source-drupal to only Import Referenced Images</title><link>https://brianperry.dev/posts/2021/gatsby-source-drupal-only-referenced-images/</link><guid isPermaLink="true">https://brianperry.dev/posts/2021/gatsby-source-drupal-only-referenced-images/</guid><description>&lt;p&gt;We&apos;ve recently started using &lt;a href=&quot;https://www.gatsbyjs.com/&quot;&gt;Gatsby&lt;/a&gt; on &lt;a href=&quot;https://www.bounteous.com/&quot;&gt;Bounteous.com&lt;/a&gt;, my company&apos;s existing Drupal 8 site. Rather than attempting to rebuild the entire front-end at once, we&apos;re starting iteratively with small portions of the site and leaving other sections rendered by Drupal (possibly forever.) This approach posed a few interesting problems, one of which was configuring the &lt;a href=&quot;https://www.gatsbyjs.com/plugins/gatsby-source-drupal/&quot;&gt;gatsby-source-drupal&lt;/a&gt; plugin to only import the content we needed for our build.&lt;/p&gt;
&lt;p&gt;The gatsby-source-drupal plugin pulls data from Drupal’s JSON:API endpoints and makes this data available to React components via Gatsby’s GraphQL API. By default, the plugin imports all data from the source Drupal site. Since for this initial phase Gatsby would only be used to build a small subset of pages, most of this data was unnecessary and also would have the side effect of greatly increasing our build times.&lt;/p&gt;
&lt;p&gt;As an initial attempt to solve this problem, we used Drupal’s &lt;a href=&quot;https://www.drupal.org/project/jsonapi_extras&quot;&gt;JSON:API Extras module&lt;/a&gt; to only expose the resources that our Gatsby build needed to depend on. This helped, but we still eventually needed to enable the file resource, which pretty much immediately sunk our build times. Gatsby was now importing (and worse yet processing) local versions of years worth of images that we didn’t need to support our new content. We eventually found that it was possible to configure gatsby-source-drupal to only import the files referenced by content that was necessary for our builds, but it required a combination of configuration options that wasn’t completely obvious from the documentation.&lt;/p&gt;
&lt;p&gt;The first step was to add the file resource as a &lt;a href=&quot;https://www.gatsbyjs.com/plugins/gatsby-source-drupal/#disallowed-link-types&quot;&gt;disallowed link type&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// In your gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-drupal`,
      options: {
        baseUrl: &amp;lt;your_url&amp;gt;,
        /// Disallow the full files endpoint
        disallowedLinkTypes: [`self`, `describedby`, `file--file`],
      },
    },
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This alone would result in all files being ignored by the plugin. A little bit further on in the disallowed link types documentation is the following note:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When using includes in your JSON:API calls the included data will automatically become available to query, even if the link types are skipped using disallowedLinkTypes. This enables you to fetch only the data you need at build time, instead of all data of a certain entity type or bundle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This essentially allows us to re-allow specific files if they are referenced by other content. What makes this feature potentially easy to miss is the fact that it uses the plugin’s filter option, which typically further restricts the data sourced from the plugin. The resulting configuration ended up looking like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// In your gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-drupal`,
      options: {
        baseUrl: &amp;lt;your_url&amp;gt;,
        /// Disallow the full files endpoint
        disallowedLinkTypes: [`self`, `describedby`, `file--file`],
        filters: {
          // Use includes so only the files associated with our decoupled content
          // types are included.
          &quot;node--decoupled_home_page&quot;: &quot;include=field_dhp_components&quot;,
          &quot;paragraph--dhp_hero&quot;: &quot;include=field_dhp_fg_img&quot;,
          &quot;paragraph--dhp_animation_cards&quot;: &quot;include=field_dhpac_images&quot;,
          &quot;paragraph--featured_post&quot;: &quot;include=field_dfp_bg_img&quot;,
        },
      },
    },
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this configuration, if a featured post paragraph is used on the homepage, any associated background images (field_dfp_bg_img) will be sourced by Gatsby as well. While it does require some maintenance as we incorporate new entities into our builds, it has kept our build times quite reasonable thus far.&lt;/p&gt;
</description><pubDate>Fri, 19 Feb 2021 00:00:00 GMT</pubDate></item><item><title>Creating Multiple Feeds with the 11ty RSS Plugin</title><link>https://brianperry.dev/til/2021/creating-multiple-feeds-with-the-11ty-rss-plugin/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/creating-multiple-feeds-with-the-11ty-rss-plugin/</guid><description>&lt;p&gt;Creating a feed using the &lt;a href=&quot;https://www.11ty.dev/docs/plugins/rss/&quot;&gt;11ty RSS plugin&lt;/a&gt; is pretty straightforward and doesn&apos;t require much configuration. Less obvious to me was how to create a second feed for a filtered subset of my data. In this case I wanted to create a feed of only posts tagged with &apos;Drupal&apos; that can be submitted to &lt;a href=&quot;https://www.drupal.org/planet&quot;&gt;Planet Drupal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since the plugin required so little configuration, it wasn&apos;t clear to me if there was a way to make the plugin aware of a second feed template. As it turns out, the plugin is smart enough to pick up on any number of feed templates, located anywhere in your codebase. I created a &lt;code&gt;/src/_feeds&lt;/code&gt; directory that contained by default &lt;code&gt;feeds.njk&lt;/code&gt; template and my new Drupal-specific &lt;code&gt;drupal.njk&lt;/code&gt; template. As long as you add a unique permalink in this template&apos;s json frontmatter, the plugin will generate a new feed for you. In this case I used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;permalink&quot;: &quot;drupal.xml&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next was determining how to filter my 11ty collection based on a specific tag. This &lt;a href=&quot;https://www.11ty.dev/docs/quicktips/tag-pages/&quot;&gt;quick tip on creating tag pages&lt;/a&gt; gave me what I needed. Instead of iterating through &lt;code&gt;collections.all&lt;/code&gt; as I did in my default feed, I added the following tag filter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{`%- for post in collections[ &apos;drupal&apos; ] | reverse %`}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a result, I had only my content tagged with Drupal. The resulting &lt;code&gt;drupal.njk&lt;/code&gt; feed template looked like this:&lt;/p&gt;
&lt;p&gt;&amp;lt;script src=&quot;https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fbacklineint%2Fbpi-11ty%2Fblob%2Fmaster%2Fsrc%2F_feeds%2Fdrupal.njk&amp;amp;style=default&amp;amp;type=code&amp;amp;showBorder=on&amp;amp;showLineNumbers=on&amp;amp;showFileMeta=on&amp;amp;showFullPath=on&amp;amp;showCopy=on&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p&gt;Now all I have to do is write some Drupal related posts...&lt;/p&gt;
</description><pubDate>Sat, 13 Feb 2021 16:54:34 GMT</pubDate></item><item><title>Hades</title><link>https://brianperry.dev/jams/2021/hades/</link><guid isPermaLink="true">https://brianperry.dev/jams/2021/hades/</guid><description>&lt;p&gt;&lt;a href=&quot;https://www.supergiantgames.com/games/hades/&quot;&gt;Hades&lt;/a&gt; has hooked me in a way that no &lt;a href=&quot;https://en.wikipedia.org/wiki/Roguelike#:~:text=Roguelike%20(or%20rogue%2Dlike),death%20of%20the%20player%20character.&quot;&gt;roguelike&lt;/a&gt; before has. There has been no shortage of virtual ink spilled on &lt;a href=&quot;https://www.polygon.com/22167819/hades-game-of-the-year-2020&quot;&gt;why this game is so great&lt;/a&gt;, but for me it comes down to a few main things: improvisational combat, evolving systems, and story.&lt;/p&gt;
&lt;h4&gt;Improvisational Combat&lt;/h4&gt;
&lt;p&gt;Hades strongly encourages you to play with different loadouts. While I have favorites, I still have fun playing regardless of the weapon I choose at the start of my run. That combined with the insane variety of upgrades you&apos;ll earn, you really won&apos;t know what combat style you&apos;ll be leaning on until it presents itself to you. This makes every run feel surprising and fun.&lt;/p&gt;
&lt;h4&gt;Evolving Systems&lt;/h4&gt;
&lt;p&gt;If someone explained all of the progression systems in Hades to me before I played the game, I would have been turned off by the complexity. But one of the game&apos;s many magic tricks is how it introduces them to you slowly over time, as to not overwhelm the player. In fact, you could argue that the game doesn&apos;t fully open up until you survive your first full run. Never have I been so motivated to keep playing after I &apos;beat&apos; the final boss. And if you don&apos;t understand, or aren&apos;t interested in some of the systems, it&apos;s fine. Don&apos;t feel like playing the fishing mini-game that pops up later on? Hades won&apos;t be mad at you.&lt;/p&gt;
&lt;h4&gt;Story&lt;/h4&gt;
&lt;p&gt;Since you&apos;re repeatedly starting over, roguelikes often don&apos;t have much of a story. Hades on the other hand, reveals the reason for Zagreus&apos; escape from the underworld patiently over many playthroughs. More impressive yet, it does so with a staggering amount of dialogue that never seems to repeat across dozens and dozens of runs. Learning more about the world and your favorite characters can be just as much as a reason to play as leveling up and beating your last run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Protip&lt;/strong&gt;: If you start to feel like you might be stuck and ready to walk away from Hades, don&apos;t be afraid to turn on the God Mode difficulty setting. It adjusts the difficulty over time to meet you where you are, and you can shut it off at any time.&lt;/p&gt;
</description><pubDate>Mon, 01 Feb 2021 00:00:00 GMT</pubDate></item><item><title>Gatsby Debugging in VS Code</title><link>https://brianperry.dev/til/2021/gatsby-debugging-in-vs-code/</link><guid isPermaLink="true">https://brianperry.dev/til/2021/gatsby-debugging-in-vs-code/</guid><description>&lt;p&gt;I&apos;ve actually never had JS debugging working correctly in my Gatsby projects, which has always bugged me. With the help of a few guides today I learned how to change that.&lt;/p&gt;
&lt;p&gt;My initial need was client side debugging, which was covered well in the appropriately named &lt;a href=&quot;https://medium.com/@arthur.rodzkin/how-to-debug-gatsby-js-build-process-and-html-in-vs-code-6d1a31512b5b&quot;&gt;How to debug Gatsby JS in VS Code&lt;/a&gt; The Node debugging instructions in that post didn&apos;t work for me, so I instead used the &lt;a href=&quot;https://www.gatsbyjs.com/docs/debugging-the-build-process/#vs-code-debugger-manual-config&quot;&gt;manual config example from the Gatsby docs&lt;/a&gt; for that.&lt;/p&gt;
&lt;p&gt;In my case, Gatsby was not in the root of my workspace, so I needed to make a few adjustments. For the client side configuration, I needed to change the webRoot to &lt;code&gt;&quot;${workspaceFolder}/gatsby&quot;&lt;/code&gt;. Adjusting the Node configurations were a little trickier. Updating the path to the Gatsby binary alone didn&apos;t work because VS Code was launching the console from the root of the workspace, which wasn&apos;t a Gatsby project. Eventually after &lt;a href=&quot;https://code.visualstudio.com/docs/editor/variables-reference#_environment-variables&quot;&gt;digging around in the VS Code docs&lt;/a&gt; I found that you could set a current working directory option. Adding &lt;code&gt;&quot;cwd&quot;: &quot;${workspaceRoot}/gatsby&quot;&lt;/code&gt; to each of the Node configurations did the trick.&lt;/p&gt;
&lt;p&gt;The resulting launch.json looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [
        {
            &quot;name&quot;: &quot;Gatsby develop&quot;,
            &quot;type&quot;: &quot;pwa-node&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;program&quot;: &quot;${workspaceRoot}/gatsby/node_modules/.bin/gatsby&quot;,
            &quot;args&quot;: [&quot;develop&quot;],
            &quot;runtimeArgs&quot;: [&quot;--nolazy&quot;],
            &quot;console&quot;: &quot;integratedTerminal&quot;,
            &quot;cwd&quot;: &quot;${workspaceRoot}/gatsby&quot;
        },
        {
            &quot;name&quot;: &quot;Gatsby build&quot;,
            &quot;type&quot;: &quot;pwa-node&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;program&quot;: &quot;${workspaceRoot}/gatsby/node_modules/.bin/gatsby&quot;,
            &quot;args&quot;: [&quot;build&quot;],
            &quot;runtimeArgs&quot;: [&quot;--nolazy&quot;],
            &quot;console&quot;: &quot;integratedTerminal&quot;,
            &quot;cwd&quot;: &quot;${workspaceRoot}/gatsby&quot;
        }
        {
            &quot;type&quot;: &quot;chrome&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;name&quot;: &quot;Client side in Chrome&quot;,
            &quot;url&quot;: &quot;http://localhost:8000&quot;,
            &quot;webRoot&quot;: &quot;${workspaceFolder}/gatsby&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
</description><pubDate>Fri, 29 Jan 2021 00:00:00 GMT</pubDate></item></channel></rss>