<?xml version="1.0" encoding="UTF-8" standalone="no"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><feed xmlns="http://www.w3.org/2005/Atom">
    <title>ExcitingAds! Cold Fusion</title>
    <link href="https://www.raymondcamden.com/feed.xml" rel="self" type="application/atom+xml"/>
    <link href="https://www.raymondcamden.com/" rel="alternate" type="text/html"/>
    <subtitle>Cold Fusion!</subtitle>

    <updated>2026-10-02T22:01:50+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>

        
            <xhtml:meta content="noindex" name="robots" xmlns:xhtml="http://www.w3.org/1999/xhtml"/><entry>
                <id>https://www.raymondcamden.com/2026/10/02/a-not-so-simple-rss-app-in-typescript</id>
                <title>A Not-So-Simple RSS App in TypeScript</title>
                <updated>2026-10-02T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/10/02/a-not-so-simple-rss-app-in-typescript" rel="alternate" title="A Not-So-Simple RSS App in TypeScript" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;Ok, I lie, this demo is also pretty simple, but I wanted to break the pattern of the last few posts and at least pretend this example is a bit more complex. As I try to find excuses to write more TypeScript, I thought a great example of that would be a simple RSS aggregator. You hit the site, tell it the feeds you care about, and it renders a date sorted list of entries.&lt;/p&gt;
&lt;p&gt;Because RSS typically can't be parsed in client-side JavaScript (don't know why? ask me below!) so I'd have to make use of a serverless function to proxy the calls (and at the same time, handle turning the XML into JSON).&lt;/p&gt;
&lt;p&gt;Now, to be fair, I had built nearly the same thing back in February: &lt;a href="https://www.raymondcamden.com/2026/02/02/building-an-rss-aggregator-with-astro"&gt;Building an RSS Aggregator with Astro
&lt;/a&gt;. This let me crib a bit of the work already done.&lt;/p&gt;
&lt;p&gt;For this one, my stack was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vite for the build process&lt;/li&gt;
&lt;li&gt;Alpine on the front end&lt;/li&gt;
&lt;li&gt;TypeScript, as much as possible&lt;/li&gt;
&lt;li&gt;Hosting on Netlify to make the serverless function easier to do&lt;/li&gt;
&lt;li&gt;WebAwesome, which I've used in the previous ones, for the UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can test this yourself here: &lt;a href="https://ts-rss-vite.netlify.app/"&gt;https://ts-rss-vite.netlify.app/&lt;/a&gt;. The full source is here: &lt;a href="https://github.com/cfjedimaster/typescript-stuff/tree/main/rss-reader-1"&gt;https://github.com/cfjedimaster/typescript-stuff/tree/main/rss-reader-1&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I figure yall have seen a few of these posts already, so let me focus on sharing the interesting stuff.&lt;/p&gt;
&lt;h2 id="run-typescript-checking" tabindex="-1"&gt;&lt;a class="header-anchor" href="#run-typescript-checking"&gt;Run TypeScript Checking&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I mentioned in my &lt;a href="https://www.raymondcamden.com/2026/09/26/a-simple-weather-app-in-typescript"&gt;last post&lt;/a&gt; that when Vite would reload, it wasn't running the TypeScript checker. It only did this when a build was run. Enabling this checking while in dev mode took two steps.&lt;/p&gt;
&lt;p&gt;First, simply add &lt;code&gt;&amp;quot;strict&amp;quot;: true,&lt;/code&gt; to &lt;code&gt;tsconfig.json&lt;/code&gt;. Funny enough, in years past I can remember removing this when I was trying to get something working in a TypeScript app and I was too lazy to learn or figure it out.&lt;/p&gt;
&lt;p&gt;Second, I had to add &lt;a href="https://www.npmjs.com/package/vite-plugin-checker"&gt;vite-plugin-checker&lt;/a&gt; to the app and modify &lt;code&gt;vite.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;import { defineConfig } from 'vite';
import checker from 'vite-plugin-checker';

