<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Raymond Camden</title>
    <link href="https://www.raymondcamden.com/feed.xml" rel="self" type="application/atom+xml"></link>
    <link href="https://www.raymondcamden.com/" rel="alternate" type="text/html"></link>
    <subtitle>Father, husband, developer relations and web standards expert, and cat demo builder.</subtitle>

    <updated>2026-08-02T18:27:37+00:00</updated>
    <author>
        <name>Raymond Camden</name>
        <email>raymondcamden@gmail.com</email>
    </author>
    <id>https://www.raymondcamden.com/feed.xml</id>

    <generator>Eleventy</generator>

        
            <entry>
                <id>https://www.raymondcamden.com/2026/08/02/links-for-you-8226</id>
                <title>Links For You (8/2/26)</title>
                <updated>2026-08-02T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/08/02/links-for-you-8226" rel="alternate" type="text/html" title="Links For You (8/2/26)"/>
                <content type="html">
				
                        &lt;p&gt;Happy August folks. Louisiana is celebrating the approach of Fall by gradually increasing the temperatures to roughly that of the surface of the sun. Last night we saw the new Spider-Man, which was really good, although not as fantastic as &amp;quot;No Way Home&amp;quot;. The point of these posts is for me to share content from other folks, but I&apos;ll remind folks of my &lt;a href=&quot;https://youtube.com/user/theraymondcamden&quot;&gt;Youtube channel&lt;/a&gt;. This past week I shared a few new videos demonstrating some cool stuff with Cursor so if you&apos;ve not checked them out yet, give me a quick watch and let me know what you think!&lt;/p&gt;
&lt;h2 id=&quot;web-browsing-on-your-gaming-console&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#web-browsing-on-your-gaming-console&quot;&gt;Web browsing on your gaming console&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First up is a great post by Vale detailing the capabilities and &lt;a href=&quot;https://vale.rocks/posts/game-console-browsers&quot;&gt;history of web browsers on game consoles&lt;/a&gt;. I&apos;ve been playing on consoles since the &lt;em&gt;very&lt;/em&gt; beginning and a lot of this was still new and surprising to me. Honestly I can&apos;t imagine surfing any web page on a browser now. I think the last time I did was on my XBox so I could find a cool background picture.&lt;/p&gt;
&lt;h2 id=&quot;audio-visualization-with-wavesurfer.js&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#audio-visualization-with-wavesurfer.js&quot;&gt;Audio visualization with wavesurfer.js&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Next up is a cool JavaScript library for audio visualization, &lt;a href=&quot;https://wavesurfer.xyz/&quot;&gt;wavesurfer.js&lt;/a&gt;. Also I think this is the first site I&apos;ve seen on the &lt;code&gt;xyz&lt;/code&gt; TLD. The library works pretty simply - point it at a div and MP3 and it visualizes it on your web page. Here&apos;s a simple example of it in action.&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-height=&quot;500&quot; data-pen-title=&quot;wavesurfer.js&quot; data-version=&quot;2&quot; data-default-tab=&quot;js,result&quot; data-slug-hash=&quot;QwdzmKq&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/019fc392-2347-7ac0-aa1e-bc54f36d7ff0&quot;&gt;
  wavesurfer.js&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;what-the-hell-happened-to-frontend%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what-the-hell-happened-to-frontend%3F&quot;&gt;What the hell happened to frontend?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ah, the good ole days, hand crafting HTML in Notepad and manually &amp;quot;deploying&amp;quot; via carefully selecting and uploading via an FTP client. In the following few decades things got... a little bit more complex. This &lt;a href=&quot;https://davidpoblador.com/deep-dives/what-happened-to-the-frontend/&quot;&gt;great deep dive&lt;/a&gt; by David Poblador i Garcia goes into detail over the changes in frontend. Those of you new to this - you missed a lot. ;)&lt;/p&gt;
