<?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-09-08T18:54:04+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/09/06/building-a-document-qa-system-with-on-device-ai</id>
                <title>Building a Document Q&amp;A System with On-Device AI</title>
                <updated>2026-09-06T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/09/06/building-a-document-qa-system-with-on-device-ai" rel="alternate" type="text/html" title="Building a Document Q&amp;A System with On-Device AI"/>
                <content type="html">
				
                        &lt;p&gt;Every now and then I build a demo for a presentation and think to myself - this deserves its own blog post. I then promptly forget to actually do that. Even better, I completely forget to even show the demo in the presentation. A few weeks ago I made my first presentation at RenderATL and gave my talk on Chrome&apos;s &lt;a href=&quot;https://developer.chrome.com/docs/ai/built-in&quot;&gt;on-device AI&lt;/a&gt; technology. I&apos;ve been blogging about this particular set of APIs for a few years now and I&apos;ve given talks on it at numerous conferences. While these APIs are still evolving, many of them (and the ones I&apos;m using today in particular) are now fully GA for Chrome. For this demo, I put together a few things I&apos;ve done before into one cohesive tool - a completely client-side application that lets you pick a file, get a summary, and then chat with it.&lt;/p&gt;
&lt;h2 id=&quot;the-stack&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-stack&quot;&gt;The Stack&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The demo is built from a few different things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, I&apos;m making use of the &lt;strong&gt;incredibly excellent&lt;/strong&gt; &lt;a href=&quot;https://officeparser.harshankur.com/&quot;&gt;officeParser&lt;/a&gt; library by Harsh Ankur. This library can take any Office document, or PDF, or ODT, or CSV, and so on, and creates a parsed version you can do nearly anything with. For me, the most important feature is the ability to convert the file into Markdown which can then be used with AI.&lt;/li&gt;
&lt;li&gt;Next, it makes use of Chrome&apos;s &lt;a href=&quot;https://developer.chrome.com/docs/ai/summarizer-api&quot;&gt;Summarizer API&lt;/a&gt; which - you guessed it - provides summaries of textual input.&lt;/li&gt;
&lt;li&gt;Finally, it makes use of Chrome&apos;s &lt;a href=&quot;https://developer.chrome.com/docs/ai/prompt-api&quot;&gt;Prompt API&lt;/a&gt; to let you ask questions about the input.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The web app itself is a simple Vite app using vanilla JavaScript. I used my AI agent to set up the application and create the design and there was quite a bit of back and forth to get things locked down, but the end result is pretty cool I think.&lt;/p&gt;
&lt;h2 id=&quot;the-app&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-app&quot;&gt;The App&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, I&apos;m obviously going to link to it, and the source, too, but I thought I&apos;d share a few screenshots for people who may be on Safari, or maybe reading this on iOS where Chrome is locked down to the Safari engine.&lt;/p&gt;
&lt;p&gt;On opening the app, you get a three panel UI. The left side is for selecting and previewing the document. The upper right is for the summary. The rest of the right column is for Q and A:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/09/dqa1.png&quot; loading=&quot;lazy&quot; alt=&quot;Initial view&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;I selected my &lt;a href=&quot;/resume&quot;&gt;resume&lt;/a&gt; and after a few seconds, I get a summary. Now - I should clarify - the reason it works fast for me is that Chrome has already downloaded the roughly 4 gig model. That&apos;s absolutely &lt;em&gt;not&lt;/em&gt; insignificant. But it&apos;s also a model shared by the browser for &lt;em&gt;any&lt;/em&gt; web site making use of these APIs so it&apos;s not something a user has to download again and again.&lt;/p&gt;
&lt;p&gt;Here&apos;s the summary that was displayed:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/09/dqa2.png&quot; loading=&quot;lazy&quot; alt=&quot;Summary&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;This is &lt;em&gt;nearly&lt;/em&gt; perfect. For some reason it thinks I worked at Microsoft and as far as I know I only mention Microsoft once in the resume and it&apos;s in regard to work done at Webflow.&lt;/p&gt;
&lt;p&gt;But that brings up a great first use of the Q and A - I specifically asked if Ray worked at Microsoft and I got the proper response:&lt;/p&gt;
&lt;p&gt;&amp;quot;The provided document does not mention Ray Camden working at Microsoft.&amp;quot;&lt;/p&gt;
&lt;p&gt;I asked a followup question about PHP and I liked the response:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/09/dqa3.png&quot; loading=&quot;lazy&quot; alt=&quot;Q and A example&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Honestly that&apos;s a pretty deep answer and kind of impressive I think.&lt;/p&gt;
&lt;p&gt;Anyway, if you like this, and are using an up to date Chrome (on desktop or Android), you can check it out here: &lt;a href=&quot;https://document-question-answer.netlify.app/&quot;&gt;https://document-question-answer.netlify.app/&lt;/a&gt;. You can peruse the source up here: &lt;a href=&quot;https://github.com/cfjedimaster/doc_qa&quot;&gt;https://github.com/cfjedimaster/doc_qa&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                <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/09/02/my-family-allowance-a-simple-allowance-tracking-site</id>
                <title>My Family Allowance - a Simple Allowance Tracking Site</title>
                <updated>2026-09-02T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/09/02/my-family-allowance-a-simple-allowance-tracking-site" rel="alternate" type="text/html" title="My Family Allowance - a Simple Allowance Tracking Site"/>
                <content type="html">
				
                        &lt;p&gt;As you can probably tell, I&apos;ve been on a bit of a builder kick the last few weeks. Today&apos;s &amp;quot;release&amp;quot; (it&apos;s been live for a few days already) is a tool to help fix a manual process we had in our family - tracking allowance for the kids.&lt;/p&gt;
