<?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-10-21T22:41:03+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/10/21/adding-a-pdf-display-to-eleventy</id>
                <title>Adding a PDF Display to Eleventy</title>
                <updated>2026-10-21T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/10/21/adding-a-pdf-display-to-eleventy" rel="alternate" type="text/html" title="Adding a PDF Display to Eleventy"/>
                <content type="html">
				
                        &lt;p&gt;This will be a short one. I&apos;ve talked about embedding PDFs on web pages for probably close to ten years now. Blame Adobe. Blame me being a nerd and finding this stuff interesting. Blame whoever. While I&apos;ve blogged about the topic, I&apos;ve also actually done that on my site here, in a variety of different ways.&lt;/p&gt;
&lt;p&gt;A while back I realized I wasn&apos;t being consistent with how I did... &amp;quot;stuff&amp;quot; here, like call outs, Youtube links, etc. I started a &lt;a href=&quot;https://github.com/cfjedimaster/raymondcamden2023/blob/main/guide.md&quot;&gt;guide doc&lt;/a&gt; so I&apos;d have a quick way to remember what I did last time, and be more consistent.&lt;/p&gt;
&lt;p&gt;One item on there I had left as &amp;quot;figure it out later&amp;quot; was proper PDF embedding. This week I finally nailed that down. &lt;a href=&quot;https://www.embedpdf.com/&quot;&gt;EmbedPDF&lt;/a&gt; is a library I&apos;ve mentioned here before so I leaned on it, and went with an incredibly simple snippet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div id=&amp;quot;pdf-dis&amp;quot; style=&amp;quot;width: 100%; height: 400px; margin-bottom: 30px&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
 
&amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;
import EmbedPDF, { ZoomMode } from &apos;https://cdn.jsdelivr.net/npm/@embedpdf/snippet@2/dist/embedpdf.js&apos;;
 
