<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: localstorage</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/localstorage.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-01-28T22:10:08+00:00</updated><author><name>Simon Willison</name></author><entry><title>Adding dynamic features to an aggressively cached website</title><link href="https://simonwillison.net/2026/Jan/28/dynamic-features-static-site/#atom-tag" rel="alternate"/><published>2026-01-28T22:10:08+00:00</published><updated>2026-01-28T22:10:08+00:00</updated><id>https://simonwillison.net/2026/Jan/28/dynamic-features-static-site/#atom-tag</id><summary type="html">
    &lt;p&gt;My blog uses aggressive caching: it sits behind Cloudflare with a 15 minute cache header, which guarantees it can survive even the largest traffic spike to any given page. I've recently added a couple of dynamic features that work in spite of that full-page caching. Here's how those work.&lt;/p&gt;
&lt;h4 id="edit-links-that-are-visible-only-to-me"&gt;Edit links that are visible only to me&lt;/h4&gt;
&lt;p&gt;This is a Django site and I manage it through the Django admin.&lt;/p&gt;
&lt;p&gt;I have &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/blog/models.py#L254-L449"&gt;four types of content&lt;/a&gt; - entries, link posts (aka blogmarks), quotations and notes. Each of those has a different model and hence a different Django admin area.&lt;/p&gt;
&lt;p&gt;I wanted an "edit" link on the public pages that was only visible to me.&lt;/p&gt;
&lt;p&gt;The button looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/edit-link.jpg" alt="Entry footer - it says Posted 27th January 2026 at 9:44 p.m. followed by a square Edit button with an icon." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I solved conditional display of this button with &lt;code&gt;localStorage&lt;/code&gt;. I have a &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/templates/base.html#L89-L105"&gt;tiny bit of JavaScript&lt;/a&gt; which checks to see if the &lt;code&gt;localStorage&lt;/code&gt; key &lt;code&gt;ADMIN&lt;/code&gt; is set and, if it is, displays an edit link based on a data attribute:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addEventListener&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'DOMContentLoaded'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;localStorage&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getItem&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'ADMIN'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelectorAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.edit-page-link'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;forEach&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;el&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getAttribute&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'data-admin-url'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;a&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;createElement&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'a'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;className&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'edit-link'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerHTML&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'&amp;lt;svg&amp;gt;...&amp;lt;/svg&amp;gt; Edit'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;appendChild&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;a&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;style&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;display&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'block'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you want to see my edit links you can run this snippet of JavaScript:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;localStorage&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setItem&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'ADMIN'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'1'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;My Django admin dashboard has &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/templates/admin/index.html#L18-L39"&gt;a custom checkbox&lt;/a&gt; I can click to turn this option on and off in my own browser:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/edit-toggle.jpg" alt="Screenshot of a Tools settings panel with a teal header reading &amp;quot;Tools&amp;quot; followed by three linked options: &amp;quot;Bulk Tag Tool - Add tags to multiple items at once&amp;quot;, &amp;quot;Merge Tags - Merge multiple tags into one&amp;quot;, &amp;quot;SQL Dashboard - Run SQL queries against the database&amp;quot;, and a checked checkbox labeled &amp;quot;Show &amp;quot;Edit&amp;quot; links on public pages&amp;quot;" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="random-navigation-within-a-tag"&gt;Random navigation within a tag&lt;/h4&gt;
&lt;p&gt;Those admin edit links are a very simple pattern. A more interesting one is a feature I added recently for navigating randomly within a tag.&lt;/p&gt;
&lt;p&gt;Here's an animated GIF showing those random tag navigations in action (&lt;a href="https://simonwillison.net/tag/ai-ethics/"&gt;try it here&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/random-by-tag.gif" alt="Animated demo. Starts on the ai-ethics tag page where a new Random button sits next to the feed icon. Clicking that button jumps to a post with that tag and moves the button into the site header - clicking it multiple times jumps to more random items." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;On any of my blog's tag pages you can click the "Random" button to bounce to a random post with that tag. That random button then persists in the header of the page and you can click it to continue bouncing to random items in that same tag.&lt;/p&gt;
&lt;p&gt;A post can have multiple tags, so there needs to be a little bit of persistent magic to remember which tag you are navigating and display the relevant button in the header.&lt;/p&gt;
&lt;p&gt;Once again, this uses &lt;code&gt;localStorage&lt;/code&gt;. Any click to a random button records both the tag and the current timestamp to the &lt;code&gt;random_tag&lt;/code&gt; key in &lt;code&gt;localStorage&lt;/code&gt; before redirecting the user to the &lt;code&gt;/random/name-of-tag/&lt;/code&gt; page, which selects a random post and redirects them there.&lt;/p&gt;
&lt;p&gt;Any time a new page loads, JavaScript checks if that &lt;code&gt;random_tag&lt;/code&gt; key has a value that was recorded within the past 5 seconds. If so, that random button is appended to the header.&lt;/p&gt;
&lt;p&gt;This means that, provided the page loads within 5 seconds of the user clicking the button, the random tag navigation will persist on the page.&lt;/p&gt;
&lt;p&gt;You can &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/templates/base.html#L106-L147"&gt;see the code for that here&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="and-the-prompts"&gt;And the prompts&lt;/h4&gt;
&lt;p&gt;I built the random tag feature entirely using Claude Code for web, prompted from my iPhone. I started with the &lt;code&gt;/random/TAG/&lt;/code&gt; endpoint (&lt;a href="https://gistpreview.github.io/?2e7de58a779271aa5eb6f4abcd412d72/index.html"&gt;full transcript&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Build /random/TAG/ - a page which picks a random post (could be an entry or blogmark or note or quote) that has that tag and sends a 302 redirect to it, marked as no-cache so Cloudflare does not cache it&lt;/p&gt;
&lt;p&gt;Use a union to build a list of every content type (a string representing the table out of the four types) and primary key for every item tagged with that tag, then order by random and return the first one&lt;/p&gt;
&lt;p&gt;Then inflate the type and ID into an object and load it and redirect to the URL&lt;/p&gt;
&lt;p&gt;Include tests - it should work by setting up a tag with one of each of the content types and then running in a loop calling that endpoint until it has either returned one of each of the four types or it hits 1000 loops at which point fail with an error&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I do not like that solution, some of my tags have thousands of items&lt;/p&gt;
&lt;p&gt;Can we do something clever with a CTE?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's the &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b8066f870a94d149f5e8cee6e787d3377c0b9507/blog/views.py#L737-L762"&gt;something clever with a CTE&lt;/a&gt; solution we ended up with.&lt;/p&gt;
&lt;p&gt;For the "Random post" button (&lt;a href="https://gistpreview.github.io/?d2d3abe380080ceb9e7fb854fa197bff/index.html"&gt;transcript&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Look at most recent commit, then modify the /tags/xxx/ page to have a "Random post" button which looks good and links to the /random/xxx/ page&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Put it before not after the feed icon. It should only display if a tag has more than 5 posts&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And finally, the &lt;code&gt;localStorage&lt;/code&gt; implementation that persists a random tag button in the header (&lt;a href="https://gistpreview.github.io/?8405b84f8e53738c8d4377b2e41dcdef/page-001.html"&gt;transcript&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Review the last two commits. Make it so clicking the Random button on a tag page sets a localStorage value for random_tag with that tag and a timestamp. On any other page view that uses the base item template add JS that checks for that localStorage value and makes sure the timestamp is within 5 seconds. If it is within 5 seconds it adds a "Random name-of-tag" button to the little top navigation bar, styled like the original Random button, which bumps the localStorage timestamp and then sends the user to /random/name-of-tag/ when they click it. In this way clicking "Random" on a tag page will send the user into an experience where they can keep clicking to keep surfing randomly in that topic.&lt;/p&gt;
&lt;/blockquote&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/caching"&gt;caching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="caching"/><category term="django"/><category term="javascript"/><category term="localstorage"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/></entry><entry><title>Cooking with Claude</title><link href="https://simonwillison.net/2025/Dec/23/cooking-with-claude/#atom-tag" rel="alternate"/><published>2025-12-23T05:01:34+00:00</published><updated>2025-12-23T05:01:34+00:00</updated><id>https://simonwillison.net/2025/Dec/23/cooking-with-claude/#atom-tag</id><summary type="html">
    &lt;p&gt;I've been having an absurd amount of fun recently using LLMs for cooking. I started out using them for basic recipes, but as I've grown more confident in their culinary abilities I've leaned into them for more advanced tasks. Today I tried something new: having Claude vibe-code up a custom application to help with the timing for a complicated meal preparation. It worked really well!&lt;/p&gt;
&lt;h4 id="a-custom-timing-app-for-two-recipes-at-once"&gt;A custom timing app for two recipes at once&lt;/h4&gt;
&lt;p&gt;We have family staying at the moment, which means cooking for four. We subscribe to a meal delivery service called &lt;a href="https://www.greenchef.com/"&gt;Green Chef&lt;/a&gt;, mainly because it takes the thinking out of cooking three times a week: grab a bag from the fridge, follow the instructions, eat.&lt;/p&gt;
&lt;p&gt;Each bag serves two portions, so cooking for four means preparing two bags at once.&lt;/p&gt;
&lt;p&gt;I have done this a few times now and it is always a mad flurry of pans and ingredients and timers and desperately trying to figure out what should happen when and how to get both recipes finished at the same time. It's fun but it's also chaotic and error-prone.&lt;/p&gt;
&lt;p&gt;This time I decided to try something different, and potentially even more chaotic and error-prone: I outsourced the planning entirely to Claude.&lt;/p&gt;
&lt;p&gt;I took this single photo of the two recipe cards side-by-side and fed it to Claude Opus 4.5 (in the Claude iPhone app) with this prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Extract both of these recipes in as much detail as possible&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/recipe-cards.jpg" alt="Two recipe cards placed next to each other on a kitchen counter. Each card has detailed instructions plus photographs of steps." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a moderately challenging vision task in that there quite a lot of small text in the photo. I wasn't confident Opus could handle it.&lt;/p&gt;
&lt;p&gt;I hadn't read the recipe cards myself. The responsible thing to do here would be a thorough review or at least a spot-check - I chose to keep things chaotic and didn't do any more than quickly eyeball the result.&lt;/p&gt;
&lt;p&gt;I asked what pots I'd need:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Give me a full list of pots I would need if I was cooking both of them at once&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then I prompted it to build a custom application to help me with the cooking process itself:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I am going to cook them both at the same time. Build me a no react, mobile, friendly, interactive, artifact that spells out the process with exact timing on when everything needs to happen have a start setting at the top, which starts a timer and persists when I hit start in localStorage in case the page reloads. The next steps should show prominently with countdowns to when they open. The full combined timeline should be shown slow with calculated times tor when each thing should happen&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I copied the result out onto my own hosting (&lt;a href="https://tools.simonwillison.net/blackened-cauliflower-and-turkish-style-stew"&gt;you can try it here&lt;/a&gt;) because I wasn't sure if localStorage would work inside the Claude app and I &lt;em&gt;really&lt;/em&gt; didn't want it to forget my times!&lt;/p&gt;
&lt;p&gt;Then I clicked "start cooking"!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/recipe-timer.gif" alt="The recipe app shows a full timeline with 00:00 Preheat Oven and onwards, plus a big Start Cooking button. In the animation clicking the button starts a timer clicking up, adds a Do this now panel showing the Start all prep work step, shows Coming Up Next with timers counting down to the next steps and updates the full timeline to show local clock times where it previously showed durations from 00:00 upwards." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Here's the &lt;a href="https://claude.ai/share/4acab994-c22b-4ddf-81bd-2f22d947c521"&gt;full Claude transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There was just one notable catch: our dog, Cleo, knows &lt;em&gt;exactly&lt;/em&gt; when her dinner time is, at 6pm sharp. I forgot to mention this to Claude, which had scheduled several key steps colliding with Cleo's meal. I got woofed at. I deserved it.&lt;/p&gt;
&lt;p&gt;To my great surprise, &lt;em&gt;it worked&lt;/em&gt;. I followed the recipe guide to the minute and served up both meals exactly 44 minutes after I started cooking.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/recipe-finished.jpg" alt="A small bowl (a beautiful blue sea textured bowl, made by Natalie Downe) contains a chickpea stew. A larger black bowl has couscous, green beans and blackened cauliflower." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The best way to learn the capabilities of LLMs is to throw tasks at them that may be beyond their abilities and see what happens. In this case I fully expected that something would get forgotten or a detail would be hallucinated and I'd end up scrambling to fix things half way through the process. I was surprised and impressed that it worked so well.&lt;/p&gt;
&lt;p&gt;Some credit for the app idea should go to my fellow hackers at &lt;a href="https://devfort.com/fort/2/"&gt;/dev/fort 2 in 2009&lt;/a&gt;, when we rented Knockbrex Castle in Dumfries, Scotland for a week and attempted to build a cooking timer application for complex meals.&lt;/p&gt;
&lt;h4 id="generating-recipes-from-scratch"&gt;Generating recipes from scratch&lt;/h4&gt;
&lt;p&gt;Most of my other cooking experiments with LLMs have been a whole lot simpler than this: I ask for a recipe, ask for some variations and then cook one of them and see what happens.&lt;/p&gt;
&lt;p&gt;This works remarkably well considering LLMs have no taste buds.&lt;/p&gt;
&lt;p&gt;I've started to think of this as asking LLMs for the &lt;em&gt;average&lt;/em&gt; recipe for a dish, based on all of the recipes they have hoovered up during their training. It turns out the mean version of every guacamole recipe on the internet is a decent guacamole!&lt;/p&gt;
&lt;p&gt;Here's an example of a recipe I tried recently that worked out really well. I was helping Natalie run her ceramic stall at the farmers market and the stall next to us sold excellent dried beans. I've never used dried beans before, so I took a photo of their selection and asked Claude what I could do with them:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/beans-1.jpg" alt="Several bags of tasty looking beans of different varieties and colors" style="width: 48%;" /&gt; &lt;img src="https://static.simonwillison.net/static/2025/beans-2.jpg" alt="More bags of beans." style="width: 48%;" /&gt;&lt;/p&gt;
&lt;p&gt;Identify these beans&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It took a guess at the beans, then I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Get me excited about cooking with these! If I bought two varietiew what could I make&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;"Get me excited" switches Claude into a sort of hype-man mode, which is kind of entertaining:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Oh, you're about to enter the &lt;em&gt;wonderful&lt;/em&gt; world of bean cooking! Let me get you pumped about some killer two-bean combos: [...]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mixed bean salad&lt;/strong&gt; with lemon, olive oil, fresh herbs, cherry tomatoes - light but satisfying [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I replied:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OK Bean salad has me interested - these are dried beans. Give me some salad options I can make that would last a long time in the fridge&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;... and after some back and forth we arrived on the &lt;a href="https://claude.ai/share/c7534766-22d8-481b-bd80-a21abc53f5b2"&gt;recipe in this transcript&lt;/a&gt;, which I cooked the following day (asking plenty of follow-up questions) and thoroughly enjoyed.&lt;/p&gt;
&lt;p&gt;I've done this a bunch of times with a bunch of different recipes across both Claude and ChatGPT and honestly I've not had a notable miss yet. Being able to say "make it vegan" or "I don't have coriander, what can I use instead?" or just "make it tastier" is a really fun way to explore cooking.&lt;/p&gt;
&lt;p&gt;It's also fun to repeat "make it tastier" multiple times to see how absurd you can get.&lt;/p&gt;
&lt;h4 id="i-really-want-someone-to-turn-this-into-a-benchmark-"&gt;I really want someone to turn this into a benchmark!&lt;/h4&gt;
&lt;p&gt;Cooking with LLMs is a lot of fun. There's an opportunity here for a &lt;em&gt;really&lt;/em&gt; neat benchmark: take a bunch of leading models, prompt them for recipes, follow those recipes and taste-test the results!&lt;/p&gt;
&lt;p&gt;The logistics of running this are definitely too much for me to handle myself. I have enough trouble cooking two meals at once, for a solid benchmark you'd ideally have several models serving meals up at the same time to a panel of tasters.&lt;/p&gt;
&lt;p&gt;If someone else wants to try this please let me know how it goes!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cooking"&gt;cooking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/devfort"&gt;devfort&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vision-llms"&gt;vision-llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cooking"/><category term="devfort"/><category term="localstorage"/><category term="tools"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="anthropic"/><category term="claude"/><category term="vision-llms"/><category term="vibe-coding"/></entry><entry><title>Useful patterns for building HTML tools</title><link href="https://simonwillison.net/2025/Dec/10/html-tools/#atom-tag" rel="alternate"/><published>2025-12-10T21:00:59+00:00</published><updated>2025-12-10T21:00:59+00:00</updated><id>https://simonwillison.net/2025/Dec/10/html-tools/#atom-tag</id><summary type="html">
    &lt;p&gt;I've started using the term &lt;strong&gt;HTML tools&lt;/strong&gt; to refer to HTML applications that I've been building which combine HTML, JavaScript, and CSS in a single file and use them to provide useful functionality. I have built &lt;a href="https://tools.simonwillison.net/"&gt;over 150 of these&lt;/a&gt; in the past two years, almost all of them written by LLMs. This article presents a collection of useful patterns I've discovered along the way.&lt;/p&gt;
&lt;p&gt;First, some examples to show the kind of thing I'm talking about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/svg-render?url=https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg"&gt;svg-render&lt;/a&gt;&lt;/strong&gt; renders SVG code to downloadable JPEGs or PNGs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/pypi-changelog?package=llm&amp;amp;compare=0.27...0.27.1"&gt;pypi-changelog&lt;/a&gt;&lt;/strong&gt; lets you generate (and copy to clipboard) diffs between different PyPI package releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/bluesky-thread?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7gzjew3ss2e&amp;amp;view=thread"&gt;bluesky-thread&lt;/a&gt;&lt;/strong&gt; provides a nested view of a discussion thread on Bluesky.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/svg-render?url=https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/svg-render.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of svg-render" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/pypi-changelog?package=llm&amp;amp;compare=0.27...0.27.1" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/pypi-changelog.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of pypi-changelog" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/bluesky-thread?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7gzjew3ss2e&amp;amp;view=thread" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/bluesky-thread.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of bluesky-thread" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;These are some of my recent favorites. I have dozens more like this that I use on a regular basis.&lt;/p&gt;
&lt;p&gt;You can explore my collection on &lt;strong&gt;&lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt;&lt;/strong&gt; - the &lt;a href="https://tools.simonwillison.net/by-month"&gt;by month&lt;/a&gt; view is useful for browsing the entire collection.&lt;/p&gt;
&lt;p&gt;If you want to see the code and prompts, almost all of the examples in this post include a link in their footer to "view source" on GitHub. The GitHub commits usually contain either the prompt itself or a link to the transcript used to create the tool.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#the-anatomy-of-an-html-tool"&gt;The anatomy of an HTML tool&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#prototype-with-artifacts-or-canvas"&gt;Prototype with Artifacts or Canvas&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#switch-to-a-coding-agent-for-more-complex-projects"&gt;Switch to a coding agent for more complex projects&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#load-dependencies-from-cdns"&gt;Load dependencies from CDNs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#host-them-somewhere-else"&gt;Host them somewhere else&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#take-advantage-of-copy-and-paste"&gt;Take advantage of copy and paste&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#build-debugging-tools"&gt;Build debugging tools&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#persist-state-in-the-url"&gt;Persist state in the URL&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#use-localstorage-for-secrets-or-larger-state"&gt;Use localStorage for secrets or larger state&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#collect-cors-enabled-apis"&gt;Collect CORS-enabled APIs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#llms-can-be-called-directly-via-cors"&gt;LLMs can be called directly via CORS&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#don-t-be-afraid-of-opening-files"&gt;Don't be afraid of opening files&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#you-can-offer-downloadable-files-too"&gt;You can offer downloadable files too&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#pyodide-can-run-python-code-in-the-browser"&gt;Pyodide can run Python code in the browser&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#webassembly-opens-more-possibilities"&gt;WebAssembly opens more possibilities&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#remix-your-previous-tools"&gt;Remix your previous tools&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#record-the-prompt-and-transcript"&gt;Record the prompt and transcript&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/#go-forth-and-build"&gt;Go forth and build&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="the-anatomy-of-an-html-tool"&gt;The anatomy of an HTML tool&lt;/h4&gt;
&lt;p&gt;These are the characteristics I have found to be most productive in building tools of this nature:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A single file: inline JavaScript and CSS in a single HTML file means the least hassle in hosting or distributing them, and crucially means you can copy and paste them out of an LLM response.&lt;/li&gt;
&lt;li&gt;Avoid React, or anything with a build step. The problem with React is that JSX requires a build step, which makes everything massively less convenient. I prompt "no react" and skip that whole rabbit hole entirely.&lt;/li&gt;
&lt;li&gt;Load dependencies from a CDN. The fewer dependencies the better, but if there's a well known library that helps solve a problem I'm happy to load it from CDNjs or jsdelivr or similar.&lt;/li&gt;
&lt;li&gt;Keep them small. A few hundred lines means the maintainability of the code doesn't matter too much: any good LLM can read them and understand what they're doing, and rewriting them from scratch with help from an LLM takes just a few minutes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The end result is a few hundred lines of code that can be cleanly copied and pasted into a GitHub repository.&lt;/p&gt;
&lt;h4 id="prototype-with-artifacts-or-canvas"&gt;Prototype with Artifacts or Canvas&lt;/h4&gt;
&lt;p&gt;The easiest way to build one of these tools is to start in ChatGPT or Claude or Gemini. All three have features where they can write a simple HTML+JavaScript application and show it to you directly.&lt;/p&gt;
&lt;p&gt;Claude calls this "Artifacts", ChatGPT and Gemini both call it "Canvas". Claude has the feature enabled by default, ChatGPT and Gemini may require you to toggle it on in their "tools" menus.&lt;/p&gt;
&lt;p&gt;Try this prompt in Gemini or ChatGPT:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build a canvas that lets me paste in JSON and converts it to YAML. No React.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Or this prompt in Claude:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build an artifact that lets me paste in JSON and converts it to YAML. No React.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I always add "No React" to these prompts, because otherwise they tend to build with React, resulting in a file that is harder to copy and paste out of the LLM and use elsewhere. I find that attempts which use React take longer to display (since they need to run a build step) and are more likely to contain crashing bugs for some reason, especially in ChatGPT.&lt;/p&gt;
&lt;p&gt;All three tools have "share" links that provide a URL to the finished application. Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chatgpt.com/canvas/shared/6938e8ece53c8191a2f9d7dfcd090d11"&gt;ChatGPT JSON to YAML Canvas&lt;/a&gt; made with GPT-5.1 Thinking - here's &lt;a href="https://chatgpt.com/share/6938e926-ee14-8006-9678-383b3a8dac78"&gt;the full ChatGPT transcript&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.ai/public/artifacts/61fdecb8-6e3b-4162-a5ab-6720dfe5ed19"&gt;Claude JSON to YAML Artifact&lt;/a&gt; made with Claude Opus 4.5 - here's &lt;a href="https://claude.ai/share/421bacb9-54b4-45b4-b41c-a436bc0ebd53"&gt;the full Claude transcript&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gemini.google.com/share/03c1ac87aa40"&gt;Gemini JSON to YAML Canvas&lt;/a&gt; made with Gemini 3 Pro - here's &lt;a href="https://gemini.google.com/share/1e27a1d8cdca"&gt;the full Gemini transcript&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="switch-to-a-coding-agent-for-more-complex-projects"&gt;Switch to a coding agent for more complex projects&lt;/h4&gt;
&lt;p&gt;Coding agents such as Claude Code and Codex CLI have the advantage that they can test the code themselves while they work on it using tools like Playwright. I often upgrade to one of those when I'm working on something more complicated, like my Bluesky thread viewer tool shown above.&lt;/p&gt;
&lt;p&gt;I also frequently use &lt;strong&gt;asynchronous coding agents&lt;/strong&gt; like Claude Code for web to make changes to existing tools. I shared a video about that in &lt;a href="https://simonwillison.net/2025/Oct/23/claude-code-for-web-video/"&gt;Building a tool to copy-paste share terminal sessions using Claude Code for web&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Claude Code for web and Codex Cloud run directly against my &lt;a href="https://github.com/simonw/tools"&gt;simonw/tools&lt;/a&gt; repo, which means they can publish or upgrade tools via Pull Requests (here are &lt;a href="https://github.com/simonw/tools/pulls?q=is%3Apr+is%3Aclosed"&gt;dozens of examples&lt;/a&gt;) without me needing to copy and paste anything myself.&lt;/p&gt;
&lt;h4 id="load-dependencies-from-cdns"&gt;Load dependencies from CDNs&lt;/h4&gt;
&lt;p&gt;Any time I use an additional JavaScript library as part of my tool I like to load it from a CDN.&lt;/p&gt;
&lt;p&gt;The three major LLM platforms support specific CDNs as part of their Artifacts or Canvas features, so often if you tell them "Use PDF.js" or similar they'll be able to compose a URL to a CDN that's on their allow-list.&lt;/p&gt;
&lt;p&gt;Sometimes you'll need to go and look up the URL on &lt;a href="https://cdnjs.com/"&gt;cdnjs&lt;/a&gt; or &lt;a href="https://www.jsdelivr.com/"&gt;jsDelivr&lt;/a&gt; and paste it into the chat.&lt;/p&gt;
&lt;p&gt;CDNs like these have been around for long enough that I've grown to trust them, especially for URLs that include the package version.&lt;/p&gt;
&lt;p&gt;The alternative to CDNs is to use npm and have a build step for your projects. I find this reduces my productivity at hacking on individual tools and makes it harder to self-host them.&lt;/p&gt;
&lt;h4 id="host-them-somewhere-else"&gt;Host them somewhere else&lt;/h4&gt;
&lt;p&gt;I don't like leaving my HTML tools hosted by the LLM platforms themselves for a couple of reasons. First, LLM platforms tend to run the tools inside a tight sandbox with a lot of restrictions. They're often unable to load data or images from external URLs, and sometimes even features like linking out to other sites are disabled.&lt;/p&gt;
&lt;p&gt;The end-user experience often isn't great either. They show warning messages to new users, often take additional time to load and delight in showing promotions for the platform that was used to create the tool.&lt;/p&gt;
&lt;p&gt;They're also not as reliable as other forms of static hosting. If ChatGPT or Claude are having an outage I'd like to still be able to access the tools I've created in the past.&lt;/p&gt;
&lt;p&gt;Being able to easily self-host is the main reason I like insisting on "no React" and using CDNs for dependencies - the absence of a build step makes hosting tools elsewhere a simple case of copying and pasting them out to some other provider.&lt;/p&gt;
&lt;p&gt;My preferred provider here is &lt;a href="https://docs.github.com/en/pages"&gt;GitHub Pages&lt;/a&gt; because I can paste a block of HTML into a file on github.com and have it hosted on a permanent URL a few seconds later. Most of my tools end up in my &lt;a href="https://github.com/simonw/tools"&gt;simonw/tools&lt;/a&gt; repository which is configured to serve static files at &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="take-advantage-of-copy-and-paste"&gt;Take advantage of copy and paste&lt;/h4&gt;
&lt;p&gt;One of the most useful input/output mechanisms for HTML tools comes in the form of &lt;strong&gt;copy and paste&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I frequently build tools that accept pasted content, transform it in some way and let the user copy it back to their clipboard to paste somewhere else.&lt;/p&gt;
&lt;p&gt;Copy and paste on mobile phones is fiddly, so I frequently include "Copy to clipboard" buttons that populate the clipboard with a single touch.&lt;/p&gt;
&lt;p&gt;Most operating system clipboards can carry multiple formats of the same copied data. That's why you can paste content from a word processor in a way that preserves formatting, but if you paste the same thing into a text editor you'll get the content with formatting stripped.&lt;/p&gt;
&lt;p&gt;These rich copy operations are available in JavaScript paste events as well, which opens up all sorts of opportunities for HTML tools.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/hacker-news-thread-export"&gt;hacker-news-thread-export&lt;/a&gt;&lt;/strong&gt; lets you paste in a URL to a Hacker News thread and gives you a copyable condensed version of the entire thread, suitable for pasting into an LLM to get a useful summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/paste-rich-text"&gt;paste-rich-text&lt;/a&gt;&lt;/strong&gt; lets you copy from a page and paste to get the HTML - particularly useful on mobile where view-source isn't available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/alt-text-extractor"&gt;alt-text-extractor&lt;/a&gt;&lt;/strong&gt; lets you paste in images and then copy out their alt text.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/hacker-news-thread-export" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/hacker-news-thread-export.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of hacker-news-thread-export" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/paste-rich-text" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/paste-rich-text.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of paste-rich-text" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/alt-text-extractor" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/alt-text-extractor.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of alt-text-extractor" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="build-debugging-tools"&gt;Build debugging tools&lt;/h4&gt;
&lt;p&gt;The key to building interesting HTML tools is understanding what's possible. Building custom debugging tools is a great way to explore these options.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/clipboard-viewer"&gt;clipboard-viewer&lt;/a&gt;&lt;/strong&gt; is one of my most useful. You can paste anything into it (text, rich text, images, files) and it will loop through and show you every type of paste data that's available on the clipboard.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/clipboard-viewer.jpg" alt="Clipboard Format Viewer. Paste anywhere on the page (Ctrl+V or Cmd+V). This shows text/rtf with a bunch of weird code, text/plain with some pasted HTML diff and a Clipboard Event Information panel that says Event type: paste, Formats available: text/rtf, text/plain, 0 files reported and 2 clipboard items reported." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This was key to building many of my other tools, because it showed me the invisible data that I could use to bootstrap other interesting pieces of functionality.&lt;/p&gt;
&lt;p&gt;More debugging examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/keyboard-debug"&gt;keyboard-debug&lt;/a&gt;&lt;/strong&gt; shows the keys (and &lt;code&gt;KeyCode&lt;/code&gt; values) currently being held down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/cors-fetch"&gt;cors-fetch&lt;/a&gt;&lt;/strong&gt; reveals if a URL can be accessed via CORS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/exif"&gt;exif&lt;/a&gt;&lt;/strong&gt; displays EXIF data for a selected photo.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/keyboard-debug" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/keyboard-debug.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of keyboard-debug" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/cors-fetch" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/cors-fetch.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of cors-fetch" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/exif" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/exif.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of exif" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="persist-state-in-the-url"&gt;Persist state in the URL&lt;/h4&gt;
&lt;p&gt;HTML tools may not have access to server-side databases for storage but it turns out you can store a &lt;em&gt;lot&lt;/em&gt; of state directly in the URL.&lt;/p&gt;
&lt;p&gt;I like this for tools I may want to bookmark or share with other people.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/icon-editor#cmdiKDIwMSwgNDYsIDg2KSxyZ2IoMjIzLCA0OCwgOTIpLHJnYigzNCwgODAsIDE3OSkscmdiKDIzNywgNTYsIDk1KSxyZ2IoMTgzLCA1MywgOTYpLHJnYigzOCwgMTA3LCAyMTApLHJnYigyMDQsIDY1LCAxMDUpLHJnYigxNzksIDEwMywgMTM2KSxyZ2IoMjMyLCA5NywgMTQ4KSxyZ2IoMzgsIDkxLCAyMDkpLHJnYigzNiwgOTUsIDIwNCkscmdiKDE5NSwgODYsIDEyOSkscmdiKDE3MywgMzEsIDU4KSxyZ2IoMjEyLCA2MSwgMTA2KSxyZ2IoOTIsIDEwNSwgMTg4KSxyZ2IoMjM3LCA3MSwgMTIzKSxyZ2IoMzksIDk2LCAyMTkpLHJnYigyOCwgODYsIDIxMCkscmdiKDIyMywgMjEyLCAzNCkscmdiKDE3MywgMTUzLCAyNikscmdiKDE0NCwgNzksIDI4KSxyZ2IoMjI0LCA1NiwgOTcpLHJnYigxOTYsIDQ4LCA4NSkscmdiKDIyMCwgNTAsIDk4KSxyZ2IoMTY2LCAxMjYsIDI1KSxyZ2IoMjA5LCAxMzAsIDE5KSxyZ2IoMTg3LCAxMTQsIDEzKSxyZ2IoMTQ3LCAxMDQsIDE4KSxyZ2IoMjE2LCA1OCwgODEpLHJnYigxNTIsIDM5LCA2NCkscmdiKDMyLCA3NSwgMTczKSxyZ2IoMTY2LCAxMjYsIDI5KSxyZ2IoMjM3LCAxODAsIDU0KSxyZ2IoMjA0LCAxMzgsIDIyKSxyZ2IoMTgxLCAxMjksIDIzKSxyZ2IoMjM0LCA4NiwgNzYpLHJnYigxOTAsIDY4LCA3NSkscmdiKDI0NSwgODksIDEzNSkscmdiKDIxMywgNjcsIDExMSkscmdiKDE0MSwgMzEsIDU2KSxyZ2IoNzIsIDc5LCAxMTYpLHJnYigxODcsIDE1NCwgNTIpLHJnYigyMDcsIDE3OSwgNzIpLHJnYigyMTAsIDE2MiwgNDMpLHJnYigyMTQsIDE0OSwgMzEpLHJnYigyMzksIDkwLCA4NCkscmdiKDIzNSwgMTMyLCA3NykscmdiKDE4MSwgMTM4LCAyOSkscmdiKDI0NSwgMTI4LCAxNzgpLHJnYigyMTcsIDk5LCAxNDUpLHJnYigxMTYsIDEwNSwgMTIyKSxyZ2IoMjA2LCAxNzYsIDY1KSxyZ2IoMTkxLCAxNjMsIDY0KSxyZ2IoMjA1LCAxNjksIDU4KSxyZ2IoMjM2LCAxNjUsIDQ2KSxyZ2IoMjM3LCA3OSwgODUpLHJnYigyMzUsIDE0NCwgODcpLHJnYigyNDksIDIwMiwgNDUpLHJnYigyMTAsIDE2NiwgMzQpLHJnYigyMjcsIDEwMywgMTYyKSxyZ2IoMjEzLCA5MCwgMTMwKSxyZ2IoNDQsIDQ4LCAxMjMpLHJnYigxMjUsIDg2LCAxNTEpLHJnYigxOTAsIDE2MywgMTA2KSxyZ2IoMTk5LCAxNjYsIDQ4KSxyZ2IoMjAyLCAxNjQsIDU2KSxyZ2IoMjIxLCAxNzAsIDUzKSxyZ2IoMjM0LCAxMzUsIDc1KSxyZ2IoMjQxLCAxNzUsIDc1KSxyZ2IoMjU1LCAyMjIsIDY1KSxyZ2IoMjU0LCAyMjYsIDY5KSxyZ2IoMjM1LCAyMDEsIDQ0KSxyZ2IoNzMsIDEzNywgMjQ3KSxyZ2IoODAsIDE0MywgMjQ4KSxyZ2IoNzksIDEzOSwgMjQzKSxyZ2IoMTM4LCA5MiwgMTc0KSxyZ2IoMTU2LCAxMTMsIDE3NikscmdiKDIwMSwgMTY4LCA2MykscmdiKDIxMSwgMTY5LCA0NikscmdiKDIxNCwgMTcxLCA1NSkscmdiKDIyOCwgMTgyLCA1NikscmdiKDI0MywgMTk1LCA1OCkscmdiKDI0NSwgMjA0LCA2NykscmdiKDI1NSwgMjIxLCA2NykscmdiKDI1NSwgMjI2LCA2OCkscmdiKDE1NCwgMTYyLCAxMzMpLHJnYigyNiwgMTA1LCAyNTUpLHJnYig2OCwgMTI5LCAyNTIpLHJnYig4NywgMTM1LCAyNDQpLHJnYig4MywgMTMxLCAyMzUpLHJnYig4MiwgMTI3LCAyMjYpLHJnYig4NSwgMTMwLCAyMjcpLHJnYig3OSwgMTIyLCAyMTgpLHJnYigxNjcsIDE0NiwgMzIpLHJnYigxNzQsIDEzOCwgMTI0KSxyZ2IoMTMzLCA2OSwgMjA1KSxyZ2IoMTcxLCAxMjAsIDE0NCkscmdiKDIxNSwgMTc2LCA1NykscmdiKDIyMCwgMTc1LCA0OSkscmdiKDIyMywgMTc5LCA1OCkscmdiKDIzNywgMTg4LCA2MCkscmdiKDI0MSwgMTkxLCA1NikscmdiKDIwMCwgMTc2LCAxMDUpLHJnYigxMTIsIDE0MSwgMjAzKSxyZ2IoODQsIDEyNywgMjM1KSxyZ2IoMTE1LCAxMzgsIDE5MSkscmdiKDgyLCAxMDMsIDE3NCkscmdiKDE1OCwgNDEsIDc2KSxyZ2IoMTcwLCA0MywgNjQpLHJnYigxOTAsIDE1NywgNTApLHJnYigyMDMsIDE3NywgNjUpLHJnYigxNjEsIDEwMiwgMTQyKSxyZ2IoMTQxLCA1OSwgMjA5KSxyZ2IoMTgwLCAxMjIsIDE1MSkscmdiKDIyOCwgMTg1LCA1OCkscmdiKDIzMywgMTg2LCA1MikscmdiKDI0MCwgMTg5LCA2NikscmdiKDI1NCwgMjEwLCA2OCkscmdiKDIwMSwgMTkxLCAxMTMpLHJnYigxMzcsIDEzOSwgMTU3KSxyZ2IoMjExLCAxNjIsIDg4KSxyZ2IoMjUwLCAyMDAsIDUwKSxyZ2IoMTc5LCAxMzEsIDIzKSxyZ2IoMTk2LCAxNjUsIDY0KSxyZ2IoMjA1LCAxNzQsIDU0KSxyZ2IoMjA5LCAxNjAsIDU5KSxyZ2IoMTY2LCA5MSwgMTYxKSxyZ2IoMTQyLCA2MCwgMjIzKSxyZ2IoMTk3LCAxMzksIDE1MCkscmdiKDI0MCwgMTk2LCA3MikscmdiKDI1MSwgMjA4LCA2MSkscmdiKDI1NSwgMjI0LCA4MCkscmdiKDI1NSwgMjUwLCA5MikscmdiKDI1NSwgMjM0LCA4OSkscmdiKDI0OSwgMTg2LCA1MSkscmdiKDI1MCwgMTgwLCAzOSkscmdiKDI0MCwgMTY2LCAzNSkscmdiKDIwMiwgMTc0LCA3MikscmdiKDIxNSwgMTY4LCA1MCkscmdiKDIyMiwgMTc1LCA0MykscmdiKDIxMiwgMTY1LCA2OSkscmdiKDE3NCwgMTAzLCAxNjcpLHJnYigxNjAsIDc4LCAyMzQpLHJnYigyMDUsIDE0NiwgMTg0KSxyZ2IoMjQ3LCAyMTgsIDEwOCkscmdiKDI1NSwgMjQ4LCA4NSkscmdiKDI1NSwgMjU1LCAxMDIpLHJnYigyNTUsIDI1NSwgMTIyKSxyZ2IoMjQwLCAyMTAsIDgyKSxyZ2IoMjE0LCAxNTAsIDMxKSxyZ2IoMjI0LCAxNTAsIDI1KSxyZ2IoMTc2LCAxMjEsIDI1KSxyZ2IoMTg5LCAxODMsIDUyKSxyZ2IoMTIyLCA4MCwgMTU4KSxyZ2IoMTkxLCAxNTEsIDEyMikscmdiKDIyOSwgMTc0LCA0MCkscmdiKDIyNSwgMTcyLCA1MSkscmdiKDIyOSwgMTg1LCA1MSkscmdiKDIzNywgMTkwLCA2MCkscmdiKDIwOSwgMTQ2LCAxNjEpLHJnYigxOTUsIDExNywgMjUxKSxyZ2IoMjI1LCAxNTUsIDIzOSkscmdiKDI1NCwgMjI3LCAxODQpLHJnYigyNTUsIDI1NSwgMTE3KSxyZ2IoMjQ5LCAyMzcsIDc2KSxyZ2IoMjA0LCAxNjcsIDU1KSxyZ2IoMTU3LCAxMTUsIDI1KSxyZ2IoMTM1LCA5OCwgMTYpLHJnYigyMDMsIDEyNSwgNTcpLHJnYigxOTgsIDEyNSwgNTMpLHJnYigxNTcsIDExMCwgMTQ0KSxyZ2IoMTQ5LCA4NCwgMTk0KSxyZ2IoMjEyLCAxNTcsIDk0KSxyZ2IoMjMyLCAxODUsIDQ3KSxyZ2IoMjM1LCAxODYsIDYyKSxyZ2IoMjUwLCAyMDQsIDY1KSxyZ2IoMjUzLCAyMzIsIDgxKSxyZ2IoMjQzLCAyMTUsIDE0OCkscmdiKDI0NywgMTgzLCAyMzMpLHJnYigyNDMsIDE2MywgMjUwKSxyZ2IoMTk4LCAxMzgsIDE3NSkscmdiKDE2MCwgMTEzLCA4MikscmdiKDEyNCwgODksIDM3KSxyZ2IoMTU3LCAxMzYsIDM2KSxyZ2IoMjAzLCAxNjQsIDgyKSxyZ2IoMTQ4LCA3MiwgMTg5KSxyZ2IoMTU4LCA4NCwgMjA0KSxyZ2IoMjE3LCAxNjgsIDExNykscmdiKDI1MCwgMjEwLCA2NykscmdiKDI1NSwgMjI5LCA3OCkscmdiKDI1NSwgMjU1LCA5NikscmdiKDI1NSwgMjU1LCA5NCkscmdiKDI0MywgMjE4LCA5NSkscmdiKDE3OCwgMTE4LCAxMDYpLHJnYigxMDMsIDQwLCAxMDIpLHJnYigxODgsIDExMSwgMjcpLHJnYigxODMsIDE1NiwgNTkpLHJnYigyMTUsIDE3NiwgNDgpLHJnYigyMDMsIDE0OCwgOTEpLHJnYigxNjcsIDg5LCAxOTcpLHJnYigxNzgsIDEwMywgMjM1KSxyZ2IoMjM1LCAxOTMsIDE3NSkscmdiKDI1NSwgMjUxLCAxMjQpLHJnYigyNDksIDI0MCwgOTIpLHJnYigyMTMsIDE4NiwgNjApLHJnYigxNjAsIDEyMSwgMjEpLHJnYigxOTEsIDE1NSwgMTA4KSxyZ2IoMjIxLCAxODAsIDQwKSxyZ2IoMjM3LCAxODksIDQ3KSxyZ2IoMjMzLCAxODYsIDk2KSxyZ2IoMjE5LCAxNjIsIDIwNykscmdiKDIzMSwgMTU5LCAyNDkpLHJnYigyMTAsIDE1OCwgMTkxKSxyZ2IoMTY5LCAxMzAsIDc1KSxyZ2IoMTQwLCA5NiwgMTE5KSxyZ2IoMTU1LCA4NSwgMjAwKSxyZ2IoMjA5LCAxNTcsIDExNSkscmdiKDI1NCwgMjI2LCA3MCkscmdiKDI1NSwgMjU1LCA4MCkscmdiKDIzNSwgMjE3LCA3NikscmdiKDE3OCwgMTMzLCA5MSkscmdiKDIwOSwgMTEwLCAxNTEpLHJnYigxNTIsIDExOCwgNTYpLHJnYigxODYsIDExNiwgMTY4KSxyZ2IoMTkzLCAxMjEsIDIzNikscmdiKDIyOSwgMTk1LCAxNjEpLHJnYigxOTcsIDE4MCwgNzUpLHJnYigxOTksIDE1OCwgNzApLHJnYigxOTcsIDE0OCwgMTM2KXxfX19fX19fXzAxX19fX19fX19fX19fX19fMl9fX19fX18zNDVfX19fX182X183OF9fOWFfX19fX2JjZGVfX19fX19fX19fZl9fX2doX2lqa19fbF9fX19fX19fbV9uX19fX19fX19vcHFyc19fX19fX19fdF9fX19fX3VfX192d3h5ejEwX19fMTExMl9fMTNfX19fX19fX18xNDE1MTYxNzE4MTkxYTFiX18xYzFkX19fX19fX19fX19fMWUxZjFnMWgxaTFqMWsxbDFtXzFuMW9fX19fX19fX19fXzFwMXExcjFzMXQxdTF2MXcxeDF5MXpfX19fXzIwMjEyMl9fX19fXzIzMjQyNTI2MjcyODI5MmEyYjJjMmQyZTJmMmcyaDJpMmoya19fX19fMmwybTJuMm8ycDJxMnIyczJ0MnUydjJ3MngyeV9fX19fX19fMnozMDMxMzIzMzM0MzUzNjM3MzgzOTNhM2IzYzNkM2VfX19fX19fX19fM2YzZzNoM2kzajNrM2wzbTNuM28zcDNxM3Izc19fX19fX19fX18zdDN1M3YzdzN4M3kzejQwNDE0MjQzNDQ0NTQ2NDc0OF9fX19fX180OTRhNGI0YzRkNGU0ZjRnNGg0aTRqNGs0bDRtNG5fX180bzRwX19fXzRxNHI0czR0NHU0djR3NHg0eTR6NTA1MTUyX19fX19fX19fXzUzNTQ1NTU2NTc1ODU5NWE1YjVjNWQ1ZV9fX19fXzVmX19fX181ZzVoNWk1ajVrNWw1bTVuNW81cF9fX19fX19fX19fX19fNXE1cjVzNXQ1dTV2NXc1eF9fX19fX19fX19fX19fXzV5NXo2MDYxNjI2MzY0X19fX19fX19fX19fNjVfX19fNjY2NzY4Njk2YV9fX19fX19fX19fX19fX19fX19fNmI2Y19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19f"&gt;icon-editor&lt;/a&gt;&lt;/strong&gt; is a custom 24x24 icon editor I built to help hack on icons for &lt;a href="https://simonwillison.net/2025/Oct/28/github-universe-badge/"&gt;the GitHub Universe badge&lt;/a&gt;. It persists your in-progress icon design in the URL so you can easily bookmark and share it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="use-localstorage-for-secrets-or-larger-state"&gt;Use localStorage for secrets or larger state&lt;/h4&gt;
&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"&gt;localStorage&lt;/a&gt; browser API lets HTML tools store data persistently on the user's device, without exposing that data to the server.&lt;/p&gt;
&lt;p&gt;I use this for larger pieces of state that don't fit comfortably in a URL, or for secrets like API keys which I really don't want anywhere near my server  - even static hosts might have server logs that are outside of my influence.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/word-counter"&gt;word-counter&lt;/a&gt;&lt;/strong&gt; is a simple tool I built to help me write to specific word counts, for things like conference abstract submissions. It uses localStorage to save as you type, so your work isn't lost if you accidentally close the tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/render-markdown"&gt;render-markdown&lt;/a&gt;&lt;/strong&gt; uses the same trick - I sometimes use this one to craft blog posts and I don't want to lose them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/haiku"&gt;haiku&lt;/a&gt;&lt;/strong&gt; is one of a number of LLM demos I've built that request an API key from the user (via the &lt;code&gt;prompt()&lt;/code&gt; function) and then store that in &lt;code&gt;localStorage&lt;/code&gt;. This one uses Claude Haiku to write haikus about what it can see through the user's webcam.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/word-counter" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/word-counter.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of word-counter" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/render-markdown" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/render-markdown.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of render-markdown" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/haiku" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/haiku.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of haiku" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="collect-cors-enabled-apis"&gt;Collect CORS-enabled APIs&lt;/h4&gt;
&lt;p&gt;CORS stands for &lt;a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing"&gt;Cross-origin resource sharing&lt;/a&gt;. It's a relatively low-level detail which controls if JavaScript running on one site is able to fetch data from APIs hosted on other domains.&lt;/p&gt;
&lt;p&gt;APIs that provide open CORS headers are a goldmine for HTML tools. It's worth building a collection of these over time.&lt;/p&gt;
&lt;p&gt;Here are some I like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iNaturalist for fetching sightings of animals, including URLs to photos&lt;/li&gt;
&lt;li&gt;PyPI for fetching details of Python packages&lt;/li&gt;
&lt;li&gt;GitHub because anything in a public repository in GitHub has a CORS-enabled anonymous API for fetching that content from the raw.githubusercontent.com domain, which is behind a caching CDN so you don't need to worry too much about rate limits or feel guilty about adding load to their infrastructure.&lt;/li&gt;
&lt;li&gt;Bluesky for all sorts of operations&lt;/li&gt;
&lt;li&gt;Mastodon has generous CORS policies too, as used by applications like &lt;a href="https://phanpy.social/"&gt;phanpy.social&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GitHub Gists are a personal favorite here, because they let you build apps that can persist state to a permanent Gist through making a cross-origin API call.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/species-observation-map#%7B%22taxonId%22%3A123829%2C%22taxonName%22%3A%22California%20Brown%20Pelican%22%2C%22days%22%3A%2230%22%7D"&gt;species-observation-map&lt;/a&gt;&lt;/strong&gt; uses iNaturalist to show a map of recent sightings of a particular species.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/zip-wheel-explorer?package=llm"&gt;zip-wheel-explorer&lt;/a&gt;&lt;/strong&gt; fetches a &lt;code&gt;.whl&lt;/code&gt; file for a Python package from PyPI, unzips it (in browser memory) and lets you navigate the files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/github-issue-to-markdown?issue=https%3A%2F%2Fgithub.com%2Fsimonw%2Fsqlite-utils%2Fissues%2F657"&gt;github-issue-to-markdown&lt;/a&gt;&lt;/strong&gt; fetches issue details and comments from the GitHub API (including expanding any permanent code links) and turns them into copyable Markdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/terminal-to-html"&gt;terminal-to-html&lt;/a&gt;&lt;/strong&gt; can optionally save the user's converted terminal session to a Gist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/bluesky-quote-finder?post=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7auwt3ma222"&gt;bluesky-quote-finder&lt;/a&gt;&lt;/strong&gt; displays quotes of a specified Bluesky post, which can then be sorted by likes or by time.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/species-observation-map#%7B%22taxonId%22%3A123829%2C%22taxonName%22%3A%22California%20Brown%20Pelican%22%2C%22days%22%3A%2230%22%7D" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/species-observation-map.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of species-observation-map" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/zip-wheel-explorer?package=llm" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/zip-wheel-explorer.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of zip-wheel-explorer" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/github-issue-to-markdown?issue=https%3A%2F%2Fgithub.com%2Fsimonw%2Fsqlite-utils%2Fissues%2F657" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/github-issue-to-markdown.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of github-issue-to-markdown" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/terminal-to-html" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/terminal-to-html.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of terminal-to-html" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/bluesky-quote-finder?post=https%3A%2F%2Fbsky.app%2Fprofile%2Fsimonwillison.net%2Fpost%2F3m7auwt3ma222" style="flex: 1; width: 20%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/bluesky-quote-finder.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of bluesky-quote-finder" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="llms-can-be-called-directly-via-cors"&gt;LLMs can be called directly via CORS&lt;/h4&gt;
&lt;p&gt;All three of OpenAI, Anthropic and Gemini offer JSON APIs that can be accessed via CORS directly from HTML tools.&lt;/p&gt;
&lt;p&gt;Unfortunately you still need an API key, and if you bake that key into your visible HTML anyone can steal it and use to rack up charges on your account.&lt;/p&gt;
&lt;p&gt;I use the &lt;code&gt;localStorage&lt;/code&gt; secrets pattern to store API keys for these services. This sucks from a user experience perspective - telling users to go and create an API key and paste it into a tool is a lot of friction - but it does work.&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/haiku"&gt;haiku&lt;/a&gt;&lt;/strong&gt; uses the Claude API to write a haiku about an image from the user's webcam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/openai-audio-output"&gt;openai-audio-output&lt;/a&gt;&lt;/strong&gt; generates audio speech using OpenAI's GPT-4o audio API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="http://tools.simonwillison.net/gemini-bbox"&gt;gemini-bbox&lt;/a&gt;&lt;/strong&gt; demonstrates Gemini 2.5's ability to return complex shaped image masks for objects in images, see &lt;a href="https://simonwillison.net/2025/Apr/18/gemini-image-segmentation/"&gt;Image segmentation using Gemini 2.5&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/haiku" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/haiku.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of haiku" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/openai-audio-output" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/openai-audio-output.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of openai-audio-output" /&gt;&lt;/a&gt;
  &lt;a href="http://tools.simonwillison.net/gemini-bbox" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/gemini-bbox.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of gemini-bbox" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="don-t-be-afraid-of-opening-files"&gt;Don't be afraid of opening files&lt;/h4&gt;
&lt;p&gt;You don't need to upload a file to a server in order to make use of the &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; element. JavaScript can access the content of that file directly, which opens up a wealth of opportunities for useful functionality.&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/ocr"&gt;ocr&lt;/a&gt;&lt;/strong&gt; is the first tool I built for my collection, described in &lt;a href="https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/"&gt;Running OCR against PDFs and images directly in your browser&lt;/a&gt;. It uses &lt;code&gt;PDF.js&lt;/code&gt; and &lt;code&gt;Tesseract.js&lt;/code&gt; to allow users to open a PDF in their browser which it then converts to an image-per-page and runs through OCR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/social-media-cropper"&gt;social-media-cropper&lt;/a&gt;&lt;/strong&gt; lets you open (or paste in) an existing image and then crop it to common dimensions needed for different social media platforms - 2:1 for Twitter and LinkedIn, 1.4:1 for Substack etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/ffmpeg-crop"&gt;ffmpeg-crop&lt;/a&gt;&lt;/strong&gt; lets you open and preview a video file in your browser, drag a crop box within it and then copy out the &lt;code&gt;ffmpeg&lt;/code&gt; command needed to produce a cropped copy on your own machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/ocr" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/ocr.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of ocr" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/social-media-cropper" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/social-media-cropper.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of social-media-cropper" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/ffmpeg-crop" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/ffmpeg-crop.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of ffmpeg-crop" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="you-can-offer-downloadable-files-too"&gt;You can offer downloadable files too&lt;/h4&gt;
&lt;p&gt;An HTML tool can generate a file for download without needing help from a server.&lt;/p&gt;
&lt;p&gt;The JavaScript library ecosystem has a huge range of packages for generating files in all kinds of useful formats.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/svg-render"&gt;svg-render&lt;/a&gt;&lt;/strong&gt; lets the user download the PNG or JPEG rendered from an SVG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/social-media-cropper"&gt;social-media-cropper&lt;/a&gt;&lt;/strong&gt; does the same for cropped images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/open-sauce-2025"&gt;open-sauce-2025&lt;/a&gt;&lt;/strong&gt; is my alternative schedule for a conference that includes a downloadable ICS file for adding the schedule to your calendar. See &lt;a href="https://simonwillison.net/2025/Jul/17/vibe-scraping/"&gt;Vibe scraping and vibe coding a schedule app for Open Sauce 2025 entirely on my phone&lt;/a&gt; for more on that project.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/svg-render" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/svg-render.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of svg-render" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/social-media-cropper" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/social-media-cropper.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of social-media-cropper" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/open-sauce-2025" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/open-sauce-2025.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of open-sauce-2025" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="pyodide-can-run-python-code-in-the-browser"&gt;Pyodide can run Python code in the browser&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://pyodide.org/"&gt;Pyodide&lt;/a&gt; is a distribution of Python that's compiled to WebAssembly and designed to run directly in browsers. It's an engineering marvel and one of the most underrated corners of the Python world.&lt;/p&gt;
&lt;p&gt;It also cleanly loads from a CDN, which means there's no reason not to use it in HTML tools!&lt;/p&gt;
&lt;p&gt;Even better, the Pyodide project includes &lt;a href="https://github.com/pyodide/micropip"&gt;micropip&lt;/a&gt; - a mechanism that can load extra pure-Python packages from PyPI via CORS.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/pyodide-bar-chart"&gt;pyodide-bar-chart&lt;/a&gt;&lt;/strong&gt; demonstrates running Pyodide, Pandas and matplotlib to render a bar chart directly in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/numpy-pyodide-lab"&gt;numpy-pyodide-lab&lt;/a&gt;&lt;/strong&gt; is an experimental interactive tutorial for Numpy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/apsw-query"&gt;apsw-query&lt;/a&gt;&lt;/strong&gt; demonstrates the &lt;a href="https://github.com/rogerbinns/apsw"&gt;APSW SQLite library&lt;/a&gt;  running in a browser, using it to show EXPLAIN QUERY plans for SQLite queries.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/pyodide-bar-chart" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/pyodide-bar-chart.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of pyodide-bar-chart" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/numpy-pyodide-lab" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/numpy-pyodide-lab.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of numpy-pyodide-lab" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/apsw-query" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/apsw-query.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of apsw-query" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="webassembly-opens-more-possibilities"&gt;WebAssembly opens more possibilities&lt;/h4&gt;
&lt;p&gt;Pyodide is possible thanks to WebAssembly. WebAssembly means that a vast collection of software originally written in other languages can now be loaded in HTML tools as well.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://squoosh.app/"&gt;Squoosh.app&lt;/a&gt; was the first example I saw that convinced me of the power of this pattern - it makes several best-in-class image compression libraries available directly in the browser.&lt;/p&gt;
&lt;p&gt;I've used WebAssembly for a few of my own tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/ocr"&gt;ocr&lt;/a&gt;&lt;/strong&gt; uses the pre-existing &lt;a href="https://tesseract.projectnaptha.com/"&gt;Tesseract.js&lt;/a&gt; WebAssembly port of the Tesseract OCR engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/sloccount"&gt;sloccount&lt;/a&gt;&lt;/strong&gt; is a port of David Wheeler's Perl and C &lt;a href="https://dwheeler.com/sloccount/"&gt;SLOCCount&lt;/a&gt; utility to the browser, using a big ball of WebAssembly duct tape. &lt;a href="https://simonwillison.net/2025/Oct/22/sloccount-in-webassembly/"&gt;More details here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/micropython"&gt;micropython&lt;/a&gt;&lt;/strong&gt; is my experiment using &lt;a href="https://www.npmjs.com/package/@micropython/micropython-webassembly-pyscript"&gt;@micropython/micropython-webassembly-pyscript&lt;/a&gt; from NPM to run Python code with a smaller initial download than Pyodide.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="display: flex; width: 100%; gap: 20px; margin-bottom: 1em;"&gt;
  &lt;a href="https://tools.simonwillison.net/ocr" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/ocr.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of ocr" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/sloccount" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/sloccount.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of sloccount" /&gt;&lt;/a&gt;
  &lt;a href="https://tools.simonwillison.net/micropython" style="flex: 1; width: 30%; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2025/html-tools/micropython.jpg" loading="lazy" style="width: 100%; height: auto; object-fit: cover;" alt="screenshot of micropython" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;h4 id="remix-your-previous-tools"&gt;Remix your previous tools&lt;/h4&gt;
&lt;p&gt;The biggest advantage of having a single public collection of 100+ tools is that it's easy for my LLM assistants to recombine them in interesting ways.&lt;/p&gt;
&lt;p&gt;Sometimes I'll copy and paste a previous tool into the context, but when I'm working with a coding agent I can reference them by name - or tell the agent to search for relevant examples before it starts work.&lt;/p&gt;
&lt;p&gt;The source code of any working tool doubles as clear documentation of how something can be done, including patterns for using editing libraries. An LLM with one or two existing tools in their context is much more likely to produce working code.&lt;/p&gt;
&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://tools.simonwillison.net/pypi-changelog"&gt;pypi-changelog&lt;/a&gt;&lt;/strong&gt; by telling Claude Code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Look at the pypi package explorer tool&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then, after it had found and read the source code for &lt;a href="https://tools.simonwillison.net/zip-wheel-explorer"&gt;zip-wheel-explorer&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build a new tool pypi-changelog.html which uses the PyPI API to get the wheel URLs of all available versions of a package, then it displays them in a list where each pair has a "Show changes" clickable in between them - clicking on that fetches the full contents of the wheels and displays a nicely rendered diff representing the difference between the two, as close to a standard diff format as you can get with JS libraries from CDNs, and when that is displayed there is a "Copy" button which copies that diff to the clipboard&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://gistpreview.github.io/?9b48fd3f8b99a204ba2180af785c89d2"&gt;the full transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;See &lt;a href="https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/"&gt;Running OCR against PDFs and images directly in your browser&lt;/a&gt; for another detailed example of remixing tools to create something new.&lt;/p&gt;
&lt;h4 id="record-the-prompt-and-transcript"&gt;Record the prompt and transcript&lt;/h4&gt;
&lt;p&gt;I like keeping (and publishing) records of everything I do with LLMs, to help me grow my skills at using them over time.&lt;/p&gt;
&lt;p&gt;For HTML tools I built by chatting with an LLM platform directly I use the "share" feature for those platforms.&lt;/p&gt;
&lt;p&gt;For Claude Code or Codex CLI or other coding agents I copy and paste the full transcript from the terminal into my &lt;a href="https://tools.simonwillison.net/terminal-to-html"&gt;terminal-to-html&lt;/a&gt; tool and share that using a Gist.&lt;/p&gt;
&lt;p&gt;In either case I include links to those transcripts in the commit message when I save the finished tool to my repository. You can see those &lt;a href="https://tools.simonwillison.net/colophon"&gt;in my tools.simonwillison.net colophon&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="go-forth-and-build"&gt;Go forth and build&lt;/h4&gt;
&lt;p&gt;I've had &lt;em&gt;so much fun&lt;/em&gt; exploring the capabilities of LLMs in this way over the past year and a half, and building tools in this way has been invaluable in helping me understand both the potential for building tools with HTML and the capabilities of the LLMs that I'm building them with.&lt;/p&gt;
&lt;p&gt;If you're interested in starting your own collection I highly recommend it! All you need to get started is a free GitHub repository with GitHub Pages enabled (Settings -&amp;gt; Pages -&amp;gt; Source -&amp;gt; Deploy from a branch -&amp;gt; main) and you can start copying in &lt;code&gt;.html&lt;/code&gt; pages generated in whatever manner you like.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;strong&gt;Bonus transcript&lt;/strong&gt;: Here's &lt;a href="http://gistpreview.github.io/?1b8cba6a8a21110339cbde370e755ba0"&gt;how I used Claude Code&lt;/a&gt; and &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; to add the screenshots to this post.&lt;/small&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/definitions"&gt;definitions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="definitions"/><category term="github"/><category term="html"/><category term="javascript"/><category term="localstorage"/><category term="projects"/><category term="tools"/><category term="ai"/><category term="webassembly"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="vibe-coding"/><category term="coding-agents"/><category term="claude-code"/></entry><entry><title>Chrome Prompt Playground</title><link href="https://simonwillison.net/2024/Jul/3/chrome-prompt-playground/#atom-tag" rel="alternate"/><published>2024-07-03T17:11:02+00:00</published><updated>2024-07-03T17:11:02+00:00</updated><id>https://simonwillison.net/2024/Jul/3/chrome-prompt-playground/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/chrome-prompt-playground"&gt;Chrome Prompt Playground&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Google Chrome Canary is currently shipping an experimental on-device LLM, in the form of Gemini Nano. You can access it via the new &lt;code&gt;window.ai&lt;/code&gt; API, after first enabling the "Prompt API for Gemini Nano" experiment in &lt;code&gt;chrome://flags&lt;/code&gt; (and then waiting an indeterminate amount of time for the ~1.7GB model file to download - I eventually spotted it in &lt;code&gt;~/Library/Application Support/Google/Chrome Canary/OptGuideOnDeviceModel&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I got Claude 3.5 Sonnet to build me this playground interface for experimenting with the model. You can execute prompts, stream the responses and all previous prompts and responses are stored in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/playground-greet.gif" alt="Animated GIF demo. The prompt is Show two greetings each in French and Spanish - on clicking the button the result streams in:  French Bonjour! Bienvenue!, Spanish Hola!, Bienvenido! Scrolling down reveals the stored history, and clicking delete on that prompt removes it from the page." width="500" class="blogmark-image"&gt;&lt;/p&gt;
&lt;p&gt;Here's the &lt;a href="https://gist.github.com/simonw/e62440114960bc98f200eb3d92593896"&gt;full Sonnet transcript&lt;/a&gt;, and the &lt;a href="https://github.com/simonw/tools/blob/be05fc38ea600bc65c6a293d5d69d0999e77be10/chrome-prompt-playground.html"&gt;final source code&lt;/a&gt; for the app.&lt;/p&gt;
&lt;p&gt;The best documentation I've found for the new API is is &lt;a href="https://github.com/explainers-by-googlers/prompt-api"&gt;explainers-by-googlers/prompt-api&lt;/a&gt; on GitHub.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;&lt;/p&gt;



</summary><category term="chrome"/><category term="google"/><category term="localstorage"/><category term="projects"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude"/><category term="gemini"/></entry><entry><title>How I make annotated presentations</title><link href="https://simonwillison.net/2023/Aug/6/annotated-presentations/#atom-tag" rel="alternate"/><published>2023-08-06T17:15:33+00:00</published><updated>2023-08-06T17:15:33+00:00</updated><id>https://simonwillison.net/2023/Aug/6/annotated-presentations/#atom-tag</id><summary type="html">
    &lt;p&gt;Giving a talk is a lot of work. I go by a rule of thumb I learned from &lt;a href="https://en.wikipedia.org/wiki/Damian_Conway"&gt;Damian Conway&lt;/a&gt;: a minimum of ten hours of preparation for every one hour spent on stage.&lt;/p&gt;
&lt;p&gt;If you're going to put that much work into something, I think it's worth taking steps to maximize the value that work produces - both for you and for your audience.&lt;/p&gt;
&lt;p&gt;One of my favourite ways of getting "paid" for a talk is when the event puts in the work to produce a really good video of that talk, and then shares that video online. &lt;a href="https://2023.northbaypython.org"&gt;North Bay Python&lt;/a&gt; is a fantastic example of an event that does this well: they team up with &lt;a href="https://nextdayvideo.com"&gt;Next Day Video&lt;/a&gt; and &lt;a href="https://whitecoatcaptioning.com"&gt;White Coat Captioning&lt;/a&gt; and have talks professionally recorded, captioned and uploaded to YouTube within 24 hours of the talk being given.&lt;/p&gt;
&lt;p&gt;Even with that quality of presentation, I don't think a video on its own is enough. My most recent talk was 40 minutes long - I'd love people to watch it, but I myself watch very few 40m long YouTube videos each year.&lt;/p&gt;
&lt;p&gt;So I like to publish my talks with a text and image version of the talk that can provide as much of the value as possible to people who don't have the time or inclination to sit through a 40m talk (or 20m if you run it at 2x speed, which I do for many of the talks I watch myself).&lt;/p&gt;
&lt;h4&gt;Annotated presentations&lt;/h4&gt;
&lt;p&gt;My preferred format for publishing these documents is as an &lt;em&gt;annotated presentation&lt;/em&gt; - a single document (no clicking "next" dozens of times) combining key slides from the talk with custom written text to accompany each one, plus additional links and resources.&lt;/p&gt;
&lt;p&gt;Here's my most recent example: &lt;a href="https://simonwillison.net/2023/Aug/3/weird-world-of-llms/"&gt;Catching up on the weird world of LLMs&lt;/a&gt;, from North Bay Python last week.&lt;/p&gt;
&lt;p&gt;More examples (see also my &lt;a href="https://simonwillison.net/tags/annotated-talks/"&gt;annotated-talks tag&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2023/May/2/prompt-injection-explained/"&gt;Prompt injection explained, with video, slides, and a transcript&lt;/a&gt; for a LangChain webinar in May 2023.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2022/Nov/26/productivity/"&gt;Coping strategies for the serial project hoarder&lt;/a&gt; for DjangoCon US 2022.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2021/Nov/4/publish-open-source-python-library/"&gt;How to build, test and publish an open source Python library&lt;/a&gt; for PyGotham 2021&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2021/Feb/7/video/"&gt;Video introduction to Datasette and sqlite-utils&lt;/a&gt; for FOSDEM February 2021&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2021/Jul/22/small-data/"&gt;Datasette—an ecosystem of tools for working with small data&lt;/a&gt; for PyGotham 2020.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2020/Nov/14/personal-data-warehouses/"&gt;Personal Data Warehouses: Reclaiming Your Data&lt;/a&gt; for the GitHub OCTO speaker series in November 2020.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://static.simonwillison.net/static/2010/redis-tutorial/"&gt;Redis tutorial&lt;/a&gt; for NoSQL Europe 2010 (my first attempt at this format).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don't tend to write a detailed script for my talks in advance. If I did, I might use that as a starting point, but I usually prepare the outline of the talk and then give it off-the-cuff on the day. I find this fits my style (best described as "enthusiastic rambling") better.&lt;/p&gt;
&lt;p&gt;Instead, I'll assemble notes for each slide from re-watching the video after it has been released.&lt;/p&gt;
&lt;p&gt;I don't just cover the things I said in the the talk - I'll also add additional context, and links to related resources. The annotated presentation isn't just for people who didn't watch the talk, it's aimed at providing extra context for people who did watch it as well.&lt;/p&gt;
&lt;h4&gt;A custom tool for building annotated presentations&lt;/h4&gt;
&lt;p&gt;For this most recent talk I finally built something I've been wanting for &lt;em&gt;years&lt;/em&gt;: a custom tool to help me construct the annotated presentation as quickly as possible.&lt;/p&gt;
&lt;p&gt;Annotated presentations look deceptively simple: each slide is an image and one or two paragraphs of text.&lt;/p&gt;
&lt;p&gt;There are a few extra details though:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The images really need good &lt;code&gt;alt=&lt;/code&gt; text - a big part of the information in the presentation is conveyed by those images, so they need to have good descriptions both for screen reader users and to index in search engines / for retrieval augmented generation.&lt;/li&gt;
&lt;li&gt;Presentations might have dozens of slides - just assembling the image tags in the correct order can be a frustrating task.&lt;/li&gt;
&lt;li&gt;For editing the annotations I like to use Markdown, as it's quicker to write than HTML. Making this as easy as possible encourages me to add more links, bullet points and code snippets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of my favourite use-cases for tools like ChatGPT is to quickly create one-off custom tools. This was a perfect fit for that.&lt;/p&gt;
&lt;p&gt;You can see the tool I create here: &lt;a href="https://til.simonwillison.net/tools/annotated-presentations"&gt;Annotated presentation creator&lt;/a&gt; (&lt;a href="https://github.com/simonw/til/blob/main/templates/pages/tools/annotated-presentations.html"&gt;source code here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The first step is to export the slides as images, being sure to have filenames which sort alphabetically in the correct order. I use Apple Keynote for my slides and it has an "Export" feature which does this for me.&lt;/p&gt;
&lt;p&gt;Next, open those images using the annotation tool.&lt;/p&gt;
&lt;p&gt;The tool is written in JavaScript and works entirely in your browser - it asks you to select images but doesn't actually upload them to a server, just displays them directly inline in the page.&lt;/p&gt;
&lt;p&gt;Anything you type in a &lt;code&gt;textarea&lt;/code&gt; as work-in-progress will be saved to &lt;code&gt;localStorage&lt;/code&gt;, so a browser crash or restart shouldn't lose any of your work.&lt;/p&gt;
&lt;p&gt;It uses &lt;a href="https://tesseract.projectnaptha.com/"&gt;Tesseract.js&lt;/a&gt; to run OCR against your images, providing a starting point for the &lt;code&gt;alt=&lt;/code&gt; attributes for each slide.&lt;/p&gt;
&lt;p&gt;Annotations can be entered in Markdown and are rendered to HTML as a live preview using the &lt;a href="https://marked.js.org/"&gt;Marked&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;Finally, it offers a templating mechanism for the final output, which works using JavaScript template literals. So once you've finished editing the &lt;code&gt;alt=&lt;/code&gt; text and writing the annotations, click "Execute template" at the bottom of the page and copy out the resulting HTML.&lt;/p&gt;
&lt;p&gt;Here's an animated GIF demo of the tool in action:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/annotated-presentation-creator.gif" alt="Animated demo of the tool. I load 90 images, each one of which becomes a slide. Then I click the OCR button and it starts populating the alt textareas with OCR text from the slides. I type some markdown into an annotation box, then scroll to the bottom and click the Execute template button to get back the final HTML." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p id="chatgpt-sessions"&gt;I ended up putting this together with the help of multiple different ChatGPT sessions. You can see those here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://chat.openai.com/share/61cd85f6-7002-4676-b204-0349a723232a"&gt;HTML and JavaScript in a single document to create an app that lets me do the following...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chat.openai.com/share/5218799e-0423-49ad-88ba-c72ee27e3fe3"&gt;JavaScript and HTML app on one page. User can select multiple image files on their own computer...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chat.openai.com/share/7867657b-aa29-4ad0-8ab3-1d353c29a224"&gt;JavaScript that runs once every 1s and builds a JavaScript object of every textarea on the page where the key is the name= attribute of that textarea and the value is its current contents. That whole object is then stored in localStorage in a key called savedTextAreas...&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chat.openai.com/share/4e6fd644-de57-4597-a1cc-412483c2adf3"&gt;Write a JavaScript function like this: executeTemplates(template, arrayOfObjects)...&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Cleaning up the transcript with Claude&lt;/h4&gt;
&lt;p&gt;Since the video was already up on YouTube when I started writing the annotations, I decided to see if I could get a head start on writing them using the YouTube generated transcript.&lt;/p&gt;
&lt;p&gt;I used my &lt;a href="https://simonwillison.net/2022/Sep/30/action-transcription/"&gt;Action Transcription&lt;/a&gt; tool to extract the transcript, but it was pretty low quality - you can see &lt;a href="https://gist.github.com/simonw/3d8a335244711c675c456db147aa05fa"&gt;a copy of it here&lt;/a&gt;. A sample:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;okay hey everyone it's uh really
exciting to be here so yeah I call this
court talk catching up on the weird
world of llms I'm going to try and give
you the last few years of of llm
developments in 35 minutes this is
impossible so uh hopefully I'll at least
give you a flavor of some of the weirder
corners of the space because the thing
about language models is the more I look
at the more I think they're practically
interesting any particular aspect of
them anything at all if you zoom in
there are just more questions there are
just more unknowns about it there are
more interesting things to get into lots
of them are deeply disturbing and
unethical lots of them are fascinating
it's um I've called it um it's it's
impossible to tear myself away from this
I I just keep on keep on finding new
aspects of it that are interesting
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's basically one big run-on sentence, with no punctuation, little capitalization and lots of umms and ahs.&lt;/p&gt;
&lt;p&gt;Anthropic's &lt;a href="https://claude.ai"&gt;Claude 2&lt;/a&gt; was &lt;a href="https://www.anthropic.com/index/claude-2"&gt;released last month&lt;/a&gt; and supports up to 100,000 tokens per prompt - a huge improvement on ChatGPT (4,000) and GPT-4 (8,000). I decided to see if I could use that to clean up my transcript.&lt;/p&gt;
&lt;p&gt;I pasted it into Claude and tried a few prompts... until I hit upon this one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reformat this transcript into paragraphs and sentences, fix the capitalization and make very light edits such as removing ums&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/claude-transcript.jpg" alt="Claude interface: Taming Large Language Models. I have pasted in a paste.txt file with 42KB of data, then prompted it to reformat. It outputs Here is the reformatted transcript: followed by that transcript." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This worked really, really well! Here's the first paragraph it produced, based on the transcript I show above:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Okay everyone, it's really exciting to be here. Yeah I call this talk "Catching Up on the Weird World of LLMs." I'm going to try and give you the last few years of LLMs developments in 35 minutes. This is impossible, so hopefully I'll at least give you a flavor of some of the weirder corners of the space. The thing about language models is the more I look at them, the more I think they're practically interesting. Focus on any particular aspect, and there are just more questions, more unknowns, more interesting things to get into.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Note that I said "fractally interesting", not "practically interesting" - but that error was there in the YouTube transcript, so Claude picked it up from there.&lt;/p&gt;
&lt;p&gt;Here's the &lt;a href="https://gist.github.com/simonw/f6d83d69cca018c07b58aaadfb4c918c"&gt;full generated transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's really impressive! At one point it even turns my dialogue into a set of bullet points:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Today the best are ChatGPT (aka GPT-3.5 Turbo), GPT-4 for capability, and Claude 2 which is free. Google has PaLM 2 and Bard. Llama and Claude are from Anthropic, a splinter of OpenAI focused on ethics. Google and Meta are the other big players.&lt;/p&gt;
&lt;p&gt;Some tips:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI models cutoff at September 2021 training data. Anything later isn't in there. This reduces issues like recycling their own text.&lt;/li&gt;
&lt;li&gt;Claude and Palm have more recent data, so I'll use them for recent events.&lt;/li&gt;
&lt;li&gt;Always consider context length. GPT has 4,000 tokens, GPT-4 has 8,000, Claude 100,000.&lt;/li&gt;
&lt;li&gt;If a friend who read the Wikipedia article could answer my question, I'm confident feeding it in directly. The more obscure, the more likely pure invention.&lt;/li&gt;
&lt;li&gt;Avoid superstitious thinking. Long prompts that "always work" are usually mostly pointless.&lt;/li&gt;
&lt;li&gt;Develop an immunity to hallucinations. Notice signs and check answers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Compare that to &lt;a href="https://gist.github.com/simonw/3d8a335244711c675c456db147aa05fa#file-transcription-txt-L327-L469"&gt;my rambling original&lt;/a&gt; to see quite how much of an improvement this is.&lt;/p&gt;
&lt;p&gt;But, all of that said... I specified "make very light edits" and it clearly did a whole lot more than just that.&lt;/p&gt;
&lt;p&gt;I didn't use the Claude version directly. Instead, I copied and pasted chunks of it into my annotation tool that made the most sense, then directly edited them to better fit what I was trying to convey.&lt;/p&gt;
&lt;p&gt;As with so many things in LLM/AI land: a significant time saver, but no silver bullet.&lt;/p&gt;
&lt;h4&gt;For workshops, publish the handout&lt;/h4&gt;
&lt;p&gt;I took the Software Carpentries &lt;a href="https://carpentries.org/become-instructor/"&gt;instructor training&lt;/a&gt; a few years ago, which was a really great experience.&lt;/p&gt;
&lt;p&gt;A key idea I got from that is that a great way to run a workshop is to prepare an extensive, detailed handout in advance - and then spend the actual workshop time working through that handout yourself, at a sensible pace, in a way that lets the attendees follow along.&lt;/p&gt;
&lt;p&gt;A bonus of this approach is that it forces you to put together a really high quality handout which you can distribute after the event.&lt;/p&gt;
&lt;p&gt;I used this approach for the 3 hour workshop I ran at PyCon US 2023: &lt;a href="https://datasette.io/tutorials/data-analysis"&gt;Data analysis with SQLite and Python&lt;/a&gt;. I turned that into a new official tutorial on the Datasette website, accompanied by the video but also useful for people who don't want to spend three hours watching me talk!&lt;/p&gt;
&lt;h4&gt;More people should do this&lt;/h4&gt;
&lt;p&gt;I'm writing this in the hope that I can inspire more people to give their talks this kind of treatment. It's not a zero amount of work - it takes me 2-3 hours any time I do this - but it greatly increases the longevity of the talk and ensures that the work I've already put into it provides maximum value, both to myself (giving talks is partly a selfish act!) and to the people I want to benefit from it.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/alt-text"&gt;alt-text&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ocr"&gt;ocr&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/speaking"&gt;speaking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/my-talks"&gt;my-talks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-talks"&gt;annotated-talks&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="alt-text"/><category term="localstorage"/><category term="ocr"/><category term="projects"/><category term="speaking"/><category term="my-talks"/><category term="tools"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="annotated-talks"/></entry><entry><title>Dillinger</title><link href="https://simonwillison.net/2017/Oct/8/dillinger/#atom-tag" rel="alternate"/><published>2017-10-08T18:38:40+00:00</published><updated>2017-10-08T18:38:40+00:00</updated><id>https://simonwillison.net/2017/Oct/8/dillinger/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dillinger.io/"&gt;Dillinger&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I really like this online Markdown editor. It has source syntax highlighting, live previews of the generated HTML and it constantly syncs to localStorage so you won’t lose your work if you accidentally shut your browser window. The code is also available open source on GitHub.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&lt;/a&gt;&lt;/p&gt;



</summary><category term="localstorage"/><category term="markdown"/></entry><entry><title>Firefox 3.5 for developers</title><link href="https://simonwillison.net/2009/Jun/30/firefox/#atom-tag" rel="alternate"/><published>2009-06-30T18:08:34+00:00</published><updated>2009-06-30T18:08:34+00:00</updated><id>https://simonwillison.net/2009/Jun/30/firefox/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.mozilla.org/en/Firefox_3.5_for_developers"&gt;Firefox 3.5 for developers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
It’s out today, and the feature list is huge. Highlights include HTML 5 drag ’n’ drop, audio and video elements, offline resources, downloadable fonts, text-shadow, CSS transforms with -moz-transform, localStorage, geolocation, web workers, trackpad swipe events, native JSON, cross-site HTTP requests, text API for canvas, defer attribute for the script element and TraceMonkey for better JS performance!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/audio"&gt;audio&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/canvas"&gt;canvas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/crossdomain"&gt;crossdomain&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csstransforms"&gt;csstransforms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dragndrop"&gt;dragndrop&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox"&gt;firefox&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox35"&gt;firefox35&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fonts"&gt;fonts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geolocation"&gt;geolocation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html5"&gt;html5&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mozilla"&gt;mozilla&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/offlineresources"&gt;offlineresources&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/textshadow"&gt;textshadow&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tracemonkey"&gt;tracemonkey&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/video"&gt;video&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;&lt;/p&gt;



</summary><category term="audio"/><category term="browsers"/><category term="canvas"/><category term="crossdomain"/><category term="csstransforms"/><category term="dragndrop"/><category term="firefox"/><category term="firefox35"/><category term="fonts"/><category term="geolocation"/><category term="html5"/><category term="javascript"/><category term="json"/><category term="localstorage"/><category term="mozilla"/><category term="offlineresources"/><category term="performance"/><category term="textshadow"/><category term="tracemonkey"/><category term="video"/><category term="webworkers"/></entry></feed>