<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: newsletter</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/newsletter.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-04-02T05:15:04+00:00</updated><author><name>Simon Willison</name></author><entry><title>March 2026 sponsors-only newsletter</title><link href="https://simonwillison.net/2026/Apr/2/march-newsletter/#atom-tag" rel="alternate"/><published>2026-04-02T05:15:04+00:00</published><updated>2026-04-02T05:15:04+00:00</updated><id>https://simonwillison.net/2026/Apr/2/march-newsletter/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent the March edition of my &lt;a href="https://github.com/sponsors/simonw/"&gt;sponsors-only monthly newsletter&lt;/a&gt;. If you are a sponsor (or if you start a sponsorship now) you can &lt;a href="https://github.com/simonw-private/monthly/blob/main/2026-03-march.md"&gt;access it here&lt;/a&gt;. In this month's newsletter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More agentic engineering patterns&lt;/li&gt;
&lt;li&gt;Streaming experts with MoE models on a Mac&lt;/li&gt;
&lt;li&gt;Model releases in March&lt;/li&gt;
&lt;li&gt;Vibe porting&lt;/li&gt;
&lt;li&gt;Supply chain attacks against PyPI and NPM&lt;/li&gt;
&lt;li&gt;Stuff I shipped&lt;/li&gt;
&lt;li&gt;What I'm using, March 2026 edition&lt;/li&gt;
&lt;li&gt;And a couple of museums&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/8b5fa061937842659dbcd5bd676ce0e8"&gt;a copy of the February newsletter&lt;/a&gt; as a preview of what you'll get. Pay $10/month to stay a month ahead of the free copy!&lt;/p&gt;

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



</summary><category term="newsletter"/></entry><entry><title>February sponsors-only newsletter</title><link href="https://simonwillison.net/2026/Mar/2/february-newsletter/#atom-tag" rel="alternate"/><published>2026-03-02T14:53:15+00:00</published><updated>2026-03-02T14:53:15+00:00</updated><id>https://simonwillison.net/2026/Mar/2/february-newsletter/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent the February edition of my &lt;a href="https://github.com/sponsors/simonw/"&gt;sponsors-only monthly newsletter&lt;/a&gt;. If you are a sponsor (or if you start a sponsorship now) you can &lt;a href="https://github.com/simonw-private/monthly/blob/main/2026-02-february.md"&gt;access it here&lt;/a&gt;. In this month's newsletter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More OpenClaw, and Claws in general&lt;/li&gt;
&lt;li&gt;I started a not-quite-a-book about Agentic Engineering&lt;/li&gt;
&lt;li&gt;StrongDM, Showboat and Rodney&lt;/li&gt;
&lt;li&gt;Kākāpō breeding season&lt;/li&gt;
&lt;li&gt;Model releases&lt;/li&gt;
&lt;li&gt;What I'm using, February 2026 edition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/36f567d1b3f8bb4ab4d872d477fbb295"&gt;a copy of the January newsletter&lt;/a&gt; as a preview of what you'll get. Pay $10/month to stay a month ahead of the free copy!&lt;/p&gt;
&lt;p&gt;I use Claude as a proofreader for spelling and grammar via &lt;a href="https://simonwillison.net/guides/agentic-engineering-patterns/prompts/#proofreader"&gt;this prompt&lt;/a&gt; which also asks it to "Spot any logical errors or factual mistakes". I'm delighted to report that Claude Opus 4.6 called me out on this one:&lt;/p&gt;
&lt;p&gt;&lt;img alt="5. &amp;quot;No new chicks for four years (due to a lack of fruiting rimu trees)&amp;quot;
The phrasing &amp;quot;lack of fruiting rimu trees&amp;quot; is slightly imprecise. The issue isn't that rimu trees failed to fruit at all, but that there was no mass fruiting (masting) event, which is the specific trigger for kākāpō breeding. Consider &amp;quot;due to a lack of rimu masting&amp;quot; or &amp;quot;due to a lack of mass rimu fruiting.&amp;quot;" src="https://static.simonwillison.net/static/2026/claude-fact-check.jpg" /&gt;&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/kakapo"&gt;kakapo&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;&lt;/p&gt;



