<?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-28T12:18:28+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/26/a-simple-weather-app-in-typescript</id>
                <title>A Simple Weather App in TypeScript</title>
                <updated>2026-09-26T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/09/26/a-simple-weather-app-in-typescript" rel="alternate" type="text/html" title="A Simple Weather App in TypeScript"/>
                <content type="html">
				
                        &lt;p&gt;Happy Sunday, my fellow nerds. If you&apos;ve been keeping up with my posts (all 4 of you - thank you!) then you know I&apos;ve been experimenting with TypeScript (and Vite) recently. This past week I decided to try building a real &amp;quot;app&amp;quot;, and of course by app I mean something incredibly small, but a bit &amp;quot;real world&amp;quot;-ish to kind of get a feel for what the development process felt like.&lt;/p&gt;
&lt;p&gt;I decided on a fairly simple weather application. The application would prompt you for a location. That location gets geocoded to longitude and latitude values. With those values, I&apos;d grab a simple weather forecast. After you add one, you can add another, and delete as well. Finally, all the values are stored in local storage so that on reload you get the same values loaded immediately.&lt;/p&gt;
&lt;p&gt;If you actually want to see this - you can head over to &lt;a href=&quot;https://weather-app-ts-vite-alpine.netlify.app/&quot;&gt;https://weather-app-ts-vite-alpine.netlify.app/&lt;/a&gt;. The complete source may be found here: &lt;a href=&quot;https://github.com/cfjedimaster/typescript-stuff/tree/main/weather-demo&quot;&gt;https://github.com/cfjedimaster/typescript-stuff/tree/main/weather-demo&lt;/a&gt;. Now let me dig into what I discovered while building it.&lt;/p&gt;
&lt;h2 id=&quot;the-one-thing&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-one-thing&quot;&gt;The One Thing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let me start off with what I think is the biggest thing that clicked with me. While building out the various parts and using (at least some) of the TypeScript features, I immediately ran into probably the biggest win - immediate feedback in my editor when I had screwed things up. And yea, I know that&apos;s something TypeScript provided since day one, but it&apos;s also a bit different to kind of see it in action while building the app. Especially since I had two or three times when I had to refactor how I was doing things. Having the types in place (specifically for my remote API calls) helped keep things in order across the different moving parts.&lt;/p&gt;
&lt;p&gt;I also found myself being forced to think more about how those parts interacted. So for example, a wrapper to my geocoding service - I instinctively knew what I had wanted - but having it spelled out in code actually made me see possible issues - modify my approach - and so forth - all earlier than I would have in my usual development process.&lt;/p&gt;
&lt;p&gt;Again - this wasn&apos;t necessarily a surprise to me. This all fell in line with what I knew about TypeScript and what I had expected. But never having built an app from scratch with TypeScript before it still felt pretty cool to actually see all this play out.&lt;/p&gt;
&lt;h2 id=&quot;the-architecture&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-architecture&quot;&gt;The Architecture&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It feels a bit silly to talk about the architecture of such a small little app, but this is how I built out the parts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Once again, I used Vite.&lt;/li&gt;
&lt;li&gt;On top of that, I used Alpine.js. In case you missed it, my &lt;a href=&quot;https://www.raymondcamden.com/2026/09/22/a-simple-alpinejs-template-with-vite-and-typescript&quot;&gt;last blog post&lt;/a&gt; discussed how to do this.&lt;/li&gt;
&lt;li&gt;The main page handles displaying the &apos;weather cards&apos; along with a simple form to add new locations.&lt;/li&gt;
&lt;li&gt;When a location is entered, I use the excellent &lt;a href=&quot;https://www.geocod.io/&quot;&gt;Geocodio&lt;/a&gt; API to translate it into longitude and latitude.&lt;/li&gt;
&lt;li&gt;With that information, I use the &lt;a href=&quot;https://pirateweather.net/&quot;&gt;Pirate Weather&lt;/a&gt; API to get the forecast.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-ui&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-ui&quot;&gt;The UI&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As mentioned above, this was a Vite and Alpine app, but I also decided to try adding a UI library in as well. I picked &lt;a href=&quot;https://webawesome.com/&quot;&gt;Web Awesome&lt;/a&gt; primarily for the Card element. I&apos;ve used this library in the past (when it was called Shoelace) and I really like it, especially as it uses web components. They also make it easy to only import what you need.&lt;/p&gt;
&lt;p&gt;In my &lt;code&gt;main.ts&lt;/code&gt;, this came down to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import &apos;@awesome.me/webawesome/dist/styles/webawesome.css&apos;;
import &apos;@awesome.me/webawesome/dist/components/card/card.js&apos;;
import &apos;@awesome.me/webawesome/dist/components/input/input.js&apos;;
import &apos;@awesome.me/webawesome/dist/components/button/button.js&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this in place, it was a simple matter to use the components. As an example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&amp;quot;wa-flank:end wa-gap-xs&amp;quot;&amp;gt;
&amp;lt;wa-input placeholder=&amp;quot;New Location (America, Canada, Mexico, UK only)&amp;quot; x-model=&amp;quot;newLocation&amp;quot;&amp;gt;&amp;lt;/wa-input&amp;gt;
&amp;lt;wa-button variant=&amp;quot;brand&amp;quot; @click=&amp;quot;addLocation(newLocation)&amp;quot;&amp;gt;Add&amp;lt;/wa-button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make note of the classes on the &lt;code&gt;div&lt;/code&gt; tag there. That comes from Web Awesome&apos;s various utility classes and this was a place where I leaned on my AI tool to help.&lt;/p&gt;
&lt;p&gt;Here&apos;s the entirety of the main HTML page which shows you both Web Awesome in play as well as my Alpine directives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot; class=&amp;quot;wa-dark&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Weather Demo&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div x-data=&amp;quot;app&amp;quot; class=&amp;quot;container wa-stack&amp;quot;&amp;gt;

      &amp;lt;header&amp;gt;
        &amp;lt;h1&amp;gt;Weather Demo&amp;lt;/h1&amp;gt;
      &amp;lt;/header&amp;gt;

      &amp;lt;template x-if=&amp;quot;locations.length === 0&amp;quot;&amp;gt; 
        &amp;lt;p&amp;gt;No locations added yet. Add a location to get started.&amp;lt;/p&amp;gt;
      &amp;lt;/template&amp;gt;
      &amp;lt;div class=&amp;quot;wa-grid wa-gap-1&amp;quot; style=&amp;quot;--min-column-size: 30ch;&amp;quot;&amp;gt;
      &amp;lt;template x-for=&amp;quot;(location,index) in locations&amp;quot; :key=&amp;quot;location.name&amp;quot;&amp;gt;
        &amp;lt;wa-card class=&amp;quot;card-basic&amp;quot; &amp;gt;
          &amp;lt;h3 slot=&amp;quot;header&amp;quot; x-text=&amp;quot;location.name&amp;quot;&amp;gt;&amp;lt;/h3&amp;gt;
          &amp;lt;wa-button appearance=&amp;quot;plain&amp;quot; @click=&amp;quot;removeLocation(index)&amp;quot; slot=&amp;quot;header-actions&amp;quot;&amp;gt;
            &amp;lt;wa-icon name=&amp;quot;trash&amp;quot; variant=&amp;quot;solid&amp;quot; label=&amp;quot;Delete&amp;quot;&amp;gt;&amp;lt;/wa-icon&amp;gt;
          &amp;lt;/wa-button&amp;gt;
          &amp;lt;template x-if=&amp;quot;location.weather&amp;quot;&amp;gt;
            &amp;lt;p x-html=&amp;quot;`Temperature: ${location.weather.temperature}°F&amp;lt;br/&amp;gt;Condition: ${location.weather.summary}&amp;lt;br&amp;gt;Low: ${location.weather.low}°F&amp;lt;br/&amp;gt;High: ${location.weather.high}°F`&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;
          &amp;lt;/template&amp;gt;
          &amp;lt;template x-if=&amp;quot;!location.weather&amp;quot;&amp;gt;
            &amp;lt;p&amp;gt;Loading weather data...&amp;lt;/p&amp;gt;
          &amp;lt;/template&amp;gt;
        &amp;lt;/wa-card&amp;gt;
      &amp;lt;/template&amp;gt;
        &amp;lt;/div&amp;gt;

      &amp;lt;div class=&amp;quot;wa-flank:end wa-gap-xs&amp;quot;&amp;gt;
      &amp;lt;wa-input placeholder=&amp;quot;New Location (America, Canada, Mexico, UK only)&amp;quot; x-model=&amp;quot;newLocation&amp;quot;&amp;gt;&amp;lt;/wa-input&amp;gt;
      &amp;lt;wa-button variant=&amp;quot;brand&amp;quot; @click=&amp;quot;addLocation(newLocation)&amp;quot;&amp;gt;Add&amp;lt;/wa-button&amp;gt;
      &amp;lt;/div&amp;gt;

    &amp;lt;/div&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/main.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-three-%22services%22&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-three-%22services%22&quot;&gt;The Three &amp;quot;Services&amp;quot;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;m using &amp;quot;services&amp;quot; in quotes there as it feels a bit overly dramatic to name it as such, but I broke out three parts into their own files - one for geocoding, one for the weather, and one for storage. Let&apos;s start with storage first.&lt;/p&gt;
