<?xml version='1.0' encoding='utf-8'?>
<feed xml:lang="en" xmlns="http://www.w3.org/2005/Atom"><title>Matthias Kestenholz</title><link href="https://406.ch/writing/atom.xml" rel="self" /><link href="https://406.ch/" rel="alternate" /><id>https://406.ch/</id><updated>2026-07-01T12:00:00Z</updated><author><name>Matthias Kestenholz</name></author><entry><title>Weeknotes (2026 week 27)</title><link href="https://406.ch/writing/weeknotes-2026-week-27/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2026-week-27/</id><published>2026-07-01T12:00:00Z</published><updated>2026-07-01T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2026-week-27"&gt;&lt;a class="toclink" href="#weeknotes-2026-week-27"&gt;Weeknotes (2026 week 27)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The &lt;a href="https://406.ch/writing/weeknotes-2026-week-17/"&gt;last entry in this series&lt;/a&gt; was published 10 weeks ago so it really is time for another review of the releases I did during this time.&lt;/p&gt;
&lt;h2 id="releases"&gt;&lt;a class="toclink" href="#releases"&gt;Releases&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="feincms3-forms"&gt;&lt;a class="toclink" href="#feincms3-forms"&gt;feincms3-forms&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://pypi.org/project/feincms3-forms/"&gt;feincms3-forms&lt;/a&gt; forms builder has gained a &lt;a href="https://feincms3-forms.readthedocs.io/"&gt;documentation page&lt;/a&gt; on the wonderful Read the Docs service. The 0.6.1 release doesn&amp;rsquo;t contain any code changes, just &lt;code&gt;pyproject.toml&lt;/code&gt; updates and the mentioned documentation rework.&lt;/p&gt;
&lt;h3 id="django-imagefield"&gt;&lt;a class="toclink" href="#django-imagefield"&gt;django-imagefield&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;django-imagefield 0.23 is still in alpha. The handling of image fields when using libvips is optimized to use less memory hopefully. We&amp;rsquo;ll see. I also added some tests to verify that &lt;code&gt;.mpo&lt;/code&gt; files are handled properly.&lt;/p&gt;
&lt;h3 id="feincms3"&gt;&lt;a class="toclink" href="#feincms3"&gt;feincms3&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The Vimeo embed now always sets the &lt;code&gt;dnt=1&lt;/code&gt; parameter on the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;, which asks Vimeo to not track the user.&lt;/p&gt;
&lt;h3 id="django-mptt"&gt;&lt;a class="toclink" href="#django-mptt"&gt;django-mptt&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I wrote about the &lt;a href="https://406.ch/writing/anything-new/"&gt;somewhat annoying maintenance&lt;/a&gt; again. The library is still officially unmaintained, but I did a lot of work either just closing issues or also fixing them. The docs also contain many clarifications. I only released 0.19rc1 for now.&lt;/p&gt;
&lt;h3 id="feincms3-sites-and-feincms3-language-sites"&gt;&lt;a class="toclink" href="#feincms3-sites-and-feincms3-language-sites"&gt;feincms3-sites and feincms3-language-sites&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://406.ch/writing/weeknotes-2026-week-17/#feincms3-sites-and-feincms3-language-sites"&gt;Last time&lt;/a&gt; I mentioned that default HTTP/S ports are now stripped so that the host matching can determine the correct site. Now a new case appeared where trailing dots weren&amp;rsquo;t stripped. The normalization of hosts has been extended. I&amp;rsquo;m sure we&amp;rsquo;re still missing some exotic cases where we should do more normalization, but we&amp;rsquo;ll cross that bridge when we get there.&lt;/p&gt;
&lt;h3 id="django-prose-editor-and-django-js-asset"&gt;&lt;a class="toclink" href="#django-prose-editor-and-django-js-asset"&gt;django-prose-editor and django-js-asset&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Various upgrades to the editor and especially the importmaps rework in both packages &amp;ndash; the importmap infrastructure should now be CSP-compatible! I wrote more about that in the last post &lt;a href="https://406.ch/writing/the-2026-way-of-using-importmaps-in-django/"&gt;The 2026 way of using importmaps in Django&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="django-content-editor"&gt;&lt;a class="toclink" href="#django-content-editor"&gt;django-content-editor&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Minor bugfixes and a major version bump because of the rework of the JavaScript code into multiple ES modules. The content editor now uses importmaps as well.&lt;/p&gt;
&lt;h3 id="django-fhadmin"&gt;&lt;a class="toclink" href="#django-fhadmin"&gt;django-fhadmin&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Small bugfix so that links aren&amp;rsquo;t underlined in the app groups list when they shouldn&amp;rsquo;t be, matching how the Django admin itself behaves.&lt;/p&gt;
&lt;h3 id="django-cabinet"&gt;&lt;a class="toclink" href="#django-cabinet"&gt;django-cabinet&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The cabinet / prose editor integration for the file (or image) picker is final and released as a stable version.&lt;/p&gt;
&lt;h3 id="django-json-schema-editor"&gt;&lt;a class="toclink" href="#django-json-schema-editor"&gt;django-json-schema-editor&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This small release only contains more correct German translations of strings.&lt;/p&gt;
&lt;h3 id="honorable-mention-django-debug-toolbar"&gt;&lt;a class="toclink" href="#honorable-mention-django-debug-toolbar"&gt;Honorable mention: django-debug-toolbar&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I didn&amp;rsquo;t actually create this release, but I contributed various changes to it. The changelog for 7.0 is &lt;a href="https://django-debug-toolbar.readthedocs.io/en/latest/changes.html"&gt;here&lt;/a&gt;.&lt;/p&gt;</summary></entry><entry><title>The 2026 way of using importmaps in Django</title><link href="https://406.ch/writing/the-2026-way-of-using-importmaps-in-django/" rel="alternate" /><id>https://406.ch/writing/the-2026-way-of-using-importmaps-in-django/</id><published>2026-06-17T12:00:00Z</published><updated>2026-06-17T12:00:00Z</updated><summary type="html">&lt;h1 id="the-2026-way-of-using-importmaps-in-django"&gt;&lt;a class="toclink" href="#the-2026-way-of-using-importmaps-in-django"&gt;The 2026 way of using importmaps in Django&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I last wrote about &lt;a href="/writing/django-javascript-modules-and-importmaps/"&gt;Django, JavaScript modules and importmaps&lt;/a&gt; in May 2025, slightly over a year ago.&lt;/p&gt;
&lt;p&gt;The main topic of this post is the &lt;a href="https://github.com/feincms/django-js-asset"&gt;django-js-asset&lt;/a&gt; 4.0 release. The library is used in many places, some of the more well-known packages using it are &lt;a href="https://github.com/django-mptt/django-mptt"&gt;django-mptt&lt;/a&gt; and &lt;a href="https://github.com/django-ckeditor/django-ckeditor"&gt;django-ckeditor&lt;/a&gt;.
I have since done a lot of work evolving the ways of integrating importmaps but the efforts to standardize upon an approach have stalled a bit.
The main reason for this, apart from time and energy, was that I wasn&amp;rsquo;t really all that happy with the global importmap. When I had only a few modules using the importmap facility, I didn&amp;rsquo;t care all that much. Now that the recently released &lt;a href="https://github.com/feincms/django-content-editor"&gt;django-content-editor 9.0&lt;/a&gt; also uses importmaps for shipping a refactored, much more modular JavaScript implementation while still keeping all the benefits of cache busting using &lt;code&gt;ManifestStaticFilesStorage&lt;/code&gt;&lt;sup id="fnref:manifest"&gt;&lt;a class="footnote-ref" href="#fn:manifest"&gt;1&lt;/a&gt;&lt;/sup&gt;, having a global importmap got annoying. The content editor JavaScript is only used within the &lt;a href="/writing/the-django-admin-is-a-cms/"&gt;Django administration interface&lt;/a&gt;, but when using a single global importmap object, the importmap entries were always there on each page that used an importmap at all.&lt;/p&gt;
&lt;p&gt;A better solution was needed. I&amp;rsquo;m a big fan of using &lt;a href="https://docs.djangoproject.com/en/6.1/topics/forms/media/"&gt;&lt;code&gt;forms.Media&lt;/code&gt;&lt;/a&gt; for collecting CSS and JavaScript from widgets, forms and utilities. It helps me avoid inline JavaScript since &lt;a href="https://406.ch/writing/django-admin-apps-and-content-security-policy-compliance/"&gt;at least 2017&lt;/a&gt;. I&amp;rsquo;m not using it for site-wide CSS and JavaScript, I&amp;rsquo;m still transpiling, PostCSS-ing and bundling the assets using &lt;a href="https://rspack.rs/"&gt;rspack&lt;/a&gt; as for example written about &lt;a href="https://406.ch/writing/avoiding-empty-javascript-files-in-css-only-entrypoints-from-rspack-builds/"&gt;here&lt;/a&gt; and &lt;a href="https://406.ch/writing/how-i-m-bundling-frontend-assets-using-django-and-rspack-these-days/"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="why-importmaps"&gt;&lt;a class="toclink" href="#why-importmaps"&gt;Why importmaps?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A quick refresher on why this matters at all. Django&amp;rsquo;s &lt;code&gt;ManifestStaticFilesStorage&lt;/code&gt; hashes the contents of each file into its name for cache busting, but out of the box it doesn&amp;rsquo;t rewrite the &lt;code&gt;import&lt;/code&gt; statements inside JavaScript modules. Importmaps bridge the gap: your code imports a stable name:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;initializeEditors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;django-prose-editor/editor&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and the importmap tells the browser where that name actually lives:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;importmap&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;imports&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;django-prose-editor/editor&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/static/django_prose_editor/editor.6e8dd4c12e2e.js&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So the import stays clean and constant while the file behind it can get a new hash on every deploy.&lt;/p&gt;
&lt;h2 id="django-js-asset-40"&gt;&lt;a class="toclink" href="#django-js-asset-40"&gt;django-js-asset 4.0&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The updated &lt;a href="https://github.com/feincms/django-js-asset"&gt;django-js-asset 4.0&lt;/a&gt; doesn&amp;rsquo;t ship the old, global importmap at all. This means the upgrade might require some work. Instead of one importmap shared across the whole site, you now get a specific importmap assembled for the context at hand &amp;ndash; either by Django itself when it collects the media of your forms, widgets and the admin, or explicitly by you in a view or context processor. The building block in both cases is the &lt;code&gt;ImportMap&lt;/code&gt; object; when it travels through &lt;code&gt;js_asset.Media&lt;/code&gt; (a subclass of &lt;code&gt;django.forms.Media&lt;/code&gt;) the maps are automatically merged into a single &lt;code&gt;&amp;lt;script type="importmap"&amp;gt;&lt;/code&gt;, by customizing and extending what Django does already when merging media instances.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/feincms/django-js-asset/blob/main/CHANGELOG.rst"&gt;release notes&lt;/a&gt; go into more detail.&lt;/p&gt;
&lt;h2 id="in-practice"&gt;&lt;a class="toclink" href="#in-practice"&gt;In practice&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re using a package such as &lt;a href="https://github.com/feincms/django-prose-editor"&gt;django-prose-editor&lt;/a&gt; in the Django admin you don&amp;rsquo;t have to do anything, things should just work.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re using such a package outside the admin, you have to remove &lt;code&gt;"js_asset.context_processors.importmap"&lt;/code&gt; from your list of context processors. On one particular website the prose editor is the only package with importmap entries outside the admin, so I have to add the &lt;code&gt;importmap&lt;/code&gt; to the template context myself:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;django_prose_editor.widgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;importmap&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;template.html&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;importmap&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;importmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The template then just renders it in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;... {{ importmap }}&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On a different site, I have a slightly more involved scenario where I previously used &lt;code&gt;importmap.update(...)&lt;/code&gt; to add my own entries to the importmap. There, I&amp;rsquo;m using a custom context processor to always add these entries to the importmap too:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;django_prose_editor.widgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;importmap&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dpe_importmap&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;js_asset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ImportMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;static_lazy&lt;/span&gt;

&lt;span class="n"&gt;_site_importmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ImportMap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;imports&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;my-module&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;static_lazy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my-module.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;_importmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dpe_importmap&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;_site_importmap&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;importmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;importmap&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_importmap&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This importmap is merged once at server startup and then served repeatedly to the client. Because we use the lazy version of the &lt;code&gt;static&lt;/code&gt; function we can do this during startup and not worry about files not yet collected by &lt;code&gt;collectstatic&lt;/code&gt; &amp;ndash; we&amp;rsquo;ll get the correct paths later.&lt;/p&gt;
&lt;p&gt;On the same site as the previous example, I also have an admin inline which requires some JavaScript and also an importmap:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;django.contrib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;django.forms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Script&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;js_asset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Media&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ImportMap&lt;/span&gt;