</summary><category term="newsletter"/><category term="kakapo"/><category term="claude"/></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>January sponsors-only newsletter is out</title><link href="https://simonwillison.net/2026/Feb/3/january/#atom-tag" rel="alternate"/><published>2026-02-03T06:36:10+00:00</published><updated>2026-02-03T06:36:10+00:00</updated><id>https://simonwillison.net/2026/Feb/3/january/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent the January edition of my &lt;a href="https://github.com/sponsors/simonw/"&gt;sponsors-only monthly newsletter&lt;/a&gt;. If you are a sponsor (or if you start a sponsorship now) you can &lt;a href="https://github.com/simonw-private/monthly/blob/main/2026-01-january.md"&gt;access it here&lt;/a&gt;. In the newsletter for January:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LLM predictions for 2026&lt;/li&gt;
&lt;li&gt;Coding agents get even more attention&lt;/li&gt;
&lt;li&gt;Clawdbot/Moltbot/OpenClaw went very viral&lt;/li&gt;
&lt;li&gt;Kakapo breeding season is off to a really strong start&lt;/li&gt;
&lt;li&gt;New options for sandboxes&lt;/li&gt;
&lt;li&gt;Web browsers are the "hello world" of coding agent swarms&lt;/li&gt;
&lt;li&gt;Sam Altman addressed the Jevons paradox for software engineering&lt;/li&gt;
&lt;li&gt;Model releases and miscellaneous extras&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/13e595a236218afce002e9aeafd75cd0"&gt;a copy of the December newsletter&lt;/a&gt; as a preview of what you'll get. Pay $10/month to stay a month ahead of the free copy!&lt;/p&gt;

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



</summary><category term="newsletter"/></entry><entry><title>December 2025 sponsors-only newsletter</title><link href="https://simonwillison.net/2026/Jan/2/december/#atom-tag" rel="alternate"/><published>2026-01-02T04:33:47+00:00</published><updated>2026-01-02T04:33:47+00:00</updated><id>https://simonwillison.net/2026/Jan/2/december/#atom-tag</id><summary type="html">
    &lt;p&gt;I sent the December edition of my &lt;a href="https://github.com/sponsors/simonw/"&gt;sponsors-only monthly newsletter&lt;/a&gt;. If you are a sponsor (or if you start a sponsorship now) you can &lt;a href="https://github.com/simonw-private/monthly/blob/main/2025-12-december.md"&gt;access a copy here&lt;/a&gt;. In the newsletter this month:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An in-depth review of LLMs in 2025&lt;/li&gt;
&lt;li&gt;My coding agent projects in December&lt;/li&gt;
&lt;li&gt;New models for December 2025&lt;/li&gt;
&lt;li&gt;Skills are an open standard now&lt;/li&gt;
&lt;li&gt;Claude's "Soul Document"&lt;/li&gt;
&lt;li&gt;Tools I'm using at the moment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/fc34b780a9ae19b6be5d732078a572c8"&gt;a copy of the November newsletter&lt;/a&gt; as a preview of what you'll get. Pay $10/month to stay a month ahead of the free copy!&lt;/p&gt;

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



</summary><category term="newsletter"/></entry><entry><title>Substack Network error = security content they don't allow to be sent</title><link href="https://simonwillison.net/2025/Dec/28/substack-network-error/#atom-tag" rel="alternate"/><published>2025-12-28T04:16:27+00:00</published><updated>2025-12-28T04:16:27+00:00</updated><id>https://simonwillison.net/2025/Dec/28/substack-network-error/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent out the &lt;a href="https://simonw.substack.com/p/a-new-way-to-extract-detailed-transcripts"&gt;latest edition&lt;/a&gt; of the newsletter version of this blog. It's a long one! Turns out I wrote a lot of stuff in the past 10 days.&lt;/p&gt;
&lt;p&gt;The newsletter is out two days later than I had planned because I kept running into an infuriating issue with Substack: it would refuse to save my content with a "Network error" and "Not saved" and I couldn't figure out why.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of the Substack UI, with a Network error message on purple and a Not saved message higher up. The content in that editor includes an explanation of a SQL injection vulnerability." src="https://static.simonwillison.net/static/2025/substack-error.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;So I &lt;a href="https://chatgpt.com/share/6950ad7d-6948-8006-9833-201d2edff1be"&gt;asked ChatGPT to dig into it&lt;/a&gt;, which dug up &lt;a href="https://news.ycombinator.com/item?id=43793526"&gt;this Hacker News&lt;/a&gt; post about the string &lt;code&gt;/etc/hosts&lt;/code&gt; triggering an error.&lt;/p&gt;
&lt;p&gt;And yeah, it turns out my newsletter included &lt;a href="https://simonwillison.net/2025/Dec/18/ssrf-clickhouse-postgresql/"&gt;this post&lt;/a&gt; describing a SQL injection attack against ClickHouse and PostgreSQL which included the full exploit that was used.&lt;/p&gt;
&lt;p&gt;Deleting that annotated example exploit allowed me to send the letter!&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sql-injection"&gt;sql-injection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&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;/p&gt;



