<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: site-upgrades</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/site-upgrades.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-03-23T02:13:13+00:00</updated><author><name>Simon Willison</name></author><entry><title>Beats now have notes</title><link href="https://simonwillison.net/2026/Mar/23/beats-now-have-notes/#atom-tag" rel="alternate"/><published>2026-03-23T02:13:13+00:00</published><updated>2026-03-23T02:13:13+00:00</updated><id>https://simonwillison.net/2026/Mar/23/beats-now-have-notes/#atom-tag</id><summary type="html">
    &lt;p&gt;Last month I &lt;a href="https://simonwillison.net/2026/Feb/20/beats/"&gt;added a feature I call beats&lt;/a&gt; to this blog, pulling in some of my other content from &lt;a href="https://simonwillison.net/elsewhere/"&gt;external sources&lt;/a&gt; and including it on the homepage, search and various archive pages on the site.&lt;/p&gt;
&lt;p&gt;On any given day these frequently outnumber my regular posts. They were looking a little bit thin and were lacking any form of explanation beyond a link, so I've added the ability to annotate them with a "note" which now shows up as part of their display.&lt;/p&gt;
&lt;p&gt;Here's what that looks like &lt;a href="https://simonwillison.net/2026/Mar/22/"&gt;for the content I published yesterday&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img class="blogmark-image" style="width:80%" src="https://static.simonwillison.net/static/2026/beats-notes.jpg" alt="Screenshot of part of my blog homepage showing four &amp;quot;beats&amp;quot; entries from March 22, 2026, each tagged as RESEARCH or TOOL, with titles like &amp;quot;PCGamer Article Performance Audit&amp;quot; and &amp;quot;DNS Lookup&amp;quot;, now annotated with short descriptive notes explaining the context behind each linked item."&gt;&lt;/p&gt;
&lt;p&gt;I've also updated the &lt;a href="https://simonwillison.net/atom/everything/"&gt;/atom/everything/&lt;/a&gt; Atom feed to include any beats that I've attached notes to.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/atom"&gt;atom&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="atom"/><category term="blogging"/><category term="site-upgrades"/></entry><entry><title>Writing about Agentic Engineering Patterns</title><link href="https://simonwillison.net/2026/Feb/23/agentic-engineering-patterns/#atom-tag" rel="alternate"/><published>2026-02-23T17:43:02+00:00</published><updated>2026-02-23T17:43:02+00:00</updated><id>https://simonwillison.net/2026/Feb/23/agentic-engineering-patterns/#atom-tag</id><summary type="html">
    &lt;p&gt;I've started a new project to collect and document &lt;strong&gt;&lt;a href="https://simonwillison.net/guides/agentic-engineering-patterns/"&gt;Agentic Engineering Patterns&lt;/a&gt;&lt;/strong&gt; - coding practices and patterns to help get the best results out of this new era of coding agent development we find ourselves entering.&lt;/p&gt;
&lt;p&gt;I'm using &lt;strong&gt;Agentic Engineering&lt;/strong&gt; to refer to building software using coding agents - tools like Claude Code and OpenAI Codex, where the defining feature is that they can both generate and &lt;em&gt;execute&lt;/em&gt; code - allowing them to test that code and iterate on it independently of turn-by-turn guidance from their human supervisor.&lt;/p&gt;
&lt;p&gt;I think of &lt;strong&gt;vibe coding&lt;/strong&gt; using its &lt;a href="https://simonwillison.net/2025/Mar/19/vibe-coding/"&gt;original definition&lt;/a&gt; of coding where you pay no attention to the code at all, which today is often associated with non-programmers using LLMs to write code.&lt;/p&gt;
&lt;p&gt;Agentic Engineering represents the other end of the scale: professional software engineers using coding agents to improve and accelerate their work by amplifying their existing expertise.&lt;/p&gt;
&lt;p&gt;There is so much to learn and explore about this new discipline! I've already published a lot &lt;a href="https://simonwillison.net/tags/ai-assisted-programming/"&gt;under my ai-assisted-programming tag&lt;/a&gt; (345 posts and counting) but that's been relatively unstructured. My new goal is to produce something that helps answer the question "how do I get good results out of this stuff" all in one place.&lt;/p&gt;
&lt;p&gt;I'll be developing and growing this project here on my blog as a series of chapter-shaped patterns, loosely inspired by the format popularized by &lt;a href="https://en.wikipedia.org/wiki/Design_Patterns"&gt;Design Patterns: Elements of Reusable Object-Oriented Software&lt;/a&gt; back in 1994.&lt;/p&gt;
&lt;p&gt;I published the first two chapters today:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://simonwillison.net/guides/agentic-engineering-patterns/code-is-cheap/"&gt;Writing code is cheap now&lt;/a&gt;&lt;/strong&gt; talks about the central challenge of agentic engineering: the cost to churn out initial working code has dropped to almost nothing, how does that impact our existing intuitions about how we work, both individually and as a team?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://simonwillison.net/guides/agentic-engineering-patterns/red-green-tdd/"&gt;Red/green TDD&lt;/a&gt;&lt;/strong&gt; describes how test-first development helps agents write more succinct and reliable code with minimal extra prompting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I hope to add more chapters at a rate of 1-2 a week. I don't really know when I'll stop, there's a lot to cover!&lt;/p&gt;
&lt;h4 id="written-by-me-not-by-an-llm"&gt;Written by me, not by an LLM&lt;/h4&gt;
&lt;p&gt;I have a strong personal policy of not publishing AI-generated writing under my own name. That policy will hold true for Agentic Engineering Patterns as well. I'll be using LLMs for proofreading and fleshing out example code and all manner of other side-tasks, but the words you read here will be my own.&lt;/p&gt;
&lt;h4 id="chapters-and-guides"&gt;Chapters and Guides&lt;/h4&gt;
&lt;p&gt;Agentic Engineering Patterns isn't exactly &lt;em&gt;a book&lt;/em&gt;, but it's kind of book-shaped. I'll be publishing it on my site using a new shape of content I'm calling a &lt;em&gt;guide&lt;/em&gt;. A guide is a collection of chapters, where each chapter is effectively a blog post with a less prominent date that's designed to be updated over time, not frozen at the point of first publication.&lt;/p&gt;
&lt;p&gt;Guides and chapters are my answer to the challenge of publishing "evergreen" content on a blog. I've been trying to find a way to do this for a while now. This feels like a format that might stick.&lt;/p&gt;

&lt;p&gt;If you're interested in the implementation you can find the code in the &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b9cd41a0ac4a232b2a6c90ca3fff9ae465263b02/blog/models.py#L262-L280"&gt;Guide&lt;/a&gt;, &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b9cd41a0ac4a232b2a6c90ca3fff9ae465263b02/blog/models.py#L349-L405"&gt;Chapter&lt;/a&gt; and &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b9cd41a0ac4a232b2a6c90ca3fff9ae465263b02/blog/models.py#L408-L423"&gt;ChapterChange&lt;/a&gt; models and the &lt;a href="https://github.com/simonw/simonwillisonblog/blob/b9cd41a0ac4a232b2a6c90ca3fff9ae465263b02/blog/views.py#L775-L923"&gt;associated Django views&lt;/a&gt;, almost all of which was written by Claude Opus 4.6 running in Claude Code for web accessed via my iPhone.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/design-patterns"&gt;design-patterns&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/writing"&gt;writing&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/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/agentic-engineering"&gt;agentic-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="design-patterns"/><category term="projects"/><category term="writing"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="vibe-coding"/><category term="coding-agents"/><category term="agentic-engineering"/><category term="site-upgrades"/></entry><entry><title>Adding TILs, releases, museums, tools and research to my blog</title><link href="https://simonwillison.net/2026/Feb/20/beats/#atom-tag" rel="alternate"/><published>2026-02-20T23:47:10+00:00</published><updated>2026-02-20T23:47:10+00:00</updated><id>https://simonwillison.net/2026/Feb/20/beats/#atom-tag</id><summary type="html">
    &lt;p&gt;I've been wanting to add indications of my various other online activities to my blog for a while now. I just turned on a new feature I'm calling "beats" (after story beats, naming this was hard!) which adds five new types of content to my site, all corresponding to activity elsewhere.&lt;/p&gt;
&lt;p&gt;Here's what beats look like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/three-beats.jpg" alt="Screenshot of a fragment of a page showing three entries from 30th Dec 2025. First: [RELEASE] &amp;quot;datasette-turnstile 0.1a0 — Configurable CAPTCHAs for Datasette paths usin…&amp;quot; at 7:23 pm. Second: [TOOL] &amp;quot;Software Heritage Repository Retriever — Download archived Git repositories f…&amp;quot; at 11:41 pm. Third: [TIL] &amp;quot;Downloading archived Git repositories from archive.softwareheritage.org — …&amp;quot; at 11:43 pm." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Those three are from &lt;a href="https://simonwillison.net/2025/Dec/30/"&gt;the 30th December 2025&lt;/a&gt; archive page.&lt;/p&gt;
&lt;p&gt;Beats are little inline links with badges that fit into different content timeline views around my site, including the homepage, search and archive pages.&lt;/p&gt;
&lt;p&gt;There are currently five types of beats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/elsewhere/release/"&gt;Releases&lt;/a&gt; are GitHub releases of my many different open source projects, imported from &lt;a href="https://github.com/simonw/simonw/blob/main/releases_cache.json"&gt;this JSON file&lt;/a&gt; that was constructed &lt;a href="https://simonwillison.net/2020/Jul/10/self-updating-profile-readme/"&gt;by GitHub Actions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/elsewhere/til/"&gt;TILs&lt;/a&gt; are the posts from my &lt;a href="https://til.simonwillison.net/"&gt;TIL blog&lt;/a&gt;, imported using &lt;a href="https://github.com/simonw/simonwillisonblog/blob/f883b92be23892d082de39dbada571e406f5cfbf/blog/views.py#L1169"&gt;a SQL query over JSON and HTTP&lt;/a&gt; against the Datasette instance powering that site.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/elsewhere/museum/"&gt;Museums&lt;/a&gt; are new posts on my &lt;a href="https://www.niche-museums.com/"&gt;niche-museums.com&lt;/a&gt; blog, imported from &lt;a href="https://github.com/simonw/museums/blob/909bef71cc8d336bf4ac1f13574db67a6e1b3166/plugins/export.py"&gt;this custom JSON feed&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/elsewhere/tool/"&gt;Tools&lt;/a&gt; are HTML and JavaScript tools I've vibe-coded on my &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; site, as described in &lt;a href="https://simonwillison.net/2025/Dec/10/html-tools/"&gt;Useful patterns for building HTML tools&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/elsewhere/research/"&gt;Research&lt;/a&gt; is for AI-generated research projects, hosted in my &lt;a href="https://github.com/simonw/research"&gt;simonw/research repo&lt;/a&gt; and described in &lt;a href="https://simonwillison.net/2025/Nov/6/async-code-research/"&gt;Code research projects with async coding agents like Claude Code and Codex&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's five different custom integrations to pull in all of that data. The good news is that this kind of integration project is the kind of thing that coding agents &lt;em&gt;really&lt;/em&gt; excel at. I knocked most of the feature out in a single morning while working in parallel on various other things.&lt;/p&gt;
&lt;p&gt;I didn't have a useful structured feed of my Research projects, and it didn't matter because I gave Claude Code a link to &lt;a href="https://raw.githubusercontent.com/simonw/research/refs/heads/main/README.md"&gt;the raw Markdown README&lt;/a&gt; that lists them all and it &lt;a href="https://github.com/simonw/simonwillisonblog/blob/f883b92be23892d082de39dbada571e406f5cfbf/blog/importers.py#L77-L80"&gt;spun up a parser regex&lt;/a&gt;. Since I'm responsible for both the source and the destination I'm fine with a brittle solution that would be too risky against a source that I don't control myself.&lt;/p&gt;
&lt;p&gt;Claude also handled all of the potentially tedious UI integration work with my site, making sure the new content worked on all of my different page types and was handled correctly by my &lt;a href="https://simonwillison.net/2017/Oct/5/django-postgresql-faceted-search/"&gt;faceted search engine&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="prototyping-with-claude-artifacts"&gt;Prototyping with Claude Artifacts&lt;/h4&gt;
&lt;p&gt;I actually prototyped the initial concept for beats in regular Claude - not Claude Code - taking advantage of the fact that it can clone public repos from GitHub these days. I started with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Clone simonw/simonwillisonblog and tell me about the models and views&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then later in the brainstorming session said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;use the templates and CSS in this repo to create a new artifact with all HTML and CSS inline that shows me my homepage with some of those inline content types mixed in&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After some iteration we got to &lt;a href="https://gisthost.github.io/?c3f443cc4451cf8ce03a2715a43581a4/preview.html"&gt;this artifact mockup&lt;/a&gt;, which was enough to convince me that the concept had legs and was worth handing over to full &lt;a href="https://code.claude.com/docs/en/claude-code-on-the-web"&gt;Claude Code for web&lt;/a&gt; to implement.&lt;/p&gt;
&lt;p&gt;If you want to see how the rest of the build played out the most interesting PRs are &lt;a href="https://github.com/simonw/simonwillisonblog/pull/592"&gt;Beats #592&lt;/a&gt; which implemented the core feature and &lt;a href="https://github.com/simonw/simonwillisonblog/pull/595/changes"&gt;Add Museums Beat importer #595&lt;/a&gt; which added the Museums content type.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/museums"&gt;museums&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/til"&gt;til&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-artifacts"&gt;claude-artifacts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="museums"/><category term="ai"/><category term="til"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude-artifacts"/><category term="claude-code"/><category term="site-upgrades"/></entry><entry><title>Experimenting with sponsorship for my blog and newsletter</title><link href="https://simonwillison.net/2026/Feb/19/sponsorship/#atom-tag" rel="alternate"/><published>2026-02-19T05:44:29+00:00</published><updated>2026-02-19T05:44:29+00:00</updated><id>https://simonwillison.net/2026/Feb/19/sponsorship/#atom-tag</id><summary type="html">
    &lt;p&gt;I've long been resistant to the idea of accepting sponsorship for my blog. I value my credibility as an independent voice, and I don't want to risk compromising that reputation.&lt;/p&gt;