&lt;span class="c1"&gt;# Initializing this once. Not necessary but I like it better that way.&lt;/span&gt;
&lt;span class="n"&gt;_importmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ImportMap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;imports&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ModelInline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StackedInline&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;media&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Media&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="n"&gt;_importmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;module.js&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;module&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As of 4.0, &lt;code&gt;JS&lt;/code&gt; and &lt;code&gt;CSS&lt;/code&gt; produce Django&amp;rsquo;s own &lt;code&gt;Script&lt;/code&gt; and &lt;code&gt;Stylesheet&lt;/code&gt; objects, so you can import and use &lt;code&gt;Script&lt;/code&gt; directly from &lt;code&gt;django.forms&lt;/code&gt; as shown above (on Django 4.2–5.1, import it from &lt;code&gt;js_asset&lt;/code&gt; instead, which backports it). The familiar &lt;code&gt;JS("module.js", {"type": "module"})&lt;/code&gt; wrapper still works unchanged if you prefer it — it just takes a positional dict instead of keyword arguments.&lt;/p&gt;
&lt;p&gt;Here, it&amp;rsquo;s really important to use the &lt;code&gt;js_asset.Media&lt;/code&gt; and not &lt;code&gt;django.forms.Media&lt;/code&gt;. &lt;code&gt;js_asset.Media&lt;/code&gt; knows how to handle importmaps &amp;ndash; all importmaps are collected from all media lists, merged and added to the output before all other CSS and especially JavaScript. The reason for that is that browsers only honour a single importmap per page, and it really has to appear before all JavaScript modules referencing any entries in the importmap.&lt;/p&gt;
&lt;p&gt;The nice thing about &lt;code&gt;js_asset.Media&lt;/code&gt; is that it doesn&amp;rsquo;t have to appear first in the list of media classes which are merged &amp;ndash; it can also appear in the middle or last, and still can do its magic after all &lt;code&gt;Media&lt;/code&gt; objects have been merged into a single one.&lt;/p&gt;
&lt;p&gt;The rest is handled by Django itself, since it already supports collecting media assets. The missing piece was just the importmap object and the &lt;code&gt;js_asset.Media&lt;/code&gt; class which knows how to special case them, and which &amp;ndash; through the power of overriding &lt;code&gt;__add__&lt;/code&gt; and &lt;code&gt;__radd__&lt;/code&gt; takes over all the other media instances.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;&lt;a class="toclink" href="#whats-next"&gt;What&amp;rsquo;s next&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I haven&amp;rsquo;t yet used CSP nonces using &lt;a href="https://docs.djangoproject.com/en/6.1/ref/templates/builtins/#std-templatetag-csp_nonce_attr"&gt;&lt;code&gt;{% csp_nonce_attr media %}&lt;/code&gt;&lt;/a&gt; in production myself, but it should just work, even with importmaps and everything else. Given that I have a passing test suite I have no reason to believe it doesn&amp;rsquo;t already work, but I&amp;rsquo;d like to have a confirmation.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m hoping to standardize some more. If we could get something like this in Django core that would be really nice. Maybe I&amp;rsquo;ll be able to work on that at &lt;a href="https://djangomed.eu/"&gt;Django on the Med 🏖️&lt;/a&gt;. Since no browser supports multiple importmaps as of today having multiple implementations of importmaps in the Django ecosystem will lead to trouble down the road. I think there is a clear case to be made for importmap support in Django and I would obviously love it if the approach implemented today in django-js-asset would be the basis for the official solution.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:manifest"&gt;
&lt;p&gt;Without having to do any overrides to enable ESM support.&amp;#160;&lt;a class="footnote-backref" href="#fnref:manifest" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</summary></entry><entry><title>«Anything new?»</title><link href="https://406.ch/writing/anything-new/" rel="alternate" /><id>https://406.ch/writing/anything-new/</id><published>2026-06-03T12:00:00Z</published><updated>2026-06-03T12:00:00Z</updated><summary type="html">&lt;h1 id="anything-new"&gt;&lt;a class="toclink" href="#anything-new"&gt;«Anything new?»&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;A lot of time has passed since I officially announced that I want to &lt;a href="https://406.ch/writing/weeknotes-2021-week-10/#django-mptt-is-not-maintained-anymore"&gt;step down from maintaining django-mptt&lt;/a&gt;. I started contributing around 2009, tagged the &lt;a href="https://github.com/django-mptt/django-mptt/tree/0.3.0"&gt;0.3 release in April 2010&lt;/a&gt;, and have been the sole active maintainer since somewhere around 2019. The post about &lt;a href="https://406.ch/writing/django-tree-queries/"&gt;django-tree-queries&lt;/a&gt; has more background, but that&amp;rsquo;s not today&amp;rsquo;s topic.&lt;/p&gt;
&lt;h2 id="stepping-away-isnt-easy"&gt;&lt;a class="toclink" href="#stepping-away-isnt-easy"&gt;Stepping away isn&amp;rsquo;t easy&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For me, abandoning a project is a bit like stepping out of a relationship: negative emotions end up being a somewhat necessary driver, because the absence of positive events alone rarely provides enough force on its own. I get a lot of satisfaction from a job well done, and walking away means letting that go.&lt;/p&gt;
&lt;p&gt;Even with time set aside for open source in my work day, I still have to choose where that time goes. django-mptt stopped being where it needed to go.&lt;/p&gt;
&lt;h2 id="the-sense-of-entitlement"&gt;&lt;a class="toclink" href="#the-sense-of-entitlement"&gt;The sense of entitlement&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When a project is obviously unmaintained, asking for free labor is walking a tightrope. It takes real care not to rekindle exactly the frustrations that led maintainers away in the first place.&lt;/p&gt;
&lt;p&gt;It takes energy not to clap back when someone is being rude or insensitive in the issue tracker. Asking &amp;ldquo;Anything new?&amp;rdquo; on a ticket where the next steps were outlined clearly and obviously nothing happened in the meantime is just one variant of this.&lt;/p&gt;
&lt;p&gt;Quietly quitting isn&amp;rsquo;t what I want to do — and as a user of django-mptt myself, I can&amp;rsquo;t really do that either. Taking the high road is the professional choice. But it costs something.&lt;/p&gt;
&lt;p&gt;I keep coming back to &lt;a href="https://youtu.be/Amj3QG2s1BI?t=226"&gt;Mona Eltahawy on refusing to be civil&lt;/a&gt;. She&amp;rsquo;s speaking about something quite different, and I&amp;rsquo;m aware I write this as a white man. The situations aren&amp;rsquo;t the same at all. But she articulated something I haven&amp;rsquo;t managed to put into words as well myself and I like the idea of speaking up and taking the fight to those who awaken these feelings instead of taking the high road.&lt;/p&gt;
&lt;h2 id="doing-it-with-ai"&gt;&lt;a class="toclink" href="#doing-it-with-ai"&gt;Doing it with AI&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No post these days is complete without the obligatory AI mention, but there&amp;rsquo;s some relevancy to it.&lt;/p&gt;
&lt;p&gt;I fixed and closed almost all open django-mptt issues in a two-hour Claude session. I&amp;rsquo;ve previously written about using &lt;a href="https://406.ch/writing/llms-for-open-source-maintenance-a-cautious-case/"&gt;LLMs for open source maintenance&lt;/a&gt;, and the productivity gain is real whatever the detractors say. And the quality isn&amp;rsquo;t suddenly getting worse. Code wasn&amp;rsquo;t perfect before either. The test suite allows a certain degree of trust in the result and according to &lt;a href="https://406.ch/writing/my-rules-for-releasing-open-source-software/"&gt;my rules for releasing Open Source software&lt;/a&gt; we don&amp;rsquo;t have to require more than that.&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t change the underlying dynamic, though. &lt;a href="https://medium.com/@tridge60/rsync-and-outrage-d9849599e5a0"&gt;rsync and outrage&lt;/a&gt; illustrates the trap neatly: Tridgell got flooded with AI-generated security reports, used AI to handle them, and then got criticized for using AI. The tools that created the workload aren&amp;rsquo;t allowed to address it. The expectation is that the work has to involve sweat and tears and uncountable unpaid hours.&lt;/p&gt;
&lt;p&gt;The common goal should be more and better open source software. What we get as Open Source maintainers is shit from both sides: One side took our free work and trained models on it without asking, the other side complains about the supposedly unethical use of AI while acting in unethical ways themselves.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s something Kantian about how open source contribution gets framed. &lt;a href="https://en.wikipedia.org/wiki/Immanuel_Kant"&gt;Kant&lt;/a&gt;&amp;rsquo;s argument was that the only truly moral acts are those driven by duty and good will — not by desire, inclination, or any expectation of compensation. By that logic, I&amp;rsquo;m only acting morally if I keep going despite the burnout and the entitlement. If I stop, I&amp;rsquo;m not.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s bleak. The problems with AI are real. The people controlling the large models are assholes. But I have to work in the world as it is while also trying to change it for the better.&lt;/p&gt;</summary></entry><entry><title>Avoiding empty JavaScript files in CSS-only entrypoints from rspack builds</title><link href="https://406.ch/writing/avoiding-empty-javascript-files-in-css-only-entrypoints-from-rspack-builds/" rel="alternate" /><id>https://406.ch/writing/avoiding-empty-javascript-files-in-css-only-entrypoints-from-rspack-builds/</id><published>2026-05-25T12:00:00Z</published><updated>2026-05-25T12:00:00Z</updated><summary type="html">&lt;h1 id="avoiding-empty-javascript-files-in-css-only-entrypoints-from-rspack-builds"&gt;&lt;a class="toclink" href="#avoiding-empty-javascript-files-in-css-only-entrypoints-from-rspack-builds"&gt;Avoiding empty JavaScript files in CSS-only entrypoints from rspack builds&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I &lt;a href="https://406.ch/writing/how-i-m-bundling-frontend-assets-using-django-and-rspack-these-days/"&gt;previously wrote about my preferred way of integrating Django with rspack&lt;/a&gt;. Minor aspects have changed, but most of it still applies.&lt;/p&gt;
&lt;h2 id="what-changed"&gt;&lt;a class="toclink" href="#what-changed"&gt;What changed&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The largest change is probably that I have switched from &lt;code&gt;HtmlWebpackPlugin&lt;/code&gt; to rspack&amp;rsquo;s own &lt;a href="https://rspack.rs/plugins/rspack/html-rspack-plugin"&gt;&lt;code&gt;HtmlRspackPlugin&lt;/code&gt;&lt;/a&gt; and have stopped configuring the template generated by it. Instead, I&amp;rsquo;m pulling out link, script, and style tags &lt;a href="https://blog.codinghorror.com/parsing-html-the-cthulhu-way/"&gt;the Cthulhu way&lt;/a&gt;&lt;sup id="fnref:parsing"&gt;&lt;a class="footnote-ref" href="#fn:parsing"&gt;1&lt;/a&gt;&lt;/sup&gt;. My own mistakes while configuring rspack have led to missing or duplicated assets. Letting rspack do its thing and post-processing the result feels safer.&lt;/p&gt;
&lt;p&gt;The rest basically stayed the same.&lt;/p&gt;
&lt;h2 id="removeemptyjsassetsplugin"&gt;&lt;a class="toclink" href="#removeemptyjsassetsplugin"&gt;&lt;code&gt;RemoveEmptyJsAssetsPlugin&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Poking around in rspack&amp;rsquo;s compiler hooks isn&amp;rsquo;t documented all that well, so I&amp;rsquo;m documenting this plugin here. It&amp;rsquo;s very specific, but being able to change rspack&amp;rsquo;s output may be useful to others and to my future self.&lt;/p&gt;
&lt;p&gt;When using a CSS-only entrypoint or when listing CSS files in &lt;code&gt;entry&lt;/code&gt;, rspack sometimes decides it wants to generate an empty JavaScript file. This wouldn&amp;rsquo;t be a problem in and of itself, but since we&amp;rsquo;re also letting rspack generate the associated script tag, we will make the browser download this empty script file unnecessarily. I didn&amp;rsquo;t find a good way to avoid these empty files, and the &lt;code&gt;RemoveEmptyScriptsPlugin&lt;/code&gt; for Webpack doesn&amp;rsquo;t work with rspack anymore. The &lt;a href="https://github.com/web-infra-dev/rspack/discussions/6189"&gt;discussion&lt;/a&gt; hasn&amp;rsquo;t gone anywhere yet. So, I had to use my own plugin. Since I&amp;rsquo;m already copying the &lt;code&gt;rspack.library.js&lt;/code&gt; file into each project, adding a plugin there is no headache.&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RemoveEmptyJsAssetsPlugin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;emptyJsFiles&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js$&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;emptyJsFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="ow"&gt;delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.html&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jsFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;emptyJsFiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;script&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;/script&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;/script&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;compilation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rspack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RawSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The plugin has to be added to the plugins list:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RemoveEmptyJsAssetsPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And presto, no more empty JavaScript files!&lt;/p&gt;
&lt;h2 id="closing"&gt;&lt;a class="toclink" href="#closing"&gt;Closing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ideally, rspack will handle this itself at some point and make the plugin obsolete. For now, it works without issues in several projects.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:parsing"&gt;
&lt;p&gt;It&amp;rsquo;s mostly fine in this case since the output is very limited and well-structured &amp;ndash; we don&amp;rsquo;t have to guard against invalid HTML.&amp;#160;&lt;a class="footnote-backref" href="#fnref:parsing" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</summary></entry><entry><title>Weeknotes (2026 week 17)</title><link href="https://406.ch/writing/weeknotes-2026-week-17/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2026-week-17/</id><published>2026-05-20T12:00:00Z</published><updated>2026-05-20T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2026-week-17"&gt;&lt;a class="toclink" href="#weeknotes-2026-week-17"&gt;Weeknotes (2026 week 17)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I published the &lt;a href="https://406.ch/writing/weeknotes-2026-week-11/"&gt;last entry&lt;/a&gt; near the beginning of March. I&amp;rsquo;m really starting to see a theme in my Weeknotes publishing schedule.&lt;/p&gt;
&lt;h2 id="releases-since-the-first-weeks-of-march"&gt;&lt;a class="toclink" href="#releases-since-the-first-weeks-of-march"&gt;Releases since the first weeks of March&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m trying out a longer-form version of those notes here than in the past. I think it&amp;rsquo;s worth going into some detail and not just listing releases with half a sentence each.&lt;/p&gt;
&lt;h3 id="feincms3-sites-and-feincms3-language-sites"&gt;&lt;a class="toclink" href="#feincms3-sites-and-feincms3-language-sites"&gt;feincms3-sites and feincms3-language-sites&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I released updates to &lt;a href="https://pypi.org/project/feincms3-sites/"&gt;feincms3-sites&lt;/a&gt; and &lt;a href="https://pypi.org/project/feincms3-language-sites/"&gt;feincms3-language-sites&lt;/a&gt; fixing the same issue in both projects: When an HTTP client didn&amp;rsquo;t strip the default ports :80 (for HTTP) or :443 (for HTTPS) from a request, finding the correct site would fail. Browsers generally strip the port already, but some other HTTP clients do not.&lt;/p&gt;
&lt;h3 id="django-tree-queries"&gt;&lt;a class="toclink" href="#django-tree-queries"&gt;django-tree-queries&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;As I wrote &lt;a href="/writing/llms-for-open-source-maintenance-a-cautious-case/"&gt;elsewhere&lt;/a&gt; I closed many issues in the repositories, mostly documentation issues but also some bugs. &lt;code&gt;{% recursetree %}&lt;/code&gt; should now work properly and not cache old data anymore, using the primary key in &lt;code&gt;.tree_fields()&lt;/code&gt; now raises an intelligible error, and I also fixed a bug with table quoting when using &lt;a href="https://pypi.org/project/django-tree-queries/"&gt;django-tree-queries&lt;/a&gt; with the not yet released Django 6.1+.&lt;/p&gt;
&lt;h3 id="feincms3-cookiecontrol"&gt;&lt;a class="toclink" href="#feincms3-cookiecontrol"&gt;feincms3-cookiecontrol&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://pypi.org/project/feincms3-cookiecontrol/"&gt;feincms3-cookiecontrol&lt;/a&gt; not only offers a cookie consent banner (which actually supports &lt;a href="https://406.ch/writing/reusable-cookie-consent-app-for-django/"&gt;only embedding tracking scripts when users give consent&lt;/a&gt;) but also a third-party content embedding functionality which allows allowlisting individual services.&lt;/p&gt;
&lt;p&gt;The privacy policies of these services are now linked inline instead of with an ugly extra link. This reduces content inside the embed which helps on small screens.&lt;/p&gt;
&lt;p&gt;Version 1.7 used a buggy trusted publishing workflow so I immediately published 1.7.1.&lt;/p&gt;
&lt;h3 id="django-cabinet-and-django-prose-editor"&gt;&lt;a class="toclink" href="#django-cabinet-and-django-prose-editor"&gt;django-cabinet and django-prose-editor&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://pypi.org/project/django-cabinet/"&gt;django-cabinet&lt;/a&gt; can now be used as a media library directly inside &lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor&lt;/a&gt;. I&amp;rsquo;m (ab)using the CKEditor 4 protocol for embedding, which uses &lt;code&gt;window.opener.CKEDITOR.callFunction&lt;/code&gt; to send data back from the file manager popup into the editor. It feels icky but works nicely. This is only available if you&amp;rsquo;re installing the alpha prereleases, but I&amp;rsquo;m already testing the functionality in production somewhere, so I feel quite good about it.&lt;/p&gt;
&lt;p&gt;django-prose-editor now also ships brand new &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/classloom.html"&gt;&lt;code&gt;ClassLoom&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/styleloom.html"&gt;&lt;code&gt;StyleLoom&lt;/code&gt;&lt;/a&gt; extensions. Both extensions allow adding either classes or inline styles to text spans or nodes. In an ideal world we might not use something like this, but to make the editor more useful in the real world, editors need more flexibility. These two extensions provide that. I already mentioned &lt;code&gt;ClassLoom&lt;/code&gt; &lt;a href="/writing/rich-text-editors-how-restrictive-can-we-be/#combining-css-classes"&gt;in December&lt;/a&gt;, now it&amp;rsquo;s actually available. I&amp;rsquo;m not completely sold on how they work yet, but both of them are already solving real issues.&lt;/p&gt;
&lt;h3 id="honorable-mentions"&gt;&lt;a class="toclink" href="#honorable-mentions"&gt;Honorable mentions&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://pypi.org/project/django-debug-toolbar/"&gt;django-debug-toolbar 6.3&lt;/a&gt; has been released, I only contributed reviews during this cycle.&lt;/p&gt;</summary></entry><entry><title>Switching all of my Python packages to PyPI trusted publishing</title><link href="https://406.ch/writing/switching-all-of-my-python-packages-to-pypi-trusted-publishing/" rel="alternate" /><id>https://406.ch/writing/switching-all-of-my-python-packages-to-pypi-trusted-publishing/</id><published>2026-04-08T12:00:00Z</published><updated>2026-04-08T12:00:00Z</updated><summary type="html">&lt;h1 id="switching-all-of-my-python-packages-to-pypi-trusted-publishing"&gt;&lt;a class="toclink" href="#switching-all-of-my-python-packages-to-pypi-trusted-publishing"&gt;Switching all of my Python packages to PyPI trusted publishing&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;As I have teased on &lt;a href="https://hachyderm.io/@matthiask/116300209761371116"&gt;Mastodon&lt;/a&gt;, I&amp;rsquo;m switching all of my packages to PyPI trusted publishing. I have been using it to release the &lt;a href="github.com/django-commons/django-debug-toolbar"&gt;django-debug-toolbar&lt;/a&gt; a few times but never set it up myself. The process seemed tedious.&lt;/p&gt;
&lt;p&gt;The malicious releases uploaded to PyPI two weeks ago and the blog post about &lt;a href="https://snarky.ca/why-pylock-toml-includes-digital-attestations/"&gt;digital attestations in &lt;code&gt;pylock.toml&lt;/code&gt;&lt;/a&gt; finally pushed me to make the switch. All of my PyPI tokens have been revoked so there is no quick shortcut.&lt;/p&gt;
&lt;div class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also looking at other code hosting platforms. I have been using git before GitHub existed and I&amp;rsquo;ll probably still use git when GitHub has completed its enshittification. For now the cost/benefit ratio of staying on GitHub is still positive for me. Trusted publishing isn&amp;rsquo;t available everywhere, so for now it is GitHub anyway.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;In the end, switching an existing project was easier than expected. I have completed the process for &lt;a href="https://github.com/feincms/django-prose-editor"&gt;django-prose-editor&lt;/a&gt; and &lt;a href="https://github.com/feincms/feincms3-cookiecontrol/"&gt;feincms3-cookiecontrol&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For my future benefit, here are the step by step instructions I have to follow:&lt;/p&gt;
&lt;ol start="0"&gt;
&lt;li&gt;
&lt;p&gt;Have a package which is buildable using e.g. &lt;code&gt;uvx build&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On PyPI add a trusted publisher in the project&amp;rsquo;s publishing settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Owner: &lt;code&gt;matthiask&lt;/code&gt;, &lt;code&gt;feincms&lt;/code&gt;, &lt;code&gt;feinheit&lt;/code&gt;, whatever the user or organization&amp;rsquo;s name is.&lt;/li&gt;
&lt;li&gt;Repository: &lt;code&gt;django-prose-editor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Workflow name: &lt;code&gt;publish.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Environment: &lt;code&gt;release&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the GitHub repository, create a &lt;code&gt;release&lt;/code&gt; environment in Settings / Environments. If I want to manually approve releases I&amp;rsquo;ll add myself and potentially also other releasers as required reviewers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;git tag x.y.z&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt;, no more &lt;code&gt;uvx twine&lt;/code&gt; or &lt;code&gt;hatch publish&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Approve the release in the actions tab on the repository.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Either enjoy or swear and repeat the steps.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&amp;rsquo;m happy with testing the release process in production. The older I get the less I care if people think I&amp;rsquo;m stupid. That&amp;rsquo;s also why feincms3-cookiecontrol 1.7.0 doesn&amp;rsquo;t exist, only 1.7.1 &amp;ndash; the process failed and I had to bump the patch version and try again. Copy the &lt;code&gt;publish.yml&lt;/code&gt; from a known good place, for example from the &lt;a href="https://github.com/feincms/django-prose-editor/blob/main/.github/workflows/publish.yml"&gt;django-prose-editor repository&lt;/a&gt;. I have added the &lt;code&gt;if: github.repository == 'feincms/django-prose-editor'&lt;/code&gt; statement which ensures that the workflow only runs in the main repository, but that&amp;rsquo;s optional if you don&amp;rsquo;t care about failing workflows.&lt;/p&gt;</summary></entry><entry><title>LLMs for Open Source maintenance: a cautious case</title><link href="https://406.ch/writing/llms-for-open-source-maintenance-a-cautious-case/" rel="alternate" /><id>https://406.ch/writing/llms-for-open-source-maintenance-a-cautious-case/</id><published>2026-03-25T12:00:00Z</published><updated>2026-03-25T12:00:00Z</updated><summary type="html">&lt;h1 id="llms-for-open-source-maintenance-a-cautious-case"&gt;&lt;a class="toclink" href="#llms-for-open-source-maintenance-a-cautious-case"&gt;LLMs for Open Source maintenance: a cautious case&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;When ChatGPT appeared on the scene I was very annoyed at all the hype surrounding it. Since I&amp;rsquo;m working in the fast moving and low margin business of communication and campaigning agencies I&amp;rsquo;m surrounded by people eager to jump on the hype train when a tool promises to lessen the workload and take stuff from everyone&amp;rsquo;s plate.&lt;/p&gt;
&lt;p&gt;These discussions coupled with the fact that the training of these tools required unfathomable amounts of &lt;strong&gt;stealing&lt;/strong&gt; were the reason for a big reluctance on my part when trying them out. I&amp;rsquo;m using the word stealing here on purpose, since that&amp;rsquo;s exactly the crime &lt;a href="https://en.wikipedia.org/wiki/Aaron_Swartz"&gt;Aaron Swartz&lt;/a&gt; &lt;a href="https://web.archive.org/web/20120526080523/http://www.justice.gov/usao/ma/news/2011/July/SwartzAaronPR.html"&gt;was accused of by the attorney&amp;rsquo;s office of the district of Massachusetts&lt;/a&gt;. It&amp;rsquo;s frustrating that some people can get away with the same crime when it is so much bigger. For example, OpenAI and Anthropic downloaded much more data than Aaron ever did.&lt;/p&gt;
&lt;p&gt;A somewhat related thing happened with the too-big-to-fail banks: There, the people at the top were even compensated with &lt;a href="https://en.wikipedia.org/wiki/Golden_parachute"&gt;golden parachutes&lt;/a&gt; at the end. LLM companies seem to be above accountability too.&lt;/p&gt;
&lt;p&gt;Despite all this, I have slowly started integrating these tools into my workflows. I don&amp;rsquo;t remember the exact point in time, but since some time in 2025 my opinions on their utility has started to change. At the beginning, I always removed the attribution and took great care to write and rewrite the code myself, only using the LLMs for inspiration and maybe to generate integration tests. More and more I have to admit that they are useful, especially in time constrained projects with a clear focus and purpose.&lt;/p&gt;
&lt;p&gt;Last month I fixed and/or closed all open issues in the &lt;a href="https://github.com/feincms/django-tree-queries"&gt;django-tree-queries&lt;/a&gt; repository with the help of Claude Code. Is that a good thing? It could be argued I should have done the work myself. But I wouldn&amp;rsquo;t have — I have other things I want to do with my time. I don&amp;rsquo;t want to (always) work on Open Source software in the evening. I definitely also have leaned heavily on LLMs when working on &lt;a href="https://github.com/feincms/django-prose-editor"&gt;django-prose-editor&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="is-faster-better"&gt;&lt;a class="toclink" href="#is-faster-better"&gt;Is faster better?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We can produce more code, more features and close tickets faster than before. In my experience the speed up isn&amp;rsquo;t as big as some people may want us to believe, but it&amp;rsquo;s there. And contrary to what people in my LinkedIn feed say, that&amp;rsquo;s not an obviously good thing. Is it a race to the bottom where we drown in LLM-generated slop in quantities impossible to maintain? It doesn&amp;rsquo;t feel like that — but it&amp;rsquo;s a race that could go both ways. Throwaway code can be thrown away though, and well tested code does what the tests say, which is good enough according to my &lt;a href="https://406.ch/writing/my-rules-for-releasing-open-source-software/"&gt;rules for releasing open source software&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Speaking as someone who has put more into the training set than they&amp;rsquo;ve taken out so far, I don&amp;rsquo;t feel all that bad using the tools. Coding agents can already be run locally with reasonable hardware requirements, at least during inference, which is where the ongoing cost sits. Maybe using them is still rationalization. But contribution and profit needing to stay in some rough balance feels like the right frame. Total abstinence isn&amp;rsquo;t the only ethical choice we have.&lt;/p&gt;
&lt;h2 id="community-tensions"&gt;&lt;a class="toclink" href="#community-tensions"&gt;Community tensions&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;What makes me less comfortable is how communities are reacting. There are real concerns within the Django world, and not just the practical one of overworked maintainers wading through hastily generated patches that don&amp;rsquo;t actually fix anything. The deeper worry is about the communal nature of contribution: that working on Django is supposed to be a learning experience, a way into the community, and that using an LLM as a vehicle rather than a tool hollows out that process. Reviewers end up interacting with what is essentially a facade, unable to tell whether anyone actually understood the problem. That&amp;rsquo;s a real concern and I don&amp;rsquo;t want to dismiss it.&lt;/p&gt;
&lt;p&gt;But it maps onto a different situation from what I&amp;rsquo;ve been describing. Using Claude Code to close issues in projects I maintain and understand is not the same as using it to paper over gaps in comprehension on a ticket in someone else&amp;rsquo;s project. Whether LLM-assisted contributions to Django itself are appropriate is a difficult question; whether it&amp;rsquo;s appropriate to use them when maintaining your own software less so.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a harder tension around quality. Django&amp;rsquo;s conservatism has real value: rigorous review, minimal magic, a coherent philosophy. The ORM and template system don&amp;rsquo;t need to reinvent themselves, they work well, are still evolving while staying rock-solid for all my use cases. And reading the release notes always brings me joy. But it could be more exciting more often. Quality isn&amp;rsquo;t a strictly positive thing. Everything has costs. It&amp;rsquo;s not great if the price of the bar is that legitimate bugs sit open for years because nobody has a few evenings to spend on them. It happened with django-tree-queries before I went through it with Claude Code. I think the bar for contributing to Django is too high. I would value a little more motion and a little less stability, even as someone running dozens of Django websites and apps.&lt;/p&gt;
&lt;p&gt;Then there&amp;rsquo;s the pile-on dynamic that plays out on Mastodon and GitHub. When the Harfbuzz and chardet maintainers disclosed LLM usage, the reaction from some corners was something to behold. People expressing what amounted to personal grievance over tooling choices in projects they may not even use. There&amp;rsquo;s a particular kind of entitlement in telling a maintainer &amp;ndash; who is keeping software alive, possibly even in their spare time &amp;ndash; that the way they choose to do that work is an affront. Open source is a gift, whether paid or not, and nobody has to accept it, but disclosing your tooling isn&amp;rsquo;t an invitation for complaints. The ethical concerns about training data, resource use and other negative externalities are legitimate and worth raising. Performative outrage directed at individual maintainers is not the same thing.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have an easy conclusion. The tools are useful, the ethics are murky, and communities are still figuring out how to respond. A cautious, honest use of them feels better to me than the alternatives.&lt;/p&gt;</summary></entry><entry><title>Weeknotes (2026 week 11)</title><link href="https://406.ch/writing/weeknotes-2026-week-11/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2026-week-11/</id><published>2026-03-11T12:00:00Z</published><updated>2026-03-11T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2026-week-11"&gt;&lt;a class="toclink" href="#weeknotes-2026-week-11"&gt;Weeknotes (2026 week 11)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Last time I wrote that I seem to be publishing weeknotes monthly. Now, a quarter of a year has passed since the &lt;a href="https://406.ch/writing/weeknotes-2025-week-49/"&gt;last entry&lt;/a&gt;. I do enjoy the fact that I have published more posts focused on a single topic. That said, what has been going on in open source land is certainly interesting too.&lt;/p&gt;
&lt;h2 id="llms-in-open-source"&gt;&lt;a class="toclink" href="#llms-in-open-source"&gt;LLMs in Open Source&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have started a longer piece to think about my stance regarding using LLMs in Open Source. The argument I&amp;rsquo;m thinking about is that there&amp;rsquo;s a balance between LLMs having ingested all of my published open source code and myself using them now to help myself and others again.&lt;/p&gt;
&lt;p&gt;The happenings in the last two weeks (think Pentagon, Iran, and the bombings of schools) have again brought to the foreground the perils of using those tools. I therefore haven&amp;rsquo;t been motivated to pursue this train of thought for the moment. When the upsides are somewhat questionable and tentative and the downsides are so clear and impossible to miss, it&amp;rsquo;s hard to use my voice to speak in favor of these tools.&lt;/p&gt;
&lt;p&gt;That said, all the shaming when someone uses an LLM that I see in my Mastodon feed also annoys me. I&amp;rsquo;ll quote part of a post here which I liked and leave it at that for the moment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The AI hype-cyclone is bad, but so is the anti-AI witch hunt. Commits co-authored by Claude do not mean that a project has &amp;ldquo;abandoned engineering as a serious endeavor&amp;rdquo;&lt;/p&gt;
&lt;p&gt;[&amp;hellip;]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;ndash; &lt;a href="https://hachyderm.io/@nedbat/116133445557306539"&gt;@nedbat on Mastodon&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="other-goings-on"&gt;&lt;a class="toclink" href="#other-goings-on"&gt;Other goings-on&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Health:&lt;/strong&gt; My back continues to improve. Some days are still bad, but the idea that the &lt;a href="https://406.ch/writing/my-2025-in-review/#sports-and-health"&gt;herniation&lt;/a&gt; may go away entirely doesn&amp;rsquo;t sound totally unreasonable anymore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gardening:&lt;/strong&gt; We started weeding the garden last week. Lots to do! Being outside is fun. Weeding isn&amp;rsquo;t the greatest part ever, but it&amp;rsquo;s meditative.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="releases-since-december"&gt;&lt;a class="toclink" href="#releases-since-december"&gt;Releases since December&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-json-schema-editor/"&gt;django-json-schema-editor 0.12.1&lt;/a&gt;: CSS fixes. I have again looked at other, more modern JSON schema editor implementations but all of them are more limited than is acceptable to act as a replacement.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-debug-toolbar/"&gt;django-debug-toolbar 6.2&lt;/a&gt;: I haven&amp;rsquo;t done much work here! Just some reviewing.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-content-editor/"&gt;django-content-editor 8.1&lt;/a&gt;: Started emitting warnings when using non-abstract base classes for plugins. Using multi table inheritance is mostly an accident and not intended in my experience when using django-content-editor, therefore we have started detecting this case and emitting system checks (warnings, not errors).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-imagefield/"&gt;django-imagefield 0.23.0a3&lt;/a&gt;: We have done some work on supporting &lt;a href="https://www.libvips.org/"&gt;libvips&lt;/a&gt; as an alternative backend to Pillow because I hoped that memory usage in Kubernetes pods might go down a bit. Results are not conclusive yet, and I&amp;rsquo;m not yet convinced the additional code complexity is worth it. Debugging and monitoring continues.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/FeinCMS/"&gt;FeinCMS 26.2.1&lt;/a&gt;: Released a few bugfixes. FeinCMS is still being maintained ~17 years later!&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-auto-admin-fieldsets/"&gt;django-auto-admin-fieldsets 0.3&lt;/a&gt;: Added a helper to remove fields from the fieldsets structure.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-tree-queries/"&gt;django-tree-queries 0.23.1&lt;/a&gt;: Shipped a small bugfix for &lt;code&gt;{% recursetree %}&lt;/code&gt; which unintentionally cached children across invocations.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3-downloads/"&gt;feincms3-downloads&lt;/a&gt;: Used &lt;code&gt;PATH&lt;/code&gt; from the environment instead of using a very restricted allowlist so that &lt;code&gt;convert&lt;/code&gt; and &lt;code&gt;pdftocairo&lt;/code&gt; are detected in more locations. This should help with local development for example on macOS.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor 0.24.1&lt;/a&gt;: Read the &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/changelog.html"&gt;CHANGELOG&lt;/a&gt;; there&amp;rsquo;s too much in there for a short notice.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/form-designer/"&gt;form-designer 0.27.3&lt;/a&gt;: Mosparo captcha support, bugfixes and additional translations.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3/"&gt;feincms3 5.5&lt;/a&gt;: Started using the &lt;code&gt;OrderableTreeNode&lt;/code&gt; from django-tree-queries.&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Podcasts I like listening to (2026 edition)</title><link href="https://406.ch/writing/podcasts-i-like-listening-to-2026-edition/" rel="alternate" /><id>https://406.ch/writing/podcasts-i-like-listening-to-2026-edition/</id><published>2026-02-13T12:00:00Z</published><updated>2026-02-13T12:00:00Z</updated><summary type="html">&lt;h1 id="podcasts-i-like-listening-to-2026-edition"&gt;&lt;a class="toclink" href="#podcasts-i-like-listening-to-2026-edition"&gt;Podcasts I like listening to (2026 edition)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I have published &lt;a href="https://406.ch/writing/podcasts-i-like-listening-to/"&gt;another post like this almost two years
ago&lt;/a&gt;. I like listening to
podcasts even more than I did back then, when I only just started diving into
this world.&lt;/p&gt;
&lt;p&gt;Sometimes I really like the silence. Other times, podcasts accompany my training
regime, my chores and my (short) commute when I&amp;rsquo;m going to the office and not
working from home.&lt;/p&gt;
&lt;p&gt;Let me say that I&amp;rsquo;m not going to differentiate between technology, society, politics and other topics. Everything is political. Especially technology. This is something which could perhaps be overlooked from a privileged position many years ago, but that&amp;rsquo;s not possible anymore without willful ignorance.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve tried organizing the podcasts according to my listening habits, the order within categories doesn&amp;rsquo;t have any particular meaning.&lt;/p&gt;
&lt;h2 id="never-miss-an-episode"&gt;&lt;a class="toclink" href="#never-miss-an-episode"&gt;Never miss an episode&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These are the podcasts I listen to every time they publish.&lt;/p&gt;
&lt;h3 id="tech-wont-save-us"&gt;&lt;a class="toclink" href="#tech-wont-save-us"&gt;&lt;a href="https://www.techwontsave.us/"&gt;Tech Won&amp;rsquo;t Save Us&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tech Won&amp;rsquo;t Save Us is unfortunately even more relevant than it was back in 2024. The CEOs of the tech industry sat in the front row of the current
president of the US of A&amp;rsquo;s inauguration and there&amp;rsquo;s really no way to overlook
their allegiance.&lt;/p&gt;
&lt;p&gt;The tech industry and the political project goes hand in hand, and we have to
push back. This podcast offers practicable ways to do that and also provides a
lot of background on what&amp;rsquo;s going on.&lt;/p&gt;
&lt;h3 id="the-weekly-show-with-jon-stewart"&gt;&lt;a class="toclink" href="#the-weekly-show-with-jon-stewart"&gt;&lt;a href="https://www.youtube.com/@WeeklyShowPodcast"&gt;The Weekly Show with Jon Stewart&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;(The description on YouTube contains links to other platforms. I don&amp;rsquo;t actually
listen on YouTube.)&lt;/p&gt;
&lt;p&gt;How could you not like what Jon Stewart is doing? Back in the days, the Daily
Show was my introduction to the world of humorous political and societal
commentary. I also like the other hosts of the daily show, the long form
interviews on the weekly show are something special though.&lt;/p&gt;
&lt;h3 id="404-media"&gt;&lt;a class="toclink" href="#404-media"&gt;&lt;a href="https://www.404media.co/"&gt;404 Media&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m one of those people who do not read all the articles 404 Media publishes,
but I&amp;rsquo;m definitely listening to the podcast episodes where they talk about the
stories they wrote. They have recently started publishing a series of
interviews where I&amp;rsquo;m more selective, though I&amp;rsquo;m interested often.&lt;/p&gt;
&lt;h3 id="gute-frage"&gt;&lt;a class="toclink" href="#gute-frage"&gt;&lt;a href="https://www.republik.ch/format/gute-frage"&gt;Gute Frage!&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A new podcast from the makers of the Republik online newspaper. I hope they
keep it going. I&amp;rsquo;m also diving into other podcasts from Republik.&lt;/p&gt;
&lt;h3 id="meyerwermuth"&gt;&lt;a class="toclink" href="#meyerwermuth"&gt;&lt;a href="https://www.sp-ps.ch/artikel-kanal/meyer-wermuth/"&gt;Meyer:Wermuth&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m not even a member of the party (SP/PS), but I know some of the politicians
personally and the podcast is a good way to get their insights.&lt;/p&gt;
&lt;h3 id="le-show"&gt;&lt;a class="toclink" href="#le-show"&gt;&lt;a href="https://harryshearer.com/le-show/"&gt;Le Show&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Le Show was recommended by a good friend who has roots both in New Orleans and
Switzerland. I like the way the show mixes critical commentary on Politics, the
AI hype, our smart world, the church and other topics with great musical
interludes.&lt;/p&gt;
&lt;h2 id="regular-rotation"&gt;&lt;a class="toclink" href="#regular-rotation"&gt;Regular rotation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These are podcasts I usually listen to but might skip some episodes.&lt;/p&gt;
&lt;h3 id="trash-future"&gt;&lt;a class="toclink" href="#trash-future"&gt;&lt;a href="https://trashfuture.co.uk/"&gt;Trash Future&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I don&amp;rsquo;t think the Future is trash and I somehow, maybe despite the fact that I
sometimes feel I should know better, don&amp;rsquo;t believe it is bleak either. I don&amp;rsquo;t
think the makers of these podcasts genuinely believe that either. That said, black
humor helps with coping, therefore this is a listen I always look forward to.&lt;/p&gt;
&lt;h3 id="django-chat"&gt;&lt;a class="toclink" href="#django-chat"&gt;&lt;a href="https://djangochat.com/"&gt;Django Chat&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I always have a great time listening to news about my favorite web framework
and especially my favorite web framework community.&lt;/p&gt;
&lt;h3 id="volksverpetzer"&gt;&lt;a class="toclink" href="#volksverpetzer"&gt;&lt;a href="https://www.volksverpetzer.de/podcast/"&gt;Volksverpetzer&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Volksverpetzer is the anti fake news podcast by the german Volksverpetzer team.
The name is hard to translate because it&amp;rsquo;s both a joke and a pun. I won&amp;rsquo;t even
try to explain it and instead link to their own &lt;a href="https://www.volksverpetzer.de/ueber-uns/"&gt;about us
page&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="datenschutzplaudereien"&gt;&lt;a class="toclink" href="#datenschutzplaudereien"&gt;&lt;a href="https://podcast.datenschutzpartner.ch/"&gt;Datenschutzplaudereien&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is still required listening for people working with customer data in
Switzerland.&lt;/p&gt;
&lt;h3 id="soft-skills-engineering"&gt;&lt;a class="toclink" href="#soft-skills-engineering"&gt;&lt;a href="https://softskills.audio/"&gt;Soft Skills Engineering&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is always a nice listen. The podcast has been recommended by
&lt;a href="https://www.better-simple.com/"&gt;Tim&lt;/a&gt; I think. I have to say I almost always
skip the first 5 minutes. The questions are great and there&amp;rsquo;s still a lot of
banter to be had after the introduction.&lt;/p&gt;
&lt;h3 id="oxide-and-friends"&gt;&lt;a class="toclink" href="#oxide-and-friends"&gt;&lt;a href="https://oxide-and-friends.transistor.fm/"&gt;Oxide and Friends&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I could listen to Bryan talk all day long. I&amp;rsquo;m sure I&amp;rsquo;m not the only one, maybe he himself included.&lt;/p&gt;
&lt;h3 id="talk-python-to-me"&gt;&lt;a class="toclink" href="#talk-python-to-me"&gt;&lt;a href="https://talkpython.fm/"&gt;Talk Python to Me&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A well-established podcast about my favorite programming language with
interesting discussions about Python and the broader ecosystem.&lt;/p&gt;
&lt;h2 id="selectivetopic-based"&gt;&lt;a class="toclink" href="#selectivetopic-based"&gt;Selective/Topic-based&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These are podcasts where I pick and choose episodes based on the topic or guest.&lt;/p&gt;
&lt;h3 id="srf-podcasts"&gt;&lt;a class="toclink" href="#srf-podcasts"&gt;&lt;a href="https://www.srf.ch/audio"&gt;SRF Podcasts&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s no single show I always listen to, but the selection is impressive.
Interviews with leaders, philosophy, politics. I think it&amp;rsquo;s especially
important to mention this in these days, because some people want to slash
public funding for the fourth estate. It shouldn&amp;rsquo;t surprise anyone that those
people are mostly aligned with the political right. They would probably like if
billionaires had more power about our media as well. Let&amp;rsquo;s hope it doesn&amp;rsquo;t come
to that and definitely throw in a resounding &lt;a href="https://halbierungsinitiative-nein.ch/"&gt;NO! in the public
referendum&lt;/a&gt; coming soon.&lt;/p&gt;
&lt;h3 id="rust-in-production"&gt;&lt;a class="toclink" href="#rust-in-production"&gt;&lt;a href="https://corrode.dev/podcast/"&gt;Rust in Production&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When Oxide and Friends are not talking about Rust I have to get my fix
somewhere else. I don&amp;rsquo;t work with Rust but I think it&amp;rsquo;s currently one of the
most genuinely interesting programming languages.&lt;/p&gt;
&lt;h3 id="corepy"&gt;&lt;a class="toclink" href="#corepy"&gt;&lt;a href="https://www.youtube.com/playlist?list=PLShJCpYUN3C3XrdguJiAZrQEnQGH-Sq26"&gt;core.py&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I didn&amp;rsquo;t find a site for this, but I like this established podcast about my
favorite programming language.&lt;/p&gt;
&lt;h3 id="dos-game-club"&gt;&lt;a class="toclink" href="#dos-game-club"&gt;&lt;a href="https://www.dosgameclub.com/"&gt;DOS Game Club&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is a fun one, especially when I have played the game discussed in the
past, but more often than not also when I didn&amp;rsquo;t.&lt;/p&gt;
&lt;h3 id="the-ezra-klein-show"&gt;&lt;a class="toclink" href="#the-ezra-klein-show"&gt;&lt;a href="https://www.nytimes.com/column/ezra-klein-podcast"&gt;The Ezra Klein Show&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The podcast is often not critical enough for my taste, but some topics and
guests are still really interesting and worth the listen.&lt;/p&gt;
&lt;h2 id="honorable-mentions"&gt;&lt;a class="toclink" href="#honorable-mentions"&gt;Honorable mentions&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These are podcasts I still like but listen to less frequently, or they serve a specific purpose in my current work.&lt;/p&gt;
&lt;h3 id="hanselminutes"&gt;&lt;a class="toclink" href="#hanselminutes"&gt;&lt;a href="https://hanselminutes.com/"&gt;Hanselminutes&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A long-running technology podcast that I still enjoy when I catch an episode.&lt;/p&gt;
&lt;h3 id="react-native-radio"&gt;&lt;a class="toclink" href="#react-native-radio"&gt;&lt;a href="https://infinite.red/react-native-radio"&gt;React Native Radio&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m working on a large React Native project these days or months, and RNR is a
way to learn more about this technology.&lt;/p&gt;
&lt;h3 id="weird-studies"&gt;&lt;a class="toclink" href="#weird-studies"&gt;&lt;a href="https://www.weirdstudies.com/"&gt;Weird Studies&lt;/a&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I really like this podcast because of its strangeness, though I listen less
than I did two years ago.&lt;/p&gt;
&lt;h2 id="leavers"&gt;&lt;a class="toclink" href="#leavers"&gt;Leavers&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Some podcasts are too lighthearted or too centrist for my taste and I have
stopped listening to them, not as a statement, just as a fact. There&amp;rsquo;s no point
in listing them, since they are well made and I think listening to them is
still much better than losing interest in our society.&lt;/p&gt;</summary></entry><entry><title>My 2025 in review</title><link href="https://406.ch/writing/my-2025-in-review/" rel="alternate" /><id>https://406.ch/writing/my-2025-in-review/</id><published>2026-01-14T12:00:00Z</published><updated>2026-01-14T12:00:00Z</updated><summary type="html">&lt;h1 id="my-2025-in-review"&gt;&lt;a class="toclink" href="#my-2025-in-review"&gt;My 2025 in review&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I have been inspired by several posts where people reviewed the past year to
write such a post too.&lt;/p&gt;
&lt;h2 id="vacations"&gt;&lt;a class="toclink" href="#vacations"&gt;Vacations&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Snow" src="/assets/20260114-snow.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I have been going snowboarding for the first time in many many years in
February. I have been skiing and snowboarding a lot in my teens and twenties.
After moving further away from the alps and also rupturing my cruciate ligament
while playing Fussball (soccer) I have been less motivated to pursue these
riskier sports and have instead mostly been running and biking. Some may argue
if mountain biking really is less risky but for me that seems sort of obvious.&lt;/p&gt;
&lt;p&gt;Anyway, the motivation to go hit the slopes again was that the kids wanted to
go and leaving the gray city in winter is always a great idea.&lt;/p&gt;
&lt;p&gt;My wife and me have also been in the black forest in Southern Germany twice at
the beginning of the year and been spending time together in a nice hotel,
eating great food and spending a lot of time in the spa.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Liguria" src="/assets/20260114-liguria.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;In late April we went to Liguria, Italy as a larger group. We did cook a lot,
went on a few bike rides along the coast and were even bathing in the
Mediterranean Sea even though the water was very cold, at least for my taste
(about 15°C).&lt;/p&gt;
&lt;p&gt;I also went to France with friends several times for manual work and leisure.
We are still in the middle of renovating and altering a house to fit our tastes
better. It&amp;rsquo;s always interesting and a good diversion from staring at the
computer all day long. I have helped with larger projects inside the house and
also restored a few city bikes to full functionality after they were collecting
dust and rust in a moldy cellar.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Sitges" src="/assets/20260114-sitges.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;In October we went to Sitges near Barcelona with my parents-in-law. We took the
train and spent the night in Lyon both ways. It&amp;rsquo;s possible to do the voyage in
a single day, but we thought it&amp;rsquo;s nice to spend a little less time per day in
the train and visit Lyon since I&amp;rsquo;ve never been there before. High speed train
networks in Europe are great, but we had some bad luck with a total collapse of
the timetable because a deer was hit by a train. Very sad for the deer and
unfortunate for us. I thought about going to &lt;a href="https://www.djangomed.eu/"&gt;Django on the Med
🏖️&lt;/a&gt; and it would have been really close, but it
didn&amp;rsquo;t fit into the schedule very well with everything else which was already
going on.&lt;/p&gt;
&lt;p&gt;In Sitges the locals didn&amp;rsquo;t go to the beach anymore, maybe it was too cold for them or
they just had enough after months of summer?&lt;/p&gt;
&lt;h2 id="sports-and-health"&gt;&lt;a class="toclink" href="#sports-and-health"&gt;Sports and health&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My full suspension bike was stolen in 2024 and I have been riding an old 26&amp;rdquo;
hardtail in the meantime. I bought a relatively cheap 29&amp;rdquo; hardtail instead of a
full suspension bike and have been enjoying the slightly more technical
descents and the much improved climbing behavior a lot. The bike trails around
here don&amp;rsquo;t really call for a full suspension bike; a lighter bike works nicely
and I feel like I get more out of the same trail again.&lt;/p&gt;
&lt;p&gt;Unfortunately, later during the year I herniated a disc in the lower back, and
biking doesn&amp;rsquo;t really help there. It isn&amp;rsquo;t over yet but at least I have now
started some strength training again. I don&amp;rsquo;t enjoy it too much but doing some
exercises while listening to podcasts is actually fine. Also, the gym
subscription includes access to the spa (sauna and steam bath) so it&amp;rsquo;s not too
bad. I do hope and am working on enjoying that more. It&amp;rsquo;s not the first time
that my back is in a bad shape despite the fact that I&amp;rsquo;m generally athletic,
just not in a very balanced way it seems.&lt;/p&gt;
&lt;h2 id="gardening"&gt;&lt;a class="toclink" href="#gardening"&gt;Gardening&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Not much to tell. The year wasn&amp;rsquo;t excellent, but was good enough and we are still happy with our little garden.&lt;/p&gt;
&lt;p&gt;The shed (it&amp;rsquo;s really not much more than that) is causing some headaches. I
somewhat wish it would come crashing down so that we could build something
simpler and smaller. The roof is bad. At least the wasps enjoy the wood and the
niches in there. They weren&amp;rsquo;t aggressive towards us so it was fine leaving them
there, after all, wasps are really valuable for the ecosystem.&lt;/p&gt;
&lt;h2 id="computering"&gt;&lt;a class="toclink" href="#computering"&gt;Computering&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I still do not enjoy the idea of working with LLMs but I admit that they are
useful. I generally work on websites and webapps, but I&amp;rsquo;m currently working on
a React Native project and I have to say the headstart I get with those tools
is really quite enjoyable. I think &lt;a href="https://rfd.shared.oxide.computer/rfd/0576"&gt;the Oxide RFD on LLM
usage&lt;/a&gt; offers some excellent
guidance. Using LLMs for writing seems like a really stupid idea, since writing
is thinking, and you don&amp;rsquo;t want to offload thinking to a machine. I want the
machine to do the busywork.&lt;/p&gt;
&lt;p&gt;Like probably many other people I have too many computers. My main working
machine is still a Thinkpad X1 carbon with Fedora. I also have a Windows
Notebook for some gaming; I enjoyed &lt;a href="https://doom.bethesda.net/"&gt;Doom: The Dark
Ages&lt;/a&gt; a lot. Not as much as Doom Eternal, but that
one will be really hard to top. I also use a Macbook now for the React Native
project to be able to run all of the iOS tools. The Mac hardware is nice again
after the butterfly keyboard disaster and the touch bar abomination. Battery
life and performance are great. The macOS interface is jarring though, each
time I use it I wish I was using &lt;a href="https://www.gnome.org/"&gt;GNOME&lt;/a&gt;, which &amp;ndash; as a
power user &amp;ndash; I like a lot. At least there&amp;rsquo;s now an easy way to switch the
keyboard layout to a Logitech/PC layout. Not because the PC layout would be
better, just because I&amp;rsquo;m accustomed to it.&lt;/p&gt;
&lt;p&gt;Apart from that the &lt;a href="https://406.ch/writing/my-2024-development-setup/"&gt;development
setup&lt;/a&gt; is essentially
unchanged.&lt;/p&gt;
&lt;h2 id="open-source"&gt;&lt;a class="toclink" href="#open-source"&gt;Open source&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m writing about that all the time on this blog, and I won&amp;rsquo;t even try giving
an overview. I think it&amp;rsquo;s important to give everything else some space now.&lt;/p&gt;
&lt;h2 id="politics-and-society"&gt;&lt;a class="toclink" href="#politics-and-society"&gt;Politics and society&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I don&amp;rsquo;t really know how to say this. I&amp;rsquo;m offended that we live in a world
where, instead of working together for a better future, we instead have to
fight the slide back into authoritarianism and fascism. I&amp;rsquo;m not exactly
surprised, but it&amp;rsquo;s hard to accept the news every day (or every week). I hoped
for a better 2026 because the new year always has a promise for me that some
new thing may happen. Let&amp;rsquo;s just say that the first two days of the new year
were nice, and then I started reading the news again.&lt;/p&gt;
&lt;p&gt;Not engaging with the world isn&amp;rsquo;t a choice I want to make either. So, all I can
do is continue trying to make a difference.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;&lt;a class="toclink" href="#closing-thoughts"&gt;Closing thoughts&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Enough said. Let&amp;rsquo;s continue hoping for and working for a better 2026!&lt;/p&gt;</summary></entry><entry><title>Rich text editors: How restrictive can we be?</title><link href="https://406.ch/writing/rich-text-editors-how-restrictive-can-we-be/" rel="alternate" /><id>https://406.ch/writing/rich-text-editors-how-restrictive-can-we-be/</id><published>2025-12-17T12:00:00Z</published><updated>2025-12-17T12:00:00Z</updated><summary type="html">&lt;h1 id="rich-text-editors-how-restrictive-can-we-be"&gt;&lt;a class="toclink" href="#rich-text-editors-how-restrictive-can-we-be"&gt;Rich text editors: How restrictive can we be?&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;How restrictive should a rich text editor be? It&amp;rsquo;s a question I keep coming back to as I work on FeinCMS and Django-based content management systems.&lt;/p&gt;
&lt;p&gt;I published the last blog post on &lt;a href="https://github.com/feincms/django-prose-editor"&gt;django-prose-editor&lt;/a&gt; specifically in August 2025, &lt;a href="https://406.ch/writing/menu-improvements-in-django-prose-editor/"&gt;Menu improvements in django-prose-editor&lt;/a&gt;. The most interesting part of the blog post was the short mention of the &lt;code&gt;TextClass&lt;/code&gt; extension at the bottom which allows adding a predefined list of CSS classes to arbitrary spans of text.&lt;/p&gt;
&lt;p&gt;In the meantime, I have spent a lot of time working on extensions that try to answer this question: the &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/textclass.html"&gt;&lt;code&gt;TextClass&lt;/code&gt; extension&lt;/a&gt; for adding CSS classes to inline text, and more recently the &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/nodeclass.html"&gt;&lt;code&gt;NodeClass&lt;/code&gt; extension&lt;/a&gt; for adding classes to nodes and marks. It&amp;rsquo;s high time to write a post about it.&lt;/p&gt;
&lt;h2 id="rich-text-editing-philosophy"&gt;&lt;a class="toclink" href="#rich-text-editing-philosophy"&gt;Rich Text editing philosophy&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;All of this convinced me that offering the user a rich text editor with too much capabilities is a really bad idea. The rich text editor in FeinCMS only has bold, italic, bullets, link and headlines activated (and the HTML code button, because that&amp;rsquo;s sort of inevitable – sometimes the rich text editor messes up and you cannot fix it other than going directly into the HTML code. Plus, if someone really knows what they are doing, I&amp;rsquo;d still like to give them the power to shoot their own foot).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;ndash; &lt;a href="https://github.com/feincms/feincms/commit/70cd7a1244438d2ba97852256f77daa2c870c345#diff-556c5559a716059d4fb714ad34de6a9845870e8d55bbd2cb9d77c732eb961388"&gt;Commit in the FeinCMS repository, August 2009&lt;/a&gt;, current version from &lt;a href="https://django-content-editor.readthedocs.io/en/latest/design-decisions.html"&gt;django-content-editor design decisions&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="should-we-let-users-shoot-themselves-in-the-foot"&gt;&lt;a class="toclink" href="#should-we-let-users-shoot-themselves-in-the-foot"&gt;Should we let users shoot themselves in the foot?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Giving power users an HTML code button would have been somewhat fine if only the editors themselves were affected. Unfortunately, that was not the case.&lt;/p&gt;
&lt;p&gt;As a team we have spent more time than we ever wanted debugging strange problems only to find out that the culprit was a blob of CSS or JavaScript inserted directly into an unsanitized rich text editor field. We saw everything from a few reasonable and well scoped lines of CSS to hundreds of KiBs of hotlinked JavaScript code that broke layouts, caused performance issues, and possibly even created security vulnerabilities.&lt;/p&gt;
&lt;p&gt;We have one more case of &lt;a href="https://en.wikipedia.org/wiki/Betteridge%27s_law_of_headlines"&gt;Betteridge&amp;rsquo;s law of headlines&lt;/a&gt; here.&lt;/p&gt;
&lt;h2 id="the-pendulum-swings"&gt;&lt;a class="toclink" href="#the-pendulum-swings"&gt;The pendulum swings&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first version of django-prose-editor which replaced the venerable CKEditor 4 in our project was much more strict and reduced &amp;ndash; no attributes, no classes, just a very short list of allowlisted HTML tags in the schema.&lt;/p&gt;
&lt;p&gt;We quickly hit some snags. When users needed similar headings with different styles, we worked around it by using H2 and H3 — not semantic at all. I wasn&amp;rsquo;t exactly involved in this decision; I just didn&amp;rsquo;t want to rock the boat too much, since I was so happy that we were even able to use the more restricted editor at all in this project.&lt;/p&gt;
&lt;p&gt;Everything was good for a while, but more and more use cases crept up until it was clear that something had to be done about it. First, the &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/textclass.html"&gt;&lt;code&gt;TextClass&lt;/code&gt; extension&lt;/a&gt; was introduced to allow adding classes to inline text, and later also the &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/nodeclass.html"&gt;&lt;code&gt;NodeClass&lt;/code&gt; extension&lt;/a&gt; mentioned above. This was a compromise: The customer wanted inline styles, we wanted as little customizability as possible without getting in the way.&lt;/p&gt;
&lt;p&gt;That said, we obviously had to move a bit. After all, going back to a less strict editor or even offering a HTML blob injection would be worse. If we try to be too restrictive we will probably have to go back to allowing everything some way or the other, after all:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The more you tighten your grip, Tarkin, the more star systems will slip through your fingers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;ndash; Princess Leia&lt;/p&gt;
&lt;h2 id="combining-css-classes"&gt;&lt;a class="toclink" href="#combining-css-classes"&gt;Combining CSS classes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The last words are definitely not spoken just yet. As &lt;a href="https://hachyderm.io/@matthiask/115650714479718340"&gt;teased on Mastodon&lt;/a&gt; at the beginning of this month I am working on an even more flexible extension which unifies the &lt;code&gt;NodeClass&lt;/code&gt; and &lt;code&gt;TextClass&lt;/code&gt; extensions into a single &lt;code&gt;ClassLoom&lt;/code&gt; extension.&lt;/p&gt;
&lt;p&gt;The code is getting real world use now, but I&amp;rsquo;m not ready to integrate it yet into the official repository. However, you can use it if you want, it&amp;rsquo;s 1:1 the version from a project repository. &lt;a href="https://gist.github.com/matthiask/64ea64b539d63d45ff71467752c2f307"&gt;Get the &lt;code&gt;ClassLoom&lt;/code&gt; extension here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This extension also allows combining classes on a single element. If you have 5 colors and 3 text styles, you&amp;rsquo;d have to add 15 combinations if you were only able to apply a single class. Allowing combinations brings the number of classes down to manageable levels.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;&lt;a class="toclink" href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So, back to the original question: How restrictive can we be?&lt;/p&gt;
&lt;p&gt;The journey from CKEditor 4&amp;rsquo;s permissiveness through django-prose-editor&amp;rsquo;s initial strictness to today&amp;rsquo;s &lt;code&gt;ClassLoom&lt;/code&gt; extension has been one of finding that balance. Each extension — &lt;code&gt;TextClass&lt;/code&gt;, &lt;code&gt;NodeClass&lt;/code&gt;, and now &lt;code&gt;ClassLoom&lt;/code&gt; — represents a step toward controlled flexibility: giving content editors the styling options they need while keeping the content structured, maintainable, and safe.&lt;/p&gt;</summary></entry><entry><title>Weeknotes (2025 week 49)</title><link href="https://406.ch/writing/weeknotes-2025-week-49/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2025-week-49/</id><published>2025-12-05T12:00:00Z</published><updated>2025-12-05T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2025-week-49"&gt;&lt;a class="toclink" href="#weeknotes-2025-week-49"&gt;Weeknotes (2025 week 49)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I seem to be publishing weeknotes monthly, so I&amp;rsquo;m now thinking about renaming the category :-)&lt;/p&gt;
&lt;h2 id="mosparo"&gt;&lt;a class="toclink" href="#mosparo"&gt;Mosparo&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have started using a self-hosted &lt;a href="https://mosparo.io/"&gt;mosparo&lt;/a&gt; instance for my captcha needs. It&amp;rsquo;s nicer than Google reCAPTCHA. Also, not sending data to Google and not training AI models on traffic signs feels better.&lt;/p&gt;
&lt;h2 id="fixes-for-the-youtube-153-error"&gt;&lt;a class="toclink" href="#fixes-for-the-youtube-153-error"&gt;Fixes for the YouTube 153 error&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Simon Willison published a nice writeup about &lt;a href="https://simonwillison.net/2025/Dec/1/youtube-embed-153-error/"&gt;YouTube embeds failing with a 153 error&lt;/a&gt;. We have also encountered this problem in the wild and fixed the &lt;a href="https://feincms3.readthedocs.io/en/latest/ref/embedding.html"&gt;feincms3&lt;/a&gt; embedding code to &lt;a href="https://github.com/feincms/feincms3/commit/3f00e5d2a15991d52f9ae0118b49fe231ea328d0"&gt;also set the required referrerpolicy attribute&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="updated-packages-since-2025-11-04"&gt;&lt;a class="toclink" href="#updated-packages-since-2025-11-04"&gt;Updated packages since 2025-11-04&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-sitemaps/"&gt;django-sitemaps 2.0.2&lt;/a&gt;: Uploaded a new release which includes a wheel build. Rebuilding the wheel all the time when creating new container images was getting annoying. The code itself is unchanged.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django_prune_uploads/"&gt;django-prune-uploads 0.3.1&lt;/a&gt;: The package now supports pruning a storage backed by django-s3-storage efficiently. I have also looked at &lt;a href="https://pypi.org/project/django-prune-media/"&gt;django-prune-media&lt;/a&gt; but since the package uses the storage API instead of enumerating files using boto3 directly it&amp;rsquo;s unusably slow for my use case.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3-forms/"&gt;feincms3-forms 0.6&lt;/a&gt;: Much better docs and a new way to reference &lt;a href="https://github.com/feincms/feincms3-forms?tab=readme-ov-file#custom-templates-for-compound-fields"&gt;individual form fields in custom templates&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-json-schema-editor/"&gt;django-json-schema-editor 0.11&lt;/a&gt;: Switched from JSON paths to &lt;code&gt;jmespath&lt;/code&gt; instances. Made the JSON model instance reference support easier and more fun to use. Added new ways of customizing the generated proxy model for individual JSON plugin instances.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/form-designer/"&gt;form-designer 0.27.1&lt;/a&gt;: Added support for the &lt;a href="https://mosparo.io/"&gt;mosparo captcha&lt;/a&gt; to the default list of field types.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/asgi-plausible/"&gt;asgi-plausible 0.1.1&lt;/a&gt;: No code change really, just added required dependencies to the package metadata.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-tree-queries/"&gt;django-tree-queries 0.23&lt;/a&gt;: The package now ships a &lt;code&gt;OrderableTreeNode&lt;/code&gt; base model which you can use when you want to order siblings manually. feincms3 already uses this base model for its pages model.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3-data/"&gt;feincms3-data 0.10&lt;/a&gt;: This is quite a big one. I discovered issues with the way &lt;code&gt;save_as_new&lt;/code&gt; (to copy data) and &lt;code&gt;delete_missing&lt;/code&gt; interacted. First the code was cleaned up to delete less data, and then to delete enough data. I&amp;rsquo;m now somewhat confident that the code does what it should again.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor 0.22.3&lt;/a&gt;: Started returning an empty string for an empty document instead of &lt;code&gt;&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;&lt;/code&gt; also when using the frontend integration; previously, this transformation was only implemented when using at least the form if not the model field. Also, &lt;code&gt;&amp;lt;ol&amp;gt;&lt;/code&gt; tags now have a &lt;code&gt;data-type&lt;/code&gt; attribute since Chrome cannot case-sensitively match e.g. &lt;code&gt;type="a"&lt;/code&gt; vs &lt;code&gt;type="A"&lt;/code&gt; for lowercase or uppercase letters. I previously only tested the code in Firefox and there it worked nicely.&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Thoughts about Django-based content management systems</title><link href="https://406.ch/writing/thoughts-about-django-based-content-management-systems/" rel="alternate" /><id>https://406.ch/writing/thoughts-about-django-based-content-management-systems/</id><published>2025-11-05T12:00:00Z</published><updated>2025-11-05T12:00:00Z</updated><summary type="html">&lt;h1 id="thoughts-about-django-based-content-management-systems"&gt;&lt;a class="toclink" href="#thoughts-about-django-based-content-management-systems"&gt;Thoughts about Django-based content management systems&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I have almost exclusively used Django for implementing content management
systems (and other backends) since 2008.&lt;/p&gt;
&lt;p&gt;In this time, content management systems have come and gone. The big three
systems many years back were &lt;a href="https://www.django-cms.org/"&gt;django CMS&lt;/a&gt;,
&lt;a href="https://github.com/stephenmcd/mezzanine"&gt;Mezzanine&lt;/a&gt; and
&lt;a href="https://feinheit.ch"&gt;our&lt;/a&gt; own
&lt;a href="https://406.ch/writing/the-future-of-feincms/"&gt;FeinCMS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;During all this time I have always kept an eye open for other CMS than our own
but have steadily continued working in my small corner of the Django space. I
think it&amp;rsquo;s time to write down why I have been doing this all this time, for
myself and possibly also for other interested parties.&lt;/p&gt;
&lt;h2 id="why-not-use-wagtail-django-cms-or-any-of-those-alternatives"&gt;&lt;a class="toclink" href="#why-not-use-wagtail-django-cms-or-any-of-those-alternatives"&gt;Why not use Wagtail, django CMS or any of those alternatives?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s start with the big one. Why not use Wagtail?&lt;/p&gt;
&lt;p&gt;The Django administration interface is actually great. Even though some people
say that it should be treated as a tool for developers only, recent
improvements to the accessibility and the general usability suggest otherwise.
I have written more about my views on this in &lt;a href="https://406.ch/writing/the-django-admin-is-a-cms/"&gt;The Django admin is a
CMS&lt;/a&gt;. Using and building on
top of the Django admin is a great way to immediately profit from all current
and future improvements without having to reimplement anything.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t want to have to reimplement Django&amp;rsquo;s features, I want to add what I
need on top.&lt;/p&gt;
&lt;h2 id="faster-updates"&gt;&lt;a class="toclink" href="#faster-updates"&gt;Faster updates&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Everyone implementing and maintaining other CMS is doing a great job and I
don&amp;rsquo;t want to throw any shade. I still feel that it&amp;rsquo;s important to point out
that systems can make it hard to adopt new Django versions on release day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The update cycle of many large apps using Django lag behind Django. Wagtail
  declares an &lt;a href="https://github.com/wagtail/wagtail/discussions/12574"&gt;upper version boundary for
  Django&lt;/a&gt; which makes it
  hard to adopt Django versions faster than Wagtail releases updates.&lt;/li&gt;