export default defineConfig({
  plugins: [checker({ typescript: true })],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file doesn't exist by default so it was a net new file for the app. As soon as I did that, if I screwed something up on purpose (ahem, I only make mistakes on purpose), I saw errors in my terminal when Vite was running.&lt;/p&gt;
&lt;h2 id="the-serverless-function" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-serverless-function"&gt;The Serverless Function&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For the backend, I used the &lt;code&gt;ntl&lt;/code&gt; CLI (this is Netlify's CLI tool) to scaffold a function, which helpfully includes a TypeScript option. For the most part, the code here is a simpler version of what I had done for the Astro site, specifically I didn't bother with BLOB caching. Anyway, here's that function:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;import Parser from 'rss-parser';
import type { Context } from '@netlify/functions';

type FeedItem = {
  title: string;
  link: string;
  pubDate: string;
  content: string;
  feedTitle: string;
};

export default async (request: Request, context: Context) =&amp;gt; {
  if (request.method !== 'POST') {
    return new Response('Method not allowed', {
      status: 405,
      headers: { Allow: 'POST' },
    })
  }

  try {
    const body: unknown = await request.json()
    const parser = new Parser();

    if (
      typeof body !== 'object' ||
      body === null ||
      !('feeds' in body) ||
      !Array.isArray(body.feeds)
    ) {
      return new Response('Request body must include a feeds array', {
        status: 400,
      })
    }
    console.log('Parsing feeds:', body.feeds);

    let reqs: Promise&amp;lt;any&amp;gt;[] = [];
    let items: FeedItem[] = [];

    for(const feed of body.feeds) {
      reqs.push(parser.parseURL(feed.url));
    }

    const results = await Promise.allSettled(reqs);
    for (const result of results) {
        if (result.status === 'fulfilled') {
            const feed = result.value;
            console.log(`Fetched feed: ${feed.title} with ${feed.items.length} items.`);
            let newItems:any[] = [];
            feed.items.forEach(item =&amp;gt; {
                /*
                will use content as a grab all for different fields
                for example, netlify had summary, not content
                */
                let content = item.contentSnippet || item.summary || item.content || '';
                newItems.push({
                    title: item.title,
                    link: item.link,
                    content: content,
                    pubDate: item.pubDate,
                    feedTitle: feed.title
                });
            });

            items.push(...newItems);

        } else {
            console.error('Error fetching/parsing feed:', result.reason);
        }
    }

    // now sort items by pubDate descending
    items.sort((a, b) =&amp;gt; new Date(b.pubDate) - new Date(a.pubDate));

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

  } catch(e) {
    console.log(e);
    return new Response('Request body must be valid JSON', {
      status: 400,
    })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is all pretty simple but I liked having the ability to create a custom type for my results.&lt;/p&gt;
&lt;p&gt;I did run into an error after I created the function - and it's one I've seen twice now. Something in what the Netlify CLI does breaks some dependencies and my app fails to run at all. I passed the error to Claude and what it said was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The most likely cause is that adding the Netlify function pulled in an older 
typescript that's now being resolved instead of your project's version. This 
usually happens when a netlify/functions package has its own package.json, 
or when netlify-cli or the functions bundler is running an older tsc from a 
different node_modules.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Its suggested fix, &lt;code&gt;npm i -D typescript@latest&lt;/code&gt;, was enough to correct it. I definitely won't forget this when it happens again.&lt;/p&gt;
&lt;h2 id="what's-next%3F" tabindex="-1"&gt;&lt;a class="header-anchor" href="#what's-next%3F"&gt;What's Next?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This has been so fun, at least for me. I've got my next two ideas ready! (Well, in my head at least. ;)&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/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" title="A Simple Weather App in TypeScript" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;Happy Sunday, my fellow nerds. If you've been keeping up with my posts (all 4 of you - thank you!) then you know I'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'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="https://weather-app-ts-vite-alpine.netlify.app/"&gt;https://weather-app-ts-vite-alpine.netlify.app/&lt;/a&gt;. The complete source may be found here: &lt;a href="https://github.com/cfjedimaster/typescript-stuff/tree/main/weather-demo"&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="the-one-thing" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-one-thing"&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's something TypeScript provided since day one, but it'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'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="the-architecture" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-architecture"&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="https://www.raymondcamden.com/2026/09/22/a-simple-alpinejs-template-with-vite-and-typescript"&gt;last blog post&lt;/a&gt; discussed how to do this.&lt;/li&gt;
&lt;li&gt;The main page handles displaying the 'weather cards' 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="https://www.geocod.io/"&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="https://pirateweather.net/"&gt;Pirate Weather&lt;/a&gt; API to get the forecast.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-ui" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-ui"&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="https://webawesome.com/"&gt;Web Awesome&lt;/a&gt; primarily for the Card element. I'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="language-ts"&gt;import '@awesome.me/webawesome/dist/styles/webawesome.css';
import '@awesome.me/webawesome/dist/components/card/card.js';
import '@awesome.me/webawesome/dist/components/input/input.js';
import '@awesome.me/webawesome/dist/components/button/button.js';
&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="language-html"&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'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'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="language-html"&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="the-three-%22services%22" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-three-%22services%22"&gt;The Three &amp;quot;Services&amp;quot;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I'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'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'd have to mark the functions &lt;code&gt;async&lt;/code&gt; of course:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;import type { SavedLocation } from './types';

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

// 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's ok for TS to consider the value as of type SavedLocation
function isSavedLocation(value: unknown): value is SavedLocation {
  if (typeof value !== 'object' || value === null) return false;
  const v = value as Record&amp;lt;string, unknown&amp;gt;;
  return (
    typeof v.name === 'string' &amp;amp;&amp;amp;
    typeof v.longitude === 'number' &amp;amp;&amp;amp;
    typeof v.latitude === 'number'
  );
}

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 'stored location' is in terms of my application. From that file, here is the definition:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&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'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's quicker, but weather data is the kind of thing that gets stale pretty quickly.&lt;/p&gt;
&lt;p&gt;Here's that file:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;import type { WeatherData } from './types';

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's the type:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;export type WeatherData = {
  summary: string;
  temperature: number;
  low: number;
  high: number;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, here's the geocoding wrapper:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;import type { GeoCodedLocation } from './types';

// Only the fields we use from Geocodio'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('No results');
    }

    return { lat: res.lat, lng: res.lng };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You'll notice I do the HTTP call slightly different here. I've got an inline type here that defines what is being used from Geocodio. I'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="language-ts"&gt;export type GeoCodedLocation = {
  lat: number;
  lng: number;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I honestly don't get the point of the inline type as well. I was having AI help me a bit, so it's on me to nail down if this makes sense in the context of the file. I'll be returning to that soon.&lt;/p&gt;
&lt;h2 id="the-core-app" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-core-app"&gt;The Core App&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now let's put it together with my core file - which is mainly Alpine.js specific:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ts"&gt;import Alpine from 'alpinejs';

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

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

Alpine.data('app', () =&amp;gt; ({
  locations: [] as SavedLocation[],
  newLocation: '',
  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 = '';
    // in theory it is wasteful to hydrate ALL of them, but it'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'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't care for that. How would you rename things? (And I'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'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've seen that in projects and it's annoyed the heck out of me, so I get why Vite's scaffold doesn't include it, but for my next project, I'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="your-turn!" tabindex="-1"&gt;&lt;a class="header-anchor" href="#your-turn!"&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'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="https://unsplash.com/@patrickian4?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText"&gt;Patrick Fore&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/black-corona-typewriter-on-brown-wood-planks-0gkw_9fy0eQ?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText"&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" title="A Simple Alpine.js Template with Vite and TypeScript" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;This will be a quick one - but as I think more about TypeScript and how I'd like to learn (and play, and build silly demos), I naturally thought it may make sense to look at how I'd use &lt;a href="https://alpinejs.dev/"&gt;Alpine.js&lt;/a&gt; with that stack. Alpine's been my go to library for web apps that reach the level of complexity where I'd like some help with DOM manipulation and such. I don't &lt;em&gt;always&lt;/em&gt; use it, because (imho) the default stack should be as vanilla as possible (obviously I'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't necessarily a &amp;quot;template&amp;quot; as it'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="step-one---the-scaffold" tabindex="-1"&gt;&lt;a class="header-anchor" href="#step-one---the-scaffold"&gt;Step One - the Scaffold&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In my &lt;a href="https://www.raymondcamden.com/2026/09/18/its-only-a-decade-late-ill-learn-typescript"&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's template was a bit more minimal.&lt;/p&gt;
&lt;h2 id="step-two---adding-alpine" tabindex="-1"&gt;&lt;a class="header-anchor" href="#step-two---adding-alpine"&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's my package.json:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&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="step-three---the-html" tabindex="-1"&gt;&lt;a class="header-anchor" href="#step-three---the-html"&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="language-html"&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'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="https://simplecss.org/"&gt;Simple.css&lt;/a&gt; to just to make it look prettier. That's absolutely not necessary. The Vite demo actually imports CSS in their main TypeScript file (you can see that on Stackblitz &lt;a href="https://stackblitz.com/edit/vitejs-vite-hpgbiipd?file=src%2Fmain.ts&amp;amp;terminal=dev"&gt;here&lt;/a&gt;) and I'm not sure how I feel about that. I know it minimizes the code, but it feels really weird to me. Anyway, here's my HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&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('Purr...')&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="step-four---enter-the-typescript" tabindex="-1"&gt;&lt;a class="header-anchor" href="#step-four---enter-the-typescript"&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="language-js"&gt;import Alpine from 'alpinejs';

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

Alpine.data('app', () =&amp;gt; ({
  message:'Hello from Alpine!',
  cats: [] as Cat[],
  init() {
    this.cats.push({ name: 'Whiskers', breed: 'Siamese', gender: 'male' });
    this.cats.push({ name: 'Fluffy', breed: 'Persian', gender: 'female' });
  },
  meow(message:string = 'Meow!') {
    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'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'm trying to learn TypeScript as well, but I kept it pretty short here. I'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'd expect - it knew the right parts of a cat and correctly flagged an error if I tried to include something that wasn't defined in the type.&lt;/p&gt;
&lt;p&gt;As a reminder, this is all done in the editor - it wouldn't be a &amp;quot;real&amp;quot; error in production - but the idea here - and the benefit of TypeScript - is that I'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="there-is-no-step-five" tabindex="-1"&gt;&lt;a class="header-anchor" href="#there-is-no-step-five"&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="https://github.com/cfjedimaster/typescript-stuff/tree/main/alpine-test-1"&gt;https://github.com/cfjedimaster/typescript-stuff/tree/main/alpine-test-1&lt;/a&gt;. I know this was pretty short, but I'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" title="Links For You (9/20/26)" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;Happy &amp;quot;Football is Back&amp;quot; season to those of you who celebrate. I'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'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="https://static.raymondcamden.com/images/2026/09/st.jpg" loading="lazy" alt="Stormtroopers" class="imgborder imgcenter"&gt;
&lt;/p&gt;
&lt;h2 id="rss-lookup" tabindex="-1"&gt;&lt;a class="header-anchor" href="#rss-lookup"&gt;RSS Lookup&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First up for the links today is &lt;a href="https://www.rsslookup.com/"&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's the meta tag used to signify a site's feed:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&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="interfaces%2C-a-collection-by-ron-domingue" tabindex="-1"&gt;&lt;a class="header-anchor" href="#interfaces%2C-a-collection-by-ron-domingue"&gt;Interfaces, a collection by Ron Domingue&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ron is someone I'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="https://interfaces.rondomingue.com/"&gt;Interfaces&lt;/a&gt; is a collection of various UI interfaces for different types of data. I'd be hard pressed to pick a favorite, but &lt;a href="https://interfaces.rondomingue.com/landfall.html"&gt;Landfall&lt;/a&gt; is a stunning visualization of hurricane tracks and their landfall.&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://static.raymondcamden.com/images/2026/09/landfall.png" loading="lazy" alt="Landfall" class="imgborder imgcenter"&gt;
&lt;/p&gt;
&lt;h2 id="remember-imagemaps%3F" tabindex="-1"&gt;&lt;a class="header-anchor" href="#remember-imagemaps%3F"&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'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="https://danq.me/2026/07/10/rescuing-7234-gifs/?utm_source=tldrdev"&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="just-for-fun" tabindex="-1"&gt;&lt;a class="header-anchor" href="#just-for-fun"&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's marked as comedy, it's a pretty deep 30-ish minute short staring Jane Kaczmarek. It's definitely worth your time!&lt;/p&gt;
&lt;lite-youtube videoid="Ec6yq3isEjg" style="background-image: url('https://i.ytimg.com/vi/Ec6yq3isEjg/hqdefault.jpg');"&gt;
  &lt;a href="https://youtube.com/watch?v=Ec6yq3isEjg" class="lty-playbtn" title="Play Video"&gt;
    &lt;span class="lyt-visually-hidden"&gt;Play Video&lt;/span&gt;
  &lt;/a&gt;
&lt;/lite-youtube&gt;
&lt;script defer src="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js"&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css" integrity="sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /&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's only a decade late, I'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" title="It's only a decade late, I'll learn TypeScript!" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;I've been a bit worried lately. Of course, my friends will tell you that's my norm. But more specifically, I'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've also had a bit of writer'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've never &lt;em&gt;really&lt;/em&gt; dug into &lt;a href="https://www.typescriptlang.org/"&gt;TypeScript&lt;/a&gt;. Sure, I've used it, many times, in the fourteen or so years it's been out, but many times I kinda guess at what I'm doing and it was never my default for projects. Partly that'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'd do some digging. Here's a few initial thoughts and experiments. Oh, and I still used AI a bit and I'll explain exactly how in the post below.&lt;/p&gt;
&lt;h2 id="typescript-via-vite" tabindex="-1"&gt;&lt;a class="header-anchor" href="#typescript-via-vite"&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="https://vite.dev/"&gt;Vite&lt;/a&gt; instead as it'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's how quick it works.&lt;/p&gt;
&lt;p&gt;I also appreciated the fact that Vite's CLI supports a &lt;a href="https://stackblitz.com/edit/vitejs-vite-rnustopr?file=index.html&amp;amp;terminal=dev"&gt;&lt;code&gt;vanilla-ts&lt;/code&gt;&lt;/a&gt; scaffold that is really lightweight. On the other hand, I'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="language-html"&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="language-js"&gt;import './style.css'
import heroImg from './assets/hero.png'
import typescriptLogo from './assets/typescript.svg'
import viteLogo from './assets/vite.svg'
import { setupCounter } from './counter.ts'

document.querySelector&amp;lt;HTMLDivElement&amp;gt;('#app')!.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;('#counter')!)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'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'm being picky, and the actual code spit out is pretty minimal. (I could go on for ages about what's &amp;quot;appropriate&amp;quot; for scaffolded projects and such. Give me a soap box and I'll tell you more.)&lt;/p&gt;
&lt;p&gt;So yeah, that'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="language-js"&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="language-js"&gt;document.querySelector&amp;lt;HTMLDivElement&amp;gt;('#app')!.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't know whether it's a div, an input, an SVG circle, etc. That 
matters because different elements have different properties: Element doesn't 
have .value, for example, but HTMLInputElement does.

Writing querySelector&amp;lt;HTMLDivElement&amp;gt;('#app') 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't verify 
it. If #app were actually a &amp;lt;span&amp;gt;, the code would still compile and run; you'd 
just have a lying type.

Fun fact: in this particular line the generic is technically unnecessary, since 
.innerHTML exists on Element anyway. It'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've really liked about AI in the past. I feel like my question, a particular syntax being used, wouldn'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="actually%2C-let's-not-build-for-the-web..." tabindex="-1"&gt;&lt;a class="header-anchor" href="#actually%2C-let's-not-build-for-the-web..."&gt;Actually, let'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'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="https://vitest.dev/"&gt;vitest&lt;/a&gt;, a &amp;quot;Vite-native&amp;quot; testing framework. I actually don'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't really required. That being said, I've written them, I'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="roll-the-dice..." tabindex="-1"&gt;&lt;a class="header-anchor" href="#roll-the-dice..."&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's the first entry: &lt;a href="https://www.raymondcamden.com/2020/07/17/testing-vuejs-application-files-that-arent-components"&gt;&amp;quot;Testing Vue.js Application Files That Aren't Components&amp;quot;&lt;/a&gt;. I never finished that project, but I don'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="language-js"&gt;export const dice = {

	roll(style) {
		let bonus=0, total=0;
		if(style.indexOf('+') &amp;gt; -1) {
			[style, bonus] = style.split('+');
		} 
		
		let [rolls, sided] = style.split('d');
		
		//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 'util' one for the random number selection.&lt;/p&gt;
&lt;p&gt;I'll share the utility function first as it was the simplest change:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&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="language-js"&gt;import { describe, it, expect } from 'vitest'
import { getRandomInt } from './utils'

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

  it('returns an integer', () =&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'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="language-js"&gt;import { getRandomInt } from './utils.ts';

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='';
        let style = '';

        if(styleInput.indexOf('+') &amp;gt; -1) {
			[style, bonusStr] = styleInput.split('+');
			bonus = Number(bonusStr);
		} else style = styleInput;
		
		const [rollsStr, sidedStr] = style.split('d');
		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'll notice it's a bit longer than the original version, and I don'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="language-js"&gt;let [rolls, sided] = style.split('d');
&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't work:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-js"&gt;let [rolls:number, sided: number] = style.split('d');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which is obvious - the result of the string split function is strings. What didn'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="language-js"&gt;let [rolls, sided] = s.split('d').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 'dice format' 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's the lowest value, and what'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="language-js"&gt;import { getRandomInt } from './utils.ts';

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 'kh':
            return dice.slice(-keepCount);
        case 'kl':
            return dice.slice(0, keepCount);
        case 'dh':
            return dice.slice(0, -keepCount);
        case 'dl':
            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't share the entire test file (don't worry, I'll link to the repo in a bit), but as I mentioned above, you can actually &amp;quot;hack&amp;quot; random and that'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="language-js"&gt;it('handles drop lowest', () =&amp;gt; {
    const spy = vi.spyOn(Math, 'random')
        .mockReturnValueOnce(0.1)  // first roll → low
        .mockReturnValueOnce(0.9);  // second roll → high

    const result = roll('2d6dl1');
    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't needed - I'll fix that (eventually, honest) - but you get the idea.&lt;/p&gt;
&lt;h2 id="the-bits%2C-and-nothing-but-the-bits..." tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-bits%2C-and-nothing-but-the-bits..."&gt;The bits, and nothing but the bits...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So this isn'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've already got an idea for a next project, one that will actually have a web presence, so I'll share that when I get the time.&lt;/p&gt;
&lt;p&gt;And this is where you come in. I'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" title="Building a Document Q&amp;A System with On-Device AI" type="text/html"/>
                <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's &lt;a href="https://developer.chrome.com/docs/ai/built-in"&gt;on-device AI&lt;/a&gt; technology. I've been blogging about this particular set of APIs for a few years now and I've given talks on it at numerous conferences. While these APIs are still evolving, many of them (and the ones I'm using today in particular) are now fully GA for Chrome. For this demo, I put together a few things I'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="the-stack" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-stack"&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'm making use of the &lt;strong&gt;incredibly excellent&lt;/strong&gt; &lt;a href="https://officeparser.harshankur.com/"&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's &lt;a href="https://developer.chrome.com/docs/ai/summarizer-api"&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's &lt;a href="https://developer.chrome.com/docs/ai/prompt-api"&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="the-app" tabindex="-1"&gt;&lt;a class="header-anchor" href="#the-app"&gt;The App&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, I'm obviously going to link to it, and the source, too, but I thought I'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="https://static.raymondcamden.com/images/2026/09/dqa1.png" loading="lazy" alt="Initial view" class="imgborder imgcenter"&gt;
&lt;/p&gt;
&lt;p&gt;I selected my &lt;a href="/resume"&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's absolutely &lt;em&gt;not&lt;/em&gt; insignificant. But it'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's not something a user has to download again and again.&lt;/p&gt;
&lt;p&gt;Here's the summary that was displayed:&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://static.raymondcamden.com/images/2026/09/dqa2.png" loading="lazy" alt="Summary" class="imgborder imgcenter"&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'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="https://static.raymondcamden.com/images/2026/09/dqa3.png" loading="lazy" alt="Q and A example" class="imgborder imgcenter"&gt;
&lt;/p&gt;
&lt;p&gt;Honestly that'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="https://document-question-answer.netlify.app/"&gt;https://document-question-answer.netlify.app/&lt;/a&gt;. You can peruse the source up here: &lt;a href="https://github.com/cfjedimaster/doc_qa"&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" title="My Family Allowance - a Simple Allowance Tracking Site" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;As you can probably tell, I've been on a bit of a builder kick the last few weeks. Today's &amp;quot;release&amp;quot; (it'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'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="https://static.raymondcamden.com/images/2026/09/over.webp" loading="lazy" alt="Overengineering everywhere" class="imgborder imgcenter"&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's it! You can check it out here: &lt;a href="https://www.myallowance.family/"&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="https://github.com/cfjedimaster/myfamilyallowance"&gt;https://github.com/cfjedimaster/myfamilyallowance&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@igalness?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText"&gt;Igal Ness&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/person-holding-fan-of-100-us-dollar-bill-9wY2ofzQ9Us?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText"&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" title="Weather Nerd? Check out My OSX Radar Widget" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;I know I'm not alone in being a bit of a weather nerd. Along with four or five weather apps on my phone, I've got two physical devices in my office that show the weather and a weather widget on my Mac that shows the day's forecast. That'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="https://static.raymondcamden.com/images/2026/09/radar1.jpg" loading="lazy" alt="Screenshot of radar widget" class="imgborder imgcenter"&gt;
&lt;/p&gt;
&lt;p&gt;Ok, to be fair, I used Cursor to build it - but it really scratched an itch I'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've set up a GitHub repo here: &lt;a href="https://github.com/cfjedimaster/precipradar"&gt;https://github.com/cfjedimaster/precipradar&lt;/a&gt;. I'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="https://www.raymondcamden.com/2026/08/26/like-lego-got-a-mac-get-my-screensaver"&gt;LEGO screensaver&lt;/a&gt;, you'll need to run a CLI command to authorize the app, but that'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" title="Like LEGO? Got a Mac? Get My Screensaver" type="text/html"/>
                <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'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="https://rebrickable.com/downloads/"&gt;Rebrickable'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's an example of how it looks:&lt;/p&gt;
&lt;p&gt;
&lt;img src="https://static.raymondcamden.com/images/2026/08/lego.png" loading="lazy" alt="LEGO screensaver example" class="imgborder imgcenter"&gt;
&lt;/p&gt;
&lt;p&gt;Alright, so before I link you to the GitHub repo, a quick warning. I'm not paying Apple for a developer's license which means if you get the build, you'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="https://github.com/cfjedimaster/lego-screensaver"&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" title="Links For You (8/23/26)" type="text/html"/>
                <content type="html">
				
                        &lt;p&gt;Greetings, programs. Once again, I'm a bit behind on these posts, but I just got back from two weeks of travel and I'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="https://static.raymondcamden.com/images/2026/08/nope.png" loading="lazy" alt="Flight track" class="imgborder imgcenter"&gt;
&lt;/p&gt;
&lt;p&gt;For this, and other reasons, I'm damn happy I've got a therapy session tomorrow. So, let's get to the links.&lt;/p&gt;
&lt;h2 id="and-the-winner-for-best-project-name-goes-to..." tabindex="-1"&gt;&lt;a class="header-anchor" href="#and-the-winner-for-best-project-name-goes-to..."&gt;And the winner for best project name goes to...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I've only given this a cursory glance, but the name itself was enough to make me giggle: &lt;a href="https://seanhelvey.com/mullet-stack/guide/"&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'm no React fan, and when I was playing with Python web frameworks last year (this year? time is weird) I didn'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="change-to-clipboard-reading-in-javascript" tabindex="-1"&gt;&lt;a class="header-anchor" href="#change-to-clipboard-reading-in-javascript"&gt;Change to Clipboard Reading in JavaScript&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you'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="https://developer.chrome.com/blog/selective-format-read?hl=en"&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'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="https://blog.master.dev/handling-paste-events-in-javascript/"&gt;Handling Paste Events in JavaScript&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="a-new-option-for-generating-rss" tabindex="-1"&gt;&lt;a class="header-anchor" href="#a-new-option-for-generating-rss"&gt;A New Option for Generating RSS&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I'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'll notice if you look for libraries like this is that - typically - they're pretty old. To be fair, RSS is a spec and hasn't changed in a while. (A quick Google check seems to confirm the last change was 2009.) The &lt;a href="https://github.com/jpmonette/feed"&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'd probably start here.&lt;/p&gt;
&lt;h2 id="just-for-fun" tabindex="-1"&gt;&lt;a class="header-anchor" href="#just-for-fun"&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="2WN0T-Ee3q4" style="background-image: url('https://i.ytimg.com/vi/2WN0T-Ee3q4/hqdefault.jpg');"&gt;
  &lt;a href="https://youtube.com/watch?v=2WN0T-Ee3q4" class="lty-playbtn" title="Play Video"&gt;
    &lt;span class="lyt-visually-hidden"&gt;Play Video&lt;/span&gt;
  &lt;/a&gt;
&lt;/lite-youtube&gt;
&lt;script defer src="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js"&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css" integrity="sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /&gt;
&lt;p&gt;

                        
                
				</content>

                
                <category term="links4you"/>
                
                
                <category term="misc"/>
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
</feed>