&lt;p&gt;Then I learned about Troy Hunt's &lt;a href="https://www.troyhunt.com/sponsorship/"&gt;approach to sponsorship&lt;/a&gt;, which he first wrote about &lt;a href="https://www.troyhunt.com/im-now-offering-sponsorship-of-this-blog/"&gt;in 2016&lt;/a&gt;. Troy runs with a simple text row in the page banner - no JavaScript, no cookies, unobtrusive while providing value to the sponsor. I can live with that!&lt;/p&gt;
&lt;p&gt;Accepting sponsorship in this way helps me maintain my independence while offsetting the opportunity cost of not taking a full-time job.&lt;/p&gt;
&lt;p&gt;To start with I'm selling sponsorship by the week. Sponsors get that unobtrusive banner across my blog and also their sponsored message at the top of &lt;a href="https://simonw.substack.com/"&gt;my newsletter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of my blog's homepage. Below the Simon Willison's Weblog heading and list of tags is a new blue page-wide banner reading &amp;quot;Sponsored by: Teleport - Secure, Govern, and Operate Al at Engineering Scale. Learn more&amp;quot;." src="https://static.simonwillison.net/static/2026/sponsor-banner.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I &lt;strong&gt;will not write content in exchange for sponsorship&lt;/strong&gt;. I hope the sponsors I work with understand that my credibility as an independent voice is a key reason I have an audience, and compromising that trust would be bad for everyone.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.freemanandforrest.com/"&gt;Freeman &amp;amp; Forrest&lt;/a&gt; helped me set up and sell my first slots. Thanks also to &lt;a href="https://t3.gg/"&gt;Theo Browne&lt;/a&gt; for helping me think through my approach.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/newsletter"&gt;newsletter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/troy-hunt"&gt;troy-hunt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="newsletter"/><category term="blogging"/><category term="troy-hunt"/><category term="site-upgrades"/></entry><entry><title>How I automate my Substack newsletter with content from my blog</title><link href="https://simonwillison.net/2025/Nov/19/how-i-automate-my-substack-newsletter/#atom-tag" rel="alternate"/><published>2025-11-19T22:00:34+00:00</published><updated>2025-11-19T22:00:34+00:00</updated><id>https://simonwillison.net/2025/Nov/19/how-i-automate-my-substack-newsletter/#atom-tag</id><summary type="html">
    &lt;p&gt;I sent out &lt;a href="https://simonw.substack.com/p/trying-out-gemini-3-pro-with-audio"&gt;my weekly-ish Substack newsletter&lt;/a&gt; this morning and took the opportunity to record &lt;a href="https://www.youtube.com/watch?v=BoPZltKDM-s"&gt;a YouTube video&lt;/a&gt; demonstrating my process and describing the different components that make it work. There's a &lt;em&gt;lot&lt;/em&gt; of digital duct tape involved, taking the content from Django+Heroku+PostgreSQL to GitHub Actions to SQLite+Datasette+Fly.io to JavaScript+Observable and finally to Substack.&lt;/p&gt;

&lt;p&gt;&lt;lite-youtube videoid="BoPZltKDM-s" js-api="js-api"
  title="How I automate my Substack newsletter with content from my blog"
  playlabel="Play: How I automate my Substack newsletter with content from my blog"
&gt; &lt;/lite-youtube&gt;&lt;/p&gt;