</summary><category term="sql-injection"/><category term="security"/><category term="newsletter"/><category term="substack"/></entry><entry><title>I sent out my November sponsor newsletter</title><link href="https://simonwillison.net/2025/Dec/1/november/#atom-tag" rel="alternate"/><published>2025-12-01T20:53:18+00:00</published><updated>2025-12-01T20:53:18+00:00</updated><id>https://simonwillison.net/2025/Dec/1/november/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent out the November edition of my &lt;a href="https://github.com/sponsors/simonw/"&gt;sponsors-only monthly newsletter&lt;/a&gt;. If you are a sponsor (or if you start a sponsorship now) you can &lt;a href="https://github.com/simonw-private/monthly/blob/main/2025-11-november.md"&gt;access a copy here&lt;/a&gt;. In the newsletter this month:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The best model for code changed hands four times&lt;/li&gt;
&lt;li&gt;Significant open weight model releases&lt;/li&gt;
&lt;li&gt;Nano Banana Pro&lt;/li&gt;
&lt;li&gt;My major coding projects with LLMs this month&lt;/li&gt;
&lt;li&gt;Prompt injection news for November&lt;/li&gt;
&lt;li&gt;Pelican on a bicycle variants&lt;/li&gt;
&lt;li&gt;Two YouTube videos and a podcast&lt;/li&gt;
&lt;li&gt;Miscellaneous extras&lt;/li&gt;
&lt;li&gt;Tools I'm using at the moment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/3385bc8c83a8157557f06865a0302753"&gt;a copy of the October newsletter&lt;/a&gt; as a preview of what you'll get. Pay $10/month to stay a month ahead of the free copy!&lt;/p&gt;

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



</summary><category term="newsletter"/></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>October 2025 sponsors-only newsletter</title><link href="https://simonwillison.net/2025/Nov/1/sponsors-only-newsletter/#atom-tag" rel="alternate"/><published>2025-11-01T22:11:58+00:00</published><updated>2025-11-01T22:11:58+00:00</updated><id>https://simonwillison.net/2025/Nov/1/sponsors-only-newsletter/#atom-tag</id><summary type="html">
    &lt;p&gt;I just hit send on the October edition of my &lt;a href="https://github.com/sponsors/simonw/"&gt;sponsors-only monthly newsletter&lt;/a&gt;. If you are a sponsor (or if you start a sponsorship now) you can &lt;a href="https://github.com/simonw-private/monthly/blob/main/2025-10-october.md"&gt;access a copy here&lt;/a&gt;. In the newsletter this month:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Coding agents and "vibe engineering"&lt;/li&gt;
&lt;li&gt;Claude Code for web&lt;/li&gt;
&lt;li&gt;NVIDIA DGX Spark&lt;/li&gt;
&lt;li&gt;Claude Skills&lt;/li&gt;
&lt;li&gt;OpenAI DevDay and GitHub Universe&lt;/li&gt;
&lt;li&gt;Python 3.14&lt;/li&gt;
&lt;li&gt;October in Chinese Al model releases&lt;/li&gt;
&lt;li&gt;Miscellaneous extras&lt;/li&gt;
&lt;li&gt;Tools I'm using at the moment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/d6d4d86afc0d76767c63f23fc5137030"&gt;a copy of the September newsletter&lt;/a&gt; as a preview of what you'll get. Pay $10/month to stay a month ahead of the free copy!&lt;/p&gt;

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



