<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="pretty-atom-feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Cantoni.org</title>
  <subtitle>Software engineering leader and occasional side project hacker</subtitle>
  <link href="https://www.cantoni.org/feed/feed.xml" rel="self" />
  <link href="https://www.cantoni.org/" />
  <updated>2026-02-08T00:00:00Z</updated>
  <id>https://www.cantoni.org/</id>
  <author>
    <name>Brian Cantoni</name>
  </author>
  <entry>
    <title>The Case for &quot;Eventually Consistent&quot; Management</title>
    <link href="https://www.cantoni.org/2026/02/08/eventually-consistent-management/" />
    <updated>2026-02-08T00:00:00Z</updated>
    <id>https://www.cantoni.org/2026/02/08/eventually-consistent-management/</id>
    <content type="html">&lt;p&gt;People often panic when two managers give slightly different answers. The instinctive reaction is &amp;quot;something is wrong&amp;quot; or &amp;quot;we&#39;re disjointed.&amp;quot; But in distributed systems, we accept temporary mismatches as a necessary trade-off for speed—a concept known as eventual consistency. Instead of treating every management inconsistency as a failure, what if we viewed leadership teams like distributed databases that sometimes just experience latency before reaching a unified state?&lt;/p&gt;
&lt;h2 id=&quot;examples&quot;&gt;Examples&lt;/h2&gt;
&lt;p&gt;The simplest examples come when someone asks me or my manager the same question (e.g. status of a project). We might give two different answers if we haven&#39;t synced yet, but that&#39;s fine because we will eventually be consistent. A momentary mismatch in answers isn&#39;t necessarily dysfunction; it&#39;s often just latency. Adopting an eventually consistent mindset lets us keep moving forward even knowing that occasionally we won&#39;t all have the same current information.&lt;/p&gt;
&lt;p&gt;For example, an engineer asks you: &amp;quot;Are we prioritizing X or Y this quarter?&amp;quot; They ask your manager the same question. They get two answers that are &lt;em&gt;directionally aligned&lt;/em&gt; but not identical. Both answers may be correct given the information available at that moment. Neither response is malicious or incompetent. The system is temporarily inconsistent, but still healthy. When in doubt, explicit communication reduces the error rate: &#39;This is my current understanding, but I need to sync with [peer] to confirm.&#39;&lt;/p&gt;
&lt;p&gt;Where does this approach not work? Anywhere you really need to provide accurate answers without speculation or later clarification. (In other words, you need strong consistency.) Two situations that come to mind for me are any customers or internal executives (more than a layer or two above my manager). In those cases, I prefer to get synced on the accurate information to avoid sending people off with the wrong understanding.&lt;/p&gt;
&lt;h2 id=&quot;origin-in-distributed-computing&quot;&gt;Origin in distributed computing&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://en.wikipedia.org/wiki/Eventual_consistency&quot;&gt;Wikipedia&lt;/a&gt; definition is a good starting point:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Eventual consistency is a consistency model used in distributed computing to achieve high availability. An eventually consistent system ensures that if no new updates are made to a given data item, eventually all read accesses to that item will return the last updated value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I first learned about eventual consistency in my last job working at &lt;a href=&quot;https://www.ibm.com/products/datastax&quot;&gt;DataStax&lt;/a&gt;. Our system was built with the distributed database Apache Cassandra which has &lt;a href=&quot;https://medium.com/kocsistem/eventual-consistency-in-apache-cassandra-how-eventual-is-it-really-22287cd79fab&quot;&gt;tunable consistency levels&lt;/a&gt; for reads and writes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Consistency level (CL) determines how many replicas must acknowledge a read or write for it to be considered successful. Higher levels (more replicas in agreement) yield stronger consistency guarantees at the cost of latency, whereas lower levels favor speed and availability at the risk of stale data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a helpful Reddit post from a couple years ago with several good examples: &lt;a href=&quot;https://www.reddit.com/r/learnprogramming/comments/1di3tay/what_is_eventual_consistency_good_for/&quot;&gt;What is eventual consistency good for?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Note: I wrote this post but credit to ChatGPT for good suggestions for illustrating this metaphor.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Why Feature Reviews Matter (and How We Ran Them at Palm)</title>
    <link href="https://www.cantoni.org/2026/01/17/feature-reviews-for-software-projects/" />
    <updated>2026-01-17T00:00:00Z</updated>
    <id>https://www.cantoni.org/2026/01/17/feature-reviews-for-software-projects/</id>
    <content type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;p&gt;At Palm Computing, we worked against hardware manufacturing schedules that made software deadlines absolute. To avoid the dreaded &quot;black box&quot; of development where stakeholders only see the result at the bitter end, we implemented Feature Reviews. These regular check-ins did more than just report status; they forced transparency, mitigated risk, and became a crucial tool I still use with my teams today.&lt;/p&gt;