&lt;p&gt;Previously we made use of a Google Sheet. Each kid had a column. When a kid would ask for something, we&apos;d look to see when we last added allowance, multiply the number of weeks by their rate, add it, and then add another row to reduce by the amount they were spending. A simple formula on the right hand side would give a running total so they could see what their current balance was.&lt;/p&gt;
&lt;p&gt;I decided to take this manual process and turn it into a proper application.&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/09/over.webp&quot; loading=&quot;lazy&quot; alt=&quot;Overengineering everywhere&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Using Cursor, I created an application with the following features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The parent signs up with their Google account.&lt;/li&gt;
&lt;li&gt;They then have a simple admin to add, edit, and delete kids while setting their weekly allowance.&lt;/li&gt;
&lt;li&gt;The parent can then add or remove funds when necessary.&lt;/li&gt;
&lt;li&gt;Every Sunday, automatically, allowance is paid.&lt;/li&gt;
&lt;li&gt;They are given a unique URL for their family and can set a basic password. A great idea for this would be the same password used for wifi in the house.&lt;/li&gt;
&lt;li&gt;The &amp;quot;family&amp;quot; URL lets kids see their totals, and even click in to see a transaction history if they want to remember what they spent money on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The stack is Astro, hosted on Netlify, with their Netlify DB as the storage system.&lt;/p&gt;
&lt;p&gt;That&apos;s it! You can check it out here: &lt;a href=&quot;https://www.myallowance.family/&quot;&gt;https://www.myallowance.family/&lt;/a&gt;. If you want to check out the source, you can check out the repo: &lt;a href=&quot;https://github.com/cfjedimaster/myfamilyallowance&quot;&gt;https://github.com/cfjedimaster/myfamilyallowance&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@igalness?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Igal Ness&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/person-holding-fan-of-100-us-dollar-bill-9wY2ofzQ9Us?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="astro" />
                
                
                <category term="misc" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/09/01/weather-nerd-check-out-my-osx-radar-widget</id>
                <title>Weather Nerd? Check out My OSX Radar Widget</title>
                <updated>2026-09-01T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/09/01/weather-nerd-check-out-my-osx-radar-widget" rel="alternate" type="text/html" title="Weather Nerd? Check out My OSX Radar Widget"/>
                <content type="html">
				
                        &lt;p&gt;I know I&apos;m not alone in being a bit of a weather nerd. Along with four or five weather apps on my phone, I&apos;ve got two physical devices in my office that show the weather and a weather widget on my Mac that shows the day&apos;s forecast. That&apos;s not enough. What I really wanted was a radar widget as well, so I built one:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/09/radar1.jpg&quot; loading=&quot;lazy&quot; alt=&quot;Screenshot of radar widget&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Ok, to be fair, I used Cursor to build it - but it really scratched an itch I&apos;ve had for a while to constantly have one more thing to distract me.&lt;/p&gt;