</summary><category term="newsletter"/></entry><entry><title>September monthly sponsors newsletter</title><link href="https://simonwillison.net/2025/Oct/1/sponsors-only-newsletter/#atom-tag" rel="alternate"/><published>2025-10-01T05:33:03+00:00</published><updated>2025-10-01T05:33:03+00:00</updated><id>https://simonwillison.net/2025/Oct/1/sponsors-only-newsletter/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent out the September edition of my &lt;a href="https://github.com/sponsors/simonw/"&gt;sponsors-only monthly newsletter&lt;/a&gt;. If you are a sponsor (or if you start a sponsorship now) you can &lt;a href="https://github.com/simonw-private/monthly/blob/main/2025-09-september.md"&gt;access a copy here&lt;/a&gt;. The sections this month are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Best model for code? GPT-5-Codex... then Claude 4.5 Sonnet&lt;/li&gt;
&lt;li&gt;I've grudgingly accepted a definition for "agent"&lt;/li&gt;
&lt;li&gt;GPT-5 Research Goblin and Google AI Mode&lt;/li&gt;
&lt;li&gt;Claude has Code Interpreter now&lt;/li&gt;
&lt;li&gt;The lethal trifecta in the Economist&lt;/li&gt;
&lt;li&gt;Other significant model releases&lt;/li&gt;
&lt;li&gt;Notable AI success stories&lt;/li&gt;
&lt;li&gt;Video models are zero-shot learners and reasoners&lt;/li&gt;
&lt;li&gt;Tools I'm using at the moment&lt;/li&gt;
&lt;li&gt;Other bits and pieces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/43bf3bd7f9951a8e82a9e61b53399ede"&gt;a copy of the August newsletter&lt;/a&gt; as a preview of what you'll get. Pay $10/month to stay a month ahead of the free copy!&lt;/p&gt;

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



</summary><category term="newsletter"/></entry><entry><title>August 2025 newsletter</title><link href="https://simonwillison.net/2025/Sep/1/august-2025/#atom-tag" rel="alternate"/><published>2025-09-01T19:41:45+00:00</published><updated>2025-09-01T19:41:45+00:00</updated><id>https://simonwillison.net/2025/Sep/1/august-2025/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent out my August 2025 &lt;strong&gt;&lt;a href="https://github.com/sponsors/simonw"&gt;sponsors-only newsletter&lt;/a&gt;&lt;/strong&gt; summarizing the past month in LLMs and my other work. Topics included GPT-5, gpt-oss, image editing models (Qwen-Image-Edit and Gemini Nano Banana), other significant model releases and the tools I'm using at the moment.&lt;/p&gt;
&lt;p&gt;If you'd like a preview of the newsletter, here's &lt;a href="https://gist.github.com/simonw/722fc2f242977cb185838353776d14f4"&gt;the July 2025 edition&lt;/a&gt; I sent out a month ago.&lt;/p&gt;
&lt;p&gt;New sponsors get access to the full archive. If you start sponsoring for $10/month or more right now you'll get instant access to &lt;a href="https://github.com/simonw-private/monthly/blob/main/2025-08-august.md"&gt;the August edition&lt;/a&gt; in my &lt;code&gt;simonw-private/monthly&lt;/code&gt; GitHub repository.&lt;/p&gt;
&lt;p&gt;If you've already read &lt;a href="https://simonwillison.net/2025/Aug/"&gt;all 85 posts&lt;/a&gt; I wrote in August the newsletter acts mainly as a recap, but I've had positive feedback from people who prefer to get the monthly edited highlights over reading the firehose that is my blog!&lt;/p&gt;
&lt;p&gt;Here's the table of contents for the August newsletter:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;GPT-5&lt;/li&gt;
&lt;li&gt;OpenAl's open models: gpt-oss-120b and gpt-oss-20b&lt;/li&gt;
&lt;li&gt;Other significant model releases in
August&lt;/li&gt;
&lt;li&gt;Image editing: Qwen-Image-Edit and
Gemini Nano Banana&lt;/li&gt;
&lt;li&gt;More prompt injection and more lethal trifecta&lt;/li&gt;
&lt;li&gt;Tools I'm using at the moment&lt;/li&gt;
&lt;li&gt;Bonus links&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&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/nano-banana"&gt;nano-banana&lt;/a&gt;&lt;/p&gt;



</summary><category term="newsletter"/><category term="nano-banana"/></entry><entry><title>July newsletter for sponsors is out</title><link href="https://simonwillison.net/2025/Aug/1/july-newsletter/#atom-tag" rel="alternate"/><published>2025-08-01T15:43:34+00:00</published><updated>2025-08-01T15:43:34+00:00</updated><id>https://simonwillison.net/2025/Aug/1/july-newsletter/#atom-tag</id><summary type="html">
    &lt;p&gt;This morning I sent out the third edition of my LLM digest newsletter for my $10/month and higher &lt;a href="https://github.com/sponsors/simonw"&gt;sponsors on GitHub&lt;/a&gt;. It included the following section headers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code&lt;/li&gt;