&lt;h2 id=&quot;origins-at-palm&quot;&gt;Origins at Palm&lt;/h2&gt;
&lt;p&gt;First, a little background on the origin story. [Palm][] was a hardware company which meant most projects were driven by the manufacturing schedule. In this environment, one key learning for software teams like mine was that schedule is &lt;strong&gt;not going to slip&lt;/strong&gt;. It was a good forcing function though; whatever software was ready by the cutoff date, that&#39;s what we shipped with. We could do patches later, but everything really had to be done.&lt;/p&gt;
&lt;p&gt;This meant we were essentially following a waterfall process even for software and even with our attempts to make our project more incremental (pre-agile). Our solution was to add some intermediate progress updates which we&#39;d share across teams and with our engineering and product leadership. We wanted to combat this waterfall idea of a long development time being a &quot;black box&quot; until the result popped out at the end:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/feature-review-timeline-1.svg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It&#39;s a simple concept - just add Feature Reviews as intermediate checkpoints along the way:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/feature-review-timeline-2.svg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;These reviews were useful for bringing together the right set of peers and leaders, to report in a sense &quot;here&#39;s what we&#39;re building.&quot; I remember we fine-tuned them to just cover the most impactful or risky projects, especially those that had both desktop and handheld components. Hosting these reviews regularly also helped in cases where we needed to make course corrections or scope reductions to ensure we would achieve the deadlines.&lt;/p&gt;
&lt;p&gt;Fun fact #1: While writing this up I remembered something about life on a software team at a hardware company. All our project end milestones were &quot;GM&quot; (Gold Master), because our code was literally going to be cut to a master CD-ROM or master device ROM. Everywhere I&#39;ve worked since has really called it &quot;GA&quot; (General Availability).&lt;/p&gt;
&lt;p&gt;Fun fact #2: My team owned the desktop applications including Palm Desktop, and we liked to include easter eggs in the product to list all the people who built it. We even got our names into some official product marketing screenshots!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/palmdesktop.jpg&quot; alt=&quot;Screenshot of Palm Desktop for Windows from around 2000&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-to-run-a-feature-review&quot;&gt;How to run a feature review&lt;/h2&gt;
&lt;p&gt;Even in an agile enterprise software environment like Tableau, feature reviews can still be valuable. Most agile teams already have sprint reviews, but feature reviews can include a broader audience, look at a longer horizon, and be more feature-centric.&lt;/p&gt;
&lt;p&gt;Here&#39;s my suggested outline for holding a feature review, including the audience and what you should cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Owner - Prepared and led by the feature team (engineering &amp;amp; product).&lt;/li&gt;
&lt;li&gt;Audience
&lt;ul&gt;
&lt;li&gt;Stakeholders (including leadership, architects, field)&lt;/li&gt;
&lt;li&gt;Peer and related teams&lt;/li&gt;
&lt;li&gt;Other interested people in the company; looping in those who are interested in what you&#39;re building that you want to keep informed and query for feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Contents
&lt;ul&gt;
&lt;li&gt;What are you building (setting context for those who aren&#39;t familiar)&lt;/li&gt;
&lt;li&gt;What&#39;s the status, show live demos&lt;/li&gt;
&lt;li&gt;What work is remaining (schedule discussion)&lt;/li&gt;
&lt;li&gt;What open questions or potential risks should be discussed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Frequency - This will depend on your overall project timeline, but we found every 4 or 6 weeks to be about right. So out of a roughly 12-week release cycle, we&#39;ll meet 2 or 3 times.&lt;/li&gt;
&lt;li&gt;Logistics - Pay attention to the usual important details for a meeting like this, including advertising in advance, keeping the conversation focused, and capturing recording/slides/actions to share afterwards.&lt;/li&gt;
&lt;li&gt;Outcomes - The team should agree ahead of time whether the purpose is mostly sharing status, or whether there are any specific decisions you need. If the right stakeholders have been included, decisions and adjustments can be made during the review or at a minimum captured for offline resolution.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The review contents will likely change a bit as you make progress. The first review can focus more on requirements and planning, but the big value in future sessions is to show off what has been built so far. For one project I&#39;m leading, our first review was more top-down (requirements, design, dates, who&#39;s working on it, etc.) with the demos at the end. Then we flipped it (bottom-line up-front style) and jumped right into the demos first with project status/logistics to follow.&lt;/p&gt;
&lt;p&gt;As an engineering manager, there&#39;s a bonus for me here which is giving your team the spotlight and encouraging them to participate and drive the demos. Presenting your work is a great skill to have and these feature reviews can be safe spaces to practice.&lt;/p&gt;
&lt;h2 id=&quot;comparison-to-the-movie-business&quot;&gt;Comparison to the movie business&lt;/h2&gt;
&lt;p&gt;I had a random thought comparing this idea of feature reviews for software projects to the &quot;dailies&quot; that are used in the movie business. AI chatbots tended to agree with me, no surprise there!&lt;/p&gt;
&lt;p&gt;Copilot wrote this comparison:&lt;/p&gt;
&lt;p&gt;Feature reviews in software projects and dailies in movie production both serve as regular, structured opportunities to review progress, engage stakeholders, and mitigate risks. The main differences lie in their frequency, scope, and the nature of what&#39;s being reviewed. Feature reviews are broader and less frequent, supporting complex, multi-phase software development. Dailies are immediate and focused, supporting the fast-paced, high-stakes environment of film production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comparison Table: Feature Reviews vs. Dailies&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Feature Reviews (Software Projects)&lt;/th&gt;
&lt;th&gt;Dailies (Movie Production)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Share progress, surface risks, gather feedback, and keep stakeholders informed&lt;/td&gt;
&lt;td&gt;Review footage for technical/artistic quality, spot issues early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Periodic (every 4–6 weeks, or as fits project timeline)&lt;/td&gt;
&lt;td&gt;Daily (after each day of filming)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broad: feature team, leadership, architects, related teams, interested stakeholders&lt;/td&gt;
&lt;td&gt;Core creative/technical team: director, cinematographer, producers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Demos, status updates, remaining work, risks, questions, action items&lt;/td&gt;
&lt;td&gt;Raw, unedited footage from the day’s shoot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Holistic project status and cross-team communication&lt;/td&gt;
&lt;td&gt;Immediate technical and creative review of filmed material&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Meetings with presentations, live demos, Q&amp;amp;A, and documentation&lt;/td&gt;
&lt;td&gt;Screening sessions, often informal, focused on viewing footage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Outcome&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Feedback, action items, risk mitigation, improved transparency&lt;/td&gt;
&lt;td&gt;Decisions on reshoots, technical fixes, or creative adjustments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skill Development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Opportunity for team members to practice presenting and communicating&lt;/td&gt;
&lt;td&gt;Opportunity for crew to refine filming techniques and performance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;br&gt;
And ChatGPT nailed it with this generated visual comparison ☺
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/feature-reviews-movie-dailies.png&quot; alt=&quot;ChatGPT comparison between movie production dailies and software project demos &quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Palm,_Inc.&quot;&gt;Palm&lt;/a&gt;&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;</content>
  </entry>
  <entry>
    <title>A Modern Manager for Browser Bookmarklets</title>
    <link href="https://www.cantoni.org/2025/12/30/modern-browser-bookmarklets/" />
    <updated>2025-12-30T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/12/30/modern-browser-bookmarklets/</id>
    <content type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;p&gt;While often overshadowed by full browser extensions, bookmarklets remain a powerful, privacy-friendly tool for automating repetitive web tasks. However, writing and formatting the necessary JavaScript can be tedious. I recently built a simple bookmarklet manager — using AI to speed up the process — that makes creating, identifying, and sharing these scripts much easier.&lt;/p&gt;
&lt;h2 id=&quot;what-is-a-bookmarklet&quot;&gt;What is a bookmarklet?&lt;/h2&gt;
&lt;p&gt;First, what exactly is a bookmarklet? It&#39;s a browser bookmark that runs a small bit of JavaScript code rather than containing a web link. The code will run in the context of your current browser page and can be useful for reading or modifying that content. Bookmarklets need to be formatted a specific way to safely run when clicked.&lt;/p&gt;
&lt;p&gt;For example, you might have a simple code snippet that displays a pop-up dialog:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;alert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Hello, world!&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It needs to be formatted as a single-line function and properly encoded like this:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token literal-property property&quot;&gt;javascript&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Balert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Hello%2C%20world!&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;3B&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;7D&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next you create a browser bookmark using a title like &quot;Hello&quot; and the location set to the formatted bookmarklet code string. Now clicking on the &quot;Hello&quot; bookmark will execute the code you defined. (In this example, a simple pop-up.)&lt;/p&gt;
&lt;p&gt;How are bookmarklets useful? Here are a few good ideas I&#39;ve used myself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copy page link in markdown format (probably my #1).&lt;/li&gt;
&lt;li&gt;Copy selected text as Markdown quote.&lt;/li&gt;
&lt;li&gt;Tweet this (send this link to the Twitter/X interface to share it; similar for LinkedIn or any social network).&lt;/li&gt;
&lt;li&gt;Highlight all links (outlines &lt;a&gt; tags to see what’s clickable).&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Show all images with alt text (flags missing or empty alt attributes).&lt;/li&gt;
&lt;li&gt;Disable all CSS (removes stylesheets to inspect raw HTML).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are some more detailed explanations of bookmarklets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/ahandsel/introduction-to-bookmarklets-javascript-everywhere-280m&quot;&gt;Introduction to Bookmarklets: JavaScript Everywhere - DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Bookmarklet&quot;&gt;Bookmarklet - Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freecodecamp.org/news/what-are-bookmarklets/&quot;&gt;What are Bookmarklets? How to Use JavaScript to Make a Bookmarklet in Chromium and Firefox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An important note about bookmarklets: like browser extensions, they have access to everything in your browser. Don&#39;t install anything without reading and understanding the code yourself to make sure it&#39;s safe.&lt;/p&gt;
&lt;h2 id=&quot;bookmarklet-manager&quot;&gt;Bookmarklet manager&lt;/h2&gt;
&lt;p&gt;As you can see from the example above, JavaScript needs to be converted into the proper format to be used as a bookmarklet. For anything beyond simple examples, this is kind of a pain. Inspired by some online tools like &lt;a href=&quot;https://caiorss.github.io/bookmarklet-maker/&quot;&gt;Bookmarklet Maker&lt;/a&gt;, I decided to build my own manager. I wanted a simple editor to convert between plain JavaScript and bookmarklet format, and to save a list of bookmarklets I&#39;ve made.&lt;/p&gt;
&lt;p&gt;Here&#39;s the result:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Live site: &lt;a href=&quot;https://bcantoni.github.io/bookmarklets/&quot;&gt;https://bcantoni.github.io/bookmarklets/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub source: &lt;a href=&quot;https://github.com/bcantoni/bookmarklets&quot;&gt;https://github.com/bcantoni/bookmarklets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Just like my last project, I used Claude Code (with Opus 4.5) and followed a spec-driven process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I wrote the initial &lt;a href=&quot;https://github.com/bcantoni/bookmarklets/blob/main/SPEC.md&quot;&gt;SPEC.md&lt;/a&gt; with the basic concept and some specific functional requirements I had in mind.&lt;/li&gt;
&lt;li&gt;I prompted Claude to review the spec, give suggestions, clarify the requirements, and update the document.&lt;/li&gt;
&lt;li&gt;I gave Claude the go signal and the initial working version was ready in a couple minutes!&lt;/li&gt;
&lt;li&gt;I tested and iterated on a few tweaks to get to the first release.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I was impressed with the simple UI design that Claude implemented. (I had not given any specifics about the design.) And the solution worked on the first iteration. I suggested using local browser storage but Claude suggested adding the import/export as JSON which is really handy for sharing across multiple computers.&lt;/p&gt;
&lt;p&gt;The manager can generate bookmarklets from JavaScript code, load or save them, and provides a drag-and-drop link to easily install them into your browser.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/bookmarklet-manager.png&quot; alt=&quot;Bookmarklet manager screenshot&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;can-ai-help-you-create-bookmarklets&quot;&gt;Can AI help you create bookmarklets?&lt;/h2&gt;
&lt;p&gt;You can probably guess the answer is Yes. AI tools know about browser bookmarklets. You can prompt an AI like Claude or ChatGPT with the details of what you want and then copying and pasting the resulting code into the bookmarklet manager.&lt;/p&gt;
&lt;p&gt;Here are a couple examples:&lt;/p&gt;
&lt;p&gt;Prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create javascript I could use in a bookmarklet that found all headings
(h1, h2, etc) on a page and reversed the characters in the string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;h1,h2,h3,h4,h5,h6&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  h&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; h&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;&#39;).reverse().join(&#39;&lt;/span&gt;&#92;&#39;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create a javascript bookmarklet to copy the web page title and URL and 
put them in the clipboard; display a toast near the page top (centered)
for 2 seconds saying &#92;&quot;markdown link copied&#92;&quot;`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Build the Markdown link&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; markdownLink &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;](&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;href&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Copy to clipboard&lt;/span&gt;
navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;clipboard&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;markdownLink&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Create toast element&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; toast &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;div&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  toast&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;markdown link copied&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Style the toast&lt;/span&gt;
  Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;toast&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fixed&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;20px&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;50%&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;translateX(-50%)&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;10px 14px&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;#333&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;#fff&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;fontSize&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;14px&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;borderRadius&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;6px&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;zIndex&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;999999&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;boxShadow&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;0 2px 6px rgba(0,0,0,.3)&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;0&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;opacity 0.2s&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Add to page&lt;/span&gt;
  document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;toast&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Fade in&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;requestAnimationFrame&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    toast&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;opacity &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;1&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Fade out after 2 seconds, then remove&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    toast&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;opacity &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;0&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      toast&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the future it might be nice to integrate an AI agent into the bookmarklet manager where the result could be dropped directly into the code editor. But for now, a little tabbing back and forth is still pretty easy.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;</content>
  </entry>
  <entry>
    <title>My First Electron App: ImageLinks</title>
    <link href="https://www.cantoni.org/2025/12/29/my-first-electron-app-imagelinks/" />
    <updated>2025-12-29T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/12/29/my-first-electron-app-imagelinks/</id>
    <content type="html">&lt;p&gt;I wanted a simple app to extract links from screenshots and used Claude Code to create a simple desktop application that did exactly that. This was my first time creating anything with the Electron cross-platform app platform and it worked quite well.&lt;/p&gt;
&lt;p&gt;My idea originated at work where I spend a lot of time in meetings and often people will be presenting Tableau dashboards or other documents without remembering to share the links. My usual workaround is to take a quick screenshot so I can find that content later. Instead of doing that manually, what if I had a Mac app that could analyze an image and show all the web links automatically?&lt;/p&gt;
&lt;p&gt;See my ImageLinks project at &lt;a href=&quot;https://github.com/bcantoni/imagelinks&quot;&gt;https://github.com/bcantoni/imagelinks&lt;/a&gt; to see all the source code and give it a try.&lt;/p&gt;
&lt;p&gt;For this project I wanted to try Claude Code to build something from scratch and using something I wasn&#39;t already familiar with -- in this case, the &lt;a href=&quot;https://www.electronjs.org/&quot;&gt;Electron platform&lt;/a&gt;. I started with a basic spec for Claude to follow and iterated on that with Claude&#39;s help. These were the key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Two modes of operation: drag and drop an image file, or use the image from the clipboard.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Supported platforms: Mac, Windows, Linux (getting Mac working as the first priority).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the image, look for URLs and QR codes (which should be decoded).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Technology: use electron and electron-builder; use only popular/common Node libraries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Started with 5 test images and expected results.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After some quick iterations I had the final &lt;a href=&quot;https://github.com/bcantoni/imagelinks/blob/main/SPEC.md&quot;&gt;SPEC.md&lt;/a&gt; ready and I prompted Claude Code to build it per the spec. ☺&lt;/p&gt;
&lt;p&gt;Claude quickly got to a working application, and we iterated on a few specific areas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Adding to the library of tests for different combinations of URLs and QR codes and some lower resolution difficult tests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Improving the speed for the image detection code. The original version from Claude was good but it was too complex. For example, it created all 4 rotated versions of an image and ran each through the image detection loop. My use case is simpler, and everything is normally in the upright orientation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Improving the detection quality for images. This was some iteration with more complicated test images and adjusting the algorithm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Creating GitHub workflows for CI builds and an automated release process.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Creating the project &lt;a href=&quot;https://github.com/bcantoni/imagelinks/blob/main/README.md&quot;&gt;README.md&lt;/a&gt; which includes a summary of the app, how to install, and developer notes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Overall, this simple project was a success, and I do use it on my work laptop. In cases where I have an image that doesn&#39;t come through correctly, I can add it as a test with the expected result and just ask Claude Code to fix the detection. I&#39;ve tested and used both the Mac and Windows installers, and everything just works. The only tricky part on the Mac side is these are &amp;quot;unsigned,&amp;quot; so I need to remove the Apple quarantine attribute after install. (The command is: &lt;code&gt;xattr -cr /Applications/ImageLinks.app&lt;/code&gt;.) If I ever have other reasons for a paid Mac developer account, I could officially sign these.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Downsizing My Flickr Library to the Free Tier</title>
    <link href="https://www.cantoni.org/2025/12/12/downsizing-flickr-library-free-tier/" />
    <updated>2025-12-12T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/12/12/downsizing-flickr-library-free-tier/</id>
    <content type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;p&gt;My Flickr photos account was a fun snapshot in time when I worked at Yahoo and before mobile phone cameras took over. This week I used AI to create a couple of scripts to help me get back down to the free account limit. Here&#39;s a quick explanation of the process I followed along with a few Flickr memories.&lt;/p&gt;
&lt;h2 id=&quot;my-time-with-flickr&quot;&gt;My time with Flickr&lt;/h2&gt;
&lt;p&gt;I started using &lt;a href=&quot;https://www.flickr.com/&quot;&gt;Flickr&lt;/a&gt; in late 2004 which was just prior to their acquisition by Yahoo in spring 2005. (I worked at Yahoo - not on Flickr - from 2004 to 2011).&lt;/p&gt;
&lt;p&gt;Here&#39;s a summary dashboard of my Flickr uploads (courtesy of Claude):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/flickr-journey.png&quot; alt=&quot;Bar graph showing Flickr photo uploads by year from 2005 through 2017; 2007 was the peak&quot;&gt;&lt;/p&gt;
&lt;p&gt;I think I had used other photo sharing sites like Yahoo! Photos and later Google Photos, but Flickr was great for several reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All of the social aspects were there including groups to join, people to follow, photos to favorite and comment on&lt;/li&gt;
&lt;li&gt;It had simple but useful controls for posting family or private photos&lt;/li&gt;
&lt;li&gt;Tags were quite popular&lt;/li&gt;
&lt;li&gt;Search combined with &lt;a href=&quot;https://www.flickrhelp.com/hc/en-us/articles/40048459562388-Copyright-Licenses-on-Flickr&quot;&gt;content licensing&lt;/a&gt; made it easy for people to use your photos in different contexts&lt;/li&gt;
&lt;li&gt;It was the first site I recall that clearly showed the camera model and settings front and center&lt;/li&gt;
&lt;li&gt;It was part of Yahoo :)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was my first exposure to the Creative Commons license. I marked all my public photos with the permissive &lt;a href=&quot;https://creativecommons.org/licenses/by/4.0/&quot;&gt;CC BY&lt;/a&gt; license which in summary means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This license enables reusers to distribute, remix, adapt, and build upon the material in any medium or format, so long as attribution is given to the creator. The license allows for commercial use. CC BY includes the following elements: Credit must be given to the creator.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With the CC license in place, it was fun to see all the places my photos would be used. I have a bunch that people have used on Wikipedia, including this one on &lt;a href=&quot;https://en.wikipedia.org/wiki/Taylor_Swift&quot;&gt;Taylor Swift&#39;s page&lt;/a&gt; visiting the Yahoo campus in 2007 promoting her debut album (&lt;a href=&quot;https://www.flickr.com/photos/cantoni/albums/72157600222365822/&quot;&gt;full album on Flickr&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/taylor-wikipedia-yahoo.png&quot; alt=&quot;Photo of Taylor Swift visiting Yahoo in 2007, Wikipedia&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here are a few more fun ones: a remote with the Netflix button (my #1 most viewed photo), my 1997 Dodge Ram just after I got it repainted, and racer Robby Gordon signing an autograph for my wife at a Phoenix race:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/netflix-button.png&quot; alt=&quot;TV remote with Red Netflix button, Wikipedia&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/dodge-ram.png&quot; alt=&quot;1997 Dodge Ram photo, Wikipedia&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/robby-gordon.png&quot; alt=&quot;Robby Gordon signing autographs at NASCAR in Phoenix&quot;&gt;&lt;/p&gt;
&lt;p&gt;A long time ago I set up a Google Alert for my name and when people used an image and properly gave me credit, I&#39;d get a hit. This Coast Guard boat was a popular one for news articles describing any kind of rescue:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/coast-guard-boat.jpg&quot; alt=&quot;Coast Guard boat on San Francisco Bay&quot;&gt;&lt;/p&gt;
&lt;p&gt;I even had a few people contact me for specific permission, including my Scantron image being included in Scott Berkun&#39;s &lt;a href=&quot;https://designmtw.com/&quot;&gt;How Design Makes the World&lt;/a&gt; book which came out in 2020:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/scott-berkun-dmtw.png&quot; alt=&quot;Screenshot from Design Makes the World Book, featuring my photo of a Scantron test form&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;flickr-download-scripts&quot;&gt;Flickr download scripts&lt;/h2&gt;
&lt;p&gt;Now to today&#39;s problem: I had a Flickr paid (Pro) plan for quite a while but dropped back to Free at some point when I was less active. Since the Free plan limits were &lt;a href=&quot;https://www.flickr.com/&quot;&gt;tightened up in 2022&lt;/a&gt;, I&#39;ve been getting periodic reminder emails reminding me I was over the limit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reminder: Some of your photos are at risk of deletion.&lt;/p&gt;
&lt;p&gt;This is a reminder that your account is still in violation of Flickr’s &lt;a href=&quot;https://blog.flickr.net/en/2022/04/19/update-free-account-limit-changes-and-enforcement-start-may-1-2022/&quot;&gt;free account limits&lt;/a&gt;. Photo deletions are underway, so now is the time to act to protect your photos.&lt;/p&gt;
&lt;p&gt;As of November 3, 2025 you have 1393 uploads to your free account. The current limit is 1,000 uploads for free accounts. As soon as possible, please subscribe to Flickr Pro OR download excess non-public photos and remove them from your Flickr account&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Free account limits I was exceeding were: 1) having more than 50 non-public photos; and 2) more than 1000 photos in total. The new pricing structure seems fair to me. The Pro level account is a pretty good deal if you use it a lot - currently $82/year for unlimited photos and a bunch of other benefits. The 1000 photo limit is still a lot, and in spite of the warning emails I never did lose any of my content. But since I really don&#39;t use the site much any more, it made sense to prune my uploaded photos, including all the private and friends/family ones.&lt;/p&gt;
&lt;p&gt;Earlier this year Flickr changed the API key policies such that only Pro accounts can create and use API keys. My workaround was to raise my subscription from Free to Pro for a month (similar to what I needed to do when closing out my Evernote free account).&lt;/p&gt;
&lt;p&gt;With Claude Code ready to help, I created a set of scripts that use the &lt;a href=&quot;https://www.flickr.com/services/api/&quot;&gt;Flickr API&lt;/a&gt; to help manage my content:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backup - download a mirror backup of all photos, including photo metadata and organized into album folders&lt;/li&gt;
&lt;li&gt;Stats - find and view the most popular photos&lt;/li&gt;
&lt;li&gt;Stats CSV - download all the interesting metadata for all photos (this is what I used to create the dashboard at the top of the post)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For all of these scripts, Claude Code did quite well. I really didn&#39;t make any edits myself but just relied on prompting to adjust the scripts including adding a retry mechanism and gracefully handling rate-limiting responses.&lt;/p&gt;
&lt;p&gt;Once I had everything backed up, I used the Flickr Batch Organize feature to remove all the private albums and prune a bunch of less interesting photos. The Organize feature is kind of old school like most of Flickr, but it works quite well.&lt;/p&gt;
&lt;p&gt;The result: I backed up my whole photostream, removed all my non-public images, and reduced the overall count from 1400 down to 750 (all under the Free limit). Next I returned my account to the Free level and now I&#39;m all set!&lt;/p&gt;
&lt;p&gt;All the source code for this project is on GitHub: &lt;a href=&quot;https://github.com/bcantoni/flickrbackup&quot;&gt;bcantoni/flickrbackup&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;an-easier-way&quot;&gt;An easier way&lt;/h2&gt;
&lt;p&gt;Part way through this project I realized / remembered that Flickr has support for viewing and downloads stats and for downloading all your photos.&lt;/p&gt;
&lt;p&gt;This was still a fun experiment to build a script that uses the API directly. If you were an active Flickr user you could automate a lot of your photo workflows with the API.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;</content>
  </entry>
  <entry>
    <title>Vibe Coding a Website on my Phone Waiting at the Barbershop</title>
    <link href="https://www.cantoni.org/2025/11/02/vibe-coding-website-phone-barbershop/" />
    <updated>2025-11-02T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/11/02/vibe-coding-website-phone-barbershop/</id>
    <content type="html">&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;p&gt;Is it possible to &quot;vibe code&quot; a website from my phone while waiting for my turn at the barbershop? The answer as you might guess is yes! I recently created an event countdown website completely from my phone (no Wi-Fi) and got it launched in the 45 minutes while I waited for my turn in the chair!&lt;/p&gt;