&lt;p&gt;If you want a copy of this yourself (or want to make it better!), I&apos;ve set up a GitHub repo here: &lt;a href=&quot;https://github.com/cfjedimaster/precipradar&quot;&gt;https://github.com/cfjedimaster/precipradar&lt;/a&gt;. I&apos;ve got both the source as well as a build for easy installation.&lt;/p&gt;
&lt;p&gt;Note that as with my &lt;a href=&quot;https://www.raymondcamden.com/2026/08/26/like-lego-got-a-mac-get-my-screensaver&quot;&gt;LEGO screensaver&lt;/a&gt;, you&apos;ll need to run a CLI command to authorize the app, but that&apos;s a onetime operation. Enjoy!&lt;/p&gt;

                        
                
				</content>

                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/08/26/like-lego-got-a-mac-get-my-screensaver</id>
                <title>Like LEGO? Got a Mac? Get My Screensaver</title>
                <updated>2026-08-26T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/08/26/like-lego-got-a-mac-get-my-screensaver" rel="alternate" type="text/html" title="Like LEGO? Got a Mac? Get My Screensaver"/>
                <content type="html">
				
                        &lt;p&gt;Good morning my fabulous readers. This will be a quick post as I just wanted to share something cool I built with Cursor over the past couple of days - a LEGO screensaver. I&apos;m a huge fan of &amp;quot;show me something random from something I like&amp;quot; and have built multiple bots in that vein. I recently discovered &lt;a href=&quot;https://rebrickable.com/downloads/&quot;&gt;Rebrickable&apos;s LEGO datasets&lt;/a&gt; which contains a CSV file of LEGO sets. The data looks like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set_num,name,year,theme_id,num_parts,img_url
0003977811-1,Ninjago: Book of Adventures,2022,761,1,https://cdn.rebrickable.com/media/sets/0003977811-1.jpg
001-1,Gears,1965,756,43,https://cdn.rebrickable.com/media/sets/001-1.jpg
0011-2,Town Mini-Figures,1979,67,12,https://cdn.rebrickable.com/media/sets/0011-2.jpg
0011-3,Castle 2 for 1 Bonus Offer,1987,199,0,https://cdn.rebrickable.com/media/sets/0011-3.jpg
0012-1,Space Mini-Figures,1979,143,12,https://cdn.rebrickable.com/media/sets/0012-1.jpg
0013-1,Space Mini-Figures,1979,143,12,https://cdn.rebrickable.com/media/sets/0013-1.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I thought it would be cool to select a random set, show the year, name, number of pieces, and render the image as well. I gave this CSV to Cursor and started iterating on building a simple OSX screensaver. I had it filter out sets where the number of pieces was greater than or equal to 50 (typically &amp;quot;swag&amp;quot;, non-buildable items) or with names that also appeared to be non-buildable things.&lt;/p&gt;
&lt;p&gt;After that, I just let Cursor handle creating the project. I know &lt;em&gt;nothing&lt;/em&gt; about OSX development, thankfully Cursor does. The only real issue I ran into was OSX aggressively caching the screensaver data which made updates a bit difficult. I think we (and by that I mean mostly Cursor, and some me) figured that out eventually.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example of how it looks:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/08/lego.png&quot; loading=&quot;lazy&quot; alt=&quot;LEGO screensaver example&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Alright, so before I link you to the GitHub repo, a quick warning. I&apos;m not paying Apple for a developer&apos;s license which means if you get the build, you&apos;ll need to run a one-time command in your terminal to whitelist the app.&lt;/p&gt;
&lt;p&gt;Ok, with that out of the way, head over to the repo and check it out: &lt;a href=&quot;https://github.com/cfjedimaster/lego-screensaver&quot;&gt;https://github.com/cfjedimaster/lego-screensaver&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/08/23/links-for-you-82326</id>
                <title>Links For You (8/23/26)</title>
                <updated>2026-08-23T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/08/23/links-for-you-82326" rel="alternate" type="text/html" title="Links For You (8/23/26)"/>
                <content type="html">
				
                        &lt;p&gt;Greetings, programs. Once again, I&apos;m a bit behind on these posts, but I just got back from two weeks of travel and I&apos;m so happy to be home for a while. My last trip was pretty stressful. On the way out, our plane blew a tire (or tires) which required an emergency landing in Dallas (which is where we were scheduled to stop anyway). Everything ended up fine, but as I said, stressful. Then coming home we were &lt;em&gt;minutes&lt;/em&gt; from our destination when the pilot had to turn around due to storms. You can actually see exactly where in the route where they said NOPE:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/08/nope.png&quot; loading=&quot;lazy&quot; alt=&quot;Flight track&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;For this, and other reasons, I&apos;m damn happy I&apos;ve got a therapy session tomorrow. So, let&apos;s get to the links.&lt;/p&gt;