&lt;li&gt;Some django CMS components such as
  &lt;a href="https://github.com/django-cms/django-filer"&gt;django-filer&lt;/a&gt; have lagged behind
  in the past. Looking at the project&amp;rsquo;s CI matrix and activity suggests that
  this is not the case anymore. That said, a &lt;a href="https://406.ch/writing/django-cabinet-a-media-library-for-django/"&gt;simpler alternative
  exists&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These larger systems have many more (very talented) people working on them. I&amp;rsquo;m
not saying I&amp;rsquo;m doing a better job. I&amp;rsquo;m only pointing out that I&amp;rsquo;m following a
different philosophy where I&amp;rsquo;m &lt;a href="https://406.ch/writing/run-less-code-in-production-or-youll-end-up-paying-the-price-later/"&gt;conservative about running code in
production&lt;/a&gt;
and I&amp;rsquo;d rather &lt;a href="https://406.ch/writing/low-maintenance-software/"&gt;have less features when the price is a lot of maintenance
later&lt;/a&gt;. I&amp;rsquo;m always thinking
about long term maintenance. I really don&amp;rsquo;t want to maintain some of these
larger projects, or even parts of them. So I&amp;rsquo;d rather not adopt them for
projects which hopefully will be developed and maintained for a long time to
come. By the way: This experience has been earned the hard way.&lt;/p&gt;
&lt;h2 id="the-rule-of-least-power"&gt;&lt;a class="toclink" href="#the-rule-of-least-power"&gt;The rule of least power&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;From &lt;a href="https://en.wikipedia.org/wiki/Rule_of_least_power"&gt;Wikipedia&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In programming, the rule of least power is a design principle that &amp;ldquo;suggests choosing the least powerful [computer] language suitable for a given purpose&amp;rdquo;. Stated alternatively, given a choice among computer languages, classes of which range from descriptive (or declarative) to procedural, the less procedural, more descriptive the language one chooses, the more one can do with the data stored in that language.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Django itself already provides lots and lots of power. I&amp;rsquo;d argue that a very
powerful platform on top of Django may be too much of a good thing. I&amp;rsquo;d rather
keep it simple and stupid.&lt;/p&gt;
&lt;h2 id="editing-heterogenous-collections-of-content"&gt;&lt;a class="toclink" href="#editing-heterogenous-collections-of-content"&gt;Editing heterogenous collections of content&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Django admin&amp;rsquo;s inlines are great, but they are not sufficient for building a
CMS. You need something to manage different types. django-content-editor does
that and has done that since 2009.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://torchbox.com/blog/rich-text-fields-and-faster-horses/"&gt;When Wagtail introduced the StreamField in
2015&lt;/a&gt; it was
definitely a great update to an already great CMS but it wasn&amp;rsquo;t a new idea
generally and not a new thing in Django land. They didn&amp;rsquo;t say it was and
&lt;a href="https://406.ch/writing/i-just-learned-about-wagtail-s-streamfield/"&gt;welcomed the fact that they also started using a better way to structure
content&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Structured content is great. Putting everything into one large rich text area
isn&amp;rsquo;t what I want. Django&amp;rsquo;s ORM and admin interface are great for actually
modelling the data in a reusable way. And when you need more flexibility than
what&amp;rsquo;s offered by Django&amp;rsquo;s forms, the community offers many projects extending
the admin. These days, I really like working with the
&lt;a href="https://406.ch/writing/django-json-schema-editor/"&gt;django-json-schema-editor&lt;/a&gt;
component; I even reference other model instances in the database and let the
JSON editor handle the referential integrity transparently for me (so that
referenced model instances do not silently disappear).&lt;/p&gt;
&lt;h2 id="more-reading"&gt;&lt;a class="toclink" href="#more-reading"&gt;More reading&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://406.ch/writing/the-future-of-feincms/"&gt;The future of FeinCMS&lt;/a&gt; and the &lt;a href="https://406.ch/writing/category-feincms/"&gt;feincms category&lt;/a&gt; may be interesting. Also, I&amp;rsquo;d love to talk about these thoughts, either by email or on Mastodon.&lt;/p&gt;</summary></entry><entry><title>Weeknotes (2025 week 45)</title><link href="https://406.ch/writing/weeknotes-2025-week-45/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2025-week-45/</id><published>2025-11-04T12:00:00Z</published><updated>2025-11-04T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2025-week-45"&gt;&lt;a class="toclink" href="#weeknotes-2025-week-45"&gt;Weeknotes (2025 week 45)&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id="autumn-is-nice"&gt;&lt;a class="toclink" href="#autumn-is-nice"&gt;Autumn is nice&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I love walking through the forest with all the colors and the rustling when you walk through the leaves on the ground.&lt;/p&gt;
&lt;h2 id="updated-packages-since-2025-10-23"&gt;&lt;a class="toclink" href="#updated-packages-since-2025-10-23"&gt;Updated packages since 2025-10-23&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3/"&gt;feincms3 5.4.3&lt;/a&gt;: Small fix for the
  YouTube IFRAME; it seems that the &lt;code&gt;referrerpolicy&lt;/code&gt; attribute is now necessary
  for the embed to work everywhere.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-json-schema-editor/"&gt;django-json-schema-editor 0.8.2&lt;/a&gt;:
  Allowed forwarding more options to the prose editor component; specifically,
  not just &lt;code&gt;extensions&lt;/code&gt; but also the undocumented &lt;code&gt;js_modules&lt;/code&gt; entry. This
  means that custom extensions are now also supported inside the JSON editor
  component.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor 0.20&lt;/a&gt;:
  I reworked the menu extension to be customizable more easily (you can now specify &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/menu.html#defining-button-groups-and-dropdowns"&gt;button groups and dropdowns&lt;/a&gt; directly without using JavaScript code) and I also extended the &lt;a href="https://django-prose-editor.readthedocs.io/en/latest/nodeclass.html"&gt;&lt;code&gt;NodeClass&lt;/code&gt; extension&lt;/a&gt; to allow assigning predefined CSS classes not only to nodes but also to marks.&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Weeknotes (2025 week 43)</title><link href="https://406.ch/writing/weeknotes-2025-week-43/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2025-week-43/</id><published>2025-10-23T12:00:00Z</published><updated>2025-10-23T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2025-week-43"&gt;&lt;a class="toclink" href="#weeknotes-2025-week-43"&gt;Weeknotes (2025 week 43)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I published the last weeknotes entry in the first half of September.&lt;/p&gt;
&lt;h2 id="drama-in-oss"&gt;&lt;a class="toclink" href="#drama-in-oss"&gt;Drama in OSS&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have been following the Ruby gems debacle a bit. Initially at &lt;a href="https://feinheit.ch/"&gt;Feinheit&lt;/a&gt; we used our own PHP-based framework &lt;a href="https://github.com/matthiask/swisdk2"&gt;swisdk2&lt;/a&gt; to build websites. This obviously didn&amp;rsquo;t scale and I was very annoyed with PHP, so I was looking for alternatives.&lt;/p&gt;
&lt;p&gt;I remember comparing Ruby on Rails and Django, and decided to switch from PHP/swisdk2 to Python/Django for two reasons: The automatically generated admin interface and the fact that Ruby source code just had too much punctuation characters for my taste. It&amp;rsquo;s a very whimsical reason and I do not put any weight on that. That being said, given how some of the exponents in Ruby/Rails land behave I&amp;rsquo;m very very glad to have chosen Python and Django. While not everything is perfect (it never is) at least those communities agree that trying to behave nicely to each other is something to be cheered and not something to be sneered at.&lt;/p&gt;
&lt;h2 id="copilot"&gt;&lt;a class="toclink" href="#copilot"&gt;Copilot&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I assigned some GitHub issues to Copilot. The result wasn&amp;rsquo;t very useful. I don&amp;rsquo;t know if I want to repeat it, local tools work fine for when I really need them.&lt;/p&gt;
&lt;h2 id="python-and-django-compatibility"&gt;&lt;a class="toclink" href="#python-and-django-compatibility"&gt;Python and Django compatibility&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s the time again to update the GitHub actions matrix and Trove identifiers. I do not like doing it. You can expect all maintained packages to be compatible with the latest and best versions, no upper bounds necessary. Man, if only AI could automate &lt;em&gt;those&lt;/em&gt; tasks&amp;hellip;&lt;/p&gt;
&lt;h2 id="updated-packages-since-2025-09-10"&gt;&lt;a class="toclink" href="#updated-packages-since-2025-09-10"&gt;Updated packages since 2025-09-10&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3/"&gt;feincms3&lt;/a&gt;: We use &lt;code&gt;hashlib.md5&lt;/code&gt;, but not for security.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-tree-queries/"&gt;django-tree-queries 0.21.2&lt;/a&gt;: django-tree-queries now bundles an admin interface for trees.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-content-editor/"&gt;django-content-editor 8.0.3&lt;/a&gt;: Minor CSS fix so that the editor looks nicer with Django 6.0.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-json-schema-editor/"&gt;django-json-schema-editor 0.8.1&lt;/a&gt;: Fixes for multiple JSON editors in the same window, better &lt;code&gt;JSONPlugin.__str__&lt;/code&gt; default implementation.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-cabinet/"&gt;django-cabinet 0.18.1&lt;/a&gt;: Fix a minor bug around the selected folder handling. Invalid folder ID values could crash the backend under some circumstances.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor 0.18.5&lt;/a&gt;: No large changes, mainly Tiptap updates.&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>My favorite Django packages</title><link href="https://406.ch/writing/my-favorite-django-packages/" rel="alternate" /><id>https://406.ch/writing/my-favorite-django-packages/</id><published>2025-10-22T12:00:00Z</published><updated>2025-10-22T12:00:00Z</updated><summary type="html">&lt;h1 id="my-favorite-django-packages"&gt;&lt;a class="toclink" href="#my-favorite-django-packages"&gt;My favorite Django packages&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Inspired by other posts I also wanted to write up a list of my favorite Django
packages. Since I&amp;rsquo;ve been working in this space for so long and since I&amp;rsquo;m
maintaining quite a large list of packages I worry a bit about tooting my own
horn too much here; that said, the reasons for choosing some packages hopefully
speak for themselves.&lt;/p&gt;
&lt;p&gt;Also, I&amp;rsquo;m sure I&amp;rsquo;m forgetting many many packages here. Sorry for that in advance.&lt;/p&gt;
&lt;h2 id="core-django"&gt;&lt;a class="toclink" href="#core-django"&gt;Core Django&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/speckenv/"&gt;speckenv&lt;/a&gt;: Loads environment variables from &lt;code&gt;.env&lt;/code&gt; and automatically converts them to their Python equivalent if &lt;a href="https://docs.python.org/3/library/ast.html#ast.literal_eval"&gt;&lt;code&gt;ast.literal_eval&lt;/code&gt;&lt;/a&gt; understands it. Also contains implementations for loading database, cache, email and storage configuration from environment variables (similar to dj-database-url). I added this functionality to speckenv when some of the available environment configuration apps&amp;rsquo; maintenance state was somewhat questionable.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-cors-headers/"&gt;django-cors-headers&lt;/a&gt;: CORS header support for Django. This would be a nice addition to Django itself.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/sentry-sdk/"&gt;sentry-sdk&lt;/a&gt;: I&amp;rsquo;m using Sentry in almost all projects.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-template-partials/"&gt;django-template-partials&lt;/a&gt;: Template partials for Django! This has been added to the upcoming 6.0 release of Django. While the Django template language has always been evolving and improving, this feels like the first larger step in a long time. As I and others have said, the combination of &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt; and django-template-partials is really really nice. It isn&amp;rsquo;t surprising at all that htmx is so well loved in the Django community.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-s3-storage/"&gt;django-s3-storage&lt;/a&gt;: Yes, django-storages exist, but I prefer django-s3-storage because of its focus and simplicity.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-imagefield/"&gt;django-imagefield&lt;/a&gt;: Image field which validates images deeply, and supports pre-rendering thumbnails etc. I understand why Django only superficially validates uploaded images because of the danger of denial of service attacks, but I&amp;rsquo;d rather not have files on the sites I manage which the great &lt;a href="https://pillow.readthedocs.io/"&gt;Pillow&lt;/a&gt; library doesn&amp;rsquo;t support.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/psycopg/"&gt;psycopg&lt;/a&gt;: Whenever I can I work with PostgreSQL, and psycopg is how I interface with the database.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="data-structures"&gt;&lt;a class="toclink" href="#data-structures"&gt;Data structures&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-tree-queries/"&gt;django-tree-queries&lt;/a&gt;: My
  favorite way to work with trees except when talking about real trees.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-translated-fields/"&gt;django-translated-fields&lt;/a&gt;: My preferred way to do model-level internationalization.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="cms-building"&gt;&lt;a class="toclink" href="#cms-building"&gt;CMS building&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have been working on &lt;a href="https://github.com/feincms/feincms/"&gt;FeinCMS&lt;/a&gt; since 2009. So, it shouldn&amp;rsquo;t surprise anyone that this is still my favorite way to
build CMS on top of Django. I like that it&amp;rsquo;s basically a thin layer on top of
Django&amp;rsquo;s administration interface and doesn&amp;rsquo;t want to take over the whole admin
interface or even the whole website.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3/"&gt;feincms3&lt;/a&gt;: The modern, focussed foundation replacing FeinCMS.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-content-editor/"&gt;django-content-editor&lt;/a&gt;: The admin interface extension providing core components to build content editing interfaces.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor&lt;/a&gt;: A nice HTML editor by yours truly.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-json-schema-editor/"&gt;django-json-schema-editor&lt;/a&gt;: JSON schema-based editing using &lt;a href="https://www.npmjs.com/package/@json-editor/json-editor"&gt;@json-editor/json-editor&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="working-with-external-content"&gt;&lt;a class="toclink" href="#working-with-external-content"&gt;Working with external content&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/micawber/"&gt;micawber&lt;/a&gt;: Micawber is my favorite tool to embed third party content (YouTube, Vimeo, whatever) when feincms3&amp;rsquo;s own very limited embedding functionality is insufficient.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/feincms3-cookiecontrol/"&gt;feincms3-cookiecontrol&lt;/a&gt;: Everyone likes cookie banner (or not). feincms3-cookiecontrol implements not just an informative cookie banner, but actually supports not embedding tracking scripts and third party content unless the user consented, either a blanket or a per-provider consent for embedded media. It does NOT support the very annoying interface where you have to deselect each tracking service individually.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="pdf-generation"&gt;&lt;a class="toclink" href="#pdf-generation"&gt;PDF generation&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/reportlab/"&gt;Reportlab&lt;/a&gt;: Reportlab is nice if you need fine-grained control over PDF generation. Reportlab has created more than 10&amp;lsquo;000 invoices for the company I work at, so I&amp;rsquo;m definitely grateful for it :-)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://weasyprint.org/"&gt;Weasyprint&lt;/a&gt;: I have been using Weasyprint more and more for generating PDFs. Using HTML and CSS can be nicer than using Reportlab&amp;rsquo;s Platypus module. Weasyprint doesn&amp;rsquo;t instrument a webbrowser to produce PDFs but instead implements the rendering engine itself. It works really well.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="testing-and-development"&gt;&lt;a class="toclink" href="#testing-and-development"&gt;Testing and development&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;I actually do like unittest. I have started using &lt;a href="https://docs.pytest.org/en/stable/"&gt;pytest&lt;/a&gt; somewhat more often because using &lt;code&gt;-k keyword&lt;/code&gt; to filter tests to run is great.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/factory-boy/"&gt;factory-boy&lt;/a&gt;: This package has always treated me well when creating data for tests.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/"&gt;playwright&lt;/a&gt;: Playwright is the end-to-end test browser automation library I prefer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Last but not least, I really like &lt;a href="https://pypi.org/project/django-debug-toolbar/"&gt;django-debug-toolbar&lt;/a&gt;. So much, that I&amp;rsquo;m even helping with the maintenance since 2016.&lt;/p&gt;
&lt;h2 id="serving"&gt;&lt;a class="toclink" href="#serving"&gt;Serving&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We mostly use Kubernetes to serve websites these days. Inside the pods, I&amp;rsquo;m working with the &lt;a href="https://pypi.org/project/granian/"&gt;granian&lt;/a&gt; RSGI/ASGI server and with &lt;a href="https://pypi.org/project/blacknoise/"&gt;blacknoise&lt;/a&gt; for serving static files.&lt;/p&gt;</summary></entry><entry><title>LLMs are making me a better programmer...</title><link href="https://406.ch/writing/llms-are-making-me-a-better-programmer/" rel="alternate" /><id>https://406.ch/writing/llms-are-making-me-a-better-programmer/</id><published>2025-09-12T12:00:00Z</published><updated>2025-09-12T12:00:00Z</updated><summary type="html">&lt;h1 id="llms-are-making-me-a-better-programmer"&gt;&lt;a class="toclink" href="#llms-are-making-me-a-better-programmer"&gt;LLMs are making me a better programmer&amp;hellip;&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I&amp;rsquo;m still undecided about LLMs for programming. Sometimes they are very useful, especially when working on a clearly stated problem within a delimited area. Cleaning the code up afterwards is painful and takes a long time though. Even for small changes I&amp;rsquo;m unsure if using LLMs is a way to save (any) resources, be it time, water, energy or whatever.&lt;/p&gt;
&lt;p&gt;They do help me get started, and help me be more ambitious. That&amp;rsquo;s not a new idea. &lt;a href="https://simonwillison.net/2023/Mar/27/ai-enhanced-development/"&gt;Simon Willison wrote a post about this in 2023&lt;/a&gt; and the more I think about it or work with AI the more I think it&amp;rsquo;s a good way to look at it.&lt;/p&gt;
&lt;p&gt;A recent example which comes to mind is writing end to end tests. I can&amp;rsquo;t say I had a love-hate relationship with end to end testing, it was mostly a hate-hate relationship. I hate writing them because it&amp;rsquo;s so tedious and I hate debugging them because of all the timing issues and the general flakyness of end to end testing. And I especially hate the fact that those tests break all the time when changing the code, even when changes are mostly unrelated.&lt;/p&gt;
&lt;p&gt;When I discovered that I could just use Claude Code to write those end to end tests I was ecstatic. Finally a way to add relevant tests to some of my open source projects without having to do all this annoying work myself! Unfortunately, I quickly discovered that Claude Code decided (ha!) it&amp;rsquo;s more important to make tests pass than actually exercising the functionality in question. When some HTML/JavaScript widget wouldn&amp;rsquo;t initialize, why not just manipulate &lt;code&gt;innerHTML&lt;/code&gt; so that the DOM looks as if the JavaScript actually ran? Of course, that&amp;rsquo;s a completely useless test. The amount of prodding and instructing the LLM agent required to stop adding workarounds and fallbacks everywhere was mindboggling. Also, since tests are also code which has to be maintained in the future, does generating a whole lot of code actually help or not? Of course, the amount of code involved wasn&amp;rsquo;t exactly a big help when I really had to dig into the code to debug a gnarly issue, and the way the test was written didn&amp;rsquo;t exactly help!&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want to go back to the previous state of things when I had only backend tests though, so I had to find a better way.&lt;/p&gt;
&lt;h2 id="playwright-codegen-to-the-rescue"&gt;&lt;a class="toclink" href="#playwright-codegen-to-the-rescue"&gt;Playwright codegen to the rescue&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I already had some experience with &lt;a href="https://playwright.dev/docs/codegen-intro"&gt;Playwright codegen&lt;/a&gt;, having used it for testing some complex onboarding code for a client project I worked on a few years back, so I was already aware of the fact that I could run the browser, click through the interface myself, and playwright would actually generate some of the required Python code for the test itself.&lt;/p&gt;
&lt;p&gt;This worked fine for a project, but what about libraries? There, I generally do not have a full project ready to be used with &lt;code&gt;./manage.py runserver&lt;/code&gt; and Playwright. So, I needed a different solution: Running Playwright from inside a test!&lt;/p&gt;
&lt;p&gt;If your test uses the &lt;a href="https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.LiveServerTestCase"&gt;&lt;code&gt;LiveServerTestCase&lt;/code&gt;&lt;/a&gt; all you have to do is insert the following lines into the body of your test, directly after creating the necessary data in the database (using fixtures, or probably better yet using something like &lt;a href="https://pypi.org/project/factory-boy/"&gt;factory-boy&lt;/a&gt;):&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Live server URL: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;live_server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;playwright&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;codegen&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;live_server_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/admin/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Press Enter when done with codegen...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or of course the equivalent invocation using &lt;code&gt;live_server.url&lt;/code&gt; when using the &lt;code&gt;live_server&lt;/code&gt; fixture from &lt;a href="https://pytest-django.readthedocs.io/en/latest/helpers.html#live-server"&gt;pytest-django&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Of course Tim &lt;a href="https://mastodon.social/@CodenameTim/115096138737981083"&gt;pointed me towards &lt;code&gt;page.pause()&lt;/code&gt;&lt;/a&gt;. I didn&amp;rsquo;t know about it; I think it&amp;rsquo;s even better than what I discovered, so I&amp;rsquo;m probably going to use that one instead. I still think writing down the discovery process makes sense.&lt;/p&gt;
&lt;p&gt;So now, when &lt;code&gt;LiveServerTestCase&lt;/code&gt; is already set up and I already have a sync Playwright context lying around, I can just do:&lt;/p&gt;
&lt;div class="chl"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="tldr"&gt;&lt;a class="toclink" href="#tldr"&gt;TLDR&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Claude Code helped getting me to get off the ground with adding end to end tests to my projects. Now, my tests are better because &amp;ndash; at least for now &amp;ndash; I&amp;rsquo;m not using AI tools anymore.&lt;/p&gt;</summary></entry><entry><title>Weeknotes (2025 week 37)</title><link href="https://406.ch/writing/weeknotes-2025-week-37/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2025-week-37/</id><published>2025-09-10T12:00:00Z</published><updated>2025-09-10T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2025-week-37"&gt;&lt;a class="toclink" href="#weeknotes-2025-week-37"&gt;Weeknotes (2025 week 37)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;I&amp;rsquo;m having a slow week after the last wisdom tooth extraction. Finally! I&amp;rsquo;m slowly recuperating from that.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m trying to split up the blog posts a bit and writing more standalone pieces instead of putting everything into weeknotes. Publishing more focussed pieces sounds like a good thing and should also help me with finding my own writing later.&lt;/p&gt;
&lt;h2 id="releases"&gt;&lt;a class="toclink" href="#releases"&gt;Releases&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-content-editor/"&gt;django-content-editor 8.0.2&lt;/a&gt;: I fixed the ordering calculation in the cloning functionality; the tests are a bit too forgiving for my taste now but I just can&amp;rsquo;t figure out why the gap for inserting cloned items is sometimes larger than it should be. It doesn&amp;rsquo;t matter though, since ordering values do not have any significance, they only have to provide a definite ordering for content items.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor 0.18.2&lt;/a&gt;: Cleaned up the documentation after the 0.18 cleanup (where automatic dependency management has been removed), fixed table styles when using dark mode.&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>Weeknotes (2025 week 35)</title><link href="https://406.ch/writing/weeknotes-2025-week-35/" rel="alternate" /><id>https://406.ch/writing/weeknotes-2025-week-35/</id><published>2025-08-29T12:00:00Z</published><updated>2025-08-29T12:00:00Z</updated><summary type="html">&lt;h1 id="weeknotes-2025-week-35"&gt;&lt;a class="toclink" href="#weeknotes-2025-week-35"&gt;Weeknotes (2025 week 35)&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Summer was and is nice. The hot days seem to be over (for now), but in the last
years summer hasn&amp;rsquo;t really left until the end of September, so we&amp;rsquo;ll see. I
personally like the warm weather but I really hoped that our leaders were
smarter. The &lt;a href="https://406.ch/writing/category-climate/"&gt;climate emergency&lt;/a&gt;
could be seen from far away. The pigheadedness is hard to stomach. And of
course it&amp;rsquo;s not the only problem we&amp;rsquo;re facing as humanity at all.&lt;/p&gt;
&lt;h2 id="releases"&gt;&lt;a class="toclink" href="#releases"&gt;Releases&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I did some longer-form writing about two of the releases here: &lt;a href="https://406.ch/writing/menu-improvements-in-django-prose-editor/"&gt;Menu improvements in django-prose-editor&lt;/a&gt; and &lt;a href="https://406.ch/writing/django-content-editor-cloning/"&gt;django-content-editor now supports cloning of content&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-debug-toolbar/"&gt;django-debug-toolbar 6.0&lt;/a&gt;:
  We have released a new version of the toolbar which supports persisting
  debugging data to the database. This is especially useful when using ASGI,
  because we cannot use threadlocal storage for this data then.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-prose-editor/"&gt;django-prose-editor 0.18&lt;/a&gt;:
  Reworked the menu system to support dropdowns, not just button groups. Added
  a custom &lt;code&gt;TextClass&lt;/code&gt; extension which allows adding classes to spans and a
  &lt;code&gt;NodeClass&lt;/code&gt; extension which allows adding classes to nodes. Tiptap supports
  adding arbitrary styles, I&amp;rsquo;d rather limit this a bit more and only offer
  predefined CSS classes.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-content-editor/"&gt;django-content-editor 8.0&lt;/a&gt;:
  Added support for cloning content. Made the region tabs stick to the top of
  the browser window.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-cabinet/"&gt;django-cabinet 0.18&lt;/a&gt;: Improved the
  folder deletion interface slightly.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/django-mptt/"&gt;django-mptt 0.18&lt;/a&gt;: I still seem to be maintaining the project even though I officially marked the project as unmaintained &lt;a href="https://github.com/django-mptt/django-mptt/commit/6f6c1c485f3adc1d579f8d22e0279ce1d52334f6"&gt;more than 4 years ago&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;</summary></entry><entry><title>django-content-editor now supports cloning of content</title><link href="https://406.ch/writing/django-content-editor-cloning/" rel="alternate" /><id>https://406.ch/writing/django-content-editor-cloning/</id><published>2025-08-27T12:00:00Z</published><updated>2025-08-27T12:00:00Z</updated><summary type="html">&lt;h1 id="django-content-editor-now-supports-cloning-of-content"&gt;&lt;a class="toclink" href="#django-content-editor-now-supports-cloning-of-content"&gt;django-content-editor now supports cloning of content&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id="what-is-the-content-editor"&gt;&lt;a class="toclink" href="#what-is-the-content-editor"&gt;What is the content editor?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Django’s builtin admin application provides a really good and usable
administration interface for creating and updating content.
&lt;code&gt;django-content-editor&lt;/code&gt; extends Django&amp;rsquo;s inlines mechanism with an interface
and tools for managing and rendering heterogenous collections of content as are
often necessary for content management systems.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://feinheit.ch"&gt;We&lt;/a&gt; are using &lt;a href="https://pypi.org/project/django-content-editor/"&gt;django-content-editor&lt;/a&gt; in basically all projects, as a part of &lt;a href="https://pypi.org/project/feincms3/"&gt;feincms3&lt;/a&gt;. The content editor is used not only for building page content, but also for blog entries, for &lt;a href="https://github.com/feincms/feincms3-forms"&gt;building multi-step intelligent form wizards&lt;/a&gt;, for &lt;a href="https://feinheit.ch/projekte/finance-mission-world/"&gt;learning units&lt;/a&gt; and even to digitize teaching materials for schools, including static and interactive content.&lt;/p&gt;
&lt;p&gt;The great thing about it is that it enables us to edit complex content inside Django&amp;rsquo;s administration interface without trying to replace it with a completely separate interface, as some other more well-known Django-based CMS want to do.&lt;/p&gt;
&lt;h2 id="cloning-content"&gt;&lt;a class="toclink" href="#cloning-content"&gt;Cloning content&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The complexity of managed content has grown a bit, especially since we introduced support for &lt;a href="https://406.ch/writing/django-content-editor-now-supports-nested-sections/"&gt;nesting sections&lt;/a&gt;. Teaching materials are often available in several learning levels, with only minor differences between them. Unfortunately, the differences aren&amp;rsquo;t purely additive: It&amp;rsquo;s not the case that higher levels just have more materials available. Otherwise, we&amp;rsquo;d probably have used a level on content items to hide content which shouldn&amp;rsquo;t be shown to students. Content is sometimes totally different. Because of this we&amp;rsquo;re using content editor&amp;rsquo;s &lt;strong&gt;regions&lt;/strong&gt; for the learning level, one region per level.&lt;/p&gt;
&lt;p&gt;Even then, the basic structure is often the same and building that manually for all levels is annoying at best. That&amp;rsquo;s why I finally got the occasion to add support for cloning content between regions to the editor.&lt;/p&gt;
&lt;p&gt;Of course, cloning should also take the other features into account and allow selecting sections as a whole instead of having to select individual items. Here&amp;rsquo;s a screenshot of the current interface:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot showing the content cloning interface" src="/assets/20250827-content-editor-cloning.png" /&gt;&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;&lt;a class="toclink" href="#closing-thoughts"&gt;Closing thoughts&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m still really happy with the content editor; I wish the Django admin would look a little bit nicer because then people would probably be more encouraged to actually learn how powerful it is. The first impression is unfortunately that it looks old and a bit too technical, but in my experience working with many many customers it&amp;rsquo;s not really the case. Most people are immediately able to work with it and find the interface well structured and appreciate the no bullshit attitude, because working with it really is efficient.&lt;/p&gt;</summary></entry></feed>