&lt;p&gt;The core process is the same as I described &lt;a href="https://simonwillison.net/2023/Apr/4/substack-observable/"&gt;back in 2023&lt;/a&gt;. I have an Observable notebook called &lt;a href="https://observablehq.com/@simonw/blog-to-newsletter"&gt;blog-to-newsletter&lt;/a&gt; which fetches content from my blog's database, filters out anything that has been in the newsletter before, formats what's left as HTML and offers a big "Copy rich text newsletter to clipboard" button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/copy-to-newsletter.jpg" alt="Screenshot of the interface. An item in a list says 9080: Trying out Gemini 3 Pro with audio transcription and a new pelican benchmark. A huge button reads Copy rich text newsletter to clipboard - below is a smaller button that says Copy just the links/quotes/TILs. A Last X days slider is set to 2. There are checkboxes for SKip content sent in prior newsletters and only include post content prior to the cutoff comment." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I click that button, paste the result into the Substack editor, tweak a few things and hit send. The whole process usually takes just a few minutes.&lt;/p&gt;
&lt;p&gt;I make very minor edits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I set the title and the subheading for the newsletter. This is often a direct copy of the title of the featured blog post.&lt;/li&gt;
&lt;li&gt;Substack turns YouTube URLs into embeds, which often isn't what I want - especially if I have a YouTube URL inside a code example.&lt;/li&gt;
&lt;li&gt;Blocks of preformatted text often have an extra blank line at the end, which I remove.&lt;/li&gt;
&lt;li&gt;Occasionally I'll make a content edit - removing a piece of content that doesn't fit the newsletter, or fixing a time reference like "yesterday" that doesn't make sense any more.&lt;/li&gt;
&lt;li&gt;I pick the featured image for the newsletter and add some tags.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's the whole process!&lt;/p&gt;
&lt;h4 id="the-observable-notebook"&gt;The Observable notebook&lt;/h4&gt;
&lt;p&gt;The most important cell in the Observable notebook is this one:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;raw_content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;await&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
      &lt;span class="pl-s"&gt;`https://datasette.simonwillison.net/simonwillisonblog.json?sql=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;        &lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;      &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;amp;_shape=array&amp;amp;numdays=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;numDays&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&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-en"&gt;json&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;This uses the JavaScript &lt;code&gt;fetch()&lt;/code&gt; function to pull data from my blog's Datasette instance, using a very complex SQL query that is composed elsewhere in the notebook.&lt;/p&gt;
&lt;p&gt;Here's a link to &lt;a href="https://datasette.simonwillison.net/simonwillisonblog?sql=with+content+as+%28%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27entry%27+as+type%2C%0D%0A++++title%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27%3Ch3%3E%3Ca+href%3D%22%27+%7C%7C+%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%0D%0A++++++%7C%7C+%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27+%7C%7C+%27%22%3E%27+%0D%0A++++++%7C%7C+title+%7C%7C+%27%3C%2Fa%3E+-+%27+%7C%7C+date%28created%29+%7C%7C+%27%3C%2Fh3%3E%27+%7C%7C+body%0D%0A++++++as+html%2C%0D%0A++++%27null%27+as+json%2C%0D%0A++++%27%27+as+external_url%0D%0A++from+blog_entry%0D%0A++union+all%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27blogmark%27+as+type%2C%0D%0A++++link_title%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27%3Cp%3E%3Cstrong%3ELink%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C+%27+%3Ca+href%3D%22%27%7C%7C+link_url+%7C%7C+%27%22%3E%27%0D%0A++++++%7C%7C+link_title+%7C%7C+%27%3C%2Fa%3E%3A%3C%2Fp%3E%3Cp%3E%27+%7C%7C+%27+%27+%7C%7C+replace%28commentary%2C+%27%0D%0A%27%2C+%27%3Cbr%3E%27%29+%7C%7C+%27%3C%2Fp%3E%27%0D%0A++++++as+html%2C%0D%0A++++json_object%28%0D%0A++++++%27created%27%2C+date%28created%29%2C%0D%0A++++++%27link_url%27%2C+link_url%2C%0D%0A++++++%27link_title%27%2C+link_title%2C%0D%0A++++++%27commentary%27%2C+commentary%2C%0D%0A++++++%27use_markdown%27%2C+use_markdown%0D%0A++++%29+as+json%2C%0D%0A++link_url+as+external_url%0D%0A++from+blog_blogmark%0D%0A++union+all%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27quotation%27+as+type%2C%0D%0A++++source%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27%3Cstrong%3Equote%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C%0D%0A++++%27%3Cblockquote%3E%3Cp%3E%3Cem%3E%27+%7C%7C%0D%0A++++replace%28quotation%2C+%27%0D%0A%27%2C+%27%3Cbr%3E%27%29+%7C%7C+%0D%0A++++%27%3C%2Fem%3E%3C%2Fp%3E%3C%2Fblockquote%3E%3Cp%3E%3Ca+href%3D%22%27+%7C%7C%0D%0A++++coalesce%28source_url%2C+%27%23%27%29+%7C%7C+%27%22%3E%27+%7C%7C+source+%7C%7C+%27%3C%2Fa%3E%27+%7C%7C%0D%0A++++case+%0D%0A++++++++when+nullif%28trim%28context%29%2C+%27%27%29+is+not+null+%0D%0A++++++++then+%27%2C+%27+%7C%7C+context+%0D%0A++++++++else+%27%27+%0D%0A++++end+%7C%7C%0D%0A++++%27%3C%2Fp%3E%27+as+html%2C%0D%0A++++%27null%27+as+json%2C%0D%0A++++source_url+as+external_url%0D%0A++from+blog_quotation%0D%0A++union+all%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27note%27+as+type%2C%0D%0A++++case%0D%0A++++++when+title+is+not+null+and+title+%3C%3E+%27%27+then+title%0D%0A++++++else+%27Note+on+%27+%7C%7C+date%28created%29%0D%0A++++end%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27No+HTML%27%2C%0D%0A++++json_object%28%0D%0A++++++%27created%27%2C+date%28created%29%2C%0D%0A++++++%27link_url%27%2C+%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%0D%0A++++++%7C%7C+%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27%2C%0D%0A++++++%27link_title%27%2C+%27%27%2C%0D%0A++++++%27commentary%27%2C+body%2C%0D%0A++++++%27use_markdown%27%2C+1%0D%0A++++%29%2C%0D%0A++++%27%27+as+external_url%0D%0A++from+blog_note%0D%0A++union+all%0D%0A++select%0D%0A++++rowid%2C%0D%0A++++%27til%27+as+type%2C%0D%0A++++title%2C%0D%0A++++created%2C%0D%0A++++%27null%27+as+slug%2C%0D%0A++++%27%3Cp%3E%3Cstrong%3ETIL%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C+%27+%3Ca+href%3D%22%27%7C%7C+%27https%3A%2F%2Ftil.simonwillison.net%2F%27+%7C%7C+topic+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%22%3E%27+%7C%7C+title+%7C%7C+%27%3C%2Fa%3E%3A%27+%7C%7C+%27+%27+%7C%7C+substr%28html%2C+1%2C+instr%28html%2C+%27%3C%2Fp%3E%27%29+-+1%29+%7C%7C+%27+%26%238230%3B%3C%2Fp%3E%27+as+html%2C%0D%0A++++%27null%27+as+json%2C%0D%0A++++%27https%3A%2F%2Ftil.simonwillison.net%2F%27+%7C%7C+topic+%7C%7C+%27%2F%27+%7C%7C+slug+as+external_url%0D%0A++from+til%0D%0A%29%2C%0D%0Acollected+as+%28%0D%0A++select%0D%0A++++id%2C%0D%0A++++type%2C%0D%0A++++title%2C%0D%0A++++case%0D%0A++++++when+type+%3D+%27til%27%0D%0A++++++then+external_url%0D%0A++++++else+%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%7C%7C+%0D%0A++++++%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27%0D%0A++++++end+as+url%2C%0D%0A++++created%2C%0D%0A++++html%2C%0D%0A++++json%2C%0D%0A++++external_url%2C%0D%0A++++case%0D%0A++++++when+type+%3D+%27entry%27+then+%28%0D%0A++++++++select+json_group_array%28tag%29%0D%0A++++++++from+blog_tag%0D%0A++++++++join+blog_entry_tags+on+blog_tag.id+%3D+blog_entry_tags.tag_id%0D%0A++++++++where+blog_entry_tags.entry_id+%3D+content.id%0D%0A++++++%29%0D%0A++++++when+type+%3D+%27blogmark%27+then+%28%0D%0A++++++++select+json_group_array%28tag%29%0D%0A++++++++from+blog_tag%0D%0A++++++++join+blog_blogmark_tags+on+blog_tag.id+%3D+blog_blogmark_tags.tag_id%0D%0A++++++++where+blog_blogmark_tags.blogmark_id+%3D+content.id%0D%0A++++++%29%0D%0A++++++when+type+%3D+%27quotation%27+then+%28%0D%0A++++++++select+json_group_array%28tag%29%0D%0A++++++++from+blog_tag%0D%0A++++++++join+blog_quotation_tags+on+blog_tag.id+%3D+blog_quotation_tags.tag_id%0D%0A++++++++where+blog_quotation_tags.quotation_id+%3D+content.id%0D%0A++++++%29%0D%0A++++++else+%27%5B%5D%27%0D%0A++++end+as+tags%0D%0A++from+content%0D%0A++where+created+%3E%3D+date%28%27now%27%2C+%27-%27+%7C%7C+%3Anumdays+%7C%7C+%27+days%27%29+++%0D%0A++order+by+created+desc%0D%0A%29%0D%0Aselect+id%2C+type%2C+title%2C+url%2C+created%2C+html%2C+json%2C+external_url%2C+tags%0D%0Afrom+collected+%0D%0Aorder+by+%0D%0A++case+type+%0D%0A++++when+%27entry%27+then+0+%0D%0A++++else+1+%0D%0A++end%2C%0D%0A++case+type+%0D%0A++++when+%27entry%27+then+created+%0D%0A++++else+-strftime%28%27%25s%27%2C+created%29+%0D%0A++end+desc%3B&amp;amp;numdays=7"&gt;see and execute that query&lt;/a&gt; directly in Datasette. It's 143 lines of convoluted SQL that assembles most of the HTML for the newsletter using SQLite string concatenation! An illustrative snippet:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;with content &lt;span class="pl-k"&gt;as&lt;/span&gt; (
  &lt;span class="pl-k"&gt;select&lt;/span&gt;
    id,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;entry&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; type,
    title,
    created,
    slug,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;h3&amp;gt;&amp;lt;a href="&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://simonwillison.net/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%Y/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created)
      &lt;span class="pl-k"&gt;||&lt;/span&gt; substr(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;JanFebMarAprMayJunJulAugSepOctNovDec&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, (strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%m&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;-&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;) &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-c1"&gt;3&lt;/span&gt;) 
      &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; cast(strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%d&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-k"&gt;integer&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; slug &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;"&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; 
      &lt;span class="pl-k"&gt;||&lt;/span&gt; title &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/a&amp;gt; - &lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-k"&gt;date&lt;/span&gt;(created) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/h3&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; body
      &lt;span class="pl-k"&gt;as&lt;/span&gt; html,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;null&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; json,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; external_url
  &lt;span class="pl-k"&gt;from&lt;/span&gt; blog_entry
  &lt;span class="pl-k"&gt;union all&lt;/span&gt;
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; ...&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;My blog's URLs look like &lt;code&gt;/2025/Nov/18/gemini-3/&lt;/code&gt; - this SQL constructs that three letter month abbreviation from the month number using a substring operation.&lt;/p&gt;
&lt;p&gt;This is a &lt;em&gt;terrible&lt;/em&gt; way to assemble HTML, but I've stuck with it because it amuses me.&lt;/p&gt;
&lt;p&gt;The rest of the Observable notebook takes that data, filters out anything that links to content mentioned in the previous newsletters and composes it into a block of HTML that can be copied using that big button.&lt;/p&gt;
&lt;p&gt;Here's the recipe it uses to turn HTML into rich text content on a clipboard suitable for Substack. I can't remember how I figured this out but it's very effective:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;Object&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;assign&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-en"&gt;html&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;button&lt;/span&gt; &lt;span class="pl-c1"&gt;style&lt;/span&gt;="&lt;span class="pl-s"&gt;font-size: 1.4em; padding: 0.3em 1em; font-weight: bold;&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;Copy rich text newsletter to clipboard`&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;onclick&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;const&lt;/span&gt; &lt;span class="pl-s1"&gt;htmlContent&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;newsletterHTML&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Create a temporary element to hold the HTML content&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;tempElement&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;"div"&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;tempElement&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-s1"&gt;htmlContent&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-c1"&gt;body&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;tempElement&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-c"&gt;// Select the HTML content&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;range&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;createRange&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-s1"&gt;range&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;selectNode&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;tempElement&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-c"&gt;// Copy the selected HTML content to the clipboard&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;selection&lt;/span&gt; &lt;span class="pl-c1"&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-en"&gt;getSelection&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-s1"&gt;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeAllRanges&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-s1"&gt;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addRange&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;range&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;execCommand&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"copy"&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;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeAllRanges&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-c1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeChild&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;tempElement&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;h4 id="from-django-postgresql-to-datasette-sqlite"&gt;From Django+Postgresql to Datasette+SQLite&lt;/h4&gt;
&lt;p&gt;My blog itself is a Django application hosted on Heroku, with data stored in Heroku PostgreSQL. Here's &lt;a href="https://github.com/simonw/simonwillisonblog"&gt;the source code for that Django application&lt;/a&gt;. I use the Django admin as my CMS.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; provides a JSON API over a SQLite database... which means something needs to convert that PostgreSQL database into a SQLite database that Datasette can use.&lt;/p&gt;
&lt;p&gt;My system for doing that lives in the &lt;a href="https://github.com/simonw/simonwillisonblog-backup"&gt;simonw/simonwillisonblog-backup&lt;/a&gt; GitHub repository. It uses GitHub Actions on a schedule that executes every two hours, fetching the latest data from PostgreSQL and converting that to SQLite.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/db-to-sqlite"&gt;db-to-sqlite&lt;/a&gt; tool is responsible for that conversion. I call it &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/dc5b9df272134ce051a5280b4de6d4daa9b2a9fc/.github/workflows/backup.yml#L44-L62"&gt;like this&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;db-to-sqlite \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;heroku config:get DATABASE_URL -a simonwillisonblog &lt;span class="pl-k"&gt;|&lt;/span&gt; sed s/postgres:/postgresql+psycopg2:/&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt; \
  simonwillisonblog.db \
  --table auth_permission \
  --table auth_user \
  --table blog_blogmark \
  --table blog_blogmark_tags \
  --table blog_entry \
  --table blog_entry_tags \
  --table blog_quotation \
  --table blog_quotation_tags \
  --table blog_note \
  --table blog_note_tags \
  --table blog_tag \
  --table blog_previoustagname \
  --table blog_series \
  --table django_content_type \
  --table redirects_redirect&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code&gt;heroku config:get DATABASE_URL&lt;/code&gt; command uses Heroku credentials in an environment variable to fetch the database connection URL for my blog's PostgreSQL database (and fixes a small difference in the URL scheme).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;db-to-sqlite&lt;/code&gt; can then export that data and write it to a SQLite database file called &lt;code&gt;simonwillisonblog.db&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--table&lt;/code&gt; options specify the tables that should be included in the export.&lt;/p&gt;
&lt;p&gt;The repository does more than just that conversion: it also exports the resulting data to JSON files that live in the repository, which gives me a &lt;a href="https://github.com/simonw/simonwillisonblog-backup/commits/main/simonwillisonblog"&gt;commit history&lt;/a&gt; of changes I make to my content. This is a cheap way to get a revision history of my blog content without having to mess around with detailed history tracking inside the Django application itself.&lt;/p&gt;
&lt;p&gt;At the &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/dc5b9df272134ce051a5280b4de6d4daa9b2a9fc/.github/workflows/backup.yml#L200-L204"&gt;end of my GitHub Actions workflow&lt;/a&gt; is this code that publishes the resulting database to Datasette running on &lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; using the &lt;a href="https://datasette.io/plugins/datasette-publish-fly"&gt;datasette publish fly&lt;/a&gt; plugin:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;datasette publish fly simonwillisonblog.db \
  -m metadata.yml \
  --app simonwillisonblog-backup \
  --branch 1.0a2 \
  --extra-options &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;--setting sql_time_limit_ms 15000 --setting truncate_cells_html 10000 --setting allow_facet off&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; \
  --install datasette-block-robots \
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; ... more plugins&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As you can see, there are a lot of moving parts! Surprisingly it all mostly just works - I rarely have to intervene in the process, and the cost of those different components is pleasantly low.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&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/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/youtube"&gt;youtube&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/newsletter"&gt;newsletter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/substack"&gt;substack&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="django"/><category term="javascript"/><category term="postgresql"/><category term="sql"/><category term="sqlite"/><category term="youtube"/><category term="heroku"/><category term="datasette"/><category term="observable"/><category term="github-actions"/><category term="fly"/><category term="newsletter"/><category term="substack"/><category term="site-upgrades"/></entry><entry><title>New tags</title><link href="https://simonwillison.net/2025/Jul/19/new-tags/#atom-tag" rel="alternate"/><published>2025-07-19T02:02:54+00:00</published><updated>2025-07-19T02:02:54+00:00</updated><id>https://simonwillison.net/2025/Jul/19/new-tags/#atom-tag</id><summary type="html">
    &lt;p&gt;A few months ago I &lt;a href="https://github.com/simonw/simonwillisonblog/commit/12da4167396c2d54526bf690add14aebbb244148"&gt;added a tool&lt;/a&gt; to my blog for bulk-applying tags to old content. It works as an extension to my existing search interface, letting me run searches and then quickly apply a tag to relevant results.&lt;/p&gt;
&lt;p&gt;Since adding this I've been much more aggressive in categorizing my older content, including adding new tags when I spot an interesting trend that warrants its own page.&lt;/p&gt;
&lt;p&gt;Today I added &lt;a href="https://simonwillison.net/tags/system-prompts/"&gt;system-prompts&lt;/a&gt; and applied it to 41 existing posts that talk about system prompts for LLM systems, including a bunch that directly quote system prompts that have been deliberately published or leaked.&lt;/p&gt;
&lt;p&gt;Other tags I've added recently include &lt;a href="https://simonwillison.net/tags/press-quotes/"&gt;press-quotes&lt;/a&gt; for times I've been quoted in the press, &lt;a href="https://simonwillison.net/tags/agent-definitions/"&gt;agent-definitions&lt;/a&gt; for my ongoing collection of different ways people define "agents" and 
&lt;a href="https://simonwillison.net/tags/paper-review/"&gt;paper-review&lt;/a&gt; for posts where I review an academic paper.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tagging"&gt;tagging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="tagging"/><category term="site-upgrades"/></entry><entry><title>Disclosures</title><link href="https://simonwillison.net/2025/Jun/23/disclosures/#atom-tag" rel="alternate"/><published>2025-06-23T18:06:02+00:00</published><updated>2025-06-23T18:06:02+00:00</updated><id>https://simonwillison.net/2025/Jun/23/disclosures/#atom-tag</id><summary type="html">
    &lt;p&gt;I've added a &lt;a href="https://simonwillison.net/about/#disclosures"&gt;Disclosures section&lt;/a&gt; to my about page, listing my various sources of income and the companies that directly sponsor my work or have supported it in the recent past.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I do not receive any compensation writing about specific topics on this blog - no sponsored content! I plan to continue this policy. If I ever change this I will disclose that both here and in the post itself. [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I see my credibility as one of my most valuable assets, so it's important to be transparent about how financial interests may influence my writing here.&lt;/p&gt;