&lt;p&gt;This file wraps the calls to LocalStorage and in theory - I could easily switch out to IndexedDB in the future - but I&apos;d have to mark the functions &lt;code&gt;async&lt;/code&gt; of course:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import type { SavedLocation } from &apos;./types&apos;;

// This is where it&apos;s stored in localStorage. 
const KEY = &apos;weather-locations&apos;;

// Type guard: checks at runtime that an unknown value really is a SavedLocation.
// localStorage can contain anything (old versions of your app, manual edits),
// so we validate instead of trusting it.
// Ray, in case you forget, the value is means that if the function returns true, 
// it&apos;s ok for TS to consider the value as of type SavedLocation
function isSavedLocation(value: unknown): value is SavedLocation {
  if (typeof value !== &apos;object&apos; || value === null) return false;
  const v = value as Record&amp;lt;string, unknown&amp;gt;;
  return (
    typeof v.name === &apos;string&apos; &amp;amp;&amp;amp;
    typeof v.longitude === &apos;number&apos; &amp;amp;&amp;amp;
    typeof v.latitude === &apos;number&apos;
  );
}

export function getLocations(): SavedLocation[] {
  const locations = localStorage.getItem(KEY);
  if(!locations) return [];

  const parsed = JSON.parse(locations);
  return Array.isArray(parsed) ? parsed.filter(isSavedLocation) : [];
}

export function addLocation(location: SavedLocation): void {
  const locations = getLocations();
  locations.push(location);
  localStorage.setItem(KEY, JSON.stringify(locations));
}

