<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: django</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/django.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-03-17T16:13:37+00:00</updated><author><name>Simon Willison</name></author><entry><title>Quoting Tim Schilling</title><link href="https://simonwillison.net/2026/Mar/17/tim-schilling/#atom-tag" rel="alternate"/><published>2026-03-17T16:13:37+00:00</published><updated>2026-03-17T16:13:37+00:00</updated><id>https://simonwillison.net/2026/Mar/17/tim-schilling/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://www.better-simple.com/django/2026/03/16/give-django-your-time-and-money/"&gt;&lt;p&gt;If you do not understand the ticket, if you do not understand the solution, or if you do not understand the feedback on your PR, then your use of LLM is hurting Django as a whole. [...]&lt;/p&gt;
&lt;p&gt;For a reviewer, it’s demoralizing to communicate with a facade of a human.&lt;/p&gt;
&lt;p&gt;This is because contributing to open source, especially Django, is a communal endeavor. Removing your humanity from that experience makes that endeavor more difficult. If you use an LLM to contribute to Django, it needs to be as a complementary tool, not as your vehicle.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://www.better-simple.com/django/2026/03/16/give-django-your-time-and-money/"&gt;Tim Schilling&lt;/a&gt;, Give Django your time and money, not your tokens&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-ethics"&gt;ai-ethics&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="open-source"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-ethics"/></entry><entry><title>Shopify/liquid: Performance: 53% faster parse+render, 61% fewer allocations</title><link href="https://simonwillison.net/2026/Mar/13/liquid/#atom-tag" rel="alternate"/><published>2026-03-13T03:44:34+00:00</published><updated>2026-03-13T03:44:34+00:00</updated><id>https://simonwillison.net/2026/Mar/13/liquid/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Shopify/liquid/pull/2056"&gt;Shopify/liquid: Performance: 53% faster parse+render, 61% fewer allocations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
PR from Shopify CEO Tobias Lütke against Liquid, Shopify's open source Ruby template engine that was somewhat inspired by Django when Tobi first created it &lt;a href="https://simonwillison.net/2005/Nov/6/liquid/"&gt;back in 2005&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Tobi found dozens of new performance micro-optimizations using a variant of &lt;a href="https://github.com/karpathy/autoresearch"&gt;autoresearch&lt;/a&gt;, Andrej Karpathy's new system for having a coding agent run hundreds of semi-autonomous experiments to find new effective techniques for training &lt;a href="https://github.com/karpathy/nanochat"&gt;nanochat&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Tobi's implementation started two days ago with this &lt;a href="https://github.com/Shopify/liquid/blob/2543fdc1a101f555db208fb0deeb2e3bf1ae9e36/auto/autoresearch.md"&gt;autoresearch.md&lt;/a&gt; prompt file and an &lt;a href="https://github.com/Shopify/liquid/blob/2543fdc1a101f555db208fb0deeb2e3bf1ae9e36/auto/autoresearch.sh"&gt;autoresearch.sh&lt;/a&gt; script for the agent to run to execute the test suite and report on benchmark scores.&lt;/p&gt;
&lt;p&gt;The PR now lists &lt;a href="https://github.com/Shopify/liquid/pull/2056/commits"&gt;93 commits&lt;/a&gt; from around 120 automated experiments. The PR description lists what worked in detail - some examples:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Replaced StringScanner tokenizer with &lt;code&gt;String#byteindex&lt;/code&gt;.&lt;/strong&gt; Single-byte &lt;code&gt;byteindex&lt;/code&gt; searching is ~40% faster than regex-based &lt;code&gt;skip_until&lt;/code&gt;. This alone reduced parse time by ~12%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure-byte &lt;code&gt;parse_tag_token&lt;/code&gt;.&lt;/strong&gt; Eliminated the costly &lt;code&gt;StringScanner#string=&lt;/code&gt; reset that was called for every &lt;code&gt;{% %}&lt;/code&gt; token (878 times). Manual byte scanning for tag name + markup extraction is faster than resetting and re-scanning via StringScanner. [...]&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cached small integer &lt;code&gt;to_s&lt;/code&gt;.&lt;/strong&gt; Pre-computed frozen strings for 0-999 avoid 267 &lt;code&gt;Integer#to_s&lt;/code&gt; allocations per render.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This all added up to a 53% improvement on benchmarks - truly impressive for a codebase that's been tweaked by hundreds of contributors over 20 years.&lt;/p&gt;
&lt;p&gt;I think this illustrates a number of interesting ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Having a robust test suite - in this case 974 unit tests - is a &lt;em&gt;massive unlock&lt;/em&gt; for working with coding agents. This kind of research effort would not be possible without first having a tried and tested suite of tests.&lt;/li&gt;
&lt;li&gt;The autoresearch pattern - where an agent brainstorms a multitude of potential improvements and then experiments with them one at a time - is really effective.&lt;/li&gt;
&lt;li&gt;If you provide an agent with a benchmarking script "make it faster" becomes an actionable goal.&lt;/li&gt;
&lt;li&gt;CEOs can code again! Tobi has always been more hands-on than most, but this is a much more significant contribution than anyone would expect from the leader of a company with 7,500+ employees. I've seen this pattern play out a lot over the past few months: coding agents make it feasible for people in high-interruption roles to productively work with code again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's Tobi's &lt;a href="https://github.com/tobi"&gt;GitHub contribution graph&lt;/a&gt; for the past year, showing a significant uptick following that &lt;a href="https://simonwillison.net/tags/november-2025-inflection/"&gt;November 2025 inflection point&lt;/a&gt; when coding agents got really good.&lt;/p&gt;
&lt;p&gt;&lt;img alt="1,658 contributions in the last year - scattered lightly through Jun, Aug, Sep, Oct and Nov and then picking up significantly in Dec, Jan, and Feb." src="https://static.simonwillison.net/static/2026/tobi-contribs.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;He used &lt;a href="https://github.com/badlogic/pi-mono"&gt;Pi&lt;/a&gt; as the coding agent and released a new &lt;a href="https://github.com/davebcn87/pi-autoresearch"&gt;pi-autoresearch&lt;/a&gt; plugin in collaboration with David Cortés, which maintains state in an &lt;code&gt;autoresearch.jsonl&lt;/code&gt; file &lt;a href="https://github.com/Shopify/liquid/blob/3182b7c1b3758b0f5fe2d0fcc71a48bbcb11c946/autoresearch.jsonl"&gt;like this one&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://x.com/tobi/status/2032212531846971413"&gt;@tobi&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/andrej-karpathy"&gt;andrej-karpathy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/agentic-engineering"&gt;agentic-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/november-2025-inflection"&gt;november-2025-inflection&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tobias-lutke"&gt;tobias-lutke&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/autoresearch"&gt;autoresearch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pi"&gt;pi&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="performance"/><category term="rails"/><category term="ruby"/><category term="ai"/><category term="andrej-karpathy"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="coding-agents"/><category term="agentic-engineering"/><category term="november-2025-inflection"/><category term="tobias-lutke"/><category term="autoresearch"/><category term="pi"/></entry><entry><title>SWE-bench February 2026 leaderboard update</title><link href="https://simonwillison.net/2026/Feb/19/swe-bench/#atom-tag" rel="alternate"/><published>2026-02-19T04:48:47+00:00</published><updated>2026-02-19T04:48:47+00:00</updated><id>https://simonwillison.net/2026/Feb/19/swe-bench/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.swebench.com/"&gt;SWE-bench February 2026 leaderboard update&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
SWE-bench is one of the benchmarks that the labs love to list in their model releases. The official leaderboard is infrequently updated but they just did a full run of it against the current generation of models, which is notable because it's always good to see benchmark results like this that &lt;em&gt;weren't&lt;/em&gt; self-reported by the labs.&lt;/p&gt;
&lt;p&gt;The fresh results are for their "Bash Only" benchmark, which runs their &lt;a href="https://github.com/SWE-agent/mini-swe-agent"&gt;mini-swe-bench&lt;/a&gt; agent (~9,000 lines of Python, &lt;a href="https://github.com/SWE-agent/mini-swe-agent/blob/v2.2.1/src/minisweagent/config/benchmarks/swebench.yaml"&gt;here are the prompts&lt;/a&gt; they use) against the &lt;a href="https://huggingface.co/datasets/princeton-nlp/SWE-bench"&gt;SWE-bench&lt;/a&gt; dataset of coding problems - 2,294 real-world examples pulled from 12 open source repos: &lt;a href="https://github.com/django/django"&gt;django/django&lt;/a&gt; (850), &lt;a href="https://github.com/sympy/sympy"&gt;sympy/sympy&lt;/a&gt; (386), &lt;a href="https://github.com/scikit-learn/scikit-learn"&gt;scikit-learn/scikit-learn&lt;/a&gt; (229), &lt;a href="https://github.com/sphinx-doc/sphinx"&gt;sphinx-doc/sphinx&lt;/a&gt; (187), &lt;a href="https://github.com/matplotlib/matplotlib"&gt;matplotlib/matplotlib&lt;/a&gt; (184), &lt;a href="https://github.com/pytest-dev/pytest"&gt;pytest-dev/pytest&lt;/a&gt; (119), &lt;a href="https://github.com/pydata/xarray"&gt;pydata/xarray&lt;/a&gt; (110), &lt;a href="https://github.com/astropy/astropy"&gt;astropy/astropy&lt;/a&gt; (95), &lt;a href="https://github.com/pylint-dev/pylint"&gt;pylint-dev/pylint&lt;/a&gt; (57), &lt;a href="https://github.com/psf/requests"&gt;psf/requests&lt;/a&gt; (44), &lt;a href="https://github.com/mwaskom/seaborn"&gt;mwaskom/seaborn&lt;/a&gt; (22), &lt;a href="https://github.com/pallets/flask"&gt;pallets/flask&lt;/a&gt; (11).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Correction&lt;/strong&gt;: &lt;em&gt;The Bash only benchmark runs against SWE-bench Verified, not original SWE-bench. Verified is a manually curated subset of 500 samples &lt;a href="https://openai.com/index/introducing-swe-bench-verified/"&gt;described here&lt;/a&gt;, funded by OpenAI. Here's &lt;a href="https://huggingface.co/datasets/princeton-nlp/SWE-bench_Verified"&gt;SWE-bench Verified&lt;/a&gt; on Hugging Face - since it's just 2.1MB of Parquet it's easy to browse &lt;a href="https://lite.datasette.io/?parquet=https%3A%2F%2Fhuggingface.co%2Fdatasets%2Fprinceton-nlp%2FSWE-bench_Verified%2Fresolve%2Fmain%2Fdata%2Ftest-00000-of-00001.parquet#/data/test-00000-of-00001?_facet=repo"&gt;using Datasette Lite&lt;/a&gt;, which cuts those numbers down to django/django (231), sympy/sympy (75), sphinx-doc/sphinx (44), matplotlib/matplotlib (34), scikit-learn/scikit-learn (32), astropy/astropy (22), pydata/xarray (22), pytest-dev/pytest (19), pylint-dev/pylint (10), psf/requests (8), mwaskom/seaborn (2), pallets/flask (1)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Here's how the top ten models performed:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Bar chart showing &amp;quot;% Resolved&amp;quot; by &amp;quot;Model&amp;quot;. Bars in descending order: Claude 4.5 Opus (high reasoning) 76.8%, Gemini 3 Flash (high reasoning) 75.8%, MiniMax M2.5 (high reasoning) 75.8%, Claude Opus 4.6 75.6%, GLM-5 (high reasoning) 72.8%, GPT-5.2 (high reasoning) 72.8%, Claude 4.5 Sonnet (high reasoning) 72.8%, Kimi K2.5 (high reasoning) 71.4%, DeepSeek V3.2 (high reasoning) 70.8%, Claude 4.5 Haiku (high reasoning) 70.0%, and a partially visible final bar at 66.6%." src="https://static.simonwillison.net/static/2026/swbench-feb-2026.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;It's interesting to see Claude Opus 4.5 beat Opus 4.6, though only by about a percentage point. 4.5 Opus is top, then Gemini 3 Flash, then MiniMax M2.5 - a 229B model released &lt;a href="https://www.minimax.io/news/minimax-m25"&gt;last week&lt;/a&gt; by Chinese lab MiniMax. GLM-5, Kimi K2.5 and DeepSeek V3.2 are three more Chinese models that make the top ten as well.&lt;/p&gt;
&lt;p&gt;OpenAI's GPT-5.2 is their highest performing model at position 6, but it's worth noting that their best coding model, GPT-5.3-Codex, is not represented - maybe because it's not yet available in the OpenAI API.&lt;/p&gt;
&lt;p&gt;This benchmark uses the same system prompt for every model, which is important for a fair comparison but does mean that the quality of the different harnesses or optimized prompts is not being measured here.&lt;/p&gt;
&lt;p&gt;The chart above is a screenshot from the SWE-bench website, but their charts don't include the actual percentage values visible on the bars. I successfully used Claude for Chrome to add these - &lt;a href="https://claude.ai/share/81a0c519-c727-4caa-b0d4-0d866375d0da"&gt;transcript here&lt;/a&gt;. My prompt sequence included:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use claude in chrome to open https://www.swebench.com/&lt;/p&gt;
&lt;p&gt;Click on "Compare results" and then select "Select top 10"&lt;/p&gt;
&lt;p&gt;See those bar charts? I want them to display the percentage on each bar so I can take a better screenshot, modify the page like that&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm impressed at how well this worked - Claude injected custom JavaScript into the page to draw additional labels on top of the existing chart.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a Claude AI conversation showing browser automation. A thinking step reads &amp;quot;Pivoted strategy to avoid recursion issues with chart labeling &amp;gt;&amp;quot; followed by the message &amp;quot;Good, the chart is back. Now let me carefully add the labels using an inline plugin on the chart instance to avoid the recursion issue.&amp;quot; A collapsed &amp;quot;Browser_evaluate&amp;quot; section shows a browser_evaluate tool call with JavaScript code using Chart.js canvas context to draw percentage labels on bars: meta.data.forEach((bar, index) =&amp;gt; { const value = dataset.data[index]; if (value !== undefined &amp;amp;&amp;amp; value !== null) { ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillStyle = '#333'; ctx.font = 'bold 12px sans-serif'; ctx.fillText(value.toFixed(1) + '%', bar.x, bar.y - 5); A pending step reads &amp;quot;Let me take a screenshot to see if it worked.&amp;quot; followed by a completed &amp;quot;Done&amp;quot; step, and the message &amp;quot;Let me take a screenshot to check the result.&amp;quot;" src="https://static.simonwillison.net/static/2026/claude-chrome-draw-on-chart.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: If you look at the transcript Claude claims to have switched to Playwright, which is confusing because I didn't think I had that configured.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/KLieret/status/2024176335782826336"&gt;@KLieret&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/benchmarks"&gt;benchmarks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-in-china"&gt;ai-in-china&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browser-agents"&gt;browser-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/minimax"&gt;minimax&lt;/a&gt;&lt;/p&gt;



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

</summary><category term="caching"/><category term="django"/><category term="javascript"/><category term="localstorage"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/></entry><entry><title>Django 6.0 released</title><link href="https://simonwillison.net/2025/Dec/4/django-6/#atom-tag" rel="alternate"/><published>2025-12-04T23:57:34+00:00</published><updated>2025-12-04T23:57:34+00:00</updated><id>https://simonwillison.net/2025/Dec/4/django-6/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.djangoproject.com/weblog/2025/dec/03/django-60-released/"&gt;Django 6.0 released&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Django 6.0 includes a &lt;a href="https://docs.djangoproject.com/en/6.0/releases/6.0/"&gt;flurry of neat features&lt;/a&gt;, but the two that most caught my eye are &lt;strong&gt;background workers&lt;/strong&gt; and &lt;strong&gt;template partials&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Background workers started out as &lt;a href="https://github.com/django/deps/blob/main/accepted/0014-background-workers.rst"&gt;DEP (Django Enhancement Proposal) 14&lt;/a&gt;, proposed and shepherded by Jake Howard. Jake prototyped the feature in &lt;a href="https://github.com/RealOrangeOne/django-tasks"&gt;django-tasks&lt;/a&gt; and wrote &lt;a href="https://theorangeone.net/posts/django-dot-tasks-exists/"&gt;this extensive background on the feature&lt;/a&gt; when it landed in core just in time for the 6.0 feature freeze back in September.&lt;/p&gt;
&lt;p&gt;Kevin Wetzels published a useful &lt;a href="https://roam.be/notes/2025/a-first-look-at-djangos-new-background-tasks/"&gt;first look at Django's background tasks&lt;/a&gt; based on the earlier RC, including notes on building a custom database-backed worker implementation.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.djangoproject.com/en/6.0/ref/templates/language/#template-partials"&gt;Template Partials&lt;/a&gt; were implemented as a Google Summer of Code project by Farhan Ali Raza. I really like the design of this. Here's an example from &lt;a href="https://docs.djangoproject.com/en/6.0/ref/templates/language/#inline-partials"&gt;the documentation&lt;/a&gt; showing the neat &lt;code&gt;inline&lt;/code&gt; attribute which lets you both use and define a partial at the same time:&lt;/p&gt;
&lt;div class="highlight highlight-text-html-django"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;{# Define and render immediately. #}&lt;/span&gt;
&lt;span class="pl-e"&gt;{%&lt;/span&gt; &lt;span class="pl-s"&gt;partialdef&lt;/span&gt; &lt;span class="pl-s"&gt;user&lt;/span&gt;-&lt;span class="pl-s"&gt;info&lt;/span&gt; &lt;span class="pl-s"&gt;inline&lt;/span&gt; &lt;span class="pl-e"&gt;%}&lt;/span&gt;
    &amp;lt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-e"&gt;id&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;user-info-{{ user.username }}&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
        &amp;lt;&lt;span class="pl-ent"&gt;h3&lt;/span&gt;&amp;gt;{{ user.name }}&amp;lt;/&lt;span class="pl-ent"&gt;h3&lt;/span&gt;&amp;gt;
        &amp;lt;&lt;span class="pl-ent"&gt;p&lt;/span&gt;&amp;gt;{{ user.bio }}&amp;lt;/&lt;span class="pl-ent"&gt;p&lt;/span&gt;&amp;gt;
    &amp;lt;/&lt;span class="pl-ent"&gt;div&lt;/span&gt;&amp;gt;
&lt;span class="pl-e"&gt;{%&lt;/span&gt; &lt;span class="pl-s"&gt;endpartialdef&lt;/span&gt; &lt;span class="pl-e"&gt;%}&lt;/span&gt;

&lt;span class="pl-c"&gt;{# Other page content here. #}&lt;/span&gt;

&lt;span class="pl-c"&gt;{# Reuse later elsewhere in the template. #}&lt;/span&gt;
&amp;lt;&lt;span class="pl-ent"&gt;section&lt;/span&gt; &lt;span class="pl-e"&gt;class&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;featured-authors&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
    &amp;lt;&lt;span class="pl-ent"&gt;h2&lt;/span&gt;&amp;gt;Featured Authors&amp;lt;/&lt;span class="pl-ent"&gt;h2&lt;/span&gt;&amp;gt;
    &lt;span class="pl-e"&gt;{%&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s"&gt;user&lt;/span&gt; &lt;span class="pl-k"&gt;in&lt;/span&gt; &lt;span class="pl-s"&gt;featured&lt;/span&gt; &lt;span class="pl-e"&gt;%}&lt;/span&gt;
        &lt;span class="pl-e"&gt;{%&lt;/span&gt; &lt;span class="pl-s"&gt;partial&lt;/span&gt; &lt;span class="pl-s"&gt;user&lt;/span&gt;-&lt;span class="pl-s"&gt;info&lt;/span&gt; &lt;span class="pl-e"&gt;%}&lt;/span&gt;
    &lt;span class="pl-e"&gt;{%&lt;/span&gt; &lt;span class="pl-k"&gt;endfor&lt;/span&gt; &lt;span class="pl-e"&gt;%}&lt;/span&gt;
&amp;lt;/&lt;span class="pl-ent"&gt;section&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can also render just a named partial from a template directly in Python code like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-en"&gt;render&lt;/span&gt;(&lt;span class="pl-s1"&gt;request&lt;/span&gt;, &lt;span class="pl-s"&gt;"authors.html#user-info"&lt;/span&gt;, {&lt;span class="pl-s"&gt;"user"&lt;/span&gt;: &lt;span class="pl-s1"&gt;user&lt;/span&gt;})&lt;/pre&gt;

&lt;p&gt;I'm looking forward to trying this out in combination with &lt;a href="https://htmx.org"&gt;HTMX&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I asked &lt;a href="https://gistpreview.github.io/?8db0c1a50aad95d5bc5b5b7d66a503ab"&gt;Claude Code to dig around in my blog's source code&lt;/a&gt; looking for places that could benefit from a template partial. Here's &lt;a href="https://github.com/simonw/simonwillisonblog/commit/9b1a6b99140b43e869ada3348ce4d4407e9a06ba"&gt;the resulting commit&lt;/a&gt; that uses them to de-duplicate the display of dates and tags from pages that list multiple types of content, such as &lt;a href="https://simonwillison.net/tags/django/"&gt;my tag pages&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/htmx"&gt;htmx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="python"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="htmx"/><category term="coding-agents"/><category term="claude-code"/></entry><entry><title>YouTube embeds fail with a 153 error</title><link href="https://simonwillison.net/2025/Dec/1/youtube-embed-153-error/#atom-tag" rel="alternate"/><published>2025-12-01T05:26:23+00:00</published><updated>2025-12-01T05:26:23+00:00</updated><id>https://simonwillison.net/2025/Dec/1/youtube-embed-153-error/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/simonwillisonblog/issues/561"&gt;YouTube embeds fail with a 153 error&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I just fixed this bug on my blog. I was getting an annoying "Error 153: Video player configuration error" on some of the YouTube video embeds (like &lt;a href="https://simonwillison.net/2024/Jun/21/search-based-rag/"&gt;this one&lt;/a&gt;) on this site. After some digging it turns out the culprit was this HTTP header, which Django's SecurityMiddleware was &lt;a href="https://docs.djangoproject.com/en/5.2/ref/middleware/#module-django.middleware.security"&gt;sending by default&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Referrer-Policy: same-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YouTube's &lt;a href="https://developers.google.com/youtube/terms/required-minimum-functionality#embedded-player-api-client-identity"&gt;embedded player terms documentation&lt;/a&gt; explains why this broke:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;API Clients that use the YouTube embedded player (including the YouTube IFrame Player API) must provide identification through the &lt;code&gt;HTTP Referer&lt;/code&gt; request header. In some environments, the browser will automatically set &lt;code&gt;HTTP Referer&lt;/code&gt;, and API Clients need only ensure they are not setting the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy"&gt;&lt;code&gt;Referrer-Policy&lt;/code&gt;&lt;/a&gt; in a way that suppresses the &lt;code&gt;Referer&lt;/code&gt; value. YouTube recommends using &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; Referrer-Policy, which is already the default in many browsers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The fix, which I &lt;a href="https://github.com/simonw/simonwillisonblog/pull/562"&gt;outsourced to GitHub Copilot agent&lt;/a&gt; since I was on my phone, was to add this to my &lt;code&gt;settings.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;a href="https://developer.chrome.com/blog/referrer-policy-new-chrome-default"&gt;explainer on the Chrome blog&lt;/a&gt; describes what the header means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; offers more privacy. With this policy, only the origin is sent in the Referer header of cross-origin requests.&lt;/p&gt;
&lt;p&gt;This prevents leaks of private data that may be accessible from other parts of the full URL such as the path and query string.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Effectively it means that any time you follow a link from my site to somewhere else they'll see this in the incoming HTTP headers even if you followed the link from a page other than my homepage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Referer: https://simonwillison.net/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The previous header, &lt;code&gt;same-origin&lt;/code&gt;, is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy"&gt;explained by MDN here&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Send the &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Origin"&gt;origin&lt;/a&gt;, path, and query string for &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Same-origin_policy"&gt;same-origin&lt;/a&gt; requests. Don't send the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referer"&gt;&lt;code&gt;Referer&lt;/code&gt;&lt;/a&gt; header for cross-origin requests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This meant that previously traffic from my site wasn't sending any HTTP referer at all!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/privacy"&gt;privacy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/youtube"&gt;youtube&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="http"/><category term="privacy"/><category term="youtube"/></entry><entry><title>Highlights from my appearance on the Data Renegades podcast with CL Kao and Dori Wilson</title><link href="https://simonwillison.net/2025/Nov/26/data-renegades-podcast/#atom-tag" rel="alternate"/><published>2025-11-26T00:29:11+00:00</published><updated>2025-11-26T00:29:11+00:00</updated><id>https://simonwillison.net/2025/Nov/26/data-renegades-podcast/#atom-tag</id><summary type="html">
    &lt;p&gt;I talked with CL Kao and Dori Wilson for an episode of their new &lt;a href="https://www.heavybit.com/library/podcasts/data-renegades"&gt;Data Renegades podcast&lt;/a&gt; titled &lt;a href="https://www.heavybit.com/library/podcasts/data-renegades/ep-2-data-journalism-unleashed-with-simon-willison"&gt;Data Journalism Unleashed with Simon Willison&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I fed the transcript into Claude Opus 4.5 to extract this list of topics with timestamps and illustrative quotes. It did such a good job I'm using what it produced almost verbatim here - I tidied it up a tiny bit and added a bunch of supporting links.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;What is data journalism and why it's the most interesting application of data analytics [02:03]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's this whole field of data journalism, which is using data and databases to try and figure out stories about the world. It's effectively data analytics, but applied to the world of news gathering. And I think it's fascinating. I think it is the single most interesting way to apply this stuff because everything is in scope for a journalist."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The origin story of Django at a small Kansas newspaper [02:31]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"We had a year's paid internship from university where we went to work &lt;a href="https://simonwillison.net/2025/Jul/13/django-birthday/"&gt;for this local newspaper&lt;/a&gt; in Kansas with this chap &lt;a href="https://holovaty.com/"&gt;Adrian Holovaty&lt;/a&gt;. And at the time we thought we were building a content management system."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Building the "Downloads Page" - a dynamic radio player of local bands [03:24]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Adrian built a feature of the site called &lt;a href="https://web.archive.org/web/20070320083540/https://www.lawrence.com/downloads/"&gt;the Downloads Page&lt;/a&gt;. And what it did is it said, okay, who are the bands playing at venues this week? And then we'll construct a little radio player of MP3s of music of bands who are playing in Lawrence in this week."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Working at The Guardian on data-driven reporting projects [04:44]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I just love that challenge of building tools that journalists can use to investigate stories and then that you can use to help tell those stories. Like if you give your audience a searchable database to back up the story that you're presenting, I just feel that's a great way of building more credibility in the reporting process."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Washington Post's opioid crisis data project and sharing with local newspapers [05:22]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Something the Washington Post did that I thought was extremely forward thinking is that they shared [&lt;a href="https://www.washingtonpost.com/national/2019/08/12/post-released-deas-data-pain-pills-heres-what-local-journalists-are-using-it/?utm_source=chatgpt.com"&gt;the opioid files&lt;/a&gt;] with other newspapers. They said, 'Okay, we're a big national newspaper, but these stories are at a local level. So what can we do so that the local newspaper and different towns can dive into that data for us?'"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NICAR conference and the collaborative, non-competitive nature of data journalism [07:00]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It's all about trying to figure out what is the most value we can get out of this technology as an industry as a whole."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://www.ire.org/training/conferences/nicar-2026/"&gt;NICAR 2026&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ProPublica and the Baltimore Banner as examples of nonprofit newsrooms [09:02]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The &lt;a href="https://www.thebanner.com/"&gt;Baltimore Banner&lt;/a&gt; are a nonprofit newsroom. They have a hundred employees now for the city of Baltimore. This is an enormously, it's a very healthy newsroom. They do amazing data reporting... And I believe they're almost breaking even on subscription revenue [correction, &lt;a href="https://localnewsinitiative.northwestern.edu/posts/2025/11/10/baltimore-local-media-resurgence/"&gt;not yet&lt;/a&gt;], which is astonishing."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The "shower revelation" that led to Datasette - SQLite on serverless hosting [10:31]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It was literally a shower revelation. I was in the shower thinking about serverless and I thought, 'hang on a second. So you can't use Postgres on serverless hosting, but if it's a read-only database, could you use SQLite? Could you just take that data, bake it into a blob of a SQLite file, ship that as part of the application just as another asset, and then serve things on top of that?'"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Datasette's plugin ecosystem and the vision of solving data publishing [12:36]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"In the past I've thought about it like how Pinterest solved scrapbooking and WordPress solved blogging, who's going to solve data like publishing tables full of data on the internet? So that was my original goal."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unexpected Datasette use cases: Copenhagen electricity grid, Brooklyn Cemetery [13:59]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Somebody was doing research on the Brooklyn Cemetery and they got hold of the original paper files of who was buried in the Brooklyn Cemetery. They digitized those, loaded the results into Datasette and now it tells the story of immigration to New York."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bellingcat using Datasette to investigate leaked Russian food delivery data [14:40]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It turns out the Russian FSB, their secret police, have an office that's not near any restaurants and they order food all the time. And so this database could tell you what nights were the FSB working late and what were the names and phone numbers of the FSB agents who ordered food... And I'm like, 'Wow, that's going to get me thrown out of a window.'"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://www.bellingcat.com/news/rest-of-world/2022/04/01/food-delivery-leak-unmasks-russian-security-agents/"&gt;Bellingcat: Food Delivery Leak Unmasks Russian Security Agents&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The frustration of open source: no feedback on how people use your software [16:14]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"An endless frustration in open source is that you really don't get the feedback on what people are actually doing with it."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open office hours on Fridays to learn how people use Datasette [16:49]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I have an &lt;a href="https://calendly.com/swillison/datasette-office-hours"&gt;open office hours Calendly&lt;/a&gt;, where the invitation is, if you use my software or want to use my software, grab 25 minutes to talk to me about it. And that's been a revelation. I've had hundreds of conversations in the past few years with people."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Data cleaning as the universal complaint - 95% of time spent cleaning [17:34]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I know every single person I talk to in data complains about the cleaning that everyone says, 'I spend 95% of my time cleaning the data and I hate it.'"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Version control problems in data teams - Python scripts on laptops without Git [17:43]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I used to work for a large company that had a whole separate data division and I learned at one point that they weren't using Git for their scripts. They had Python scripts, littering laptops left, right and center and lots of notebooks and very little version control, which upset me greatly."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Carpentries organization teaching scientists Git and software fundamentals [18:12]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's an organization called &lt;a href="https://carpentries.org/"&gt;The Carpentries&lt;/a&gt;. Basically they teach scientists to use Git. Their entire thing is scientists are all writing code these days. Nobody ever sat them down and showed them how to use the UNIX terminal or Git or version control or write tests. We should do that."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Data documentation as an API contract problem [21:11]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"A coworker of mine said, you do realize that this should be a documented API interface, right? Your data warehouse view of your project is something that you should be responsible for communicating to the rest of the organization and we weren't doing it."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The importance of "view source" on business reports [23:21]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If you show somebody a report, you need to have view source on those reports... somebody would say 25% of our users did this thing. And I'm thinking I need to see the query because I knew where all of the skeletons were buried and often that 25% was actually a 50%."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fact-checking process for data reporting [24:16]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Their stories are fact checked, no story goes out the door without someone else fact checking it and without an editor approving it. And it's the same for data. If they do a piece of data reporting, a separate data reporter has to audit those numbers and maybe even produce those numbers themselves in a separate way before they're confident enough to publish them."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Queries as first-class citizens with version history and comments [27:16]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I think the queries themselves need to be first class citizens where like I want to see a library of queries that my team are using and each one I want to know who built it and when it was built. And I want to see how that's changed over time and be able to post comments on it."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Two types of documentation: official docs vs. temporal/timestamped notes [29:46]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's another type of documentation which I call temporal documentation where effectively it's stuff where you say, 'Okay, it's Friday, the 31st of October and this worked.' But the timestamp is very prominent and if somebody looks that in six months time, there's no promise that it's still going to be valid to them."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Starting an internal blog without permission - instant credibility [30:24]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The key thing is you need to start one of these without having to ask permission first. You just one day start, you can do it in a Google Doc, right?... It gives you so much credibility really quickly because nobody else is doing it."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Building a search engine across seven documentation systems [31:35]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It turns out, once you get a search engine over the top, it's good documentation. You just have to know where to look for it. And if you are the person who builds the search engine, you secretly control the company."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The TIL (Today I Learned) blog approach - celebrating learning basics [33:05]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I've done &lt;a href="https://til.simonwillison.net/"&gt;TILs&lt;/a&gt; about 'for loops' in Bash, right? Because okay, everyone else knows how to do that. I didn't... It's a value statement where I'm saying that if you've been a professional software engineer for 25 years, you still don't know everything. You should still celebrate figuring out how to learn 'for loops' in Bash."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Coding agents like Claude Code and their unexpected general-purpose power [34:53]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"They pretend to be programming tools but actually they're basically a sort of general agent because they can do anything that you can do by typing commands into a Unix shell, which is everything."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Skills for Claude - markdown files for census data, visualization, newsroom standards [36:16]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Imagine a markdown file for census data. Here's where to get census data from. Here's what all of the columns mean. Here's how to derive useful things from that. And then you have another skill for here's how to visualize things on a map using D3... At the Washington Post, our data standards are this and this and this."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://simonwillison.net/2025/Oct/16/claude-skills/"&gt;Claude Skills are awesome, maybe a bigger deal than MCP&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The absurd 2025 reality: cutting-edge AI tools use 1980s terminal interfaces [38:22]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The terminal is now accessible to people who never learned the terminal before 'cause you don't have to remember all the commands because the LLM knows the commands for you. But isn't that fascinating that the cutting edge software right now is it's like 1980s style— I love that. It's not going to last. That's a current absurdity for 2025."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cursor for data? Generic agent loops vs. data-specific IDEs [38:18]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"More of a notebook interface makes a lot more sense than a Claude Code style terminal 'cause a Jupyter Notebook is effectively a terminal, it's just in your browser and it can show you charts."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Future of BI tools: prompt-driven, instant dashboard creation [39:54]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You can copy and paste a big chunk of JSON data from somewhere into [an LLM] and say build me a dashboard. And they do such a good job. Like they will just decide, oh this is a time element so we'll do a bar chart over time and these numbers feel big so we'll put those in a big green box."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Three exciting LLM applications: text-to-SQL, data extraction, data enrichment [43:06]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"LLMs are stunningly good at outputting SQL queries. Especially if you give them extra metadata about the columns. Maybe a couple of example queries and stuff."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LLMs extracting structured data from scanned PDFs at 95-98% accuracy [43:36]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You file a freedom of information request and you get back horrifying scanned PDFs with slightly wonky angles and you have to get the data out of those. LLMs for a couple of years now have been so good at, 'here's a page of a police report, give me back JSON with the name of the arresting officer and the date of the incident and the description,' and they just do it."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Data enrichment: running cheap models in loops against thousands of records [44:36]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's something really exciting about the cheaper models, Gemini Flash 2.5 Lite, things like that. Being able to run those in a loop against thousands of records feels very valuable to me as well."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://enrichments.datasette.io/"&gt;datasette-enrichments&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Multimodal LLMs for images, audio transcription, and video processing [45:42]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"At one point I calculated that using Google's least expensive model, if I wanted to generate captions for like 70,000 photographs in my personal photo library, it would cost me like $13 or something. Wildly inexpensive."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Correction: with Gemini 1.5 Flash 8B &lt;a href="https://simonwillison.net/2025/May/15/building-on-llms/#llm-tutorial-intro.009.jpeg"&gt;it would cost 173.25 cents&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;First programming language: hated C++, loved PHP and Commodore 64 BASIC [46:54]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I hated C++ 'cause I got my parents to buy me a book on it when I was like 15 and I did not make any progress with Borland C++ compiler... Actually, my first program language was Commodore 64 BASIC. And I did love that. Like I tried to build a database in Commodore 64 BASIC back when I was like six years old or something."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Biggest production bug: crashing The Guardian's MPs expenses site with a progress bar [47:46]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I tweeted a screenshot of that progress bar and said, 'Hey, look, we have a progress bar.' And 30 seconds later the site crashed because I was using SQL queries to count all 17,000 documents just for this one progress bar."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://simonwillison.net/2009/Dec/20/crowdsourcing/"&gt;Crowdsourced document analysis and MP expenses&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Favorite test dataset: San Francisco's tree list, updated several times a week [48:44]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's &lt;a href="https://data.sfgov.org/City-Infrastructure/Street-Tree-List/tkzw-k3nq"&gt;195,000 trees in this CSV file&lt;/a&gt; and it's got latitude and longitude and species and age when it was planted... and get this, it's updated several times a week... most working days, somebody at San Francisco City Hall updates their database of trees, and I can't figure out who."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Showrunning TV shows as a management model - transferring vision to lieutenants [50:07]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Your job is to transfer your vision into their heads so they can go and have the meetings with the props department and the set design and all of those kinds of things... I used to sniff at the idea of a vision when I was young and stupid. And now I'm like, no, the vision really is everything because if everyone understands the vision, they can make decisions you delegate to them."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://okbjgm.weebly.com/uploads/3/1/5/0/31506003/11_laws_of_showrunning_nice_version.pdf"&gt;The Eleven Laws of Showrunning&lt;/a&gt; by Javier Grillo-Marxuach&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hot take: all executable code with business value must be in version control [52:21]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I think it's inexcusable to have executable code that has business value that is not in version control somewhere."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hacker News automation: GitHub Actions scraping for notifications [52:45]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I've got &lt;a href="https://simonwillison.net/2022/Mar/14/scraping-web-pages-shot-scraper/"&gt;a GitHub actions thing&lt;/a&gt; that runs a piece of software I wrote called &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; that runs Playwright, that loads up a browser in GitHub actions to scrape that webpage and turn the results into JSON, which then get turned into an atom feed, which I subscribe to in NetNewsWire."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dream project: whale detection camera with Gemini AI [53:47]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I want to point a camera at the ocean and take a snapshot every minute and feed it into Google Gemini or something and just say, is there a whale yes or no? That would be incredible. I want push notifications when there's a whale."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Favorite podcast: Mark Steel's in Town (hyperlocal British comedy) [54:23]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Every episode he goes to a small town in England and he does a comedy set in a local venue about the history of the town. And so he does very deep research... I love that sort of like hyperlocal, like comedy, that sort of British culture thing."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://www.bbc.co.uk/programmes/b00rtbk8/episodes/player"&gt;Mark Steel's in Town&lt;/a&gt; available episodes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Favorite fiction genre: British wizards caught up in bureaucracy [55:06]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"My favorite genre of fiction is British wizards who get caught up in bureaucracy... I just really like that contrast of like magical realism and very clearly researched government paperwork and filings."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://www.antipope.org/charlie/blog-static/2020/10/the-laundry-files-an-updated-c.html"&gt;The Laundry Files&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Rivers_of_London_(book_series)"&gt;Rivers of London&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/The_Rook_(novel)"&gt;The Rook&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="podcast-colophon"&gt;Colophon&lt;/h4&gt;

&lt;p&gt;I used a Claude Project for the initial analysis, pasting in the HTML of the transcript since that included &lt;code&gt;&amp;lt;span data-timestamp="425"&amp;gt;&lt;/code&gt; elements. The project uses the following custom instructions&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You will be given a transcript of a podcast episode. Find the most interesting quotes in that transcript - quotes that best illustrate the overall themes, and quotes that introduce surprising ideas or express things in a particularly clear or engaging or spicy way. Answer just with those quotes - long quotes are fine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I then added a follow-up prompt saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Now construct a bullet point list of key topics where each item includes the mm:ss in square braces at the end&lt;/p&gt;
&lt;p&gt;Then suggest a very comprehensive list of supporting links I could find&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then one more follow-up:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Add an illustrative quote to every one of those key topics you identified&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://claude.ai/share/b2b83b99-c506-4865-8d40-dee290723ac9"&gt;the full Claude transcript&lt;/a&gt; of the analysis.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/data"&gt;data&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/podcast-appearances"&gt;podcast-appearances&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="data"/><category term="data-journalism"/><category term="django"/><category term="ai"/><category term="datasette"/><category term="podcast-appearances"/></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>Talk Python: Celebrating Django's 20th Birthday With Its Creators</title><link href="https://simonwillison.net/2025/Aug/29/talk-python/#atom-tag" rel="alternate"/><published>2025-08-29T20:02:50+00:00</published><updated>2025-08-29T20:02:50+00:00</updated><id>https://simonwillison.net/2025/Aug/29/talk-python/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://talkpython.fm/episodes/show/518/celebrating-djangos-20th-birthday-with-its-creators"&gt;Talk Python: Celebrating Django&amp;#x27;s 20th Birthday With Its Creators&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I recorded this podcast episode recently to celebrate Django's 20th birthday with Adrian Holovaty, Will Vincent, Jeff Triplet, and Thibaud Colas.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We didn’t know that it was a web framework. We thought it was a tool for building local newspaper websites. [...]&lt;/p&gt;
&lt;p&gt;Django’s original tagline was ‘Web development on journalism deadlines’. That’s always been my favorite description of the project.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/adrian-holovaty"&gt;adrian-holovaty&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/podcast-appearances"&gt;podcast-appearances&lt;/a&gt;&lt;/p&gt;



</summary><category term="adrian-holovaty"/><category term="django"/><category term="python"/><category term="podcast-appearances"/></entry><entry><title>Happy 20th birthday Django! Here's my talk on Django Origins from Django's 10th</title><link href="https://simonwillison.net/2025/Jul/13/django-birthday/#atom-tag" rel="alternate"/><published>2025-07-13T18:47:13+00:00</published><updated>2025-07-13T18:47:13+00:00</updated><id>https://simonwillison.net/2025/Jul/13/django-birthday/#atom-tag</id><summary type="html">
    &lt;p&gt;Today is the &lt;a href="https://www.djangoproject.com/weblog/2025/jul/13/happy-20th-birthday-django/"&gt;20th anniversary&lt;/a&gt; of &lt;a href="https://github.com/django/django/commit/d6ded0e91bcdd2a8f7a221f6a5552a33fe545359"&gt;the first commit&lt;/a&gt; to the public Django repository!&lt;/p&gt;
&lt;p&gt;Ten years ago we threw a multi-day 10th birthday party for Django back in its birthtown of Lawrence, Kansas. As a personal celebration of the 20th, I'm revisiting the talk I gave at &lt;em&gt;that&lt;/em&gt; event and writing it up here.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://www.youtube.com/watch?v=wqii_iX0RTs"&gt;the YouTube video&lt;/a&gt;. Below is a full transcript, plus my slides and some present-day annotations.&lt;/p&gt;

&lt;p&gt;&lt;lite-youtube videoid="wqii_iX0RTs" js-api="js-api"
  title="Django Origins"
  playlabel="Play: Django Origins"
&gt; &lt;/lite-youtube&gt;&lt;/p&gt;

&lt;h4&gt;Django Origins (and some things I have built with Django)&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Presented 11th July 2015 at Django Birthday in Lawrence, Kansas&lt;/em&gt;&lt;/p&gt;

  &lt;p&gt;My original talk title, as you'll see on your programs, was "Some Things I've Built with Django." But then I realized that we're here in the birthplace of Django, celebrating the 10th birthday of the framework, and nobody's told the origin story yet. So, I've switched things around a little bit. I'm going to talk about the origin story of Django, and then if I have time, I'll do the self-indulgent bit and talk about some of the projects I've shipped since then.&lt;/p&gt;
&lt;p&gt;I think Jacob's introduction hit on something I've never really realized about myself. I do love shipping things. The follow-up and the long-term thing I'm not quite so strong on. And that came to focus when I was putting together this talk and realized that basically every project I'm going to show you, I had to dig out of the Internet Archive.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Ten years on from writing this talk I'm proud that I've managed to overcome my weakness in following-up - I'm now actively maintaining a bewildering array of projects, having finally figured out how to &lt;a href="https://simonwillison.net/2022/Nov/26/productivity/"&gt;maintain things&lt;/a&gt; in addition to creating them!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But that said, I will tell you the origin story of Django.&lt;/p&gt;

&lt;div class="slide" id="django-birthday02.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday02.jpg" alt="adrian holovaty blog post

May 31, 2003, 11:49 AM ET
Job opportunity: Web programmer/developer

I interrupt this blogging hiatus to announce a job opportunity.

World Online, my employer here in beautiful Lawrence, Kansas, is looking for another Web programmer to help build cool stuff for our three sites, ljworld.com, lawrence.com and kusports.com ...
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday02.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;For me, the story starts very much like Jacob's. I was reading RSS feeds back in 2003, and I saw &lt;a href="https://www.holovaty.com/writing/211/"&gt;this entry on Adrian's blog&lt;/a&gt;, talking about a job opportunity for a web programmer or developer in Lawrence, Kansas.&lt;/p&gt;
&lt;p&gt;Now, I was in England. I was at university. But my university had just given me the opportunity to take a year abroad, to take a year out to do an internship year in industry. My girlfriend at the time was off to Germany to do her year in industry. So I was like, well, you know, do I stay at university? And then this comes along.&lt;/p&gt;
&lt;p&gt;So I got in touch with Adrian and said, you know, could this work as a year-long internship instead? And he was reading my blog and I was reading his blog, and we knew that we aligned on a bunch of things. So we thought we'd give it a go.&lt;/p&gt;
&lt;p&gt;Now, if you look through this job ad, you'll see that this is all about expert knowledge of PHP and experience designing and maintaining databases, particularly MySQL. So this was a PHP and MySQL gig.&lt;/p&gt;
&lt;p&gt;But when I arrived in Kansas, we quickly realized that we were both kind of over PHP. You know, we'd both built substantial systems in PHP, and we were running up against the limits of what you can do in PHP and have your code still be clean and maintainable.&lt;/p&gt;
&lt;p&gt;And at the same time, we were both reading &lt;a href="https://web.archive.org/web/20020324174618/http://diveintomark.org/"&gt;Mark Pilgrim's blog&lt;/a&gt; (archive link). Mark Pilgrim had been publishing Dive into Python and making a really strong case for why Python was a great web language.&lt;/p&gt;
&lt;p&gt;So we decided that this was the thing we wanted to do. But we both had very strong opinions about how you should build websites. Things like URL design matters, and we care about the difference between get and post, and we want to use this new thing called CSS to lay out our web pages. And none of the existing Python web frameworks really seemed to let us do what we wanted to do.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday03.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday03.jpg" alt="Lawrence JOURNAL-WORLD
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday03.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Now, before I talk more about that, I'll back up and talk about the organization we're working for, the &lt;a href="https://en.wikipedia.org/wiki/Lawrence_Journal-World"&gt;Lawrence Journal World&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;David &lt;a href="https://www.youtube.com/watch?v=FDsqFD4pDy4"&gt;gave a great introduction&lt;/a&gt; to why this is an interesting organization. Now, we're talking about a newspaper with a circulation of about 10,000, like a tiny newspaper, but with a world-class newsroom, huge amounts of money being funneled into it, and like employing full-time software developers to work at a local newspaper in Kansas.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday04.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday04.jpg" alt="Rob Curley (and a photo of Rob)" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday04.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And part of what was going on here was this guy. This is Rob Curley. He's been mentioned a few times before already.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday05.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday05.jpg" alt="Unofficial mission statement: “build cool shit”
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday05.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And yeah, Rob Curley set this unofficial mission statement that we "build cool shit". This is something that Adrian would certainly never say. It's not really something I'd say. But this is Rob through and through. He was a fantastic showman.&lt;/p&gt;
&lt;p&gt;And this was really the appeal of coming out to Lawrence, seeing the stuff they'd already built and the ambitions they had.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday06.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday06.jpg" alt="Screenshot of Lawrence.com - Focus on Kansas. Community blogs, calendars, merch, links to movies, video games, eating out and more." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday06.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;This is Lawrence.com. This is actually the Lawrence.com written in PHP that Adrian had built as the sole programmer at the Lawrence Journal World. And you should check this out. Like, even today, this is the best local entertainment website I have ever seen. This has everything that is happening in the town of Lawrence, Kansas population, 150,000 people. Every gig, every venue, all of the stuff that's going on.&lt;/p&gt;
&lt;p&gt;And it was all written in PHP. And it was a very clean PHP code base, but it was really stretching the limits of what it's sensible to do using PHP 4 back in 2003.&lt;/p&gt;
&lt;p&gt;So we had this goal when we started using Python. We wanted to eventually rebuild Lawrence.com using Python. But in order to get there, we had to first build -- we didn't even know it was a web framework. We called it the CMS.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday07.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday07.jpg" alt="6 Weather Lawrence. An image shows the Lawrence skyline with different conditions for the next 6 days." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday07.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And so when we started working on Django, the first thing that we shipped was actually this website. We had a lot of the six-news Lawrence. This is the six-news Lawrence -- six-news is the TV channel here -- six-news Lawrence weather page.&lt;/p&gt;
&lt;p&gt;And I think this is pretty cool. So Dan Cox, the designer, was a fantastic illustrator. We actually have this illustration of the famous Lawrence skyline with each panel could be displayed with different weather conditions depending on the weather.&lt;/p&gt;
&lt;p&gt;And in case you're not from Kansas, you might not have realized that the weather is a big deal here. You know, you have never seen more excited weathermen than when there's a tornado warning and they get to go on local news 24 hours a day giving people updates.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday08.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday08.jpg" alt="6 News Lawrence - 6 TV news anchor portrait photos in the heading." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday08.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So we put the site live first. This was the first ever Django website. We then did the rest of the 6 News Lawrence website.&lt;/p&gt;
&lt;p&gt;And this -- Adrian reminded me this morning -- the launch of this was actually delayed by a week because the most important feature on the website, which is clearly the photograph of the news people who are on TV, they didn't like their hairdos. They literally told us we couldn't launch the website until they'd had their hair redone, had the headshots retaken, had a new image put together. But, you know, image is important for these things.&lt;/p&gt;
&lt;p&gt;So anyway, we did that. We did six-news Lawrence. And by the end of my year in Kansas, Adrian had rewritten all of Lawrence.com as well.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday09.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday09.jpg" alt="Lawrence.com with a new design, it looks very cool." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday09.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So this is the Lawrence.com powered by Django. And one thing I think is interesting about this is when you talk to like David Heinemeier Hansson about Rails, he'll tell you that Rails is a framework that was extracted from Basecamp. They built Basecamp and then they pulled out the framework that they used and open sourced it.&lt;/p&gt;
&lt;p&gt;I see Django the other way around. Django is a framework that was built up to create Lawrence.com. Lawrence.com already existed. So we knew what the web framework needed to be able to do. And we just kept on iterating on Django or the CMS until it was ready to produce this site here.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday10.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday10.jpg" alt="LJWorld.com Game 2006 - photos of kids playing sports, stories about kid sports, links to photo galleries and playing locations and schedules and more." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday10.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And for me, the moment I realized that we were onto something special was actually when we built this thing. This is a classic Rob Curley project. So Rob was the boss. He had the crazy ideas and he didn't care how you implemented them. He just wanted this thing done.&lt;/p&gt;
&lt;p&gt;And he came to us one day and said, you know, the kids' little league season is about to start. Like kids playing softball or baseball. Whatever the American kids with bats thing is. So he said, kids' little league season is about to start. And we are going to go all out.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday11.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday11.jpg" alt="A Game page showing DCABA 10K Blue - a local team plus their schedule." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday11.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;I want to treat these kids like they're the New York Yankees. We're going to have player profiles and schedules and photos and results.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday12.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday12.jpg" alt="A form to sign up for cell phone updates for that team." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday12.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And, you know, we're going to have the ability for parents to get SMS notifications whenever their kid scores.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday13.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday13.jpg" alt="An index page showing 360 degree field photos for 12 different venues around town." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday13.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And we're going to have 360 degree, like, interactive photos of all of the pitches in Lawrence, Kansas, that these kids are playing games on.&lt;/p&gt;
&lt;p&gt;They actually did send a couple of interns out with a rig to take 360 degree virtual panoramas of Fenway Park and Lawrence High School and all of these different places.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday14.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday14.jpg" alt="... in three days
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday14.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And he said -- and it starts in three days. You've got three days to put this all together.&lt;/p&gt;
&lt;p&gt;And we pulled it off because Django, even at that very early stage, had all of the primitives you needed to build 360 degree interactives. That was all down to the interns. But we had all of the pieces we needed to pull this together.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday15.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday15.jpg" alt="&amp;quot;The CMS&amp;quot;" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday15.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So when we were working on it back then, we called it the CMS.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday16.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday16.jpg" alt="brazos
webbing
physique
anson
The Tornado Publishing System
private dancer
fizgig
lavalier
pythy

https://jacobian.org/writing/private_dancer/
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday16.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;A few years ago, &lt;a href="https://jacobian.org/2005/sep/9/private_dancer/"&gt;Jacob found a wiki page&lt;/a&gt; with some of the names that were being brainstormed for the open source release. And some of these are great. There's Brazos -- I don't know where that came from -- Webbing, Physique, Anson.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday17.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday17.jpg" alt="Highlighted: The Tornado Publishing System" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday17.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;This is my favorite name. I think this is what I proposed -- is the Tornado Publishing System.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday18.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday18.jpg" alt="Screenshot from Office Space. Lumbergh says &amp;quot;Yeah, if you could go ahead and get those TPS reprots to me as soon as possible... that&amp;#39;d be great&amp;quot;." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday18.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And the reason is that I was a really big fan of &lt;a href="https://en.wikipedia.org/wiki/Office_Space"&gt;Office Space&lt;/a&gt;. And if we had the Tornado, we could produce TPS reports, which I thought would be amazing.&lt;/p&gt;
&lt;p&gt;But unfortunately, this being Kansas, the association of Tornadoes isn't actually a positive one.&lt;/p&gt;
&lt;p&gt;Private Dancer, Physgig, Lavalia, Pithy -- yeah. I'm very, very pleased that they picked the name that they did.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday19.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday19.jpg" alt="“Wouldn&amp;#39;t It be cool If...”
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday19.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So one of our philosophies was build cool shit. The other philosophy we had was what we called "Wouldn't it be cool if?"&lt;/p&gt;
&lt;p&gt;So there were no user stories or careful specs or anything. We'd all sit around in the basement and then somebody would go "Wouldn't it be cool if...", and they'd say something. And if we thought it was a cool idea, we'd build it and we'd ship it that day.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday20.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday20.jpg" alt="Lawrence.com featured audio page - a list of bands each with links to their music and information about where they are playing in town this week." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday20.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And my favorite example of "Wouldn't it be cool if?" -- this is a classic Adrian one -- is "Wouldn't it be cool if the downloads page on Lawrence.com featured MP3s you could download of local bands?" And seeing as we've also got the schedule of when the bands are playing, why don't we feature the audio from bands who you can go and see that week?&lt;/p&gt;
&lt;p&gt;So this page will say, "OK Jones are playing on Thursday at the Bottleneck. Get their MP3. Listen to the radio station." We had a little MP3 widget in there. Go and look at their band profile. All of this stuff.&lt;/p&gt;
&lt;p&gt;Really, these kinds of features are what you get when you take 1970s relational database technology and use it to power websites, which -- back in 2003, in the news industry -- still felt incredibly cutting edge. But, you know, it worked.&lt;/p&gt;
&lt;p&gt;And that philosophy followed me through the rest of my career, which is sometimes a good idea and often means that you're left maintaining features that seemed like a good idea at the time and quickly become a massive pain!&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday21.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday21.jpg" alt="YAHOO!
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday21.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;After I finished my internship, I finished my degree in England and then ended up joining up with Yahoo. I was actually working out of the Yahoo UK office but for a R&amp;amp;D team in the States. I was there for about a year and a half.&lt;/p&gt;
&lt;p&gt;One of the things I learned is that you should never go and work for an R&amp;amp;D team, because the problem with R&amp;amp;D teams is you never ship. I was there for a year and a half and I basically have nothing to show for it in terms of actual shipped features.&lt;/p&gt;
&lt;p&gt;We built some very cool prototypes. And actually, after I left, one of the projects I worked on, &lt;a href="https://en.wikipedia.org/wiki/Fire_Eagle"&gt;Yahoo FireEagle&lt;/a&gt;, did end up getting spun out and turned into a real product.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday22.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday22.jpg" alt="YAHOO! ASTRONEWSOLOGY

Dick Cheneey (age 65)

Compare their horoscope with our recent news stories!

A very close friend or a member of your current peer group -- who means a great deal to you -- has recently found it necessary to go out of their way to tick you off. At least, that&amp;#39;s the way it seems. It&amp;#39;s worked, too -- better than it should have. You&amp;#39;re not just angry, you&amp;#39;re furious. Before you let go and let them have it, be sure you&amp;#39;re right. Feeling righteous is far better than feeling guilty

Fox News wins battle for Cheney interview (Reuters) - 16th February, 12:13
Cheney Says He Has Power to Declassify Info (AP) - 16th February, 09:56
Cheney Mishap Takes Focus Off CIA Leak (AP) - 16th February, 09:13
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday22.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;But there is one project -- the first project I built at Yahoo using Django that I wanted to demonstrate. This was for Yahoo's internal hack day. And so Tom Coates and myself, who were working together, we decided that we were going to build a mashup, because it was 2005 and mashups were the cutting edge of computer science.&lt;/p&gt;
&lt;p&gt;So we figured, OK, let's take the two most unlikely Yahoo products and combine them together and see what happens. My original suggestion was that we take Yahoo Dating and Yahoo Pets. But I was told that actually there was this thing called Dogster and this other thing called Catster, which already existed and did exactly that.&lt;/p&gt;
&lt;p&gt;So the next best thing, we went for Yahoo News and Yahoo Horoscopes. And what we ended up building -- and again, this is the first Django application within Yahoo -- was Yahoo Astronewsology.&lt;/p&gt;
&lt;p&gt;And the idea was you take the news feed from Yahoo News, you pull out anything that looks like it's a celebrity's name, look up their birth date, use that to look up their horoscope, and then combine them on the page.
And in a massive stroke of luck, we built this the week that Dick Cheney &lt;a href="https://en.wikipedia.org/wiki/Dick_Cheney_hunting_accident"&gt;shot his friend in the face&lt;/a&gt; while out hunting.&lt;/p&gt;
&lt;p&gt;Dick Cheney's horoscope for that week says, "A very close friend who means a great deal to you has found it necessary to go out of their way to tick you off. You're not just angry, you're furious. Before you let go and let them have it, be sure you're right. Feeling righteous is far better than feeling guilty."&lt;/p&gt;
&lt;p&gt;And so if Dick Cheney had only had only been reading his horoscopes, maybe that whole situation would have ended very differently.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday23.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday23.jpg" alt="The Guardian" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday23.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So after Yahoo, I spent a while doing consulting and things, mainly &lt;a href="https://simonwillison.net/tags/openid/"&gt;around OpenID&lt;/a&gt; because I was determined to make OpenID work. I was absolutely convinced that if OpenID didn't take off, just one company would end up owning single sign-on for the entire internet, and that would be a total disaster.&lt;/p&gt;
&lt;p&gt;And with hindsight, it didn't quite happen. Facebook login looked like it was going to do that a few years ago, but these days there's enough variety out there that I don't feel like we all have to submit to our Facebook masters.&lt;/p&gt;
&lt;p&gt;But, you know, I was enjoying freelancing and consulting and so on. And then I ended up going for coffee with somebody who worked for &lt;a href="https://www.theguardian.com/"&gt;The Guardian&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm sure you've all heard of The Guardian. It's one of England's most internationally focused newspapers. It's a very fine publication. And I realized that I really missed working in a newsroom environment. And I was incredibly jealous of people like Adrian, who'd gone off to the Washington Post and was doing data journalism there, and Derek Willis as well, who bounced from the Post and The New York Times. There was all of this cool data journalism stuff going on.&lt;/p&gt;
&lt;p&gt;And The Guardian's pitch was basically, we've been building a CMS from scratch in Java with a giant team of engineers, and we've built it and it's really cool, but we're not shipping things quickly. We want to start exploring this idea of building things much faster to fit in with the news cycle.&lt;/p&gt;
&lt;p&gt;And that was a very, very tempting thing for me to get involved with. So I went to work for The Guardian.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday24.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday24.jpg" alt="Photo of Simon Rogers, looking like a man who can find you the right data." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday24.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And The Guardian have a really interesting way of doing onboarding of new staff. The way they do it is they set you up on coffee dates with people from all over the organization. So one day you'll be having coffee with somebody who sells ads, and the next day it'll be the deputy editor of the newsroom, and the next day it'll be a journalist somewhere. And each of these people will talk to you and then they'll suggest other people for you to meet up with. So over the first few weeks that you're there, you meet a huge variety of people.&lt;/p&gt;
&lt;p&gt;And time and time again, as I was talking to people, they were saying, "You know what? You should go and talk to Simon Rogers, this journalist in the newsroom."&lt;/p&gt;
&lt;p&gt;This is Simon Rogers. I went down to talk to him, and we had this fascinating conversation. So Simon is a journalist. He worked in the newsroom, and his speciality was gathering data for The Guardian's infographics. Because they are in the paper. They post, they have graphs and charts and all sorts of things like that that they publish.&lt;/p&gt;
&lt;p&gt;It turns out that Simon was the journalist who knew how to get that data out of basically any source you can imagine. If you wanted data, he would make some phone calls, dig into some government contacts and things, and he'd get those raw numbers. And all of the other journalists thought he was a bit weird, because he liked hanging out and editing Excel spreadsheets and stuff.&lt;/p&gt;
&lt;p&gt;So I said to him halfway through this conversation, "Just out of interest, what do you do with those Excel spreadsheets?" And he's like, "Oh, I keep them all on my hard drive." And showed me this folder with hundreds and hundreds of meticulously researched, properly citable news quality spreadsheets full of data about everything you could imagine. And they lived on his hard drive and nowhere else.&lt;/p&gt;
&lt;p&gt;And I was like, "Have you ever talked to anyone in the engineering department upstairs?" And we made this connection.&lt;/p&gt;
&lt;p&gt;And so from then on, we had this collaboration going where he would get data and he'd funnel it to me and see if we could, see if I or someone else in the engineering department at Guardian could do something fun with it.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday25.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday25.jpg" alt="Guardian website screenshot.

BNP members: the far right map of Britain

A court injunction prevents the distribution of the names on the
BNP membership leaked online. This map shows you which
constituencies have the most BNP members

Then a BNP membership by constituency colourful map.
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday25.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And so that was some of the most rewarding work of my career, because it's journalism, you know, it's news, it's stuff that matters. The deadlines are ridiculous. If a news story breaks and it takes you three weeks to turn around a piece of data journalism around it, why did you even bother? And it's perfect for applying Django to.&lt;/p&gt;
&lt;p&gt;So the first story I got to work on at the Guardian was actually one of the early WikiLeaks things. This is before WikiLeaks was like massively high profile. But quite early on, WikiLeaks leaked a list of all of the members of the British National Party, basically the British Nazis. They leaked a list of all of their names and addresses.&lt;/p&gt;
&lt;p&gt;And the Guardian is an ethical newspaper, so we're not going to just publish 18,000 people's names and addresses. But we wanted to figure out if there was something we could do that would make use of that data but wouldn't be violating anyone's individual privacy.&lt;/p&gt;
&lt;p&gt;And so what we did is we took all of the addresses, geocoded them, figured out which parliamentary constituency they lived in, and used that to generate a heat map that's actually called a choropleth map, I think, of the UK showing where the hotspots of BNP activity were.&lt;/p&gt;
&lt;p&gt;And this works because in the UK a parliamentary constituency is, they're designed to all have around about the same population. So if you just like make the color denser for the larger numbers of BNP members, you get this really interesting heat map of the country.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday26.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday26.jpg" alt="A photo of that same map shown in a paper newspaper" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday26.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And what was really cool about this is that I did this using SVG, because we have an infographics department with Illustrator who are good at working with SVG. And it's very easy with an SVG file with the right class names on things to set colors on different regions.&lt;/p&gt;
&lt;p&gt;And because we produced it in SVG, we could then hand it over to the print department, and the next day it was out in the paper. It was like a printed thing on paper, on like dead trees distributed all over the country, which I thought was super cool.&lt;/p&gt;
&lt;p&gt;So that was the first data journalism project that we did at The Guardian. And it really helped prove that given the right data sets and like the right tools and a bit of freedom, you can do some really cool things.&lt;/p&gt;
&lt;p&gt;The first few times I did this, I did it by hand. Then we had The Guardian's first hack day and I was like, well okay, I'm going to build a little self-service tool for our infographics journalists to like dump in a bunch of CSV numbers and get one of these maps out of it.&lt;/p&gt;
&lt;p&gt;So I built this tool. I didn't have anywhere official to deploy it, so I just ran it on my Linux desktop underneath my desk. And they started using it and putting things in the paper and I kind of forgot about it. And every now and then I get a little feature request.&lt;/p&gt;
&lt;p&gt;A few years after I left The Guardian, I ran into someone who worked there. And he was like, yeah, you know that thing that you built? So we had to keep your desktop running for six months after you left. And then we had to like convert it into a VMware instance. And as far as I know, my desktop is still running as a VMware instance somewhere in The Guardian.&lt;/p&gt;
&lt;p&gt;Which ties into the Simon database, I guess. The hard thing is building stuff is easy. Keeping it going it turns out is surprisingly difficult.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday27.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday27.jpg" alt="Website:  Investigate your MP&amp;#39;s expenses
mps-expenses.guaraian.co.uk

Join us in digging through the documents of MPs&amp;#39; expenses to identify individual claims, or documents that you think merit further investigation. You can work through your own MP&amp;#39;s expenses, or just hit the button below to start reviewing. 

A progress bar shows 28,801 of you have reviewed 221,220 of them, only 237o612 to go..." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday27.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;This was my favorite project at The Guardian. There was &lt;a href="https://en.wikipedia.org/wiki/United_Kingdom_parliamentary_expenses_scandal"&gt;a scandal in the UK a few years ago&lt;/a&gt; where it turned out that UK members of parliament had all been fiddling their expenses.&lt;/p&gt;
&lt;p&gt;And actually the background on this is that they're the lowest paid MPs anywhere in Europe. And it seems like the culture had become that you become an MP and on your first day somebody takes you aside and goes, look, I know the salary is terrible. But here's how to fill your expenses and make up for it.&lt;/p&gt;
&lt;p&gt;This was a scandal that was brewing for several years. The Guardian had actually filed freedom of information requests to try and get these expense reports. Because they were pretty sure something dodgy was going on. The government had dragged their heels in releasing the documents.&lt;/p&gt;
&lt;p&gt;And then just when they were a month before they finally released the documents, a rival newspaper, the Telegraph, managed to get hold of a leaked copy of all of these expenses. And so the Telegraph had 30 days lead on all of the other newspapers to dig through and try and find the dirt.&lt;/p&gt;
&lt;p&gt;So when they did release the expenses 30 days later, we had a race on our hands because we needed to analyze 20,000 odd pages of documents. Actually, here it says 450,000 pages of documents in order to try and find anything left that was newsworthy.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday28.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday28.jpg" alt="Page 34 of Janet Dean&amp;#39;s Incidental Expenses Provision 2007/08

Much of the page is redacted. 

What kind of page is this? Buttons for:
Claim, Proof, Blank, Other

Is this page interesting? Should we investigate it further?" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday28.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And so we tackled this with crowdsourcing. We stuck up a website. We told people, we told Guardian readers, come to this website, hit the button, we'll show you a random page from someone's expenses. And then you can tell us if you think it's not interesting, interesting, or we should seek an investigative reporter on it.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday29.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday29.jpg" alt="Hywel Francis MP&amp;#39;s expenses

Labour MP for Aberavon. A photo of him smiling. Below is a table of documents each showing progress through reviewing each one." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday29.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And one of the smartest things we did with this is we added a feature where you could put in your postcode, we'd figure out who your MP was, and then we would show you their smug press photo. You know, their smug face next to all of their expense claims that they'd filed.&lt;/p&gt;
&lt;p&gt;And this was incredibly effective. People were like, "Ooh, you look so smug. I'm going to get you." And once we put this up, and within 18 hours, our community had burned through hundreds of thousands of pages of expense documents trying to find this stuff.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday30.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday30.jpg" alt="Screenshot showing thumbnails of a document that is being processed." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday30.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And again, this was built in Django. We had, I think, five days warning that these documents are coming out. And so it was a total, like, I think I built a proof of concept on day one. That was enough to show that it was possible. So I got a team with a designer and a couple of other people to help out. And we had it ready to go when the document dump came out on that Friday.&lt;/p&gt;
&lt;p&gt;And it was pretty successful. We dug up some pretty interesting stories from it. And it was also just a fantastic interactive way of engaging our community. And, you know, the whole crowdsourcing side of it was super fun.&lt;/p&gt;
&lt;p&gt;So I guess the thing I've learned from that is that, oh, my goodness, it's fun working for newspapers. And actually, if you -- the Lawrence Journal world, sadly, no longer has its own technology team. But there was a period a few years ago where they were doing some cracking data journalism work. Things like tracking what the University of Kansas had been using its private jet for, and letting people explore the data around that and so on.&lt;/p&gt;
&lt;p&gt;The other thing we did at the Guardian, this is going back to Simon Rogers, is he had all of these spreadsheets on his hard drive. And we're like, okay, we should really try and publish this stuff as raw data. Because living on your hard drive under your head is a crying shame.&lt;/p&gt;
&lt;p&gt;And the idea we came up with was essentially to start something we called &lt;a href="https://www.theguardian.com/news/datablog/2009/mar/10/blogpost1"&gt;the Data blog&lt;/a&gt; and publish them as Google spreadsheets. You know, we spent a while thinking, well, you know, what's the best format to publish these things in? And we're like, well, they're in Excel. Google spreadsheets exists and it's pretty good. Let's just put a few of them up as Google sheets and see what people do with them.&lt;/p&gt;
&lt;p&gt;And it turns out that was enough to build this really fun community of data nerds around the Guardian's data blog who would build their own visualizations. They'd dig into the data. And it meant that we could get all sorts of -- like, we could get so much extra value from the work that we were already doing to gather these numbers for the newspaper. That stuff was super fun.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday31.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday31.jpg" alt="Side projects
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday31.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Now, while I was working at the Guardian, I also got into the habit of building some projects with my girlfriend at the time, now my wife Natalie. So Natalie and I have skill sets that fit together very nicely. She's a front-end web developer. I do back-end Django stuff. I do just enough ops to be dangerous. And so between the two of us, we can build websites.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday32.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday32.jpg" alt="Django People

A map of the world with green markers, plus a table of the countries with the most registered Django community members." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday32.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The first things we worked on together is a site which I think some people here should be familiar with, called Django People. The idea was just, you know, the Django community appears to be quite big now. Let's try and get people to stick a pin on a map and tell us where they are.&lt;/p&gt;
&lt;p&gt;Django People still exists today. It's online thanks to a large number of people constantly bugging me at Django Cons and saying, look, just give us the code and the data and we'll get it set up somewhere so it can continue to work. And that's great. I'm really glad I did that because this is the one project that I'm showing you today which is still available on the web somewhere. (&lt;em&gt;2025 update: the site is no longer online.&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;But Django People was really fun. And the thing we learned from this, my wife and I, is that we can work together really well on things.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday33.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday33.jpg" alt="/dev/fort

A photo of a very cool looking sea fortress." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday33.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The other side project we did was much more of a collaborative effort. Again, this no longer exists, or at least it's no longer up on the web. And I'm deeply sad about this because it's my favorite thing I'm going to show you.&lt;/p&gt;
&lt;p&gt;But before I show you the project, I'll show you how we built it. We were at a BarCamp in London with a bunch of our new friends and somebody was showing photographs of this Napoleonic sea fortress that they had rented out for the weekend from an organization in the UK called &lt;a href="https://www.landmarktrust.org.uk/"&gt;the Landmark Trust&lt;/a&gt;, who basically take historic buildings and turn them into vacation rentals as part of the work to restore them.&lt;/p&gt;
&lt;p&gt;And we were like, "Oh, wouldn't it be funny if we rented a castle for a week and all of us went out there and we built stuff together?" And then we were like, "That wouldn't be funny. That would be freaking amazing."&lt;/p&gt;
&lt;p&gt;So we rented this place. This is called &lt;a href="https://en.wikipedia.org/wiki/Fort_Clonque"&gt;Fort Clonque&lt;/a&gt;. It's in the Channel Islands, halfway between England and France. And I think it cost something like $2,000 for the week, but you split that between a dozen people and it's like youth hostel prices to stay in a freaking fortress.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday34.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday34.jpg" alt="Group photos of people hanging out on the fort with their laptops." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday34.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So we got a bunch of people together and we went out there and we just spent a week. We called it &lt;a href="https://devfort.com/"&gt;/dev/fort&lt;/a&gt;. We spent a week just building something together.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday35.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday35.jpg" alt="Where&amp;#39;s my nearest llama?" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday35.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And the thing we ended up building was called Wildlife Near You. And what Wildlife Near You does is it solves the eternal question, "Where is my nearest llama?"&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday36.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday36.jpg" alt="WildlifeNearYou.com

Seen any more animals? Why not add another trip 
or import some photos from Flickr. Or you could
help people identify the animals in their photos!" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday36.jpg"&gt;#&lt;/a&gt;
  
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday37.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday37.jpg" alt="My family trip to Gigrin Farm r-ed Kite Feeding station on 15th April 2008 

Sightings: Common Raven, Common Buzzard, Red Kite" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday37.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Once again, this is a crowdsourcing system. The idea is that you go to wildlifenearyou.com and you've just been on a trip to like a nature park or a zoo or something. And so you create a trip report saying, "I went to the Red Kite feeding station and I saw a common raven and a common buzzard and a red kite." And you import any of your photos from Flickr and so forth.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday38.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday38.jpg" alt="WildlifeNearYou: cookieyum - list of recent trips for this user" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday38.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And you build up this profile saying, "Here are all the places I've been and my favorite animals and things I've seen."&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday39.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday39.jpg" alt="Search &amp;quot;llamas&amp;quot; near &amp;quot;brighton&amp;quot; - shows Ashdown Forest Llama Farm." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday39.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And then once we've got that data set, we can solve the problem. You can say, "Search for llamas near Brighton." And it'll say, "Your nearest llama is 18 miles away and it'll show you pictures of llamas and all of the llama things."&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday40.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday40.jpg" alt="Red Panda: 17 people love this animal. Link to Wikipedia. Your nearest Red Panda is at Marwell Zoo, 51 miles away from Brighton and Hove UK." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday40.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And we have species pages. So here's the red panda page. 17 people love red pandas. You can see them at Taronga Zoo.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday41.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday41.jpg" alt="Which Marmot photo is better?

Two marmot photos - you can select one or the other or click &amp;quot;skip&amp;quot;." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday41.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And then our most viral feature was we had all of these photos of red pandas, but how do we know which is the best photo of a red panda that we should highlight on the red panda page? So we basically built Hot or Not for photographs of wildlife.&lt;/p&gt;
&lt;p&gt;So it's like, "Which marmot photo is better?" And you say, "Well, clearly the one on the right." And it's like, "Okay, which skunk photo is better?"&lt;/p&gt;
&lt;p&gt;I was looking at the logs and people would go through hundreds and hundreds of photos. And you'd get scores and you can see, "Oh, wow, my marmot photo is the second best marmot photo on the whole website."&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday42.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday42.jpg" alt="Find owls near you!
owlsnearyou.com
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday42.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So that was really fun. And then we eventually took it a step further and said, "Okay, well, this is really fun, but this is a website that you have to type on, right?" And meanwhile, mobile phones are now getting HTML5 geolocation and stuff. So can we go a step further?&lt;/p&gt;
&lt;p&gt;So we built owlsnearyou.com. And what owlsnearyou.com does is you type in the location, and it says, "Your nearest owl is 49 miles away." It's a spectacle owl at London Zoo. It was spotted one year ago by Natalie.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday43.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday43.jpg" alt="Owls near 2-3 Kensington St,
Brighton, Brighton and Hove

49.1 miles away
We think your nearest owl is a Spectacled Owl at London Zoo! Spotted
twice, most recently by natbat 1 year ago.
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday43.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And if you went here on a mobile phone-- If you went here on a device that supported geolocation, it doesn't even ask you where you live. It's just like, "Oh, okay, here's your nearest owl."&lt;/p&gt;
&lt;p&gt;And I think we shipped lions near you and monkeys near you and a couple of other domains, but owlsnearyou.com was always my favorite.&lt;/p&gt;
&lt;p&gt;So looking at this now, we should really get this stuff up and running again. It was freaking amazing. Like, this for me is the killer app of all killer apps.&lt;/p&gt;
&lt;p&gt;(&lt;em&gt;We did eventually bring this idea back as &lt;a href="https://www.owlsnearme.com/"&gt;www.owlsnearme.com&lt;/a&gt;, using data from &lt;a href="https://www.inaturalist.org/"&gt;iNaturalist&lt;/a&gt; - that's online today.&lt;/em&gt;)&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday44.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday44.jpg" alt="‘Bugle is a Twitter-like
application for groups of
hackers collaborating in a
castle (or fort, or other
defensive structure) with no
internet connection”
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday44.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So there have actually been a bunch of Devforts since then. One of the things we learned from Devfort is that building applications-- If you want to do a side project, doing one with user accounts and logins and so on, it's a freaking nightmare. It actually took us almost a year after we finished on the fort to finally ship Wildlife Near You because there were so many complexities. And then we had to moderate it and keep an eye on it and so on.&lt;/p&gt;
&lt;p&gt;So if you look at the more recent Devforts, they've taken that to heart. And now they try and ship things which just work and don't require ongoing users logging in and all of that kind of rubbish.&lt;/p&gt;
&lt;p&gt;But one of the other projects I wanted to show you that came out of a Devfort was something called Bugle. And the idea of Bugle is Bugle is a Twitter-like application for groups of hackers collaborating in a castle, fort, or other defensive structure who don't have an internet connection.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday45.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday45.jpg" alt="Screenshot of Bugle - it looks like Twitter, has a &amp;quot;blast! button, various messages include todo list items and git commits and messages and at-mentions" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday45.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;This was basically to deal with Twitter withdrawal when we were all on the fort together and we had an internal network. So Bugle, looking at it now, we could have been Slack! We could have been valued at $2 billion.&lt;/p&gt;
&lt;p&gt;Yeah, Bugle is like an internal Twitter clone with a bunch of extra features like it's got a paste bin and to-do lists and all sorts of stuff like that.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday46.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday46.jpg" alt="So I said to Ben Firshman...
“Wouldn&amp;#39;t it be cool if Twitter
apps on the network could
talk to Bugle instead?”
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday46.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And does anyone here know Ben Firshman? I think quite a few people do. Excellent. So Ben Firshman was out on a Devfort and I did a "Wouldn't it be cool if" on him. I said, "Wouldn't it be cool if all of our Twitter apps and our phones talked to Bugle instead on the network?"&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday47.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday47.jpg" alt="Magic Twitter support

To make Twitter clients magically work with Bugle on a network, we need to mess with BIND.

Shows BIND settings" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday47.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And so if you &lt;a href="https://github.com/simonw/bugle_project/blob/master/README.md#magic-twitter-support"&gt;go and look on GitHub&lt;/a&gt;, I bet this doesn't work anymore. But he did add magic Twitter support where you could run a local DNS server, redirect Twitter to Bugle and we cloned, he cloned enough of the Twitter API that like Twitter apps would work and it would be able to Bugle instead.&lt;/p&gt;
&lt;p&gt;We wanted to do a Devfort in America. You don't really have castles and forts that you can rent for the most part. If anyone knows of one, please come and talk to me because there's a distinct lack of defensible structures at least of the kind that we are used to back in Europe.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday48.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday48.jpg" alt="Lanyrd.com
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday48.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So I'm running out of time, but that's OK because the most recent project, Lanyrd, is something which most people here have probably encountered.&lt;/p&gt;
&lt;p&gt;I will tell a little bit of the backstory of Lanyrd because it's kind of fun.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday49.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday49.jpg" alt="A photo of Natalie and myself in wedding attire with a Golden Eagle perched on a glove on my hand." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday49.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Lanyrd was a honeymoon project. &lt;/p&gt;
&lt;p&gt;Natalie and I got married.  The wildlife near you influence affected our wedding - it was a freaking awesome wedding! You know, in England, you can get a man with a golden eagle and a barn owl and various other birds to show up for about $400 for the day. And then you get to take photos like this.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday50.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday50.jpg" alt="Natalie and I riding a camel on a beach" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday50.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So anyway, we got married, we quit our jobs, I had to leave the Guardian because we wanted to spend the next year or two of our lives just traveling around the world, doing freelancing work on our laptops and so on.&lt;/p&gt;
&lt;p&gt;We got as far as Morocco, we were six months in, when we contracted food poisoning in Casablanca and we were too sick to keep on travelling, so we figured we needed to like, you know, and it was also Ramadan, so it was really hard to get food and stuff. So we rented an apartment for two weeks and said, "Okay, well, since we're stuck for two weeks, let's like finish that side project we've been talking about and ship it and see if anyone's interested."&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday51.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday51.jpg" alt="Lanyrd screenshot: Your contacts&amp;#39; calendar. Shows 303 conferences your Twitter contacts are interested in." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday51.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So we shipped Lanyrd, which was built around the idea of helping people who use Twitter find conferences and events to go to. What we hadn't realised is that if you build something around Twitter, especially back in 2010, it instantly goes viral amongst people who use Twitter.&lt;/p&gt;
&lt;p&gt;So that ended up cutting our honeymoon short, and we actually applied for Y Combinator from Egypt and ended up spending three years building a startup and like hiring people and doing that whole thing.&lt;/p&gt;
&lt;p&gt;(&lt;em&gt;Natalie wrote more about our startup in &lt;a href="https://blog.natbat.net/post/61658401806/lanyrd-from-idea-to-exit-the-story-of-our"&gt;Lanyrd: from idea to exit - the story of our startup&lt;/a&gt;.&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;The only thing I'll say about that is everything in the... Startups have to give the impression that everything's super easy and fun and cool all the time, because people say, "How's your startup going?" And the only correct answer is, "Oh man, it's amazing. It's doing so well." Because everyone has to lie about the misery, pain, anguish and stress that's happening behind the scenes.&lt;/p&gt;
&lt;p&gt;So it was a very interesting three years, and we built some cool stuff and we learnt a lot, and I don't regret it, but do not take startups lightly.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday52.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday52.jpg" alt="Eventbrite
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday52.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;So a year and a half ago, we ended up selling Lanyrd to Eventbrite and moving out to San Francisco. And at Eventbrite, I've been mostly on the management team building side of things, but occasionally managing to sneak some code out as well.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday53.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday53.jpg" alt="Screenshot of the My Events page on Eventbrite - at the top is an orange bar showing SQL render time and number of templates and log lines and requests." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday53.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The one thing I want to show you from Eventbrite, because I really want to open source this thing, is again at Hack Day, we built a tool called the Tikibar, which is essentially like the Django debug toolbar, but it's designed to be run in production. Because the really tough things to debug don't happen in your dev environment. They happen in production when you're hitting a hundred million row database or whatever.&lt;/p&gt;
&lt;p&gt;And so the Tikibar is designed to add as little overhead as possible, but to still give you detailed timelines of SQL queries that are executing and service calls and all of that kind of stuff. It's called the Tikibar because I really like Tikibars.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday54.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday54.jpg" alt="The orange bar is now expanded, it shows a line for each SQL query with a timeline indicating how long each one took." style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday54.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And the best feature is if a page takes over 500 milliseconds to load, the eyes on the Tiki God glow red in disapproval at you.&lt;/p&gt;
&lt;p&gt;If anyone wants a demo of that, come and talk to me. I would love to get a few more instrumentation hooks into Django to make this stuff easier.&lt;/p&gt;
&lt;p&gt;(&lt;em&gt;The Tikibar was eventually open sourced as &lt;a href="https://github.com/eventbrite/tikibar"&gt;eventbrite/tikibar&lt;/a&gt; on GitHub.&lt;/em&gt;)&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class="slide" id="django-birthday55.jpg"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/django-birthday/django-birthday55.jpg" alt="“build cool shit”
(thanks, Rob)
" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/Jul/13/django-birthday/#django-birthday55.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;This has been a whistle-stop tour of the highlights of my career working with Django.&lt;/p&gt;
&lt;p&gt;And actually, in putting this presentation together, I realized that really it's that Rob Curley influence from all the way back in 2003. The reason I love Django is it makes it really easy to build cool shit and to ship it. And, you know, swearing aside, I think that's a reasonable moral to take away from this.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h4 id="colophon"&gt;Colophon&lt;/h4&gt;
&lt;p&gt;I put this annotated version of my 10 year old talk together using a few different tools.&lt;/p&gt;
&lt;p&gt;I fetched the audio from YouTube using &lt;a href="https://github.com/yt-dlp/yt-dlp"&gt;yt-dlp&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yt-dlp -x --audio-format mp3 \
  "https://youtube.com/watch?v=wqii_iX0RTs"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then ran &lt;a href="https://static.simonwillison.net/static/2025/django-birthday.mp3"&gt;the mp3&lt;/a&gt; through &lt;a href="https://goodsnooze.gumroad.com/l/macwhisper"&gt;MacWhisper&lt;/a&gt; to generate an initial transcript. I cleaned that up by &lt;a href="https://claude.ai/share/5fc8a371-7000-4373-afd6-91f1347680cc"&gt;pasting it into Claude Opus 4&lt;/a&gt; with this prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Take this audio transcript of a talk and clean it up very slightly - I want paragraph breaks and tiny edits like removing ums or "sort of" or things like that, but other than that the content should be exactly as presented.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I converted &lt;a href="https://static.simonwillison.net/static/2025/django-birthday.pdf"&gt;a PDF of the slides&lt;/a&gt; into a JPEG per page using this command (found with the &lt;a href="https://github.com/simonw/llm-cmd"&gt;llm-cmd&lt;/a&gt; plugin):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pdftoppm -jpeg -jpegopt quality=70 django-birthday.pdf django-birthday
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I used my &lt;a href="https://tools.simonwillison.net/annotated-presentations"&gt;annotated presentations tool&lt;/a&gt; (&lt;a href="https://simonwillison.net/2023/Aug/6/annotated-presentations/"&gt;described here&lt;/a&gt;) to combine the slides and transcript, making minor edits and adding links using Markdown in that interface.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/adrian-holovaty"&gt;adrian-holovaty&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/devfort"&gt;devfort&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/history"&gt;history&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jacob-kaplan-moss"&gt;jacob-kaplan-moss&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/lawrence"&gt;lawrence&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/lawrence-com"&gt;lawrence-com&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/lawrence-journal-world"&gt;lawrence-journal-world&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/my-talks"&gt;my-talks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/the-guardian"&gt;the-guardian&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-talks"&gt;annotated-talks&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="adrian-holovaty"/><category term="devfort"/><category term="django"/><category term="history"/><category term="jacob-kaplan-moss"/><category term="lawrence"/><category term="lawrence-com"/><category term="lawrence-journal-world"/><category term="python"/><category term="my-talks"/><category term="the-guardian"/><category term="annotated-talks"/></entry><entry><title>Quoting Django’s security policies</title><link href="https://simonwillison.net/2025/Jul/11/django-security-policies/#atom-tag" rel="alternate"/><published>2025-07-11T16:51:08+00:00</published><updated>2025-07-11T16:51:08+00:00</updated><id>https://simonwillison.net/2025/Jul/11/django-security-policies/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://docs.djangoproject.com/en/dev/internals/security/#ai-assisted-reports"&gt;&lt;p&gt;Following the widespread availability of large language models (LLMs), the Django Security Team has received a growing number of security reports generated partially or entirely using such tools. Many of these contain inaccurate, misleading, or fictitious content. While AI tools can help draft or analyze reports, they must not replace human understanding and review.&lt;/p&gt;
&lt;p&gt;If you use AI tools to help prepare a report, you must:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disclose&lt;/strong&gt; which AI tools were used and specify what they were used for (analysis, writing the description, writing the exploit, etc).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify&lt;/strong&gt; that the issue describes a real, reproducible vulnerability that otherwise meets these reporting guidelines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid&lt;/strong&gt; fabricated code, placeholder text, or references to non-existent Django features.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reports that appear to be unverified AI output will be closed without response. Repeated low-quality submissions may result in a ban from future reporting&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://docs.djangoproject.com/en/dev/internals/security/#ai-assisted-reports"&gt;Django’s security policies&lt;/a&gt;, on AI-Assisted Reports&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-ethics"&gt;ai-ethics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-misuse"&gt;ai-misuse&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-security-research"&gt;ai-security-research&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="open-source"/><category term="security"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-ethics"/><category term="ai-misuse"/><category term="ai-security-research"/></entry><entry><title>PR #537: Fix Markdown in og descriptions</title><link href="https://simonwillison.net/2025/Jun/3/openai-codex-pr/#atom-tag" rel="alternate"/><published>2025-06-03T23:58:34+00:00</published><updated>2025-06-03T23:58:34+00:00</updated><id>https://simonwillison.net/2025/Jun/3/openai-codex-pr/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/simonwillisonblog/pull/537"&gt;PR #537: Fix Markdown in og descriptions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Since &lt;a href="https://openai.com/index/introducing-codex/"&gt;OpenAI Codex&lt;/a&gt; is now available to us ChatGPT Plus subscribers I decided to try it out against my blog.&lt;/p&gt;
&lt;p&gt;It's a very nice implementation of the GitHub-connected coding "agent" pattern, as also seen in Google's &lt;a href="https://jules.google/"&gt;Jules&lt;/a&gt; and Microsoft's &lt;a href="https://github.blog/changelog/2025-05-19-github-copilot-coding-agent-in-public-preview/"&gt;Copilot Coding Agent&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First I had to configure an environment for it. My Django blog uses PostgreSQL which isn't part of the &lt;a href="https://github.com/openai/codex-universal"&gt;default Codex container&lt;/a&gt;, so I had Claude Sonnet 4 &lt;a href="https://claude.ai/share/a5ce65c2-a9a4-4ae7-b645-71bd9fd6ea2c"&gt;help me&lt;/a&gt; come up with a startup recipe to get PostgreSQL working.&lt;/p&gt;
&lt;p&gt;I attached my &lt;a href="https://github.com/simonw/simonwillisonblog"&gt;simonw/simonwillisonblog&lt;/a&gt; GitHub repo and used the following as the "setup script" for the environment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Install PostgreSQL
apt-get update &amp;amp;&amp;amp; apt-get install -y postgresql postgresql-contrib

# Start PostgreSQL service
service postgresql start

# Create a test database and user
sudo -u postgres createdb simonwillisonblog
sudo -u postgres psql -c "CREATE USER testuser WITH PASSWORD 'testpass';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE simonwillisonblog TO testuser;"
sudo -u postgres psql -c "ALTER USER testuser CREATEDB;"

pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I left "Agent internet access" off for reasons &lt;a href="https://simonwillison.net/2025/Jun/3/codex-agent-internet-access/"&gt;described previously&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then I prompted Codex with the following (after one previous experimental task to check that it could run my tests):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Notes and blogmarks can both use Markdown.&lt;/p&gt;
&lt;p&gt;They serve &lt;code&gt;meta property="og:description" content="&lt;/code&gt; tags on the page, but those tags include that raw Markdown which looks bad on social media previews.&lt;/p&gt;
&lt;p&gt;Fix it so they instead use just the text with markdown stripped - so probably render it to HTML and then strip the HTML tags.&lt;/p&gt;
&lt;p&gt;Include passing tests.&lt;/p&gt;
&lt;p&gt;Try to run the tests, the postgresql details are:&lt;/p&gt;
&lt;p&gt;database = simonwillisonblog
username = testuser
password = testpass&lt;/p&gt;
&lt;p&gt;Put those in the DATABASE_URL environment variable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I left it to churn away for a few minutes (4m12s, to be precise) and &lt;a href="https://chatgpt.com/s/cd_683f8b81657881919a8d1ce71978a2df"&gt;it came back&lt;/a&gt; with a fix that edited two templates and added one more (passing) test. Here's &lt;a href="https://github.com/simonw/simonwillisonblog/pull/537/files"&gt;that change in full&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And sure enough, the social media cards for my posts now look like this - no visible Markdown any more:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a web browser showing a blog post preview card on Bluesky. The URL in the address bar reads &amp;quot;https://simonwillison.net/2025/Jun/3/pr-537-fix-markdown-in-og-descriptions/&amp;quot;. The preview card shows the title &amp;quot;PR #537: Fix Markdown in og descriptions&amp;quot; and begins with the text &amp;quot;Since OpenAI Codex is now available to us ChatGPT Plus subscribers I decided to try it out against my blog. It's a very nice implementation of the GitHub-connected coding&amp;quot;. The domain &amp;quot;simonwillison.net&amp;quot; appears at the bottom of the card." src="https://static.simonwillison.net/static/2025/codex-fix.jpg" /&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-agents"&gt;ai-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/async-coding-agents"&gt;async-coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jules"&gt;jules&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex"&gt;codex&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="github"/><category term="postgresql"/><category term="testing"/><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="ai-assisted-programming"/><category term="ai-agents"/><category term="coding-agents"/><category term="async-coding-agents"/><category term="jules"/><category term="codex"/></entry><entry><title>django-simple-deploy</title><link href="https://simonwillison.net/2025/May/17/django-simple-deploy/#atom-tag" rel="alternate"/><published>2025-05-17T12:49:52+00:00</published><updated>2025-05-17T12:49:52+00:00</updated><id>https://simonwillison.net/2025/May/17/django-simple-deploy/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://django-simple-deploy.readthedocs.io/"&gt;django-simple-deploy&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Eric Matthes presented a lightning talk about this project at PyCon US this morning. "Django has a deploy command now". You can run it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install django-simple-deploy[fly_io]
# Add django_simple_deploy to INSTALLED_APPS.
python manage.py deploy --automate-all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's plugin-based (&lt;a href="https://github.com/django-simple-deploy/django-simple-deploy/issues/313"&gt;inspired by Datasette!&lt;/a&gt;) and the project has stable plugins for three hosting platforms: &lt;a href="https://github.com/django-simple-deploy/dsd-flyio"&gt;dsd-flyio&lt;/a&gt;, &lt;a href="https://github.com/django-simple-deploy/dsd-heroku"&gt;dsd-heroku&lt;/a&gt; and &lt;a href="https://github.com/django-simple-deploy/dsd-platformsh"&gt;dsd-platformsh&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Currently in development: &lt;a href="https://github.com/django-simple-deploy/dsd-vps"&gt;dsd-vps&lt;/a&gt; - a plugin that should work with any VPS provider, using &lt;a href="https://www.paramiko.org/"&gt;Paramiko&lt;/a&gt; to connect to a newly created instance and &lt;a href="https://github.com/django-simple-deploy/dsd-vps/blob/a372fc7b7fd31cd2ad3cf22d68b9c9fecb65d17a/dsd_vps/utils.py"&gt;run all of the commands&lt;/a&gt; needed to start serving a Django application.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/paramiko"&gt;paramiko&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&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/fly"&gt;fly&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="paramiko"/><category term="plugins"/><category term="python"/><category term="heroku"/><category term="datasette"/><category term="fly"/></entry><entry><title>Django: what’s new in 5.2</title><link href="https://simonwillison.net/2025/Apr/10/django-whats-new-in-52/#atom-tag" rel="alternate"/><published>2025-04-10T16:27:27+00:00</published><updated>2025-04-10T16:27:27+00:00</updated><id>https://simonwillison.net/2025/Apr/10/django-whats-new-in-52/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://adamj.eu/tech/2025/04/07/django-whats-new-5.2/"&gt;Django: what’s new in 5.2&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Adam Johnson provides extremely detailed unofficial annotated release notes for the &lt;a href="https://docs.djangoproject.com/en/5.2/releases/5.2/"&gt;latest Django&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I found his explanation and example of &lt;a href="https://adamj.eu/tech/2025/04/07/django-whats-new-5.2/#form-boundfield-customization"&gt;Form BoundField customization&lt;/a&gt; particularly useful - here's the new pattern for customizing the &lt;code&gt;class=&lt;/code&gt; attribute on the label associated with a &lt;code&gt;CharField&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;django&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;forms&lt;/span&gt;

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;WideLabelBoundField&lt;/span&gt;(&lt;span class="pl-s1"&gt;forms&lt;/span&gt;.&lt;span class="pl-c1"&gt;BoundField&lt;/span&gt;):
    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;label_tag&lt;/span&gt;(&lt;span class="pl-s1"&gt;self&lt;/span&gt;, &lt;span class="pl-s1"&gt;contents&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;None&lt;/span&gt;, &lt;span class="pl-s1"&gt;attrs&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;None&lt;/span&gt;, &lt;span class="pl-s1"&gt;label_suffix&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;None&lt;/span&gt;):
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;attrs&lt;/span&gt; &lt;span class="pl-c1"&gt;is&lt;/span&gt; &lt;span class="pl-c1"&gt;None&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;attrs&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {}
        &lt;span class="pl-s1"&gt;attrs&lt;/span&gt;[&lt;span class="pl-s"&gt;"class"&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"wide"&lt;/span&gt;
        &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-en"&gt;super&lt;/span&gt;().&lt;span class="pl-c1"&gt;label_tag&lt;/span&gt;(&lt;span class="pl-s1"&gt;contents&lt;/span&gt;, &lt;span class="pl-s1"&gt;attrs&lt;/span&gt;, &lt;span class="pl-s1"&gt;label_suffix&lt;/span&gt;)

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;NebulaForm&lt;/span&gt;(&lt;span class="pl-s1"&gt;forms&lt;/span&gt;.&lt;span class="pl-c1"&gt;Form&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;name&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;forms&lt;/span&gt;.&lt;span class="pl-c1"&gt;CharField&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;max_length&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;100&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;label&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Nebula Name"&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;bound_field_class&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-v"&gt;WideLabelBoundField&lt;/span&gt;,
    )&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd also missed the new &lt;a href="https://adamj.eu/tech/2025/04/07/django-whats-new-5.2/#httpresponse-get-preferred-type"&gt;HttpResponse.get_preferred_type() method&lt;/a&gt; for implementing HTTP content negotiation:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;content_type&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-c1"&gt;get_preferred_type&lt;/span&gt;(
    [&lt;span class="pl-s"&gt;"text/html"&lt;/span&gt;, &lt;span class="pl-s"&gt;"application/json"&lt;/span&gt;]
)&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/adam-johnson"&gt;adam-johnson&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="python"/><category term="adam-johnson"/></entry><entry><title>Composite primary keys in Django</title><link href="https://simonwillison.net/2025/Apr/2/composite-primary-keys-in-django/#atom-tag" rel="alternate"/><published>2025-04-02T14:51:53+00:00</published><updated>2025-04-02T14:51:53+00:00</updated><id>https://simonwillison.net/2025/Apr/2/composite-primary-keys-in-django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.djangoproject.com/en/5.2/topics/composite-primary-key/"&gt;Composite primary keys in Django&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Django 5.2 is &lt;a href="https://www.djangoproject.com/weblog/2025/apr/02/django-52-released/"&gt;out today&lt;/a&gt; and a big new feature is composite primary keys, which can now be defined like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Release&lt;/span&gt;(&lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-c1"&gt;Model&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;pk&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-c1"&gt;CompositePrimaryKey&lt;/span&gt;(
        &lt;span class="pl-s"&gt;"version"&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;
    )
    &lt;span class="pl-s1"&gt;version&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-c1"&gt;IntegerField&lt;/span&gt;()
    &lt;span class="pl-s1"&gt;name&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;models&lt;/span&gt;.&lt;span class="pl-c1"&gt;CharField&lt;/span&gt;(&lt;span class="pl-s1"&gt;max_length&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;20&lt;/span&gt;)&lt;/pre&gt;

&lt;p&gt;They don't yet work with the Django admin or as targets for foreign keys.&lt;/p&gt;
&lt;p&gt;Other smaller new features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All ORM models are now automatically imported into &lt;code&gt;./manage.py shell&lt;/code&gt; - a feature borrowed from &lt;code&gt;./manage.py shell_plus&lt;/code&gt; in &lt;a href="https://django-extensions.readthedocs.io/"&gt;django-extensions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Feeds from the Django syndication framework can now specify &lt;a href="https://docs.djangoproject.com/en/5.2/ref/contrib/syndication/#feed-stylesheets"&gt;XSLT stylesheets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse.text"&gt;response.text&lt;/a&gt; now returns the string representation of the body - I'm so happy about this, now I don't have to litter my Django tests with &lt;code&gt;response.content.decode("utf-8")&lt;/code&gt; any more&lt;/li&gt;
&lt;li&gt;a new &lt;a href="https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#django.template.Library.simple_block_tag"&gt;simple_block_tag&lt;/a&gt; helper making it much easier to create a custom Django template tag that further processes its own inner rendered content&lt;/li&gt;
&lt;li&gt;A bunch more in the &lt;a href="https://docs.djangoproject.com/en/5.2/releases/5.2/"&gt;full release notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;5.2 is also an LTS release, so it will receive security and data loss bug fixes up to April 2028.


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



</summary><category term="django"/><category term="python"/></entry><entry><title>suitenumerique/docs</title><link href="https://simonwillison.net/2025/Mar/17/docs/#atom-tag" rel="alternate"/><published>2025-03-17T18:51:50+00:00</published><updated>2025-03-17T18:51:50+00:00</updated><id>https://simonwillison.net/2025/Mar/17/docs/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/suitenumerique/docs"&gt;suitenumerique/docs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New open source (MIT licensed) collaborative text editing web application, similar to Google Docs or Notion, notable because it's a joint effort funded by the French and German governments and "currently onboarding the Netherlands".&lt;/p&gt;
&lt;p&gt;It's built using Django and React:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docs is built on top of &lt;a href="https://www.django-rest-framework.org/"&gt;Django Rest Framework&lt;/a&gt;, &lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt;, &lt;a href="https://www.blocknotejs.org/"&gt;BlockNote.js&lt;/a&gt;, &lt;a href="https://tiptap.dev/docs/hocuspocus/introduction"&gt;HocusPocus&lt;/a&gt; and &lt;a href="https://yjs.dev/"&gt;Yjs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Deployments currently &lt;a href="https://github.com/suitenumerique/docs/blob/main/docs/installation.md"&gt;require&lt;/a&gt; Kubernetes, PostgreSQL, memcached, an S3 bucket (or compatible) and an OIDC provider.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3"&gt;s3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/react"&gt;react&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kubernetes"&gt;kubernetes&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="open-source"/><category term="postgresql"/><category term="s3"/><category term="react"/><category term="kubernetes"/></entry><entry><title>Smoke test your Django admin site</title><link href="https://simonwillison.net/2025/Mar/13/smoke-test-your-django-admin/#atom-tag" rel="alternate"/><published>2025-03-13T15:02:09+00:00</published><updated>2025-03-13T15:02:09+00:00</updated><id>https://simonwillison.net/2025/Mar/13/smoke-test-your-django-admin/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://jmduke.com/posts/post/django-admin-changelist-test/"&gt;Smoke test your Django admin site&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Justin Duke demonstrates a neat pattern for running simple tests against your internal Django admin site: introspect every admin route via &lt;code&gt;django.urls.get_resolver()&lt;/code&gt; and loop through them with &lt;code&gt;@pytest.mark.parametrize&lt;/code&gt; to check they all return a 200 HTTP status code.&lt;/p&gt;
&lt;p&gt;This catches simple mistakes with the admin configuration that trigger exceptions that might otherwise go undetected.&lt;/p&gt;
&lt;p&gt;I rarely write automated tests against my own admin sites and often feel guilty about it. I wrote &lt;a href="https://til.simonwillison.net/django/testing-django-admin-with-pytest"&gt;some notes&lt;/a&gt; on testing it with &lt;a href="https://pytest-django.readthedocs.io/en/latest/helpers.html#fixtures"&gt;pytest-django fixtures&lt;/a&gt; a few years ago.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="django-admin"/><category term="python"/><category term="testing"/><category term="pytest"/></entry><entry><title>My approach to running a link blog</title><link href="https://simonwillison.net/2024/Dec/22/link-blog/#atom-tag" rel="alternate"/><published>2024-12-22T18:37:16+00:00</published><updated>2024-12-22T18:37:16+00:00</updated><id>https://simonwillison.net/2024/Dec/22/link-blog/#atom-tag</id><summary type="html">
    &lt;p&gt;I started running a basic link blog on this domain &lt;a href="https://simonwillison.net/2003/Nov/24/blogmarks/"&gt;back in November 2003&lt;/a&gt; - publishing links (which I called "blogmarks") with a title, URL, short snippet of commentary and a "via" link where appropriate.&lt;/p&gt;
&lt;p&gt;So far I've published &lt;a href="https://simonwillison.net/search/?type=blogmark"&gt;7,607 link blog posts&lt;/a&gt; and counting.&lt;/p&gt;
&lt;p&gt;In April of this year I finally &lt;a href="https://simonwillison.net/2024/Apr/25/blogmarks-that-use-markdown/"&gt;upgraded my link blog to support Markdown&lt;/a&gt;, allowing me to expand my link blog into something with a lot more room.&lt;/p&gt;
&lt;p&gt;The way I use my link blog has evolved substantially in the eight months since then. I'm going to describe the informal set of guidelines I've set myself for how I link blog, in the hope that it might encourage other people to give this a try themselves.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Dec/22/link-blog/#writing-about-things-i-ve-found"&gt;Writing about things I've found&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Dec/22/link-blog/#trying-to-add-something-extra"&gt;Trying to add something extra&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Dec/22/link-blog/#the-technology"&gt;The technology&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Dec/22/link-blog/#more-people-should-do-this"&gt;More people should do this&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="writing-about-things-i-ve-found"&gt;Writing about things I've found&lt;/h4&gt;
&lt;p&gt;Back in November 2022 I wrote &lt;a href="https://simonwillison.net/2022/Nov/6/what-to-blog-about/"&gt;What to blog about&lt;/a&gt;, which started with this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You should start a blog. Having your own little corner of the internet is good for the soul!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The point of that article was to emphasize that blogging doesn't have to be about unique insights. The value is in writing frequently and having something to show for it over time - worthwhile even if you don't attract much of an audience (or any audience at all).&lt;/p&gt;
&lt;p&gt;In that article I proposed two categories of content that are low stakes and high value: &lt;strong&gt;things I learned&lt;/strong&gt; and &lt;strong&gt;descriptions of my projects&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I realize now that link blogging deserves to be included a third category of low stakes, high value writing. We could think of that category as &lt;strong&gt;things I've found&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That's the purpose of my link blog: it's an ongoing log of things I've found - effectively a combination of public bookmarks and my own thoughts and commentary on why those things are interesting.&lt;/p&gt;
&lt;h4 id="trying-to-add-something-extra"&gt;Trying to add something extra&lt;/h4&gt;
&lt;p&gt;When I first started link blogging I would often post a link with a one sentence summary of the linked content, and maybe a tiny piece of opinionated commentary.&lt;/p&gt;
&lt;p&gt;After I upgraded my link blog to support additional markup (links, images, quotations) I decided to be more ambitious. Here are some of the things I try to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I always include &lt;strong&gt;the names of the people&lt;/strong&gt; who created the content I am linking to, if I can figure that out. Credit is really important, and it's also useful for myself because I can later search for someone's name and find other interesting things they have created that I linked to in the past. If I've linked to someone's work three or more times I also try to notice and upgrade them to &lt;a href="https://simonwillison.net/tags/"&gt;a dedicated tag&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I try to &lt;strong&gt;add something extra&lt;/strong&gt;. My goal with any link blog post is that if you read both my post and the source material you'll have an enhanced experience over if you read just the source material itself.
&lt;ul&gt;
&lt;li&gt;Ideally I'd like you to take something useful away even if you don't follow the link itself. This can be a slightly tricky balance: I don't want to steal attention from the authors and plagiarize their message. Generally I'll try to find some key idea that's worth emphasizing. Slightly cynically, I may try to capture that idea as backup against the original source vanishing from the internet. Link rot is real!&lt;/li&gt;
&lt;li&gt;My most basic version of this is trying to provide context as to why I think this particular thing is worth reading - especially important for longer content. A good recent example is my post about Anthropic's &lt;a href="https://simonwillison.net/2024/Dec/20/building-effective-agents/"&gt;Building effective agents&lt;/a&gt; essay the other day.&lt;/li&gt;
&lt;li&gt;I might tie it together to other similar concepts, including things I've written about in the past, for example linking &lt;a href="https://simonwillison.net/2024/Aug/14/prompt-caching-with-claude/"&gt;Prompt caching with Claude&lt;/a&gt; to my coverage of &lt;a href="https://simonwillison.net/2024/May/14/context-caching-for-google-gemini/"&gt;Context caching for Google Gemini&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If part of the material is a video, I might &lt;strong&gt;quote a snippet of the transcript&lt;/strong&gt; (often extracted using MacWhisper) like I did in &lt;a href="https://simonwillison.net/2024/Dec/12/clio/"&gt;this post about Anthropic's Clio&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A lot of stuff I link to involves programming. I'll often include a &lt;strong&gt;direct link to relevant code&lt;/strong&gt;, using the GitHub feature where I can link to a snippet as-of a particular commit. One example is the &lt;a href="https://simonwillison.net/2024/Oct/5/uv-with-github-actions-to-run-an-rss-to-readme-project/"&gt;fetch-rss.py link in this post&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I'm liberal with &lt;strong&gt;quotations&lt;/strong&gt;. Finding and quoting a paragraph that captures the key theme of a post is a very quick and effective way to summarize it and help people decide if it's worth reading the whole thing. My post on &lt;a href="https://simonwillison.net/2024/Dec/20/openai-o3-breakthrough/"&gt;François Chollet's o3 ARC-AGI analysis&lt;/a&gt; is an example of that.&lt;/li&gt;
&lt;li&gt;If the original author reads my post, I want them to &lt;strong&gt;feel good about it&lt;/strong&gt;. I know from my own experience that often when you publish something online the silence can be deafening. Knowing that someone else read, appreciated, understood and then shared your work can be very pleasant.&lt;/li&gt;
&lt;li&gt;A slightly self-involved concern I have is that I like to &lt;strong&gt;prove that I've read it&lt;/strong&gt;. This is more for me than for anyone else: I don't like to recommend something if I've not read that thing myself, and sticking in a detail that shows I read past the first paragraph helps keep me honest about that.&lt;/li&gt;
&lt;li&gt;I've started leaning more into &lt;strong&gt;screenshots&lt;/strong&gt; and even short video or audio clips. A screenshot can be considered a visual quotation - I'll sometimes snap these from interesting frames in a YouTube video or live demo associated with the content I'm linking to. I used a screenshot of the Clay debugger in &lt;a href="https://simonwillison.net/2024/Dec/21/clay-ui-library/"&gt;my post about Clay&lt;/a&gt;.&lt;/li&gt;
&lt;p style="margin-top: 0.5em"&gt;There are a lot of great link blogs out there, but the one that has influenced me the most in how I approach my own is John Gruber's &lt;a href="https://daringfireball.net/"&gt;Daring Fireball&lt;/a&gt;. I really like the way he mixes commentary, quotations and value-added relevant information.&lt;/p&gt;
&lt;/ul&gt;
&lt;h4 id="the-technology"&gt;The technology&lt;/h4&gt;
&lt;p&gt;The technology behind my link blog is probably the least interesting thing about it. It's part of my &lt;a href="https://github.com/simonw/simonwillisonblog"&gt;simonwillisonblog&lt;/a&gt; Django application - the main model is called &lt;a href="https://github.com/simonw/simonwillisonblog/blob/c781a1a42ab0a0237f75c7790f069bacc2d70d3f/blog/models.py#L328-L337"&gt;Blogmark&lt;/a&gt; and it inherits from a &lt;a href="https://github.com/simonw/simonwillisonblog/blob/c781a1a42ab0a0237f75c7790f069bacc2d70d3f/blog/models.py#L172-L203"&gt;BaseModel&lt;/a&gt; defining things like tags and draft modes that are shared across my other types of content (entries and quotations).&lt;/p&gt;
&lt;p&gt;I use the Django Admin to create and edit entries, &lt;a href="https://github.com/simonw/simonwillisonblog/blob/c781a1a42ab0a0237f75c7790f069bacc2d70d3f/blog/admin.py#L73-L76"&gt;configured here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The most cumbersome part of link blogging for me right now is images. I convert these into smaller JPEGs using a &lt;a href="https://tools.simonwillison.net/image-resize-quality"&gt;tiny custom tool&lt;/a&gt; I built (&lt;a href="https://gist.github.com/simonw/58a06a8028515999e5949a0166cd4c4f"&gt;with Claude&lt;/a&gt;), then upload them to my &lt;code&gt;static.simonwillison.net&lt;/code&gt; S3 bucket using Transmit and drop them into my posts using a Markdown image reference. I generate a first draft of the alt text using a Claude Project with &lt;a href="https://gist.github.com/simonw/1fa7e4e3dcb18fdeca2b3d6ac2c6c628"&gt;these custom instructions&lt;/a&gt;, then usually make a few changes  before including that in the markup. At some point I'll wire together a UI that makes this process a little smoother.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;static.simonwillison.net&lt;/code&gt; bucket is then served via Cloudflare's free tier, which means I effectively never have to think about the cost of serving up those image files.&lt;/p&gt;
&lt;p&gt;I wrote up a TIL about &lt;a href="https://til.simonwillison.net/django/building-a-blog-in-django"&gt;Building a blog in Django&lt;/a&gt; a while ago which describes a similar setup to the one I'm using for my link blog, including how the RSS feed works (using &lt;a href="https://docs.djangoproject.com/en/4.2/ref/contrib/syndication/"&gt;Django's syndication framework&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The most technically interesting component is my &lt;a href="https://simonwillison.net/search/?type=blogmark"&gt;search feature&lt;/a&gt;. I wrote about how that works in &lt;a href="https://simonwillison.net/2017/Oct/5/django-postgresql-faceted-search/"&gt;Implementing faceted search with Django and PostgreSQL&lt;/a&gt; - the most recent code for that can be found in &lt;a href="https://github.com/simonw/simonwillisonblog/blob/main/blog/search.py"&gt;blog/search.py&lt;/a&gt; on GitHub.&lt;/p&gt;
&lt;p&gt;One of the most useful small enhancements I added was &lt;a href="https://github.com/simonw/simonwillisonblog/issues/488"&gt;draft mode&lt;/a&gt;, which lets me assign a URL to an item and preview it in my browser without publishing it to the world. This really helps when I am editing posts on my mobile phone as it gives me a reliable preview so I can check for any markup mistakes.&lt;/p&gt;
&lt;p&gt;I also send out an approximately weekly &lt;a href="https://simonw.substack.com/"&gt;email newsletter&lt;/a&gt; version of my blog, for people who want to subscribe in their inbox. This is a straight copy of content from my blog - Substack doesn't have an API for this but their editor does accept copy and paste, so I have a delightful digital duct tape solution for assembling the newsletter which I described in &lt;a href="https://simonwillison.net/2023/Apr/4/substack-observable/"&gt;Semi-automating a Substack newsletter with an Observable notebook&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="more-people-should-do-this"&gt;More people should do this&lt;/h4&gt;
&lt;p&gt;I posted this on Bluesky &lt;a href="https://bsky.app/profile/simonwillison.net/post/3ldu6jywnos2j"&gt;last night&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I wish people would post more links to interesting things&lt;/p&gt;
&lt;p&gt;I feel like Twitter and LinkedIn and Instagram and TikTok have pushed a lot of people out of the habit of doing that, by penalizing shared links in the various "algorithms"&lt;/p&gt;
&lt;p&gt;Bluesky doesn't have that misfeature, thankfully!&lt;/p&gt;
&lt;p&gt;(In my ideal world everyone would get their own link blog too, but sharing links on Bluesky and Mastodon is almost as good)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sharing interesting links with commentary is a low effort, high value way to contribute to internet life at large.&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/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/john-gruber"&gt;john-gruber&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="django"/><category term="django-admin"/><category term="john-gruber"/></entry><entry><title>Is async Django ready for prime time?</title><link href="https://simonwillison.net/2024/Nov/24/async-django/#atom-tag" rel="alternate"/><published>2024-11-24T17:47:27+00:00</published><updated>2024-11-24T17:47:27+00:00</updated><id>https://simonwillison.net/2024/Nov/24/async-django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://jonathanadly.com/is-async-django-ready-for-prime-time"&gt;Is async Django ready for prime time?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Jonathan Adly reports on his experience using Django to build &lt;a href="https://colivara.com/"&gt;ColiVara&lt;/a&gt;, a hosted RAG API that uses &lt;a href="https://huggingface.co/vidore/colqwen2-v1.0"&gt;ColQwen2&lt;/a&gt; visual embeddings, inspired by the &lt;a href="https://arxiv.org/abs/2407.01449"&gt;ColPali&lt;/a&gt; paper.&lt;/p&gt;
&lt;p&gt;In a breach of &lt;a href="https://en.wikipedia.org/wiki/Betteridge%27s_law_of_headlines"&gt;Betteridge's law of headlines&lt;/a&gt; the answer to the question posed by this headline is “yes”.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We believe async Django is ready for production. In theory, there should be no performance loss when using async Django instead of FastAPI for the same tasks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The ColiVara application is itself open source, and you can see how it makes use of Django’s relatively new &lt;a href="https://docs.djangoproject.com/en/5.1/topics/db/queries/#asynchronous-queries"&gt;asynchronous ORM features&lt;/a&gt; in the &lt;a href="https://github.com/tjmlabs/ColiVara/blob/main/web/api/views.py"&gt;api/views.py module&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also picked up a useful trick &lt;a href="https://github.com/tjmlabs/ColiVarE/blob/0761a9f9f7ba582f56e49a48d9fdefedcfaa87a5/Dockerfile#L14"&gt;from their Dockerfile&lt;/a&gt;: if you want &lt;code&gt;uv&lt;/code&gt; in a container you can install it with this one-liner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
&lt;/code&gt;&lt;/pre&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=42225088"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/asynchronous"&gt;asynchronous&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/embeddings"&gt;embeddings&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rag"&gt;rag&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;



</summary><category term="asynchronous"/><category term="django"/><category term="python"/><category term="embeddings"/><category term="rag"/><category term="uv"/></entry><entry><title>django-plugin-django-debug-toolbar</title><link href="https://simonwillison.net/2024/Nov/13/django-plugin-django-debug-toolbar/#atom-tag" rel="alternate"/><published>2024-11-13T01:14:22+00:00</published><updated>2024-11-13T01:14:22+00:00</updated><id>https://simonwillison.net/2024/Nov/13/django-plugin-django-debug-toolbar/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tomviner/django-plugin-django-debug-toolbar"&gt;django-plugin-django-debug-toolbar&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Tom Viner built a plugin for my &lt;a href="https://djp.readthedocs.io/"&gt;DJP Django plugin system&lt;/a&gt; that configures the excellent &lt;a href="https://django-debug-toolbar.readthedocs.io/"&gt;django-debug-toolbar&lt;/a&gt; debugging tool.&lt;/p&gt;
&lt;p&gt;You can see everything it sets up for you &lt;a href="https://github.com/tomviner/django-plugin-django-debug-toolbar/blob/0.3.2/django_plugin_django_debug_toolbar/__init__.py"&gt;in this Python code&lt;/a&gt;: it configures installed apps, URL patterns and middleware and sets the &lt;code&gt;INTERNAL_IPS&lt;/code&gt; and &lt;code&gt;DEBUG&lt;/code&gt; settings.&lt;/p&gt;
&lt;p&gt;Here are Tom's &lt;a href="https://github.com/tomviner/django-plugin-django-debug-toolbar/issues/1"&gt;running notes&lt;/a&gt; as he created the plugin.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/tomviner/status/1856498919359828152"&gt;@tomviner&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djp"&gt;djp&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="plugins"/><category term="djp"/></entry><entry><title>Quoting Jacob Kaplan-Moss</title><link href="https://simonwillison.net/2024/Oct/20/jacob-kaplan-moss/#atom-tag" rel="alternate"/><published>2024-10-20T16:34:28+00:00</published><updated>2024-10-20T16:34:28+00:00</updated><id>https://simonwillison.net/2024/Oct/20/jacob-kaplan-moss/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://jacobian.org/2024/oct/18/dsf-board-2025/"&gt;&lt;p&gt;It feels like we’re at a bit of an inflection point for the Django community. [...] One of the places someone could have the most impact is by serving on the DSF Board. Like the community at large, the DSF is at a transition point: we’re outgrowing the “small nonprofit” status, and have the opportunity to really expand our ambition and reach. In all likelihood, the decisions the Board makes over the next year or two will define our direction and strategy for the next decade.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://jacobian.org/2024/oct/18/dsf-board-2025/"&gt;Jacob Kaplan-Moss&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jacob-kaplan-moss"&gt;jacob-kaplan-moss&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dsf"&gt;dsf&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="jacob-kaplan-moss"/><category term="dsf"/></entry><entry><title>2025 DSF Board Nominations</title><link href="https://simonwillison.net/2024/Oct/16/2025-dsf-board-nominations/#atom-tag" rel="alternate"/><published>2024-10-16T23:01:22+00:00</published><updated>2024-10-16T23:01:22+00:00</updated><id>https://simonwillison.net/2024/Oct/16/2025-dsf-board-nominations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.djangoproject.com/weblog/2024/sep/25/2025-dsf-board-nominations/"&gt;2025 DSF Board Nominations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The Django Software Foundation board elections are coming up. There are four positions open, seven directors total. Terms last two years, and the deadline for submitting a nomination is October 25th (the date of the election has not yet been decided).&lt;/p&gt;
&lt;p&gt;Several community members have shared "DSF initiatives I'd like to see" documents to inspire people who may be considering running for the board:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/sarahboyce/68ffaaeae24d2501cf27a914f77fb97c"&gt;Sarah Boyce&lt;/a&gt; (current Django Fellow) wants a marketing strategy, better community docs, more automation and a refresh of the Django survey.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.better-simple.com/django/2024/10/13/dsf-initiatives-i-would-like-to-see/"&gt;Tim Schilling&lt;/a&gt; wants one big sponsor, more community recognition and a focus on working groups.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://noumenal.es/posts/dsf-board-election/N8W/"&gt;Carlton Gibson&lt;/a&gt; wants an Executive Director, an updated website and better integration of the community into that website.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jacobian.org/2024/oct/18/dsf-board-2025/"&gt;Jacob Kaplan-Moss&lt;/a&gt; wants effectively all of the above.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There's also a useful FAQ &lt;a href="https://forum.djangoproject.com/t/2025-dsf-board-elections/35253/7"&gt;on the Django forum&lt;/a&gt; by Thibaud Colas.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jacob-kaplan-moss"&gt;jacob-kaplan-moss&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dsf"&gt;dsf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/carlton-gibson"&gt;carlton-gibson&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="jacob-kaplan-moss"/><category term="dsf"/><category term="carlton-gibson"/></entry><entry><title>jefftriplett/django-startproject</title><link href="https://simonwillison.net/2024/Oct/12/django-startproject/#atom-tag" rel="alternate"/><published>2024-10-12T23:19:01+00:00</published><updated>2024-10-12T23:19:01+00:00</updated><id>https://simonwillison.net/2024/Oct/12/django-startproject/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jefftriplett/django-startproject"&gt;jefftriplett/django-startproject&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Django's &lt;code&gt;django-admin startproject&lt;/code&gt; and &lt;code&gt;startapp&lt;/code&gt; commands include &lt;a href="https://docs.djangoproject.com/en/5.1/ref/django-admin/#cmdoption-startapp-template"&gt;a --template option&lt;/a&gt; which can be used to specify an alternative template for generating the initial code.&lt;/p&gt;
&lt;p&gt;Jeff Triplett actively maintains his own template for new projects, which includes the pattern that I personally prefer of keeping settings and URLs in a &lt;a href="https://github.com/jefftriplett/django-startproject/tree/main/config"&gt;config/ folder&lt;/a&gt;. It also configures the development environment to run using Docker Compose.&lt;/p&gt;
&lt;p&gt;The latest update adds support for Python 3.13, Django 5.1 and uv. It's neat how you can get started without even installing Django using &lt;code&gt;uv run&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run --with=django django-admin startproject \
  --extension=ini,py,toml,yaml,yml \
  --template=https://github.com/jefftriplett/django-startproject/archive/main.zip \
  example_project
&lt;/code&gt;&lt;/pre&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://mastodon.social/@webology/113296450222943336"&gt;@webology&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jeff-triplett"&gt;jeff-triplett&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="python"/><category term="docker"/><category term="jeff-triplett"/><category term="uv"/></entry><entry><title>If we had $1,000,000…</title><link href="https://simonwillison.net/2024/Oct/8/if-we-had-a-million-dollars/#atom-tag" rel="alternate"/><published>2024-10-08T19:59:39+00:00</published><updated>2024-10-08T19:59:39+00:00</updated><id>https://simonwillison.net/2024/Oct/8/if-we-had-a-million-dollars/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://jacobian.org/2024/oct/8/dsf-one-million/"&gt;If we had $1,000,000…&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Jacob Kaplan-Moss gave my favorite talk at DjangoCon this year, imagining what the Django Software Foundation could do if it quadrupled its annual income to $1 million and laying out a realistic path for getting there. Jacob suggests leaning more into large donors than increasing our small donor base:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s far easier for me to picture convincing eight or ten or fifteen large companies to make large donations than it is to picture increasing our small donor base tenfold. So I think a major donor strategy is probably the most realistic one for us.&lt;/p&gt;
&lt;p&gt;So when I talk about major donors, who am I talking about? I’m talking about four major categories: large corporations, high net worth individuals (very wealthy people), grants from governments (e.g. the Sovereign Tech Fund run out of Germany), and private foundations (e.g. the Chan Zuckerberg Initiative, who’s given grants to the PSF in the past).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Also included: a TIL on &lt;a href="https://jacobian.org/til/talk-to-writeup-workflow/"&gt;Turning a conference talk into an annotated presentation&lt;/a&gt;. Jacob used &lt;a href="https://til.simonwillison.net/tools/annotated-presentations"&gt;my annotated presentation tool&lt;/a&gt; to OCR text from images of keynote slides, extracted a Whisper transcript from the YouTube livestream audio and then cleaned that up a little with &lt;a href="https://llm.datasette.io"&gt;LLM&lt;/a&gt; and Claude 3.5 Sonnet (&lt;code&gt;"Split the content of this transcript up into paragraphs with logical breaks. Add newlines between each paragraph."&lt;/code&gt;) before editing and re-writing it all into the final post.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jacob-kaplan-moss"&gt;jacob-kaplan-moss&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/whisper"&gt;whisper&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-3-5-sonnet"&gt;claude-3-5-sonnet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dsf"&gt;dsf&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="jacob-kaplan-moss"/><category term="whisper"/><category term="llm"/><category term="claude-3-5-sonnet"/><category term="dsf"/></entry><entry><title>Django Commons</title><link href="https://simonwillison.net/2024/Oct/8/django-commons/#atom-tag" rel="alternate"/><published>2024-10-08T03:27:40+00:00</published><updated>2024-10-08T03:27:40+00:00</updated><id>https://simonwillison.net/2024/Oct/8/django-commons/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/django-commons"&gt;Django Commons&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Django Commons is a really promising initiative started by Tim Schilling, aimed at the problem of keeping key Django community projects responsibly maintained on a long-term basis.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Django Commons is an organization dedicated to supporting the community's efforts to maintain packages. It seeks to improve the maintenance experience for all contributors; reducing the barrier to entry for new contributors and reducing overhead for existing maintainers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’ve stated recently that I’d love to see the Django Software Foundation take on this role - adopting projects and ensuring they are maintained long-term. Django Commons looks like it solves that exact problem, assuring the future of key projects beyond their initial creators.&lt;/p&gt;
&lt;p&gt;So far the Commons has taken on responsibility for &lt;a href="https://github.com/django-commons/django-fsm-2"&gt;django-fsm-2&lt;/a&gt;, &lt;a href="https://github.com/django-commons/django-tasks-scheduler"&gt;django-tasks-scheduler&lt;/a&gt; and, as-of this week, &lt;a href="https://github.com/django-commons/django-typer"&gt;diango-typer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here’s Tim &lt;a href="https://www.better-simple.com/django/2024/05/22/looking-for-help-django-commons/"&gt;introducing the project&lt;/a&gt; back in May. Thoughtful governance has been baked in from the start:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Having multiple administrators makes the role more sustainable, lessens the impact of a person stepping away, and shortens response time for administrator requests. It’s important to me that the organization starts with multiple administrators so that collaboration and documentation are at the forefront of all decisions.&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="open-source"/></entry><entry><title>Thoughts on the Treasurer Role at Tech NonProfits</title><link href="https://simonwillison.net/2024/Oct/7/thoughts-on-the-treasurer-role-at-tech-nonprofits/#atom-tag" rel="alternate"/><published>2024-10-07T22:41:53+00:00</published><updated>2024-10-07T22:41:53+00:00</updated><id>https://simonwillison.net/2024/Oct/7/thoughts-on-the-treasurer-role-at-tech-nonprofits/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://wsvincent.com/thoughts-on-pyconnz/"&gt;Thoughts on the Treasurer Role at Tech NonProfits&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Will Vincent, Django Software Foundation treasurer from 2020-2022, explains what’s involved in the non-profit role with the highest level of responsibility and trust.


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



</summary><category term="django"/><category term="dsf"/></entry><entry><title>Building an automatically updating live blog in Django</title><link href="https://simonwillison.net/2024/Oct/2/live-blog-in-django/#atom-tag" rel="alternate"/><published>2024-10-02T15:42:39+00:00</published><updated>2024-10-02T15:42:39+00:00</updated><id>https://simonwillison.net/2024/Oct/2/live-blog-in-django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/django/live-blog"&gt;Building an automatically updating live blog in Django&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's an extended write-up of how I implemented the live blog feature I used for &lt;a href="https://simonwillison.net/2024/Oct/1/openai-devday-2024-live-blog/"&gt;my coverage of OpenAI DevDay&lt;/a&gt; yesterday. I built the first version using Claude while waiting for the keynote to start, then upgraded it during the lunch break with the help of GPT-4o to add sort options and incremental fetching of new updates.


    &lt;p&gt;Tags: &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/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="javascript"/><category term="ai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude"/></entry><entry><title>Weeknotes: Three podcasts, two trips and a new plugin system</title><link href="https://simonwillison.net/2024/Sep/30/weeknotes/#atom-tag" rel="alternate"/><published>2024-09-30T17:43:22+00:00</published><updated>2024-09-30T17:43:22+00:00</updated><id>https://simonwillison.net/2024/Sep/30/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;I fell behind a bit on my weeknotes. Here's most of what I've been doing in September.&lt;/p&gt;
&lt;h4 id="lisbon-portugal-and-durham-north-carolina"&gt;Lisbon, Portugal and Durham, North Carolina&lt;/h4&gt;
&lt;p&gt;I had two trips this month. The first was a short visit to Lisbon, Portugal for the Python Software Foundation's annual board retreat. This inspired me to write about &lt;a href="https://simonwillison.net/2024/Sep/18/board-of-the-python-software-foundation/"&gt;Things I've learned serving on the board of the Python Software Foundation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second was to Durham, North Carolina for DjangoCon US 2024. I wrote about that one in &lt;a href="https://simonwillison.net/2024/Sep/27/themes-from-djangocon-us-2024/"&gt;Themes from DjangoCon US 2024&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My talk at DjangoCon was about plugin systems, and in a classic example of conference-driven development I ended up writing and releasing a new plugin system for Django in preparation for that talk. I introduced that in &lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/"&gt;DJP: A plugin system for Django&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="podcasts"&gt;Podcasts&lt;/h4&gt;
&lt;p&gt;I haven't been a podcast guest &lt;a href="https://simonwillison.net/search/?year=2024&amp;amp;month=1&amp;amp;tag=podcasts"&gt;since January&lt;/a&gt;, and then three came along at once! All three appearences involved LLMs in some way but I don't think there was a huge amount of overlap in terms of what I actually said.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I went on &lt;a href="https://simonwillison.net/2024/Sep/10/software-misadventures/"&gt;The Software Misadventures Podcast&lt;/a&gt; to talk about my career to-date.&lt;/li&gt;
&lt;li&gt;My appearance &lt;a href="https://simonwillison.net/2024/Sep/20/using-llms-for-code/"&gt;on TWIML&lt;/a&gt; dug into ways in which I use Claude and ChatGPT to help me write code.&lt;/li&gt;
&lt;li&gt;I was the guest for the inaugral episode of Gergely Orosz's &lt;a href="https://newsletter.pragmaticengineer.com/p/ai-tools-for-software-engineers-simon-willison"&gt;Pragmatic Engineer Podcast&lt;/a&gt;, which ended up touching on a whole array of different topics relevant to modern software engineering, from the importance of open source to the impact AI tools are likely to have on our industry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gergely has been sharing neat edited snippets from our conversation on Twitter. Here's &lt;a href="https://twitter.com/GergelyOrosz/status/1839682428471779596"&gt;one on RAG&lt;/a&gt; and another about &lt;a href="https://twitter.com/GergelyOrosz/status/1840779737297260646"&gt;how open source has been the the biggest productivity boost&lt;/a&gt; of my career.&lt;/p&gt;
&lt;h4 id="on-the-blog"&gt;On the blog&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/29/notebooklm-audio-overview/"&gt;NotebookLM's automatically generated podcasts are surprisingly effective&lt;/a&gt; - Sept. 29, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/27/themes-from-djangocon-us-2024/"&gt;Themes from DjangoCon US 2024&lt;/a&gt; - Sept. 27, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/25/djp-a-plugin-system-for-django/"&gt;DJP: A plugin system for Django&lt;/a&gt; - Sept. 25, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/20/using-llms-for-code/"&gt;Notes on using LLMs for code&lt;/a&gt; - Sept. 20, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/18/board-of-the-python-software-foundation/"&gt;Things I've learned serving on the board of the Python Software Foundation&lt;/a&gt; - Sept. 18, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/12/openai-o1/"&gt;Notes on OpenAI's new o1 chain-of-thought models&lt;/a&gt; - Sept. 12, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/10/software-misadventures/"&gt;Notes from my appearance on the Software Misadventures Podcast&lt;/a&gt; - Sept. 10, 2024&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://simonwillison.net/2024/Sep/8/teresa-t-whale-pillar-point/"&gt;Teresa T is name of the whale in Pillar Point Harbor near Half Moon Bay&lt;/a&gt; - Sept. 8, 2024&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="museums"&gt;Museums&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.niche-museums.com/112"&gt;The Vincent and Ethel Simonetti Historic Tuba Collection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="releases"&gt;Releases&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/shot-scraper/releases/tag/1.5"&gt;shot-scraper 1.5&lt;/a&gt;&lt;/strong&gt; - 2024-09-27&lt;br /&gt;A command-line utility for taking automated screenshots of websites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-datasette/releases/tag/0.2"&gt;django-plugin-datasette 0.2&lt;/a&gt;&lt;/strong&gt; - 2024-09-26&lt;br /&gt;Django plugin to run Datasette inside of Django&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/djp/releases/tag/0.3.1"&gt;djp 0.3.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-26&lt;br /&gt;A plugin system for Django&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-gemini/releases/tag/0.1a5"&gt;llm-gemini 0.1a5&lt;/a&gt;&lt;/strong&gt; - 2024-09-24&lt;br /&gt;LLM plugin to access Google's Gemini family of models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-blog/releases/tag/0.1.1"&gt;django-plugin-blog 0.1.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-24&lt;br /&gt;A blog for Django as a DJP plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-database-url/releases/tag/0.1"&gt;django-plugin-database-url 0.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-24&lt;br /&gt;Django plugin for reading the DATABASE_URL environment variable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-plugin-django-header/releases/tag/0.1.1"&gt;django-plugin-django-header 0.1.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-23&lt;br /&gt;Add a Django-Compositions HTTP header to a Django app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-jina-api/releases/tag/0.1a0"&gt;llm-jina-api 0.1a0&lt;/a&gt;&lt;/strong&gt; - 2024-09-20&lt;br /&gt;Access Jina AI embeddings via their API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm/releases/tag/0.16"&gt;llm 0.16&lt;/a&gt;&lt;/strong&gt; - 2024-09-12&lt;br /&gt;Access large language models from the command-line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-acl/releases/tag/0.4a4"&gt;datasette-acl 0.4a4&lt;/a&gt;&lt;/strong&gt; - 2024-09-10&lt;br /&gt;Advanced permission management for Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-cmd/releases/tag/0.2a0"&gt;llm-cmd 0.2a0&lt;/a&gt;&lt;/strong&gt; - 2024-09-09&lt;br /&gt;Use LLM to generate and execute commands in your shell&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.3"&gt;files-to-prompt 0.3&lt;/a&gt;&lt;/strong&gt; - 2024-09-09&lt;br /&gt;Concatenate a directory full of files into a single prompt for use with LLMs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/json-flatten/releases/tag/0.3.1"&gt;json-flatten 0.3.1&lt;/a&gt;&lt;/strong&gt; - 2024-09-07&lt;br /&gt;Python functions for flattening a JSON object to a single dictionary of pairs, and unflattening that dictionary back to a JSON object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/csv-diff/releases/tag/1.2"&gt;csv-diff 1.2&lt;/a&gt;&lt;/strong&gt; - 2024-09-06&lt;br /&gt;Python CLI tool and library for diffing CSV and JSON files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette/releases/tag/1.0a16"&gt;datasette 1.0a16&lt;/a&gt;&lt;/strong&gt; - 2024-09-06&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-search-all/releases/tag/1.1.4"&gt;datasette-search-all 1.1.4&lt;/a&gt;&lt;/strong&gt; - 2024-09-06&lt;br /&gt;Datasette plugin for searching all searchable tables at once&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="tils"&gt;TILs&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/llms/streaming-llm-apis"&gt;How streaming LLM APIs work&lt;/a&gt; - 2024-09-21&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/podcasts"&gt;podcasts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/psf"&gt;psf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djp"&gt;djp&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="podcasts"/><category term="weeknotes"/><category term="psf"/><category term="llms"/><category term="djp"/></entry><entry><title>Ensuring a block is overridden in a Django template</title><link href="https://simonwillison.net/2024/Sep/29/ensuring-a-block-is-overridden/#atom-tag" rel="alternate"/><published>2024-09-29T19:25:43+00:00</published><updated>2024-09-29T19:25:43+00:00</updated><id>https://simonwillison.net/2024/Sep/29/ensuring-a-block-is-overridden/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://carrick.eu/blog/ensuring-a-block-is-overridden-in-a-django-template/"&gt;Ensuring a block is overridden in a Django template&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat Django trick by Tom Carrick: implement a Django template tag that raises a custom exception, then you can use this pattern in your templates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% block title %}{% ensure_overridden %}{% endblock %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To ensure you don't accidentally extend a base template but forget to fill out a critical block.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://fosstodon.org/@carlton/113222141146688288"&gt;Carlton Gibson&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="django"/><category term="python"/></entry><entry><title>DjangoTV</title><link href="https://simonwillison.net/2024/Sep/28/djangotv/#atom-tag" rel="alternate"/><published>2024-09-28T04:48:04+00:00</published><updated>2024-09-28T04:48:04+00:00</updated><id>https://simonwillison.net/2024/Sep/28/djangotv/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://djangotv.com/"&gt;DjangoTV&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Brand new site by Jeff Triplett gathering together videos from Django conferences around the world. Here's &lt;a href="https://micro.webology.dev/2024/09/27/announcing-djangotv.html"&gt;Jeff's blog post&lt;/a&gt; introducing the project.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://mastodon.social/@webology/113211787119021118"&gt;@webology&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jeff-triplett"&gt;jeff-triplett&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="jeff-triplett"/></entry></feed>