&lt;p&gt;I took inspiration from &lt;a href="https://www.mollywhite.net/crypto-disclosures/"&gt;Molly White's disclosures page&lt;/a&gt;.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/molly-white"&gt;molly-white&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="molly-white"/><category term="site-upgrades"/></entry><entry><title>Subscribe to my sponsors-only monthly newsletter.</title><link href="https://simonwillison.net/2025/May/25/sponsors-only-newsletter/#atom-tag" rel="alternate"/><published>2025-05-25T06:06:17+00:00</published><updated>2025-05-25T06:06:17+00:00</updated><id>https://simonwillison.net/2025/May/25/sponsors-only-newsletter/#atom-tag</id><summary type="html">
    &lt;h3 style="margin-top: 0"&gt;Subscribe to my sponsors-only monthly newsletter&lt;/h3&gt;

&lt;p&gt;I’ve never liked the idea of charging for my content. I get enormous value from putting all of my writing and research out there for free.&lt;/p&gt;
&lt;p&gt;So I’m trying something a little different: &lt;strong&gt;pay me to send you less&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I’m starting a sponsors-only monthly newsletter featuring just my heavily curated and edited highlights. If you only have ten minutes, what are the most important things not to miss from the last month?&lt;/p&gt;
&lt;p&gt;Don’t want to pay? That’s fine, you can continue to follow my firehose for free!&lt;/p&gt;
&lt;p&gt;Anyone who sponsors me for &lt;a href="https://github.com/sponsors/simonw"&gt;$10/month (or $50/month or more) on GitHub sponsors&lt;/a&gt; will receive my new newsletter on approximately the last day of the month. I’ll be sending out the first edition next week.&lt;/p&gt;
&lt;p&gt;This blog and &lt;a href="https://simonw.substack.com/"&gt;my newsletter&lt;/a&gt; will continue at their same breakneck pace. Paying subscribers can get a &lt;em&gt;lower&lt;/em&gt; volume of stuff.&lt;/p&gt;
&lt;p&gt;I'm cautiously optimistic that this could work. I've never liked the idea of business models that incentivize me to publish less. This feels like it encourages me to do what I'm doing already while giving people a rational reason to support my work, at a relatively small incremental cost to myself.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/email"&gt;email&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/newsletter"&gt;newsletter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="blogging"/><category term="github"/><category term="email"/><category term="newsletter"/><category term="site-upgrades"/></entry><entry><title>Backfill your blog</title><link href="https://simonwillison.net/2025/Apr/25/backfill-your-blog/#atom-tag" rel="alternate"/><published>2025-04-25T15:30:37+00:00</published><updated>2025-04-25T15:30:37+00:00</updated><id>https://simonwillison.net/2025/Apr/25/backfill-your-blog/#atom-tag</id><summary type="html">
    &lt;p&gt;Fun fact: there's no rule that says you can't create a new blog today and backfill (and backdate) it with your writing from other platforms or sources, even going back many years.&lt;/p&gt;
&lt;p&gt;I'd love to see more people do this!&lt;/p&gt;
&lt;p&gt;&lt;small&gt;(Inspired by &lt;a href="https://twitter.com/jwuphysics/status/1915422889224147335"&gt;this tweet&lt;/a&gt; by John F. Wu introducing &lt;a href="https://jwuphysics.github.io/blog/"&gt;his new blog&lt;/a&gt;. I did this myself when I &lt;a href="https://simonwillison.net/2017/Oct/1/ship/"&gt;relaunched this blog&lt;/a&gt; back in 2017.)&lt;/small&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="site-upgrades"/></entry><entry><title>Note on 26th March 2025</title><link href="https://simonwillison.net/2025/Mar/26/notes/#atom-tag" rel="alternate"/><published>2025-03-26T06:11:30+00:00</published><updated>2025-03-26T06:11:30+00:00</updated><id>https://simonwillison.net/2025/Mar/26/notes/#atom-tag</id><summary type="html">
    &lt;p&gt;I've added a new content type to my blog: &lt;strong&gt;notes&lt;/strong&gt;. These join my existing types: &lt;a href="https://simonwillison.net/search/?type=entry"&gt;entries&lt;/a&gt;, &lt;a href="https://simonwillison.net/search/?type=blogmark"&gt;bookmarks&lt;/a&gt; and &lt;a href="https://simonwillison.net/search/?type=quotation"&gt;quotations&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A note is a little bit like a bookmark without a link. They're for short form writing - thoughts or images that don't warrant a full entry with a title. The kind of things I used to post to Twitter, but that don't feel right to cross-post to multiple social networks (Mastodon and Bluesky, for example.)&lt;/p&gt;
&lt;p&gt;I was partly inspired by Molly White's &lt;a href="https://www.mollywhite.net/micro"&gt;short thoughts, notes, links, and musings&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've been thinking about this for a while, but the amount of work involved in modifying all of the parts of my site that handle the three different content types was daunting. Then this evening I tried running my blog's source code (using &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; and &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt;) through &lt;a href="https://simonwillison.net/2025/Mar/25/gemini/"&gt;the new Gemini 2.5 Pro&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; -e py -c &lt;span class="pl-k"&gt;|&lt;/span&gt; \
  llm -m gemini-2.5-pro-exp-03-25 -s \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;I want to add a new type of content called a Note,&lt;/span&gt;
&lt;span class="pl-s"&gt;  similar to quotation and bookmark and entry but it&lt;/span&gt;
&lt;span class="pl-s"&gt;  only has a markdown text body. Output all of the&lt;/span&gt;
&lt;span class="pl-s"&gt;  code I need to add for that feature and tell me&lt;/span&gt;
&lt;span class="pl-s"&gt;  which files to add  the code to.&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Gemini gave me &lt;a href="https://gist.github.com/simonw/6d9fb3e33e7105d391a31367d6a235de#response"&gt;a detailed 13 step plan&lt;/a&gt; covering all of the tedious changes I'd been avoiding having to figure out!&lt;/p&gt;
&lt;p&gt;The code &lt;a href="https://github.com/simonw/simonwillisonblog/pull/527"&gt;is in this PR&lt;/a&gt;, which touched 18 different files. The whole project took around 45 minutes start to finish.&lt;/p&gt;
&lt;p&gt;(I used Claude to &lt;a href="https://claude.ai/share/17656d59-6f52-471e-8aeb-6abbe1464471"&gt;brainstorm names&lt;/a&gt; for the feature - I had it come up with possible nouns and then "rank those by least pretentious to most pretentious", and "notes" came out on top.)&lt;/p&gt;
&lt;p&gt;This is now far too long for a note and should really be upgraded to an entry, but I need to post a first note to make sure everything is working as it should.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&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/molly-white"&gt;molly-white&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="projects"/><category term="gemini"/><category term="ai-assisted-programming"/><category term="claude"/><category term="molly-white"/><category term="files-to-prompt"/><category term="site-upgrades"/></entry><entry><title>OpenAI DevDay 2024 live blog</title><link href="https://simonwillison.net/2024/Oct/1/openai-devday-2024-live-blog/#atom-tag" rel="alternate"/><published>2024-10-01T17:17:13+00:00</published><updated>2024-10-01T17:17:13+00:00</updated><id>https://simonwillison.net/2024/Oct/1/openai-devday-2024-live-blog/#atom-tag</id><summary type="html">
    &lt;p&gt;I'm at &lt;a href="https://openai.com/devday/"&gt;OpenAI DevDay&lt;/a&gt; in San Francisco, and I'm trying something new: a live blog, where this entry will be updated with new notes during the event.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://simonwillison.net/2024/Oct/2/not-digital-god/"&gt;OpenAI DevDay: Let’s build developer tools, not digital God&lt;/a&gt; for my notes written after the event, and &lt;a href="https://til.simonwillison.net/django/live-blog"&gt;Building an automatically updating live blog in Django&lt;/a&gt; for details about how this live blogging system worked under the hood.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&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/live-blog"&gt;live-blog&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="live-blog"/><category term="site-upgrades"/></entry><entry><title>New blog feature: Support for markdown in quotations</title><link href="https://simonwillison.net/2024/Jun/24/markdown-in-quotations/#atom-tag" rel="alternate"/><published>2024-06-24T15:51:03+00:00</published><updated>2024-06-24T15:51:03+00:00</updated><id>https://simonwillison.net/2024/Jun/24/markdown-in-quotations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/simonwillisonblog/issues/451"&gt;New blog feature: Support for markdown in quotations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Another incremental improvement to my blog. I've been collecting quotations here since 2006 - I now render them using Markdown (previously they were just plain text). &lt;a href="https://simonwillison.net/2024/Jun/17/russ-cox/"&gt;Here's one example&lt;/a&gt;. The full set of 920 (and counting) quotations can be explored &lt;a href="https://simonwillison.net/search/?type=quotation"&gt;using this search filter&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="projects"/><category term="markdown"/><category term="site-upgrades"/></entry><entry><title>Tags with descriptions</title><link href="https://simonwillison.net/2024/Jun/18/tags-with-descriptions/#atom-tag" rel="alternate"/><published>2024-06-18T16:50:07+00:00</published><updated>2024-06-18T16:50:07+00:00</updated><id>https://simonwillison.net/2024/Jun/18/tags-with-descriptions/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://simonwillison.net/dashboard/tags-with-descriptions/"&gt;Tags with descriptions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Tiny new feature on my blog: I can now add optional descriptions to my tag pages, for example on &lt;a href="https://simonwillison.net/tags/datasette/"&gt;datasette&lt;/a&gt; and &lt;a href="https://simonwillison.net/tags/sqlite-utils/"&gt;sqlite-utils&lt;/a&gt; and &lt;a href="https://simonwillison.net/tags/prompt-injection/"&gt;prompt-injection&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I built this feature on a live call this morning as an unplanned demonstration of GitHub's new &lt;a href="https://githubnext.com/projects/copilot-workspace"&gt;Copilot Workspace&lt;/a&gt; feature, where you can run a prompt against a repository and have it plan, implement and file a pull request implementing a change to the code.&lt;/p&gt;
&lt;p&gt;My prompt was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Add a feature that lets me add a description to my tag pages, stored in the database table for tags and visible on the /tags/x/ page at the top&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It wasn't as compelling a demo as I expected: Copilot Workspace currently has to stream an entire copy of each file it modifies, which can take a long time if your codebase includes several large files that need to be changed.&lt;/p&gt;
&lt;p&gt;It did create &lt;a href="https://github.com/simonw/simonwillisonblog/pull/443/commits/b48f4bd1c7ec6845b097ebc1f4fca02d97c468ef"&gt;a working implementation&lt;/a&gt; on its first try, though I had given it an extra tip not to forget the database migration. I ended up making a bunch of changes myself before I shipped it, &lt;a href="https://github.com/simonw/simonwillisonblog/pull/443"&gt;listed in the pull request&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've been using Copilot Workspace quite a bit recently as a code explanation tool - I'll prompt it to e.g. "add architecture documentation to the README" on a random repository not owned by me, then read its initial plan to see what it's figured out without going all the way through to the implementation and PR phases. Example in &lt;a href="https://twitter.com/simonw/status/1802432912568279441"&gt;this tweet&lt;/a&gt; where I figured out the rough design of the Jina AI Reader API for &lt;a href="https://simonwillison.net/2024/Jun/16/jina-ai-reader/"&gt;this post&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tagging"&gt;tagging&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/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="github"/><category term="projects"/><category term="tagging"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="site-upgrades"/></entry><entry><title>A homepage redesign for my blog's 22nd birthday</title><link href="https://simonwillison.net/2024/Jun/12/homepage-redesign/#atom-tag" rel="alternate"/><published>2024-06-12T19:59:17+00:00</published><updated>2024-06-12T19:59:17+00:00</updated><id>https://simonwillison.net/2024/Jun/12/homepage-redesign/#atom-tag</id><summary type="html">
    &lt;p&gt;This blog is 22 years old today! I wrote up &lt;a href="https://simonwillison.net/2022/Jun/12/twenty-years/"&gt;a whole bunch of higlights&lt;/a&gt; for the 20th birthday a couple of years ago. Today I'm celebrating with something a bit smaller: I finally redesigned the homepage.&lt;/p&gt;