&lt;h2 id=&quot;and-the-winner-for-best-project-name-goes-to...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#and-the-winner-for-best-project-name-goes-to...&quot;&gt;And the winner for best project name goes to...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve only given this a cursory glance, but the name itself was enough to make me giggle: &lt;a href=&quot;https://seanhelvey.com/mullet-stack/guide/&quot;&gt;The Mullet Stack&lt;/a&gt;. The Mullet Stack describes itself as &amp;quot;JavaScript in the front, Python in the back&amp;quot;, and describes the combination of a backend built with FastAPI and Pydantic and a front end using React and TypeScript. Now, I&apos;m no React fan, and when I was playing with Python web frameworks last year (this year? time is weird) I didn&apos;t get around to checking out FastAPI, but this guide does a great job of walking you through the process of setting up both ends and wiring things together nicely.&lt;/p&gt;
&lt;h2 id=&quot;change-to-clipboard-reading-in-javascript&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#change-to-clipboard-reading-in-javascript&quot;&gt;Change to Clipboard Reading in JavaScript&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you&apos;ve ever done anything with JavaScript and the clipboard, the Chrome team has made a change to the API in order to improve performance. &lt;a href=&quot;https://developer.chrome.com/blog/selective-format-read?hl=en&quot;&gt;&amp;quot;Selective format read: A better default for the Async Clipboard API&amp;quot;&lt;/a&gt; discusses how, at least previously, reading from the clipboard could be a bit of a performance hit as the browser would read in all representations of data in the clipboard (along with sanitization) even though your code (typically) only used one format. (As an FYI, when you copy something to the clipboard, it can be represented in multiple different formats.) Their article describes how data is now lazy loaded and - well that&apos;s mainly it. This change has also been implemented in Safari and Firefox has indicated they may adopt this as well.&lt;/p&gt;
&lt;p&gt;You may also be interested in my post about this here: &lt;a href=&quot;https://blog.master.dev/handling-paste-events-in-javascript/&quot;&gt;Handling Paste Events in JavaScript&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;a-new-option-for-generating-rss&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#a-new-option-for-generating-rss&quot;&gt;A New Option for Generating RSS&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve spent a &lt;em&gt;heck&lt;/em&gt; of a lot of time writing code to both generate and parse RSS. One thing you&apos;ll notice if you look for libraries like this is that - typically - they&apos;re pretty old. To be fair, RSS is a spec and hasn&apos;t changed in a while. (A quick Google check seems to confirm the last change was 2009.) The &lt;a href=&quot;https://github.com/jpmonette/feed&quot;&gt;feed&lt;/a&gt; JavaScript/Typescript library is an example of this, with portions over 8 years old, but it very recently was updated and if you find yourself needing to generate RSS, I&apos;d probably start here.&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;Last time I shared a good cheesy 80s video so why not follow it up with even &lt;em&gt;more&lt;/em&gt; cheese? This is from a movie I plan on sharing with my kids real soon now...&lt;/p&gt;
&lt;lite-youtube videoid=&quot;2WN0T-Ee3q4&quot; style=&quot;background-image: url(&apos;https://i.ytimg.com/vi/2WN0T-Ee3q4/hqdefault.jpg&apos;);&quot;&gt;
  &lt;a href=&quot;https://youtube.com/watch?v=2WN0T-Ee3q4&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/08/22/using-the-cursor-sdk-to-build-a-project-documenter</id>
                <title>Using the Cursor SDK to Build a Project Documenter</title>
                <updated>2026-08-22T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/08/22/using-the-cursor-sdk-to-build-a-project-documenter" rel="alternate" type="text/html" title="Using the Cursor SDK to Build a Project Documenter"/>
                <content type="html">
				
                        &lt;p&gt;I&apos;ve shared a few posts recently talking about how I&apos;m using Cursor to learn and employ agentic development. Primarily that work has all been done via our Agent view, but this is just one of the surfaces you can use with the platform. I had a chance recently to try out the SDK and I thought I&apos;d share a little demo I built with it.&lt;/p&gt;
&lt;p&gt;First - some explanation would be helpful. You can use Cursor with the desktop application, on the web, via CLI, on iOS, even via API. But the SDK lets you use the platform from your code. There&apos;s an SDK for &lt;a href=&quot;https://cursor.com/docs/sdk/typescript&quot;&gt;TypeScript&lt;/a&gt;, &lt;a href=&quot;https://cursor.com/docs/sdk/python&quot;&gt;Python&lt;/a&gt;, and a &lt;a href=&quot;https://cursor.com/docs/sdk/bridge&quot;&gt;bridge&lt;/a&gt; that embeds a TypeScript server and lets you use any language. Honestly I&apos;ve never seen that before in a platform and it&apos;s pretty freaking cool. (And... just to remind folks, I do work at Cursor so I&apos;m biased, but that&apos;s absolutely an honest opinion.)&lt;/p&gt;
&lt;p&gt;With the SDK, you can do all the things you can usually do with the platform - run prompts in different modes - swap models at will, including the auto router which makes it easier, even kick off agents that run in the cloud. Definitely check the &lt;a href=&quot;https://cursor.com/docs/sdk/python&quot;&gt;docs&lt;/a&gt; for a full detailed list of what you can do, but it&apos;s basically the platform itself - in your code.&lt;/p&gt;
&lt;p&gt;I built a quick demo with the TypeScript SDK, but then switched over to Python as I felt it a bit easier for me to use. The &lt;a href=&quot;https://cursor.com/docs/sdk/python#quick-start&quot;&gt;quickstart&lt;/a&gt; shows how easy it is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

from cursor_sdk import Agent, LocalAgentOptions

with Agent.create(
    model=&amp;quot;composer-2.5&amp;quot;,
    api_key=&amp;quot;crsr_key&amp;quot;,
    local=LocalAgentOptions(cwd=os.getcwd()),
) as agent:
    print(agent.send(&amp;quot;Summarize what this repository does&amp;quot;).text())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This returns the final result of the prompt, but you also have the ability to get &lt;em&gt;everything&lt;/em&gt; that was generated, so for example, the detailed list of tool calls and such. You can also decide between streaming or just waiting and getting the final result.&lt;/p&gt;
&lt;p&gt;All of this just works - which is what you want in a SDK - and the quick start example is one of my favorite things to do with Cursor - basically &amp;quot;what the heck is this repo/set of code doing&amp;quot; - and I thought it would be cool to turn this into a CLI tool.&lt;/p&gt;
&lt;p&gt;To be clear, normally I&apos;d use the Cursor Agent window, open the repo or folder, and use Ask mode to describe the project. But if I&apos;m not planning to work with the code later and just want a good explanation, a CLI tool could be useful for this purpose instead.&lt;/p&gt;
&lt;p&gt;Of course, the easiest way to build this is to use Cursor itself. I set up a new folder and started a new plan:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/plan create a Python CLI that makes use of the Cursor SDK. The CLI is a 
tool that will scan a code base and generated a detailed report of the 
application/code base of the folder being scanned. What it does, what 
technologies it use, what frameworks are in place, and so forth.

The CLI should support a help command. 
The CLI will scan the current directory by default, but supports an 
argument to specify a path.
The CLI will output Markdown, but an argument allows for a PDF file output.
Require a filename to store the result. 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This created the following plan:&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: Python SDK CLI
overview: Build a greenfield Python CLI (&lt;code&gt;codebase-report&lt;/code&gt;) that uses the Cursor Python SDK’s local agent to analyze a target directory and write a structured Markdown or PDF report to a required output filename.
todos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;id: scaffold
content: Create pyproject.toml, package layout, README with install/auth/usage
status: completed&lt;/li&gt;
&lt;li&gt;id: cli
content: &amp;quot;Implement argparse CLI: required --output, optional PATH, --pdf, --help&amp;quot;
status: completed&lt;/li&gt;
&lt;li&gt;id: scanner
content: Implement Agent.prompt local scan with structured Markdown prompt and exit codes
status: completed&lt;/li&gt;
&lt;li&gt;id: export
content: Implement Markdown write and Markdown-to-PDF via markdown + xhtml2pdf
status: completed
isProject: false&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1 id=&quot;python-cli%3A-codebase-report-via-cursor-sdk&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#python-cli%3A-codebase-report-via-cursor-sdk&quot;&gt;Python CLI: Codebase Report via Cursor SDK&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;goal&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#goal&quot;&gt;Goal&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ship an installable Python CLI that points a &lt;strong&gt;local&lt;/strong&gt; Cursor agent at a folder, asks it to produce a detailed codebase report, and writes the result to a required output file as &lt;strong&gt;Markdown&lt;/strong&gt; (default) or &lt;strong&gt;PDF&lt;/strong&gt; (&lt;code&gt;--pdf&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id=&quot;cli-interface&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#cli-interface&quot;&gt;CLI interface&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Console script: &lt;code&gt;codebase-report&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;codebase-report --output REPORT.md [PATH]
codebase-report --pdf --output REPORT.pdf [PATH]
codebase-report --help
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arg&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional; defaults to current working directory (&lt;code&gt;.&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-o&lt;/code&gt; / &lt;code&gt;--output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Required&lt;/strong&gt; filename/path for the report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Convert agent Markdown to PDF and write that instead of &lt;code&gt;.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-h&lt;/code&gt; / &lt;code&gt;--help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard argparse help (covers the “help command” requirement)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Validation: &lt;code&gt;PATH&lt;/code&gt; must exist and be a directory; refuse missing &lt;code&gt;--output&lt;/code&gt;; exit non-zero on SDK startup vs run failures (see below).&lt;/p&gt;
&lt;h2 id=&quot;architecture&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#architecture&quot;&gt;Architecture&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  CLI[cli.py argparse] --&amp;gt; Scan[scanner.py Agent.prompt]
  Scan --&amp;gt; Agent[Local Cursor agent cwd=PATH]
  Agent --&amp;gt; MD[Markdown text]
  MD --&amp;gt; Export[export.py]
  Export --&amp;gt;|default| FileMD[write --output]
  Export --&amp;gt;|--pdf| FilePDF[MD to HTML to PDF]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Runtime choice:&lt;/strong&gt; local agent with &lt;code&gt;LocalAgentOptions(cwd=resolved_path)&lt;/code&gt;. The agent reads the tree on disk; no cloud clone needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Invocation pattern:&lt;/strong&gt; one-shot &lt;code&gt;Agent.prompt(...)&lt;/code&gt; (create → run → dispose). No multi-turn, no streaming required for v1; optionally print a short “Scanning…” status to stderr.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auth:&lt;/strong&gt; &lt;code&gt;CURSOR_API_KEY&lt;/code&gt; from the environment (documented in README). Pass &lt;code&gt;api_key=os.environ[&amp;quot;CURSOR_API_KEY&amp;quot;]&lt;/code&gt; explicitly so a missing key fails clearly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;composer-2.5&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;project-layout-(greenfield)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#project-layout-(greenfield)&quot;&gt;Project layout (greenfield)&lt;/a&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pyproject.toml          # package + console_scripts entry
README.md               # install, CURSOR_API_KEY, usage examples
src/codebase_report/
  __init__.py
  __main__.py           # python -m codebase_report
  cli.py                # argparse + main()
  scanner.py            # Agent.prompt + prompt text + error handling
  export.py             # write markdown / pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dependencies in &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cursor-sdk&lt;/code&gt; (Python ≥3.10)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;markdown&lt;/code&gt; + &lt;code&gt;xhtml2pdf&lt;/code&gt; for PDF (pure pip; no system WeasyPrint/Pandoc)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;core-implementation-details&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#core-implementation-details&quot;&gt;Core implementation details&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;prompt-(scanner.py)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#prompt-(scanner.py)&quot;&gt;Prompt (&lt;code&gt;scanner.py&lt;/code&gt;)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Instruct the agent to &lt;strong&gt;explore the codebase&lt;/strong&gt; (read manifests, configs, source layout) and return &lt;strong&gt;only Markdown&lt;/strong&gt; covering at least:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Overview / purpose&lt;/li&gt;
&lt;li&gt;Primary languages and runtimes&lt;/li&gt;
&lt;li&gt;Frameworks and major libraries&lt;/li&gt;
&lt;li&gt;Architecture / top-level structure&lt;/li&gt;
&lt;li&gt;Entry points and how to run/build/test (if discoverable)&lt;/li&gt;
&lt;li&gt;Notable tooling (CI, linters, package managers)&lt;/li&gt;
&lt;li&gt;Anything else material about the app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also instruct: do not modify files; do not wrap the whole answer in a single fenced code block. The CLI owns writing the output file from &lt;code&gt;result.result&lt;/code&gt; (or equivalent final text).&lt;/p&gt;
&lt;h3 id=&quot;agent-call&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#agent-call&quot;&gt;Agent call&lt;/a&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from cursor_sdk import Agent, AgentOptions, LocalAgentOptions, CursorAgentError

result = Agent.prompt(
    PROMPT,
    AgentOptions(
        api_key=os.environ[&amp;quot;CURSOR_API_KEY&amp;quot;],
        model=&amp;quot;composer-2.5&amp;quot;,
        local=LocalAgentOptions(cwd=str(scan_path.resolve())),
    ),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Error handling per SDK guidance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CursorAgentError&lt;/code&gt; → stderr + exit &lt;code&gt;1&lt;/code&gt; (auth/config/network; never started)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;result.status == &amp;quot;error&amp;quot;&lt;/code&gt; → stderr + exit &lt;code&gt;2&lt;/code&gt; (run failed)&lt;/li&gt;
&lt;li&gt;success → extract Markdown text, then export&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;export-(export.py)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#export-(export.py)&quot;&gt;Export (&lt;code&gt;export.py&lt;/code&gt;)&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Markdown:&lt;/strong&gt; write UTF-8 text to &lt;code&gt;--output&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDF:&lt;/strong&gt; &lt;code&gt;markdown.markdown(...)&lt;/code&gt; → HTML → &lt;code&gt;xhtml2pdf&lt;/code&gt; → write bytes to &lt;code&gt;--output&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Light post-process: if the agent returns a single outer &lt;code&gt;markdown ... &lt;/code&gt; fence, strip it before writing.&lt;/p&gt;
&lt;h2 id=&quot;readme-(minimal)&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#readme-(minimal)&quot;&gt;README (minimal)&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip install -e .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;export CURSOR_API_KEY=...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Example scans of &lt;code&gt;.&lt;/code&gt; and an explicit path, Markdown and PDF&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;out-of-scope-for-v1&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#out-of-scope-for-v1&quot;&gt;Out of scope for v1&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cloud agents, resume, streaming UI, interactive REPL&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Config files / model override flags&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Uploading or publishing the report&lt;/p&gt;
&lt;/script&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/zero-md&gt;
&lt;p&gt;By the way, the Architecture was a pretty Mermaid chart that&apos;s not rendering on my blog:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/08/mermaid.png&quot; loading=&quot;lazy&quot; alt=&quot;Mermaid&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;The end result - in terminal I can create a report in either Markdown or PDF. I&apos;ll share a link to the entire thing below, but let&apos;s take a look at the Python file responsible for analyzing the codebase via the Cursor SDK:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Run a local Cursor agent to produce a codebase Markdown report.&amp;quot;&amp;quot;&amp;quot;

from __future__ import annotations

import os
import sys
from pathlib import Path

from cursor_sdk import Agent, AgentOptions, CursorAgentError, LocalAgentOptions

PROMPT = &amp;quot;&amp;quot;&amp;quot;\
Explore this codebase thoroughly (manifests, configs, source layout, docs) and \
produce a detailed report about the application.

Cover at least:
- Overview / purpose of the project
- Primary languages and runtimes
- Frameworks and major libraries
- Architecture and top-level structure
- Entry points and how to run, build, and test (if discoverable)
- Notable tooling (CI, linters, package managers, etc.)
- Anything else material about how the app works

Rules:
- Do not modify, create, or delete any files.
- Return ONLY Markdown for the report (headings, lists, short code snippets as needed).
- Do not wrap the entire reply in a single fenced code block.
&amp;quot;&amp;quot;&amp;quot;


class ScanError(Exception):
    &amp;quot;&amp;quot;&amp;quot;CLI-facing scan failure with an exit code.&amp;quot;&amp;quot;&amp;quot;

    def __init__(self, message: str, exit_code: int) -&amp;gt; None:
        super().__init__(message)
        self.exit_code = exit_code


def _require_api_key() -&amp;gt; str:
    api_key = os.environ.get(&amp;quot;CURSOR_API_KEY&amp;quot;, &amp;quot;&amp;quot;).strip()
    if not api_key:
        raise ScanError(
            &amp;quot;CURSOR_API_KEY is not set. Export it before running codebase-report.&amp;quot;,
            exit_code=1,
        )
    return api_key


def _extract_markdown(result: object) -&amp;gt; str:
    text = getattr(result, &amp;quot;result&amp;quot;, None)
    if isinstance(text, str) and text.strip():
        return text
    raise ScanError(&amp;quot;Agent finished but returned no report text.&amp;quot;, exit_code=2)


def scan_codebase(scan_path: Path) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;Analyze *scan_path* with a local Cursor agent and return Markdown.&amp;quot;&amp;quot;&amp;quot;
    api_key = _require_api_key()
    cwd = str(scan_path.resolve())

    print(f&amp;quot;Scanning {cwd} with Cursor agent…&amp;quot;, file=sys.stderr)

    try:
        result = Agent.prompt(
            PROMPT,
            AgentOptions(
                api_key=api_key,
                model=&amp;quot;composer-2.5&amp;quot;,
                local=LocalAgentOptions(cwd=cwd),
            ),
        )
    except CursorAgentError as err:
        retryable = getattr(err, &amp;quot;is_retryable&amp;quot;, False)
        raise ScanError(
            f&amp;quot;startup failed: {err.message} (retryable={retryable})&amp;quot;,
            exit_code=1,
        ) from err

    status = getattr(result, &amp;quot;status&amp;quot;, None)
    if status == &amp;quot;error&amp;quot;:
        run_id = getattr(result, &amp;quot;id&amp;quot;, &amp;quot;unknown&amp;quot;)
        raise ScanError(f&amp;quot;run failed: {run_id}&amp;quot;, exit_code=2)

    return _extract_markdown(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is pretty robust and the prompt it uses is really well written. (Ok, as a reminder folks, don&apos;t forget prompt writing is still important &lt;em&gt;and&lt;/em&gt; you can cheat at that by asking your AI agent to improve your prompt before you actually run it.)&lt;/p&gt;
&lt;p&gt;I did a quick run of this on my blog and got the following:&lt;/p&gt;
&lt;iframe src=&quot;https://static.raymondcamden.com/images/2026/08/foo.pdf&quot; width=&quot;100%&quot; height=&quot;500&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;This is a rather simple example, but being able to use the Cursor platform in code like this could be really freaking powerful I think. If you&apos;ve done something like this, I&apos;d love to hear more, share a comment below. You can check out the full code here: &lt;a href=&quot;https://github.com/cfjedimaster/cursor_python_sdk_cli_demo&quot;&gt;https://github.com/cfjedimaster/cursor_python_sdk_cli_demo&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                <category term="python" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/08/11/file-preview-a-simple-web-component-for-file-previews</id>
                <title>&lt;file-preview&gt; - A simple web component for file previews</title>
                <updated>2026-08-11T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/08/11/file-preview-a-simple-web-component-for-file-previews" rel="alternate" type="text/html" title="&lt;file-preview&gt; - A simple web component for file previews"/>
                <content type="html">
				
                        &lt;p&gt;This was a &amp;quot;I can&apos;t fall asleep and my mind is wondering&amp;quot; type of idea and honestly, I&apos;m surprised at how good it came out. For a while now I&apos;ve shared demos where a user picks an image and ... something. It doesn&apos;t matter. But I typically add a preview of the image when it&apos;s selected. This works because once the user selects a file on their local system, JavaScript can then access the bits of that file, making it trivial then to render it. The idea I had, late at night, was simple - what if this could be a web component and expanded to cover different file types?&lt;/p&gt;
&lt;p&gt;In my imagination, it would look something like this in code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;form&amp;gt;
    &amp;lt;!-- various other form things --&amp;gt;
    &amp;lt;label for=&amp;quot;upload&amp;quot;&amp;gt;Select a file: &amp;lt;/label&amp;gt;
    &amp;lt;input type=&amp;quot;file&amp;quot; id=&amp;quot;upload&amp;quot; name=&amp;quot;upload&amp;quot;&amp;gt;
    &amp;lt;file-preview file-id=&amp;quot;upload&amp;quot;&amp;gt;&amp;lt;/file-preview&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The component takes one attribute, &lt;code&gt;file-id&lt;/code&gt;, which points to the ID value of the field to monitor. As for preview support, I figured I could cover these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Images (of course)&lt;/li&gt;
&lt;li&gt;HTML, .txt&lt;/li&gt;
&lt;li&gt;Markdown (after converting to HTML)&lt;/li&gt;
&lt;li&gt;Office docs (Using the excellent &lt;a href=&quot;https://officeparser.harshankur.com/&quot;&gt;officeParser&lt;/a&gt; library)&lt;/li&gt;
&lt;li&gt;PDF (ditto)&lt;/li&gt;
&lt;li&gt;Audio and Video (using the browser&apos;s built-in support)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And more. I decided to let Cursor take a swing at this, so I started with this prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;I want to create a web component that does the following:

It&apos;s tied to a input[type=file] control. When the user selects a file, it attempts to preview it. 

It looks like so: &amp;lt;file-preview file-id=&amp;quot;id of file field&amp;quot;&amp;gt;&amp;lt;/file-preview&amp;gt;

The user would position/size it with CSS as they see fit. The component would support previews for:

* Office docs and PDFs via https://officeparser.harshankur.com/. This will be lazy loaded when a matching file type is selected. It will render the HTML version
* HTML docs - as is.
* Markdown - parse to HTML via the `marked` library and render the HTML
* Images - render as usual
* Video and Audio - create a new instance of &amp;lt;video&amp;gt; or &amp;lt;audio&amp;gt; so the user could click play to preview

Anything else I&apos;m missing?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cursor came back with a bunch of suggestions, including supporting text type documents (JSON, txt, etc) and said it should handle noting when the associated file field was reset.&lt;/p&gt;
&lt;p&gt;And that was pretty much it. In the first iteration there was a bug with the iframe it used to render a PDF. This was due to the iframe&apos;s &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox&quot;&gt;sandbox&lt;/a&gt; value and easily corrected.&lt;/p&gt;
&lt;p&gt;Cursor&apos;s agent also built a good demo (&lt;a href=&quot;https://cfjedimaster.github.io/file-preview/index.html&quot;&gt;https://cfjedimaster.github.io/file-preview/index.html&lt;/a&gt;) but it was focused on the component, and not a &amp;quot;real&amp;quot; form.&lt;/p&gt;
&lt;p&gt;I returned to Cursor with this prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;the demo you built in index.html is ok, but the idea here was to 
include the web component in a form. make a new file, demo.html, 
and have a few form fields in it, one of which is the file 
preview. just have it post to nothing that&apos;s fine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that created a better demo (imo!):&lt;/p&gt;
&lt;iframe src=&quot;https://cfjedimaster.github.io/file-preview/demo.html&quot; style=&quot;width:100%; height: 600px&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;You can see the full code at the repo here: &lt;a href=&quot;https://github.com/cfjedimaster/file-preview&quot;&gt;https://github.com/cfjedimaster/file-preview&lt;/a&gt;. If enough people ask (ok, let&apos;s say 2), I&apos;ll put this up on NPM for easier installation.&lt;/p&gt;

                        
                
				</content>

                
                <category term="web components" />
                
                <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/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>
        
</feed>