&lt;li&gt;Model releases in July&lt;/li&gt;
&lt;li&gt;Gold medal performances in the IMO&lt;/li&gt;
&lt;li&gt;Reverse engineering system prompts&lt;/li&gt;
&lt;li&gt;Tools I'm using at the moment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The newsletter is a condensed summary of highlights from the past month of my blog. I published &lt;a href="https://simonwillison.net/2025/Jul/"&gt;98 posts in July&lt;/a&gt; - the concept for the newsletter is that you can pay me for the version that only takes 10 minutes to read!&lt;/p&gt;
&lt;p&gt;Here are the newsletters I sent out for &lt;a href="https://gist.github.com/simonw/de3e12de506c9a3df4e1119b842e79f7"&gt;June 2025&lt;/a&gt; and &lt;a href="https://gist.github.com/simonw/07eb3f32bad5b9f21b4e65f86327e302"&gt;May 2025&lt;/a&gt;, if you want a taste of what you'll be getting as a sponsor. New sponsors instantly get access to the archive of previous newsletters, including the one I sent this morning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: I also &lt;a href="https://simonw.substack.com/p/reverse-engineering-some-updates"&gt;sent out&lt;/a&gt; my much longer, more frequent and free weekly-ish newsletter - this edition covers just the last three days because there's been so much going on. That one is entirely copy-and-pasted from my blog so if you read me &lt;a href="https://simonwillison.net/about/#atom"&gt;via feeds&lt;/a&gt; you'll have seen it all already.&lt;/p&gt;

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



</summary><category term="newsletter"/></entry><entry><title>June newsletter for sponsors has been sent</title><link href="https://simonwillison.net/2025/Jul/1/monthly/#atom-tag" rel="alternate"/><published>2025-07-01T03:52:53+00:00</published><updated>2025-07-01T03:52:53+00:00</updated><id>https://simonwillison.net/2025/Jul/1/monthly/#atom-tag</id><summary type="html">
    &lt;p&gt;I just sent out the second edition of my &lt;a href="https://simonwillison.net/2025/May/25/sponsors-only-newsletter/"&gt;sponsors only monthly newsletter&lt;/a&gt;. Anyone who sponsors me for &lt;a href="https://github.com/sponsors/simonw/"&gt;$10/month or more on GitHub&lt;/a&gt; gets this carefully hand-curated summary of the last month in AI/LLMs/my projects designed to be readable in ten minutes or less.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://simonw.substack.com/"&gt;regular newsletter&lt;/a&gt; remains free - the monthly one is the only paywalled content I produce, the idea being that you can &lt;strong&gt;pay me to send you less&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/07eb3f32bad5b9f21b4e65f86327e302"&gt;the first edition for May 2025&lt;/a&gt; as a preview of what you can expect. You'll get access to the June digest and the full archive automatically if you decide to start sponsoring.&lt;/p&gt;

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



</summary><category term="newsletter"/></entry><entry><title>First monthly sponsor newsletter tomorrow</title><link href="https://simonwillison.net/2025/May/29/newsletter-tomorrow/#atom-tag" rel="alternate"/><published>2025-05-29T04:33:32+00:00</published><updated>2025-05-29T04:33:32+00:00</updated><id>https://simonwillison.net/2025/May/29/newsletter-tomorrow/#atom-tag</id><summary type="html">
    &lt;p&gt;I'll be sending out my first &lt;a href="https://simonwillison.net/2025/May/25/sponsors-only-newsletter/"&gt;curated monthly highlights newsletter&lt;/a&gt; tomorrow, only to $10/month and up sponsors. &lt;a href="https://github.com/sponsors/simonw/"&gt;Sign up now&lt;/a&gt; if you want to pay me to send you less!&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://simonw.substack.com/"&gt;weekly-ish&lt;/a&gt; newsletter remains free, in fact I just sent out &lt;a href="https://simonw.substack.com/p/large-language-models-can-run-tools"&gt;the latest edition&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/newsletter"&gt;newsletter&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="newsletter"/></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>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></feed>