&lt;p&gt;I&#39;ve been wanting to create an &quot;event countdown&quot; style website for an upcoming wedding and picked it as a good simple target to see if everything could be done from the phone. While this is not ground-breaking, several components are involved, including GitHub Pages for hosting, my domain provider for the domain name, GitHub Copilot for the code repository and AI help. Also my current barber does not take appointments, so on a recent Friday I was 3rd in line, so I had about 45 minutes to crank this out.&lt;/p&gt;
&lt;p&gt;The plan worked!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/event-countdown-website.png&quot; alt=&quot;Event countdown website showing the time remaining in a countdown format, laid on top a photo of Lake Tahoe&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, how did we get here? This is everything I used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Mobile&lt;/strong&gt; This is actually quite powerful especially for the most common tasks including managing issues, pull requests, and code changes. I did need to log in to github.com for a couple things.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ChatGPT Mobile&lt;/strong&gt; Worked great, no notes!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unsplash&lt;/strong&gt; I&#39;m pretty sure they have a mobile app but I just used the website.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS provider&lt;/strong&gt; No mobile app, but the website worked well enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is the rough sequence of steps I went through, including just the most interesting bits:&lt;/p&gt;
&lt;p&gt;First I registered the domain name with my existing provider. Then in GitHub mobile I create the project repo with the initial commit and CNAME file. I started here just to make sure the new domain name had time to percolate across the internet a bit.&lt;/p&gt;
&lt;p&gt;Next to the key step where AI was helpful: using ChatCPT with a single prompt to create the countdown website (just a single HTML file):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.cantoni.org/images/event-countdown-chatgpt.png&quot; alt=&quot;Prompting ChatGPT mobile to create a website&quot;&gt;&lt;/p&gt;
&lt;p&gt;This was the text of the prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How to create a fun event countdown web site with a big months, day, hour, minute, second countdown floating on top of a background image that rotates periodically?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(Writing this post now, I notice I started with &quot;How to create...&quot; rather than a more imperative like &quot;Create...&quot;. That&#39;s probably a nuance that doesn&#39;t matter; ChatGPT answered my question by doing the thing anyways.)&lt;/p&gt;
&lt;p&gt;I brought that index.html file into the GitHub app, adjusted the event title and date/time, and added a few Lake Tahoe image links from &lt;a href=&quot;https://unsplash.com&quot;&gt;Unsplash&lt;/a&gt;. I also had to adjust to make sure the event and countdown were in Pacific time, and ChatGPT helped with me a code snippet for that.&lt;/p&gt;
&lt;p&gt;Next I found a couple photos from my phone I wanted to include. I tried Copilot inside GitHub mobile and it worked very well; it created a pull request which I reviewed and merged.&lt;/p&gt;
&lt;p&gt;The final step was turning on GitHub Pages for this domain and making sure everything worked. (This was the trickiest part of this whole experiment.) For most of this I had to log in to github.com because these settings aren&#39;t available in the mobile app. After a few rounds of custom DNS and GitHub settings, it worked! Even the custom domain SSL support worked after a short time.&lt;/p&gt;
&lt;p&gt;So in the end, can you create a website while waiting for your barber? Yes of course.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;</content>
  </entry>
  <entry>
    <title>Simple Lossless Image Compression</title>
    <link href="https://www.cantoni.org/2025/10/14/simple-lossless-image-compression/" />
    <updated>2025-10-14T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/10/14/simple-lossless-image-compression/</id>
    <content type="html">&lt;p&gt;During the migration from WordPress to Eleventy, I &lt;a href=&quot;https://www.cantoni.org/2025/08/18/solving-my-image-dimension-problem-with-an-eleventy-transform/&quot;&gt;decided against the Eleventy standard image transform plugin&lt;/a&gt; and decided to just implement the pieces I needed. One remaining piece was automating the compression of images for better web performance. I usually prepare images before adding to the blog by adjusting to the right size and image type. That means in this script I&#39;m sticking with lossless compression to save a few more bytes.&lt;/p&gt;
&lt;p&gt;Because this blog system is built by JavaScript, I started by implementing the &lt;a href=&quot;https://github.com/imagemin/imagemin&quot;&gt;imagemin&lt;/a&gt; package and plugins. The script was quickly put together by Copilot and gave great results. However, I noticed the additional npm package dependencies contained quite a few security vulnerabilities. It turns out that much of the imagemin ecosystem is quite old.&lt;/p&gt;
&lt;p&gt;Next I switched to &lt;a href=&quot;https://imagemagick.org/script/magick.php&quot;&gt;ImageMagick&lt;/a&gt; and set up to run locally just on my Mac and just on command. (I might try something more automated in the future). With a little research and a few trial runs, I settled on some good settings for the types of PNG and JPEG files I have on the blog:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;magick input.jpg &lt;span class=&quot;token parameter variable&quot;&gt;-quality&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;85&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-strip&lt;/span&gt; output.jpg
magick input.png &lt;span class=&quot;token parameter variable&quot;&gt;-quality&lt;/span&gt; 00 &lt;span class=&quot;token parameter variable&quot;&gt;-strip&lt;/span&gt; output.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JPEG files optimized: 85/126&lt;/li&gt;
&lt;li&gt;PNG files optimized: 158/273&lt;/li&gt;
&lt;li&gt;Total files optimized: 243/399&lt;/li&gt;
&lt;li&gt;Original total size: 25.0 MB&lt;/li&gt;
&lt;li&gt;Optimized total size: 22.8 MB&lt;/li&gt;
&lt;li&gt;Total bytes saved: 2.1 MB (8.6%)&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Solving my Image Dimension Problem with an Eleventy Transform</title>
    <link href="https://www.cantoni.org/2025/08/18/solving-my-image-dimension-problem-with-an-eleventy-transform/" />
    <updated>2025-08-18T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/08/18/solving-my-image-dimension-problem-with-an-eleventy-transform/</id>
    <content type="html">&lt;p&gt;When migrating from WordPress to Eleventy, I wanted to automatically add width and height attributes to my images for better performance and layout stability. The official Eleventy image plugin felt overcomplicated for my needs, so I built a simple transform that inspects image files and adds dimensions to HTML img tags during the build process.&lt;/p&gt;
&lt;h2 id=&quot;eleventy-image-handling&quot;&gt;Eleventy Image Handling&lt;/h2&gt;
&lt;p&gt;When I migrated this site from WordPress to Eleventy, the &lt;a href=&quot;https://www.11ty.dev/docs/plugins/image/&quot;&gt;image transform plugin&lt;/a&gt; was already included in the starter blog template I cloned. This plugin does a lot including outputting multiple sizes and formats. I think it was doing too much for what I needed. I really struggled trying to bring over my WordPress posts and images in a way that retained the old links. Even referencing a local image from Markdown wasn&#39;t working. I decided to pull out the 11ty image transform and manage images myself.&lt;/p&gt;
&lt;p&gt;I wanted to start with automatically adding height and width attributes, so this is what I tried:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Image plugin -- too complicated, removed&lt;/li&gt;
&lt;li&gt;Inspect the image plugin source to possibly copy just the height/width code -- too complicated&lt;/li&gt;
&lt;li&gt;Write my own plugin -- seems doable but probably overkill&lt;/li&gt;
&lt;li&gt;Write a &lt;a href=&quot;https://www.11ty.dev/docs/transforms/&quot;&gt;transform&lt;/a&gt; -- this is starting to look the best path!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Eleventy transforms are run late in the build cycle (&amp;quot;postprocessing&amp;quot;) and allow any transformations of the built page content. I had a good outline of how I would approach writing this, so I wrote a short spec for Copilot to help. I should have saved that prompt, but I could not get this code to work even with a bunch of adjustments and iterations I made.&lt;/p&gt;
&lt;p&gt;After an overnight break I decided to go up a level and just ask Copilot for what I wanted :) This was the simple prompt, focusing just on the outcome:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;write an eleventy transform that will automatically add height and width attributes to any image found in the HTML source&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This worked and with only minimal adjustments got me exactly what I needed!&lt;/p&gt;
&lt;h2 id=&quot;image-dimensions-transform&quot;&gt;Image Dimensions Transform&lt;/h2&gt;
&lt;p&gt;This is my solution for adding height and width attributes to image tags in the HTML output files. A few quick notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This transform examines the DOM to find all the images, inspect the image file to find the height and width, then add those attributes to the image element&lt;/li&gt;
&lt;li&gt;We&#39;re only interested in .html output files&lt;/li&gt;
&lt;li&gt;If the page doesn&#39;t have any images, skip it (importantly for transforms: they need to just return the original unmodified content)&lt;/li&gt;
&lt;li&gt;Also skip any images hosted elsewhere (we&#39;re only processing local images)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To use this script, include this snippet in your &lt;code&gt;eleventy.config.js&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Eleventy transform to add width and height to &amp;lt;img&gt; tags
 */&lt;/span&gt;
eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addTransform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;img-dimensions&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; outputPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;outputPath &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.html&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dom &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;JSDOM&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; imgs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; dom&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;img[src]:not([width]):not([height])&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// If no images, return the original content&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imgs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; img &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; imgs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; src &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; img&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;src&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;http&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Skip remote images&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// Remove leading slash if present&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; imgPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; src&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^&#92;/&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; filePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;./public/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;imgPath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; buffer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;promisify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;readFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; dimensions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;imageSize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;buffer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dimensions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; dimensions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          img&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;width&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dimensions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          img&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dimensions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Error processing image &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;img&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; dom&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also add these imports at the top of &lt;code&gt;eleventy.config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; promisify &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;util&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; readFile &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; imageSize &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image-size&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSDOM&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;jsdom&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, install the new library dependencies:&lt;/p&gt;
&lt;pre class=&quot;language-shell&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; jsdom image-size&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;example&quot;&gt;Example&lt;/h2&gt;
&lt;p&gt;Here&#39;s an example from my &lt;a href=&quot;https://www.cantoni.org/2025/01/21/summarizing-youtube-videos-with-llms/&quot;&gt;Summarizing YouTube Videos with LLMs post&lt;/a&gt;. In the Markdown source file, the image is included with this markup:&lt;/p&gt;
&lt;pre class=&quot;language-md&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-md&quot;&gt;&lt;span class=&quot;token url&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;[&lt;span class=&quot;token content&quot;&gt;Thumbnail for the Honey Influencer Scam YouTube video&lt;/span&gt;](&lt;span class=&quot;token url&quot;&gt;/images/honey-scam-thumbnail.jpg&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the build process and the image dimensions transform, the HTML has the alt text preserved and the newly calculated width and height attributes added:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/images/honey-scam-thumbnail.jpg&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Thumbnail for the Honey Influencer Scam YouTube video&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;300&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;180&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;One thing I need to look at is the build time. It&#39;s not doing any caching, so each build needs to process the 400+ images. Right now my img-dimensions is taking up 23% of the time which isn&#39;t horrible but could be better:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[11ty] Benchmark   2668ms  23%   706× (Configuration) &amp;quot;img-dimensions&amp;quot; Transform
[11ty] Benchmark   5770ms  50%   661× (Configuration) &amp;quot;gitLastModified&amp;quot; Nunjucks Filter
[11ty] Copied 453 Wrote 696 files in 11.48 seconds (16.5ms each, v3.0.0)
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>How to Add Playwright Automated Tests to an Eleventy Static Blog</title>
    <link href="https://www.cantoni.org/2025/08/17/how-to-add-playwright-automated-tests-to-an-eleventy-static-blog/" />
    <updated>2025-08-17T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/08/17/how-to-add-playwright-automated-tests-to-an-eleventy-static-blog/</id>
    <content type="html">&lt;p&gt;Now that this blog has moved to the Eleventy system, I wanted to add some simple smoke tests to make sure the build process is working correctly and catch me if I break anything. I decided to go with &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt; because we use it at work and it&#39;s been on my to-learn list. Everything worked well, and I achieved good test coverage with 10 different tests, each containing multiple assertions. The trickiest part was making it all run correctly in the GitHub Actions pipelines.&lt;/p&gt;
&lt;h2 id=&quot;learning-playwright&quot;&gt;Learning Playwright&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://playwright.dev/docs/writing-tests&quot;&gt;Playwright docs&lt;/a&gt; are very good so I started there. Getting set up, writing some initial tests, learning about &lt;a href=&quot;https://playwright.dev/docs/test-assertions&quot;&gt;assertions&lt;/a&gt; and &lt;a href=&quot;https://playwright.dev/docs/locators&quot;&gt;locators&lt;/a&gt; were my starting points.&lt;/p&gt;
&lt;p&gt;Once I had the basic structure running, I created an outline of the tests I wanted to create. These are all pretty simple for this static site, with a focus on making sure everything was built correctly.&lt;/p&gt;
&lt;p&gt;For each test I dropped the outline into a code comment like this:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/*
test home page
- go to home page
- confirm &amp;lt;title&gt; is set to &quot;Brian Cantoni&quot;
- confirm &amp;lt;h1&gt; heading is set to &quot;Brian Cantoni&quot;
- confirm exactly 10 blog posts are listed
*/&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I would select the comment and ask Copilot to implement it. It did very well at this, particularly when using Agent mode and asking it to implement the test and then run it, iterating until it worked.&lt;/p&gt;
&lt;p&gt;From the above outline, Copilot and I created the Playwright test:&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Home page&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; page &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveTitle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;Brian Cantoni&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getByRole&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;heading&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Brian Cantoni&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toBeVisible&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;locator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;li.postlist-item&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toHaveCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach worked well unless I tried creating and selecting multiple comment blocks like this. Copilot got confused and started adding code in random locations in the JavaScript file and then reported the file as &amp;quot;corrupted&amp;quot; :) Another good reminder for having frequent git commits of your work in progress.&lt;/p&gt;
&lt;h2 id=&quot;playwright-with-netlify-preview-builds&quot;&gt;Playwright with Netlify Preview Builds&lt;/h2&gt;
&lt;p&gt;Beyond running tests locally, I wanted them to run against the Netlify preview builds as well. I have &lt;a href=&quot;https://docs.netlify.com/deploy/deploy-types/deploy-previews/&quot;&gt;Netlify deploy previews&lt;/a&gt; configured against my private GitHub repo and every pull request creates a unique &amp;quot;preview&amp;quot; build that goes with it. I&#39;m not using Netlify for the production build, but these preview builds are very helpful especially for checking any tricky changes.&lt;/p&gt;
&lt;p&gt;To run tests against the preview build, the GitHub action workflow needs to wait for the preview deploy to finish before running the tests. Luckily there are a few published actions that do this, including &lt;a href=&quot;https://github.com/JakePartusch/wait-for-netlify-action&quot; title=&quot;A GitHub action that will wait until a Netlify Preview deploy has completed before continuing on&quot;&gt;JakePartusch/wait-for-netlify-action&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Incorporating this as an action step is quite simple:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Wait for Netlify Deploy Preview
  &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; waitForDeployPreview
  &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; jakepartusch/wait&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;for&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;netlify&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v1.4&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then a later step uses the unique preview URL to run the tests:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Run Playwright tests
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npx playwright test
  &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PLAYWRIGHT_BASE_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the end it&#39;s a clean solution which works well, but it took &lt;em&gt;so many&lt;/em&gt; incremental pushes to test the actions, it was frustrating. It didn&#39;t help that I trusted some Google AI search results which gave me a .yml workflow file that looked great but was far from a working example.&lt;/p&gt;
&lt;p&gt;One thing I learned that really helped: Copilot very good at reviewing a workflow file and either pointing out improvements or actual problems (some of which were just dumb mistakes on my part).&lt;/p&gt;
&lt;h2 id=&quot;playwright-with-github-build&quot;&gt;Playwright with GitHub Build&lt;/h2&gt;
&lt;p&gt;Once I got the Netlify preview tests running, I realized I should also handle the simpler case of just building and testing on GitHub.&lt;/p&gt;
&lt;p&gt;Doing this doesn&#39;t require any custom actions. Instead, it&#39;s just a sequence of starting the local server, waiting for that server to finish, and then running the tests:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Start local server for Playwright
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npx http&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server _site &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;p 8080 &amp;amp;

&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Wait for server to start
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
    timeout 30 bash -c &#39;until curl -f http://localhost:8080; do sleep 1; done&#39;
    echo &quot;Server is ready&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Run Playwright tests
  &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; npx playwright test
  &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;PLAYWRIGHT_BASE_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//localhost&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8080&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;Right now, my smoke test coverage is pretty good and should catch any build issues. Also, now that the tests are part of the automatic build pipeline, it will be easy to add incremental tests for any issues that pop up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Added:&lt;/strong&gt; Just after publishing this blog post, I realized I had a problem with my &lt;code&gt;.htaccess&lt;/code&gt; files (for Apache server redirects). With my new test framework in place, it was easy to add a set of checks to ensure that file is properly deployed.&lt;/p&gt;
&lt;p&gt;The Playwright test report is uploaded as a zip file, but I want to adjust that to be viewable online.&lt;/p&gt;
&lt;p&gt;This test setup is simple because the static blog is simple. (Really the only interactive part is the search box.) I want to learn and use Playwright for some actual applications too. This would have been great for end-to-end testing of my retired &lt;a href=&quot;https://www.cantoni.org/tweetfave.com/&quot;&gt;Tweetfave project&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Copilot is my Copilot</title>
    <link href="https://www.cantoni.org/2025/07/24/copilot-is-my-copilot/" />
    <updated>2025-07-24T00:00:00Z</updated>
    <id>https://www.cantoni.org/2025/07/24/copilot-is-my-copilot/</id>
    <content type="html">&lt;p&gt;Migrating this blog to Eleventy has been a good project for learning &lt;a href=&quot;https://code.visualstudio.com/docs/copilot/overview&quot;&gt;GitHub Copilot&lt;/a&gt; with some real-world scenarios and problem solving. We&#39;re currently using Cursor and other in-house AI tools at work, so playing with Copilot here has been a nice comparison. Here&#39;s a sample of several ways I used Copilot and how well it worked.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall experience with this blog project:&lt;/strong&gt; Overall Copilot has been very helpful and it&#39;s been fun to learn how to get the most out of it. I believe my choice of popular technologies here (JavaScript, Python, Nunjucks templates, Eleventy blog system) have helped Copilot give good answers because there&#39;s a ton of existing knowledge out there. Ask mode, edit mode and inline suggestions have really improved even as I&#39;ve just been using Copilot for a few months now. &lt;span class=&quot;material-symbols-outlined&quot; style=&quot;color:green&quot;&gt;sentiment_satisfied&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent mode:&lt;/strong&gt; This was my first steps into letting the AI run in Agent mode and it showed how effective it can be. I haven&#39;t had it do anything super complicated, but for any type of &amp;quot;fix or improve this&amp;quot; task, just giving it the ability to iterate by making a change and then running a build was really helpful. For any new changes I&#39;m most likely to start with Agent mode. &lt;span class=&quot;material-symbols-outlined&quot; style=&quot;color:green&quot;&gt;sentiment_satisfied&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contextual questions:&lt;/strong&gt; I like selecting something in a file and asking &amp;quot;explain this&amp;quot; or &amp;quot;why is this JavaScript file imported this way?&amp;quot;. A great example was a weird-looking regular expression which it helpfully explained -- it was something I had implemented a few weeks ago but promptly forgot! Comments can be a gift to your future self. &lt;span class=&quot;material-symbols-outlined&quot; style=&quot;color:green&quot;&gt;sentiment_satisfied&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Creating scripts from a short spec:&lt;/strong&gt; Copilot does very well when given even a short definition of what you want to accomplish. It tends to be pretty verbose and implements things the long simple way, but that&#39;s kind of my habit too. A great example was a markup change I needed made in all 600 blog post Markdown files. Originally I had Copilot running in Agent mode to find and modify these files, but it would either get stuck or pause after a few files. Instead I asked it to write the equivalent Python script which I then ran myself and it was quickly finished. Scripts also let you try one first to make sure it&#39;s right, then let it loose. &lt;span class=&quot;material-symbols-outlined&quot; style=&quot;color:green&quot;&gt;sentiment_satisfied&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Eleventy and Nunjucks templates:&lt;/strong&gt; Just using straight Ask mode, Copilot was pretty knowledgeable about the Eleventy blog system and Nunjucks templates, but would sometimes get stuck on simple things or would suggest things that were incorrect. Adding &lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/custom-instructions/adding-repository-custom-instructions-for-github-copilot&quot;&gt;custom instructions for Copilot&lt;/a&gt; helped a little bit (I think), but switching to Agent mode where it could check its own work really helped. (See above.) &lt;span class=&quot;material-symbols-outlined&quot; style=&quot;color:#daa520&quot;&gt;sentiment_neutral&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Print stylesheets:&lt;/strong&gt; &lt;a href=&quot;https://www.cantoni.org/2004/02/16/printstyle/&quot;&gt;Print stylesheets&lt;/a&gt; is something I&#39;ve always found really helpful. Even if I&#39;m the last person who still prints things, even printing to PDF comes through a lot cleaner. It&#39;s a nice touch for visitors to your site. For my WordPress site I had hand-crafted a print stylesheet, so I asked Copilot to do the same for the new Eleventy blog. I used a pretty simple prompt: &amp;quot;In this CSS file please create a print media stylesheet that will adjust styles to make everything look clean when printed. The index.html file is an example output page from this blog. I&#39;d like the print output to include the page title, date and the content section.&amp;quot; The results were pretty good, if not a bit verbose. It really liked the &lt;code&gt;!important&lt;/code&gt; attribute and used it liberally :) I made a couple of manual tweaks but overall it worked pretty well. &lt;span class=&quot;material-symbols-outlined&quot; style=&quot;color:#daa520&quot;&gt;sentiment_neutral&lt;/span&gt;&lt;/p&gt;
</content>
  </entry>
</feed>