&lt;p&gt;I publish three kinds of content on my blog: &lt;a href="https://simonwillison.net/search/?type=entry"&gt;entries&lt;/a&gt; (like this one), "&lt;a href="https://simonwillison.net/search/?type=blogmark"&gt;blogmarks&lt;/a&gt;" (aka annotated links) and &lt;a href="https://simonwillison.net/search/?type=quotation"&gt;quotations&lt;/a&gt;. Until recently the entries were the main feature on the (desktop) homepage, with blogmarks and quotations relegated to the sidebar.&lt;/p&gt;
&lt;p&gt;Back in April I &lt;a href="https://simonwillison.net/2024/Apr/25/blogmarks-that-use-markdown/"&gt;implemented Markdown support&lt;/a&gt; for my blogmarks, allowing me to include additional links and quotations in the body of those descriptions.&lt;/p&gt;
&lt;p&gt;I was inspired in this by &lt;a href="https://daringfireball.net/"&gt;Daring Fireball&lt;/a&gt;, which has long published a combination of annotated links combined with longer essay style entries.&lt;/p&gt;
&lt;p&gt;It turns out I &lt;em&gt;really like&lt;/em&gt; posting longer-form content attached to links! Here's one from &lt;a href="https://simonwillison.net/2024/Jun/12/generative-ai-is-not-going-to-build-your-engineering-team/"&gt;earlier today&lt;/a&gt; which rivals my full entries in length.&lt;/p&gt;
&lt;p&gt;These were looking pretty cramped in the sidebar:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/homepage-old.jpg" alt="Screenshot of my blog with a big entry about Thoughts on the WWDC 2024 keynote on the left and a sidebar with a long blogmark description in the sidebar on the right" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So I've done a small redesign. The left hand column on my homepage now displays entries, quotations and blogmarks as a combined list, reusing the format I already had in place for the &lt;a href="https://simonwillison.net/tags/blogging/"&gt;tag page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The right hand column is for "highlights", aka my longer form blog entries.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/homepage-new.jpg" alt="Screenshot of my blog with a blogmark on the left and a list of article headlines on the right" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The mobile version of my site was already serving content mixed together like this, so this change mainly brings the desktop version in line with the mobile one.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/simonwillisonblog/issues/438"&gt;the issue on GitHub&lt;/a&gt; and &lt;a href="https://github.com/simonw/simonwillisonblog/commit/8e38a3f51ec50501fcb6fcc19a26acde2fa5cd4b"&gt;the commit that implemented the change&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="site-upgrades"/></entry><entry><title>Blogmarks that use markdown</title><link href="https://simonwillison.net/2024/Apr/25/blogmarks-that-use-markdown/#atom-tag" rel="alternate"/><published>2024-04-25T04:34:18+00:00</published><updated>2024-04-25T04:34:18+00:00</updated><id>https://simonwillison.net/2024/Apr/25/blogmarks-that-use-markdown/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://simonwillison.net/dashboard/blogmarks-that-use-markdown/"&gt;Blogmarks that use markdown&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I needed to attach a correction to an older blogmark (my 20-year old name for short-form links with commentary on my blog) today - but the commentary field has always been text, not HTML, so I didn't have a way to add the necessary link.&lt;/p&gt;
&lt;p&gt;This motivated me to finally add optional &lt;strong&gt;Markdown&lt;/strong&gt; support for blogmarks to my blog's custom Django CMS. I then went through and added inline code markup to a bunch of different older posts, and built this Django SQL Dashboard to keep track of which posts I had updated.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="projects"/><category term="markdown"/><category term="django-sql-dashboard"/><category term="site-upgrades"/></entry><entry><title>Semi-automating a Substack newsletter with an Observable notebook</title><link href="https://simonwillison.net/2023/Apr/4/substack-observable/#atom-tag" rel="alternate"/><published>2023-04-04T17:55:28+00:00</published><updated>2023-04-04T17:55:28+00:00</updated><id>https://simonwillison.net/2023/Apr/4/substack-observable/#atom-tag</id><summary type="html">
    &lt;p&gt;I recently started sending out &lt;a href="https://simonw.substack.com/"&gt;a weekly-ish email newsletter&lt;/a&gt; consisting of content from my blog. I've mostly automated that, using &lt;a href="https://observablehq.com/@simonw/blog-to-newsletter"&gt;an Observable Notebook&lt;/a&gt; to generate the HTML. Here's how that system works.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/substack-index.jpg" alt="Screenshot of Substack: Simon Willison' Newsletter, with a big podcast promo image next to Think of language models like GhatGPT as a calculator for words, followed by two other recent newsletter headlines." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4&gt;What goes in my newsletter&lt;/h4&gt;
