This is a valid Atom 1.0 feed.
This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.
line 186, column 0: (5 occurrences) [help]
<content type="html"><html><head></head><body>&l ...
line 186, column 0: (5 occurrences) [help]
<content type="html"><html><head></head><body>&l ...
line 186, column 0: (5 occurrences) [help]
<content type="html"><html><head></head><body>&l ...
line 362, column 0: (2 occurrences) [help]
<p>I need to fix the build process so that the #file:sitemap.xml.njk t ...
<?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>2025-08-18T00:00:00Z</updated>
<id>https://www.cantoni.org/</id>
<author>
<name>Brian Cantoni</name>
</author>
<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"><p>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.</p>
<h2 id="eleventy-image-handling">Eleventy Image Handling</h2>
<p>When I migrated this site from WordPress to Eleventy, the <a href="https://www.11ty.dev/docs/plugins/image/">image transform plugin</a> 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't working. I decided to pull out the 11ty image transform and manage images myself.</p>
<p>I wanted to start with automatically adding height and width attributes, so this is what I tried:</p>
<ol>
<li>Image plugin -- too complicated, removed</li>
<li>Inspect the image plugin source to possibly copy just the height/width code -- too complicated</li>
<li>Write my own plugin -- seems doable but probably overkill</li>
<li>Write a <a href="https://www.11ty.dev/docs/transforms/">transform</a> -- this is starting to look the best path!</li>
</ol>
<p>Eleventy transforms are run late in the build cycle (&quot;postprocessing&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.</p>
<p>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:</p>
<blockquote>
<p>write an eleventy transform that will automatically add height and width attributes to any found in the HTML source</p>
</blockquote>
<p>This worked and with only minimal adjustments got me exactly what I needed!</p>
<h2 id="image-dimensions-transform">Image Dimensions Transform</h2>
<p>This is my solution for adding height and width attributes to image tags in the HTML output files. A few quick notes:</p>
<ul>
<li>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</li>
<li>We're only interested in .html output files</li>
<li>If the page doesn't have any images, skip it (importantly for transforms: they need to just return the original unmodified content)</li>
<li>Also skip any images hosted elsewhere (we're only processing local images)</li>
</ul>
<p>To use this script, include this snippet in your <code>eleventy.config.js</code> file:</p>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token comment">/**
* Eleventy transform to add width and height to &lt;img> tags
*/</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addTransform</span><span class="token punctuation">(</span>
<span class="token string">"img-dimensions"</span><span class="token punctuation">,</span>
<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">content<span class="token punctuation">,</span> outputPath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>outputPath <span class="token operator">||</span> <span class="token operator">!</span>outputPath<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">".html"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">return</span> content<span class="token punctuation">;</span>
<span class="token keyword">const</span> dom <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">JSDOM</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> imgs <span class="token operator">=</span> dom<span class="token punctuation">.</span>window<span class="token punctuation">.</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span>
<span class="token string">"img[src]:not([width]):not([height])"</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// If no images, return the original content</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>imgs<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token keyword">return</span> content<span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> img <span class="token keyword">of</span> imgs<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> src <span class="token operator">=</span> img<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">"src"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>src<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"http"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">continue</span><span class="token punctuation">;</span> <span class="token comment">// Skip remote images</span>
<span class="token comment">// Remove leading slash if present</span>
<span class="token keyword">let</span> imgPath <span class="token operator">=</span> src<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^\/</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span> <span class="token string">""</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> filePath <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./public/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>imgPath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">let</span> buffer <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">promisify</span><span class="token punctuation">(</span>readFile<span class="token punctuation">)</span><span class="token punctuation">(</span>filePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> dimensions <span class="token operator">=</span> <span class="token function">imageSize</span><span class="token punctuation">(</span>buffer<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>dimensions<span class="token punctuation">.</span>width <span class="token operator">&amp;&amp;</span> dimensions<span class="token punctuation">.</span>height<span class="token punctuation">)</span> <span class="token punctuation">{</span>
img<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"width"</span><span class="token punctuation">,</span> dimensions<span class="token punctuation">.</span>width<span class="token punctuation">)</span><span class="token punctuation">;</span>
img<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"height"</span><span class="token punctuation">,</span> dimensions<span class="token punctuation">.</span>height<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error processing image </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>img<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">"src"</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>e<span class="token punctuation">.</span>message<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> dom<span class="token punctuation">.</span><span class="token function">serialize</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Also add these imports at the top of <code>eleventy.config.js</code>:</p>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> promisify <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"util"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> readFile <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"fs"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> imageSize <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"image-size"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> <span class="token constant">JSDOM</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"jsdom"</span><span class="token punctuation">;</span></code></pre>
<p>Finally, install the new library dependencies:</p>
<pre class="language-shell" tabindex="0"><code class="language-shell"><span class="token function">npm</span> <span class="token function">install</span> jsdom image-size</code></pre>
<h2 id="example">Example</h2>
<p>Here's an example from my <a href="https://www.cantoni.org/2025/01/21/summarizing-youtube-videos-with-llms/">Summarizing YouTube Videos with LLMs post</a>. In the Markdown source file, the image is included with this markup:</p>
<pre class="language-md" tabindex="0"><code class="language-md"><span class="token url"><span class="token operator">!</span>[<span class="token content">Thumbnail for the Honey Influencer Scam YouTube video</span>](<span class="token url">/images/honey-scam-thumbnail.jpg</span>)</span></code></pre>
<p>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:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/images/honey-scam-thumbnail.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Thumbnail for the Honey Influencer Scam YouTube video<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>300<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>180<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span></code></pre>
<h2 id="next">Next</h2>
<p>One thing I need to look at is the build time. It'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't horrible but could be better:</p>
<pre><code>[11ty] Benchmark 2668ms 23% 706× (Configuration) &quot;img-dimensions&quot; Transform
[11ty] Benchmark 5770ms 50% 661× (Configuration) &quot;gitLastModified&quot; Nunjucks Filter
[11ty] Copied 453 Wrote 696 files in 11.48 seconds (16.5ms each, v3.0.0)
</code></pre>
</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"><p>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 <a href="https://playwright.dev/">Playwright</a> because we use it at work and it'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.</p>
<h2 id="learning-playwright">Learning Playwright</h2>
<p>The <a href="https://playwright.dev/docs/writing-tests">Playwright docs</a> are very good so I started there. Getting set up, writing some initial tests, learning about <a href="https://playwright.dev/docs/test-assertions">assertions</a> and <a href="https://playwright.dev/docs/locators">locators</a> were my starting points.</p>
<p>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.</p>
<p>For each test I dropped the outline into a code comment like this:</p>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token comment">/*
test home page
- go to home page
- confirm &lt;title> is set to "Brian Cantoni"
- confirm &lt;h1> heading is set to "Brian Cantoni"
- confirm exactly 10 blog posts are listed
*/</span></code></pre>
<p>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.</p>
<p>From the above outline, Copilot and I created the Playwright test:</p>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token function">test</span><span class="token punctuation">(</span><span class="token string">"Home page"</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> page <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">await</span> <span class="token function">expect</span><span class="token punctuation">(</span>page<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toHaveTitle</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">Brian Cantoni</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">await</span> <span class="token function">expect</span><span class="token punctuation">(</span>
page<span class="token punctuation">.</span><span class="token function">getByRole</span><span class="token punctuation">(</span><span class="token string">"heading"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">"Brian Cantoni"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBeVisible</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">await</span> <span class="token function">expect</span><span class="token punctuation">(</span>page<span class="token punctuation">.</span><span class="token function">locator</span><span class="token punctuation">(</span><span class="token string">"li.postlist-item"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toHaveCount</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>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 &quot;corrupted&quot; :) Another good reminder for having frequent git commits of your work in progress.</p>
<h2 id="playwright-with-netlify-preview-builds">Playwright with Netlify Preview Builds</h2>
<p>Beyond running tests locally, I wanted them to run against the Netlify preview builds as well. I have <a href="https://docs.netlify.com/deploy/deploy-types/deploy-previews/">Netlify deploy previews</a> configured against my private GitHub repo and every pull request creates a unique &quot;preview&quot; build that goes with it. I'm not using Netlify for the production build, but these preview builds are very helpful especially for checking any tricky changes.</p>
<p>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 <a href="https://github.com/JakePartusch/wait-for-netlify-action" title="A GitHub action that will wait until a Netlify Preview deploy has completed before continuing on">JakePartusch/wait-for-netlify-action</a>.</p>
<p>Incorporating this as an action step is quite simple:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Wait for Netlify Deploy Preview
<span class="token key atrule">id</span><span class="token punctuation">:</span> waitForDeployPreview
<span class="token key atrule">uses</span><span class="token punctuation">:</span> jakepartusch/wait<span class="token punctuation">-</span>for<span class="token punctuation">-</span>netlify<span class="token punctuation">-</span>action@v1.4</code></pre>
<p>And then a later step uses the unique preview URL to run the tests:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Run Playwright tests
<span class="token key atrule">run</span><span class="token punctuation">:</span> npx playwright test
<span class="token key atrule">env</span><span class="token punctuation">:</span>
<span class="token key atrule">PLAYWRIGHT_BASE_URL</span><span class="token punctuation">:</span> $</code></pre>
<p>In the end it's a clean solution which works well, but it took <em>so many</em> incremental pushes to test the actions, it was frustrating. It didn'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.</p>
<p>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).</p>
<h2 id="playwright-with-github-build">Playwright with GitHub Build</h2>
<p>Once I got the Netlify preview tests running, I realized I should also handle the simpler case of just building and testing on GitHub.</p>
<p>Doing this doesn't require any custom actions. Instead, it's just a sequence of starting the local server, waiting for that server to finish, and then running the tests:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Start local server for Playwright
<span class="token key atrule">run</span><span class="token punctuation">:</span> npx http<span class="token punctuation">-</span>server _site <span class="token punctuation">-</span>p 8080 &amp;
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Wait for server to start
<span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
timeout 30 bash -c 'until curl -f http://localhost:8080; do sleep 1; done'
echo "Server is ready"</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Run Playwright tests
<span class="token key atrule">run</span><span class="token punctuation">:</span> npx playwright test
<span class="token key atrule">env</span><span class="token punctuation">:</span>
<span class="token key atrule">PLAYWRIGHT_BASE_URL</span><span class="token punctuation">:</span> http<span class="token punctuation">:</span>//localhost<span class="token punctuation">:</span><span class="token number">8080</span></code></pre>
<h2 id="next">Next</h2>
<p>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.</p>
<p><strong>Added:</strong> Just after publishing this blog post, I realized I had a problem with my <code>.htaccess</code> 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.</p>
<p>The Playwright test report is uploaded as a zip file, but I want to adjust that to be viewable online.</p>
<p>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 <a href="https://www.cantoni.org/tweetfave.com/">Tweetfave project</a>.</p>
</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"><p>Migrating this blog to Eleventy has been a good project for learning <a href="https://code.visualstudio.com/docs/copilot/overview">GitHub Copilot</a> with some real-world scenarios and problem solving. We're currently using Cursor and other in-house AI tools at work, so playing with Copilot here has been a nice comparison. Here's a sample of several ways I used Copilot and how well it worked.</p>
<p><strong>Overall experience with this blog project:</strong> Overall Copilot has been very helpful and it'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's a ton of existing knowledge out there. Ask mode, edit mode and inline suggestions have really improved even as I've just been using Copilot for a few months now. <span class="material-symbols-outlined" style="color:green">sentiment_satisfied</span></p>
<p><strong>Agent mode:</strong> This was my first steps into letting the AI run in Agent mode and it showed how effective it can be. I haven't had it do anything super complicated, but for any type of &quot;fix or improve this&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'm most likely to start with Agent mode. <span class="material-symbols-outlined" style="color:green">sentiment_satisfied</span></p>
<p><strong>Contextual questions:</strong> I like selecting something in a file and asking &quot;explain this&quot; or &quot;why is this JavaScript file imported this way?&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. <span class="material-symbols-outlined" style="color:green">sentiment_satisfied</span></p>
<p><strong>Creating scripts from a short spec:</strong> 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'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's right, then let it loose. <span class="material-symbols-outlined" style="color:green">sentiment_satisfied</span></p>
<p><strong>Eleventy and Nunjucks templates:</strong> 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 <a href="https://docs.github.com/en/copilot/how-tos/custom-instructions/adding-repository-custom-instructions-for-github-copilot">custom instructions for Copilot</a> helped a little bit (I think), but switching to Agent mode where it could check its own work really helped. (See above.) <span class="material-symbols-outlined" style="color:#daa520">sentiment_neutral</span></p>
<p><strong>Print stylesheets:</strong> <a href="https://www.cantoni.org/2004/02/16/printstyle/">Print stylesheets</a> is something I've always found really helpful. Even if I'm the last person who still prints things, even printing to PDF comes through a lot cleaner. It'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: &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'd like the print output to include the page title, date and the content section.&quot; The results were pretty good, if not a bit verbose. It really liked the <code>!important</code> attribute and used it liberally :) I made a couple of manual tweaks but overall it worked pretty well. <span class="material-symbols-outlined" style="color:#daa520">sentiment_neutral</span></p>
</content>
</entry>
<entry>
<title>Migrating WordPress To Eleventy</title>
<link href="https://www.cantoni.org/2025/07/12/migrating-wordpress-to-eleventy/" />
<updated>2025-07-12T00:00:00Z</updated>
<id>https://www.cantoni.org/2025/07/12/migrating-wordpress-to-eleventy/</id>
<content type="html"><html><head></head><body><p>After 11 years on WordPress, I <a href="https://www.cantoni.org/2025/05/28/moved-to-eleventy/">moved this blog to Eleventy (11ty)</a> and I'm very happy with the results. These are my notes on the background and process for anyone going through a similar move.</p>
<h2 id="why-the-change">Why the change?</h2>
<p>My blog isn't too big - about 650 posts and 410 images since 2002 - but I've enjoyed the process of keeping it going and keeping everything intact as I've migrated through several blogging tools. (<a href="https://www.w3.org/Provider/Style/URI">Cool URIs don't change</a> and all that.)</p>
<p>After about 11 years on WordPress I was ready for a change for several reasons:</p>
<ul>
<li>Keeping up with WordPress and plugin updates was time consuming and I never finished my ideas for making it more automatic</li>
<li>I would occasionally have security issues where spam links would be injected randomly across posts</li>
<li>The worst example was when it was really <a href="https://www.cantoni.org/2015/03/11/recovering-from-a-wordpress-hack/">hacked</a> (essentially taken down), but luckily after I restored the site the <a href="https://www.cantoni.org/2015/04/28/traffic-resuming-after-wordpress-hack/">traffic returned to normal</a></li>
<li>Plugins like Yoast SEO were powerful and quickly added capabilities to the site, but the constant nagging to upgrade to premium was really a drag</li>
<li>More an issue with my web host rather than WordPress, but I wanted to finally implement SSL and control things like caching myself; removing the need for a MySQL database also helps enable that move</li>
</ul>
<p>On the positive side:</p>
<ul>
<li>It was fun learning about WordPress and its ecosystem, developing my own plugins, tweaking my themes <em>just right</em></li>
<li>The ability to post from anywhere was nice (although in practice it didn't use it that way very often)</li>
<li>Learning WP led me to use it for some site projects for sports clubs I was involved in with my kids</li>
<li>My <a href="https://www.cantoni.org/2019/10/25/wordpress-github-docker/">WordPress powered by Docker</a> project was fun to build and helped a lot with local testing</li>
</ul>
<h2 id="static-site-generator">Static site generator</h2>
<p>Okay so moving off WordPress - but what to switch to? I wanted to go back to a static site generator to avoid the need for a database and have it be more resilient to any hacking (after all it's just static HTML files). But which one to choose? The Jamstack site has a nice directory with 369 different tools as of today: <a href="https://jamstack.org/generators/">Static Site Generators - Top Open Source SSGs</a>.</p>
<p>For the new system I wanted something that was popular, fast enough, and had simple blog themes I could use to get started.</p>
<p>At first I gave Hugo a try because it's fast and popular and would give me a chance to play with the Go language. I couldn't find a good simple blog theme to start with and struggled to understand how it worked.</p>
<p>Next I tried Eleventy (also abbreviated as 11ty) and it really clicked for me. I'm no JavaScript expert, but the way the build process and layout worked made it easier for me to understand (even if just at the surface level). What really hooked me was finding the <a href="https://github.com/11ty/eleventy-base-blog">11ty/eleventy-base-blog starter project</a>; I cloned this and I was off and running with the migration.</p>
<p>From the start I've kept everything in a GitHub repo, so that's essentially my new blog database :) It's been great to experiment in branches (especially those that didn't pan out), and to keep track of every step in case I need to revert any changes.</p>
<h2 id="migration-notes">Migration notes</h2>
<p>Part of the rationale for choosing something popular was to benefit from those that have gone before us. Migrating WordPress to Eleventy fits that model; these are the posts I benefitted the most from (bits and pieces from each):</p>
<ul>
<li><a href="https://www.11ty.dev/docs/migrate/wordpress/">Migrating from WordPress to Eleventy — Eleventy</a></li>
<li><a href="https://cfjedimaster.github.io/eleventy-blog-guide/guide.html">A Complete Guide to Building a Blog with Eleventy</a></li>
<li><a href="https://www.joshcanhelp.com/taking-wordpress-to-eleventy/">Taking WordPress to Eleventy - Josh Can Help</a></li>
<li><a href="https://deepakness.com/blog/from-wordpress-to-11ty/">Migrating from WordPress to 11ty | DeepakNess</a></li>
</ul>
<p>The core of the migration process was to create the new blog project (using the starter project), then run the official <a href="https://github.com/11ty/eleventy-import">11ty/eleventy-import)</a> to import all the WordPress posts and images. This brought all the content over in Markdown format which was exactly what I wanted and is the default for Eleventy blogs. This part was pretty smooth; the rest of the time was spent wrestling with a few key areas:</p>
<h3 id="post-dates">Post dates</h3>
<p>A key migration requirement was to keep blog post links the same as before. I was okay if images had moved around, but I wanted to ensure the original links didn't change (except for the eventual http → https redirect).</p>
<p>This was my first step into trying to understand the Eleventy template and build process and I definitely struggled. The import script correctly brought over blog posts from WordPress and created the "frontmatter" needed for Eleventy. Here's an example:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token punctuation">---</span>
<span class="token key atrule">title</span><span class="token punctuation">:</span> How to Convert Word DOC to DOCX Format
<span class="token key atrule">authors</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Brian Cantoni
<span class="token key atrule">url</span><span class="token punctuation">:</span> http<span class="token punctuation">:</span>//www.cantoni.org/author/bcantoni
<span class="token key atrule">avatarUrl</span><span class="token punctuation">:</span> <span class="token punctuation">&gt;</span><span class="token punctuation">-</span>
https<span class="token punctuation">:</span>//secure.gravatar.com/avatar/d2a3e9017efa2d8f0212315e590dd6a7<span class="token punctuation">?</span>s=96<span class="token important">&amp;d=mm&amp;r=g</span>
<span class="token key atrule">date</span><span class="token punctuation">:</span> <span class="token datetime number">2020-01-16T05:12:41.000Z</span>
<span class="token key atrule">metadata</span><span class="token punctuation">:</span>
<span class="token key atrule">categories</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> Tools
<span class="token punctuation">-</span> Web
<span class="token key atrule">uuid</span><span class="token punctuation">:</span> 11ty/import<span class="token punctuation">:</span><span class="token punctuation">:</span>wordpress<span class="token punctuation">:</span><span class="token punctuation">:</span>http<span class="token punctuation">:</span>//www.cantoni.org/<span class="token punctuation">?</span>p=1081
<span class="token key atrule">type</span><span class="token punctuation">:</span> wordpress
<span class="token key atrule">url</span><span class="token punctuation">:</span> http<span class="token punctuation">:</span>//www.cantoni.org/2020/01/15/how<span class="token punctuation">-</span>to<span class="token punctuation">-</span>convert<span class="token punctuation">-</span>word<span class="token punctuation">-</span>doc<span class="token punctuation">-</span>to<span class="token punctuation">-</span>docx<span class="token punctuation">-</span>format
<span class="token key atrule">tags</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> tools
<span class="token punctuation">-</span> web
<span class="token punctuation">---</span></code></pre>
<p>My blog uses the path convention /yyyy/mm/dd/post and you can see this post was originally published on /2020/01/15/. However, the <code>date</code> from the import uses the UTC time which happens to be the <em>next</em> day. Publishing under /2020/01/16/ would be wrong, so I spent a regrettable amount of time trying to make build script changes to correctly handle these (e.g. calculate back to my Pacific time), but also work correctly new posts written after migration. To be fair to myself I was also learning the whole Eleventy system at the same time :)</p>
<p>In the end I decided to simplify and just rely on the file path and use it as the published date. Much simpler! I created a <code>post_permalink</code> filter (eleventy.config.js):</p>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token comment">// determine permalink from the file path in yyyy/mm/dd/slug/index.md format</span>
<span class="token comment">// this replaces using 'date' in the frontmatter which was problematic after WP migration</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><span class="token string">"post_permalink"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">page</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> match <span class="token operator">=</span> page<span class="token punctuation">.</span>inputPath<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span>
<span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\/(\d{4})\/(\d{2})\/(\d{2})\/([^\/]+)\/index\.md$</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>match<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> <span class="token punctuation">[</span><span class="token punctuation">,</span> year<span class="token punctuation">,</span> month<span class="token punctuation">,</span> day<span class="token punctuation">,</span> slug<span class="token punctuation">]</span> <span class="token operator">=</span> match<span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>year<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>month<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>day<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Unexpected inputPath format for post_permalink: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>page<span class="token punctuation">.</span>inputPath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And then used it by default for all blog posts (blog.11tydata.js):</p>
<pre class="language-liquid" tabindex="0"><code class="language-liquid">export default {
tags: ["posts"],
layout: "layouts/post.njk",
permalink: "2025/07/12/migrating-wordpress-to-eleventy/",
};</code></pre>
<p>Now this old link <a href="http://www.cantoni.org/2020/01/15/how-to-convert-word-doc-to-docx-format">http://www.cantoni.org/2020/01/15/how-to-convert-word-doc-to-docx-format</a> redirects to <a href="https://www.cantoni.org/2020/01/15/how-to-convert-word-doc-to-docx-format/">https://www.cantoni.org/2020/01/15/how-to-convert-word-doc-to-docx-format/</a> and it's working as I planned.</p>
<h3 id="images">Images</h3>
<p>Image handling was another tricky area. The <a href="https://www.11ty.dev/docs/plugins/image/">Eleventy image plugin</a> is pretty powerful and handles things like automatic height/width attributes, serving images in different modern and legacy formats, and outputting multiple sizes.</p>
<p>I was fine with all of that, but couldn't quite get it to work correctly with my existing images, especially referencing them from these imported blog posts. With the plugin enabled, I couldn't do simple things like referencing "/images/file.png". I also couldn't find or get any help from the community so I decided once again to "do the simple thing" and removed that plugin. Now everything works and as a bonus the images are mostly in their original locations under /images, so any old search results should still work.</p>
<p>I did write my own build script to automatically add height and width attributes; I'll write up more on this later.</p>
<h3 id="search">Search</h3>
<p>The built-in search for WordPress was really nice - being a PHP-based site really helps with dynamic content like this. The new static site doesn't have that obviously, but luckily there are a lot of static site search tools.</p>
<p>I went with <a href="https://pagefind.app/">PageFind</a> which during build time reads all content and creates a search index JSON file. Then a little bit of JavaScript runs when someone does a search, and the results look pretty good. Future improvement: only load that JavaScript when someone clicks to search (rather than every page load) and make it look nicer.</p>
<p>Hat tip to Robb Knight's article which made this super easy: <a href="https://rknight.me/blog/using-pagefind-with-eleventy-for-search/">Using PageFind with Eleventy for Search</a>!</p>
<h3 id="excerpts">Excerpts</h3>
<p>Excerpts needed a little extra help to accommodate the WordPress convention of manually using a <code>&lt;!-- more --&gt;</code> tag to separate the excerpt from the rest of the post. A quick parsing option made this easy. Still todo: make this more automatic so I don't have to go back and mark all the old posts.</p>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token comment">// enable manual excerpts marked in the content with &lt;!-- more --&gt;</span>
<span class="token comment">// https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">setFrontMatterParsingOptions</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">excerpt</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token literal-property property">excerpt_separator</span><span class="token operator">:</span> <span class="token string">"&lt;!-- more --&gt;"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h3 id="other-files">Other files</h3>
<p>A final piece was pretty straightforward, copying over a bunch of files managed outside of WordPress, including:</p>
<ul>
<li>images under /images</li>
<li>downloadable files under /files</li>
<li>robots.txt</li>
<li>.htaccess (redirects and cache settings)</li>
<li>individual .html files</li>
</ul>
<p>It's also nice now that 100% of the blog files are all in GitHub, so they're all managed in one place.</p>
<h3 id="comments">Comments</h3>
<p>This step was simple because I've long disabled comments on the old site. (In fact it used a distinct WordPress plugin just to disable the comments.) There are a lot of different ways to support comments with a static site, but it's not something I need.</p>
<h3 id="authoring-workflow">Authoring workflow</h3>
<p>In my WordPress configuration, I was writing everything in Markdown format (I had disabled the Gutenberg editor). I also managed images through WordPress which was pretty nice. (Although to avoid spam issues I had to manually change permissions on the "upload" directory each time.)</p>
<p>In the new configuration, this is my workflow, aided by a "drafts" Python script I wrote:</p>
<ol>
<li>Work on new blog post in Markdown in the ./drafts folder</li>
<li>Add and reference images as needed</li>
<li>When ready to publish, run <code>make draft</code> which handles several steps
<ul>
<li>move blog post to correct /yyyy/mm/dd/ folder location</li>
<li>move images to /images and change references</li>
<li>suggest and add tags</li>
<li>suggest and finalize post title and slug</li>
</ul>
</li>
<li>Test locally with <code>make start</code></li>
<li>(Optional) create and add to a branch if not ready to publish immediately</li>
<li>Add to GitHub and push</li>
</ol>
<h2 id="deployment">Deployment</h2>
<p>For deployment I wanted something that could run locally (which Eleventy already supports) and also handle preview and production pushes automatically. Here's what I came up with:</p>
<p>Local: <code>make start</code> (which just runs the Eleventy command <code>npm run start</code>)</p>
<p>Preview: Pull request builds automatically trigger <a href="https://www.netlify.com/">Netlify</a> preview builds. This is really handy especially for anything tricky that I want to double-check before pushing live. I'm only on the free plan, so these are automatically deleted after 30 days, but none are needed that long.</p>
<p>Production: Netlify also can push to production, but I wanted to go more DIY and host this myself. (It's on a small DigitalOcean droplet running the Apache web server.) I created a GitHub Action workflow to do a build and push (rsync) with any push to the <code>main</code> branch. I can also trigger this manually if needed. I learned how to use restricted SSH keys for this (meaning: an SSH key that can only do the rsync to the specific destination) and it worked well.</p>
<p>I could potentially move this to a different hosting solution in the future, but for now I like combing through the Apache error and access logs directly to keep an eye out for errors.</p>
<h2 id="performance">Performance</h2>
<p>I'll do some more work here on performance and accessibility, but it's nice to start out with "4 hundreds" on the Lighthouse performance test.</p>
<p><img src="https://www.cantoni.org/images/lighthouse.png" alt="4 100% scores on Lighthouse"></p>
<p>It definitely helps to have a minimal site without much CSS or JavaScript bundled into each page. I brought over some of my old cache settings but will probably tweak that a bit more.</p>
</body></html></content>
</entry>
<entry>
<title>Proper Sitemap Update Dates for Eleventy</title>
<link href="https://www.cantoni.org/2025/07/10/proper-sitemap-update-dates-for-eleventy/" />
<updated>2025-07-10T00:00:00Z</updated>
<id>https://www.cantoni.org/2025/07/10/proper-sitemap-update-dates-for-eleventy/</id>
<content type="html"><p>Sitemaps are the recommended way to ensure search engines understand all the pages on your site. This site is pretty simple and I'm not sure whether a sitemap is strictly needed, but I had one with WordPress and I'd like to keep it going. One important piece for this 23 year old blog is signalling when old posts are <em>updated</em>, so I had to make sure my new Eleventy-powered site handled that piece correctly.</p>
<h2 id="sitemaps">Sitemaps</h2>
<p>First of all what are sitemaps? From the <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview">Google developer site</a>:</p>
<blockquote>
<p>A sitemap is a file where you provide information about the pages, videos, and other files on your site, and the relationships between them. Search engines like Google read this file to crawl your site more efficiently. A sitemap tells search engines which pages and files you think are important in your site, and also provides valuable information about these files.</p>
</blockquote>
<p>In short it's an XML file you can submit to search engines like Google. This site's sitemap lives at <a href="https://www.cantoni.org/sitemap.xml">https://www.cantoni.org/sitemap.xml</a>. In its simplest form it's a collection of links (<code>&lt;loc&gt;</code>) along with the date last updated (<code>&lt;lastmod&gt;</code>).</p>
<p>The Eleventy static site system doesn't come with a built-in sitemap template, but there are plenty of examples out there. Since I used the <a href="https://github.com/11ty/eleventy-base-blog">11ty/eleventy-base-blog</a> starter project, it has a built-in <a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> template that looks like this:</p>
<pre class="language-liquid" tabindex="0"><code class="language-liquid">---
permalink: /sitemap.xml
layout: false
eleventyExcludeFromCollections: true
---
<span class="token prolog">&lt;?xml version="1.0" encoding="utf-8"?></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>urlset</span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.sitemaps.org/schemas/sitemap/0.9<span class="token punctuation">"</span></span> <span class="token attr-name"><span class="token namespace">xmlns:</span>xhtml</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/1999/xhtml<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token liquid language-liquid"><span class="token delimiter punctuation">{%-</span> <span class="token keyword">for</span> <span class="token object">page</span> <span class="token keyword">in</span> collections<span class="token punctuation">.</span>all <span class="token delimiter punctuation">%}</span></span> <span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> set absoluteUrl <span class="token delimiter punctuation">%}</span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">page</span><span class="token punctuation">.</span>url <span class="token operator">|</span> <span class="token function filter">htmlBaseUrl</span><span class="token punctuation">(</span>metadata<span class="token punctuation">.</span>url<span class="token punctuation">)</span> <span class="token delimiter punctuation">}}</span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{%</span> endset <span class="token delimiter punctuation">%}</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>url</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>loc</span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> absoluteUrl <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>loc</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lastmod</span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">page</span><span class="token punctuation">.</span><span class="token object">date</span> <span class="token operator">|</span> <span class="token function filter">htmlDateString</span> <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lastmod</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>url</span><span class="token punctuation">></span></span> <span class="token liquid language-liquid"><span class="token delimiter punctuation">{%-</span> <span class="token keyword">endfor</span> <span class="token delimiter punctuation">%}</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>urlset</span><span class="token punctuation">></span></span></code></pre>
<p>Notice the <code>&lt;lastmod&gt;</code> element there which is reflecting the last modified date for that content. This template uses the post date, but that's a problem because that date will never change, <em>even if you make edits to the post</em>. WordPress stores everything in a database and can keep track of created versus modified times. However, for this static site we <em>do</em> have a database of sorts since everything is stored in git. With the help of GitHub Copilot, let's try to use the git &quot;last modified&quot; time to improve our sitemap.</p>
<h2 id="first-attempt">First attempt</h2>
<p>I'm still learning the Eleventy system and in particular the data model. My first thought was to create a page variable that has the last-modified date as found with the <code>git log</code> command. This was my prompt:</p>
<blockquote>
<p>I want to add a new page metadata variable for this Eleventy blog with the following details:</p>
<ol>
<li>During the build process, create a new variable <code>lastmod</code> that will be usable by templates as <code>page.lastmod</code></li>
<li>The <code>lastmod</code> variable should be determined by calling out to the shell with the command: <code>git log -1 --format=%cd --date=iso-strict PAGE</code> where PAGE is the <code>page.inputPath</code> value</li>
<li>Just make the javascript code changes; I will modify the template separately</li>
</ol>
</blockquote>
<p>The JavaScript code changes Copilot created looked great, but this didn't work and I spent hours fiddling around with it. The <code>git log</code> part was working, but it seems you can't create a variable on the fly like this (addition to the page metadata). I think my approach here was too low level and was based on my (probably slightly wrong) assumptions about how Eleventy works.</p>
<h2 id="second-attempt">Second attempt</h2>
<p>I abandoned that approach, took a break, then decided to start at a higher level by explaining <em>what</em> I wanted to accomplish (rather than <em>how</em> to build it). This time I used Copilot agent mode with full permissions enabled and this new prompt:</p>
<blockquote>
<p>I need to fix the build process so that the #file:sitemap.xml.njk template which generates _site/sitemap.xml file sets the &quot;<lastmod>&quot; value to the date in yyyy-mm-dd format of the last git commit. (Rather than right now which is the page.date value.) The last modified date can be fetched by shelling to git like this: git log -1 --date=format:'%Y-%m-%d' --format=&quot;%cd&quot; -- FILENAME. After you make changes, you can run the build with make build. One test value that will show that it's working: in _site/sitemap.xml the entry for &quot;https://www.cantoni.org/2015/03/11/recovering-from-a-wordpress-hack/&quot; should be &quot;<lastmod>2025-05-28</lastmod>&quot;</lastmod></p>
</blockquote>
<p>Allowing Copilot to make the changes, run the build, and then check the results seems helpful because it can iterate on its solution. I also gave it one specific older blog post with an expected result.</p>
<p>There are two parts to this solution - first is a new <code>gitLastModified</code> filter which is pretty straightforward:</p>
<pre class="language-js" tabindex="0"><code class="language-js"><span class="token comment">// Get the last modified date of a file using git log (used for sitemap.xml)</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addFilter</span><span class="token punctuation">(</span><span class="token string">"gitLastModified"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">inputPath</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> today <span class="token operator">=</span> DateTime<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toFormat</span><span class="token punctuation">(</span><span class="token string">"yyyy-MM-dd"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Return today's date for any templated pages (home page, Atom feed, /blog, /tags, etc.)</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>inputPath<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">".njk"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> today<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token function">execSync</span><span class="token punctuation">(</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">git log -1 --date=format:'%Y-%m-%d' --format="%cd" -- "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>inputPath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span>
<span class="token punctuation">{</span> <span class="token literal-property property">encoding</span><span class="token operator">:</span> <span class="token string">"utf-8"</span> <span class="token punctuation">}</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>result<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> result<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> today<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Error getting last modified date for </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>inputPath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>I added the special handling for template-driven pages like the home page, blog archive, tags archive and Atom feed so they will always reflect the build date. Otherwise this automation was using the date the <em>template</em> was edited which is not the intent.</p>
<p>The second step is to use <code>gitLastModified</code> in the sitemap.xml template:</p>
<pre class="language-liquid" tabindex="0"><code class="language-liquid"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>lastmod</span><span class="token punctuation">></span></span><span class="token liquid language-liquid"><span class="token delimiter punctuation">{{</span> <span class="token object">page</span><span class="token punctuation">.</span>inputPath <span class="token operator">|</span> <span class="token function filter">gitLastModified</span> <span class="token delimiter punctuation">}}</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>lastmod</span><span class="token punctuation">></span></span></code></pre>
<p>It worked!</p>
<p>Doing a <code>git log</code> for each blog post does slow down the build, but it's still under 12 seconds total so it's quite reasonable.</p>
<h2 id="final-piece">Final piece</h2>
<p>Well it almost worked correctly. While writing this blog post I did a quick check of the live sitemap.xml and found it was wrong: all <code>&lt;lastmod&gt;</code> dates were the date I last built the whole site. Luckily my human brain solved this one quickly because I realized that CI pipelines (GitHub Actions in this case) usually only do a shallow check out of the repository. (Shallow checkout means it downloads just the latest change, not the full git history.)</p>
<p>Switching this to checking out the full repo history was a quick solution. Setting <code>fetch-depth</code> to 0 will fetch all history (<a href="https://github.com/actions/checkout">actions/checkout docs</a>):</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Check out repository
<span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4
<span class="token key atrule">with</span><span class="token punctuation">:</span>
<span class="token key atrule">fetch-depth</span><span class="token punctuation">:</span> <span class="token number">0</span></code></pre>
<p>Normally you wouldn't do this for a big code project (because it makes the Actions run more slowly), but for this small mostly-Markdown repo it's fine.</p>
<p>Now I periodically check the Google Search Console to see if it's reported any errors and so far it's looking good.</p>
</content>
</entry>
<entry>
<title>Migrated to Static Site (Eleventy)</title>
<link href="https://www.cantoni.org/2025/05/28/moved-to-eleventy/" />
<updated>2025-05-28T00:00:00Z</updated>
<id>https://www.cantoni.org/2025/05/28/moved-to-eleventy/</id>
<content type="html"><html><head></head><body><p>After a nice 11 year run on <a href="https://www.cantoni.org/2014/04/08/migration-from-movabletype-complete/">WordPress</a> I have spent the last few weeks migrating this site to the <a href="https://www.11ty.dev/">Eleventy (11ty) static site generator</a> and switched over on the 24th. WordPress was fun to learn and had some nice features, but in the end was overkill for what I needed and I tired of keeping up with version upgrades and recovering from spammers hacking my site.
I have some more detailed notes to write up later, but some quick thoughts...</p>
<p>Why move off WordPress?</p>
<ul>
<li>Hosting it myself meant keep up with WordPress and plugin upgrades to keep current</li>
<li>I made progress on but never quite finished my <a href="https://www.cantoni.org/2019/10/25/wordpress-github-docker/">WordPress Powered by GitHub and Docker</a> idea</li>
<li>Tired of being hacked, whether it was simple spam link injections or <a href="https://www.cantoni.org/2015/04/28/traffic-resuming-after-wordpress-hack/">full site takeovers</a></li>
<li>Dependent on plugins such as SEO which were <em>constantly</em> pushing to become a paid/premium member</li>
</ul>
<p>Why move to Eleventy static site generator?</p>
<ul>
<li>Simplify
<ul>
<li>No database involved; everything in Markdown files</li>
<li>Entire site stored in GitHub</li>
<li>Minimal JavaScript running on the client side (majority is during the build)</li>
</ul>
</li>
<li>Chose Eleventy because it's quite popular (finding help, ideas, themes, etc)</li>
<li>Learn something new, especially the full npm/JavaScript build environment</li>
</ul>
<p><img src="https://www.cantoni.org/images/under-contruction.jpg" alt="Under construction sign" title="Under construction"></p>
</body></html></content>
</entry>
<entry>
<title>Tableau Workbook Thumbnail Viewer by Claude</title>
<link href="https://www.cantoni.org/2025/03/11/tableau-workbook-thumbnail-viewer-by-claude/" />
<updated>2025-03-11T22:52:59Z</updated>
<id>https://www.cantoni.org/2025/03/11/tableau-workbook-thumbnail-viewer-by-claude/</id>
<content type="html"><html><head></head><body><p>Claude wrote for me a simple tool for viewing and optionally removing thumbnails from a Tableau workbook file. I’m calling it my Aha! moment for AI-assisted development :)
I’ve been following along with Simon Willison’s blog and was really intrigued with <a href="https://simonwillison.net/2024/Oct/21/claude-artifacts/">Everything I built with Claude Artifacts this week</a>:</p>
<blockquote>
<p>I’m a huge fan of Claude’s Artifacts feature, which lets you prompt Claude to create an interactive Single Page App (using HTML, CSS and JavaScript) and then view the result directly in the Claude interface, iterating on it further with the bot and then, if you like, copying out the resulting code.</p>
</blockquote>
<p>My first idea was something I’d recently been looking into: Tableau workbook thumbnails. Tableau workbooks are saved in .TWB or .TWBX format. Both of these are actually XML files and the difference is the .TWB contains just the workbook while .TWBX also includes the data. Thumbnail images are automatically saved in a &lt;thumbnails&gt; element that includes the Base64-encoded PNG file:</p>
<pre class="language-xml" tabindex="0"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>thumbnails</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>thumbnail</span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">'</span>384<span class="token punctuation">'</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">'</span>Commission Model<span class="token punctuation">'</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">'</span>384<span class="token punctuation">'</span></span><span class="token punctuation">&gt;</span></span>
iVBORw0KGgoAAAANSUhEUgAAAYAAAAGACAYAAACkx7W/AAAACXBIWXMAAA7DAAAOwwHHb6hk
AAAgAElEQVR4nOzdZ3wd1Z34/8/cXlWuepclW7Jk2bIt23LHvYFNDS0EAiQQ0hOSbPaf3Syb
3SS/bDbLkiUBTCcQOhhj3HBTb1bvvfderqRb5/9AjoyxwDYYO0Tn/UQv3Tlzzpkzc+c7c+bO
...
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>thumbnail</span><span class="token punctuation">&gt;</span></span>
...
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>thumbnails</span><span class="token punctuation">&gt;</span></span></code></pre>
<p>Thumbnails are used in places like the recently-opened list in Tableau Desktop:</p>
<p><img src="https://www.cantoni.org/images/tableau-workbook-thumbnails.png" alt="Tableau Desktop recently opened workbooks represented by thumbnail images"></p>
<p>Thumbnails are handy but could be a concern if you’re sharing a workbook with other people and you’ve used it for viewing sensitive data. In that case you might want to remove those thumbnails before sharing.</p>
<p>My instructions to Claude (copying / inspired by Simon’s examples) were as follows:</p>
<blockquote>
<p>Build an artifact – no react – that accepts XML in a textarea (either typed in or with an upload button). Below the textarea create a button called Go that inspects the XML, finds how many &lt;thumbnail&gt; elements are present, and reports that number below. The elements contain base64 encoded PNG images – after printing the number of thumbnail elements, for each one decode and display the image on the web page.</p>
</blockquote>
<p>After a few iterations including adding buttons for removing the thumbnails and downloading the modified XML file, it worked! (Insert Aha! moment here.) Everything worked fine in the Claude console except for the download button. (I think there is some limitation when running in the iframe within Claude, but downloading and running locally worked fine.)</p>
<p>Here’s an example of the result, loading a workbook with the Superstore dataset which had 9 thumbnails. The app let’s you remove those thumbnails and save the .TWB file again. Success!</p>
<p><img src="https://www.cantoni.org/images/tableau-thumbnail-viewer.png" alt="Tableau workbook thumbnail viewer created by Claude"></p>
</body></html></content>
</entry>
<entry>
<title>From Laptop to Cloud: Scaling YouTube Video Transcription with LLMs</title>
<link href="https://www.cantoni.org/2025/02/03/laptop-to-cloud-scaling-transcription-with-llms/" />
<updated>2025-02-04T10:38:07Z</updated>
<id>https://www.cantoni.org/2025/02/03/laptop-to-cloud-scaling-transcription-with-llms/</id>
<content type="html"><html><head></head><body><p>My experiments around <a href="http://www.cantoni.org/2025/01/21/summarizing-youtube-videos-with-llms">Summarizing YouTube Videos with LLMs</a> have been working well on my Mac laptop, but I’d really like to run it as a service on a regular schedule. Running the <a href="https://github.com/openai/whisper">OpenAI Whisper</a> transcription process locally requires more than 1GB that my favorite Digital Ocean droplets provide. Instead I’ve added an option to run it directly against the <a href="https://platform.openai.com/docs/guides/speech-to-text">OpenAI Transcriptions AI</a> (the official <a href="https://github.com/openai/openai-python">OpenAI Python library</a> makes it very easy).
To use the online API you’ll need an API key set up either as pay-as-you-go or with a ChatGPT Plus subscription (which is what I have). After I’ve run this for a while I’ll be able to tell what the usage might cost and whether upgrading to a larger droplet size would make more sense.</p>
<p>In any case this is working fine and I have it running hourly in the cloud!</p>
<p>Sample email from a Network Chuck video:</p>
<p><img src="https://www.cantoni.org/images/traaack-email-sample-network-c.png" alt="Sample email generated by summarizing the conversation in a YouTube video"></p>
</body></html></content>
</entry>
<entry>
<title>Summarizing YouTube Videos with LLMs</title>
<link href="https://www.cantoni.org/2025/01/21/summarizing-youtube-videos-with-llms/" />
<updated>2025-01-22T06:05:08Z</updated>
<id>https://www.cantoni.org/2025/01/21/summarizing-youtube-videos-with-llms/</id>
<content type="html"><html><head></head><body><p>Over the holiday break I was thinking about taking transcriptions of YouTube videos and running them through an LLM for summaries. My inspiration was the dust up around Honey coupon code browser extension. Most of the news pointed to MegaLag’s video <a href="https://www.youtube.com/watch?v=vc4yL3YTwWk">Exposing the Honey Influencer Scam</a>. It’s a 23 minute video which isn’t too bad, but I wondered whether a summary would help here?
<strong>Python Solution</strong></p>
<p><img src="https://www.cantoni.org/images/traaack-flow.png" alt="A flowchart showing a process from YouTube Channel to RSS Feed, Audio Download, Whisper Transcript, LLM Summarize, and finally Email, with arrows indicating the progression between each step."></p>
<p>I built a simple pipeline in Python that takes a list of YouTube channels and produces a summary email for each published video:</p>
<p><strong>YouTube</strong> channels have a standard RSS feed which makes this first step very simple. From the video URL I’m using yt-dlp to download the “best” audio stream which comes down in WebM format. Each video has a range of different audio qualities you can download, but this seemed to work fine. I didn’t go back to see if “worst” audio produced worse transcriptions, but the download is so small it doesn’t really matter. However what <em>does</em> matter is downloading from YouTube is painful and there’s a constant battle with the download tools out there. Since this is just for personal use, I’m using my own cookies and that seems to be pretty reliable. It would be tricky to build a real service like this unless there was a real YouTube API for getting this data.</p>
<p>For <strong>transcription</strong> I’m using <a href="https://github.com/openai/whisper">OpenAI Whisper</a> running locally. It has 6 different model sizes with tradeoffs in speed and accuracy. Right now I’m using “turbo” which is pretty high on memory usage but gives good quality results. This is definitely the slow part of the pipeline; the time to transcript goes in hand with the video length.</p>
<p>I started to run this project on a <strong>Digital Ocean</strong> droplet, but my usual cheap choice (1 GB memory, 1 GB disk) definitely did not have enough memory to run Whisper. I did some quick tests with the OpenAI <a href="https://platform.openai.com/docs/guides/speech-to-text">Speech to text API</a> which worked great, so I might switch to that. In the meantime, this project runs on my MacBook Air.</p>
<p>To <strong>summarize</strong> the generated transcript text, I used my favorite <a href="https://llm.datasette.io/en/stable/">LLM command line tool</a> from the Datasette project and the GPT-4o mini model. These are the prompts I used:</p>
<ul>
<li>“summarize this transcript of a youtube video” → this one is very simple but you could also make it a bit richer by asking for pull quotes for example</li>
<li>“reformat this text to add punctuation and paragraph breaks where it makes sense; try to keep as much of the original text, while making it easier to read” → this helped make the full transcription text much easier to read while not changing the content too much</li>
</ul>
<p>Also this was the first time I used <strong>Github Copilot</strong> chat and autocomplete for a project from the start. The autocomplete in particular was surprisingly good with its suggestions; I suspect because this type of Python code is pretty common and I wasn’t exactly breaking any new ground here, but I had multiple “that’s exactly what I was going to type!” moments. I had mixed results with chat; for example I asked for a simple example of sending email with Python and it gave me a very convoluted example that didn’t actually work. Luckily the <a href="https://docs.python.org/3/library/email.examples.html">Python email module examples</a> were all I needed.</p>
<p><strong>Example</strong></p>
<p><img src="https://www.cantoni.org/images/honey-scam-thumbnail.jpg" alt="Thumbnail for the Honey Influencer Scam YouTube video"></p>
<p>Going back to the Honey Influencer Scam video, here’s a sample of what the summaries look like.</p>
<p>First, the simple “summarize this video” result:</p>
<blockquote>
<p>The video outlines an extensive investigation into Honey, a free browser extension owned by PayPal, that promises to automatically apply coupon codes at checkout to save consumers money. The narrator alleges that Honey not only exploits consumers but also engages in deceptive practices that defraud influencers and content creators out of their rightful affiliate commissions.</p>
<p>Key Points from the Investigation:</p>
<ol>
<li>Commission Theft: When consumers use Honey after clicking on an influencer’s affiliate link, Honey replaces the affiliate cookie with its own, claiming the commission for itself, even if it doesn’t find a working coupon. This practice undermines influencers who promote products, as Honey effectively steals their commissions.</li>
<li>Honey Gold (Cashback) Mechanism: Honey introduced a cashback feature (now called PayPal Rewards) that encourages users to click through Honey at checkout to earn rewards. This system further benefits Honey by allowing them to claim commissions instead of paying it to the referring influencer.</li>
<li>Deceptive Practices: Honey often only shows coupons that are approved by partnering stores, which allows those stores to control which discounts are promoted. This contradicts Honey’s claim of finding the best deals available, leading to no transparency for consumers who believe they are getting the best price.</li>
<li>Influencer Response: Over time, some influencers, notably Linus Tech Tips, recognized that Honey was undermining their affiliate links and eventually ended their partnerships with Honey. However, the narrator expresses disappointment that these influencers didn’t publicly address these issues sooner given their significant audience reach.</li>
<li>Misleading Marketing Claims: The video critiques Honey’s marketing strategy, highlighting exaggerated claims about its capabilities to find every coupon code on the internet. The Better Business Bureau even investigated Honey’s advertising but ceased after Honey discontinued claim practices for “business reasons.”</li>
<li>Consumer Impact: The investigation suggests that the practices of Honey could cost influencers, bloggers, and other small content creators millions in lost commissions while misleading consumers into thinking they are getting the best deals. The narrator emphasizes that this deceptive model would not be tolerated in a traditional retail environment.</li>
</ol>
<p>The video presents Honey as a largely predatory service that financially benefits at the expense of both influencers and consumers by exploiting the affiliate marketing system and manipulating coupon code promotions.</p>
</blockquote>
<p>Second, here’s a snippet of the reformatted original transcript which I was pretty impressed with:</p>
<blockquote>
<p>But now, you’ve probably heard about Honey, you know, the browser extension that saves you money. So, you have Honey installed? What’s Honey? Oh, no, no.</p>
<p>Honey? Honey is a free browser extension. Free browser extension. Free browser extension. But automatically applies coupon codes when you check out online. Free money, basically. Literally free money. It doesn’t make sense not to be using this.</p>
<p>So what’s the catch?</p>
<p>There is… okay, join Honey. Join Honey. Honey. Honey. Honey. Honey. Honey.</p>
<p>Yeah, I hate to break it to you, but your favorite influencers sold you a lie. Honey is a scam, and the majority of claims promoted by those influencers aren’t even remotely true. But it gets worse. Honey hasn’t just been scamming you, the consumer; they’ve also been stealing money from influencers, including the very ones they pay to promote their product. And I’m not just talking about a few bucks here. I believe the scam has likely cost content creators millions of dollars.</p>
<p>Sound crazy? Well, I didn’t believe it at first either. Until I experienced it myself, firsthand. In fact, I’m confident this might just be the biggest influencer scam of all time, which is insane considering Honey is owned and run by PayPal, who purchased this company for $4 billion. This three-part series is the result of a multi-year investigation where I believe I’ve uncovered signs of advertising fraud, affiliate fraud, the illegal collection of personal data, deception, lies, coercion, extortion… the list goes on. I’ve reviewed hundreds of documents, advertisements, sponsorships; I’ve reviewed emails between Honey and merchants, interviewed victims—believe me, this runs deep.</p>
<p>Now, I want to be clear: the views, allegations, and conclusions expressed in the series are my opinions, based on evidence I have gathered, which will be shared throughout. With that said, ladies and gentlemen, this is the Honey Trip.</p>
</blockquote>
<p><strong>Future Ideas and Conclusion</strong></p>
<p>Future ideas to explore:</p>
<ul>
<li>Include a time-stamped transcript in the result (Whisper generates a file you could use for this)</li>
<li>Explore Whisper models to see if a faster model can be just as accurate</li>
<li>Switch from local to cloud Whisper model and try running everything from a small Digital Ocean droplet</li>
<li>Play around with the LLM prompts more</li>
</ul>
<p>So does this meet my goal of making videos easier to understand without watching? I think so, especially for the videos which are mainly narrating as opposed to building or doing something. I’m running with a set of 6 YouTube channels I follow to see how it goes.</p>
</body></html></content>
</entry>
<entry>
<title>Forced to Retire Weather by Text Service</title>
<link href="https://www.cantoni.org/2020/11/19/retire-weather-by-text/" />
<updated>2020-11-20T13:16:55Z</updated>
<id>https://www.cantoni.org/2020/11/19/retire-weather-by-text/</id>
<content type="html"><p>I finally had to turn off the public number for my <a href="http://scooterlabs.com/wx/">free Weather by Text service</a> which I’ve been running for a few years. I was fine with the minimal cost running this on Twilio, but over the last couple of months someone has been abusing the number presumably with some automated script. The challenge with Twilio SMS is there is <strong>no way to block any abusive incoming text messages</strong>. Even though the pricing is pretty cheap ($ 0.0075 per message), it adds up quickly because you’re charged for both the incoming and outgoing messages. In the end, it’s not worth running a free service where one user can drive hundreds of abusive messages each day with no recourse.
Coincidentally it was one year ago today I <a href="http://www.cantoni.org/2019/11/19/weather-by-text-twilio-darksky">rewrote the whole service</a> using <a href="https://developer.here.com/">HERE</a> for geocoding and <a href="https://darksky.net/poweredby/">DarkSky</a> for weather forecasts. I still have the code up on GitHub (<a href="https://github.com/bcantoni/wxtext">bcantoni/wxtext</a>) for anyone interested in running it themselves or just seeing how I built it.</p>
<p><strong>Update:</strong> After turning the service off I did hear back from Twilio tech support who let me know there <em>is</em> a way to block incoming fraudulent traffic. That’s encouraging. In this case I already decommissioned the service, but good to know for the future.</p>
</content>
</entry>
</feed>
If you would like to create a banner that links to this page (i.e. this validation result), do the following:
Download the "valid Atom 1.0" banner.
Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)
Add this HTML to your page (change the image src
attribute if necessary):
If you would like to create a text link instead, here is the URL you can use:
http://www.feedvalidator.org/check.cgi?url=http%3A//www.cantoni.org/feed/feed.xml