export function removeLocation(index: number): void {
  const locations = getLocations();
  locations.splice(index, 1);
  localStorage.setItem(KEY, JSON.stringify(locations));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first thing you see on top is importing a type that defines what a &apos;stored location&apos; is in terms of my application. From that file, here is the definition:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export type SavedLocation = {
  name: string;
  longitude: number;
  latitude: number, 
  weather?: WeatherData;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should mostly make sense - the name comes from the user, the longitude and latitude from the geocoding service.&lt;/p&gt;
&lt;p&gt;The weather part actually comes &lt;em&gt;after&lt;/em&gt; the app loads in the existing values from local storage and hits the weather API. There&apos;s a part of me that looks at that and thinks maybe I should have a type for the &amp;quot;stored&amp;quot; value and one for the &amp;quot;live&amp;quot; value. I also think I could possibly cache the weather so that on reload it&apos;s quicker, but weather data is the kind of thing that gets stale pretty quickly.&lt;/p&gt;
&lt;p&gt;Here&apos;s that file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import type { WeatherData } from &apos;./types&apos;;

const KEY = import.meta.env.VITE_PIRATE_KEY as string;

export async function getWeather(lat: number, lng: number): Promise&amp;lt;WeatherData&amp;gt; {
    const req = await fetch(`https://api.pirateweather.net/forecast/${KEY}/${lat},${lng}?units=us&amp;amp;exclude=minutely,hourly,alerts,flags`);
    const res = await req.json();

    return {
        summary: res.currently.summary,
        temperature: res.currently.temperature,
        low: res.daily.data[0].temperatureLow,
        high: res.daily.data[0].temperatureHigh
    };

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here&apos;s the type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export type WeatherData = {
  summary: string;
  temperature: number;
  low: number;
  high: number;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, here&apos;s the geocoding wrapper:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import type { GeoCodedLocation } from &apos;./types&apos;;

// Only the fields we use from Geocodio&apos;s &amp;quot;simple&amp;quot; format
type GeocodioSimpleResponse = {
  lat?: number;
  lng?: number;
};

const KEY = import.meta.env.VITE_GEOCODIO_KEY as string;

export async function geoCode(location: string): Promise&amp;lt;GeoCodedLocation&amp;gt; {
    const req = await fetch(`https://api.geocod.io/v1.7/geocode?q=${encodeURIComponent(location)}&amp;amp;format=simple&amp;amp;api_key=${KEY}`);
    const res = (await req.json()) as GeocodioSimpleResponse;

    if(!res || !res.lat || !res.lng) {
        throw new Error(&apos;No results&apos;);
    }

    return { lat: res.lat, lng: res.lng };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&apos;ll notice I do the HTTP call slightly different here. I&apos;ve got an inline type here that defines what is being used from Geocodio. I&apos;ll be honest and say I want to think about these two approaches and figure out when I should use each. I mean, when you look at &lt;code&gt;GeoCodedLocation&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export type GeoCodedLocation = {
  lat: number;
  lng: number;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I honestly don&apos;t get the point of the inline type as well. I was having AI help me a bit, so it&apos;s on me to nail down if this makes sense in the context of the file. I&apos;ll be returning to that soon.&lt;/p&gt;
&lt;h2 id=&quot;the-core-app&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-core-app&quot;&gt;The Core App&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now let&apos;s put it together with my core file - which is mainly Alpine.js specific:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import Alpine from &apos;alpinejs&apos;;

import &apos;@awesome.me/webawesome/dist/styles/webawesome.css&apos;;
import &apos;@awesome.me/webawesome/dist/components/card/card.js&apos;;
import &apos;@awesome.me/webawesome/dist/components/input/input.js&apos;;
import &apos;@awesome.me/webawesome/dist/components/button/button.js&apos;;
import &apos;./style.css&apos;;

import type { SavedLocation } from &apos;./types&apos;;
import { getLocations, addLocation, removeLocation } from &apos;./storage&apos;;
import { getWeather } from &apos;./weather&apos;;
import { geoCode } from &apos;./geocode&apos;;

Alpine.data(&apos;app&apos;, () =&amp;gt; ({
  locations: [] as SavedLocation[],
  newLocation: &apos;&apos;,
  init() {
    this.locations = getLocations();
    if(this.locations.length &amp;gt; 0) {
      this.hydrateWeather();
    }
  },
  async addLocation(location: string) {
    if(!location) return;
    const geo = await geoCode(location);
    const newLoc: SavedLocation = { name: location, longitude: geo.lng, latitude: geo.lat };
    this.locations.push(newLoc);
    // just noticed my method is addLocation as is the imported one. works but - eww. 
    addLocation(newLoc);
    this.newLocation = &apos;&apos;;
    // in theory it is wasteful to hydrate ALL of them, but it&apos;s a super quick call
    this.hydrateWeather();
  }, 
  removeLocation(index: number) {
    this.locations.splice(index, 1);
    // same issue with naming - advice?
    removeLocation(index);
  },
  async hydrateWeather() {
    for(const loc of this.locations) {
      const weather = await getWeather(loc.latitude, loc.longitude);
      loc.weather = weather;
    }
  }
}));

Alpine.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you actually took the time to read all that, you can see I wrote a few questions to myself. By accident, I ended up having Alpine methods with the same name as methods in my services, and that &lt;em&gt;really&lt;/em&gt; bugs me... but it also works... so... yeah. It&apos;s kinda clear that &lt;code&gt;this.something&lt;/code&gt; is Alpine and &lt;code&gt;something&lt;/code&gt; is being imported in, but I still don&apos;t care for that. How would you rename things? (And I&apos;d assume the rename would be on the Alpine side.)&lt;/p&gt;
&lt;p&gt;I also notice a few lines where I didn&apos;t define my type. A quick search shows that by adding &lt;code&gt;strict:true&lt;/code&gt; to my &lt;code&gt;tsconfig.json&lt;/code&gt; would fix that. I know in the past I&apos;ve seen that in projects and it&apos;s annoyed the heck out of me, so I get why Vite&apos;s scaffold doesn&apos;t include it, but for my next project, I&apos;m going to turn that on so I can be a bit more precise in my learning with TypeScript.&lt;/p&gt;
&lt;h2 id=&quot;your-turn!&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#your-turn!&quot;&gt;Your Turn!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, I know blogging is dead (again) and no humans read these posts again, but if you are reading this, and know TypeScript well, I&apos;d &lt;strong&gt;love&lt;/strong&gt; any and all feedback! Give me a comment below and help me keep learning this.&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@patrickian4?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Patrick Fore&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/black-corona-typewriter-on-brown-wood-planks-0gkw_9fy0eQ?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                <category term="typescript" />
                
                <category term="alpinejs" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/09/22/a-simple-alpinejs-template-with-vite-and-typescript</id>
                <title>A Simple Alpine.js Template with Vite and TypeScript</title>
                <updated>2026-09-22T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/09/22/a-simple-alpinejs-template-with-vite-and-typescript" rel="alternate" type="text/html" title="A Simple Alpine.js Template with Vite and TypeScript"/>
                <content type="html">
				
                        &lt;p&gt;This will be a quick one - but as I think more about TypeScript and how I&apos;d like to learn (and play, and build silly demos), I naturally thought it may make sense to look at how I&apos;d use &lt;a href=&quot;https://alpinejs.dev/&quot;&gt;Alpine.js&lt;/a&gt; with that stack. Alpine&apos;s been my go to library for web apps that reach the level of complexity where I&apos;d like some help with DOM manipulation and such. I don&apos;t &lt;em&gt;always&lt;/em&gt; use it, because (imho) the default stack should be as vanilla as possible (obviously I&apos;m going a bit off ranch with these explorations into TypeScript and Vite) but Alpine is lightweight and simple and just a great little library in general. What follows isn&apos;t necessarily a &amp;quot;template&amp;quot; as it&apos;s got a bit of template code with it, but I thought it would be helpful to share. As always, let me know what you think!&lt;/p&gt;
&lt;h2 id=&quot;step-one---the-scaffold&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-one---the-scaffold&quot;&gt;Step One - the Scaffold&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In my &lt;a href=&quot;https://www.raymondcamden.com/2026/09/18/its-only-a-decade-late-ill-learn-typescript&quot;&gt;first post&lt;/a&gt; a few days ago, I mentioned that the Vite scaffold support lets you create a vanilla application with TypeScript. I used that for my code and removed as much of the demo code as I could. I do wish Vite&apos;s template was a bit more minimal.&lt;/p&gt;
&lt;h2 id=&quot;step-two---adding-alpine&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-two---adding-alpine&quot;&gt;Step Two - Adding Alpine&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Usually I make use of the Alpine CDN in my demos, for this one, I instead installed it as a dependency. I also installed the Alpine types dependency. Here&apos;s my package.json:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;name&amp;quot;: &amp;quot;alpine-test-1&amp;quot;,
  &amp;quot;private&amp;quot;: true,
  &amp;quot;version&amp;quot;: &amp;quot;0.0.0&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;module&amp;quot;,
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;dev&amp;quot;: &amp;quot;vite&amp;quot;,
    &amp;quot;build&amp;quot;: &amp;quot;tsc &amp;amp;&amp;amp; vite build&amp;quot;,
    &amp;quot;preview&amp;quot;: &amp;quot;vite preview&amp;quot;
  },
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;@types/alpinejs&amp;quot;: &amp;quot;^3.13.11&amp;quot;,
    &amp;quot;typescript&amp;quot;: &amp;quot;~6.0.2&amp;quot;,
    &amp;quot;vite&amp;quot;: &amp;quot;^8.3.0&amp;quot;
  },
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;alpinejs&amp;quot;: &amp;quot;^3.17.4&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;step-three---the-html&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-three---the-html&quot;&gt;Step Three - the HTML&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, so this part is really simple. Alpine &amp;quot;ties&amp;quot; to your DOM usually via an &lt;code&gt;x-data&lt;/code&gt; attribute. That means this is the bare minimum:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div x-data=&amp;quot;app&amp;quot;&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But of course you&apos;ll have more Alpine directives and more HTML. Also, &amp;quot;app&amp;quot; is not required, but is the name I pretty much use all the time. For my template/demo, I output a couple of variables and included a few click directives just to test stuff out. Oh, I also added &lt;a href=&quot;https://simplecss.org/&quot;&gt;Simple.css&lt;/a&gt; to just to make it look prettier. That&apos;s absolutely not necessary. The Vite demo actually imports CSS in their main TypeScript file (you can see that on Stackblitz &lt;a href=&quot;https://stackblitz.com/edit/vitejs-vite-hpgbiipd?file=src%2Fmain.ts&amp;amp;terminal=dev&quot;&gt;here&lt;/a&gt;) and I&apos;m not sure how I feel about that. I know it minimizes the code, but it feels really weird to me. Anyway, here&apos;s my HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://cdn.simplecss.org/simple.min.css&amp;quot;&amp;gt;
    &amp;lt;title&amp;gt;alpine-test-1&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div x-data=&amp;quot;app&amp;quot;&amp;gt;
      &amp;lt;p&amp;gt;
      &amp;lt;span x-text=&amp;quot;message&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;
      &amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;
      &amp;lt;button @click=&amp;quot;meow()&amp;quot;&amp;gt;Meow (default)&amp;lt;/button&amp;gt;
      &amp;lt;button @click=&amp;quot;meow(&apos;Purr...&apos;)&amp;quot;&amp;gt;Meow (custom)&amp;lt;/button&amp;gt;
      &amp;lt;/p&amp;gt;
      &amp;lt;ul&amp;gt;
        &amp;lt;template x-for=&amp;quot;(cat, index) in cats&amp;quot; :key=&amp;quot;index&amp;quot;&amp;gt;
          &amp;lt;li x-text=&amp;quot;cat.name&amp;quot;&amp;gt;&amp;lt;/li&amp;gt;
        &amp;lt;/template&amp;gt;
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/main.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;step-four---enter-the-typescript&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#step-four---enter-the-typescript&quot;&gt;Step Four - Enter the TypeScript&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, for the final bit - my TypeScript code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import Alpine from &apos;alpinejs&apos;;

type Cat = {
  name: string;
  breed: string;
  gender: &apos;male&apos; | &apos;female&apos;;
};

Alpine.data(&apos;app&apos;, () =&amp;gt; ({
  message:&apos;Hello from Alpine!&apos;,
  cats: [] as Cat[],
  init() {
    this.cats.push({ name: &apos;Whiskers&apos;, breed: &apos;Siamese&apos;, gender: &apos;male&apos; });
    this.cats.push({ name: &apos;Fluffy&apos;, breed: &apos;Persian&apos;, gender: &apos;female&apos; });
  },
  meow(message:string = &apos;Meow!&apos;) {
    alert(message);
  }
}));

Alpine.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let me point out the important bits. Unlike my usual Alpine demos, I import Alpine here from the local install. I also need to fire &lt;code&gt;Alpine.start()&lt;/code&gt; manually. But outside of that, it&apos;s pretty vanilla Alpine - define the app and include relevant variables and methods, which in this case is pretty small.&lt;/p&gt;
&lt;p&gt;Now obviously I&apos;m trying to learn TypeScript as well, but I kept it pretty short here. I&apos;ve defined a type for &lt;code&gt;Cat&lt;/code&gt; and when I worked with the data, my editor (Visual Studio Code) provided support as I&apos;d expect - it knew the right parts of a cat and correctly flagged an error if I tried to include something that wasn&apos;t defined in the type.&lt;/p&gt;
&lt;p&gt;As a reminder, this is all done in the editor - it wouldn&apos;t be a &amp;quot;real&amp;quot; error in production - but the idea here - and the benefit of TypeScript - is that I&apos;d (hopefully!) catch it much earlier.&lt;/p&gt;
&lt;p&gt;Outside of that, you can also see where I define &lt;code&gt;cats&lt;/code&gt; as an array of &lt;code&gt;Cat&lt;/code&gt; and specify that the &lt;code&gt;message&lt;/code&gt; argument to &lt;code&gt;meow&lt;/code&gt; is string.&lt;/p&gt;
&lt;p&gt;Again - this is pretty minimal TypeScript usage, but I dig it, and I can really see how in some of my larger Alpine demos in the past, the additional safety/checking/etc would have been real helpful I think.&lt;/p&gt;
&lt;h2 id=&quot;there-is-no-step-five&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#there-is-no-step-five&quot;&gt;There Is No Step Five&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you want the code to try it yourself, you can copy it from here: &lt;a href=&quot;https://github.com/cfjedimaster/typescript-stuff/tree/main/alpine-test-1&quot;&gt;https://github.com/cfjedimaster/typescript-stuff/tree/main/alpine-test-1&lt;/a&gt;. I know this was pretty short, but I&apos;d still love any feedback or advice, so hit up the comments below!&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                <category term="typescript" />
                
                <category term="alpinejs" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/09/20/links-for-you-92026</id>
                <title>Links For You (9/20/26)</title>
                <updated>2026-09-20T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/09/20/links-for-you-92026" rel="alternate" type="text/html" title="Links For You (9/20/26)"/>
                <content type="html">
				
                        &lt;p&gt;Happy &amp;quot;Football is Back&amp;quot; season to those of you who celebrate. I&apos;m currently watching my Saints take on the Ravens (who are also a favorite of mine) and hoping for our first win. The last few weeks have been incredibly busy (I feel like I always say that, sorry) with a trip for work that then morphed into a short vacation with my wife and I which included my first Blizzcon (which is freaking cool as hell) and our first visit to Disneyland. (We&apos;ve been to Disney World a few times.) I think we both agreed that Disney World is cooler, but the crowds and - most importantly - the weather - was much better in California.&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/09/st.jpg&quot; loading=&quot;lazy&quot; alt=&quot;Stormtroopers&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;h2 id=&quot;rss-lookup&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#rss-lookup&quot;&gt;RSS Lookup&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First up for the links today is &lt;a href=&quot;https://www.rsslookup.com/&quot;&gt;RSS Lookup&lt;/a&gt;, a simple web site that lets you paste in a URL and see if it has a RSS feed. Not every site does a good job of letting you know about their RSS options so this can make it easier. Most likely I think the bookmarklet will be more useful to folks.&lt;/p&gt;
&lt;p&gt;If your curious, here&apos;s the meta tag used to signify a site&apos;s feed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&amp;quot;alternate&amp;quot; type=&amp;quot;application/rss+xml&amp;quot; 
title=&amp;quot;Your Site Title - RSS Feed&amp;quot; href=&amp;quot;https://example.com&amp;quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;interfaces%2C-a-collection-by-ron-domingue&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#interfaces%2C-a-collection-by-ron-domingue&quot;&gt;Interfaces, a collection by Ron Domingue&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ron is someone I&apos;ve known for a while, a local down here in Louisiana, who has done some &lt;em&gt;incredibly&lt;/em&gt; stunning visual works. His &lt;a href=&quot;https://interfaces.rondomingue.com/&quot;&gt;Interfaces&lt;/a&gt; is a collection of various UI interfaces for different types of data. I&apos;d be hard pressed to pick a favorite, but &lt;a href=&quot;https://interfaces.rondomingue.com/landfall.html&quot;&gt;Landfall&lt;/a&gt; is a stunning visualization of hurricane tracks and their landfall.&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/09/landfall.png&quot; loading=&quot;lazy&quot; alt=&quot;Landfall&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;h2 id=&quot;remember-imagemaps%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#remember-imagemaps%3F&quot;&gt;Remember Imagemaps?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Imagemaps are one of those old web techs that used to be everywhere, and then disappeared. In case you don&apos;t remember, the idea was that one image on a web page, when clicked, could have multiple different destinations based on where you clicked. Typically this was defined in HTML with coordinates mapping to URLs. But you could also handle the location on the server side as well. In that form, the coordinates are included in the URL and your server side tech has to handle figuring out what goes where.&lt;/p&gt;
&lt;p&gt;This post, &lt;a href=&quot;https://danq.me/2026/07/10/rescuing-7234-gifs/?utm_source=tldrdev&quot;&gt;&amp;quot;Today I Rescued 7,234 Old GIFs&amp;quot;&lt;/a&gt;, by Dan Q explores how he attempted (and succeeded) in scraping a bunch of Gif icons for an old directory.&lt;/p&gt;
&lt;p&gt;Not gonna lie - kinda miss the old web. :)&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;Ok, usually I share cool music videos here, but today I want to share something different. My father-in-law shared this short with us and while it&apos;s marked as comedy, it&apos;s a pretty deep 30-ish minute short staring Jane Kaczmarek. It&apos;s definitely worth your time!&lt;/p&gt;
&lt;lite-youtube videoid=&quot;Ec6yq3isEjg&quot; style=&quot;background-image: url(&apos;https://i.ytimg.com/vi/Ec6yq3isEjg/hqdefault.jpg&apos;);&quot;&gt;
  &lt;a href=&quot;https://youtube.com/watch?v=Ec6yq3isEjg&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/09/18/its-only-a-decade-late-ill-learn-typescript</id>
                <title>It&apos;s only a decade late, I&apos;ll learn TypeScript!</title>
                <updated>2026-09-18T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/09/18/its-only-a-decade-late-ill-learn-typescript" rel="alternate" type="text/html" title="It&apos;s only a decade late, I&apos;ll learn TypeScript!"/>
                <content type="html">
				
                        &lt;p&gt;I&apos;ve been a bit worried lately. Of course, my friends will tell you that&apos;s my norm. But more specifically, I&apos;ve been worried about my coding skills. As I use AI to generate more and more code, and it does that job well, I kinda miss actually &lt;em&gt;writing&lt;/em&gt; code. I&apos;ve also had a bit of writer&apos;s block in terms of &lt;em&gt;topics&lt;/em&gt; to cover, and especially in the area of the web platform. A few days ago it occurred to me - I&apos;ve never &lt;em&gt;really&lt;/em&gt; dug into &lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;. Sure, I&apos;ve used it, many times, in the fourteen or so years it&apos;s been out, but many times I kinda guess at what I&apos;m doing and it was never my default for projects. Partly that&apos;s due to my philosophy of avoiding a build step in my projects and just KISS in general.&lt;/p&gt;
&lt;p&gt;But as I said - the inspiration hit me this past week and I thought I&apos;d do some digging. Here&apos;s a few initial thoughts and experiments. Oh, and I still used AI a bit and I&apos;ll explain exactly how in the post below.&lt;/p&gt;
&lt;h2 id=&quot;typescript-via-vite&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#typescript-via-vite&quot;&gt;TypeScript via Vite&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Working with TypeScript can be as simple as &lt;code&gt;npm i -g typescript&lt;/code&gt; and then &lt;code&gt;tsc somefile.ts&lt;/code&gt;. I decided to use &lt;a href=&quot;https://vite.dev/&quot;&gt;Vite&lt;/a&gt; instead as it&apos;s focused on web projects and most of my development involves web projects. I assume most of my readers know of Vite, but if you somehow have managed to avoid hearing about it or using it yet, it is a &lt;em&gt;lightning&lt;/em&gt; quick build tool for web projects. By &amp;quot;lightning quick&amp;quot; I mean that I still remember the first time I used it thinking it had failed to actually do anything - that&apos;s how quick it works.&lt;/p&gt;
&lt;p&gt;I also appreciated the fact that Vite&apos;s CLI supports a &lt;a href=&quot;https://stackblitz.com/edit/vitejs-vite-rnustopr?file=index.html&amp;amp;terminal=dev&quot;&gt;&lt;code&gt;vanilla-ts&lt;/code&gt;&lt;/a&gt; scaffold that is really lightweight. On the other hand, I&apos;m not necessarily a fan of what it actually shows as it feels a bit like bad practice, specifically an index file with just one div:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
    &amp;lt;link rel=&amp;quot;icon&amp;quot; type=&amp;quot;image/svg+xml&amp;quot; href=&amp;quot;/favicon.svg&amp;quot; /&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot; /&amp;gt;
    &amp;lt;title&amp;gt;Vite + TS&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id=&amp;quot;app&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;script type=&amp;quot;module&amp;quot; src=&amp;quot;/src/main.ts&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then injects a bunch of HTML in main.ts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import &apos;./style.css&apos;
import heroImg from &apos;./assets/hero.png&apos;
import typescriptLogo from &apos;./assets/typescript.svg&apos;
import viteLogo from &apos;./assets/vite.svg&apos;
import { setupCounter } from &apos;./counter.ts&apos;

document.querySelector&amp;lt;HTMLDivElement&amp;gt;(&apos;#app&apos;)!.innerHTML = `
&amp;lt;section id=&amp;quot;center&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;hero&amp;quot;&amp;gt;
    &amp;lt;img src=&amp;quot;${heroImg}&amp;quot; class=&amp;quot;base&amp;quot; width=&amp;quot;170&amp;quot; height=&amp;quot;179&amp;quot;&amp;gt;
    &amp;lt;img src=&amp;quot;${typescriptLogo}&amp;quot; class=&amp;quot;framework&amp;quot; alt=&amp;quot;TypeScript logo&amp;quot;/&amp;gt;
    &amp;lt;img src=&amp;quot;${viteLogo}&amp;quot; class=&amp;quot;vite&amp;quot; alt=&amp;quot;Vite logo&amp;quot; /&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;Get started&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;Edit &amp;lt;code&amp;gt;src/main.ts&amp;lt;/code&amp;gt; and save to test &amp;lt;code&amp;gt;HMR&amp;lt;/code&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;button id=&amp;quot;counter&amp;quot; type=&amp;quot;button&amp;quot; class=&amp;quot;counter&amp;quot;&amp;gt;&amp;lt;/button&amp;gt;
&amp;lt;/section&amp;gt;

&amp;lt;div class=&amp;quot;ticks&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;section id=&amp;quot;next-steps&amp;quot;&amp;gt;
  &amp;lt;div id=&amp;quot;docs&amp;quot;&amp;gt;
    &amp;lt;svg class=&amp;quot;icon&amp;quot; role=&amp;quot;presentation&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;use href=&amp;quot;/icons.svg#documentation-icon&amp;quot;&amp;gt;&amp;lt;/use&amp;gt;&amp;lt;/svg&amp;gt;
    &amp;lt;h2&amp;gt;Documentation&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;Your questions, answered&amp;lt;/p&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li&amp;gt;
        &amp;lt;a href=&amp;quot;https://vite.dev/&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;
          &amp;lt;img class=&amp;quot;logo&amp;quot; src=&amp;quot;${viteLogo}&amp;quot; alt=&amp;quot;&amp;quot; /&amp;gt;
          Explore Vite
        &amp;lt;/a&amp;gt;
      &amp;lt;/li&amp;gt;
      &amp;lt;li&amp;gt;
        &amp;lt;a href=&amp;quot;https://www.typescriptlang.org&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;
          &amp;lt;img class=&amp;quot;button-icon&amp;quot; src=&amp;quot;${typescriptLogo}&amp;quot; alt=&amp;quot;&amp;quot;&amp;gt;
          Learn more
        &amp;lt;/a&amp;gt;
      &amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div id=&amp;quot;social&amp;quot;&amp;gt;
    &amp;lt;svg class=&amp;quot;icon&amp;quot; role=&amp;quot;presentation&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;use href=&amp;quot;/icons.svg#social-icon&amp;quot;&amp;gt;&amp;lt;/use&amp;gt;&amp;lt;/svg&amp;gt;
    &amp;lt;h2&amp;gt;Connect with us&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;Join the Vite community&amp;lt;/p&amp;gt;
    &amp;lt;ul&amp;gt;
      &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;https://github.com/vitejs/vite&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;&amp;lt;svg class=&amp;quot;button-icon&amp;quot; role=&amp;quot;presentation&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;use href=&amp;quot;/icons.svg#github-icon&amp;quot;&amp;gt;&amp;lt;/use&amp;gt;&amp;lt;/svg&amp;gt;GitHub&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
      &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;https://chat.vite.dev/&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;&amp;lt;svg class=&amp;quot;button-icon&amp;quot; role=&amp;quot;presentation&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;use href=&amp;quot;/icons.svg#discord-icon&amp;quot;&amp;gt;&amp;lt;/use&amp;gt;&amp;lt;/svg&amp;gt;Discord&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
      &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;https://x.com/vite_js&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;&amp;lt;svg class=&amp;quot;button-icon&amp;quot; role=&amp;quot;presentation&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;use href=&amp;quot;/icons.svg#x-icon&amp;quot;&amp;gt;&amp;lt;/use&amp;gt;&amp;lt;/svg&amp;gt;X.com&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
      &amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;https://bsky.app/profile/vite.dev&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;&amp;lt;svg class=&amp;quot;button-icon&amp;quot; role=&amp;quot;presentation&amp;quot; aria-hidden=&amp;quot;true&amp;quot;&amp;gt;&amp;lt;use href=&amp;quot;/icons.svg#bluesky-icon&amp;quot;&amp;gt;&amp;lt;/use&amp;gt;&amp;lt;/svg&amp;gt;Bluesky&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/section&amp;gt;

&amp;lt;div class=&amp;quot;ticks&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;section id=&amp;quot;spacer&amp;quot;&amp;gt;&amp;lt;/section&amp;gt;
`

setupCounter(document.querySelector&amp;lt;HTMLButtonElement&amp;gt;(&apos;#counter&apos;)!)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;m probably being &lt;em&gt;real&lt;/em&gt; nit picky here, but having 99% of the HTML for something in JavaScript versus HTML that gets enhanced feels off to me. But again - I&apos;m being picky, and the actual code spit out is pretty minimal. (I could go on for ages about what&apos;s &amp;quot;appropriate&amp;quot; for scaffolded projects and such. Give me a soap box and I&apos;ll tell you more.)&lt;/p&gt;
&lt;p&gt;So yeah, that&apos;s how I started. I pretty much nuked most of the code in main.ts and simplified even more.&lt;/p&gt;
&lt;p&gt;I mentioned earlier I still used a bit of AI in my exploration, and one example was to help clarify things a bit. For example, my very high level basic understanding of TypeScript basically came down to type annotations like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let name:string = &amp;quot;Ray&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This line in the sample code confused me:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;document.querySelector&amp;lt;HTMLDivElement&amp;gt;(&apos;#app&apos;)!.innerHTML = `
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I simply asked Claude to explain it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The &amp;lt;HTMLDivElement&amp;gt; (generic type argument)

By default, querySelector returns Element | null. Element is a very generic type — 
TypeScript doesn&apos;t know whether it&apos;s a div, an input, an SVG circle, etc. That 
matters because different elements have different properties: Element doesn&apos;t 
have .value, for example, but HTMLInputElement does.

Writing querySelector&amp;lt;HTMLDivElement&amp;gt;(&apos;#app&apos;) tells TypeScript: &amp;quot;trust me, the 
thing this selector finds is a div.&amp;quot; So the return type becomes 
HTMLDivElement | null instead of Element | null, and you get proper autocomplete 
and type checking for div-specific properties.

Like the !, this is a compile-time-only assertion — TypeScript doesn&apos;t verify 
it. If #app were actually a &amp;lt;span&amp;gt;, the code would still compile and run; you&apos;d 
just have a lying type.

Fun fact: in this particular line the generic is technically unnecessary, since 
.innerHTML exists on Element anyway. It&apos;s there mostly out of habit/convention 
(this looks like the Vite starter template). It becomes genuinely useful when 
you need element-specific properties like .value, .src, or .checked.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cool. And this is something I&apos;ve really liked about AI in the past. I feel like my question, a particular syntax being used, wouldn&apos;t Google well and AI tools do a better job of getting to the right answer.&lt;/p&gt;
&lt;p&gt;At this point, I felt ready to write something.&lt;/p&gt;
&lt;h2 id=&quot;actually%2C-let&apos;s-not-build-for-the-web...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#actually%2C-let&apos;s-not-build-for-the-web...&quot;&gt;Actually, let&apos;s not build for the web...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So when I figured out a good simple thing to build, I realized I didn&apos;t actually &lt;em&gt;need&lt;/em&gt; a web UI for it, at least not initially, so I did a quick pivot and looked up info on &lt;a href=&quot;https://vitest.dev/&quot;&gt;vitest&lt;/a&gt;, a &amp;quot;Vite-native&amp;quot; testing framework. I actually don&apos;t have a huge amount of experience with unit tests as so much of my work is educational, demo based, and such, that tests aren&apos;t really required. That being said, I&apos;ve written them, I&apos;m familiar with Jest (which vitest is compatible with), so it seemed like an easy win.&lt;/p&gt;
&lt;p&gt;I did a quick &lt;code&gt;npm i -D vitest&lt;/code&gt; and added this to my &lt;code&gt;package.json&lt;/code&gt; scripts block: &lt;code&gt;&amp;quot;test&amp;quot;: &amp;quot;vitest&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This let me run &lt;code&gt;npm run test&lt;/code&gt; in my terminal and get immediate feedback on what I was writing.&lt;/p&gt;
&lt;h2 id=&quot;roll-the-dice...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#roll-the-dice...&quot;&gt;Roll the dice...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;About five years ago, I thought it would be fun, and surely pretty simple, to build an RPG in Vue.js (here&apos;s the first entry: &lt;a href=&quot;https://www.raymondcamden.com/2020/07/17/testing-vuejs-application-files-that-arent-components&quot;&gt;&amp;quot;Testing Vue.js Application Files That Aren&apos;t Components&amp;quot;&lt;/a&gt;. I never finished that project, but I don&apos;t care - it was fun to work on.&lt;/p&gt;
&lt;p&gt;As part of that project, I built a simple &amp;quot;dice&amp;quot; utility that supported typical D&amp;amp;D dice rolls of the form: xDy. This translates to roll a Y-sided dice X times. It can get fancy too, so &lt;code&gt;2d6+2&lt;/code&gt; means roll a six sided die two times and then add two tp the final result. (You could imagine a scenario where a player is attempting to do something they are talented at, so the &lt;code&gt;+2&lt;/code&gt; represents a bonus to their chance to do the thing.)&lt;/p&gt;
&lt;p&gt;The code for that looked like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const dice = {

	roll(style) {
		let bonus=0, total=0;
		if(style.indexOf(&apos;+&apos;) &amp;gt; -1) {
			[style, bonus] = style.split(&apos;+&apos;);
		} 
		
		let [rolls, sided] = style.split(&apos;d&apos;);
		
		//console.log(rolls, sided);
		for(let i=0;i&amp;lt;rolls;i++) {
			total += getRandomIntInclusive(1, sided);
		}
		total += parseInt(bonus);
		return total;
	}
}

function getRandomIntInclusive(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum is inclusive and the minimum is inclusive 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I thought a good first test would be to attempt to rewrite this in a TypeScript file. I actually broke it out into two files, one for the roll action and a &apos;util&apos; one for the random number selection.&lt;/p&gt;
&lt;p&gt;I&apos;ll share the utility function first as it was the simplest change:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export function getRandomInt(min: number, max: number): number {
  min = Math.ceil(min)
  max = Math.floor(max)
  return Math.floor(Math.random() * (max - min + 1)) + min
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was really simple - just adding types to my arguments and result. I also removed &lt;code&gt;inclusive&lt;/code&gt; from the name as I never see myself needing a non-inclusive random number selection.&lt;/p&gt;
&lt;p&gt;The test for this was pretty simple too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { describe, it, expect } from &apos;vitest&apos;
import { getRandomInt } from &apos;./utils&apos;

describe(&apos;getRandomInt&apos;, () =&amp;gt; {
  it(&apos;returns a number between min and max&apos;, () =&amp;gt; {
    const min = 1;
    const max = 2;
    const randomInt = getRandomInt(min, max);
    expect(randomInt).toBeGreaterThanOrEqual(min);
    expect(randomInt).toBeLessThanOrEqual(max);
  })

  it(&apos;returns an integer&apos;, () =&amp;gt; {
    const randomInt = getRandomInt(1, 10);
    expect(Number.isInteger(randomInt)).toBe(true);
  })
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I discovered &lt;em&gt;after&lt;/em&gt; writing this that you can actually screw with &lt;code&gt;Math.random()&lt;/code&gt; which is cool af as the kids say, so I may return to this. But now let&apos;s turn to the dice stuff which got REALLY complicated, but in a fun way.&lt;/p&gt;
&lt;p&gt;My first version went a bit like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { getRandomInt } from &apos;./utils.ts&apos;;

export const dice = {

	/*
	xdy

	x rolls of a y sided dice

	xdy+n
	xdy

	x rolls of a y side dice + n
	*/
	roll(styleInput: string): number {
		let bonus:number=0, total:number=0;
        let bonusStr:string=&apos;&apos;;
        let style = &apos;&apos;;

        if(styleInput.indexOf(&apos;+&apos;) &amp;gt; -1) {
			[style, bonusStr] = styleInput.split(&apos;+&apos;);
			bonus = Number(bonusStr);
		} else style = styleInput;
		
		const [rollsStr, sidedStr] = style.split(&apos;d&apos;);
		const rolls = Number(rollsStr);
		const sided = Number(sidedStr);

		//console.log(rolls, sided);
		for(let i=0;i&amp;lt;rolls;i++) {
			total += getRandomInt(1, sided);
		}

		total += Number(bonus);
		return total;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you&apos;ll notice it&apos;s a bit longer than the original version, and I don&apos;t mean more characters, I expected that, but some things took me a bit by surprise. Take this (original) line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let [rolls, sided] = style.split(&apos;d&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My original code just coerced &lt;code&gt;rolls&lt;/code&gt; into a number, which is bad, but I tried this and it didn&apos;t work:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let [rolls:number, sided: number] = style.split(&apos;d&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which is obvious - the result of the string split function is strings. What didn&apos;t occur to me at the time was to chain to &lt;code&gt;map&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let [rolls, sided] = s.split(&apos;d&apos;).map(Number);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Live and learn, right? I built some basic tests for that and was curious what else I could do. I turned to my AI agent (Claude in this case) and just asked what other &apos;dice format&apos; options were available, and was pleasantly surprised to discover quite a few. In particular I thought these made sense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep highest, lowest, using KH or KL. So for example, &lt;code&gt;3D6KH1&lt;/code&gt; means roll a six sided die three times, keep the highest one.&lt;/li&gt;
&lt;li&gt;Given a dice string input, what&apos;s the lowest value, and what&apos;s the highest value?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At this point, I decided to let AI help me write a bit of the code and handle updating the library to be a bit more flexible. The final result supports all of the above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { getRandomInt } from &apos;./utils.ts&apos;;

const VALID_ROLL_REGEX =
    /^(\d*)d(\d+)(?:(kh|kl|dh|dl)(\d+))?([+-]\d+)?$/i;

function validRoll(style: string): boolean {
    return VALID_ROLL_REGEX.test(style);
}

function parseRoll(styleInput: string) {
    if (!validRoll(styleInput)) {
        throw new Error(`Invalid dice roll style: ${styleInput}`);
    }

    const match = styleInput.match(VALID_ROLL_REGEX)!;

    return {
        rolls: match[1] ? Number(match[1]) : 1,
        sided: Number(match[2]),
        keepMode: match[3]?.toLowerCase(),
        keepCount: match[4] ? Number(match[4]) : 0,
        bonus: match[5] ? Number(match[5]) : 0,
    };
}

function applyKeepDrop(
    dice: number[],
    keepMode: string | undefined,
    keepCount: number,
): number[] {
    if (!keepMode || keepCount === 0) {
        return dice;
    }

    dice.sort((a, b) =&amp;gt; a - b);

    switch (keepMode) {
        case &apos;kh&apos;:
            return dice.slice(-keepCount);
        case &apos;kl&apos;:
            return dice.slice(0, keepCount);
        case &apos;dh&apos;:
            return dice.slice(0, -keepCount);
        case &apos;dl&apos;:
            return dice.slice(keepCount);
        default:
            return dice;
    }
}

export function roll(styleInput: string): number {
    const { rolls, sided, keepMode, keepCount, bonus } =
        parseRoll(styleInput);

    const dice: number[] = [];

    for (let i = 0; i &amp;lt; rolls; i++) {
        dice.push(getRandomInt(1, sided));
    }

    const total = applyKeepDrop(dice, keepMode, keepCount)
        .reduce((sum, die) =&amp;gt; sum + die, 0);

    return total + bonus;
}

export function rollWithAdvantage(styleInput: string): number {
    return Math.max(roll(styleInput), roll(styleInput));
}

export function rollWithDisadvantage(styleInput: string): number {
    return Math.min(roll(styleInput), roll(styleInput));
}

export function minRoll(styleInput: string): number {
    const { rolls, keepMode, keepCount, bonus } = parseRoll(styleInput);
    const dice = Array(rolls).fill(1);

    return applyKeepDrop(dice, keepMode, keepCount)
        .reduce((sum, die) =&amp;gt; sum + die, 0) + bonus;
}

export function maxRoll(styleInput: string): number {
    const { rolls, sided, keepMode, keepCount, bonus } =
        parseRoll(styleInput);
    const dice = Array(rolls).fill(sided);

    return applyKeepDrop(dice, keepMode, keepCount)
        .reduce((sum, die) =&amp;gt; sum + die, 0) + bonus;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I won&apos;t share the entire test file (don&apos;t worry, I&apos;ll link to the repo in a bit), but as I mentioned above, you can actually &amp;quot;hack&amp;quot; random and that&apos;s really freaking sweet. For example, here is a test for rolls where you drop the lowest value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;it(&apos;handles drop lowest&apos;, () =&amp;gt; {
    const spy = vi.spyOn(Math, &apos;random&apos;)
        .mockReturnValueOnce(0.1)  // first roll → low
        .mockReturnValueOnce(0.9);  // second roll → high

    const result = roll(&apos;2d6dl1&apos;);
    expect(result).toBe(6);

    spy.mockRestore();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looking at it just now I see the second mock isn&apos;t needed - I&apos;ll fix that (eventually, honest) - but you get the idea.&lt;/p&gt;
&lt;h2 id=&quot;the-bits%2C-and-nothing-but-the-bits...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-bits%2C-and-nothing-but-the-bits...&quot;&gt;The bits, and nothing but the bits...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So this isn&apos;t a &amp;quot;real&amp;quot; project, but I loved getting my feet wet in TypeScript and - tbh - just writing some darn code. The repo is here: https://github.com/cfjedimaster/typescript-stuff/tree/main/vite-one&lt;/p&gt;
&lt;p&gt;I&apos;ve already got an idea for a next project, one that will actually have a web presence, so I&apos;ll share that when I get the time.&lt;/p&gt;
&lt;p&gt;And this is where you come in. I&apos;d be willing to bet 99% of you are writing TypeScript daily and have plenty of comments - so let me know. Just be gentle. Please.&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                <category term="typescript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <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>
        
</feed>