&lt;h2 id=&quot;just-for-fun&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#just-for-fun&quot;&gt;Just For Fun&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With AI being everywhere, I&apos;m curious how many of my readers ever saw &lt;a href=&quot;https://www.imdb.com/title/tt0087197/&quot;&gt;Electric Dreams&lt;/a&gt;, an &lt;em&gt;incredibly&lt;/em&gt; corny 80s film I remember watching on VHS over and over again. The story is... well... it was the 80s so that gives you an idea, but it had a pretty good songs in it including the main theme. Enjoy!&lt;/p&gt;
&lt;lite-youtube videoid=&quot;r9foZ7KVSng&quot; style=&quot;background-image: url(&apos;https://i.ytimg.com/vi/r9foZ7KVSng/hqdefault.jpg&apos;);&quot;&gt;
  &lt;a href=&quot;https://youtube.com/watch?v=r9foZ7KVSng&quot; class=&quot;lty-playbtn&quot; title=&quot;Play Video&quot;&gt;
    &lt;span class=&quot;lyt-visually-hidden&quot;&gt;Play Video&lt;/span&gt;
  &lt;/a&gt;
&lt;/lite-youtube&gt;
&lt;script defer src=&quot;https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js&quot;&gt;&lt;/script&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css&quot; integrity=&quot;sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; /&gt;
&lt;p&gt;

                        
                
				</content>

                
                <category term="links4you" />
                
                
                <category term="misc" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/29/updating-my-watch-site-for-algolia-support</id>
                <title>Updating my Watch Site for Algolia Support</title>
                <updated>2026-07-29T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/29/updating-my-watch-site-for-algolia-support" rel="alternate" type="text/html" title="Updating my Watch Site for Algolia Support"/>
                <content type="html">
				
                        &lt;p&gt;Alright, so I was supposed to share this a bit quicker, but life, work, etc, kept me busy. I&apos;ve had this particular change up for a few days but I&apos;m just now getting around to blogging about it. What changes?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Adding Algolia&apos;s MCP to my Cursor environment&lt;/li&gt;
&lt;li&gt;Using Algolia&apos;s MCP (what worked, what did not)&lt;/li&gt;
&lt;li&gt;Adding Algolia content as a scheduled service with Netlify&lt;/li&gt;
&lt;li&gt;Adding a basic search front end to &lt;a href=&quot;https://iwatchwatches.netlify.app/&quot;&gt;I Watch Watches&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before going on, be sure you&apos;ve read the &lt;a href=&quot;https://www.raymondcamden.com/2026/07/24/building-agentically-and-celebrating-watches&quot;&gt;last piece&lt;/a&gt; where I talked about agentic development in general, and specifically how I built the site.&lt;/p&gt;
&lt;h2 id=&quot;if-you-don&apos;t-have-a-log%2C-did-it-happen%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#if-you-don&apos;t-have-a-log%2C-did-it-happen%3F&quot;&gt;If you don&apos;t have a log, did it happen?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve been doing some work with different profiles and my Cursor environment, and in doing so, I lost the chat I had with Cursor about these features. I know it&apos;s there but I think I lost it the transition/testing I&apos;ve been doing. (I &lt;em&gt;did&lt;/em&gt; find the chat actually, but I&apos;ll still probably skip including it directly here.)&lt;/p&gt;
&lt;h2 id=&quot;the-plan---in-two-parts&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-plan---in-two-parts&quot;&gt;The Plan - in Two Parts&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I spelled this out above, but it bears repeating. I wanted to add search to my site and wanted to use &lt;a href=&quot;https://algolia.com&quot;&gt;Algolia&lt;/a&gt;. Algolia makes use of an index that&apos;s a copy of your site data. For me that&apos;s the RSS feed items from the watch blogs I aggregate. Creating and setting up the index is a one time operation. Populating the index and keeping it up to date is something you do forever. (To be technical, you&apos;ve got a lot of options in that regard. So for example, in a site with &lt;em&gt;millions&lt;/em&gt; of updates you could hold off updating your index until a particular time every day or other time period.) So the first operation was setting up that process.&lt;/p&gt;
&lt;p&gt;The second part is much simpler - wiring up the search UI. For that I used my interface here as a guide - a nav search field that loads up a search page with results, and another form there so you can quickly change your search.&lt;/p&gt;
&lt;h2 id=&quot;algolia-and-mcp&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#algolia-and-mcp&quot;&gt;Algolia and MCP&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Algolia&apos;s MCP &lt;a href=&quot;https://www.algolia.com/doc/guides/model-context-protocol&quot;&gt;docs&lt;/a&gt; were a bit confusing to me. The first example mentioned is the &amp;quot;Algolia Public MCP&amp;quot; - but this is the &amp;quot;per app&amp;quot; MCP I talked about here: &lt;a href=&quot;https://www.raymondcamden.com/2026/07/11/til-algolia-makes-creating-an-mcp-server-stupid-easy&quot;&gt;&amp;quot;TIL - Algolia Makes Creating an MCP Server Stupid Easy&amp;quot;&lt;/a&gt;. In other words, it&apos;s an MCP server for your data.&lt;/p&gt;
&lt;p&gt;Instead, you want the &lt;a href=&quot;https://www.algolia.com/doc/guides/model-context-protocol#algolia-productivity-mcp&quot;&gt;Algolia Productivity MCP&lt;/a&gt;, which is what&apos;s used to work with your account at a high level. But - right away you see:&lt;/p&gt;
&lt;p&gt;&amp;quot;Read-only index access, with an extended set of tools such as analytics.&amp;quot;&lt;/p&gt;
&lt;p&gt;So the MCP server can&apos;t actually make changes to your account, like creating or modifying an index. That&apos;s a bit of a bummer (and I let my buddy at Algolia know this), but it does give you MCP access via your agent so you can do things like list your indexes and perform searches right from your AI harness.&lt;/p&gt;
&lt;p&gt;Here&apos;s a test I did in Cursor:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/07/amcp.png&quot; loading=&quot;lazy&quot; alt=&quot;Cursor Response&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Pretty standard stuff, but notice both Cursor and Algolia&apos;s MCP gracefully handled the fact that I didn&apos;t specify a particular index for my account. It was smart enough to recognize which index would most likely have &lt;code&gt;rolex&lt;/code&gt; as a term.&lt;/p&gt;
&lt;p&gt;One more nit to be aware of. You have to &lt;em&gt;enable&lt;/em&gt; Algolia MCP. This is &lt;a href=&quot;https://www.algolia.com/doc/guides/model-context-protocol/productivity-mcp&quot;&gt;documented&lt;/a&gt;, but given that the URL is standard (not something special per index, it&apos;s for Algolia in general) and given you have to authenticate to use it, it felt weird that I also had to enable it in my account. This is also feedback I shared with my contact at Algolia.&lt;/p&gt;
&lt;p&gt;So given I couldn&apos;t use the MCP to &lt;em&gt;create&lt;/em&gt; the index, I just did it myself. By hand. Uphill. In the snow. Both ways. After the 20 to 30 seconds of that work was done, I was ready to start building.&lt;/p&gt;
&lt;h2 id=&quot;index-seeding-and-updating&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#index-seeding-and-updating&quot;&gt;Index Seeding and Updating&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My first choice was a bit difficult. For my blog here, I&apos;ve got a huge amount of content, and keeping Algolia in sync could be a bit complex. I went with an approach that gets recent items and does an upsert to my index using the batch API.&lt;/p&gt;
&lt;p&gt;For I Watch Watches, I decided to use the same API, the same upsert, but to pass everything. My spidey-sense tells me this may be problematic later as the aggregator grows, and I&apos;ll probably have to go with a &amp;quot;last 1k&amp;quot; or so filter, but for now it worked.&lt;/p&gt;
&lt;p&gt;In Cursor, I described my ask - create a Netlify scheduled function to upsert my data to my Algolia index. Netlify has my Algolia secrets and serverless functions can use it, so I set Cursor to building it out, and it did a great job. In fact, it did it a bit differently than I would.&lt;/p&gt;
&lt;p&gt;First, it created a core Algolia module in &lt;code&gt;src/lib/algolia.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { algoliasearch } from &amp;quot;algoliasearch&amp;quot;;
import { db } from &amp;quot;../db&amp;quot;;
import { feedItems } from &amp;quot;../db/schema&amp;quot;;

const BATCH_SIZE = 1000;

export type AlgoliaSyncSummary = {
  total: number;
  batches: number;
  indexName: string;
};

function getAlgoliaConfig() {
  const appId = process.env.ALGOLIA_APP_ID;
  const apiKey = process.env.ALGOLIA_SECRET_KEY;
  const indexName = process.env.ALGOLIA_INDEX;

  const missing = [
    !appId &amp;amp;&amp;amp; &amp;quot;ALGOLIA_APP_ID&amp;quot;,
    !apiKey &amp;amp;&amp;amp; &amp;quot;ALGOLIA_SECRET_KEY&amp;quot;,
    !indexName &amp;amp;&amp;amp; &amp;quot;ALGOLIA_INDEX&amp;quot;,
  ].filter(Boolean);

  if (missing.length &amp;gt; 0) {
    throw new Error(`Missing required Algolia environment variables: ${missing.join(&amp;quot;, &amp;quot;)}`);
  }

  return { appId: appId!, apiKey: apiKey!, indexName: indexName! };
}

export async function syncFeedItemsToAlgolia(): Promise&amp;lt;AlgoliaSyncSummary&amp;gt; {
  const { appId, apiKey, indexName } = getAlgoliaConfig();
  const client = algoliasearch(appId, apiKey);

  const rows = await db
    .select({
      id: feedItems.id,
      title: feedItems.title,
      url: feedItems.url,
      synopsis: feedItems.synopsis,
      publishedAt: feedItems.publishedAt,
    })
    .from(feedItems);

  const objects = rows.map((row) =&amp;gt; ({
    objectID: String(row.id),
    title: row.title,
    url: row.url,
    synopsis: row.synopsis,
    published_at: row.publishedAt ? row.publishedAt.toISOString() : null,
  }));

  if (objects.length === 0) {
    return { total: 0, batches: 0, indexName };
  }

  const responses = await client.saveObjects({
    indexName,
    objects,
    batchSize: BATCH_SIZE,
  });

  return {
    total: objects.length,
    batches: responses.length,
    indexName,
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then it built the serverless function which ended up being much smaller with the core logic abstracted.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import type { Config } from &amp;quot;@netlify/functions&amp;quot;;
import { syncFeedItemsToAlgolia } from &amp;quot;../../src/lib/algolia&amp;quot;;

export default async (req: Request) =&amp;gt; {
  const body = await req.json().catch(() =&amp;gt; ({}));

  try {
    const result = await syncFeedItemsToAlgolia();
    const summary = {
      next_run: body?.next_run ?? null,
      ...result,
    };

    console.log(&amp;quot;Algolia sync complete&amp;quot;, JSON.stringify(summary));

    return new Response(JSON.stringify(summary), {
      headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot; },
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : &amp;quot;Unknown error&amp;quot;;
    console.error(&amp;quot;Algolia sync failed&amp;quot;, message);

    return new Response(JSON.stringify({ error: message }), {
      status: 500,
      headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot; },
    });
  }
};

export const config: Config = {
  schedule: &amp;quot;@daily&amp;quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running locally, I was able to use curl to run this function and then the Algolia MCP to search and confirm it actually worked.&lt;/p&gt;
&lt;h2 id=&quot;searching&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#searching&quot;&gt;Searching&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Algolia has multiple options for performing searches, but for me the basic JavaScript SDK is all I need. I asked Cursor to implement that following the pattern I have on my blog here and as I described above. Cursor wired up the search in the top nav, built a new page, &lt;code&gt;search.astro&lt;/code&gt;, and mostly followed the same pattern I did here. Here&apos;s the complete Astro page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;---
import Header from &amp;quot;../components/Header.astro&amp;quot;;
import { SITE_NAME } from &amp;quot;../config&amp;quot;;
import BaseLayout from &amp;quot;../layouts/BaseLayout.astro&amp;quot;;
---

&amp;lt;BaseLayout title={`Search · ${SITE_NAME}`} description=&amp;quot;Search watch articles and news&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;site-shell&amp;quot;&amp;gt;
    &amp;lt;Header /&amp;gt;

    &amp;lt;main class=&amp;quot;search-page&amp;quot;&amp;gt;
      &amp;lt;h1&amp;gt;Search&amp;lt;/h1&amp;gt;

      &amp;lt;form class=&amp;quot;search-page-form&amp;quot; action=&amp;quot;/search&amp;quot; method=&amp;quot;get&amp;quot; role=&amp;quot;search&amp;quot;&amp;gt;
        &amp;lt;input
          id=&amp;quot;search-input&amp;quot;
          type=&amp;quot;search&amp;quot;
          name=&amp;quot;search&amp;quot;
          placeholder=&amp;quot;Search watches, brands, or articles&amp;quot;
          autocomplete=&amp;quot;off&amp;quot;
        /&amp;gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Search&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;

      &amp;lt;p id=&amp;quot;search-status&amp;quot; class=&amp;quot;search-status&amp;quot; aria-live=&amp;quot;polite&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;div id=&amp;quot;search-results&amp;quot; class=&amp;quot;search-results&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;

      &amp;lt;p class=&amp;quot;search-attribution&amp;quot;&amp;gt;
        Search by
        &amp;lt;a href=&amp;quot;https://www.algolia.com/&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;Algolia&amp;lt;/a&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/BaseLayout&amp;gt;

&amp;lt;script is:inline src=&amp;quot;https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script is:inline&amp;gt;
  (() =&amp;gt; {
    const appId = &amp;quot;0FJBPN4K5D&amp;quot;;
    const apiKey = &amp;quot;8f741f50b983176875b65e252402b140&amp;quot;;
    const indexName = &amp;quot;iwatchwatches&amp;quot;;

    const statusEl = document.getElementById(&amp;quot;search-status&amp;quot;);
    const resultsEl = document.getElementById(&amp;quot;search-results&amp;quot;);
    const inputEl = document.getElementById(&amp;quot;search-input&amp;quot;);

    const params = new URLSearchParams(window.location.search);
    const term = (params.get(&amp;quot;search&amp;quot;) || &amp;quot;&amp;quot;).trim();

    if (inputEl) {
      inputEl.value = term;
    }

    if (!term) {
      statusEl.textContent = &amp;quot;Enter a search term above to find articles.&amp;quot;;
      return;
    }

    if (typeof algoliasearch !== &amp;quot;function&amp;quot;) {
      statusEl.textContent = &amp;quot;Search is temporarily unavailable.&amp;quot;;
      return;
    }

    statusEl.textContent = &amp;quot;Searching…&amp;quot;;

    const client = algoliasearch(appId, apiKey);
    const index = client.initIndex(indexName);
    const dateFormatter = new Intl.DateTimeFormat(&amp;quot;en&amp;quot;, {
      month: &amp;quot;short&amp;quot;,
      day: &amp;quot;numeric&amp;quot;,
      year: &amp;quot;numeric&amp;quot;,
    });

    function escapeHtml(value) {
      return String(value)
        .replace(/&amp;amp;/g, &amp;quot;&amp;amp;amp;&amp;quot;)
        .replace(/&amp;lt;/g, &amp;quot;&amp;amp;lt;&amp;quot;)
        .replace(/&amp;gt;/g, &amp;quot;&amp;amp;gt;&amp;quot;)
        .replace(/&amp;quot;/g, &amp;quot;&amp;amp;quot;&amp;quot;)
        .replace(/&apos;/g, &amp;quot;&amp;amp;#39;&amp;quot;);
    }

    function formatDate(value) {
      if (!value) return &amp;quot;&amp;quot;;
      const date = new Date(value);
      if (Number.isNaN(date.getTime())) return &amp;quot;&amp;quot;;
      return dateFormatter.format(date).toUpperCase();
    }

    index
      .search(term, {
        hitsPerPage: 50,
        attributesToRetrieve: [&amp;quot;title&amp;quot;, &amp;quot;url&amp;quot;, &amp;quot;synopsis&amp;quot;, &amp;quot;published_at&amp;quot;],
      })
      .then((response) =&amp;gt; {
        const hits = response.hits || [];
        const count = response.nbHits ?? hits.length;

        if (hits.length === 0) {
          statusEl.textContent = &amp;quot;Sorry, but there were no results.&amp;quot;;
          resultsEl.innerHTML = &amp;quot;&amp;quot;;
          return;
        }

        statusEl.textContent =
          count === 1
            ? &amp;quot;There was one result for this search.&amp;quot;
            : `There were ${count} results for this search.`;

        resultsEl.innerHTML = hits
          .map((hit) =&amp;gt; {
            const title = escapeHtml(hit.title || &amp;quot;Untitled&amp;quot;);
            const url = escapeHtml(hit.url || &amp;quot;#&amp;quot;);
            const synopsis = hit.synopsis ? escapeHtml(hit.synopsis) : &amp;quot;&amp;quot;;
            const published = formatDate(hit.published_at);

            return `
              &amp;lt;article class=&amp;quot;search-result&amp;quot;&amp;gt;
                &amp;lt;h2 class=&amp;quot;search-result-title&amp;quot;&amp;gt;
                  &amp;lt;a href=&amp;quot;${url}&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;${title}&amp;lt;/a&amp;gt;
                &amp;lt;/h2&amp;gt;
                ${published ? `&amp;lt;p class=&amp;quot;search-result-meta&amp;quot;&amp;gt;${published}&amp;lt;/p&amp;gt;` : &amp;quot;&amp;quot;}
                ${synopsis ? `&amp;lt;p class=&amp;quot;search-result-synopsis&amp;quot;&amp;gt;${synopsis}&amp;lt;/p&amp;gt;` : &amp;quot;&amp;quot;}
              &amp;lt;/article&amp;gt;
            `;
          })
          .join(&amp;quot;&amp;quot;);
      })
      .catch(() =&amp;gt; {
        statusEl.textContent = &amp;quot;Search failed. Please try again.&amp;quot;;
        resultsEl.innerHTML = &amp;quot;&amp;quot;;
      });
  })();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In case you&apos;re curious, the key embedded in code is safe - it&apos;s a read only search key.&lt;/p&gt;
&lt;h2 id=&quot;show-me-the-code...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#show-me-the-code...&quot;&gt;Show me the code...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you want to see everything behind the aggregator, you can check out the repo here: &lt;a href=&quot;https://github.com/cfjedimaster/iwatchwatches&quot;&gt;https://github.com/cfjedimaster/iwatchwatches&lt;/a&gt;. Note that the readme was updated by Cursor and it did a dang good job. It handled updating the configuration as well as the &lt;code&gt;Changes&lt;/code&gt; section.&lt;/p&gt;
&lt;p&gt;The next change will be somewhat larger - a web based interface to add new feeds to the site - behind authentication of course.&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@shahrukh_rehman1?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Shahrukh Rehman&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/silver-and-black-chronograph-watch-05IxAEjVNl0?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/24/building-agentically-and-celebrating-watches</id>
                <title>Building Agentically and Celebrating Watches</title>
                <updated>2026-07-24T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/24/building-agentically-and-celebrating-watches" rel="alternate" type="text/html" title="Building Agentically and Celebrating Watches"/>
                <content type="html">
				
                        &lt;p&gt;I&apos;ve shared past experiences with vibe coding and it&apos;s been a fascinating way to experiment and build on a small scale, but while it works well for POCs and other small utilities (I won&apos;t say &apos;toys&apos; as I&apos;ve built useful, if small, things now multiple times), it isn&apos;t really a &amp;quot;process&amp;quot; and not one that would work over a project with a longer time frame. This is where &amp;quot;Agentic Development&amp;quot; comes in. It&apos;s not always referred to as such and it&apos;s a huge topic, but let me give you my opinion on it and then demonstrate how I used it to build my newest project, &lt;a href=&quot;https://iwatchwatches.netlify.app/&quot;&gt;I Watch Watches&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;agentic-development-in-a-nutshell&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#agentic-development-in-a-nutshell&quot;&gt;Agentic Development in a Nutshell&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Vibe coding is traditionally defined by a quick prompt passed to an AI agent. There may be a few rounds of changes afterwards, but the process is relatively simple and straightforward. Acceptable for a team of one and smaller projects, but not necessarily a &amp;quot;real&amp;quot; enterprise/larger environment.&lt;/p&gt;
&lt;p&gt;Agentic development integrates your AI agent or harness across an entire development process. Broadly speaking it entails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A planning stage where you may start with a prompt, but the agent doesn&apos;t write any code, but instead creates a plan. That plan is shared with you and you have the opportunity to adjust as you see fit. Add something, remove something, tweak something, this is where you and the agent agree on not just what is being done, but how. For developers who worry about AI taking your job, this is where your experience comes into play. I &lt;em&gt;always&lt;/em&gt; validate that the plan makes sense and is building how I (or my organization) would build.&lt;/li&gt;
&lt;li&gt;An implementation phase where the AI agent takes over - but even here the process can be far different from simple vibe coding. A good harness (like Cursor!) will let you define rules (&amp;quot;always do X&amp;quot;) as well as skills (&amp;quot;when doing Y, here is how we do it&amp;quot;) which lets you give firm guardrails for how code will be generated. This is especially useful in cases where you are working on a team within a large codebase and need to ensure you follow the established procedures for that project.&lt;/li&gt;
&lt;li&gt;Validation is also part of the process. A good agent/harness will not just build something but also validate that it actually worked. This could also be combined with creating tests to cover new features.&lt;/li&gt;
&lt;li&gt;On top of validating the change worked, you can also add an additional level of verification, so for example, when a PR is submitted, you can use an agent (at Cursor this is Bugbot, but of course other options exist).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s a pretty high level overview, but the end result is that you&apos;ve got AI agents working for you in a &lt;em&gt;much&lt;/em&gt; more controlled, and reliable way. Honestly it&apos;s already changed how I do a lot of my work even in cases where I&apos;ve written every bit of code. Having the agent there to help validate and check what I&apos;ve done is like having a virtual intern who never complains.&lt;/p&gt;
&lt;p&gt;So, how about an example?&lt;/p&gt;
&lt;h2 id=&quot;watches-are-my-bling&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#watches-are-my-bling&quot;&gt;Watches are my bling&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve been a huge fan of watches for pretty much all my life. I remember Swatches as a kid (I literally just signed up for a pre-order for a new one) and as I got older, I started collecting more and more watches and finding myself stopping at jewelry stores just to peruse their collections. I&apos;ve got some cheap ones, some expensive ones, and some weird ones, but I love them all.&lt;/p&gt;
&lt;p&gt;This is my current favorite and I&apos;ve got it on right now: &lt;a href=&quot;https://www.camdenwatchcompany.com/products/no253mementomorirosegold&quot;&gt;Memento Mori Rose&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/07/watch.png&quot; loading=&quot;lazy&quot; alt=&quot;Memento Mori&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Yes, this is a Camden watch, and the name initially caught my attention, but their collection is incredible (I have two). Even better, they literally check the timezone of where their watches are being shipped and will set the watch to the appropriate time.&lt;/p&gt;
&lt;p&gt;My typical way of discovering new watches is either seeing it on a person or seeing it on Facebook. Shockingly, if you Like a few watch ads, you end up seeing a lot more, and honestly I&apos;m fine with that. (Facebook is where I first saw the Camden Watch company.) A few days ago it finally clicked that there&apos;s probably a decent amount of watch blogs out there, why not start aggregating them so I can see watches my own way - in my own app.&lt;/p&gt;
&lt;h2 id=&quot;how-i-started&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-i-started&quot;&gt;How I Started&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So obviously I&apos;m using Cursor (where I work, just as a reminder!) and it&apos;s got a dedicated &lt;a href=&quot;https://cursor.com/docs/agent/plan-mode&quot;&gt;Plan mode&lt;/a&gt;. I switched to that and started off with this prompt:&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;p&gt;I want to build an RSS aggregator dedicated to watches. This is for consumers, people who love watches. The stack should be:&lt;/p&gt;
&lt;p&gt;Netlify for hosting
Netlify Database to store a table of feeds (name of site, url to site, rss url to site) and a table of feed items. That table drives the UI and is also used to recognized new versus existing feed items.
Astro 7.x (latest Astro)
Front end code should be minimal as it&apos;s just rendering database items, but if any code is needed, use vanilla JS or Alpine.js if the code gets complex (again, i tshouldn&apos;t)&lt;/p&gt;
&lt;p&gt;the app right now will one public page, the main page showing feed items aggregated. generally an aggregator is a list of links with title, time posted, the blog name, and a brief synopsis. that would be ok, but im kinda open to other UX options - perhaps a Card list of items in rows left to right (to the edge of screen of course)&lt;/p&gt;
&lt;p&gt;we also need a way to search + perhaps a filtering by tag system, although i checked two feeds and both didnt have categories or tags - so lets assume just search&lt;/p&gt;
&lt;p&gt;i will provide (at the very end of this prompt) a JSON list of feeds to begin with. (adding, editing, and deleting feeds is a V2 issue)&lt;/p&gt;
&lt;p&gt;design wise, i like dark, but lets support light/dark mode and store the preference in local storage so it persists&lt;/p&gt;
&lt;p&gt;feeds are driven by a serverless function. Astro supports API routes and in theory i could build a cron job, but lets use a Netlify Serverless function that has a CRON schedule (once a day i think) is fine. that function gets all the feeds, gets each item, and uses the item url as a unique identifier in the database to recognize if it needs to add an item&lt;/p&gt;
&lt;p&gt;i believe that&apos;s it. ask your follow up questions and also i want to see 2 to 3 design mocks for the front end before you begin&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;
&lt;p&gt;My stack was Astro, on Netlify, using their built-in database support, which is not something I&apos;ve actually used before. Note I gave direction on how aggregation should occur, specifically Netlify CRON-based serverless functions.&lt;/p&gt;
&lt;p&gt;When I submitted this, I got a few follow-up questions including my mocks. At some point (honestly I can&apos;t find it now), I directed Cursor to &lt;em&gt;not&lt;/em&gt; implement search for now. (If you visit the site today, search &lt;em&gt;does&lt;/em&gt; work, but that was a second session which I&apos;ll turn into a second post.) I also said I wanted pagination with ten items per page, but I told Cursor to ensure that number was easily configurable. I&apos;d say the follow-up questions and back and forth was probably about ten more minutes of work. When done, this plan was created. Note that the links below won&apos;t work as they are local to my project. They absolutely did work post creation which was super helpful to verify how things were done.&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;h1 id=&quot;build-i-watch-watches&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#build-i-watch-watches&quot;&gt;Build I Watch Watches&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;foundation-and-database&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#foundation-and-database&quot;&gt;Foundation and database&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Scaffold Astro 7 with the Netlify adapter and server rendering in &lt;a href=&quot;astro.config.mjs&quot;&gt;&lt;code&gt;astro.config.mjs&lt;/code&gt;&lt;/a&gt;; add Netlify Database, Drizzle beta packages, and RSS/Atom parsing dependencies.&lt;/li&gt;
&lt;li&gt;Define &lt;code&gt;feeds&lt;/code&gt; and &lt;code&gt;feed_items&lt;/code&gt; schemas in &lt;a href=&quot;src/db/schema.ts&quot;&gt;&lt;code&gt;src/db/schema.ts&lt;/code&gt;&lt;/a&gt;. Enforce uniqueness on feed URLs and article URLs, relate items to feeds, and index publication time for newest-first rendering.&lt;/li&gt;
&lt;li&gt;Add a generated migration under &lt;a href=&quot;drizzle/&quot;&gt;&lt;code&gt;drizzle/&lt;/code&gt;&lt;/a&gt; that creates both tables and seeds the six supplied feeds. Netlify will apply hosted migrations during deploy; local setup will use the Netlify Database CLI.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;daily-feed-ingestion&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#daily-feed-ingestion&quot;&gt;Daily feed ingestion&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Implement &lt;a href=&quot;netlify/functions/update-feeds.ts&quot;&gt;&lt;code&gt;netlify/functions/update-feeds.ts&lt;/code&gt;&lt;/a&gt; using the modern Netlify scheduled-function API with &lt;code&gt;schedule: &amp;quot;@daily&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fetch the six feeds concurrently, parse both RSS and Atom, normalize title/link/date/synopsis, extract an image from common enclosure/media/content fields, and insert new items with an article-URL conflict guard.&lt;/li&gt;
&lt;li&gt;Isolate failures per feed with &lt;code&gt;Promise.allSettled&lt;/code&gt;, log feed-level outcomes, and keep execution within the scheduled function’s 30-second limit.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;public-astro-page&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#public-astro-page&quot;&gt;Public Astro page&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Define &lt;code&gt;ITEMS_PER_PAGE = 10&lt;/code&gt; in &lt;a href=&quot;src/config.ts&quot;&gt;&lt;code&gt;src/config.ts&lt;/code&gt;&lt;/a&gt; so the page size can be changed in one place after reviewing live data.&lt;/li&gt;
&lt;li&gt;Query feed items server-side in &lt;a href=&quot;src/pages/index.astro&quot;&gt;&lt;code&gt;src/pages/index.astro&lt;/code&gt;&lt;/a&gt; using a validated &lt;code&gt;?page=&lt;/code&gt; parameter, a total-count query, and database &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt;; join source names and links, then render the first item on each page as the featured story and the other nine as a responsive magazine card grid matching Mock 2.&lt;/li&gt;
&lt;li&gt;Add semantic previous/next and numbered pagination controls, preserving a canonical first-page URL and returning an empty/not-found state for page numbers beyond the available results.&lt;/li&gt;
&lt;li&gt;Create small reusable Astro components under &lt;a href=&quot;src/components/&quot;&gt;&lt;code&gt;src/components/&lt;/code&gt;&lt;/a&gt; for the header, featured article, and article cards, including graceful image fallbacks and semantic external links.&lt;/li&gt;
&lt;li&gt;Add global responsive styling in &lt;a href=&quot;src/styles/global.css&quot;&gt;&lt;code&gt;src/styles/global.css&lt;/code&gt;&lt;/a&gt;: refined serif headings, sans-serif metadata, dark graphite/gold default styling, a complete light theme, and mobile/tablet layouts.&lt;/li&gt;
&lt;li&gt;Add minimal vanilla JavaScript for a no-flash theme preference stored in &lt;code&gt;localStorage&lt;/code&gt;. Render the search field as a clearly disabled/non-operational control ready for later Algolia wiring.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;configuration-and-verification&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#configuration-and-verification&quot;&gt;Configuration and verification&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Configure Netlify build/function settings in &lt;a href=&quot;netlify.toml&quot;&gt;&lt;code&gt;netlify.toml&lt;/code&gt;&lt;/a&gt;, document local database initialization, migrations, feed refresh, and deployment in &lt;a href=&quot;README.md&quot;&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/a&gt;, and ensure &lt;a href=&quot;.gitignore&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/a&gt; excludes &lt;code&gt;.netlify&lt;/code&gt; and local artifacts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify Astro type checking/build, migration validity, pagination boundaries and configurable page size, both theme states, responsive layouts, empty database behavior, malformed feed handling, and idempotent duplicate ingestion.&lt;/p&gt;
&lt;/script&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/zero-md&gt;
&lt;p&gt;I told Cursor to execute and it went to work. When it was done, I got this:&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;h2 id=&quot;assistant&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#assistant&quot;&gt;Assistant&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I Watch Watches is built and passing &lt;code&gt;astro build&lt;/code&gt; / &lt;code&gt;astro check&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What’s in place&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Astro 7 SSR + Netlify adapter&lt;/li&gt;
&lt;li&gt;Netlify Database schema (&lt;code&gt;feeds&lt;/code&gt;, &lt;code&gt;feed_items&lt;/code&gt;) with migrations + seed of your 6 feeds&lt;/li&gt;
&lt;li&gt;Daily scheduled function at &lt;code&gt;netlify/functions/update-feeds.ts&lt;/code&gt; (&lt;code&gt;@daily&lt;/code&gt;), unique on article URL&lt;/li&gt;
&lt;li&gt;Mock 2 UI: featured story + card grid, light/dark via &lt;code&gt;localStorage&lt;/code&gt;, disabled search placeholder&lt;/li&gt;
&lt;li&gt;Pagination at 10/page via &lt;code&gt;ITEMS_PER_PAGE&lt;/code&gt; in &lt;a href=&quot;src/config.ts&quot;&gt;&lt;code&gt;src/config.ts&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;To run locally&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx netlify database init --yes   # if not already set up
npm run db:migrate
npx netlify dev
curl http://localhost:8888/.netlify/functions/update-feeds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After deploy, trigger that function once so the first articles land before the daily cron.&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;
&lt;p&gt;I love, love, love how it ended with clear instructions on how to test myself, specifically the Netlify CLI database stuff that I&apos;d never used before. And I knew how to run a Netlify serverless function already, but the &lt;code&gt;curl&lt;/code&gt; command was a handy reminder. (As an FYI, a CRON serverless function for Netlify &lt;em&gt;can&lt;/em&gt; be called via HTTP locally, but not in production. That&apos;s handy!)&lt;/p&gt;
&lt;p&gt;At this point I was able to test and it worked perfectly... almost. One blog&apos;s items were returning broken images. I asked Cursor about that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;it looks like the media for ablogtowatch all return broken images. investigate why
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&apos;s the response. Honestly, I do not think I could have figured this out on my own:&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;p&gt;Found it: aBlogtoWatch&apos;s image server uses referer-based hotlink protection.&lt;/p&gt;
&lt;p&gt;The feed parsing is fine — we extract a valid URL like &lt;code&gt;https://www.ablogtowatch.com/wp-content/uploads/2026/07/CronusArt-RT06-SP-FI-768x346.jpg&lt;/code&gt; from the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; inside &lt;code&gt;content:encoded&lt;/code&gt;. The URL itself works. What breaks is loading it from &lt;em&gt;our&lt;/em&gt; page. Test results against that exact URL:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;Referer&lt;/code&gt; header → &lt;strong&gt;200&lt;/strong&gt;, image served&lt;/li&gt;
&lt;li&gt;Browser user agent, no referer → &lt;strong&gt;200&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Browser user agent + cross-site &lt;code&gt;Referer&lt;/code&gt; (what a browser sends when our page embeds their image) → &lt;strong&gt;403&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the CDN in front of aBlogtoWatch rejects any image request whose referer isn&apos;t their own site. Hodinkee, Fratello, etc. don&apos;t do this, which is why only aBlogtoWatch cards show broken images.&lt;/p&gt;
&lt;p&gt;Two ways to fix it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Add &lt;code&gt;referrerpolicy=&amp;quot;no-referrer&amp;quot;&lt;/code&gt; to the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags&lt;/strong&gt; in &lt;code&gt;ArticleCard.astro&lt;/code&gt; and &lt;code&gt;FeaturedArticle.astro&lt;/code&gt;. The browser then omits the referer entirely, and their server returns 200 (confirmed above). One-line change, no infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proxy through Netlify Image CDN&lt;/strong&gt; (&lt;code&gt;/.netlify/images?url=...&lt;/code&gt; with their domain in the remote-images allowlist). More robust — also normalizes sizes and formats — but the fetch originates from Netlify with no referer anyway, so it solves the same problem with more moving parts.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Option 1 is the fix I&apos;d make; want me to apply it?&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;
&lt;p&gt;I selected the first option and let Cursor implement it.&lt;/p&gt;
&lt;p&gt;After this, I made a few more changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Initially the footer just repeated the title. I had it link to my blog instead.&lt;/li&gt;
&lt;li&gt;Cursor can generate favicons, so I asked for three options and picked one. When I did, Cursor went ahead and wired it into the HTML.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what&apos;s-next&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what&apos;s-next&quot;&gt;What&apos;s Next&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I hope this made sense and I hope the power and flexibility of how I built it is clear. As I mentioned, I&apos;ve already done improvements, but I want to cover that in the next blog post. You can find the site here, &lt;a href=&quot;https://iwatchwatches.netlify.app/&quot;&gt;https://iwatchwatches.netlify.app/&lt;/a&gt;. I may spring for a real domain later. You can find the code here, &lt;a href=&quot;https://github.com/cfjedimaster/iwatchwatches&quot;&gt;https://github.com/cfjedimaster/iwatchwatches&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/20/copying-html-tables-as-text-markdown-and-csv</id>
                <title>Copying HTML Tables as Text, Markdown, and CSV</title>
                <updated>2026-07-20T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/20/copying-html-tables-as-text-markdown-and-csv" rel="alternate" type="text/html" title="Copying HTML Tables as Text, Markdown, and CSV"/>
                <content type="html">
				
                        &lt;p&gt;Earlier today and in a non web-related conversation, I saw someone talk about copying tabular data into different formats, like plain text and CSV. It got me thinking - this could be a useful feature for web sites and surely something pretty trivial to do in JavaScript. So I whipped up a quick demo.&lt;/p&gt;
&lt;p&gt;First, I added a table of course:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;table id=&amp;quot;table1&amp;quot;&amp;gt;
	&amp;lt;thead&amp;gt;
		&amp;lt;tr&amp;gt;
			&amp;lt;th&amp;gt;Name&amp;lt;/th&amp;gt;
			&amp;lt;th&amp;gt;DOB&amp;lt;/th&amp;gt;
			&amp;lt;th&amp;gt;Gender&amp;lt;/th&amp;gt;
			&amp;lt;th&amp;gt;Breed&amp;lt;/th&amp;gt;
		&amp;lt;/tr&amp;gt;
	&amp;lt;/thead&amp;gt;
	&amp;lt;tbody&amp;gt;
	&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Whiskers&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;2019-03-14&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Male&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Maine Coon&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
	&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Luna&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;2020-07-22&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Female&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Siamese&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
	&amp;lt;!-- bunch more rows --&amp;gt;
	&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Nala&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;2022-04-18&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Female&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Persian&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
	&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Molly&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;2022-01-11&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;Female&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;LaPerm&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;				
	&amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alright, so to enable this feature, I thought data attributes might be nice. This would let you use it on anything, a button, a link, image, etc. The data attribute would be responsible for pointing to the table (via an id) and specifying a format. Here&apos;s an example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- data-copy defaults to text --&amp;gt;
&amp;lt;a href=&amp;quot;&amp;quot; data-table=&amp;quot;table1&amp;quot;&amp;gt;Copy as Text&amp;lt;/a&amp;gt; ~ 
&amp;lt;a href=&amp;quot;&amp;quot; data-table=&amp;quot;table1&amp;quot; data-copy=&amp;quot;md&amp;quot;&amp;gt;Copy as Markdown&amp;lt;/a&amp;gt; ~
&amp;lt;a href=&amp;quot;&amp;quot; data-table=&amp;quot;table1&amp;quot; data-copy=&amp;quot;csv&amp;quot;&amp;gt;Copy as CSV&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now all I need to do is wire it up to code that looks for &lt;code&gt;data-table&lt;/code&gt; and adds the appropriate handlers. The first part is trivial:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const links = document.querySelectorAll(&apos;*[data-table]&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that while I named the variable &lt;code&gt;links&lt;/code&gt;, you can still use this with buttons or images.&lt;/p&gt;
&lt;p&gt;I loop over each match and first get the table referenced, throwing an error in console if it doesn&apos;t exist:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;links.forEach(l =&amp;gt; {
	let table = document.querySelector(`#${l.dataset.table}`);
	if(!table) {
		console.error(`Unable to connect to table with id ${l.dataset.table}`);
		return;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I check the type, defaulting to text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let type = l.dataset.copy || &apos;text&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final bit is to assign the click handler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;l.addEventListener(&apos;click&apos;, async e =&amp;gt; {
	e.preventDefault();
	console.log(`about to copy table ${table.id} to ${type}`);
	let data = getRawData(table);
	let formattedData = formatData(data, type);
	await navigator.clipboard.writeText(formattedData);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each, I get the raw data from the HTML table, convert it to the right format, and then write to the clipboard. (Check out my &lt;a href=&quot;https://master.dev/blog/writing-to-the-clipboard-in-javascript/&quot;&gt;article&lt;/a&gt; on working with the clipboard in JavaScript.)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getRawData&lt;/code&gt; just iterates through the table&apos;s DOM creating a 2D array:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const getRawData = t =&amp;gt; {
	let data = [];
	t.querySelectorAll(&apos;tr&apos;).forEach(r =&amp;gt; {
		let row = [];
		r.querySelectorAll(&apos;th, td&apos;).forEach(d =&amp;gt; {
			row.push(d.innerText);
		});
		data.push(row);
	});
	return data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;formatData&lt;/code&gt; is a bit more complex and I used AI to help me with each of the main sections. I added support for plain text, Markdown, and CSV:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const formatData = (data,type) =&amp;gt; {
	let numCols, colWidths, lines, buildRow, buildDivider;
	
	switch(type) {
		case &amp;quot;text&amp;quot;: 

			numCols = Math.max(...data.map(r =&amp;gt; r.length));
			// Compute max width for each column
			colWidths = Array(numCols).fill(0);
			for (const row of data) {
			for (let i = 0; i &amp;lt; numCols; i++) {
				const cell = row[i] !== undefined ? String(row[i]) : &apos;&apos;;
				colWidths[i] = Math.max(colWidths[i], cell.length);
			}
			}
		
			buildDivider = () =&amp;gt;
			&apos;+&apos; + colWidths.map(w =&amp;gt; &apos;-&apos;.repeat(w + 2)).join(&apos;+&apos;) + &apos;+&apos;;
		
			buildRow = (row) =&amp;gt;
			&apos;|&apos; + colWidths.map((w, i) =&amp;gt; {
				const cell = row[i] !== undefined ? String(row[i]) : &apos;&apos;;
				return &apos; &apos; + cell.padEnd(w) + &apos; &apos;;
			}).join(&apos;|&apos;) + &apos;|&apos;;
		
			lines = [];
			lines.push(buildDivider());
			lines.push(buildRow(data[0])); // treat first row as header
			lines.push(buildDivider());
			for (let i = 1; i &amp;lt; data.length; i++) {
			lines.push(buildRow(data[i]));
			}
			lines.push(buildDivider());
		
			return lines.join(&apos;\n&apos;);

		case &amp;quot;md&amp;quot;:
		case &amp;quot;markdown&amp;quot;: 
			numCols = Math.max(...data.map(r =&amp;gt; r.length));
			
			buildRow = (row) =&amp;gt;
				&apos;| &apos; + Array.from({ length: numCols }, (_, i) =&amp;gt;
					row[i] !== undefined ? String(row[i]) : &apos;&apos;
				).join(&apos; | &apos;) + &apos; |&apos;;
		
			lines = [];
			lines.push(buildRow(data[0])); // header
			lines.push(&apos;| &apos; + Array(numCols).fill(&apos;---&apos;).join(&apos; | &apos;) + &apos; |&apos;);
			for (let i = 1; i &amp;lt; data.length; i++) {
				lines.push(buildRow(data[i]));
			}
		
			return lines.join(&apos;\n&apos;);

		case &amp;quot;csv&amp;quot;: 
			const escapeCell = (value) =&amp;gt; {
			const str = value !== undefined &amp;amp;&amp;amp; value !== null ? String(value) : &apos;&apos;;
			// Quote if it contains comma, quote, or newline
			if (/[&amp;quot;,\n\r]/.test(str)) {
				return &apos;&amp;quot;&apos; + str.replace(/&amp;quot;/g, &apos;&amp;quot;&amp;quot;&apos;) + &apos;&amp;quot;&apos;;
			}
			return str;
			};
		
			return data.map(row =&amp;gt; row.map(escapeCell).join(&apos;,&apos;)).join(&apos;\n&apos;);
			
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With my test table (and I&apos;m stripping out a bit here for brevity), here&apos;s the plain text version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+----------+------------+--------+----------------------+
| Name     | DOB        | Gender | Breed                |
+----------+------------+--------+----------------------+
| Whiskers | 2019-03-14 | Male   | Maine Coon           |
| Luna     | 2020-07-22 | Female | Siamese              |
| Oliver   | 2018-11-05 | Male   | British Shorthair    |
| Max      | 2019-06-25 | Male   | Sphynx               |
| Stella   | 2021-05-04 | Female | American Curl        |
| Rocky    | 2018-09-20 | Male   | Burmese              |
| Molly    | 2022-01-11 | Female | LaPerm               |
+----------+------------+--------+----------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s the Markdown:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| Name | DOB | Gender | Breed |
| --- | --- | --- | --- |
| Whiskers | 2019-03-14 | Male | Maine Coon |
| Luna | 2020-07-22 | Female | Siamese |
| Oliver | 2018-11-05 | Male | British Shorthair |
| Max | 2019-06-25 | Male | Sphynx |
| Stella | 2021-05-04 | Female | American Curl |
| Rocky | 2018-09-20 | Male | Burmese |
| Molly | 2022-01-11 | Female | LaPerm |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&apos;s CSV:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Name,DOB,Gender,Breed
Whiskers,2019-03-14,Male,Maine Coon
Luna,2020-07-22,Female,Siamese
Oliver,2018-11-05,Male,British Shorthair
Max,2019-06-25,Male,Sphynx
Stella,2021-05-04,Female,American Curl
Rocky,2018-09-20,Male,Burmese
Molly,2022-01-11,Female,LaPerm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wrapped this all up in a function that you could add to your page. After adding it, don&apos;t forget to actually add the links/button/etc to enable it.&lt;/p&gt;
&lt;p&gt;There&apos;s two issues with my code that come to mind, and I&apos;d love people to comment in with their ideas. First, both the text and Markdown assume the first row is a header. In theory that might not always be accurate. I could update the code to see if the first cell in the first row is &lt;code&gt;th&lt;/code&gt; versus &lt;code&gt;td&lt;/code&gt;, but that kinda feels like overkill.&lt;/p&gt;
&lt;p&gt;The second issue that comes to mind is user feedback. Usually these types of things will provide some kind of visual feedback (&amp;quot;Data copied!&amp;quot;). My code does not. I honestly don&apos;t quite know how I&apos;d do that. Perhaps a data attribute that would replace the text (assuming it&apos;s a link or button) temporarily? I&apos;m not sure.&lt;/p&gt;
&lt;p&gt;If you&apos;ve got ideas, leave me a comment below. Here&apos;s the demo:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-height=&quot;500&quot; data-pen-title=&quot;Copy Table As (2)&quot; data-version=&quot;2&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;xbgJOyP&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/019f80d2-42ea-7545-83bb-8f112023abce&quot;&gt;
  Copy Table As (2)&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
                        
                
				</content>

                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/18/links-for-you-71826</id>
                <title>Links For You (7/18/26)</title>
                <updated>2026-07-18T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/18/links-for-you-71826" rel="alternate" type="text/html" title="Links For You (7/18/26)"/>
                <content type="html">
				
                        &lt;p&gt;Good morning, programs. I&apos;m currently at a conference (&lt;a href=&quot;https://www.sqlsatbr.com/&quot;&gt;Day of Data&lt;/a&gt;), but thankfully one just down the road. I&apos;ll be back tonight, and thought I&apos;d take a break between sessions to share some links. I&apos;ll be presenting later today one of my favorite talks, &amp;quot;A Beginner&apos;s Guide to Wrangling Asynchronicity in JavaScript&amp;quot;. I love the talk, but given I&apos;m up against &lt;em&gt;eight&lt;/em&gt; other talks at the same time and I&apos;m not talking AI... well it may be a small personal affair. We shall see.&lt;/p&gt;
&lt;h2 id=&quot;building-a-blog-with-astro&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#building-a-blog-with-astro&quot;&gt;Building a Blog with Astro&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It&apos;s been a hot minute since I&apos;ve written about &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;. I first started using it since Webflow was a sponsor and found that I &lt;em&gt;really&lt;/em&gt; dug it, but over the past couple of months I&apos;ve not had the bandwidth to keep playing with it. That being said, I keep my eyes open for cool posts and this by Zell is awesome: &lt;a href=&quot;https://zellwk.com/blog/real-astro-blog-made-easy/&quot;&gt;Building a Real Blog Easily in Astro&lt;/a&gt;. I&apos;m pretty sure my first blog post on Astro showed a basic blog, but this article goes really deep.&lt;/p&gt;
&lt;h2 id=&quot;interactive-svgs-and-pdfs&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#interactive-svgs-and-pdfs&quot;&gt;Interactive SVGs and PDFs&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As they say, you can take the dev out of PDFs but not the PDFs out of the dev. I&apos;m now two... three? years post Adobe and Foxit and I still perk up when I see a blog post about PDFs. For today, I&apos;m sharing a post that discusses &lt;a href=&quot;https://vexlio.com/blog/svgs-and-pdfs-can-both-be-interactive/&quot;&gt;interactivity with SVGs and PDFs&lt;/a&gt;. The SVG part is pretty much what you expect - DOM stuff and JavaScript. The PDF part is a bit more esoteric as it &lt;a href=&quot;https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/index.html&quot;&gt;references&lt;/a&gt; a JavaScript engine built into PDFs from like the Mesozoic era of computers.&lt;/p&gt;
&lt;h2 id=&quot;animating-a-dialog&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#animating-a-dialog&quot;&gt;Animating a Dialog&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog&quot;&gt;Dialog&lt;/a&gt; element still feels relatively new, but has great support (even in Safari) so there&apos;s no reason not to use it. The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API&quot;&gt;View Transition API&lt;/a&gt; also has stellar support. So this &lt;a href=&quot;https://pqina.nl/blog/animating-the-dialog-element-using-view-transitions/&quot;&gt;article&lt;/a&gt; by &lt;a href=&quot;https://pqina.nl/&quot;&gt;pqina&lt;/a&gt; is a great example of combining the two. I&apos;ve used Dialog once or twice and want to use it more. The View Transition API still hasn&apos;t quite &amp;quot;clicked&amp;quot; with me yet but I&apos;d like to use it more (where appropriate).&lt;/p&gt;
&lt;h2 id=&quot;just-for-fun&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#just-for-fun&quot;&gt;Just For Fun&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve shared &lt;a href=&quot;https://www.postmodernjukebox.com/&quot;&gt;Post Modern Jukebox&lt;/a&gt; a few times here so why not once again. Here&apos;s their version of &amp;quot;Red Right Hand&amp;quot; by Nick Cave. Enjoy!&lt;/p&gt;
&lt;lite-youtube videoid=&quot;T6I7AozTxdE&quot; style=&quot;background-image: url(&apos;https://i.ytimg.com/vi/T6I7AozTxdE/hqdefault.jpg&apos;);&quot;&gt;
  &lt;a href=&quot;https://youtube.com/watch?v=T6I7AozTxdE&quot; class=&quot;lty-playbtn&quot; title=&quot;Play Video&quot;&gt;
    &lt;span class=&quot;lyt-visually-hidden&quot;&gt;Play Video&lt;/span&gt;
  &lt;/a&gt;
&lt;/lite-youtube&gt;
&lt;script defer src=&quot;https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js&quot;&gt;&lt;/script&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css&quot; integrity=&quot;sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; /&gt;
&lt;p&gt;

                        
                
				</content>

                
                <category term="links4you" />
                
                
                <category term="misc" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/11/til-algolia-makes-creating-an-mcp-server-stupid-easy</id>
                <title>TIL - Algolia Makes Creating an MCP Server Stupid Easy</title>
                <updated>2026-07-11T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/11/til-algolia-makes-creating-an-mcp-server-stupid-easy" rel="alternate" type="text/html" title="TIL - Algolia Makes Creating an MCP Server Stupid Easy"/>
                <content type="html">
				
                        &lt;p&gt;A few days ago I was chatting with &lt;a href=&quot;https://www.linkedin.com/in/chuckm/&quot;&gt;Chuck Meyer&lt;/a&gt; (devrel for &lt;a href=&quot;https://algolia.com&quot;&gt;Algolia&lt;/a&gt;) and I casually mentioned, &amp;quot;It sure would be cool if Algolia had an easy way to turn my search index into an MCP server.&amp;quot; He promptly responded, &amp;quot;Of course, you complete and utter dufus, which is why we already &lt;em&gt;have&lt;/em&gt; that feature.&amp;quot; (Not an exact quote.) This is &lt;em&gt;exactly&lt;/em&gt; the kind of thing I love to hear. I&apos;ve been using Algolia for my &lt;a href=&quot;/search&quot;&gt;search&lt;/a&gt; here for &lt;em&gt;years&lt;/em&gt; and honestly have not paid much attention to the new additions on their platform. That was a mistake. Let me show you how dang easy they made this.&lt;/p&gt;
&lt;h2 id=&quot;first---what&apos;s-algolia%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#first---what&apos;s-algolia%3F&quot;&gt;First - what&apos;s Algolia?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Chuck, forgive me, I&apos;m going to do a horrible job summarizing your entire company into a few sentences. &lt;a href=&quot;https://algolia.com&quot;&gt;Algolia&lt;/a&gt; provides search as a service. You create an index (or multiple) that represent what you want to search, whether it be textual content (like this blog) or ecommerce products (perhaps Dungeon Crawler Carl merch). Via multiple ways, you populate this index and keep it up to date (when you create/edit/delete content locally, you make a corresponding edit to your index).&lt;/p&gt;
&lt;p&gt;Once the index is created (and tuned, you can do a &lt;em&gt;lot&lt;/em&gt; to tune how your index works), you can then search it. My &lt;a href=&quot;/search&quot;&gt;search&lt;/a&gt; is just one simple example, but it&apos;s one JavaScript library and a few lines of code.&lt;/p&gt;
&lt;p&gt;That&apos;s basically it. Well no, there&apos;s a &lt;em&gt;lot&lt;/em&gt; more, but at least for my implementation here, that&apos;s basically it.&lt;/p&gt;
&lt;h2 id=&quot;second---what&apos;s-mcp%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#second---what&apos;s-mcp%3F&quot;&gt;Second - what&apos;s MCP?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;MCP boils down to &amp;quot;if a person is needing to do some kind of action, an MCP server can tell the AI agent, hey, I handle that, just hand it off to me!&amp;quot; At Webflow, for example (sorry, you lay me off you don&apos;t get links), the MCP server handled API calls to your Webflow site and integrated with Cursor, Claude, and other agents. If you asked something like, &amp;quot;What Webflow sites do I have&amp;quot;, because the Webflow MCP server was added to your agent, and authenticated, it could handle doing the appropriate API calls and returning it such that your agent could document the result.&lt;/p&gt;
&lt;p&gt;For a documentation site, an MCP server can help you use free form questions about the docs and get responses in your AI agent.&lt;/p&gt;
&lt;h2 id=&quot;third---ok%2C-now-what%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#third---ok%2C-now-what%3F&quot;&gt;Third - Ok, now what?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To enable it in Algolia, you log in to your dashboard and select the appropriate application (an application can have one or more indexes), and then click Generate AI in the navigation. In the sub nav, you&apos;ll have a section titled &amp;quot;MCP Servers / Public&amp;quot;, and then in that UI, just click the &amp;quot;Create Public MCP Server&amp;quot; button.&lt;/p&gt;
&lt;p&gt;On that UI, you can select what indexes and tools to enable. I&apos;ve only got one &amp;quot;real&amp;quot; index, so I picked that and gave my server an incredibly useful and informative name:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/07/alg1.png&quot; loading=&quot;lazy&quot; alt=&quot;Algolia MCP Settings&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;On the next page, you can select which tools you want enabled. I picked everything, but I&apos;m not using the Recommendation product so I probably should turn that off.&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/07/alg2.png&quot; loading=&quot;lazy&quot; alt=&quot;Algolia MCP Settings - Tool selection&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;The end result is the URL to your MCP server. Mine is https://0FJBPN4K5D.algolia.net/mcp/1/vMOWpYc9RK6FuHnWxZcoyg/mcp. At this point, you follow your &amp;quot;usual procedure&amp;quot; on adding MCP to your tools. I tested in a few, including Cursor of course.&lt;/p&gt;
&lt;h2 id=&quot;actually-using-it...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#actually-using-it...&quot;&gt;Actually using it...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Two big use cases come to mind for this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Externally, this creates a &lt;em&gt;very&lt;/em&gt; powerful way for users to interact with your documentation. Most documentation for technical products already include this, including &lt;a href=&quot;https://www.algolia.com/doc&quot;&gt;Algolia&apos;s docs&lt;/a&gt; of course. But the target user here is your audience.&lt;/li&gt;
&lt;li&gt;Internally, this could be very powerful. As a content writer, I can already use the existing Algolia search as a quick way to see, &amp;quot;did I blog about X&amp;quot;, but with my MCP server, I can ask deeper questions, and even use it to help create &lt;em&gt;new&lt;/em&gt; content.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I did some testing related to both of these scenarios, not terribly deep, but enough to be pretty impressed.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;agy&lt;/code&gt;, the Antigravity CLI (Google Gemini&apos;s models), I asked:&lt;/p&gt;
&lt;blockquote&gt;
search Ray&apos;s mcp server for his opinion on React
&lt;/blockquote&gt;
&lt;p&gt;And the response was near perfect:&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;p&gt;Based on the search results from Ray&apos;s MCP server (which indexes his blog posts), Raymond Camden&apos;s view of React has transitioned from passive avoidance to active experimentation, and finally to deliberate
exclusion.&lt;/p&gt;
&lt;p&gt;Here is a summary of Ray&apos;s opinion on React:&lt;/p&gt;
&lt;h3 id=&quot;1.-historical-apprehension-%26-vue%2Falpine-preference-(pre-2025)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#1.-historical-apprehension-%26-vue%2Falpine-preference-(pre-2025)&quot;&gt;1. Historical Apprehension &amp;amp; Vue/Alpine Preference (Pre-2025)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;• Early Thoughts (2015): Ray attended a React session by Brian Holt at Fluent Conf in 2015. While he noted that writing ES6 classes with React felt &amp;quot;pretty freaking cool,&amp;quot; he did not end up adopting it.
• &amp;quot;Not for Me&amp;quot; (2023): In a September 2023 post, Ray shared Josh Collinsworth&apos;s article &amp;quot;Things you forgot (or never knew) because of React&amp;quot;, commenting that React had always felt like a &amp;quot;good framework
that&apos;s not for me&amp;quot;. He preferred Vue.js for full web apps and later Alpine.js for lightweight interactivity.&lt;/p&gt;
&lt;h3 id=&quot;2.-the-%22let&apos;s-learn-react%22-experiment-(march%E2%80%93may-2025)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#2.-the-%22let&apos;s-learn-react%22-experiment-(march%E2%80%93may-2025)&quot;&gt;2. The &amp;quot;Let&apos;s Learn React&amp;quot; Experiment (March–May 2025)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;• Giving it a &amp;quot;Fair Shake&amp;quot; (March 2025): Ray attempted to learn the framework during his Code Break livestream series, in a session titled &amp;quot;I Don&apos;t like React. Let&apos;s Learn React!&amp;quot; He clarified: &amp;quot;To be clear,
it isn&apos;t that I dislike React, it&apos;s just that every time I&apos;ve looked at the code, it just didn&apos;t gel with me.&amp;quot;
• The Struggle and Ditching (May 2025): The learning process proved to be &amp;quot;painful&amp;quot; and did not go well. By May 2025, Ray decided to stop, stating: &amp;quot;I&apos;m ditching any further exploration of React for now, but
instead turning my attention to something I think is really fun - building desktop apps.&amp;quot;&lt;/p&gt;
&lt;h3 id=&quot;3.-banning-react-%26-%22vanilla-ftw%22-(late-2025)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#3.-banning-react-%26-%22vanilla-ftw%22-(late-2025)&quot;&gt;3. Banning React &amp;amp; &amp;quot;Vanilla FTW&amp;quot; (Late 2025)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;• Running Joke with AI (July–September 2025): Ray began specifically instructing AI coding tools to avoid React altogether: &amp;quot;We are only using HTML, CSS, and JavaScript. No libraries. No React. No Preact.
Don&apos;t even say the word React. Just vanilla JavaScript.&amp;quot; He noted that AI assistants &amp;quot;really, really, really want&amp;quot; to use React, prompting him to actively decline it.
• My Tech Stack (December 2025): In his &amp;quot;My Tech Stack (2025)&amp;quot; post, Ray summarized: &amp;quot;Vanilla JavaScript FTW! I&apos;ll still go to Alpine.js for more complex needs, but I don&apos;t use Vue anymore and I definitely
don&apos;t use React if I can help it.&amp;quot;&lt;/p&gt;
&lt;h3 id=&quot;summary-conclusion&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#summary-conclusion&quot;&gt;Summary Conclusion&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Ray&apos;s ultimate opinion is that React is a popular and powerful framework but is not for him. He values simplicity, lightweight payloads, and vanilla web standards, preferring to build with Vanilla
JavaScript, Alpine.js, Eleventy, and Astro.&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;
&lt;p&gt;&amp;quot;deliberate exclusion&amp;quot; is the name of my Goth band.&lt;/p&gt;
&lt;p&gt;In Cursor, I added the MCP server via the app settings and took the approach of the content creator. I asked:&lt;/p&gt;
&lt;blockquote&gt;
using Ray&apos;s MCP server, what do you recommend for a new blog post?
&lt;/blockquote&gt;
&lt;p&gt;And here&apos;s what Cursor and my MCP server came back with:&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;p&gt;Based on Ray&apos;s MCP server search data, here are my top recommendations for his next blog post — ranked by how naturally they follow what he&apos;s already writing.&lt;/p&gt;
&lt;h2 id=&quot;1.-%22automating-blog-reviews-with-cursor-hooks%22-(strongest-pick)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#1.-%22automating-blog-reviews-with-cursor-hooks%22-(strongest-pick)&quot;&gt;1. &lt;strong&gt;&amp;quot;Automating Blog Reviews with Cursor Hooks&amp;quot;&lt;/strong&gt; (strongest pick)&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is the most obvious sequel. In &lt;a href=&quot;https://www.raymondcamden.com/2026/07/08/turning-cursor-performance-work-repeatable-skill&quot;&gt;Turning my Cursor Performance Work into a Repeatable Skill&lt;/a&gt;, he explicitly says he &lt;em&gt;plans to look into automations with Cursor later&lt;/em&gt; and that he initially considered hooks before choosing manual skills.&lt;/p&gt;
&lt;p&gt;A post that walks through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When hooks beat skills (and vice versa)&lt;/li&gt;
&lt;li&gt;Wiring &lt;code&gt;/precheck&lt;/code&gt; or performance review to run on commit/save&lt;/li&gt;
&lt;li&gt;What he learned from the manual-first approach&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…would close a loop he&apos;s already teed up across his last three posts.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2.-%22i-built-an-mcp-server-for-my-blog%22&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#2.-%22i-built-an-mcp-server-for-my-blog%22&quot;&gt;2. &lt;strong&gt;&amp;quot;I Built an MCP Server for My Blog&amp;quot;&lt;/strong&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This fits his current arc and hasn&apos;t been written yet. He&apos;s covered Algolia extensively (&lt;a href=&quot;https://www.raymondcamden.com/2024/05/27/adding-recommendations-to-my-blog-with-algolia&quot;&gt;Adding Recommendations&lt;/a&gt;, &lt;a href=&quot;https://www.raymondcamden.com/2023/07/12/using-pdfs-with-algolia-and-adobe-pdf-extract-api&quot;&gt;PDF search&lt;/a&gt;, &lt;a href=&quot;https://www.raymondcamden.com/2025/09/15/integrating-algolia-with-boxlang&quot;&gt;BoxLang integration&lt;/a&gt;), and he&apos;s now at Cursor helping with AI adoption.&lt;/p&gt;
&lt;p&gt;A post on exposing his blog index via MCP would be timely and meta — especially showing real queries like &amp;quot;what should I write next?&amp;quot; or &amp;quot;what has Ray covered on Eleventy?&amp;quot; He could even note that trending recommendations aren&apos;t configured yet (the MCP server&apos;s &lt;code&gt;trending-items&lt;/code&gt; and &lt;code&gt;trending-facets&lt;/code&gt; models return index errors), which sets up a natural follow-up.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3.-%22my-complete-cursor-skills-workflow-for-blogging%22&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#3.-%22my-complete-cursor-skills-workflow-for-blogging%22&quot;&gt;3. &lt;strong&gt;&amp;quot;My Complete Cursor Skills Workflow for Blogging&amp;quot;&lt;/strong&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;He&apos;s building a small skill ecosystem:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change type&lt;/th&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New blog post&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/prepublish&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eleventy / Netlify / engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/precheck&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance review&lt;/td&gt;
&lt;td&gt;web-perf skill&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A single &amp;quot;here&apos;s my full workflow&amp;quot; post would tie together &lt;code&gt;/prepublish&lt;/code&gt;, &lt;code&gt;/precheck&lt;/code&gt;, and the performance skill into one narrative. That&apos;s very aligned with his new role and gives readers a concrete adoption story, not just feature demos.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4.-%22making-my-blog-ai-friendly-(and-what-it-cost-me)%22&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#4.-%22making-my-blog-ai-friendly-(and-what-it-cost-me)%22&quot;&gt;4. &lt;strong&gt;&amp;quot;Making My Blog AI-Friendly (and What It Cost Me)&amp;quot;&lt;/strong&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;His &lt;a href=&quot;https://www.raymondcamden.com/2026/07/07/asking-cursor-to-review-my-blog-for-performance&quot;&gt;performance review post&lt;/a&gt; already touches this — Markdown copies, content-negotiation edge function, unintended build-time cost from globbing 6k+ files. A dedicated post could go deeper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why he added &lt;code&gt;.md&lt;/code&gt; endpoints and content negotiation&lt;/li&gt;
&lt;li&gt;The performance tradeoffs Cursor found&lt;/li&gt;
&lt;li&gt;How MCP/search fits into the &amp;quot;AI can read my blog&amp;quot; story&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5.-practical-follow-ups-on-recent-threads&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#5.-practical-follow-ups-on-recent-threads&quot;&gt;5. &lt;strong&gt;Practical follow-ups on recent threads&lt;/strong&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thread&lt;/th&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSS accessibility series&lt;/td&gt;
&lt;td&gt;Part 3: keyboard testing with real screen readers, or applying the pattern to checkboxes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.raymondcamden.com/2026/06/22/use-ai-to-not-use-ai-as-much&quot;&gt;Use AI to not use AI&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;When to reach for AI vs. write it yourself — a decision framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.raymondcamden.com/2026/07/05/links-for-you-7526&quot;&gt;Links For You (7/5/26)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Another links roundup (~2 week cadence; he&apos;s due around now)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transformers.js (mentioned Jul 2025)&lt;/td&gt;
&lt;td&gt;Eleventy build-time related posts without Algolia API calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-pick&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#my-pick&quot;&gt;My pick&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;#1 (Cursor Hooks)&lt;/strong&gt; if he wants to continue the Cursor-on-my-blog series.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#2 (MCP Server)&lt;/strong&gt; if he wants something fresh that showcases his new role and ties together Algolia + AI tooling in one post readers can actually try.&lt;/p&gt;
&lt;p&gt;Both build directly on what the MCP index shows as his hottest recent topics: &lt;strong&gt;generative AI&lt;/strong&gt;, &lt;strong&gt;Eleventy&lt;/strong&gt;, and &lt;strong&gt;Cursor workflows&lt;/strong&gt;.&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;
&lt;p&gt;In case you just skimmed, it literally suggested, as the second suggestion, the blog post I&apos;m right now. Great minds, right?&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#tldr&quot;&gt;TLDR&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You can add an MCP server to Algolia indexes in 5 minutes. Actually less than that - it took me longer to remember what 2FA app I used with their dashboard.&lt;/p&gt;
&lt;p&gt;Oh, and like most of Algolia&apos;s products, this falls into their &lt;a href=&quot;https://www.algolia.com/pricing&quot;&gt;Build&lt;/a&gt; tier which is free up to a certain amount of usage, which I&apos;ve never hit. I&apos;ve told Chuck before they are too damn generous with their free tier and thankfully they&apos;ve ignored me.&lt;/p&gt;
&lt;p&gt;Let me know what you think - are you using this? Are you using Algolia at all? Leave me a comment below as I&apos;d love to hear from some folks who may already be using this.&lt;/p&gt;

                        
                
				</content>

                
                <category term="algolia" />
                
                <category term="generative ai" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/08/turning-cursor-performance-work-repeatable-skill</id>
                <title>Turning my Cursor Performance Work into a Repeatable Skill</title>
                <updated>2026-07-08T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/08/turning-cursor-performance-work-repeatable-skill" rel="alternate" type="text/html" title="Turning my Cursor Performance Work into a Repeatable Skill"/>
                <content type="html">
				
                        &lt;p&gt;Yesterday, I wrote about using Cursor to &lt;a href=&quot;https://www.raymondcamden.com/2026/07/07/asking-cursor-to-review-my-blog-for-performance&quot;&gt;review my blog for performance&lt;/a&gt;. Cursor (and again, for full disclosure I work here now ;) did a &lt;em&gt;dang&lt;/em&gt; good job of finding performance issues with my site, handling the huge size and various languages, platforms, and so forth. As I said, it worked well, but I wanted to look into making this more of a repeatable process.&lt;/p&gt;
&lt;p&gt;I&apos;m saying &amp;quot;repeatable&amp;quot;, not &amp;quot;automated&amp;quot;, on purpose here. I &lt;em&gt;can&lt;/em&gt; automate the process, but a full performance review feels much more like something I should run when I think it makes sense, for example, when I know I&apos;ve tweaked my blog at the &apos;code&apos; level versus writing cat-related blogs. I do plan to look into automations with Cursor later, but for now, what I&apos;ve done is something I&apos;ll run when I think it makes the most sense.&lt;/p&gt;
&lt;h2 id=&quot;what&apos;s-a-skill-again%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what&apos;s-a-skill-again%3F&quot;&gt;What&apos;s a skill again?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At the simplest level, a skill simply gives direction to an AI agent on how to perform a task. It also describes itself in such a way that your AI agent knows when to invoke the skill. This is done in a simple Markdown file that can, and probably should, be checked into your repo.&lt;/p&gt;
&lt;p&gt;So for example, if I had a skill related to checking the health of my cat, my AI agent should be smart enough to recognize when it should use the skill based on my prompt. So for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I&apos;m worried about Donut&apos;s health, anything I can do?&lt;/li&gt;
&lt;li&gt;Sir Fluffalot is doing fine, but what steps can I do to ensure she&apos;s healthy?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s a pretty quick summary but you get the idea. Where the benefit comes in is helping document and describe a process that you need to run again and again, or a process your organization/team/herd of cats needs to standardize on and wants to be consistent.&lt;/p&gt;
&lt;h2 id=&quot;turning-my-performance-review-into-a-skill&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#turning-my-performance-review-into-a-skill&quot;&gt;Turning my performance review into a skill&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When I first did my performance review, I kept the prompt pretty simple. I did this on another machine so I don&apos;t have it in front of me, but I believe it was literally as simple as:&lt;/p&gt;
&lt;blockquote&gt;
Review this site and tell me of any performance improvements you can find.
&lt;/blockquote&gt;
&lt;p&gt;That generated the review I shared in my &lt;a href=&quot;https://www.raymondcamden.com/2026/07/07/asking-cursor-to-review-my-blog-for-performance&quot;&gt;last post&lt;/a&gt; and guided my development.&lt;/p&gt;
&lt;p&gt;Cursor provides a few ways to make skills:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Since it&apos;s just a Markdown file, you can just make a file, old school like we did in the old days.&lt;/li&gt;
&lt;li&gt;Cursor has a skill to make skills: &lt;code&gt;/create-skill i need a skill to help with cat care&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You can also just ask Cursor to do this in a prompt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last option is what I did, asking Cursor to make a skill like my &lt;a href=&quot;https://github.com/cfjedimaster/raymondcamden2023/blob/main/.agents/skills/prepublish/SKILL.md&quot;&gt;&lt;code&gt;prepublish&lt;/code&gt;&lt;/a&gt; one.&lt;/p&gt;
&lt;p&gt;Cursor responded with:&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;p&gt;Created a &lt;strong&gt;&lt;code&gt;precheck&lt;/code&gt;&lt;/strong&gt; skill as the engine-code counterpart to &lt;code&gt;prepublish&lt;/code&gt;. It lives in both skill directories, same as your blog skill:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.claude/skills/precheck/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.agents/skills/precheck/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-to-use-it&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-to-use-it&quot;&gt;How to use it&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before committing engine changes, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/precheck
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or ask something like “review my engine changes before I commit.”&lt;/p&gt;
&lt;h2 id=&quot;what-it-does&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what-it-does&quot;&gt;What it does&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Finds scope&lt;/strong&gt; — Uses &lt;code&gt;git diff&lt;/code&gt; / &lt;code&gt;git status&lt;/code&gt; and filters to engine paths (&lt;code&gt;eleventy.config.js&lt;/code&gt;, &lt;code&gt;config/&lt;/code&gt;, &lt;code&gt;src/_data/&lt;/code&gt;, &lt;code&gt;netlify/&lt;/code&gt;, &lt;code&gt;scripts/&lt;/code&gt;, Algolia/search templates, client JS). Ignores &lt;code&gt;src/posts/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smoke test&lt;/strong&gt; — Runs &lt;code&gt;SKIP_REMOTE_DATA=1 npx eleventy&lt;/code&gt; when config, data, or search templates changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reviews&lt;/strong&gt; — Groups findings as Critical / Warning / Note, focused on build breaks, deploy/runtime issues, performance (collection loops, async shortcodes, caching), and Algolia/search compatibility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applies fixes only on request&lt;/strong&gt; — Same handoff pattern as &lt;code&gt;prepublish&lt;/code&gt;; you commit when ready.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;your-workflow&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#your-workflow&quot;&gt;Your workflow&lt;/a&gt;&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change type&lt;/th&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New blog post&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/prepublish&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eleventy, Netlify, JS/TS engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/precheck&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;No hooks involved — you invoke it manually when you want it.&lt;/p&gt;
&lt;p&gt;If you want the description tuned so it auto-suggests itself when you say “commit my eleventy changes,” I can adjust the frontmatter trigger phrases.&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;
&lt;p&gt;A note on the last paragraph of the response. I had started my conversation with Cursor initially thinking I was going to automate it with a hook, but changed my mind. That&apos;s why it&apos;s mentioned here. The last bit &amp;quot;frontmatter trigger phrases&amp;quot; goes back to what I said earlier - a skill is both a guide on how to do something as well as a description of when it should be run.&lt;/p&gt;
&lt;p&gt;The result was pretty astounding I think, specifically in how Cursor recognized the &amp;quot;engine&amp;quot; parts of my blog versus the &amp;quot;content&amp;quot; bits. It did this well in the one off so I&apos;m not surprised, but I love having the various parts spelled out specifically in the skill itself. Like yesterday, I&apos;ll share the entire skill at the end, but let me talk a bit about what it did when I tried running it.&lt;/p&gt;
&lt;h2 id=&quot;running-the-skill&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#running-the-skill&quot;&gt;Running the Skill&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When I ran the skill, I didn&apos;t expect it to find anything as my last commit was just a blog post. Cursor responded as such mentioning there was nothing staged. It noted two files that were dirty but not important:&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;p&gt;&lt;strong&gt;Precheck result: nothing in scope to review.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Your working tree has two changes, neither in the engine paths this skill covers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;In scope?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;package-lock.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;modified&lt;/td&gt;
&lt;td&gt;No — lockfile only; syncs &lt;code&gt;@netlify/edge-functions&lt;/code&gt; already in &lt;code&gt;package.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/test.liquid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;untracked&lt;/td&gt;
&lt;td&gt;No — scratch page (moon-phase font test), not engine/build code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;No staged changes. No diffs under &lt;code&gt;eleventy.config.js&lt;/code&gt;, &lt;code&gt;config/&lt;/code&gt;, &lt;code&gt;src/_data/&lt;/code&gt;, &lt;code&gt;netlify/&lt;/code&gt;, &lt;code&gt;scripts/&lt;/code&gt;, search/feed templates, client JS, or deploy config.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stopping here&lt;/strong&gt; per the skill workflow.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you meant something else:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Recent commits&lt;/strong&gt; (e.g. &lt;code&gt;1117ccc2 perf changes&lt;/code&gt;) — say “precheck the last N commits” and I’ll review those engine diffs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Before you commit&lt;/strong&gt; &lt;code&gt;package-lock.json&lt;/code&gt; — worth committing alongside the existing &lt;code&gt;package.json&lt;/code&gt; dep so installs stay reproducible; no code review needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;src/test.liquid&lt;/code&gt;&lt;/strong&gt; — looks like a local experiment; don’t commit unless you want it published.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Want me to review a specific commit or branch instead?&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;
&lt;p&gt;I asked it to review the last two and it also noticed they didn&apos;t have anything relevant either - but what got me excited was that it went ahead and looked at one more and noted relevant changes there. Here&apos;s how that response ended:&lt;/p&gt;
&lt;blockquote&gt;
Want me to precheck 1117ccc2 (or 1117ccc2 + 06626459 together)? That is where the meaningful review would be.
&lt;/blockquote&gt;
&lt;p&gt;As I mentioned, I didn&apos;t do &lt;em&gt;everything&lt;/em&gt; in the initial performance check, so I figured, why not, and I approved this scan. It noted two places I had used &lt;code&gt;rss-parser&lt;/code&gt; and not yet added the timeout change so I went ahead and let Cursor make those changes as well.&lt;/p&gt;
&lt;h2 id=&quot;the-skill&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-skill&quot;&gt;The Skill&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here&apos;s the &lt;a href=&quot;https://github.com/cfjedimaster/raymondcamden2023/blob/main/.agents/skills/prepublish/SKILL.md&quot;&gt;skill&lt;/a&gt; created by Cursor. Let me know what you think! Also, check out Cursor&apos;s &lt;a href=&quot;https://cursor.com/docs/skills&quot;&gt;docs on Skills&lt;/a&gt; - it&apos;s not just specific to Cursor and is a great reference in general.&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;hr&gt;
&lt;p&gt;name: precheck&lt;/p&gt;
&lt;p&gt;description: Review engine and infrastructure changes before commit. Use when the user runs /precheck, asks to review site engine code, or wants a pre-commit check on JavaScript/TypeScript, Eleventy config, Netlify functions, or build tooling — not blog posts.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id=&quot;precheck-engine-review&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#precheck-engine-review&quot;&gt;Precheck engine review&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Run this before committing changes to the site&apos;s core engine — Eleventy config, build filters, data fetchers, Netlify functions, search/indexing, or client JS that affects builds or performance. Blog posts in &lt;code&gt;src/posts/&lt;/code&gt; are out of scope; use &lt;code&gt;prepublish&lt;/code&gt; for those.&lt;/p&gt;
&lt;h2 id=&quot;step-1-%E2%80%94-find-the-changes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-1-%E2%80%94-find-the-changes&quot;&gt;Step 1 — Find the changes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Use git to determine what changed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status --short
git diff
git diff --cached
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Filter to engine-relevant paths only:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Paths&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eleventy entry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eleventy.config.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine modules&lt;/td&gt;
&lt;td&gt;&lt;code&gt;config/**/*.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build-time data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/_data/*.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netlify runtime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;netlify/functions/**&lt;/code&gt;, &lt;code&gt;netlify/edge-functions/**&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance scripts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scripts/**&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search / feeds&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/misc/algolia_*.liquid&lt;/code&gt;, &lt;code&gt;src/search.liquid&lt;/code&gt;, &lt;code&gt;src/feed*.liquid&lt;/code&gt;, &lt;code&gt;src/sitemap.njk&lt;/code&gt;, &lt;code&gt;src/llms.liquid&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client JS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/assets/js/**&lt;/code&gt;, &lt;code&gt;src/assets/service-worker.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;netlify.toml&lt;/code&gt;, &lt;code&gt;src/_redirects&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Exclude:&lt;/strong&gt; &lt;code&gt;src/posts/**&lt;/code&gt;, static pages (&lt;code&gt;about.md&lt;/code&gt;, &lt;code&gt;speaking.md&lt;/code&gt;, etc.), images/CSS unless they affect build behavior, &lt;code&gt;_site/&lt;/code&gt;, &lt;code&gt;node_modules/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If nothing in scope changed, say so and stop. If both engine files and blog posts changed, review only the engine files unless the user asks otherwise.&lt;/p&gt;
&lt;p&gt;Confirm in one line which files you are reviewing before proceeding.&lt;/p&gt;
&lt;h2 id=&quot;step-2-%E2%80%94-build-smoke-test&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-2-%E2%80%94-build-smoke-test&quot;&gt;Step 2 — Build smoke test&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If any of these changed — &lt;code&gt;eleventy.config.js&lt;/code&gt;, &lt;code&gt;config/**&lt;/code&gt;, &lt;code&gt;src/_data/**&lt;/code&gt;, search/feed templates — run a local build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SKIP_REMOTE_DATA=1 npx eleventy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Report success or paste the error. If the change is limited to &lt;code&gt;netlify/functions/**&lt;/code&gt;, &lt;code&gt;scripts/**&lt;/code&gt;, or client JS with no template/config impact, skip the build and note why.&lt;/p&gt;
&lt;p&gt;Do not auto-fix build failures. Diagnose and report.&lt;/p&gt;
&lt;h2 id=&quot;step-3-%E2%80%94-code-review&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-3-%E2%80%94-code-review&quot;&gt;Step 3 — Code review&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Read every changed in-scope file. Review for:&lt;/p&gt;
&lt;h3 id=&quot;build-breaks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#build-breaks&quot;&gt;Build breaks&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Broken imports, mixed ESM/CJS (&lt;code&gt;scripts/rebuildalgolia.js&lt;/code&gt; is CommonJS; the rest is ESM)&lt;/li&gt;
&lt;li&gt;Collection mutations in &lt;code&gt;getPosts&lt;/code&gt; / &lt;code&gt;categories&lt;/code&gt; — permalink and &lt;code&gt;outputPath&lt;/code&gt; changes affect every post&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;_data&lt;/code&gt; files that ignore &lt;code&gt;SKIP_REMOTE_DATA=1&lt;/code&gt; (local dev must not require live API keys)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eleventy.after&lt;/code&gt; markdown copy hook — path bugs silently skip posts for content negotiation&lt;/li&gt;
&lt;li&gt;Algolia JSON shape in &lt;code&gt;src/misc/algolia_*.liquid&lt;/code&gt; — must stay compatible with &lt;code&gt;deploy-succeeded&lt;/code&gt; partial reindex and &lt;code&gt;scripts/rebuildalgolia.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;runtime-%2F-deploy-breaks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#runtime-%2F-deploy-breaks&quot;&gt;Runtime / deploy breaks&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Netlify functions: missing env vars, timeout risk, uncached Algolia calls&lt;/li&gt;
&lt;li&gt;Edge function &lt;code&gt;content-negotiation.ts&lt;/code&gt; — must not break HTML serving for normal browsers&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deploy-succeeded&lt;/code&gt; — partial Algolia update logic; a bad filter can leave the index stale&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;performance&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#performance&quot;&gt;Performance&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Work added inside &lt;code&gt;getPosts&lt;/code&gt; or other per-post loops (runs on every build)&lt;/li&gt;
&lt;li&gt;New async shortcodes or &lt;code&gt;_data&lt;/code&gt; fetchers without caching (&lt;code&gt;@11ty/eleventy-fetch&lt;/code&gt; pattern in &lt;code&gt;stoot.js&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cssmin&lt;/code&gt; filter global cache — stale output if inputs change without cache invalidation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;htmlmin&lt;/code&gt; transform — CI-only; errors are swallowed; new HTML patterns may minify badly&lt;/li&gt;
&lt;li&gt;Client JS: blocking scripts, large bundles, service-worker cache busting&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;correctness-and-safety&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#correctness-and-safety&quot;&gt;Correctness and safety&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Secrets or API keys hardcoded instead of env vars&lt;/li&gt;
&lt;li&gt;Error handling that fails open vs fails closed (especially fetchers returning &lt;code&gt;[]&lt;/code&gt; on error)&lt;/li&gt;
&lt;li&gt;Redirect or header changes in &lt;code&gt;netlify.toml&lt;/code&gt; / &lt;code&gt;src/_redirects&lt;/code&gt; with unintended side effects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Do NOT&lt;/strong&gt; nitpick style, naming, or formatting unless it risks a real bug. This site has no linter; focus on things that break builds, deploys, search, or page performance.&lt;/p&gt;
&lt;p&gt;Present findings grouped by severity:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Critical&lt;/strong&gt; — will break build, deploy, or search; must fix before commit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Warning&lt;/strong&gt; — likely problem under load, in CI, or at runtime&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Note&lt;/strong&gt; — worth knowing, optional improvement&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each item: one or two sentences, file path, and line number when possible. Do not rewrite code — point at the issue and let the user decide.&lt;/p&gt;
&lt;h2 id=&quot;step-4-%E2%80%94-apply-changes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-4-%E2%80%94-apply-changes&quot;&gt;Step 4 — Apply changes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ask which suggestions to act on. Apply only what is confirmed. Then stop — the user handles the actual commit/push.&lt;/p&gt;
  &lt;/script&gt;
&lt;/zero-md&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                <category term="eleventy" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/07/asking-cursor-to-review-my-blog-for-performance</id>
                <title>Asking Cursor to Review My Blog for Performance</title>
                <updated>2026-07-07T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/07/asking-cursor-to-review-my-blog-for-performance" rel="alternate" type="text/html" title="Asking Cursor to Review My Blog for Performance"/>
                <content type="html">
				
                        &lt;p&gt;Last week I decided to try something interesting. I opened my blog up in Cursor and asked for a basic performance review. That seems like a no-brainer, but keep in mind, my &lt;a href=&quot;https://github.com/cfjedimaster/raymondcamden2023&quot;&gt;blog&apos;s source code&lt;/a&gt; clocks in at near &lt;strong&gt;seven thousand&lt;/strong&gt; files (ignoring &lt;code&gt;node_modules&lt;/code&gt; of course), so this wasn&apos;t some small request.&lt;/p&gt;
&lt;p&gt;My blog is built with the &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt; static site generator. It&apos;s a mix of JavaScript and Markdown primarily, with a &lt;em&gt;huge&lt;/em&gt; portion of the codebase being Markdown and not &apos;code&apos; per se, but me rambling on about cats and Star Wars. There&apos;s also Liquid templates which are parsed into HTML by a JavaScript library. But that doesn&apos;t quite tell the whole story.&lt;/p&gt;
&lt;p&gt;In Eleventy, when converting my Markdown into HTML, it also applies the Liquid template syntax to Markdown which means my blog posts can actually be dynamic, at build time anyway. This impacts how fast the site builds in ways I can completely forget about down the road.&lt;/p&gt;
&lt;p&gt;This is why I let Cursor look over the entire code base instead of ignoring my Markdown (&lt;a href=&quot;https://cursor.com/docs/reference/ignore-file&quot;&gt;Cursor docs on ignore files&lt;/a&gt;), and I&apos;m glad I did. I&apos;ll share the complete report at the end of this post, but let me talk about the highlights.&lt;/p&gt;
&lt;h2 id=&quot;rss-parsing-and-a-dead-mastodon-server&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#rss-parsing-and-a-dead-mastodon-server&quot;&gt;RSS Parsing and a dead Mastodon server&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The biggest fix Cursor discovered concerned code I had to work with, and render, Mastodon posts. I&apos;ve got a shortcode that given a server and username will find the last post via the RSS feed generated for the account. I use another shortcode that renders the post into HTML.&lt;/p&gt;
&lt;p&gt;Cursor discovered I had multiple calls out to &lt;code&gt;botsin.space&lt;/code&gt;, a Mastodon server that was built to target bots and unfortunately had to be shut down. I had multiple blog posts trying to get content from that server. The code, which made use &lt;a href=&quot;https://www.npmjs.com/package/rss-parser&quot;&gt;rss-parser&lt;/a&gt;, would get a quick failure but was keeping an HTTP socket open until it timed out, roughly 70 seconds later.&lt;/p&gt;
&lt;p&gt;To be clear, the &apos;bug&apos; here was in my implementation, not the RSS library. The fix was pretty simple. Going from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import Parser from &apos;rss-parser&apos;;

const parser = new Parser();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import https from &apos;https&apos;;
import Parser from &apos;rss-parser&apos;;

const parser = new Parser({
  requestOptions: {
    timeout: 5000,
    agent: new https.Agent({ keepAlive: false }),
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This alone helped, but I also found instances in my Markdown where I was calling out to botsin.space and replaced it with mastodon.social (where I moved my bots).&lt;/p&gt;
&lt;h2 id=&quot;trying-to-be-ai-friendly-had-an-unintended-consequence&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#trying-to-be-ai-friendly-had-an-unintended-consequence&quot;&gt;Trying to be AI friendly had an unintended consequence&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A few months back I made a change to my blog to be more AI friendly. That involved two steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For every post, I now have a copy of the Markdown version. So for example, this &lt;a href=&quot;https://www.raymondcamden.com/2026/07/06/my-new-role-helping-ai-adoption-at-cursor&quot;&gt;https://www.raymondcamden.com/2026/07/06/my-new-role-helping-ai-adoption-at-cursor&lt;/a&gt; goes to HTML, and you can go to &lt;a href=&quot;https://www.raymondcamden.com/2026/07/06/my-new-role-helping-ai-adoption-at-cursor/index.md&quot;&gt;https://www.raymondcamden.com/2026/07/06/my-new-role-helping-ai-adoption-at-cursor/index.md&lt;/a&gt; for the Markdown&lt;/li&gt;
&lt;li&gt;I used a Netlify Edge serverless function to sniff for a content request that prefers plain text or Markdown, what an AI agent would send, and redirects to that automatically. You can see that code here: &lt;a href=&quot;https://github.com/cfjedimaster/raymondcamden2023/blob/main/netlify/edge-functions/content-negotiation.ts&quot;&gt;https://github.com/cfjedimaster/raymondcamden2023/blob/main/netlify/edge-functions/content-negotiation.ts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To enable the Markdown copy, my &lt;code&gt;eleventy.config.js&lt;/code&gt; uses an event handler for builds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;eleventyConfig.on(&amp;quot;eleventy.after&amp;quot;, async ({ dir }) =&amp;gt; {
    const inputDir = dir.input;   
    const outputDir = dir.output; 

    // blog posts only
    const mdFiles = glob.sync(&amp;quot;posts/**/*.md&amp;quot;, {
        cwd: inputDir,
        ignore: [&amp;quot;node_modules/**&amp;quot;],
    });

    for (const file of mdFiles) {
        const srcPath = path.join(inputDir, file);
        /*
        My input file looks like so:
        src/posts/2026/03/04/2026-03-04-dyanimically-adjusting-image-text-for-contrast.md
        I need to save to

        outputdir/2026/03/04/dynamically etc
        */
        let newFile = file.replace(&apos;posts&apos;, &apos;&apos;);
        newFile = newFile.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2}-/,&apos;&apos;);
        newFile = newFile.replace(&apos;.md&apos;,&apos;/index.md&apos;);

        const destPath = path.join(outputDir, newFile);


        fs.mkdirSync(path.dirname(destPath), { recursive: true });
        fs.copyFileSync(srcPath, destPath);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important bit is the glob. When I run my blog locally, I have an &lt;code&gt;.eleventyignore&lt;/code&gt; file which ignores blog posts from 2003 to 2024 or so, basically 90% of my blog posts (I &lt;a href=&quot;/stats&quot;&gt;blogged a lot more&lt;/a&gt; in the old days). However, my glob wasn&apos;t using that and every change resulted in 6k+ files being copied. Locally that meant every change I made while writing a post would take about 3 seconds to process. That&apos;s not bad in theory, but I tend to write, check the HTML, edit, etc, quickly, and I absolutely noticed times when I&apos;d have to reload again because I reloaded too quickly the first time.&lt;/p&gt;
&lt;p&gt;For the fix, I went back to Cursor and simply asked for help and the response was perfect:&lt;/p&gt;
  &lt;div class=&quot;callout&quot;&gt;The simplest fix is to stop globbing and copy only posts Eleventy actually built. The eleventy.after event gives you a results array of everything Eleventy processed, which already respects all ignore sources.&lt;/div&gt;
&lt;p&gt;I missed this in Eleventy&apos;s &lt;a href=&quot;https://www.11ty.dev/docs/events/#eleventy-after&quot;&gt;docs on the event&lt;/a&gt; that&apos;s 100% on me. I let Cursor update the code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;eleventyConfig.on(&amp;quot;eleventy.after&amp;quot;, async ({ directories, results }) =&amp;gt; {
    const inputDir = directories.input;
    const outputDir = directories.output;

    // Only copy posts Eleventy actually built (respects .eleventyignore)
    const postResults = results.filter((r) =&amp;gt; {
        const rel = path.relative(inputDir, r.inputPath);
        return rel.startsWith(`posts${path.sep}`) &amp;amp;&amp;amp; rel.endsWith(&apos;.md&apos;);
    });

    // rest of event...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;bypassing-api-calls-locally&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#bypassing-api-calls-locally&quot;&gt;Bypassing API calls locally&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;One last thing it suggested that I implemented involved my &lt;code&gt;_data&lt;/code&gt; files. These files use remote APIs to fetch data for my site, most of which you can see on my &lt;a href=&quot;/now&quot;&gt;Now&lt;/a&gt; page. I had code to look for a local run, or a lack of an environment variable for a key. My code though was a bit of a hack, like looking for &lt;code&gt;/home/ray&lt;/code&gt;, and that failed on new machines where my local username didn&apos;t match. It didn&apos;t dramatically impact my builds, but Cursor pointing this out made me build a nicer solution - which was just to look for an environment variable instead, one I include in my npm scripts now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;dev2&amp;quot;: &amp;quot;SKIP_REMOTE_DATA=1 eleventy --serve --quiet&amp;quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The use of &apos;dev2&apos; is another story and related to a bug in the Netlify CLI.)&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#wrap-up&quot;&gt;Wrap Up&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, I work at Cursor, I&apos;m biased, but of course other tools could create similar results. I&apos;m just impressed by how &lt;em&gt;deep&lt;/em&gt; Cursor looked into my code and found things that I never would have realized. As I write this and save, I&apos;m seeing build times of 0.33 seconds or so, which is freaking great. My &amp;quot;full&amp;quot; builds aren&apos;t necessarily faster, but that&apos;s fine. (Netlify and Eleventy both perform incredibly well with a typical site build of less than four minutes.)&lt;/p&gt;
&lt;p&gt;Here&apos;s the complete initial report Cursor created.&lt;/p&gt;
 &lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/zero-md@3?register&quot;&gt;&lt;/script&gt;
&lt;zero-md&gt;
&lt;template data-append&gt;
&lt;style&gt;
.markdown-body {
padding: 20px;
margin-bottom: 30px;
}
&lt;/style&gt;
&lt;/template&gt;
  &lt;script type=&quot;text/markdown&quot;&gt;
&lt;p&gt;site_performance_recommendations.md
Open File
View Rendered&lt;/p&gt;
&lt;h1 id=&quot;site-performance-recommendations&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#site-performance-recommendations&quot;&gt;Site Performance Recommendations&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Analysis of the &lt;code&gt;raymondcamden2023&lt;/code&gt; Eleventy build (June 30, 2026).&lt;/p&gt;
&lt;h2 id=&quot;summary&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#summary&quot;&gt;Summary&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Local builds report &lt;strong&gt;~4–5 seconds&lt;/strong&gt; of actual Eleventy work, but the full process takes &lt;strong&gt;~72 seconds&lt;/strong&gt;. Most of that gap is not template rendering — it is the Node process waiting on open I/O handles after Eleventy finishes.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Active posts built (2023+)&lt;/td&gt;
&lt;td&gt;~463&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total markdown files on disk&lt;/td&gt;
&lt;td&gt;6,784&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files written per build&lt;/td&gt;
&lt;td&gt;~571&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files in &lt;code&gt;_site&lt;/code&gt; (including markdown copies)&lt;/td&gt;
&lt;td&gt;~13,785&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eleventy-reported build time&lt;/td&gt;
&lt;td&gt;~4–5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actual wall-clock build time&lt;/td&gt;
&lt;td&gt;~72s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1.-biggest-win%3A-fix-the-~67s-post-build-hang&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#1.-biggest-win%3A-fix-the-~67s-post-build-hang&quot;&gt;1. Biggest Win: Fix the ~67s Post-Build Hang&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After Eleventy prints &lt;code&gt;Wrote 571 files&lt;/code&gt;, the Node process sits idle for about &lt;strong&gt;67 more seconds&lt;/strong&gt; before exiting. Profiling shows it waiting in the event loop on open I/O handles.&lt;/p&gt;
&lt;h3 id=&quot;root-cause&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#root-cause&quot;&gt;Root cause&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;lasttoot&lt;/code&gt; shortcode in &lt;code&gt;config/shortcodes/index.js&lt;/code&gt; uses &lt;code&gt;rss-parser&lt;/code&gt; without disabling HTTP keep-alive. A failing fetch to &lt;code&gt;botsin.space&lt;/code&gt; (visible in build logs) leaves a socket open until it times out (~70s).&lt;/p&gt;
&lt;p&gt;This post still runs the shortcode at build time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/posts/2023/03/17/2023-03-17-another-week-another-mastodon-bot-random-album-cover.md
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{% capture &amp;quot;lasttoot_rac&amp;quot; %}
{% lasttoot &amp;quot;botsin.space&amp;quot;, &amp;quot;randomalbumcover&amp;quot; %}
{% endcapture %}

{% stoot &amp;quot;botsin.space&amp;quot;, lasttoot_rac %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;verification&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#verification&quot;&gt;Verification&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A single failed &lt;code&gt;botsin.space&lt;/code&gt; RSS fetch via &lt;code&gt;rss-parser&lt;/code&gt; fails in ~0.8s, but the process does not exit for ~71s.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;keepAlive: false&lt;/code&gt; drops total time to &lt;strong&gt;under 1 second&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Successful fetches to other hosts (e.g. Letterboxd) exit in ~0.3s with no hang.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;recommended-fix&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#recommended-fix&quot;&gt;Recommended fix&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Update &lt;code&gt;lasttoot&lt;/code&gt; (and the &lt;code&gt;_data&lt;/code&gt; RSS files) to disable keep-alive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import https from &apos;https&apos;;
import Parser from &apos;rss-parser&apos;;

const parser = new Parser({
  requestOptions: {
    timeout: 5000,
    agent: new https.Agent({ keepAlive: false }),
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, migrate &lt;code&gt;lasttoot&lt;/code&gt; to &lt;code&gt;@11ty/eleventy-fetch&lt;/code&gt; (as &lt;code&gt;stoot&lt;/code&gt; already does in &lt;code&gt;config/shortcodes/stoot.js&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;expected-impact&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#expected-impact&quot;&gt;Expected impact&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Full builds should drop from &lt;strong&gt;~72s to ~5s&lt;/strong&gt; locally, with similar improvement on Netlify.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2.-high-impact%3A-stop-copying-6%2C784-markdown-files-after-every-build&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#2.-high-impact%3A-stop-copying-6%2C784-markdown-files-after-every-build&quot;&gt;2. High Impact: Stop Copying 6,784 Markdown Files After Every Build&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;eleventy.after&lt;/code&gt; hook in &lt;code&gt;eleventy.config.js&lt;/code&gt; copies &lt;strong&gt;all&lt;/strong&gt; posts to &lt;code&gt;_site&lt;/code&gt;, including years ignored by &lt;code&gt;.eleventyignore&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;eleventyConfig.on(&amp;quot;eleventy.after&amp;quot;, async ({ dir }) =&amp;gt; {
  const mdFiles = glob.sync(&amp;quot;posts/**/*.md&amp;quot;, {
    cwd: inputDir,
    ignore: [&amp;quot;node_modules/**&amp;quot;],
  });

  for (const file of mdFiles) {
    // ... copies every file ...
    fs.copyFileSync(srcPath, destPath);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;6,784&lt;/strong&gt; markdown files exist on disk&lt;/li&gt;
&lt;li&gt;Only &lt;strong&gt;~463&lt;/strong&gt; are actively built (2023+)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.eleventyignore&lt;/code&gt; excludes 2000–2022 but the after-hook ignores that&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_site&lt;/code&gt; grows to &lt;strong&gt;~13,785&lt;/strong&gt; files because of this&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;recommended-fix-1&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#recommended-fix-1&quot;&gt;Recommended fix&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Respect the same ignore rules as &lt;code&gt;.eleventyignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Only copy files Eleventy actually built&lt;/li&gt;
&lt;li&gt;Or move archived posts out of &lt;code&gt;src/posts/&lt;/code&gt; entirely&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;expected-impact-1&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#expected-impact-1&quot;&gt;Expected impact&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;~1s saved on the copy loop itself&lt;/li&gt;
&lt;li&gt;Much faster deploy uploads&lt;/li&gt;
&lt;li&gt;Smaller &lt;code&gt;_site&lt;/code&gt; output&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3.-medium-impact%3A-expensive-templates&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#3.-medium-impact%3A-expensive-templates&quot;&gt;3. Medium Impact: Expensive Templates&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;%2Fall-page-%E2%80%94-o(years-%C3%97-posts)-loop&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#%2Fall-page-%E2%80%94-o(years-%C3%97-posts)-loop&quot;&gt;&lt;code&gt;/all&lt;/code&gt; page — O(years × posts) loop&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;File: &lt;code&gt;src/misc/all.liquid&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{% for year in (firstYear..thisYear) reversed %}
  {% for post in posts %}
    {% if postYear == year %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This nested loop iterates all posts for every year. A prior benchmark noted &lt;strong&gt;~3.6s+&lt;/strong&gt; for this page alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Pre-group posts by year in a collection or JS data file and iterate once.&lt;/p&gt;
&lt;h3 id=&quot;algolia_full.liquid-%E2%80%94-all-posts%2C-full-content&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#algolia_full.liquid-%E2%80%94-all-posts%2C-full-content&quot;&gt;&lt;code&gt;algolia_full.liquid&lt;/code&gt; — all posts, full content&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;File: &lt;code&gt;src/misc/algolia_full.liquid&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Runs &lt;code&gt;templateContent | algExcerpt&lt;/code&gt; across every post in one JSON file. The regex work on full rendered HTML adds up across ~463 posts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Generate this only in CI/deploy, or only when search index content changes. (&lt;code&gt;algolia_new.liquid&lt;/code&gt; already limits to 5 posts for deploy hooks.)&lt;/p&gt;
&lt;h3 id=&quot;tag-pagination-generates-many-pages&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#tag-pagination-generates-many-pages&quot;&gt;Tag pagination generates many pages&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Files: &lt;code&gt;src/tags.liquid&lt;/code&gt;, &lt;code&gt;src/tag_feeds.liquid&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Both paginate over &lt;strong&gt;all collections&lt;/strong&gt; (not just tags), generating two pages per collection entry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use an explicit tag list instead of &lt;code&gt;collections&lt;/code&gt; as pagination data.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4.-medium-impact%3A-build-time-network-calls&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#4.-medium-impact%3A-build-time-network-calls&quot;&gt;4. Medium Impact: Build-Time Network Calls&lt;/a&gt;&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/_data/medium.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RSS fetch every build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/_data/letterboxd_films.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RSS fetch every build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/_data/hardcover_books.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GraphQL fetch when &lt;code&gt;HARDCOVER_BOOKS&lt;/code&gt; env var is set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lasttoot&lt;/code&gt; / &lt;code&gt;stoot&lt;/code&gt; in ~4 posts&lt;/td&gt;
&lt;td&gt;Mastodon API/RSS at build time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#notes&quot;&gt;Notes&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;untappd_beers.js&lt;/code&gt; already short-circuits locally when &lt;code&gt;ELEVENTY_ROOT&lt;/code&gt; includes &lt;code&gt;/raymondcamden2023&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;medium.js&lt;/code&gt; and &lt;code&gt;letterboxd_films.js&lt;/code&gt; only skip on &lt;code&gt;/home/ray&lt;/code&gt; — inconsistent with local dev on other paths&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/bots&lt;/code&gt; page (&lt;code&gt;src/misc/bots.md&lt;/code&gt;) already loads toots &lt;strong&gt;client-side&lt;/strong&gt;; build-time &lt;code&gt;stoot&lt;/code&gt;/&lt;code&gt;lasttoot&lt;/code&gt; embeds in old posts may not be worth the complexity&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;recommended-fix-2&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#recommended-fix-2&quot;&gt;Recommended fix&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Add a consistent &lt;code&gt;SKIP_REMOTE_DATA&lt;/code&gt; env flag (or broaden the local path short-circuit)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;@11ty/eleventy-fetch&lt;/code&gt; with disk cache for all remote data&lt;/li&gt;
&lt;li&gt;Consider replacing build-time Mastodon embeds with static IDs or client-side loading&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5.-lower-impact-%2F-polish&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#5.-lower-impact-%2F-polish&quot;&gt;5. Lower Impact / Polish&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;htmlmin-transform-in-ci&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#htmlmin-transform-in-ci&quot;&gt;&lt;code&gt;htmlmin&lt;/code&gt; transform in CI&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In &lt;code&gt;eleventy.config.js&lt;/code&gt;, HTML minification runs on every &lt;code&gt;.html&lt;/code&gt; file when &lt;code&gt;process.env.CI&lt;/code&gt; is set. Netlify gzip/brotli may make this redundant.&lt;/p&gt;
&lt;h3 id=&quot;eleventy-incremental-mode&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#eleventy-incremental-mode&quot;&gt;Eleventy incremental mode&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;eleventy --incremental&lt;/code&gt; during dev when editing a single post.&lt;/p&gt;
&lt;h3 id=&quot;netlify-build-cache&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#netlify-build-cache&quot;&gt;Netlify build cache&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Cache &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.cache/eleventy-fetch-*&lt;/code&gt; between deploys (Eleventy Fetch cache already exists under &lt;code&gt;.cache/&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;comment-includes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#comment-includes&quot;&gt;Comment includes&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;5,111 legacy comment &lt;code&gt;.inc&lt;/code&gt; files under &lt;code&gt;src/_includes/comments/&lt;/code&gt;. The &lt;code&gt;hasAnyComments&lt;/code&gt; / &lt;code&gt;commentInclude&lt;/code&gt; shortcodes do sync &lt;code&gt;fs.existsSync&lt;/code&gt; on every post via &lt;code&gt;src/_includes/mydisqus.liquid&lt;/code&gt;. Minor cost, but could precompute a Set at build start.&lt;/p&gt;
&lt;h3 id=&quot;%40rknightuk%2Feleventy-plugin-post-graph&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#%40rknightuk%2Feleventy-plugin-post-graph&quot;&gt;&lt;code&gt;@rknightuk/eleventy-plugin-post-graph&lt;/code&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Bundles its own Eleventy 2.x copy — housekeeping item, not a major speed issue. Only used on &lt;code&gt;src/misc/postgraph.md&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;archived-posts-on-disk&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#archived-posts-on-disk&quot;&gt;Archived posts on disk&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;6,784 markdown files remain in &lt;code&gt;src/posts/&lt;/code&gt; even though 2000–2022 are in &lt;code&gt;.eleventyignore&lt;/code&gt;. Moving archives out of the input tree reduces repo noise and glob scan scope.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;suggested-priority&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#suggested-priority&quot;&gt;Suggested Priority&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fix &lt;code&gt;lasttoot&lt;/code&gt; / &lt;code&gt;rss-parser&lt;/code&gt; keep-alive&lt;/strong&gt; (~67s saved — do this first)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limit the &lt;code&gt;eleventy.after&lt;/code&gt; markdown copy&lt;/strong&gt; to active posts only&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimize &lt;code&gt;/all&lt;/code&gt; and &lt;code&gt;algolia_full.liquid&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add a consistent skip flag&lt;/strong&gt; for remote &lt;code&gt;_data&lt;/code&gt; fetches in dev&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable incremental builds + Netlify caching&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;posts-still-using-build-time-mastodon-shortcodes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#posts-still-using-build-time-mastodon-shortcodes&quot;&gt;Posts Still Using Build-Time Mastodon Shortcodes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These active (2023+) posts invoke &lt;code&gt;stoot&lt;/code&gt; and/or &lt;code&gt;lasttoot&lt;/code&gt; at build time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/posts/2024/10/23/2024-10-23-getting-and-displaying-a-mostodon-post-in-client-side-javascript.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/posts/2023/08/14/2023-08-14-building-a-mastodon-bot-with-cloudflare-workers.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/posts/2023/03/17/2023-03-17-another-week-another-mastodon-bot-random-album-cover.md&lt;/code&gt; (includes failing &lt;code&gt;lasttoot&lt;/code&gt; to botsin.space)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/posts/2023/02/17/2023-02-17-links-for-you.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/posts/2023/02/15/2023-02-15-building-a-mastodon-bot-listing-page-in-eleventy.md&lt;/code&gt; (examples in &lt;code&gt;{% raw %}&lt;/code&gt; blocks; not executed)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;current-stack-(for-context)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#current-stack-(for-context)&quot;&gt;Current Stack (for context)&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Site generator:&lt;/strong&gt; Eleventy (11ty) v3&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hosting:&lt;/strong&gt; Netlify&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Content:&lt;/strong&gt; Markdown posts under &lt;code&gt;src/posts/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Templates:&lt;/strong&gt; Liquid, EJS, Nunjucks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; Static HTML in &lt;code&gt;_site/&lt;/code&gt;&lt;/p&gt;
&lt;/script&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/zero-md&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@tschannik?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Yannik Zimmermann&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/a-cat-that-is-walking-across-a-street-aT432COAiFs?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                <category term="eleventy" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/06/my-new-role-helping-ai-adoption-at-cursor</id>
                <title>My New Role - Helping AI Adoption at Cursor</title>
                <updated>2026-07-06T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/06/my-new-role-helping-ai-adoption-at-cursor" rel="alternate" type="text/html" title="My New Role - Helping AI Adoption at Cursor"/>
                <content type="html">
				
                        &lt;p&gt;This will be a short and sweet post to announce that today is my first day at &lt;a href=&quot;https://cursor.com&quot;&gt;Cursor&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/07/happy-dance.gif&quot; loading=&quot;lazy&quot; alt=&quot;Happy Dance&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;I&apos;m starting today as an AI Adoption Engineer, which basically boils down to helping our customers make better use of Cursor and AI in general. If you&apos;ve been visiting my site for a while now, you know I&apos;ve been using AI for quite some time, but more importantly, I&apos;ve also been adopting AI in my own workflows and processes. It takes time and energy but the payoff can be so incredible, and that&apos;s what I get to help our developers with.&lt;/p&gt;
&lt;p&gt;Happy Day One!&lt;/p&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                
                <category term="misc" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/07/05/links-for-you-7526</id>
                <title>Links For You (7/5/26)</title>
                <updated>2026-07-05T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/07/05/links-for-you-7526" rel="alternate" type="text/html" title="Links For You (7/5/26)"/>
                <content type="html">
				
                        &lt;p&gt;In my &lt;a href=&quot;https://www.raymondcamden.com/2026/06/21/links-for-you-62126&quot;&gt;last links post&lt;/a&gt;, I hinted that I may have some good news on the job front, and if you follow me on &lt;a href=&quot;https://linkedin.com/in/raymondcamden&quot;&gt;LinkedIn&lt;/a&gt; you already know that I&apos;ve signed on to my next gig. Tomorrow morning I&apos;ll share on here (and on LI) details about the new gig, but I am &lt;em&gt;beyond&lt;/em&gt; excited about this new job. With that out of the way, how about some happy links for what&apos;s going to be a really dang good week?&lt;/p&gt;
&lt;h2 id=&quot;javascript-in-the-shadows&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#javascript-in-the-shadows&quot;&gt;JavaScript in the Shadows&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As one of the new proposals for JavaScript, the &lt;a href=&quot;https://github.com/tc39/proposal-shadowrealm&quot;&gt;ShadowRealm API&lt;/a&gt; is pretty interesting. It allows for the creation of a realm, or in my mind, an environment, with JavaScript that is 100% disconnected from the main environment of your browser window. Mat Marquis gives a great introduction to this in &lt;a href=&quot;https://css-tricks.com/soon-we-can-finally-banish-javascript-to-the-shadowrealm/&quot;&gt;Soon We Can Finally Banish JavaScript to the ShadowRealm&lt;/a&gt; over on CSS Tricks. (If for some reason you&apos;ve never heard of &lt;a href=&quot;https://css-tricks.com&quot;&gt;css-tricks&lt;/a&gt;, you should absolutely bookmark it and make it part of your required reading.) Mat&apos;s introduction is a pretty fun read as well.&lt;/p&gt;
&lt;h2 id=&quot;brownies-for-storage&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#brownies-for-storage&quot;&gt;Brownies for Storage&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;m a sucker for anything browser-storage related, and &lt;a href=&quot;https://github.com/franciscop/brownies&quot;&gt;Brownies&lt;/a&gt; is a great example of that. Brownies is a library that wraps cookies, Web Storage, and IndexedDB, all in one. Here&apos;s an example from their docs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { cookies, local, db } from &apos;brownies&apos;;

cookies.token = 42;     // Set it
let t = cookies.token;  // Get it
delete cookies.token;   // Eat it

local.token = 42;       // Set it
let t = local.token;    // Get it
delete local.token;     // Del it

// db is ASYNC so read is different
db.token = 42;          // Set it
let t = await db.token; // Get it
delete db.token;        // Del it
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This looks quite delicious. It&apos;s probably not fair to say it is a wrapper for IndexedDB as from what I can see, it&apos;s just doing a key/value store and not letting you specify custom object stores and such. In fact, the docs for &lt;code&gt;db&lt;/code&gt; seem to be missing, and the repository doesn&apos;t have issues enabled so I&apos;m going to have to find another way to leave that feedback, but check it out nonetheless. (I sent the author an email about this.)&lt;/p&gt;
&lt;h2 id=&quot;don&apos;t-use-javascript!&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#don&apos;t-use-javascript!&quot;&gt;Don&apos;t use JavaScript!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My first two links were JavaScript related, so why not advise &lt;em&gt;against&lt;/em&gt; JavaScript in my third link? While not an &amp;quot;anti&amp;quot; JavaScript resource, &lt;a href=&quot;https://aarontgrogg.github.io/NoLoJS/&quot;&gt;NoLoJS&lt;/a&gt; is a resource showing multiple different UX/UI components that can be built with no, or at least little, JavaScript. As I &lt;a href=&quot;https://www.raymondcamden.com/2026/07/01/building-custom-form-selection-blocks-no-js-all-css&quot;&gt;discovered last week&lt;/a&gt;, there&apos;s a lot you can do with CSS and removing JavaScript dependencies is just one benefit.&lt;/p&gt;
&lt;h2 id=&quot;just-for-fun&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#just-for-fun&quot;&gt;Just For Fun&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Yesterday was July 4th which means hot dogs and fireworks and that&apos;s all... right? Unsurprisingly, there&apos;s quite a few examples of fireworks being rendered in HTML. Here&apos;s one fun example that&apos;s pure CSS:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-height=&quot;700&quot; data-pen-title=&quot;Pure CSS Fireworks&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;WNMmQX&quot; data-user=&quot;yshlin&quot; style=&quot;height: 700px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/yshlin/pen/WNMmQX&quot;&gt;
  Pure CSS Fireworks&lt;/a&gt; by Eddie Lin (&lt;a href=&quot;https://codepen.io/yshlin&quot;&gt;@yshlin&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
                        
                
				</content>

                
                <category term="links4you" />
                
                
                <category term="misc" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
</feed>