EmbedPDF.init({
	type: &apos;container&apos;,
	target: document.getElementById(&apos;pdf-dis&apos;),
	src: &apos;https:// ....&apos;,
	theme: { preference: &apos;system&apos; },
	disabledCategories: [&apos;annotation&apos;],
	zoom: {
		defaultZoomLevel: ZoomMode.FitWidth
	}
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This does a few things that make sense on my blog, and may not for you. I use the &lt;code&gt;ZoomMode&lt;/code&gt; feature to make the PDF document automatically be as wide as the container. Honestly, I don&apos;t know why this isn&apos;t the default, but given that PDFs can specify their own default view, the library may just usually let it decide. The rest of the code is pretty much vanilla from the docs.&lt;/p&gt;
&lt;p&gt;So that&apos;s the snippet. To make it a shortcode in Eleventy, I added this to my page which defines all the shortcodes for my blog:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const embedPDF = (id, url, height=&amp;quot;500px&amp;quot;) =&amp;gt; {
  return `
&amp;lt;div id=&amp;quot;${id}&amp;quot; style=&amp;quot;width: 100%; height: ${height}; margin-bottom: 30px&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
 
&amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;
import EmbedPDF, { ZoomMode } from &apos;https://cdn.jsdelivr.net/npm/@embedpdf/snippet@2/dist/embedpdf.js&apos;;
 
EmbedPDF.init({
	type: &apos;container&apos;,
	target: document.getElementById(&apos;${id}&apos;),
	src: &apos;${url}&apos;,
	theme: { preference: &apos;system&apos; },
	disabledCategories: [&apos;annotation&apos;],
	zoom: {
		defaultZoomLevel: ZoomMode.FitWidth
	}
});
&amp;lt;/script&amp;gt;
  `;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gets loaded by my Eleventy config script and inside a post, I can:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% embedPDF &amp;quot;pdf-viewer-1&amp;quot;, &amp;quot;https://static.raymondcamden.com/assets/document.pdf&amp;quot; %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which gives you:&lt;/p&gt;
&lt;div id=&quot;pdf-viewer-1&quot; style=&quot;width: 100%; height: 500px; margin-bottom: 30px&quot;&gt;&lt;/div&gt;
&lt;script type=&quot;module&quot;&gt;
import EmbedPDF, { ZoomMode } from &apos;https://cdn.jsdelivr.net/npm/@embedpdf/snippet@2/dist/embedpdf.js&apos;;
 
EmbedPDF.init({
	type: &apos;container&apos;,
	target: document.getElementById(&apos;pdf-viewer-1&apos;),
	src: &apos;https://static.raymondcamden.com/assets/document.pdf&apos;,
	theme: { preference: &apos;system&apos; },
	disabledCategories: [&apos;annotation&apos;],
	zoom: {
		defaultZoomLevel: ZoomMode.FitWidth
	}
});
&lt;/script&gt;
&lt;p&gt;I found this useful, and I hope you do too.&lt;/p&gt;

                        
                
				</content>

                
                <category term="eleventy" />
                
                
                <category term="jamstack" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/10/19/links-for-you-101926</id>
                <title>Links For You (10/19/26)</title>
                <updated>2026-10-19T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/10/19/links-for-you-101926" rel="alternate" type="text/html" title="Links For You (10/19/26)"/>
                <content type="html">
				
                        &lt;p&gt;Wowza - I&apos;m like nearly a month late on this. This is where I&apos;d try to come up with a good excuse, but I think if I just randomly point around at things and said... &amp;quot;life&amp;quot; most of you would completely understand where I&apos;m coming from. It&apos;s been a difficult few months for myself, and for nearly every person I know. This week I&apos;m going to try focus on the positive and the things that make me smile, and I hope all my lovely readers can do the same!&lt;/p&gt;
&lt;h2 id=&quot;make-music%2C-not-war&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#make-music%2C-not-war&quot;&gt;Make Music, Not War&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First up is the appropriately named &lt;a href=&quot;https://hyperblam.how/&quot;&gt;Hyperblam&lt;/a&gt;, a music making tool that lets you use a declarative language to create sick beats as the kids say. What does that look like? Here&apos;s one example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!-- bus! (master signal chain) --&amp;gt;
&amp;lt;chain-blam id=&amp;quot;bus&amp;quot;&amp;gt;
&amp;lt;gain-blam id=&amp;quot;bits&amp;quot; gain=&amp;quot;0.25&amp;quot; bypass&amp;gt;
    &amp;lt;lfo-blam type=&amp;quot;square&amp;quot; beats=&amp;quot;0.001&amp;quot; prop=&amp;quot;gain&amp;quot; gain=&amp;quot;0.25&amp;quot;&amp;gt;
    &amp;lt;blam-blam from=&amp;quot;#kick&amp;quot; prop=&amp;quot;beats&amp;quot; value=&amp;quot;0.001~0.003&amp;quot; chance=&amp;quot;0.3&amp;quot;&amp;gt;&amp;lt;/blam-blam&amp;gt;
    &amp;lt;/lfo-blam&amp;gt;
&amp;lt;/gain-blam&amp;gt;
&amp;lt;phaser-blam id=&amp;quot;swoosh&amp;quot; center=&amp;quot;6000&amp;quot; depth=&amp;quot;4000&amp;quot; q=&amp;quot;0.5&amp;quot; bypass&amp;gt;&amp;lt;/phaser-blam&amp;gt;
&amp;lt;analyser-blam id=&amp;quot;analyser&amp;quot; smoothing=&amp;quot;0.6&amp;quot;&amp;gt;&amp;lt;/analyser-blam&amp;gt;
&amp;lt;/chain-blam&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you can play the song this is a part of here:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;-2&quot; data-height=&quot;500&quot; data-pen-title=&quot;HYPERBLAM (HELLO HYPERBLAM)&quot; data-preview=&quot;true&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;RNKgMEe&quot; data-user=&quot;heydon&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/heydon/pen/RNKgMEe&quot;&gt;
  HYPERBLAM (HELLO HYPERBLAM)&lt;/a&gt; by Heydon (&lt;a href=&quot;https://codepen.io/heydon&quot;&gt;@heydon&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;this-ui-library-will-make-you-jelly...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#this-ui-library-will-make-you-jelly...&quot;&gt;This UI library will make you jelly...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Jelly as in jealous - and again - that&apos;s what the kids say, right? &lt;a href=&quot;https://jelly-ui.com/&quot;&gt;Jelly UI&lt;/a&gt; is a UI web component library for &amp;quot;jelly&amp;quot; type interfaces. Think fat, but soft, bright colors and such. I love web components for libraries like this, and this one is on my list to try soon in a demo.&lt;/p&gt;
&lt;p&gt;I copied a few samples from their docs and threw it in a CodePen:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;-2&quot; data-height=&quot;500&quot; data-pen-title=&quot;Jelly UI&quot; data-preview=&quot;true&quot; data-version=&quot;2&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;azJGPMW&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/01a15441-9c56-7fc4-beda-0c7c391a2e01&quot;&gt;
  Jelly UI&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;your-new-rss-feed-reader&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#your-new-rss-feed-reader&quot;&gt;Your new RSS Feed Reader&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Last but definitely not least is an incredible new product from my buddy &lt;a href=&quot;https://kisdigital.com/&quot;&gt;Robert&lt;/a&gt; - &lt;a href=&quot;https://feedhouse.app/&quot;&gt;Feedhouse&lt;/a&gt; is an RSS reader, but with so many features it goes far beyond just RSS parsing. It has great social aspects (you can mark up and comment on items in your feed, you can share collections of feeds), it supports sites without RSS feeds, it lets you sign up for newsletters with a custom address and automatically parse the items into your feed, and so much more. If you want to learn more about how it&apos;s built, Robert wrote up a great post on it: &lt;a href=&quot;https://kisdigital.com/posts/2026/10/everything-feedhouse-does-from-the-boring-parts-up&quot;&gt;Everything Feedhouse Does, From the Boring Parts Up&lt;/a&gt;.&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;My readers know I&apos;m a huge fan of covers and imagine my surprise when I discovered this one by School of Seven Bells... enjoy!&lt;/p&gt;
&lt;lite-youtube videoid=&quot;bQlVb4ffhCs&quot; style=&quot;background-image: url(&apos;https://i.ytimg.com/vi/bQlVb4ffhCs/hqdefault.jpg&apos;);&quot;&gt;
  &lt;a href=&quot;https://youtube.com/watch?v=bQlVb4ffhCs&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/10/17/generating-pdfs-from-word-templates-with-officeparser</id>
                <title>Generating PDFs from Word Templates with officeParser</title>
                <updated>2026-10-17T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/10/17/generating-pdfs-from-word-templates-with-officeparser" rel="alternate" type="text/html" title="Generating PDFs from Word Templates with officeParser"/>
                <content type="html">
				
                        &lt;p&gt;I&apos;ve been singing the praises of the &lt;a href=&quot;https://officeparser.harshankur.com/&quot;&gt;officeParser&lt;/a&gt; library for a while. It&apos;s an open source JavaScript library that can parse, extract, and convert between PDFs and Office documents, as well as OpenOffice, ePub and more. My demos have all focused on client-side examples but obviously the library would work great in a server side workflow as well. I haven&apos;t worked at Adobe for years now, but I still get strangely excited working with documents. I&apos;ll just add that to the long list of reasons I&apos;m a big nerd.&lt;/p&gt;
&lt;p&gt;A few weeks ago, I reached out to &lt;a href=&quot;https://harshankur.com/&quot;&gt;Harsh Ankur&lt;/a&gt; about an idea inspired by Adobe&apos;s &lt;a href=&quot;https://developer.adobe.com/document-services/docs/overview/document-generation-api/&quot;&gt;Document Generation API&lt;/a&gt;. This was probably my favorite API in their suite of PDF services. You take a Word document, put in tokens, and call the API with your data. The API would swap out the tokens with real data and give you a PDF in return. (Or Word again if you wanted that.) I suggested this to Harsh and then - voila - he added it!&lt;/p&gt;
&lt;p&gt;At this time, the token system is simply a replacement system. You add &lt;code&gt;My name is &lt;/code&gt; in your Word document, and if you pass &lt;code&gt;{ &amp;quot;name&amp;quot;:&amp;quot;Juniper Montage&amp;quot;}&lt;/code&gt; via the library&apos;s APIs you&apos;ll get &lt;code&gt;My name is Juniper Montage&lt;/code&gt; on the way out.&lt;/p&gt;
&lt;p&gt;This new support is a core API, &lt;a href=&quot;https://officeparser.harshankur.com/#spec/template&quot;&gt;OfficeTemplate&lt;/a&gt;, and supports both replacing values from a regular JavaScript object or returning an array of results from a JavaScript array of objects. (Which, by the way, is something I was trying to get Adobe to add to their APIs. One of their competitors, &lt;a href=&quot;https://www.docugenerate.com/&quot;&gt;DocuGenerate&lt;/a&gt;, has had this for a while.)&lt;/p&gt;
&lt;p&gt;Here&apos;s how simple it is to use. First, create and save a Word document. Formatting doesn&apos;t matter - make it as pretty or simple as you would like. Here&apos;s an example with a few tokens:&lt;/p&gt;
&lt;div id=&quot;pdf-viewer-1&quot; style=&quot;width: 100%; height: 500px; margin-bottom: 30px&quot;&gt;&lt;/div&gt;
&lt;script type=&quot;module&quot;&gt;
import EmbedPDF, { ZoomMode } from &apos;https://cdn.jsdelivr.net/npm/@embedpdf/snippet@2/dist/embedpdf.js&apos;;
 
EmbedPDF.init({
	type: &apos;container&apos;,
	target: document.getElementById(&apos;pdf-viewer-1&apos;),
	src: &apos;https://static.raymondcamden.com/assets/document.pdf&apos;,
	theme: { preference: &apos;system&apos; },
	disabledCategories: [&apos;annotation&apos;],
	zoom: {
		defaultZoomLevel: ZoomMode.FitWidth
	}
});
&lt;/script&gt;
&lt;p&gt;Imagine this is stored as &lt;code&gt;customerReport.docx&lt;/code&gt;. To turn this into a PDF, you would first import the relevant libraries (in the sample below I&apos;m using an npm installed version, not CDN):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { OfficeConverter, OfficeTemplate } from &apos;officeparser/browser-native-pdf&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then - grab the Word document as binary data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const response = await fetch(&apos;/assets/customerReport.docx&apos;)
const template = new Uint8Array(await response.arrayBuffer())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Get some sample data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const data = {
  customerName: &apos;Northwind &amp;amp; Co&apos;,
  date: &apos;2026-10-16&apos;,
  productsTotal: 7,
  reportYear: 2026,
  profit: 2500,
  losses: 125,
  recommendation: &apos;Continue growing. Duh&apos;,
  reporter: &apos;Juniper Montage&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, use the library to convert the template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const docx = await OfficeTemplate.render(template, { data })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, turn the rendered document into a PDF. The result&apos;s &lt;code&gt;value&lt;/code&gt; is the PDF&apos;s binary
data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const { value: pdfBytes } = await OfficeConverter.convert(docx, &apos;pdf&apos;, {
  generatorConfig: { pdfConfig: { engine: &apos;native&apos; } },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point you can do whatever, like open it in a new tab:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const pdf = new Blob([pdfBytes], { type: &apos;application/pdf&apos; })
window.open(URL.createObjectURL(pdf), &apos;_blank&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a note, this requires an additional package, &lt;strong&gt;only in the browser&lt;/strong&gt;, &lt;a href=&quot;https://www.npmjs.com/package/pdf-lib&quot;&gt;pdf-lib&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;a-real-demo&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#a-real-demo&quot;&gt;A Real Demo&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To show this in action, I built a simple demo that lets you fill in a form, and get the result as a PDF.&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/10/form1.png&quot; loading=&quot;lazy&quot; alt=&quot;The entry form&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;This form is tied to a bit of JavaScript that grabs the values, makes use of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl&quot;&gt;Intl&lt;/a&gt; to format dates, numbers, and currencies, and then uses officeParser to create a dynamic PDF from your values. Here&apos;s an example:&lt;/p&gt;
&lt;div id=&quot;pdf-viewer-2&quot; style=&quot;width: 100%; height: 500px; margin-bottom: 30px&quot;&gt;&lt;/div&gt;
&lt;script type=&quot;module&quot;&gt;
import EmbedPDF, { ZoomMode } from &apos;https://cdn.jsdelivr.net/npm/@embedpdf/snippet@2/dist/embedpdf.js&apos;;
 
EmbedPDF.init({
	type: &apos;container&apos;,
	target: document.getElementById(&apos;pdf-viewer-2&apos;),
	src: &apos;https://static.raymondcamden.com/assets/foo.pdf&apos;,
	theme: { preference: &apos;system&apos; },
	disabledCategories: [&apos;annotation&apos;],
	zoom: {
		defaultZoomLevel: ZoomMode.FitWidth
	}
});
&lt;/script&gt;
&lt;p&gt;Try this yourself here: &lt;a href=&quot;https://cfjedimaster.github.io/webdemos/officeparser/wordtemplate/dist/index.html&quot;&gt;https://cfjedimaster.github.io/webdemos/officeparser/wordtemplate/dist/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can find the complete source for this here: &lt;a href=&quot;https://github.com/cfjedimaster/webdemos/tree/master/officeparser/wordtemplate&quot;&gt;https://github.com/cfjedimaster/webdemos/tree/master/officeparser/wordtemplate&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As I mentioned above, currently the token system is relatively simple, but I&apos;ve been talking with Harsh about some improvements that I bet will land pretty soon. Let me know what you think!&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/10/12/adding-google-auth-to-tinyjs</id>
                <title>Adding Google Auth to TinyJS</title>
                <updated>2026-10-12T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/10/12/adding-google-auth-to-tinyjs" rel="alternate" type="text/html" title="Adding Google Auth to TinyJS"/>
                <content type="html">
				
                        &lt;p&gt;A few days ago I &lt;a href=&quot;https://www.raymondcamden.com/2026/10/08/tiny-apps-with-tinyjs&quot;&gt;posted&lt;/a&gt; about my experience playing around with &lt;a href=&quot;https://tinyjs.app/&quot;&gt;TinyJS&lt;/a&gt;. This is a &lt;em&gt;very&lt;/em&gt; light &amp;quot;web to desktop app&amp;quot; platform for - well - building desktop apps. Its simplicity and similarity to PhoneGap (at least for the desktop) has got me interested in seeing what I can build with the project. One of my ideas would be improved with a login system so I naturally looked into adding a &amp;quot;Login with Google&amp;quot; feature. While this was &lt;em&gt;mostly&lt;/em&gt; straightforward, I ran into some oddities and figured I&apos;d share it here.&lt;/p&gt;
&lt;p&gt;I went into this exploration with a basic idea of how I&apos;d approach it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create an app in the Google Cloud console to represent my app, getting the client id and secret.&lt;/li&gt;
&lt;li&gt;Generate a link using the standard OAuth flow&lt;/li&gt;
&lt;li&gt;For my redirect URL, use a custom URI scheme (like &lt;code&gt;mycustomapp://&lt;/code&gt;) so that on redirect from Google, it would open the app automatically&lt;/li&gt;
&lt;li&gt;The response would include the code I&apos;d exchange for an access token (and refresh token), at which point I could get the user&apos;s profile and have their name and email address. I&apos;m only thinking of that as my mental model for this little POC is &lt;em&gt;just&lt;/em&gt; login, not using other Google services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Right away I hit a couple of snags.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google&apos;s OAuth flow specifically blocks the use of a custom URI scheme. Instead, you need to hit a locally running web server which would handle getting and verifying the code. I&apos;ve actually seen this before, you end up with a new tab in your browser that says you can close the tab, but I had never implemented it before.&lt;/li&gt;
&lt;li&gt;The next bit isn&apos;t a snag per se, but in my research, I found that in most places, your desktop app can include just the client id, but for Google, you must include the secret as well. Obviously that makes me nervous as heck, but again, this is apparently a well known thing for desktop apps that authenticate with Google. While I don&apos;t feel 100% certain about this, I asked multiple agents, Googled, and so forth, and they all seemed to be in agreement. Absolutely correct me in a comment below if I&apos;m wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alright, so with all of that in mind - I want to once again thank the author of the program for pointing me to the &lt;a href=&quot;https://github.com/tarwin/tinyjsapp-examples/tree/main/platter&quot;&gt;platter demo&lt;/a&gt; which implements the local web server mentioned above. I was able to point my coding agent to that when working through the demo.&lt;/p&gt;
&lt;p&gt;I have &lt;em&gt;not&lt;/em&gt; put my code into a GitHub repo, but will include everything below. As this was just a POC I didn&apos;t feel like it was necessary, but if you want a zip, just ask. Nicely. Or not. I&apos;ll still send it.&lt;/p&gt;
&lt;h2 id=&quot;the-code&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-code&quot;&gt;The Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, let me start with the simplest part, the HTML. I&apos;m not using Alpine for this, or any real CSS. It&apos;s just this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;
&amp;lt;title&amp;gt;__NAME__&amp;lt;/title&amp;gt;
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;style.css&amp;quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;main&amp;gt;
  &amp;lt;h1&amp;gt;Google sign-in Test&amp;lt;/h1&amp;gt;
  &amp;lt;a id=&amp;quot;login-link&amp;quot; href=&amp;quot;#&amp;quot;&amp;gt;Login with Google&amp;lt;/a&amp;gt;
  &amp;lt;section id=&amp;quot;profile&amp;quot; aria-live=&amp;quot;polite&amp;quot; hidden&amp;gt;
    &amp;lt;h2&amp;gt;Signed in&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Name:&amp;lt;/strong&amp;gt; &amp;lt;span id=&amp;quot;profile-name&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Email:&amp;lt;/strong&amp;gt; &amp;lt;span id=&amp;quot;profile-email&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;/section&amp;gt;
  &amp;lt;p id=&amp;quot;login-error&amp;quot; role=&amp;quot;alert&amp;quot; hidden&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/main&amp;gt;

&amp;lt;script src=&amp;quot;app.js&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;I&apos;ve got a login link (blank, that gets filled in later), a section that&apos;s displayed when you are logged in, and a block for errors. The login link should be hidden as well (I&apos;ll explain why later), but again I was trying to keep this simple.&lt;/p&gt;
&lt;p&gt;Now let&apos;s look at the JavaScript, and this is a bit of a mess. With a proper framework like Alpine it would be better organized, but it gets the job done.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const CLIENT_ID = &amp;quot;safe but why not be sure&amp;quot;;

const REDIRECT_URI = &amp;quot;http://127.0.0.1:8765/callback&amp;quot;;
const TOKENS_KEY = &amp;quot;google-oauth-tokens&amp;quot;;

async function makeLoginUrl() {
  const b64url = (buf) =&amp;gt;
    btoa(String.fromCharCode(...new Uint8Array(buf)))
      .replace(/\+/g, &amp;quot;-&amp;quot;).replace(/\//g, &amp;quot;_&amp;quot;).replace(/=+$/, &amp;quot;&amp;quot;);

  const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
  const challenge = b64url(
    await crypto.subtle.digest(&amp;quot;SHA-256&amp;quot;, new TextEncoder().encode(verifier))
  );
  const state = b64url(crypto.getRandomValues(new Uint8Array(16)));

  // keep these for the token exchange step
  sessionStorage.setItem(&amp;quot;pkce_verifier&amp;quot;, verifier);
  sessionStorage.setItem(&amp;quot;oauth_state&amp;quot;, state);

  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: &amp;quot;code&amp;quot;,
    scope: &amp;quot;openid email profile&amp;quot;,
    access_type: &amp;quot;offline&amp;quot;,
    prompt: &amp;quot;consent&amp;quot;,
    code_challenge: challenge,
    code_challenge_method: &amp;quot;S256&amp;quot;,
    state,
  });

  return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
}

document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, async () =&amp;gt; {
  const loginLink = document.querySelector(&amp;quot;#login-link&amp;quot;);

  try {
    const savedTokens = await tiny.app.secrets.get(TOKENS_KEY);
    if (savedTokens) {
      try {
        const session = await tiny.api.call(&amp;quot;restoreSession&amp;quot;, {
          tokens: JSON.parse(savedTokens),
          clientId: CLIENT_ID,
        });
        await tiny.app.secrets.set(TOKENS_KEY, JSON.stringify(session.tokens));
        showProfile(session.profile);
        return;
      } catch {
        await tiny.app.secrets.delete(TOKENS_KEY);
      }
    }
    await prepareLogin(loginLink);
  } catch (error) {
    showError(`Google sign-in failed: ${error.message}`);
  }
});

async function prepareLogin(loginLink) {
  const url = await makeLoginUrl();
  const state = sessionStorage.getItem(&amp;quot;oauth_state&amp;quot;);
  const codeVerifier = sessionStorage.getItem(&amp;quot;pkce_verifier&amp;quot;);
  loginLink.href = url;
  loginLink.target = &amp;quot;_blank&amp;quot;;
  loginLink.rel = &amp;quot;noopener&amp;quot;;

  loginLink.onclick = async () =&amp;gt; {
    try {
      const session = await tiny.api.call(&amp;quot;waitForCode&amp;quot;, {
        port: 8765,
        state,
        codeVerifier,
        clientId: CLIENT_ID,
      });
      await tiny.app.secrets.set(TOKENS_KEY, JSON.stringify(session.tokens));
      showProfile(session.profile);
    } catch (error) {
      showError(`Google sign-in failed: ${error.message}`);
      await prepareLogin(loginLink);
    }
  };
}

function showProfile(profile) {
  document.querySelector(&amp;quot;#profile-name&amp;quot;).textContent = profile.name || &amp;quot;Not provided&amp;quot;;
  document.querySelector(&amp;quot;#profile-email&amp;quot;).textContent = profile.email || &amp;quot;Not provided&amp;quot;;
  document.querySelector(&amp;quot;#profile&amp;quot;).hidden = false;
  document.querySelector(&amp;quot;#login-link&amp;quot;).hidden = true;
  document.querySelector(&amp;quot;#login-error&amp;quot;).hidden = true;
}

function showError(message) {
  const errorOutput = document.querySelector(&amp;quot;#login-error&amp;quot;);
  errorOutput.textContent = message;
  errorOutput.hidden = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I start off with a utility function to generate my login link. Note that redirect URI - it&apos;s going to be hosted by my app via the backend script and makes an assumption on a port that&apos;s free. A better version would try a set of ports to see which can be used.&lt;/p&gt;
&lt;p&gt;In my &lt;code&gt;DOMContentLoaded&lt;/code&gt; block I&apos;ve got multiple things going on.&lt;/p&gt;
&lt;p&gt;First off - I look at a secure storage system for an existing access token. This is documented in the &lt;a href=&quot;https://tinyjs.app/docs.html#secrets&quot;&gt;secrets and auth&lt;/a&gt; section of TinyJS&apos;s docs. If the values exist, a call to the backend is made to try to verify the values and treat the user as already logged in.&lt;/p&gt;
&lt;p&gt;If this fails, or the values weren&apos;t there, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get the login URL and update the DOM&lt;/li&gt;
&lt;li&gt;Tell the backend to fire up the HTTP server&lt;/li&gt;
&lt;li&gt;And then wait for a success.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final two functions just display the profile or an error. Now let&apos;s look at the backend:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Yes, I said it was safe, still, why share it here?
const GOOGLE_CLIENT_SECRET = &amp;quot;my secret brings the boys to the yard&amp;quot;;

async function exchangeCodeForToken({ code, codeVerifier, clientId, port }) {
  const response = await fetch(&amp;quot;https://oauth2.googleapis.com/token&amp;quot;, {
    method: &amp;quot;POST&amp;quot;,
    headers: {
      &amp;quot;content-type&amp;quot;: &amp;quot;application/x-www-form-urlencoded&amp;quot;,
    },
    body: new URLSearchParams({
      code,
      client_id: clientId,
      client_secret: GOOGLE_CLIENT_SECRET,
      redirect_uri: `http://127.0.0.1:${port}/callback`,
      grant_type: &amp;quot;authorization_code&amp;quot;,
      code_verifier: codeVerifier,
    }),
  });
  const tokens = await response.json();
  if (!response.ok || tokens.error) {
    throw new Error(tokens.error_description || tokens.error || `Token request failed (${response.status}).`);
  }
  return tokens;
}

async function refreshAccessToken(refreshToken, clientId) {
  const response = await fetch(&amp;quot;https://oauth2.googleapis.com/token&amp;quot;, {
    method: &amp;quot;POST&amp;quot;,
    headers: {
      &amp;quot;content-type&amp;quot;: &amp;quot;application/x-www-form-urlencoded&amp;quot;,
    },
    body: new URLSearchParams({
      client_id: clientId,
      client_secret: GOOGLE_CLIENT_SECRET,
      refresh_token: refreshToken,
      grant_type: &amp;quot;refresh_token&amp;quot;,
    }),
  });
  const tokens = await response.json();
  if (!response.ok || tokens.error) {
    throw new Error(tokens.error_description || tokens.error || `Token refresh failed (${response.status}).`);
  }
  return tokens;
}

function expiresAt(tokens) {
  const expiresIn = Number(tokens.expires_in);
  if (!tokens.access_token || !Number.isFinite(expiresIn) || expiresIn &amp;lt;= 0) {
    throw new Error(&amp;quot;Google returned an invalid access token response.&amp;quot;);
  }
  return Date.now() + expiresIn * 1000;
}

async function getUserProfile(accessToken) {
  const response = await fetch(&amp;quot;https://openidconnect.googleapis.com/v1/userinfo&amp;quot;, {
    headers: {
      authorization: `Bearer ${accessToken}`,
    },
  });
  const profile = await response.json();
  if (!response.ok) {
    throw new Error(profile.error_description || profile.error || `Profile request failed (${response.status}).`);
  }
  return {
    name: profile.name || &amp;quot;&amp;quot;,
    email: profile.email || &amp;quot;&amp;quot;,
  };
}

async function restoreSession(tokens, clientId) {
  if (!tokens?.access_token || !tokens?.refresh_token || !tokens?.expires_at) {
    throw new Error(&amp;quot;Saved sign-in is incomplete.&amp;quot;);
  }

  let refreshedTokens = {};
  if (tokens.expires_at &amp;lt;= Date.now() + 60000) {
    refreshedTokens = await refreshAccessToken(tokens.refresh_token, clientId);
  }

  const updatedTokens = {
    access_token: refreshedTokens.access_token || tokens.access_token,
    refresh_token: refreshedTokens.refresh_token || tokens.refresh_token,
    expires_at: refreshedTokens.access_token
      ? expiresAt(refreshedTokens)
      : tokens.expires_at,
  };
  const profile = await getUserProfile(updatedTokens.access_token);
  return { profile, tokens: updatedTokens };
}

async function waitForCode(port, expectedState, codeVerifier, clientId) {
  return new Promise((resolve, reject) =&amp;gt; {
    let server;
    let timer;
    let settled = false;
    let processing = false;

    const stopServer = () =&amp;gt; {
      if (timer) clearTimeout(timer);
      if (!server) return;
      const activeServer = server;
      server = null;
      try {
        activeServer.close();
      } catch (error) {
        console.error(&amp;quot;Failed to close OAuth callback server:&amp;quot;, error);
      }
    };

    const finish = (error, code, closeImmediately = false) =&amp;gt; {
      if (settled) return;
      settled = true;
      if (timer) clearTimeout(timer);
      if (closeImmediately) stopServer();
      else setTimeout(stopServer, 1000);
      if (error) reject(error);
      else resolve(code);
    };

    const escapeHtml = (value) =&amp;gt; String(value)
      .replace(/&amp;amp;/g, &amp;quot;&amp;amp;amp;&amp;quot;)
      .replace(/&amp;lt;/g, &amp;quot;&amp;amp;lt;&amp;quot;)
      .replace(/&amp;gt;/g, &amp;quot;&amp;amp;gt;&amp;quot;)
      .replace(/&amp;quot;/g, &amp;quot;&amp;amp;quot;&amp;quot;)
      .replace(/&apos;/g, &amp;quot;&amp;amp;#39;&amp;quot;);
    const htmlResponse = (message, status = 200) =&amp;gt; new Response(
      `&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h3&amp;gt;${escapeHtml(message)}&amp;lt;/h3&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;`,
      {
        status,
        headers: {
          &amp;quot;content-type&amp;quot;: &amp;quot;text/html; charset=utf-8&amp;quot;,
          &amp;quot;content-security-policy&amp;quot;: &amp;quot;default-src &apos;none&apos;; style-src &apos;unsafe-inline&apos;&amp;quot;,
        },
      }
    );

    server = tjs.serve({
      port,
      fetch: async (request) =&amp;gt; {
        let url;
        try {
          url = new URL(request.url);
        } catch {
          return htmlResponse(&amp;quot;Invalid callback request.&amp;quot;, 400);
        }

        if (url.pathname !== &amp;quot;/callback&amp;quot;) return htmlResponse(&amp;quot;Not found.&amp;quot;, 404);
        if (settled) return htmlResponse(&amp;quot;This login request has already been processed.&amp;quot;, 410);
        if (processing) return htmlResponse(&amp;quot;This login request is being processed.&amp;quot;, 409);
        if (url.searchParams.get(&amp;quot;state&amp;quot;) !== expectedState) {
          return htmlResponse(&amp;quot;State mismatch. Return to the app and try again.&amp;quot;, 400);
        }

        const oauthError = url.searchParams.get(&amp;quot;error&amp;quot;);
        if (oauthError) {
          finish(new Error(oauthError));
          return htmlResponse(&amp;quot;Login failed: &amp;quot; + oauthError);
        }

        const code = url.searchParams.get(&amp;quot;code&amp;quot;);
        if (!code) {
          finish(new Error(&amp;quot;OAuth callback did not include an authorization code.&amp;quot;));
          return htmlResponse(&amp;quot;Login failed: authorization code was missing.&amp;quot;, 400);
        }

        processing = true;
        try {
          const tokens = await exchangeCodeForToken({
            code,
            codeVerifier,
            clientId,
            port,
          });
          const profile = await getUserProfile(tokens.access_token);
          finish(null, {
            profile,
            tokens: {
              access_token: tokens.access_token,
              refresh_token: tokens.refresh_token,
              expires_at: expiresAt(tokens),
            },
          });
          return htmlResponse(&amp;quot;Login complete. You can close this tab.&amp;quot;);
        } catch (error) {
          finish(error);
          return htmlResponse(&amp;quot;Token exchange failed: &amp;quot; + error.message, 502);
        }
      },
    });

    timer = setTimeout(() =&amp;gt; {
      finish(new Error(&amp;quot;Timed out waiting for the OAuth callback.&amp;quot;), null, true);
    }, 120000);
  });
}

export const api = {
  restoreSession: ({ tokens, clientId }) =&amp;gt; {
    if (!clientId) throw new Error(&amp;quot;Google client ID is required.&amp;quot;);
    return restoreSession(tokens, clientId);
  },
  waitForCode: ({ port, state, codeVerifier, clientId }) =&amp;gt; {
    if (!codeVerifier || !clientId) {
      throw new Error(&amp;quot;OAuth code verifier and client ID are required.&amp;quot;);
    }
    return waitForCode(port, state, codeVerifier, clientId);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one&apos;s a bit of a beast, but some are fairly standard. Both &lt;code&gt;exchangeCodeForToken&lt;/code&gt; and &lt;code&gt;refreshAccessToken&lt;/code&gt; are standard OAuth stuff. Ditto for &lt;code&gt;getUserProfile&lt;/code&gt;. The important bit is &lt;code&gt;waitForCode&lt;/code&gt;. This is where the small web server is kicked off that attempts to handle the redirect back from Google. I said up front I used AI to build most of this and I have to be honest, I&apos;m glad I did as this part would have taken me quite some time.&lt;/p&gt;
&lt;p&gt;All in all - it works great. The only real issue is a nit-picky one - when the app loads you see the login link for a second before the stored auth is verified. A bit of UX magic there would handle that.&lt;/p&gt;
&lt;p&gt;There&apos;s an important part left out of this that I&apos;ll need to tackle next - which is how I would take the verified identity here and pass it to a remote service in a secure way such that the remote service can also verify it (&amp;quot;Yes, this was Raymond Camden&amp;quot;) before performing an action.&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/10/08/tiny-apps-with-tinyjs</id>
                <title>Tiny Apps with TinyJS</title>
                <updated>2026-10-08T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/10/08/tiny-apps-with-tinyjs" rel="alternate" type="text/html" title="Tiny Apps with TinyJS"/>
                <content type="html">
				
                        &lt;p&gt;About a year ago, I was still running my live stream (&lt;a href=&quot;https://www.youtube.com/watch?v=BzRUsvAR4oQ&amp;amp;list=PL_z-rqJYNijqhtPcEbwacp34TiFfb8eyO&quot;&gt;Code Break&lt;/a&gt;) and my final three streams all covered one topic - building desktop applications with non-native code. I covered &lt;a href=&quot;https://v2.tauri.app/&quot;&gt;Tauri&lt;/a&gt; (HTML on the front, Rust on the back) and &lt;a href=&quot;https://flet.dev/&quot;&gt;Flet&lt;/a&gt; (all Python). Recently I came across another option, &lt;a href=&quot;https://tinyjs.app/&quot;&gt;tinyjs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;TinyJS (technically the name is &amp;quot;tinyjs&amp;quot; but I can&apos;t make myself use the lowercase in my writing like that ;) is a lot like PhoneGap/Apache Cordova. Your application is the system web view shipped along with the ability to make system calls via a bridge to &lt;a href=&quot;https://txikijs.org/&quot;&gt;txiki.js&lt;/a&gt;. This bridge gives you full access to the file system and more, like any other native app.&lt;/p&gt;
&lt;p&gt;Unlike PhoneGap, this is &lt;em&gt;just&lt;/em&gt; for desktop apps, which is fine, but I think the biggest selling point is the size of the bundled app - typically around 6 megs. That&apos;s tiny versus something like Electron.&lt;/p&gt;
&lt;p&gt;It&apos;s got hooks into everything a desktop app could need, like native dialogs and OS menus and such. It can write securely to the OS&apos;s credential store, use SQLite, and even handle application updating.&lt;/p&gt;
&lt;p&gt;I&apos;ve just begun playing with it recently and I&apos;ve got a few ideas for apps I want to build, but I thought I&apos;d share a quick demo. This one&apos;s a bit ugly, and to be clear, that&apos;s on me. As it&apos;s a web view, you&apos;ve got full control over how the end result looks. I was just going for something quick and dirty in my test.&lt;/p&gt;
&lt;h2 id=&quot;first-steps&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#first-steps&quot;&gt;First Steps&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After doing a quick &lt;a href=&quot;https://tinyjs.app/docs.html#install&quot;&gt;install&lt;/a&gt;, you can scaffold at the command line with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tinyjs new appname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default this scaffolds out a vanilla application (no React default, woot!) with a few files and folders - the important ones (at least initially while testing and learning) are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;frontend - the UI/UX layer (HTML, CSS, JS)&lt;/li&gt;
&lt;li&gt;main.js - a &amp;quot;backend&amp;quot; file that lets you build services you expose to your front end code. This is where you do &amp;quot;system&amp;quot; stuff your frontend JavaScript can&apos;t do.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As an example, your backend code could use this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const api: Record&amp;lt;string, TinyApiHandler&amp;gt; = {
  hello: async ({ name }: { name: string }) =&amp;gt; &apos;hi &apos; + name + &apos; — from the backend&apos;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which would then be available in the frontend as &lt;code&gt;await tiny.api.call(&apos;hello&apos;, {...})&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Running the app is done via &lt;code&gt;tinyjs dev&lt;/code&gt; which will load the app and enable HMR. In my testing, reloading was &lt;em&gt;incredibly&lt;/em&gt; quick, but I didn&apos;t build anything large (yet).&lt;/p&gt;
&lt;h2 id=&quot;working-with-alpine&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#working-with-alpine&quot;&gt;Working with Alpine&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The CLI has got a number of different scaffolds you can use, including Alpine. (And I should say - huge thanks to &lt;a href=&quot;https://tarwin.art/&quot;&gt;tarwin&lt;/a&gt;, the creator - who answered a lot of my questions via email and added stuff I asked for left and right!) I scaffolded one like that and one thing I&apos;ll note - the scaffold for different frameworks will differ a bit from the simple app you get by default. Each project has a &lt;code&gt;tinyjs.json&lt;/code&gt; file which has metadata for the project, including where your backend file is and how the frontend works.&lt;/p&gt;
&lt;p&gt;For my simple test, I wanted to check a few things. First, is CORS still enforced when using &lt;code&gt;fetch&lt;/code&gt;? The short answer is yes, it is. The longer answer is that doesn&apos;t matter as you can use &lt;code&gt;tiny.fetch&lt;/code&gt; instead. I thought it would be fun then to build a simple &amp;quot;RSS to HTML&amp;quot; application where I could enter an RSS feed and render the items.&lt;/p&gt;
&lt;p&gt;I copied the browser distributable for the &lt;a href=&quot;https://www.npmjs.com/package/rss-parser&quot;&gt;&lt;code&gt;rss-parser&lt;/code&gt;&lt;/a&gt; project and simply included it in my HTML via a script tag. Here&apos;s the entirety of that file:&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;RSS Parser&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;section id=&amp;quot;center&amp;quot; x-data=&amp;quot;hello&amp;quot;&amp;gt;
      &amp;lt;h1&amp;gt;RSS Parser&amp;lt;/h1&amp;gt;
      &amp;lt;input x-model=&amp;quot;url&amp;quot; placeholder=&amp;quot;RSS feed URL&amp;quot; /&amp;gt; &amp;lt;button @click=&amp;quot;parse&amp;quot;&amp;gt;Parse&amp;lt;/button&amp;gt;
      &amp;lt;template x-if=&amp;quot;items.length &amp;gt; 0&amp;quot;&amp;gt;
        &amp;lt;div&amp;gt;
        &amp;lt;h2&amp;gt;Items&amp;lt;/h2&amp;gt;
        &amp;lt;ul&amp;gt;
          &amp;lt;template x-for=&amp;quot;item in items&amp;quot; :key=&amp;quot;item.link&amp;quot;&amp;gt;
            &amp;lt;li&amp;gt;
              &amp;lt;a :href=&amp;quot;item.link&amp;quot; target=&amp;quot;_blank&amp;quot; x-text=&amp;quot;item.title&amp;quot;&amp;gt;&amp;lt;/a&amp;gt;
            &amp;lt;/li&amp;gt;
          &amp;lt;/template&amp;gt;
        &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/template&amp;gt;
    &amp;lt;/section&amp;gt;
    &amp;lt;script src=&amp;quot;/src/rss-parser.min.js&amp;quot;&amp;gt;&amp;lt;/script&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;That&apos;s all regular Alpine.js stuff, nothing fancy there. Now for the JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import &apos;./style.css&apos;;
import &apos;simpledotcss/simple.min.css&apos;;

import Alpine from &apos;alpinejs&apos;;

Alpine.data(&apos;hello&apos;, () =&amp;gt; ({
  name: &apos;world&apos;,
  url: &apos;https://www.raymondcamden.com/feed.xml&apos;,
  items: [],
  async init() {
  },
  async parse() {
    const parser = new RSSParser();
    let req = await tiny.fetch(this.url);
    let res = await req.text();
    let feed = await parser.parseString(res);
    this.items = feed.items;
  },
  async ask() {
    // Every function in the backend&apos;s api object is callable from the page.
    this.reply = await tiny.api.call(&apos;hello&apos;, { name: this.name })
  },
}))

Alpine.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few things to note here - I&apos;ve got some junk in the file still, like the &lt;code&gt;ask&lt;/code&gt; function which was a leftover from the scaffold. The important bit is the &lt;code&gt;parse&lt;/code&gt; function. The RSSParser object has the ability to parse a URL, but it uses fetch and is blocked by CORS. So instead I get the XML via &lt;code&gt;tiny.fetch&lt;/code&gt; and pass it to &lt;code&gt;parseString&lt;/code&gt;. And that&apos;s literally it. Here&apos;s a screenshot of the application running:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/10/tj1.png&quot; loading=&quot;lazy&quot; alt=&quot;App running&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;h2 id=&quot;more-info&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#more-info&quot;&gt;More Info&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://tinyjs.app/docs.html&quot;&gt;docs&lt;/a&gt; obviously are a great place to start, and if you&apos;re curious about more of how it actually works, there&apos;s a good &lt;a href=&quot;https://tinyjs.app/docs.html#howitworks&quot;&gt;explanation&lt;/a&gt; of that as well.&lt;/p&gt;
&lt;p&gt;My next demo is going to be testing an OAuth login flow with the application. The project supports registering custom URL schemes which is typically how that&apos;s handled so it &lt;em&gt;should&lt;/em&gt; be possible, but I want to test it out myself of course. :)&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@goumbik?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Lukas Blazek&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/coffee-latte-near-white-wireless-keyboard-and-apple-earpods-on-the-table-photography-GnvurwJsKaY?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="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/10/06/adding-my-latest-codepens-to-my-blog</id>
                <title>Adding My Latest CodePens to My Blog</title>
                <updated>2026-10-06T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/10/06/adding-my-latest-codepens-to-my-blog" rel="alternate" type="text/html" title="Adding My Latest CodePens to My Blog"/>
                <content type="html">
				
                        &lt;p&gt;I&apos;ve been a fan of CodePen for many, many years, and it&apos;s one of the few services I spend money on to help support. A few months back, they released an entirely new version that dramatically improved the service (CodePen 2.0) and it just made me more happy I was a supporter. Just a few days ago they &lt;a href=&quot;https://codepen.io/changelog/2026-10-02&quot;&gt;announced more changes&lt;/a&gt; including a full &lt;a href=&quot;https://blog.codepen.io/docs/api/api-v2/&quot;&gt;API&lt;/a&gt;. I thought it would be fun to add a display of my recent pens. Yes, I thought that would be fun, because that&apos;s the kind of nerd I am.&lt;/p&gt;
&lt;h2 id=&quot;adding-dynamic-content-to-a-ssg&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#adding-dynamic-content-to-a-ssg&quot;&gt;Adding Dynamic Content to a SSG&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It has been a &lt;em&gt;very&lt;/em&gt; long time since I blogged about anything related to static site generators. There&apos;s no particular reason for that, but I figure this is a good time to bring up the topic again, specifically about how static sites can include dynamic content. There&apos;s two main paths to this.&lt;/p&gt;
&lt;p&gt;The first is getting content at build time. For Eleventy, this is done with data files that are executed at build time and can return ad hoc data for use in your templates. As an example of that, the list of books I&apos;m reading on my &lt;a href=&quot;https://www.raymondcamden.com/now&quot;&gt;Now&lt;/a&gt; page are gathered via a GraphQL query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const HARDCOVER_BOOKS = process.env.HARDCOVER_BOOKS;

export default async function() {

    if(process.env.SKIP_REMOTE_DATA) return [];
    if(!HARDCOVER_BOOKS) return [];
    let req;

    let body = `
    {
    user_books(
        where: {user_id: {_eq: 65213}, status_id: {_eq: 2}}
    ) {
        book {
            title
            image {
                url
            }
            contributions {
                author {
                    name
                }
            }
        }
    }
    }
    `.trim();

    try {
        req = await fetch(&apos;https://api.hardcover.app/v1/graphql&apos;, {
            method:&apos;POST&apos;, 
            headers: {
                &apos;authorization&apos;:HARDCOVER_BOOKS,
                &apos;Content-Type&apos;:&apos;application/json&apos;
            },
            body:JSON.stringify({query:body})
        });
    } catch (e) {
        console.log(&apos;Hardcover API error&apos;, e);
        return [];
    }

    let data = (await req.json()).data.user_books.map(ob =&amp;gt; ob.book);
    /* normalize authors */
    data = data.map(b =&amp;gt; {
    b.authors = b.contributions.reduce((list,c) =&amp;gt; {
        if(c.author) list.push(c.author.name);
        return list;
        },[]);
        return b;
    });

    return data;

};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The list of books returned there can then be rendered in my page template:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&amp;quot;films&amp;quot;&amp;gt;
{% for book in hardcover_books  %}
  &amp;lt;div class=&amp;quot;film&amp;quot;&amp;gt;
  {% if book.image != null %}
  &amp;lt;img src=&amp;quot;https://res.cloudinary.com/raymondcamden/image/fetch/c_fit,w_216/{{book.image.url}}&amp;quot; alt=&amp;quot;Cover of {{ book.title }}&amp;quot;&amp;gt;
  {% else  %}
  &amp;lt;img src=&amp;quot;https://res.cloudinary.com/raymondcamden/image/fetch/c_fit,w_216/https://static.raymondcamden.com/images/no_cover_available.jpg&amp;quot; alt=&amp;quot;No Cover Available&amp;quot;&amp;gt;
  {% endif %}
  &amp;quot;{{ book.title  }}&amp;quot; by {{ book.authors | join: &apos;, &apos; }}
  &amp;lt;/div&amp;gt;
{% endfor %}
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can tell by the class names, I use the same logic for the films I&apos;ve watched. In both cases, the data doesn&apos;t change terribly often so I&apos;m ok with it being a bit out of date. I also publish &lt;em&gt;something&lt;/em&gt; to the blog every week so it&apos;s never too far behind.&lt;/p&gt;
&lt;p&gt;That&apos;s option 1. Option 2 is to use JavaScript and an API. That ensures the content is always 100% up to date but it has other trade offs. Some APIs don&apos;t have CORS support and APIs requiring keys that can&apos;t be in client-side code. In that case, I simply use a serverless function which Netlify makes pretty simple.&lt;/p&gt;
&lt;h2 id=&quot;getting-my-pens&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#getting-my-pens&quot;&gt;Getting my Pens&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, so given that I&apos;ve decided to fetch my CodePens via JavaScript on the front-end via a serverless function, I first created a key in CodePen&apos;s settings, added to my Netlify configuration, and then wrote a quick and dirty function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Context } from &apos;@netlify/functions&apos;

export default async (request: Request, context: Context) =&amp;gt; {
  try {

    const CP_KEY = process.env.CODEPEN_KEY;

    const req = await fetch(&apos;https://api.codepen.io/v2/pens?sort=updated&amp;amp;direction=desc&amp;amp;access=Public&amp;amp;template=false&amp;amp;per_page=10&apos;, {
      method: &apos;GET&apos;,
      headers: {
        &apos;Authorization&apos;: `Bearer ${CP_KEY}`,
        &apos;Content-Type&apos;: &apos;application/json&apos;
      }});
    
    let data = await req.json();

    // now lets do some mapping 
    data = data.map((pen: any) =&amp;gt; {
      return {
        title: pen.title,
        url: pen.urls.editor,
        created_at: pen.created_at,
        updated_at: pen.updated_at,
        screenshot: pen.screenshot_urls.small
      }
    });

    return new Response(JSON.stringify(data), {
      headers: {
        &apos;Content-Type&apos;: &apos;application/json&apos;
      }
    })
  } catch (error) {
    return new Response(error.toString(), {
      status: 500,
    })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;m filtering by public pens and pens that aren&apos;t templates, sorted by last updated, and capping the list to 10. Then I map the data to a &lt;em&gt;much&lt;/em&gt; smaller set of information as I only need part of what&apos;s returned in my front end.&lt;/p&gt;
&lt;p&gt;Then all I needed to do was add it to my &lt;a href=&quot;/stuff&quot;&gt;stuff&lt;/a&gt; page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let $pens = document.querySelector(&apos;#pens&apos;);

let pens = await fetch(&apos;/.netlify/functions/get-codepens&apos;).then(r =&amp;gt; r.json());

pens.forEach(p =&amp;gt; {
	let pen = document.createElement(&apos;a&apos;);
	pen.className = &apos;penBox&apos;;
	pen.href = p.url;
	pen.target = &apos;_blank&apos;;
	pen.rel = &apos;noopener noreferrer&apos;;

	let screenshot = document.createElement(&apos;img&apos;);
	screenshot.src = p.screenshot;
	screenshot.alt = &apos;&apos;;
	screenshot.loading = &apos;lazy&apos;;

	let title = document.createElement(&apos;h3&apos;);
	title.textContent = p.title;

	pen.append(screenshot, title);
	$pens.append(pen);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sweet. Of course, I think I&apos;m the only person who visits either my &lt;a href=&quot;/stuff&quot;&gt;stuff&lt;/a&gt; or &lt;a href=&quot;/now&quot;&gt;now&lt;/a&gt; pages, but they make me happy!&lt;/p&gt;

                        
                
				</content>

                
                <category term="eleventy" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/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" type="text/html" title="A Not-So-Simple RSS App in TypeScript"/>
                <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&apos;t be parsed in client-side JavaScript (don&apos;t know why? ask me below!) so I&apos;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=&quot;https://www.raymondcamden.com/2026/02/02/building-an-rss-aggregator-with-astro&quot;&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&apos;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=&quot;https://ts-rss-vite.netlify.app/&quot;&gt;https://ts-rss-vite.netlify.app/&lt;/a&gt;. The full source is here: &lt;a href=&quot;https://github.com/cfjedimaster/typescript-stuff/tree/main/rss-reader-1&quot;&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=&quot;run-typescript-checking&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#run-typescript-checking&quot;&gt;Run TypeScript Checking&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I mentioned in my &lt;a href=&quot;https://www.raymondcamden.com/2026/09/26/a-simple-weather-app-in-typescript&quot;&gt;last post&lt;/a&gt; that when Vite would reload, it wasn&apos;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=&quot;https://www.npmjs.com/package/vite-plugin-checker&quot;&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=&quot;language-js&quot;&gt;import { defineConfig } from &apos;vite&apos;;
import checker from &apos;vite-plugin-checker&apos;;

export default defineConfig({
  plugins: [checker({ typescript: true })],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file doesn&apos;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=&quot;the-serverless-function&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-serverless-function&quot;&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&apos;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&apos;t bother with BLOB caching. Anyway, here&apos;s that function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import Parser from &apos;rss-parser&apos;;
import type { Context } from &apos;@netlify/functions&apos;;

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

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

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

    if (
      typeof body !== &apos;object&apos; ||
      body === null ||
      !(&apos;feeds&apos; in body) ||
      !Array.isArray(body.feeds)
    ) {
      return new Response(&apos;Request body must include a feeds array&apos;, {
        status: 400,
      })
    }
    console.log(&apos;Parsing feeds:&apos;, 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 === &apos;fulfilled&apos;) {
            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 || &apos;&apos;;
                newItems.push({
                    title: item.title,
                    link: item.link,
                    content: content,
                    pubDate: item.pubDate,
                    feedTitle: feed.title
                });
            });

            items.push(...newItems);

        } else {
            console.error(&apos;Error fetching/parsing feed:&apos;, 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(&apos;Request body must be valid JSON&apos;, {
      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&apos;s one I&apos;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&apos;s now being resolved instead of your project&apos;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&apos;t forget this when it happens again.&lt;/p&gt;
&lt;h2 id=&quot;what&apos;s-next%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what&apos;s-next%3F&quot;&gt;What&apos;s Next?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This has been so fun, at least for me. I&apos;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" 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>
        
</feed>