&lt;p&gt;My blog has three types of content: &lt;a href="https://simonwillison.net/search/?type=entry"&gt;entries&lt;/a&gt;, &lt;a href="https://simonwillison.net/search/?type=blogmark"&gt;blogmarks&lt;/a&gt; and &lt;a href="https://simonwillison.net/search/?type=quotation"&gt;quotations&lt;/a&gt;. "Blogmarks" is a name I came up with for bookmarks &lt;a href="https://simonwillison.net/2003/Nov/24/blogmarks/"&gt;in 2003&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Blogmarks and quotations show up in my blog's sidebar, entries get the main column - but on mobile the three are combined into a single flow.&lt;/p&gt;
&lt;p&gt;These live in a PostgreSQL database managed by Django. You can see them defined &lt;a href="https://github.com/simonw/simonwillisonblog/blob/main/blog/models.py"&gt;in models.py&lt;/a&gt; in my blog's open source repo.&lt;/p&gt;
&lt;p&gt;My newsletter consists of all of the new entries, blogmarks and quotations since I last sent it out. I include the entries first in reverse chronological order, since usually the entry I've just written is the one I want to use for the email subject. The blogmarks and quotations come in chronological order afterwards.&lt;/p&gt;
&lt;p&gt;I'm including the full HTML for everything: people don't need to click through back to my blog to read it, all of the content should be right there in their email client.&lt;/p&gt;
&lt;h4&gt;The Substack API: RSS and copy-and-paste&lt;/h4&gt;
&lt;p&gt;Substack doesn't yet offer an API, and &lt;a href="https://support.substack.com/hc/en-us/articles/360038433912-Does-Substack-have-an-API-"&gt;have no public plans&lt;/a&gt; to do so.&lt;/p&gt;
&lt;p&gt;They do offer an RSS feed of each newsletter though - add &lt;code&gt;/feed&lt;/code&gt; to the newsletter subdomain to get it. Mine is at &lt;a href="https://simonw.substack.com/feed"&gt;https://simonw.substack.com/feed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So we can get data back out again... but what about getting data in? I don't want to manually assemble a newsletter from all of these different sources of data.&lt;/p&gt;
&lt;p&gt;That's where copy-and-paste comes in.&lt;/p&gt;
&lt;p&gt;The Substack compose editor incorporates a well built rich-text editor. You can paste content into it and it will clean it up to fit the subset of HTML that Substack supports... but that's a pretty decent subset. Headings, paragraphs, lists, links, code blocks and images are all supported.&lt;/p&gt;
&lt;p&gt;The vast majority of content on my blog fits that subset neatly.&lt;/p&gt;
&lt;p&gt;Crucially, pasting in images as part of that rich text content Just Works: Substack automatically copies any images to their &lt;code&gt;substack-post-media&lt;/code&gt; S3 bucket and embeds links to their CDN in the body of the newsletter.&lt;/p&gt;
&lt;p&gt;So... if I can generate the intended rich-text HTML for my whole newsletter, I can copy and paste it directly into the Substack.&lt;/p&gt;
&lt;p&gt;That's exactly what my new Observable notebook does: &lt;a href="https://observablehq.com/@simonw/blog-to-newsletter"&gt;https://observablehq.com/@simonw/blog-to-newsletter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Generating HTML is a well trodden path, but I also wanted a "copy to clipboard" button that would copy the rich text version of that HTML such that pasting it into Substack would do the right thing.&lt;/p&gt;
&lt;p&gt;With a bit of help from &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard"&gt;MDN&lt;/a&gt; and &lt;a href="https://til.simonwillison.net/javascript/copy-rich-text-to-clipboard"&gt;ChatGPT (my TIL)&lt;/a&gt; I figured out the following:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;copyRichText&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;html&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;htmlContent&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;html&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Create a temporary element to hold the HTML content&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;tempElement&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;"div"&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;tempElement&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-s1"&gt;htmlContent&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-c1"&gt;body&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;tempElement&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-c"&gt;// Select the HTML content&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;range&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;createRange&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-s1"&gt;range&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;selectNode&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;tempElement&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-c"&gt;// Copy the selected HTML content to the clipboard&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;selection&lt;/span&gt; &lt;span class="pl-c1"&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-en"&gt;getSelection&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-s1"&gt;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeAllRanges&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-s1"&gt;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addRange&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;range&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;execCommand&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"copy"&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;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeAllRanges&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-c1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeChild&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;tempElement&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;This works great! Set up a button that triggers that function and clicking that button will copy a rich text version of the HTML to the clipboard, such that pasting it directly into the Substack editor has the desired effect.&lt;/p&gt;
&lt;h4&gt;Assembling the HTML&lt;/h4&gt;
&lt;p&gt;I love using &lt;a href="https://observablehq.com/"&gt;Observable Notebooks&lt;/a&gt; for this kind of project: quick data integration tools that need a UI and will likely be incrementally improved over time.&lt;/p&gt;
&lt;p&gt;Using Observable for these means I don't need to host anything and I can iterate my way to the right solution really quickly.&lt;/p&gt;
&lt;p&gt;First, I needed to retrieve my entries, blogmarks and quotations.&lt;/p&gt;
&lt;p&gt;I never built an API for my Django blog directly, but a while ago I set up a mechanism that &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/main/.github/workflows/backup.yml"&gt;exports the contents&lt;/a&gt; of my blog to &lt;a href="https://github.com/simonw/simonwillisonblog-backup"&gt;my simonwillisonblog-backup&lt;/a&gt; GitHub repository for safety, and then deploys a Datasette/SQLite copy of that data to &lt;a href="https://datasette.simonwillison.net/"&gt;https://datasette.simonwillison.net/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; offers a JSON API for querying that data, and exposes open CORS headers which means JavaScript running in Observable can query it directly.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.simonwillison.net/simonwillisonblog?sql=select+*+from+blog_entry+order+by+id+desc+limit+5"&gt;Here's an example SQL query&lt;/a&gt; running against that Datasette instance - click the &lt;code&gt;.json&lt;/code&gt; link on that page to get that data back as JSON instead.&lt;/p&gt;
&lt;p&gt;My Observable notebook can then retrieve the exact data it needs to construct the HTML for the newsletter.&lt;/p&gt;
&lt;p&gt;The smart thing to do would have been to retrieve the data from the API and then use JavaScript inside Observable to compose that together into the HTML for the newsletter.&lt;/p&gt;
&lt;p&gt;I decided to challenge myself to doing most of the work in SQL instead, and came up with the following absolute monster of a query:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;with content &lt;span class="pl-k"&gt;as&lt;/span&gt; (
  &lt;span class="pl-k"&gt;select&lt;/span&gt;
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;entry&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; type, title, created, slug,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;h3&amp;gt;&amp;lt;a href="&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://simonwillison.net/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%Y/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created)
      &lt;span class="pl-k"&gt;||&lt;/span&gt; substr(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;JanFebMarAprMayJunJulAugSepOctNovDec&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, (strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%m&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;-&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;) &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-c1"&gt;3&lt;/span&gt;) 
      &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; cast(strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%d&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-k"&gt;integer&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; slug &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;"&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; 
      &lt;span class="pl-k"&gt;||&lt;/span&gt; title &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/a&amp;gt; - &lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-k"&gt;date&lt;/span&gt;(created) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/h3&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; body
      &lt;span class="pl-k"&gt;as&lt;/span&gt; html,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; external_url
  &lt;span class="pl-k"&gt;from&lt;/span&gt; blog_entry
  &lt;span class="pl-k"&gt;union all&lt;/span&gt;
  &lt;span class="pl-k"&gt;select&lt;/span&gt;
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;blogmark&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; type,
    link_title, created, slug,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Link&amp;lt;/strong&amp;gt; &lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-k"&gt;date&lt;/span&gt;(created) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt; &amp;lt;a href="&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;||&lt;/span&gt; link_url &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;"&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      &lt;span class="pl-k"&gt;||&lt;/span&gt; link_title &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/a&amp;gt;:&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt; &lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; commentary &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/p&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      &lt;span class="pl-k"&gt;as&lt;/span&gt; html,
  link_url &lt;span class="pl-k"&gt;as&lt;/span&gt; external_url
  &lt;span class="pl-k"&gt;from&lt;/span&gt; blog_blogmark
  &lt;span class="pl-k"&gt;union all&lt;/span&gt;
  &lt;span class="pl-k"&gt;select&lt;/span&gt;
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;quotation&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; type,
    source, created, slug,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;strong&amp;gt;Quote&amp;lt;/strong&amp;gt; &lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-k"&gt;date&lt;/span&gt;(created) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;blockquote&amp;gt;&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-k"&gt;||&lt;/span&gt; replace(quotation, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;br&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;/blockquote&amp;gt;&amp;lt;p&amp;gt;&amp;lt;a href="&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt;
    coalesce(source_url, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;#&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;"&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; source &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
    &lt;span class="pl-k"&gt;as&lt;/span&gt; html,
    source_url &lt;span class="pl-k"&gt;as&lt;/span&gt; external_url
  &lt;span class="pl-k"&gt;from&lt;/span&gt; blog_quotation
),
collected &lt;span class="pl-k"&gt;as&lt;/span&gt; (
  &lt;span class="pl-k"&gt;select&lt;/span&gt;
    type,
    title,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://simonwillison.net/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%Y/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created)
      &lt;span class="pl-k"&gt;||&lt;/span&gt; substr(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;JanFebMarAprMayJunJulAugSepOctNovDec&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, (strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%m&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;-&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;) &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-c1"&gt;3&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; 
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; cast(strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%d&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-k"&gt;integer&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; slug &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      &lt;span class="pl-k"&gt;as&lt;/span&gt; url,
    created,
    html,
    external_url
  &lt;span class="pl-k"&gt;from&lt;/span&gt; content
  &lt;span class="pl-k"&gt;where&lt;/span&gt; created &lt;span class="pl-k"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;date&lt;/span&gt;(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;now&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;-&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; :numdays &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt; days&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;)   
  &lt;span class="pl-k"&gt;order by&lt;/span&gt; created &lt;span class="pl-k"&gt;desc&lt;/span&gt;
)
&lt;span class="pl-k"&gt;select&lt;/span&gt; type, title, url, created, html, external_url
&lt;span class="pl-k"&gt;from&lt;/span&gt; collected 
&lt;span class="pl-k"&gt;order by&lt;/span&gt; 
  case type 
    when &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;entry&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; then &lt;span class="pl-c1"&gt;0&lt;/span&gt; 
    else &lt;span class="pl-c1"&gt;1&lt;/span&gt; 
  end,
  case type 
    when &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;entry&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; then created 
    else &lt;span class="pl-k"&gt;-&lt;/span&gt;strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%s&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) 
  end &lt;span class="pl-k"&gt;desc&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This logic really should be in the JavaScript instead! You can &lt;a href="https://datasette.simonwillison.net/simonwillisonblog?sql=with+content+as+%28%0D%0A++select%0D%0A++++%27entry%27+as+type%2C+title%2C+created%2C+slug%2C%0D%0A++++%27%3Ch3%3E%3Ca+href%3D%22%27+%7C%7C+%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%0D%0A++++++%7C%7C+%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27+%7C%7C+%27%22%3E%27+%0D%0A++++++%7C%7C+title+%7C%7C+%27%3C%2Fa%3E+-+%27+%7C%7C+date%28created%29+%7C%7C+%27%3C%2Fh3%3E%27+%7C%7C+body%0D%0A++++++as+html%2C%0D%0A++++%27%27+as+external_url%0D%0A++from+blog_entry%0D%0A++union+all%0D%0A++select%0D%0A++++%27blogmark%27+as+type%2C%0D%0A++++link_title%2C+created%2C+slug%2C%0D%0A++++%27%3Cp%3E%3Cstrong%3ELink%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C+%27+%3Ca+href%3D%22%27%7C%7C+link_url+%7C%7C+%27%22%3E%27%0D%0A++++++%7C%7C+link_title+%7C%7C+%27%3C%2Fa%3E%3A%27+%7C%7C+%27+%27+%7C%7C+commentary+%7C%7C+%27%3C%2Fp%3E%27%0D%0A++++++as+html%2C%0D%0A++link_url+as+external_url%0D%0A++from+blog_blogmark%0D%0A++union+all%0D%0A++select%0D%0A++++%27quotation%27+as+type%2C%0D%0A++++source%2C+created%2C+slug%2C%0D%0A++++%27%3Cstrong%3EQuote%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C+%27%3Cblockquote%3E%3Cp%3E%3Cem%3E%27%0D%0A++++%7C%7C+replace%28quotation%2C+%27%0D%0A%27%2C+%27%3Cbr%3E%27%29+%7C%7C+%27%3C%2Fem%3E%3C%2Fp%3E%3C%2Fblockquote%3E%3Cp%3E%3Ca+href%3D%22%27+%7C%7C%0D%0A++++coalesce%28source_url%2C+%27%23%27%29+%7C%7C+%27%22%3E%27+%7C%7C+source+%7C%7C+%27%3C%2Fa%3E%3C%2Fp%3E%27%0D%0A++++as+html%2C%0D%0A++++source_url+as+external_url%0D%0A++from+blog_quotation%0D%0A%29%2C%0D%0Acollected+as+%28%0D%0A++select%0D%0A++++type%2C%0D%0A++++title%2C%0D%0A++++%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%7C%7C+%0D%0A++++++%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27%0D%0A++++++as+url%2C%0D%0A++++created%2C%0D%0A++++html%2C%0D%0A++++external_url%0D%0A++from+content%0D%0A++where+created+%3E%3D+date%28%27now%27%2C+%27-%27+%7C%7C+%3Anumdays+%7C%7C+%27+days%27%29+++%0D%0A++order+by+created+desc%0D%0A%29%0D%0Aselect+type%2C+title%2C+url%2C+created%2C+html%2C+external_url%0D%0Afrom+collected+%0D%0Aorder+by+%0D%0A++case+type+%0D%0A++++when+%27entry%27+then+0+%0D%0A++++else+1+%0D%0A++end%2C%0D%0A++case+type+%0D%0A++++when+%27entry%27+then+created+%0D%0A++++else+-strftime%28%27%25s%27%2C+created%29+%0D%0A++end+desc&amp;amp;numdays=7"&gt;try that query in Datasette&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are a bunch of tricks in there, but my favourite is this one:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://simonwillison.net/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%Y/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created)
  &lt;span class="pl-k"&gt;||&lt;/span&gt; substr(
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;JanFebMarAprMayJunJulAugSepOctNovDec&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;,
    (strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%m&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;-&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;) &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-c1"&gt;3&lt;/span&gt;
  ) &lt;span class="pl-k"&gt;||&lt;/span&gt;  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; cast(strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%d&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-k"&gt;integer&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; slug &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
  &lt;span class="pl-k"&gt;as&lt;/span&gt; url&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is the trick I'm using to generate the URL for each entry, blogmark and quotation.&lt;/p&gt;
&lt;p&gt;These are stored as datetime values in the database, but the eventual URLs look like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://simonwillison.net/2023/Apr/2/calculator-for-words/"&gt;https://simonwillison.net/2023/Apr/2/calculator-for-words/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So I need to turn that date into a YYYY/Mon/DD URL component.&lt;/p&gt;
&lt;p&gt;One problem: SQLite doesn't have a date format string that produces a three letter month abbreviation. But... with cunning application of the &lt;code&gt;substr()&lt;/code&gt; function and a string of all the month abbreviations I can get what I need.&lt;/p&gt;
&lt;p&gt;The above SQL query plus a little bit of JavaScript provides almost everything I need to generate the HTML for my newsletter.&lt;/p&gt;
&lt;h4&gt;Excluding previously sent content&lt;/h4&gt;
&lt;p&gt;There's one last problem to solve: I want to send a newsletter containing everything that's new since my last edition - I don't want to send out the same content twice.&lt;/p&gt;
&lt;p&gt;I came up with a delightfully gnarly solution to that as well.&lt;/p&gt;
&lt;p&gt;As mentioned earlier, Substack provides an RSS feed of previous editions. I can use that data to avoid including content that's already been sent.&lt;/p&gt;
&lt;p&gt;One problem: the Substack RSS feed does't include CORS headers, which means I can't access it directly from my notebook.&lt;/p&gt;
&lt;p&gt;GitHub offers CORS headers for every file in every repository. I already had a repo that was backing up my blog... so why not set that to backup my RSS feed from Substack as well?&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/c42b3afd6bd8cb2a4e8fa928c77426ec71552194/.github/workflows/backup.yml#L70-L74"&gt;added this&lt;/a&gt; to my existing &lt;code&gt;backup.yml&lt;/code&gt; GitHub Actions workflow:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;- &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Backup Substack&lt;/span&gt;
  &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;|-&lt;/span&gt;
&lt;span class="pl-s"&gt;    curl 'https://simonw.substack.com/feed' | \&lt;/span&gt;
&lt;span class="pl-s"&gt;      python -c "import sys, xml.dom.minidom; print(xml.dom.minidom.parseString(sys.stdin.read()).toprettyxml(indent='  '))" \&lt;/span&gt;
&lt;span class="pl-s"&gt;      &amp;gt; simonw-substack-com.xml&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I'm piping it through a tiny Python script here to pretty-print the XML before saving it, because pretty-printed XML is easier to read diffs against later on.&lt;/p&gt;
&lt;p&gt;Now &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/c42b3afd6bd8cb2a4e8fa928c77426ec71552194/simonw-substack-com.xml"&gt;simonw-substack-com.xml&lt;/a&gt; is a copy of my RSS feed in a GitHub repo, which means I can access the data directly from JavaScript running on Observable.&lt;/p&gt;
&lt;p&gt;Here's the code I wrote there to fetch that RSS feed, parse it as XML and return a string containing just the HTML of all of the posts:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;previousNewsletters&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-s1"&gt;const&lt;/span&gt; response &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-s"&gt;"https://raw.githubusercontent.com/simonw/simonwillisonblog-backup/main/simonw-substack-com.xml"&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;rss&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;text&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-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;parser&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;DOMParser&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-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;xmlDoc&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;parser&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parseFromString&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;rss&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"application/xml"&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;xpathExpression&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"//content:encoded"&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-en"&gt;namespaceResolver&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;prefix&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;const&lt;/span&gt; &lt;span class="pl-s1"&gt;ns&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s"&gt;"http://purl.org/rss/1.0/modules/content/"&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;return&lt;/span&gt; &lt;span class="pl-s1"&gt;ns&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;prefix&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt; &lt;span class="pl-c1"&gt;||&lt;/span&gt; &lt;span class="pl-c1"&gt;null&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-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;xmlDoc&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;evaluate&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-s1"&gt;xpathExpression&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-s1"&gt;xmlDoc&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-en"&gt;namespaceResolver&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-v"&gt;XPathResult&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;ANY_TYPE&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;null&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;let&lt;/span&gt; &lt;span class="pl-s1"&gt;node&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;text&lt;/span&gt; &lt;span class="pl-c1"&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-k"&gt;while&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;node&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;iterateNext&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-s1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;push&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;node&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;textContent&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-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;join&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"\n"&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;Then I span up a regular expression to extract all of the URLs from that HTML:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;previousLinks&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-s1"&gt;const&lt;/span&gt; regex &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;span class="pl-kos"&gt;(?:&lt;/span&gt;"&lt;span class="pl-c1"&gt;|&lt;/span&gt;&amp;amp;quot;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;https?:&lt;span class="pl-cce"&gt;\/&lt;/span&gt;&lt;span class="pl-cce"&gt;\/&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;^&lt;span class="pl-cce"&gt;\s&lt;/span&gt;"&amp;lt;&amp;gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-c1"&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;|&lt;/span&gt;&amp;amp;quot;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;g&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-v"&gt;Array&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;from&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;previousNewsletters&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;matchAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;regex&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-s1"&gt;match&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-s1"&gt;match&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;1&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;Added a "skip existing" toggle checkbox to my notebook:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;viewof&lt;/span&gt; &lt;span class="pl-s1"&gt;skipExisting&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Inputs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toggle&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;label&lt;/span&gt;: &lt;span class="pl-s"&gt;"Skip content sent in prior newsletters"&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;And added this code to filter the raw content based on whether or not the toggle was selected:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;skipExisting&lt;/span&gt;
  ? &lt;span class="pl-s1"&gt;raw_content&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;filter&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;e&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-c1"&gt;!&lt;/span&gt;&lt;span class="pl-s1"&gt;previousLinks&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;includes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="pl-c1"&gt;!&lt;/span&gt;&lt;span class="pl-s1"&gt;previousLinks&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;includes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;external_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-s1"&gt;raw_content&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;url&lt;/code&gt; is the URL to the post on my blog. &lt;code&gt;external_url&lt;/code&gt; is the URL to the original source of the blogmark or quotation. A match against ether of those should exclude the content from my next newsletter.&lt;/p&gt;
&lt;h4&gt;My workflow for sending a newsletter&lt;/h4&gt;
&lt;p&gt;Given all of the above, sending a newsletter out is hardly any work at all:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ensure the most recent backup of my blog has run, such that the Datasette instance contains my latest content. I do that by &lt;a href="https://github.com/simonw/simonwillisonblog-backup/actions/workflows/backup.yml"&gt;triggering this action&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;a href="https://observablehq.com/@simonw/blog-to-newsletter"&gt;https://observablehq.com/@simonw/blog-to-newsletter&lt;/a&gt; - select "Skip content sent in prior newsletters" and then click the "Copy rich text newsletter to clipboard" button.&lt;/li&gt;
&lt;li&gt;Navigate to the Substack "publish" interface and paste that content into the rich text editor.&lt;/li&gt;
&lt;li&gt;Pick a title and subheading, and maybe add a bit of introductory text.&lt;/li&gt;
&lt;li&gt;Preview it. If the preview looks good, hit "send".&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/newsletter-small.gif" alt="Animated screenshot showing the process of sending the newsletter as described above" style="max-width: 100%;" loading="lazy" /&gt;&lt;/p&gt;
&lt;h4&gt;Copy and paste APIs&lt;/h4&gt;
&lt;p&gt;I think copy and paste is under-rated as an API mechanism.&lt;/p&gt;
&lt;p&gt;There are no rate limits or API keys to worry about.&lt;/p&gt;
&lt;p&gt;It's supported by almost every application, even ones that are resistant to API integrations.&lt;/p&gt;
&lt;p&gt;It even works great on mobile phones, especially if you include a "copy to clipboard" button.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://datasette.io/plugins/datasette-copyable"&gt;datasette-copyable&lt;/a&gt; plugin for Datasette is one of my earlier explorations of this. It makes it easy to copy data out of Datasette in a variety of useful formats.&lt;/p&gt;
&lt;p&gt;This Observable newsletter project has further convinced me that the clipboard is an under-utilized mechanism for building tools to help integrate data together in creative ways.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/newsletter"&gt;newsletter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/substack"&gt;substack&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="projects"/><category term="datasette"/><category term="observable"/><category term="cors"/><category term="newsletter"/><category term="substack"/><category term="site-upgrades"/></entry><entry><title>Getting the blog back together</title><link href="https://simonwillison.net/2017/Oct/1/ship/#atom-tag" rel="alternate"/><published>2017-10-01T22:15:37+00:00</published><updated>2017-10-01T22:15:37+00:00</updated><id>https://simonwillison.net/2017/Oct/1/ship/#atom-tag</id><summary type="html">
    &lt;p&gt;Getting this blog up and running again has turned out to be one of those side-projects that keeps threatening to fall down a rabbit hole.&lt;/p&gt;

&lt;p&gt;I kicked off the work &lt;a href="https://github.com/simonw/simonwillisonblog/commit/e6d0327b37debdf820b5cfef4fb7d09a9624cea9"&gt;a couple of years ago&lt;/a&gt; at the &lt;a href="https://djangobirthday.com/"&gt;Django Birthday&lt;/a&gt; event in Lawrence, KS and got most of the way done. Then life got in the way... and last weekend I decided to try and get it over the finish line. Today, I'm forcing myself to ship even though there are still tons of things I want to do with it. Perfect is the enemy of done. That goes for my first-blog-post-in-seven-years too.&lt;/p&gt;

&lt;p&gt;I may not have blogged in seven years, but I did answer a whole bunch of questions on &lt;a href="https://www.quora.com/"&gt;Quora&lt;/a&gt;. I've imported &lt;a href="/tags/quora/"&gt;my Quora answers&lt;/a&gt; (extracted using &lt;a href="https://openuserjs.org/scripts/simonw/Quora_Answer_Export"&gt;this userscript&lt;/a&gt;) and used them to help fill out the intervening years. I plan to do the same with a few other types of content as well.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="quora"/><category term="site-upgrades"/></entry><entry><title>1000th Blogmark</title><link href="https://simonwillison.net/2004/Aug/26/milestone/#atom-tag" rel="alternate"/><published>2004-08-26T00:30:54+00:00</published><updated>2004-08-26T00:30:54+00:00</updated><id>https://simonwillison.net/2004/Aug/26/milestone/#atom-tag</id><summary type="html">
    &lt;p id="p-0"&gt;I just posted my 1000th &lt;a href="http://simon.incutio.com/blogmarks/"&gt;blogmark&lt;/a&gt;. I can't emphasize enough how much of an impact this &lt;a href="/2003/Nov/24/blogmarks/"&gt;15 minute hack&lt;/a&gt; has had on both my browsing and my blogging habits. While I still tend to leave browser windows open for days at a time, I now at least have a procedure for getting rid of the ones that still interest me. More importantly, having blogmarks has eliminated the temptation to write a full blog entry (with quotation) just to share a link. This has dramatically reduced my posting rate, but has meant that when I do post an entry I usually have something moderately interesting to say.&lt;/p&gt;

&lt;p id="p-1"&gt;To celebrate this personal milestone, I've linked up the rudimentary LIKE query search engine I've been using for a while on the blogmarks index page. My long term aim is still to integrate them with my main content and add comments in the style of &lt;a href="http://photomatt.net/"&gt;photomatt&lt;/a&gt;, but that would require more time spent hacking on my blogging system (or switching to &lt;a href="http://wordpress.org/"&gt;WordPress&lt;/a&gt;) than I have to spend right now.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="site-upgrades"/></entry><entry><title>More blogmark tweaks</title><link href="https://simonwillison.net/2003/Dec/11/blogmarkComments/#atom-tag" rel="alternate"/><published>2003-12-11T00:28:50+00:00</published><updated>2003-12-11T00:28:50+00:00</updated><id>https://simonwillison.net/2003/Dec/11/blogmarkComments/#atom-tag</id><summary type="html">
    &lt;p&gt;I'm up to 110 &lt;a href="http://simon.incutio.com/blogmarks/"&gt;blogmarks&lt;/a&gt; now, and from my point of view they're the single most useful feature I've added to this site in a long time. I've modified my &lt;a href="/2003/Dec/10//" title="Simon Willison: Archive for 10th December 2003"&gt;day archive pages&lt;/a&gt; to show the blogmarks added on that day, and I'm considering adding them to individual entry pages as well so that an entry is displayed along with the blogmarks added while that entry was at the top of my blog. The idea there is that I could aim to blogmark "related items" for the top entry, although obviously unrelated sites would end up in there as well.&lt;/p&gt;

&lt;p&gt;The other thing I'm currently mulling over is whether or not blogmarks should allow comments. &lt;a href="http://www.kottke.org/"&gt;Jason Kottke's&lt;/a&gt; inline links get a decent amount of comment traffic but I'm not sure mine would get enough individual traffic to warrant a comments thread for each one. I could always add comments to the &lt;a href="http://simon.incutio.com/blogmarks/2003/12/10/" title="Blogmarks for 10th December 2003"&gt;blogmark daily archive pages&lt;/a&gt;, although that could get confusing on days with a lot of comment-worthy links.&lt;/p&gt;

&lt;p&gt;Does anyone actually look at the blogmarks? Would you comment on them or read the comments if they were available?&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jason-kottke"&gt;jason-kottke&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="jason-kottke"/><category term="site-upgrades"/></entry><entry><title>Blogmarks</title><link href="https://simonwillison.net/2003/Nov/24/blogmarks/#atom-tag" rel="alternate"/><published>2003-11-24T00:52:16+00:00</published><updated>2003-11-24T00:52:16+00:00</updated><id>https://simonwillison.net/2003/Nov/24/blogmarks/#atom-tag</id><summary type="html">
    &lt;p&gt;This entry was going to be another list of links, together with a note about how much I really needed to set up a separate link blog. Then I realised that it would make more sense just to set one up so that's exactly what I've done. I still need to implement the archive but it's &lt;span class="in-joke" title="and I am likely to be eaten by a grue"&gt;getting dark&lt;/span&gt; so I'm posting this and heading home.&lt;/p&gt;

&lt;p&gt;My main points of inspiration were Paul Hammond's &lt;a href="http://www.paranoidfish.org/links/"&gt;bookmark store&lt;/a&gt;, Mark Pilgrim's &lt;a href="http://diveintomark.org/"&gt;b-links&lt;/a&gt;, Anil Dash's &lt;a href="http://www.dashes.com/links/"&gt;Daily Links&lt;/a&gt; and Jason Kottke's &lt;a href="http://www.kottke.org/remainder/"&gt;Remaindered Links&lt;/a&gt;. Since there didn't seem to be any naming convention I decided to call them blogmarks, which &lt;a href="http://www.google.com/search?q=blogmarks" title="Google Search: blogmarks"&gt;isn't a new term&lt;/a&gt; but doesn't seem to have a widely accepted meaning yet either.&lt;/p&gt;

&lt;p&gt;The system is powered by a simple bookmarklet. To make things a little more interesting I'm capturing the referral information and using it to automatically generate the 'via' link; since the title of the previous page isn't available in Javascript I extract is using a server side script instead. I swayed briefly between using page extracts a la Hammond or sarcastic commentary a la Pilgrim and decided that commentary would be far more fun.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/anil-dash"&gt;anil-dash&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jason-kottke"&gt;jason-kottke&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mark-pilgrim"&gt;mark-pilgrim&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/paul-hammond"&gt;paul-hammond&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="anil-dash"/><category term="blogging"/><category term="jason-kottke"/><category term="mark-pilgrim"/><category term="paul-hammond"/><category term="site-upgrades"/></entry><entry><title>New anti-comment-spam measure</title><link href="https://simonwillison.net/2003/Oct/13/linkRedirects/#atom-tag" rel="alternate"/><published>2003-10-13T08:22:09+00:00</published><updated>2003-10-13T08:22:09+00:00</updated><id>https://simonwillison.net/2003/Oct/13/linkRedirects/#atom-tag</id><summary type="html">
    &lt;p&gt;I've added a new anti-comment-spam measure to this site. The majority of comment spam exists for one reason and one reason only to increase the Google PageRank of the site linked from the spam and specifically to increase its ranking for the term used in the link. This is why so many comment spams include links like this: &lt;a href="http://jeremy.zawodny.com/blog/archives/001002.html"&gt;Cheap Viagra&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cut off the PageRank boost and you cut off the advantage of spamming, simple as that. I've altered my comments system to redirect ALL outgoing links through a simple redirect script, and added that script to &lt;a href="/robots.txt"&gt;my robots.txt file&lt;/a&gt;. Links still work fine (even the referral information persists across the redirect) but Google will ignore them completely when calculating PageRank.&lt;/p&gt;

&lt;p&gt;Will this reduce the floods of comment spam my site receives? Probably not; I've added a note about the restriction to my 'add comment' form but I doubt many spammers bother to read much about the sites they are targetting. What's really needed is for this technique to become widespread by being integrated in to existing blogging tools - are you listening Moveable Type hackers?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; Sencer has &lt;a href="http://www.sencer.de/index.php?p=81" title="Google Comment Spammers, Redirects and PR"&gt;pointed out&lt;/a&gt; in the comments that PageRank persists over redirects, and Google appears to ignore robots.txt when used to hide a redirecting page. I've updated my redirection script to use javascript to power the redirect (with a link for people with javascript disabled) and an extra meta tag to remind Google not to follow the link. This has the unfortunate side effect that referral information no longer persists across the redirect.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/robots-txt"&gt;robots-txt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/spam"&gt;spam&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="robots-txt"/><category term="spam"/><category term="site-upgrades"/></entry><entry><title>Signing comments on blogs</title><link href="https://simonwillison.net/2003/Jul/22/signingComments/#atom-tag" rel="alternate"/><published>2003-07-22T21:10:50+00:00</published><updated>2003-07-22T21:10:50+00:00</updated><id>https://simonwillison.net/2003/Jul/22/signingComments/#atom-tag</id><summary type="html">
    &lt;p&gt;Adrian Holovaty has implemented &lt;a href="http://www.holovaty.com/blog/archive/2003/07/22/0211" title="New weblog feature: Reserved comment names"&gt;reserved comment names&lt;/a&gt; in his blog, a feature that prevents anyone apart from him from using the names "Adrian", "Adrian H." or "Adrian Holovaty" when posting a comment. François Nonnenmacher suggests &lt;a href="http://www.padawan.info/trusted_comments.html" title="Trusted comments"&gt;extending the idea&lt;/a&gt; to allow people to "confirm" their authorship of comments on any blog using a TrackBack sent to their site that in turn causes them to be sent an alert email, which they can then use to confirm their comment. I like his idea of authentication based on &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt;s (email addresses are no good; they should not be publically displayed for fear of spam harvesters) but I think I've come up with an alternative authentication scheme that removes the need for the user to manually confirm authorship. This is pretty complicated, so bare with me.&lt;/p&gt;

&lt;ol&gt;
 &lt;li&gt;The comment author enter's their comment in to a form on the site. They see a standard icon indicating that the blog in question supports comment signing. Rather than manually entering their name and &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt;, they activate a bookmarklet that they have previously added to their browser.&lt;/li&gt;
 &lt;li&gt;The bookmarklet fills in the name and &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt; fields for them. It also takes the comment, appends a secret key (stored in the bookmarklet) and finds the MD5 hash of the new string, using the &lt;a href="http://pajhome.org.uk/crypt/md5/index.html"&gt;Javascript MD5 library&lt;/a&gt;. It inserts this hash in to a hidden field in the comment form.&lt;/li&gt;
 &lt;li&gt;The user can now submit the new comment. That's all they have to do.&lt;/li&gt;
 &lt;li&gt;The weblog server now kicks in to action. If the comment has not been signed (there is no hash in the hidden field) it adds the comment normally, noting that it should be displayed as an "unsigned" comment on the comments page. End of story.&lt;/li&gt;
 &lt;li&gt;If it &lt;em&gt;has&lt;/em&gt; been signed, the server has some work to do. First it must start loading the &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt; indicated by the user on the comment form. It is looking for a &lt;code&gt;&amp;lt;link rel="signature"&amp;gt;&lt;/code&gt; element, which will provide the &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt; of a signature authenticating web service. If the &amp;lt;/head&amp;gt; tag is reached, the system can assume the link element does not exist and can mark the comment as "unsigned",&lt;/li&gt;
 &lt;li&gt;If the web service is found, the server can now send it the comment and the User's site &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt;. The web service (which knows the user's secret key) will respond with a hash created in the same way as the one constructed by the bookmarklet.&lt;/li&gt;
 &lt;li&gt;If the hash returned by the web service matches the hash provided by the bookmarklet, the comment is considered "signed". The server can store it as such, and later display it with an icon or style that indicates it is a signed comment. If they do not match, the server can either store the comment as "unsigned" or even flag it as "untrusted", since it was incorrectly signed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As you can see, it's a relatively complicated system. The comment authors must have a custom bookmarklet and add a tag to their home page indicating their authenticating web service &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt;. Note that they do &lt;em&gt;not&lt;/em&gt; need to host the authentication web service themselves - they can instead point to one run by someone else who they trust (trust here is essential as the web service must know the user's private key). Meanwhile, the blogging system needs to be able to perform &lt;acronym title="HyperText Transfer Protocol"&gt;HTTP&lt;/acronym&gt; requests.&lt;/p&gt;

&lt;p&gt;The key advantage of my system is that, being based on MD5, it is relatively easy to implement (as opposed to a system based on something like &lt;acronym title="Pretty Good Privacy"&gt;PGP&lt;/acronym&gt;). Provided no one points out any immediate flaws, I would happily construct a prototype in &lt;acronym title="PHP: Hypertext Preprocessor"&gt;PHP&lt;/acronym&gt;. I'm sure a Perl implementation for Moveable Type users would not prove much of a challenge to any talented plugin author.&lt;/p&gt;

&lt;p&gt;Security wise, it strikes me that the weakest link is the client side bookmarklet which comment authors would need to use. However, comment signing is not the most critical security application in the world and comment authors could easily change their password by updating their bookmarklet and alerting their signature web-service provider (which could even be themselves) of the change.&lt;/p&gt;

&lt;p&gt;And if the signature idea doesn't win any favour, the idea of having a bookmarklet to fill in your name and &lt;acronym title="Uniform Resource Locator"&gt;URL&lt;/acronym&gt; in blog comment forms is one I've been meaning to share for some time.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/adrian-holovaty"&gt;adrian-holovaty&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hashing"&gt;hashing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/trackback"&gt;trackback&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="adrian-holovaty"/><category term="blogging"/><category term="hashing"/><category term="security"/><category term="trackback"/><category term="site-upgrades"/></entry><entry><title>Small design tweak, big difference</title><link href="https://simonwillison.net/2003/Jun/14/smallDesignTweak/#atom-tag" rel="alternate"/><published>2003-06-14T02:25:31+00:00</published><updated>2003-06-14T02:25:31+00:00</updated><id>https://simonwillison.net/2003/Jun/14/smallDesignTweak/#atom-tag</id><summary type="html">
    &lt;p&gt;I've changed from using the day as the principle heading on the front page to using the title of each post instead. This is quite a minor alteration, but I expect it to have a relatively large impact on my blogging habits. For the past year I have treated my blog as a daily endeavour, thanks almost entirely to the way the site was layed out. This was intentional; when I orginally launched blog I made the decision to keep each entry as part of an ongoing narrative, with no individual entry titles and permalinks to entries in the context of the day they were posted.&lt;/p&gt;

&lt;p&gt;The decision not to use entry titles turned out to be &lt;a href="/2003/Mar/25/dateCentricVsEntryCentric/" title="Date-centric vs Entry-centric"&gt;a costly mistake&lt;/a&gt;, which I eventually fixed &lt;a href="/2003/Apr/23/titlesAllTheWay/" title="Titles all the way"&gt;back in April&lt;/a&gt;. The problem with the date headers was far more subtle: over the past few months, I've found myself frequently struggling to rush out an entry before midnight in a bid to keep up the "daily" nature of the site. This bizzare compulsion was spurred on by the glaring hole left in my front page if I missed a day.&lt;/p&gt;

&lt;p&gt;By removing the day headers entirely, I hope to shift the focus of this blog from religious daily updates to entries with a little more substance. I think the psychology of a blog's design is easily under-rated; I've already noticed that my blog entries have been getting longer since I started adding entry titles. At any rate, with this latest design tweak I certainly won't be rushing out poor quality entries before midnight any more.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/design"&gt;design&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="design"/><category term="site-upgrades"/></entry><entry><title>Category specific RSS feeds</title><link href="https://simonwillison.net/2003/Apr/8/categorySpecificRssFeeds/#atom-tag" rel="alternate"/><published>2003-04-08T21:59:12+00:00</published><updated>2003-04-08T21:59:12+00:00</updated><id>https://simonwillison.net/2003/Apr/8/categorySpecificRssFeeds/#atom-tag</id><summary type="html">
    &lt;p&gt;I frequently check the &lt;a href="http://mechanicalcat.net/pyblagg.html"&gt;Python Programmer Weblogs&lt;/a&gt; page for an addition to my daily blogging fix. It's a simple but very effective idea: Subscribe an aggregator to a bunch of feeds about a similar topic and publish the results for all to see. I'm one of the more prolific bloggers of the ones listed there, and since I tend to post a whole bunch of entries in a relatively narrow timeframe my stuff often appears in a big lump on the front page of the site. I've been feeling slightly guilty about this, as most of my posts have nothing to do with Python at all. So I've finally got my act together and knocked out &lt;a href="http://simon.incutio.com/categories/"&gt;RSS feeds for individual blog categories&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Python Programmer Weblogs script apparently scrapes &lt;a href="http://lowlife.jp/cgi-bin/moin.cgi/PythonProgrammersWeblog" title="PythonProgrammersWeblog"&gt;this wiki page&lt;/a&gt; for feed information, so I've updated the link on there to my RSS feed.&lt;/p&gt;


    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="site-upgrades"/></entry><entry><title>Pingback implemented</title><link href="https://simonwillison.net/2002/Sep/2/pingBackImplemented/#atom-tag" rel="alternate"/><published>2002-09-02T15:18:27+00:00</published><updated>2002-09-02T15:18:27+00:00</updated><id>https://simonwillison.net/2002/Sep/2/pingBackImplemented/#atom-tag</id><summary type="html">
    &lt;p&gt;I've implemented PingBack on my blog. PingBack is a system for tracking who is linking to your blog in a controlled way, based on a &lt;a href="http://www.kryogenix.org/days/000138.cas" title="Making TrackBack happen automatically"&gt;post by Stuart&lt;/a&gt; a few months ago. The idea is that when you link to a PingBack enabled blog you (or your blogging tool) should send an XML-RPC "ping" to that blog's PingBack server telling it where you have linked to and where you linked from. The PingBack server can then grab your page, check that the link is there and extract a title and short description from the blog. The system is an alternative to (and was inspired by) MoveableType's &lt;a href="http://www.moveabletype.org/trackback/"&gt;TrackBack&lt;/a&gt; feature. Stuart and I are actively developing the idea and will be releasing code and documentation to help other people experiment with the system in the near future.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pingback"&gt;pingback&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stuart-langridge"&gt;stuart-langridge&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/trackback"&gt;trackback&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xml-rpc"&gt;xml-rpc&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="pingback"/><category term="projects"/><category term="stuart-langridge"/><category term="trackback"/><category term="xml-rpc"/><category term="site-upgrades"/></entry><entry><title>Fixed validation again</title><link href="https://simonwillison.net/2002/Jun/16/fixedValidationAgain/#atom-tag" rel="alternate"/><published>2002-06-16T12:19:15+00:00</published><updated>2002-06-16T12:19:15+00:00</updated><id>https://simonwillison.net/2002/Jun/16/fixedValidationAgain/#atom-tag</id><summary type="html">
    &lt;p&gt;The road to &lt;a href="http://validator.w3.org/check/referer" title="W3C Validator results for this page"&gt;validity&lt;/a&gt; is frought with peril. I've just fixed another small group of errors that were preventing this page from validating (after spotting the ominous W3C validator in today's user-agent logs). This time is was a couple of forgotten &amp;lt;/p&amp;gt; tags and an unescaped ampersand.&lt;/p&gt;

&lt;p&gt;There has to be a technological way of helping avoid these errors. Originally I wanted to be able to edit my entries in some kind of specialised markup language (such as &lt;a href="http://c2.com/cgi/wiki?TextFormattingRules"&gt;WikiText&lt;/a&gt; or UBBCode) that the blogging sofftware could convert into valid XHTML, but I quickly realised that the most flexible markup language for blog entries is XHTML itself thanks to the built in support for everything from quotes to lists and code samples.&lt;/p&gt;

&lt;p&gt;Thinking about it, almost all of the common errors I am experiencing come from the XML parser rather than the rules governing XHTML. I need an XML parser that examines each post as (or before) it is added to the blog and checks for well-formedness. Expat (used in PHP for event based XML parsing) does not validate documents against a DTD but it DOES die with an error if an XML document is malformed. It looks like it could be just what I need.&lt;/p&gt;

&lt;p&gt;The ideal alternative would be for the W3C to create a web service back end for their validator so blogging software can check the validity of new entries automatically.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="site-upgrades"/></entry></feed>