<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Perl Hacks</title>
	<atom:link href="https://perlhacks.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://perlhacks.com</link>
	<description>Just another Perl Hacker&#039;s blog</description>
	<lastBuildDate>Sun, 23 Aug 2026 12:26:30 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>
<site xmlns="com-wordpress:feed-additions:1">40678030</site>	<item>
		<title>Accidentally Building a CPAN Web Platform</title>
		<link>https://perlhacks.com/2026/08/accidentally-building-a-cpan-web-platform/</link>
					<comments>https://perlhacks.com/2026/08/accidentally-building-a-cpan-web-platform/#respond</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 23 Aug 2026 12:26:30 +0000</pubDate>
				<category><![CDATA[CPAN]]></category>
		<category><![CDATA[cpan]]></category>
		<category><![CDATA[documentation]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[markdown]]></category>
		<category><![CDATA[metacpan]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[website]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2503</guid>

					<description><![CDATA[<p>Sometimes you design a system. And sometimes you make a series of vaguely related decisions over five years and eventually realise that, quite by accident, you&#8217;ve designed a system. I think I&#8217;ve just done the latter. Back in February 2021, Gabor Szabo wrote an article called Perl modules with their own web sites. He pointed [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/08/accidentally-building-a-cpan-web-platform/">Accidentally Building a CPAN Web Platform</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Sometimes you design a system. And sometimes you make a series of vaguely related decisions over five years and eventually realise that, quite by accident, you&#8217;ve designed a system.</p>
<p>I think I&#8217;ve just done the latter.</p>
<p>Back in February 2021, Gabor Szabo wrote an article called <a href="https://dev.to/szabgab/perl-modules-with-their-own-web-site-2gmo"><em>Perl modules with their own web sites</em></a>. He pointed out that a number of the larger Perl projects had websites of their own, in addition to their pages on MetaCPAN, and wondered whether this was something that more CPAN modules could benefit from.</p>
<p>I liked the idea. In fact, I liked it enough that later the same day I wrote <a href="https://dev.to/davorg/simple-cpan-module-web-sites-with-github-pages-29ka"><em>Simple CPAN module web sites with GitHub Pages</em></a>.</p>
<p>My argument was basically that GitHub Pages made this almost embarrassingly easy. If your CPAN distribution was already on GitHub, then you already had almost everything you needed. Turn on GitHub Pages, put some content in the repository, and a few minutes later your module could have a website.</p>
<p>At the time I wrote:</p>
<blockquote><p>I think that anything which increases the amount of up-to-date Perl content on the web is a good idea.</p></blockquote>
<p>Five years later, I still think that&#8217;s true.</p>
<h2>Attempt one: automate everything</h2>
<p>Of course, being a programmer, I immediately decided that something which took less than five minutes clearly needed automating.</p>
<p>A few months later I released <a href="https://metacpan.org/dist/App-CPANModuleSite"><code inline="">App::CPANModuleSite</code></a>, which attempted to generate a useful website for a CPAN distribution. I wrote about it in <a href="https://dev.to/davorg/easier-web-sites-for-cpan-modules-1nn4"><em>Easier web sites for CPAN modules</em></a>.</p>
<p>It worked.</p>
<p>Nobody used it.</p>
<p>That&#8217;s fine. Not every experiment needs to become an indispensable part of the Perl ecosystem. <code inline="">App::CPANModuleSite</code> is one of those projects that seemed like a good idea at the time but didn&#8217;t really go anywhere.</p>
<p>But the underlying idea never quite went away.</p>
<h2>Meanwhile, the modules moved house</h2>
<p>A couple of years later I made another, apparently unrelated, change.</p>
<p>For years, the Git repositories for my CPAN distributions had lived alongside everything else in my personal GitHub account. In 2023 I decided that wasn&#8217;t particularly useful, so I created a GitHub organisation called <a href="https://github.com/davorg-cpan"><code inline="">davorg-cpan</code></a> and started moving my CPAN repositories there.</p>
<p>I wrote about the reasons for doing that in <a href="https://perlhacks.com/2023/08/github-organisations/"><em>GitHub Organisations</em></a>.</p>
<p>It separated my CPAN work from all the other random things in my GitHub account and gave the distributions a useful identity of their own.</p>
<p>And, importantly for what happened later, GitHub organisations get GitHub Pages sites just like users do.</p>
<p>So I created one.</p>
<p>Eventually I gave the organisation site a custom domain: it lives at <a href="https://cpan.perlhacks.com/">cpan.perlhacks.com</a></p>
<p>I didn&#8217;t do very much with it. It existed. It listed some modules. That was about it.</p>
<p>But one useful piece of infrastructure was now in place.</p>
<h2>Your README is already a website</h2>
<p>Earlier this year I returned to another idea I&#8217;d been playing with: for many small software projects, the README already contains almost everything you would want on a simple project website.</p>
<p>So why maintain both?</p>
<p>I wrote a GitHub Action called <a href="https://github.com/marketplace/actions/readme-to-index-html"><code inline="">readme-to-index</code></a>. I described the idea in <a href="https://dev.to/davorg/your-readme-is-already-a-website-dg7"><em>Your README is Already a Website</em></a>.</p>
<p>It takes a repository&#8217;s README, runs it through <a href="https://pandoc.org/">Pandoc</a>, adds <a href="https://simplecss.org/">Simple.css</a> and publishes the result as a very simple single-page website using GitHub Pages.</p>
<p>There&#8217;s deliberately not much to it. No Jekyll configuration. No theme to maintain. No second copy of the documentation. The README remains the source of truth. Push a change to the README and the website changes with it.</p>
<p>I&#8217;ve now used it on several of my CPAN distributions, including <a href="https://cpan.perlhacks.com/app-httpthis/"><code inline="">App::HTTPThis</code></a>, <a href="https://cpan.perlhacks.com/moox-role-seotags/"><code inline="">MooX::Role::SEOTags</code></a> and <a href="https://cpan.perlhacks.com/webserver-dirindex/"><code inline="">WebServer::DirIndex</code></a>.</p>
<p>And that&#8217;s when I noticed something rather pleasing.</p>
<h2>The bit I hadn&#8217;t planned</h2>
<p>A GitHub organisation Pages site normally lives at:</p>
<p><code inline="">https://organisation.github.io/</code></p>
<p>A project site within that organisation lives underneath it:</p>
<p><code inline="">https://organisation.github.io/project/</code></p>
<p>But GitHub Pages also lets the organisation site have a custom domain.</p>
<p>Mine is:</p>
<p><code inline="">https://cpan.perlhacks.com/</code></p>
<p>And the individual project sites don&#8217;t have their own <code inline="">CNAME</code> files. That means they simply inherit the organisation site&#8217;s domain. So the module sites automatically appear at addresses like:</p>
<p><code inline="">https://cpan.perlhacks.com/app-httpthis/</code></p>
<p><code inline="">https://cpan.perlhacks.com/moox-role-seotags/</code></p>
<p><code inline="">https://cpan.perlhacks.com/webserver-dirindex/</code></p>
<p>No additional DNS configuration. No new domains. No <code inline="">CNAME</code> management for individual projects. Move a CPAN distribution into the organisation, add the <code inline="">readme-to-index</code> workflow, enable GitHub Pages, and it becomes another part of the same family of sites.</p>
<p>And suddenly I realised I&#8217;d accidentally built almost exactly the thing I was looking for in 2021.</p>
<h2>A CPAN neighbourhood</h2>
<p>MetaCPAN remains the canonical place to find information about my distributions. I&#8217;m certainly not trying to replace it.</p>
<p>But MetaCPAN has to present every distribution in approximately the same way. That&#8217;s one of its strengths.</p>
<p>A project&#8217;s own site serves a slightly different purpose. It&#8217;s somewhere I can send someone who just wants to know what this thing is and how to use it. It can emphasise examples and introductions rather than package metadata. And, perhaps most importantly, it gives the project a URL that looks like it belongs to the project rather than to a hosting service.</p>
<p>What I particularly like about the current arrangement is that the individual sites also clearly belong together.</p>
<p>They&#8217;re not a collection of unrelated domains. They&#8217;re a little CPAN neighbourhood: <a href="https://cpan.perlhacks.com/">cpan.perlhacks.com </a>with each distribution living underneath it.</p>
<p>The organisation provides the namespace and domain. GitHub provides the hosting and deployment infrastructure. <code inline="">readme-to-index</code> turns documentation I&#8217;m already maintaining into the website.</p>
<p>There&#8217;s very little left to maintain.</p>
<h2>Five years of accidental architecture</h2>
<p>None of this was planned.</p>
<p>In 2021, I experimented with GitHub Pages because Gabor suggested that more CPAN modules should have websites.</p>
<p>Then I wrote a tool to automate that and discovered that nobody particularly wanted it.</p>
<p>In 2023, for largely unrelated reasons, I moved my CPAN repositories into a GitHub organisation.</p>
<p>Then I created an organisation Pages site.</p>
<p>Then I attached a custom domain to it.</p>
<p>Then, earlier this year, I wrote a GitHub Action for turning READMEs into tiny project websites.</p>
<p>Each decision made sense on its own.</p>
<p>Put them together and I now have a system where adding a website to one of my CPAN distributions is almost trivial, and every new site automatically becomes part of a consistent collection under <code inline="">cpan.perlhacks.com</code>.</p>
<p>Sometimes architecture comes from diagrams, planning meetings and carefully written requirements. And sometimes you look back at five years of tinkering and discover that you&#8217;ve accidentally built something useful.</p>
<p>So over the next few weeks I&#8217;m going to start adding <code inline="">readme-to-index</code> to more of the distributions in the <code inline="">davorg-cpan</code> organisation and putting a bit more effort into the organisation site itself.</p>
<p>But perhaps you could do the same thing.</p>
<p>I bet your CPAN distribution already has a README. It might even be written in Markdown. If so, why not spend fifteen minutes turning it into a website?</p>
<p>You don&#8217;t need to design anything. You don&#8217;t need to maintain another copy of your documentation. And you don&#8217;t even need to pay for hosting.</p>
<p>Your README is already a website.</p>
<p>You just need to publish it.</p><p>The post <a href="https://perlhacks.com/2026/08/accidentally-building-a-cpan-web-platform/">Accidentally Building a CPAN Web Platform</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/08/accidentally-building-a-cpan-web-platform/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2503</post-id>	</item>
		<item>
		<title>The Future Isn&#8217;t Evenly Distributed</title>
		<link>https://perlhacks.com/2026/08/the-future-isnt-evenly-distributed/</link>
					<comments>https://perlhacks.com/2026/08/the-future-isnt-evenly-distributed/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sat, 01 Aug 2026 15:13:07 +0000</pubDate>
				<category><![CDATA[Business]]></category>
		<category><![CDATA[archaeology]]></category>
		<category><![CDATA[history]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[softwareengineering]]></category>
		<category><![CDATA[technicaldebt]]></category>
		<category><![CDATA[webdev]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2492</guid>

					<description><![CDATA[<p>&#8220;The future is already here — it&#8217;s just not evenly distributed.&#8221;— William Gibson I was reminded of that quote this week when I received a support request for a Perl CGI script I helped write about twenty-five years ago. If you&#8217;re a software developer, your immediate reaction is probably the same as mine was. &#8220;A [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/08/the-future-isnt-evenly-distributed/">The Future Isn’t Evenly Distributed</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<blockquote>
<p data-start="243" data-end="566"><em data-start="208" data-end="274">&#8220;The future is already here — it&#8217;s just not evenly distributed.&#8221;</em><br data-start="274" data-end="277" />— William Gibson</p>
</blockquote>
<p class="PDq2pG_selectionAnchorContainer" data-start="297" data-end="435">I was reminded of that quote this week when I received a support request for a Perl CGI script I helped write about twenty-five years ago.</p>
<p data-start="437" data-end="526">If you&#8217;re a software developer, your immediate reaction is probably the same as mine was.</p>
<p data-start="528" data-end="559"><em data-start="528" data-end="559">&#8220;A Perl CGI script? In 2026?&#8221;</em></p>
<p data-start="561" data-end="698">But the more I thought about it, the more I realised that my surprise said more about me than it did about the person who sent the email.</p>
<h2 data-section-id="1olfbgj" data-start="700" data-end="739">We assume everyone lives where we do</h2>
<p data-start="741" data-end="1051">If you spend your days reading Hacker News, watching conference talks and experimenting with AI coding tools, it&#8217;s easy to believe that everyone is building applications in containers, deploying them to cloud platforms and arguing about which JavaScript framework will replace the current JavaScript framework.</p>
<p data-start="1053" data-end="1070">That&#8217;s our world. It isn&#8217;t everyone else&#8217;s.</p>
<p class="PDq2pG_selectionAnchorContainer" data-start="280" data-end="376">One of the hardest lessons for any software engineer is accepting that <strong data-start="351" data-end="375">we are not our users</strong>.</p>
<p data-start="381" data-end="589">We spend our time talking to other software engineers, reading software engineering blogs and attending software engineering conferences. It&#8217;s easy to conclude that the whole world is moving at the same pace.</p>
<p data-start="594" data-end="603">It isn&#8217;t.</p>
<p data-start="608" data-end="808">Most businesses don&#8217;t think in terms of technology at all. They think in terms of problems. The website is there to answer customers&#8217; questions when the office is closed. If it does that reliably, they&#8217;re happy.</p>
<p data-start="1099" data-end="1376">Most businesses don&#8217;t employ software engineers. They employ accountants, manufacturers, retailers, charities, estate agents and consultants. Their website isn&#8217;t their product. It&#8217;s simply another business expense, somewhere between the telephone system and the office printer.</p>
<p data-start="1378" data-end="1624">They&#8217;re not interested in Kubernetes. They don&#8217;t care whether their site is hosted on Cloud Run or a Raspberry Pi in someone&#8217;s garage. They certainly aren&#8217;t waking up each morning wondering whether they should migrate to the latest web framework.</p>
<p data-start="1626" data-end="1692">They just want the website to keep doing what it&#8217;s done for years.</p>
<h2 data-section-id="13h0mgh" data-start="1694" data-end="1731">A support request from another era</h2>
<p data-start="1733" data-end="1789">The email came from the CEO of a small European company.</p>
<p data-start="1791" data-end="2054">Their contact form had suddenly stopped working and, after a little digging, they&#8217;d discovered that the software processing it was NMS FormMail, part of the NMS project that I helped run around the turn of the millennium. They wanted to know whether I could help.</p>
<p data-start="2056" data-end="2093">The diagnosis took about two minutes.</p>
<p data-start="2095" data-end="2319">When the form was submitted, the browser displayed the Perl source code instead of executing it. That meant the program wasn&#8217;t failing. It wasn&#8217;t even running. The web server was simply serving the CGI script as a text file.</p>
<p data-start="2321" data-end="2446">I suggested that the hosting company had almost certainly migrated them to a new server where Perl CGI was no longer enabled.</p>
<p data-start="2448" data-end="2479">A few hours later they replied.</p>
<p data-start="2481" data-end="2516">That was exactly what had happened.</p>
<h2 data-section-id="smr7d" data-start="2518" data-end="2552">The software wasn&#8217;t the problem</h2>
<p data-start="2554" data-end="2587">There&#8217;s an important lesson here.</p>
<p data-start="2589" data-end="2624">Nothing in the software had broken.</p>
<p data-start="2626" data-end="2817">The contact form had apparently been working happily for years. The Perl compiler hadn&#8217;t suddenly forgotten how to execute CGI programs. NMS hadn&#8217;t developed a bug after twenty-five years.</p>
<p data-start="2819" data-end="2874">The thing that had changed was the hosting environment.</p>
<p data-start="2876" data-end="3234">Twenty years ago, almost every budget Linux web host proudly advertised support for Perl CGI. Today, very few do. There simply isn&#8217;t enough demand to justify it. The overwhelming majority of customers want WordPress, PHP or a managed website builder, and hosting companies naturally invest in the technologies that almost all of their customers actually use.</p>
<p data-start="3236" data-end="3516">Supporting Perl CGI today means maintaining packages that very few customers need, testing an increasingly niche platform and employing engineers with knowledge that has become correspondingly rare. That&#8217;s difficult to justify when you&#8217;re selling hosting for a few pounds a month.</p>
<p data-start="3518" data-end="3551">The platform quietly disappeared.</p>
<h2 data-section-id="1oiw0z6" data-start="3553" data-end="3602">Legacy software isn&#8217;t necessarily bad software</h2>
<p data-start="3604" data-end="3698">Software developers often talk about &#8220;legacy software&#8221; as though it&#8217;s automatically a problem.</p>
<p data-start="3700" data-end="3716">Sometimes it is. But very often, legacy software is simply software that solved a problem so well nobody felt the need to replace it.</p>
<p data-start="3836" data-end="3862">Think about the economics.</p>
<p data-start="3864" data-end="3985">Suppose you&#8217;d phoned this company every year for the last twenty years and offered to modernise their website for £2,000.</p>
<p data-start="3987" data-end="4016">Why would they have said yes? The website worked. The contact form worked. Customers could get in touch. There were no new features they needed.</p>
<p data-start="4137" data-end="4277">From the business&#8217;s perspective, spending thousands of pounds to replace something that wasn&#8217;t causing a problem would have been irrational.</p>
<p class="PDq2pG_selectionAnchorContainer" data-start="1082" data-end="1144">From my perspective, the website looked like a museum exhibit. From theirs, it looked like a website.</p>
<p data-start="1192" data-end="1251">That&#8217;s the difference between software engineers and users. We see technology. They see outcomes.</p>
<p data-start="1302" data-end="1520">That&#8217;s why we&#8217;re often so bad at predicting what people will actually adopt. We assume they&#8217;ll make technical decisions for technical reasons. In reality, most businesses make technology decisions for economic reasons.</p>
<h2 data-section-id="1e7k2j" data-start="4445" data-end="4478">Good engineering has a context</h2>
<p data-start="4480" data-end="4525">One part of the story particularly amused me.</p>
<p data-start="4527" data-end="4599">The customer mentioned that they&#8217;d actually migrated <strong data-start="4580" data-end="4586">to</strong> NMS in 2023.</p>
<p data-start="4601" data-end="4631">Think about that for a moment.</p>
<p data-start="4633" data-end="4777">NMS was created around 2000 as a modern replacement for Matt&#8217;s Script Archive. More than twenty years later, somebody was still upgrading to it.</p>
<p data-start="4779" data-end="4852">That tells us something about the extraordinary lifespan of web software.</p>
<p data-start="4854" data-end="4946">It also reminded me that engineering decisions only make sense in the context of their time.</p>
<p data-start="4948" data-end="5265">One of the improvements NMS made over Matt&#8217;s original scripts was to use <code data-start="5021" data-end="5029">CGI.pm</code> wherever possible. Back then, that was exactly what experienced Perl programmers recommended. <code data-start="5124" data-end="5132">CGI.pm</code> was part of the standard Perl distribution, thoroughly tested and far preferable to everyone writing their own CGI parameter parser.</p>
<p data-start="5267" data-end="5581">Today, <code data-start="5274" data-end="5282">CGI.pm</code> isn&#8217;t installed by default with Perl. It still exists, but as a separate package. Ironically, if you somehow found a web host that still supported Perl CGI, there&#8217;s a reasonable chance that Matt&#8217;s original scripts would run immediately while NMS would first complain that <code data-start="5555" data-end="5563">CGI.pm</code> wasn&#8217;t installed.</p>
<p data-start="5583" data-end="5628">That doesn&#8217;t mean we made the wrong decision. It means the world changed.</p>
<h2 data-section-id="rufira" data-start="5659" data-end="5687">The future really is here</h2>
<p data-start="5689" data-end="5781">William Gibson&#8217;s quote is often used when talking about AI, robotics or autonomous vehicles.</p>
<p data-start="5783" data-end="5840">I think it&#8217;s even more applicable to enterprise software.</p>
<p data-start="5842" data-end="5861">The future is here.</p>
<p data-start="5863" data-end="6090">It&#8217;s just that it exists alongside Visual Basic applications, Microsoft Access databases, COBOL systems, Classic ASP websites and, until last week, a Perl CGI script quietly sitting in a <code data-start="6050" data-end="6059">cgi-bin</code> directory somewhere in Europe.</p>
<p data-start="6092" data-end="6151">Those systems aren&#8217;t still around because people love them.</p>
<p data-start="6153" data-end="6242">They&#8217;re still around because they continue to solve the problem they were built to solve.</p>
<p data-start="6244" data-end="6430">Software engineers tend to imagine technology replacing technology. Businesses replace technology only when the old technology stops delivering value or the ecosystem around it moves on.</p>
<p data-start="6432" data-end="6466">That&#8217;s exactly what happened here. The Perl program didn&#8217;t fail. The web simply stopped expecting it to exist.</p>
<h2 data-section-id="r01x55" data-start="6546" data-end="6561">Looking back</h2>
<p class="PDq2pG_selectionAnchorContainer" data-start="1677" data-end="1723">Looking back, the email was a useful reminder.</p>
<p data-start="1728" data-end="1883">Software engineers spend our careers chasing the future. We get excited by new languages, new frameworks and new deployment models. That&#8217;s part of the job.</p>
<p data-start="1888" data-end="1935">But most of the world isn&#8217;t chasing the future.</p>
<p data-start="1940" data-end="2021">Most of the world simply wants yesterday&#8217;s software to carry on working tomorrow.</p>
<p data-start="2026" data-end="2215">That&#8217;s why William Gibson&#8217;s observation resonates so strongly. The future really is here. It&#8217;s just concentrated in places like conference talks, GitHub repositories and startup incubators.</p>
<p data-start="2220" data-end="2421">Elsewhere, quietly and without anyone really noticing, there are still Perl CGI scripts processing contact forms, COBOL programs paying pensions and Microsoft Access databases running small businesses.</p>
<p data-start="2426" data-end="2480">They aren&#8217;t there because people are afraid of change.</p>
<p data-start="2485" data-end="2580">They&#8217;re there because, until something around them changes, they continue to solve the problem.</p>
<p data-start="2585" data-end="2843">As software engineers, it&#8217;s worth remembering that <strong data-start="2636" data-end="2660">we are not our users</strong>. The technologies we&#8217;re excited about today won&#8217;t become mainstream simply because they&#8217;re better. They&#8217;ll become mainstream when replacing yesterday&#8217;s solution makes economic sense.</p>
<hr />
<h2 data-start="2585" data-end="2843">Previously</h2>
<p>As I previewed this post, I saw that the &#8220;related articles&#8221; section had found <span style="box-sizing: border-box; margin: 0px; padding: 0px;"><a href="https://perlhacks.com/2024/02/the-present-isnt-evenly-distributed-either/" target="_blank" rel="noopener">the 2024 article &#8220;The Present Isn&#8217;t Evenly Distributed Either</a>.&#8221;</span> I think my opinions on this have become more nuanced. Previously, I wrote &#8220;The present isn&#8217;t evenly distributed&#8221;, and now I&#8217;m adding, &#8220;&#8230;because the economics aren&#8217;t evenly distributed either&#8221;.</p><p>The post <a href="https://perlhacks.com/2026/08/the-future-isnt-evenly-distributed/">The Future Isn’t Evenly Distributed</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/08/the-future-isnt-evenly-distributed/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2492</post-id>	</item>
		<item>
		<title>Remember PerlScript? When Internet Explorer Could Run Perl</title>
		<link>https://perlhacks.com/2026/07/remember-perlscript-when-internet-explorer-could-run-perl/</link>
					<comments>https://perlhacks.com/2026/07/remember-perlscript-when-internet-explorer-could-run-perl/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Thu, 23 Jul 2026 15:12:37 +0000</pubDate>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[activescript]]></category>
		<category><![CDATA[browserwars]]></category>
		<category><![CDATA[history]]></category>
		<category><![CDATA[internetexplorer]]></category>
		<category><![CDATA[webdev]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2487</guid>

					<description><![CDATA[<p>Every now and then I&#8217;m reminded of a technology from the early days of the web and wonder if I imagined it. This week, it was Mohammad using WebAssembly to run Perl in his browser. Which led to me thinking about PerlScript. If you&#8217;re a certain age, and you spent any time developing web applications [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/07/remember-perlscript-when-internet-explorer-could-run-perl/">Remember PerlScript? When Internet Explorer Could Run Perl</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Every now and then I&#8217;m reminded of a technology from the early days of the web and wonder if I imagined it.</p>
<p>This week, it was <a href="https://github.com/manwar/perl-wasm/">Mohammad using WebAssembly to run Perl in his browser</a>. Which led to me thinking about PerlScript.</p>
<p>If you&#8217;re a certain age, and you spent any time developing web applications on Windows in the late 1990s, you&#8217;ll probably remember it. If you&#8217;re younger than that, you&#8217;re quite likely to think I&#8217;m making it up.</p>
<p>Because, for a brief period, Internet Explorer could run Perl inside <code inline="">&lt;script&gt;</code> tags.</p>
<p>Not JavaScript.</p>
<p>Perl.</p>
<p>It sounds absurd now.</p>
<p>It wasn&#8217;t absurd then.</p>
<h2>The browser wars were weird</h2>
<p>It&#8217;s difficult to explain to younger developers just how chaotic the web was in the late 1990s.</p>
<p>Today, if you&#8217;re building a web application, you write HTML, CSS and JavaScript. Every browser understands them. If you invent your own alternatives, you&#8217;re going to have a very lonely website.</p>
<p>None of that had been settled in 1998.</p>
<p>Microsoft and Netscape were engaged in what became known as the Browser Wars. Every release was packed with new features that only worked in one browser. Sometimes the other side copied them. Sometimes they ignored them. Either way, developers were left trying to support both.</p>
<p>We got Java applets. ActiveX controls. VBScript. Browser-specific CSS. Browser-specific DOM APIs. <code inline="">&lt;marquee&gt;</code>. <code inline="">&lt;blink&gt;</code>. Layers. Filters. Behaviours.</p>
<p>And, almost forgotten today, the ability to write client-side Perl.</p>
<p>Seen in that context, PerlScript wasn&#8217;t especially strange.</p>
<p>It was just another way for Microsoft to say, &#8220;Look how much more capable Internet Explorer is.&#8221;</p>
<h2>Active Scripting</h2>
<p>The clever part was that Internet Explorer wasn&#8217;t actually designed to execute JavaScript.</p>
<p>It was designed to host <em>scripting engines</em>.</p>
<p>JavaScript (or, more accurately, Microsoft&#8217;s implementation called JScript) was simply one of the engines that happened to ship with the browser. VBScript was another.</p>
<p>If you installed additional scripting engines, Internet Explorer would happily use those too.</p>
<p>One of those engines came from ActiveState.</p>
<p>It was called PerlScript.</p>
<p>That meant you could write something like this:</p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;script language="PerlScript"&gt;
  $window-&gt;document-&gt;write("&lt;h1&gt;Hello, world!&lt;/h1&gt;");
&lt;/script&gt;</pre><p>Load the page in Internet Explorer on a machine with ActivePerl installed and it would execute exactly as you&#8217;d expect.</p>
<p>The browser itself didn&#8217;t understand Perl.</p>
<p>It simply handed the script to the installed PerlScript engine.</p>
<p>Looking back, it&#8217;s actually a pretty elegant architecture.</p>
<h2>ActiveState&#8217;s role</h2>
<p>Around this time, ActiveState was doing fantastic work making Perl a first-class citizen on Windows.</p>
<p>For years, Windows had been an awkward platform for Perl developers. ActivePerl largely solved that, becoming the standard distribution for Windows users.</p>
<p>PerlScript was one part of a much bigger picture.</p>
<p>The same scripting engine worked not only inside Internet Explorer, but also with Microsoft&#8217;s Active Server Pages (ASP) and Windows Script Host.</p>
<p>Write the engine once and Windows would happily host it wherever scripting was supported.</p>
<p>It was a neat example of software reuse.</p>
<h2>Why it never caught on</h2>
<p>Of course, there was one rather obvious problem.</p>
<p>For your web page to work, your visitors had to:</p>
<ul>
<li>be using Internet Explorer;</li>
<li>be running Windows; and</li>
<li>have PerlScript installed.</li>
</ul>
<p>That isn&#8217;t exactly the recipe for a successful public web application.</p>
<p>For internal corporate systems it made a certain amount of sense. On the open web, JavaScript was always going to win.</p>
<h2>An alternative web</h2>
<p>What I find fascinating is that Microsoft&#8217;s approach wasn&#8217;t obviously wrong.</p>
<p>Imagine an alternative history where browsers had standardised on the idea of pluggable scripting engines.</p>
<p>Instead of asking whether a browser supported JavaScript, we&#8217;d ask whether it had PythonScript, PerlScript or RubyScript installed.</p>
<p>The browser itself would simply dispatch each <code inline="">&lt;script&gt;</code> block to the appropriate interpreter.</p>
<p>In another timeline, perhaps that&#8217;s exactly what happened.</p>
<p>Instead, the web converged on a much simpler idea.</p>
<p>Every browser should speak the same language.</p>
<p>That language happened to be JavaScript.</p>
<h2>Sanity eventually prevailed</h2>
<p>Ironically, the browser wars eventually destroyed themselves.</p>
<p>By 2001 Internet Explorer had effectively won. IE6 reached an extraordinary market share, Netscape faded away and Microsoft slowed browser development to a crawl.</p>
<p>That turned out to be good for nobody.</p>
<p>Firefox arrived in 2004 and reminded Microsoft that browsers were still worth competing over. Chrome followed in 2008 and accelerated the change.</p>
<p>But something important had shifted.</p>
<p>The competition was no longer about inventing proprietary technologies that locked developers into one browser.</p>
<p>Instead, browsers competed on standards compliance, JavaScript performance and developer tools.</p>
<p>There were still plenty of innovations, but they increasingly became standards rather than vendor-specific features.</p>
<p>Frankly, that was a much healthier place for the web to end up.</p>
<h2>Forgotten technology</h2>
<p>One thing that surprised me while looking into PerlScript again was how little remains on the web.</p>
<p>There are countless articles about Flash, Java applets and Silverlight.</p>
<p>PerlScript barely gets a mention.</p>
<p>Perhaps that&#8217;s because hardly anyone used it. Or perhaps it was simply overshadowed by ActiveState&#8217;s much more successful server-side work.</p>
<p>Either way, it feels like an important little footnote in the history of the web.</p>
<p>A reminder that the future wasn&#8217;t always as inevitable as it now appears.</p>
<p>So no, I hadn&#8217;t imagined it.</p>
<p>There really was a time when you could install Perl on a Windows PC, open Internet Explorer and use it to execute Perl in a web page.</p>
<p>Looking back nearly thirty years later, it feels less like a forgotten product and more like a glimpse into an alternative history of the web.</p><p>The post <a href="https://perlhacks.com/2026/07/remember-perlscript-when-internet-explorer-could-run-perl/">Remember PerlScript? When Internet Explorer Could Run Perl</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/07/remember-perlscript-when-internet-explorer-could-run-perl/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2487</post-id>	</item>
		<item>
		<title>Perl School Goes Paperback</title>
		<link>https://perlhacks.com/2026/07/perl-school-goes-paperback/</link>
					<comments>https://perlhacks.com/2026/07/perl-school-goes-paperback/#respond</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sat, 11 Jul 2026 10:42:58 +0000</pubDate>
				<category><![CDATA[Books]]></category>
		<category><![CDATA[books]]></category>
		<category><![CDATA[design patterns]]></category>
		<category><![CDATA[paperback]]></category>
		<category><![CDATA[perl school]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2482</guid>

					<description><![CDATA[<p>Back in 2020 I gave a pretty definitive statement of my position on physical books. When brian d foy interviewed me for perl.com, I said, among other things: &#8220;I do like a real book. But they take up too much space, so I&#8217;ve pretty much completely stopped buying them over the last five years.&#8221; That [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/07/perl-school-goes-paperback/">Perl School Goes Paperback</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Back in 2020 I gave a pretty definitive statement of my position on physical books. When <a href="https://www.perl.com/article/perl-hacks-perl-school-and-the-future-of-perl-publishing/">brian d foy interviewed me for perl.com</a>, I said, among other things: &#8220;I do like a real book. But they take up too much space, so I&#8217;ve pretty much completely stopped buying them over the last five years.&#8221;</p>
<p>That interview was done to promote the sixth Perl School book, <em><a href="https://perlschool.com/books/the-best-of-perl-hacks/">The Best of Perl Hacks</a></em>, so by that point I&#8217;d already been running an ebook-only publishing imprint for three years and was apparently still very comfortable with that decision.</p>
<p>And I stand by the sentiment. I love ebooks. I can carry my entire library in my pocket. Font size, brightness, instant delivery — what&#8217;s not to like? For years I&#8217;d been mildly irritated by publishers who promised me an &#8220;ebook&#8221; and delivered a PDF that was made to be read at a fixed size and fell apart on anything smaller than a large tablet. A proper ebook format reflows. It adapts. It goes where you go.</p>
<p>It seemed obvious, then, that Perl School should be ebook-only. No printing, no warehousing, no minimum print runs, no logistics. We started in 2017 with <em><a href="https://perlschool.com/books/perl-taster/">Perl Taster</a></em> and have published nine titles since then — everything from a <a href="https://perlschool.com/books/selenium-perl/">Selenium testing guide</a> to <a href="https://perlschool.com/books/cucumber/">a book on Cucumber</a> to <a href="https://perlschool.com/books/the-best-of-perl-hacks/">a collection of my best Perl Hacks posts</a>. All ebooks. All on Amazon and/or LeanPub.</p>
<p>It turns out I am not representative of my audience. This is, of course, an important marketing lesson!</p>
<p>This should probably not have come as a surprise. Assuming you are a typical user is the most dangerous thing you can do as a technologist, and I&#8217;ve been in this industry long enough to know that. Yet somehow I still managed to assume that Perl developers were like me: screen-first, happiest with an ebook, not missing physical books at all.</p>
<p>The emails asking whether Perl School books were available in print suggested otherwise. And when Mohammad Sajid Anwar finished <a href="https://perlschool.com/books/design-patterns/">Design Patterns in Modern Perl</a>, the demand for a physical edition was too consistent to ignore. Mohammad is a prolific Perl contributor (you may know him from his work on <a href="https://theweeklychallenge.org/">the Weekly Challenge</a> or <a href="https://perlweekly.com/">Perl Weekly</a>), and his readers, it turns out, wanted something they could put on a shelf.</p>
<p>Amazon&#8217;s Kindle Direct Publishing (KDP) is the system you use to push ebooks into the Amazon machine. But you can also use it to publish paperbacks. And the barrier to entry is genuinely low. You supply a properly-formatted PDF for the interior, a cover image, choose your trim size and paper type, set a price, and wait for a proof copy to arrive in the post. No printing contracts, no warehousing, no minimum print runs. Print-on-demand means each copy is produced when someone orders it. KDP handles fulfilment, returns, and royalty payments. As a publisher, you just need to get the files right.</p>
<p>That said, &#8220;low barrier&#8221; doesn&#8217;t mean &#8220;no work.&#8221;</p>
<p>Our existing build pipeline already took Markdown source through pandoc to produce both a PDF and an EPUB. The PDF looked good on screen and worked fine for the A4 digital edition we&#8217;d been producing. But KDP paperbacks have their own specific requirements: the correct page dimensions for your chosen trim size (we went with 7&#8243;×10&#8243;, roughly the size of a standard technical textbook), margins that meet their minimum gutter requirements, and bleed on the cover image.</p>
<p>The gutter requirement caught me out. KDP scales its minimum gutter—the margin on the binding edge—with the page count of the book, because thicker books flex more when opened and text can disappear into the spine. For a 294-page book, the minimum gutter is half an inch. That&#8217;s more than we&#8217;d originally set, so the CSS needed adjusting. KDP&#8217;s print-readiness checker caught this when we uploaded the first attempt.</p>
<p>We also discovered that some code lines in the benchmark output sections were overflowing the right margin. In an ebook, this is invisible because text reflows to fit the screen. In print, it is not invisible at all—the text simply runs off the edge of the page. The fix was a small Perl script that identifies those specific lines and wraps them for the print build only, leaving the ebook output completely untouched. The same Markdown source produces both formats; the script runs only when building the KDP PDF.</p>
<p>The cover was its own adventure. The ebook cover image had been designed at 1600×2400 pixels, which works well for an ebook. But KDP&#8217;s front cover panel for a 7&#8243;×10&#8243; book with bleed requires a different size and aspect ratio entirely. I had to work out the exact pixel dimensions—accounting for the trim size, the 0.125&#8243; bleed allowance on all outer edges, and the spine width (which KDP calculates from the page count and paper type)—and then extend the original background pattern to fill the larger canvas. That&#8217;s the kind of fiddly arithmetic that&#8217;s easy to get slightly wrong, and I did get it slightly wrong the first time.</p>
<p>I also discovered, while working on the new cover, that the ebook cover had &#8220;Perl&#8221; duplicated in the title. The intended layout has &#8220;Design Patterns in Modern&#8221; on the first line, then a large &#8220;Perl&#8221; wordmark below it—so the full title reads as &#8220;Design Patterns in Modern Perl.&#8221; But the first line had originally been set to &#8220;Design Patterns in Modern Perl,&#8221; meaning the cover actually read &#8220;Design Patterns in Modern Perl Perl.&#8221; This had been the case for six months. The corrected ebook cover is in progress.</p>
<p>For all of this, I had help from Claude Cowork, Anthropic&#8217;s desktop AI tool, which I&#8217;d been using more and more for exactly this kind of technical publishing work. It helped write the CSS changes for the print layout, worked through the cover dimension calculations with me, rendered specific pages of the generated PDF to check for overflows and margin problems, and spotted the &#8220;Perl Perl&#8221; duplication that I had entirely failed to notice.</p>
<p>That last one stings a bit. But in fairness, catching subtle errors in things you&#8217;ve looked at many times is exactly the kind of task where a second pair of eyes—artificial or otherwise—earns its keep. It&#8217;s not the same as having a professional production editor, but for a small independent publisher with no budget for one, it covered a surprising amount of ground.</p>
<p><a href="https://perlschool.com/books/design-patterns/">Design Patterns in Modern Perl is now available as a paperback.</a> The page links to your local Amazon store. It&#8217;s priced at £20 (or local equivalent), which produces a per-copy royalty that compares rather favourably with what the traditional publishers were paying me back in the day—though we&#8217;ll need to actually sell some copies first before that becomes relevant.</p>
<p>This is an experiment. If the book sells—and I&#8217;m deliberately not defining &#8220;sells well,&#8221; because I don&#8217;t want to commit to a number I&#8217;ll later have to explain—then more Perl School titles will follow the same route to print. Mohammad has written a genuinely useful book. If you&#8217;re working with Perl and want to think more carefully about how you structure your code, it&#8217;s worth your time. And if you buy a copy, you&#8217;ll be helping to settle the question of whether Perl developers really do want physical books, or whether I was right all along.</p>
<p>I suspect I wasn&#8217;t.</p><p>The post <a href="https://perlhacks.com/2026/07/perl-school-goes-paperback/">Perl School Goes Paperback</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/07/perl-school-goes-paperback/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2482</post-id>	</item>
		<item>
		<title>How One Pull Request Took App::HTTPThis to Version 1.0</title>
		<link>https://perlhacks.com/2026/07/how-one-pull-request-took-apphttpthis-to-version-1-0/</link>
					<comments>https://perlhacks.com/2026/07/how-one-pull-request-took-apphttpthis-to-version-1-0/#respond</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 05 Jul 2026 09:13:21 +0000</pubDate>
				<category><![CDATA[CPAN]]></category>
		<category><![CDATA[cpan]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[semantic versioning]]></category>
		<category><![CDATA[wsl]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2480</guid>

					<description><![CDATA[<p>Back in January I wrote about App::HTTPThis, the tiny web server I seem to reach for whenever I need to share the contents of a directory over HTTP. It&#8217;s a deliberately simple tool, wrapping Plack::App::DirectoryIndex in a command-line interface that gets out of your way. This week, thanks to a blog post from Olaf Alders, [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/07/how-one-pull-request-took-apphttpthis-to-version-1-0/">How One Pull Request Took App::HTTPThis to Version 1.0</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p><a href="https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/">Back in January I wrote</a> about <a href="https://metacpan.org/pod/App::HTTPThis">App::HTTPThis</a>, the tiny web server I seem to reach for whenever I need to share the contents of a directory over HTTP. It&#8217;s a deliberately simple tool, wrapping <code inline="">Plack::App::DirectoryIndex</code> in a command-line interface that gets out of your way.</p>
<p>This week, thanks to a blog post from Olaf Alders, I discovered that it wasn&#8217;t behaving quite the way I thought it was.</p>
<p>Well&#8230; not quite. It wasn&#8217;t behaving the way I <em>used</em> it.</p>
<p>And that led to three releases in quick succession.</p>
<h2>It wasn&#8217;t really serving &#8220;localhost&#8221;</h2>
<p>When you started <code inline="">http_this</code>, it printed something like this:</p><pre class="urvanov-syntax-highlighter-plain-tag">Exporting '.', available at:

    http://localhost:7007/</pre><p>Like most people, I took that at face value. If a program tells me it&#8217;s serving on <code inline="">localhost</code>, I naturally assume it&#8217;s only accessible from my own machine.</p>
<p>In reality, that wasn&#8217;t always what it was doing.</p>
<p>To be clear, <code inline="">http_this</code> has long had a <code inline="">--host</code> option. If you explicitly supplied a host address, everything behaved exactly as you&#8217;d expect—it binds to the named network interface. The surprising behaviour only occurred when <code inline="">--host</code> was omitted—which, as it happens, is exactly how I always used the tool.</p>
<p>In that case, the server actually listened on every network interface (<code inline="">0.0.0.0</code>). On many machines, a firewall would prevent anyone else from connecting, so most users would never notice. But the output was misleading. The software was saying one thing while doing another.</p>
<p>Olaf spotted this, wrote about it in his post <a href="https://www.olafalders.com/2026/06/30/keep-it-local/"><em>Keep it Local</em></a>, and submitted a pull request.</p>
<h2>The first fix</h2>
<p>Interestingly, Olaf&#8217;s pull request didn&#8217;t change the server&#8217;s behaviour. It simply made the output truthful.</p>
<p>If the server was listening on every interface, then the startup message should say so, together with a warning that other machines on the local network may be able to connect.</p>
<p>That was a worthwhile improvement in its own right.</p>
<p>But reviewing the change immediately prompted a more important question.</p>
<h2>Should this really be the default?</h2>
<p>Once the output accurately described the behaviour, I had to ask whether the behaviour itself was actually sensible.</p>
<p>For the vast majority of people, <code inline="">http_this</code> is a quick way to preview a local website, browse some generated documentation, or inspect the output from a static site generator.</p>
<p>None of those use cases requires the server to be visible to every machine on the local network.</p>
<p>So in the next release, I changed the default. If you don&#8217;t specify <code inline="">--host</code>, <code inline="">http_this</code> now binds to <code inline="">127.0.0.1</code>.</p>
<p>If you genuinely want the previous behaviour, you can now ask for it explicitly using <code inline="">--all</code>. That option also has an alias: <code inline="">--promiscuous</code>.</p>
<p>I rather like that alias. It&#8217;s technically accurate, but it also makes you stop for a moment and think about whether exposing your server on every network interface is really what you intended.</p>
<p>To me, that&#8217;s a much better design. Software should generally do the safest reasonable thing unless the user explicitly asks for something different.</p>
<h2>Breaking changes aren&#8217;t always bad</h2>
<p>Changing a long-standing default like this is, by definition, a breaking change. Anyone who had come to rely on the previous behaviour would suddenly find that their workflow had changed.</p>
<p>That&#8217;s exactly what semantic versioning is for.</p>
<p>Rather than quietly slipping the change into a minor release, I bumped the major version number and released App::HTTPThis 1.0.0. The module has taken rather longer than most to reach a 1.0.0 release, but changing a long-established default seemed like an appropriate milestone.</p>
<p>I also made sure the documentation was very explicit about the behavioural change so existing users wouldn&#8217;t be caught by surprise.</p>
<p>(There&#8217;s now a 1.0.1 release too, but that&#8217;s just a small follow-up fix.)</p>
<h2>Unfortunately, the first person I broke was me</h2>
<p>I was feeling rather pleased with myself.</p>
<p>Then I tried using <code inline="">http_this</code> from WSL.</p>
<p>One of the nice things about WSL is being able to run development tools under Linux while using a browser running on Windows. That&#8217;s a workflow I use every day.</p>
<p>Unfortunately, binding to <code inline="">127.0.0.1</code> inside WSL isn&#8217;t quite the same thing as making the server available to Windows. By fixing the default behaviour, I&#8217;d accidentally made my own development workflow considerably less convenient.</p>
<p>Fortunately, I didn&#8217;t need to back out the security improvement.</p>
<p>Instead, I added a <code inline="">--wsl</code> option (which can also be enabled in your configuration file). Rather than listening on every interface, it determines the appropriate WSL network address and binds specifically to that interface instead.</p>
<p>The result is that <code inline="">http_this</code> now has three distinct modes:</p>
<ul>
<li>the default, which binds only to <code inline="">127.0.0.1</code>;</li>
<li><code inline="">--wsl</code>, which binds to the interface Windows can actually reach; and</li>
<li><code inline="">--all</code> (or <code inline="">--promiscuous</code>), which exposes the server on every network interface.</li>
</ul>
<p>That feels like a much better balance between security and convenience.</p>
<h2>A nice example of open source collaboration</h2>
<p>One thing I particularly like about this sequence of events is how neatly it illustrates the way good open source software evolves.</p>
<p>Olaf didn&#8217;t arrive with a proposal to redesign the module. He noticed that the program&#8217;s output didn&#8217;t accurately describe what it was doing and submitted a focused pull request to correct that.</p>
<p>Reviewing his contribution naturally led me to question whether the underlying behaviour was actually the right default. Fixing that exposed an edge case in my own WSL workflow, which in turn led to a better solution for WSL users.</p>
<p>Looking back over the week, one small pull request ended up making the module more honest about what it was doing, safer by default, and more convenient to use in one of my favourite development environments.</p>
<p>Not bad for a week&#8217;s work.</p>
<p>Sometimes the best bug reports don&#8217;t just fix one bug. They make you realise there&#8217;s a better way for the software to behave.</p>
<p>Oh, and if reading this post makes you think I&#8217;m not really someone you wouldn&#8217;t want to trust with your network configuration—you&#8217;re probably right!</p>
<h2>Further reading</h2>
<ul data-spread="false">
<li>My original article: <a href="https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/"><strong>App::HTTPThis – the Tiny Web Server I Keep Reaching For</strong></a></li>
<li>Olaf Alders: <a href="https://www.olafalders.com/2026/06/30/keep-it-local/"><strong>Keep it Local</strong></a></li>
<li>The <a href="https://github.com/davorg-cpan/app-httpthis/pull/13">pull request that corrected the startup message</a></li>
<li>The <a href="https://github.com/davorg-cpan/app-httpthis/pull/15">pull request that changed the default behaviour to bind to localhost</a></li>
<li>The <a href="https://github.com/davorg-cpan/app-httpthis/commit/5ba2303e2408e9cd7b6e1da8da9ef1cb14c4b195">follow-up commit adding WSL support</a></li>
</ul><p>The post <a href="https://perlhacks.com/2026/07/how-one-pull-request-took-apphttpthis-to-version-1-0/">How One Pull Request Took App::HTTPThis to Version 1.0</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/07/how-one-pull-request-took-apphttpthis-to-version-1-0/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2480</post-id>	</item>
		<item>
		<title>Choosing the Right Database Abstraction</title>
		<link>https://perlhacks.com/2026/06/choosing-the-right-database-abstraction/</link>
					<comments>https://perlhacks.com/2026/06/choosing-the-right-database-abstraction/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 28 Jun 2026 09:24:43 +0000</pubDate>
				<category><![CDATA[Databases]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[dbi]]></category>
		<category><![CDATA[dbix::class]]></category>
		<category><![CDATA[mojolicious]]></category>
		<category><![CDATA[orm]]></category>
		<category><![CDATA[Software Architecture]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2461</guid>

					<description><![CDATA[<p>A question came up recently in the Perl community asking whether, in a Mojolicious application, it&#8217;s better to use DBIx::Class or a Mojolicious-specific module like Mojo::Pg. It&#8217;s an interesting question, but I think it&#8217;s asking the wrong thing. I&#8217;ve spent almost forty years moving up the stack of database abstractions. Every few years, someone invents [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/06/choosing-the-right-database-abstraction/">Choosing the Right Database Abstraction</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>A question came up recently in the Perl community asking whether, in a Mojolicious application, it&#8217;s better to use <a href="https://metacpan.org/pod/DBIx::Class">DBIx::Class</a> or a Mojolicious-specific module like <a href="https://metacpan.org/pod/Mojo::Pg">Mojo::Pg</a>.</p>
<p>It&#8217;s an interesting question, but I think it&#8217;s asking the wrong thing.</p>
<p>I&#8217;ve spent almost forty years moving <strong>up</strong> the stack of database abstractions. Every few years, someone invents a new layer, and every few years, experienced programmers explain that they don&#8217;t need it because they&#8217;re perfectly capable of working at the layer below. I&#8217;ve watched exactly the same argument play out over CGI, web frameworks, ORMs, containers and now AI-assisted coding.</p>
<p>Sometimes the new abstraction turns out to be a dead end. More often, it&#8217;s simply another step that lets us spend less time on plumbing and more time solving the problems our users actually care about.</p>
<p>I think that&#8217;s what&#8217;s really going on here.</p>
<h2>You&#8217;re comparing different layers</h2>
<p>The first thing that struck me about the discussion was that it jumped straight from DBIx::Class to Mojo::Pg, as though they were equivalent choices. I don&#8217;t think they are.</p>
<p>If you zoom out a little, there are several layers involved in database access:</p>
<ul>
<li>Application</li>
<li>DBIx::Class / DBIO (ORM)</li>
<li>DBI</li>
<li>DBD::Pg / DBD::SQLite / DBD::MariaDB</li>
<li>Database</li>
</ul>
<p>Each layer builds on the one below it. DBIx::Class sits on top of DBI, DBI sits on top of a database driver, and the driver talks to the database itself. Every layer gives you a slightly richer abstraction whilst hiding a little more of the mechanics.</p>
<p>Mojo::Pg occupies a rather different place. It&#8217;s a PostgreSQL library designed specifically for Mojolicious. That&#8217;s a perfectly sensible design if you&#8217;ve already decided you&#8217;re using both PostgreSQL and Mojolicious, but it couples together decisions that I&#8217;d normally prefer to keep separate.</p>
<p>Personally, I like choosing my web framework independently of my database, and my database independently of my data access layer. The more independent those decisions are, the easier it is to change one without affecting the others.</p>
<h2>Why I moved up the stack</h2>
<p>I started writing embedded SQL in C in the late 1980s. When I moved to Perl in 1996, I used <a href="https://metacpan.org/pod/sybperl">Sybperl</a>, which was a thin layer over the proprietary Sybase database API. When I discovered <a href="https://metacpan.org/pod/DBI">DBI</a>, I adopted it enthusiastically because it removed an enormous amount of repetitive code whilst remaining database-independent. Later, when DBIx::Class became mature enough for production systems, I happily moved another level up the stack.</p>
<p>None of those decisions happened because I couldn&#8217;t write SQL. I&#8217;ve been writing SQL since 1988 and I&#8217;m perfectly happy doing so.</p>
<p>The point was that I no longer wanted to spend my time writing the same joins, the same foreign key lookups and the same bits of boilerplate over and over again. Those aren&#8217;t the interesting parts of my applications. They&#8217;re necessary, but they&#8217;re plumbing. Good abstractions let me write that plumbing once and then think about the domain I&#8217;m modelling instead.</p>
<h2>This isn&#8217;t just about databases</h2>
<p>A few weeks ago, I wrote about <a href="https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/"><em>The Long Road from CGI to Containers</em></a>. The point of that article was that the history of web development is largely the history of building better abstractions.</p>
<p>We moved from CGI scripts to web frameworks. From hand-written deployment procedures to containers. Every step let us express our intent at a higher level whilst worrying less about the implementation details.</p>
<p>Database programming has followed exactly the same path.</p>
<h2>But I like writing SQL</h2>
<p>Whenever I recommend an ORM, someone inevitably assumes it&#8217;s because I don&#8217;t know SQL.</p>
<p>Nothing could be further from the truth.</p>
<p>SQL is probably the programming language I&#8217;ve been writing the longest. I enjoy writing SQL. But I don&#8217;t enjoy writing the same SQL repeatedly.</p>
<p>I don&#8217;t want every controller containing another join between the same tables. I don&#8217;t want to duplicate the same <code inline="">WHERE</code> clause in half a dozen places. I don&#8217;t want to remember every foreign key relationship every time I touch the code.</p>
<p>Those things aren&#8217;t business logic; they&#8217;re plumbing.</p>
<p>An ORM lets me move that plumbing into one place and give it names.</p>
<h2>What DBIx::Class gives me</h2>
<p>For me, the biggest advantage of DBIx::Class isn&#8217;t that it generates SQL.</p>
<p>It&#8217;s that it models my application.</p>
<ul>
<li>A Person has many Titles.</li>
<li>A Title belongs to a Person.</li>
<li>A User has many Orders.</li>
</ul>
<p>Those relationships become part of the vocabulary of the application. Queries become reusable methods. Business rules become object methods instead of comments beside SQL statements.</p>
<p>Instead of thinking about joins, I&#8217;m thinking about people, titles and orders.</p>
<p>That&#8217;s a much nicer level of abstraction.</p>
<h2>Isn&#8217;t it slower?</h2>
<p>One criticism often levelled at DBIx::Class is performance.</p>
<p>That&#8217;s not entirely unfair. It&#8217;s perfectly possible to write inefficient DBIx::Class code. I know because I&#8217;ve done it.</p>
<p>While recently carrying out some optimisation on my <a href="https://lineofsuccession.co.uk/">Line of Succession</a> application, I discovered one page was generating hundreds of unnecessary database queries. The solution wasn&#8217;t to abandon DBIx::Class and replace it with hand-written SQL. The solution was to understand the ORM better.</p>
<p>Adding a couple of well-placed <code inline="">prefetch</code> clauses reduced the number of queries dramatically with almost no change to the surrounding code.</p>
<p>The ORM wasn&#8217;t the problem; my use of the ORM was.</p>
<p>That&#8217;s a useful lesson. Every abstraction has a learning curve. If you don&#8217;t understand how it&#8217;s working underneath, you&#8217;ll eventually hit performance problems. But once you do understand it, you can often achieve dramatic improvements without sacrificing the higher-level API that made you choose the abstraction in the first place.</p>
<h2>What if I just want to write SQL?</h2>
<p>One thing that struck me about the original discussion is that it jumped straight from DBIx::Class to Mojo::Pg, as though those were the only two choices.</p>
<p>I don&#8217;t think they are.</p>
<p>If all I wanted was to execute SQL from a Mojolicious application, I&#8217;d probably start by looking for a DBI-based solution rather than one that&#8217;s tied specifically to PostgreSQL. Something like <a href="https://metacpan.org/pod/Mojolicious::Plugin::Database">Mojolicious::Plugin::Database</a> seems like a more natural fit, as it lets me use the standard Perl database abstraction whilst integrating cleanly with Mojolicious.</p>
<p>That still leaves me free to choose PostgreSQL, SQLite, MariaDB or whatever other database makes sense for the project.</p>
<p>By contrast, choosing Mojo::Pg bakes the decision to use PostgreSQL into your application at a deeper level than I&#8217;d be comfortable with.</p>
<p>Good abstractions should reduce coupling, not increase it.</p>
<h2>The model layer</h2>
<p>Of course, in larger applications, there&#8217;s usually another abstraction on top of the ORM itself. My controllers don&#8217;t generally talk directly to DBIx::Class resultsets; they ask domain-level questions such as &#8220;Who was sovereign on this date?&#8221; The fact that the answer currently comes from DBIx::Class is an implementation detail. That&#8217;s another useful abstraction—but it&#8217;s probably a topic for another article.</p>
<div class="aside">
<h3>Aside: What happened to DBIx::Class?</h3>
<p>For a long time, this was the awkward question.</p>
<p>Around a decade ago, active development effectively stopped. This wasn&#8217;t because the technology had failed or because the community lost interest. The maintainer had explicitly stopped development, and measures such as the CPAN <code inline="">NOXFER</code> co-maintainer flag prevented the normal open source succession process from happening.</p>
<p>For years, that left DBIx::Class in an uncomfortable position. It remained stable and widely used, but there was understandable uncertainty about its long-term future.</p>
<p>Fortunately, that situation finally appears to be changing. <a href="https://metacpan.org/pod/DBIO">DBIO</a> appeared on CPAN just a few days ago. It has taken the hard-fork route, preserving the ideas that made DBIx::Class successful whilst allowing active development to resume.</p>
<p>Whether DBIO ultimately becomes the successor to DBIx::Class isn&#8217;t really the important point.</p>
<p>The important point is that these ideas are still valuable enough that people are prepared to invest significant effort in carrying them forward.</p>
</div>
<h2>Choosing your abstraction</h2>
<p>I don&#8217;t think this is fundamentally a question about Mojolicious.</p>
<p>I&#8217;d happily use DBIx::Class with Mojolicious, Dancer2 or any other web framework. I&#8217;d happily use DBI with all of them too. The web framework and the data access layer are largely independent architectural decisions, and I generally prefer to keep them that way.</p>
<p>The important decision isn&#8217;t the framework.</p>
<p>It isn&#8217;t even the database.</p>
<p>It&#8217;s deciding where you want your application to live.</p>
<p>Do you want to think in terms of SQL?</p>
<p>Do you want to think in terms of tables?</p>
<p>Or do you want to think in terms of the concepts that make up your application&#8217;s domain?</p>
<p>After nearly forty years of writing SQL, I know which one I&#8217;d rather spend my day thinking about.</p>
<p>The best abstraction isn&#8217;t the one that hides the most detail.</p>
<p>It&#8217;s the one that lets you spend most of your time thinking about the problem you&#8217;re actually trying to solve.</p>
<h2>Further reading</h2>
<p>I&#8217;ve written about different aspects of this topic before:</p>
<ul>
<li><a href="https://perlhacks.com/2012/10/dbic-vs-dbi/"><strong>DBIx::Class vs DBI</strong></a> (2012) — why I prefer to use an ORM over raw SQL.</li>
<li><a href="https://perlhacks.com/2015/09/the-joy-of-prefetch/"><strong>The Joy of Prefetch</strong></a> (2015) — how understanding your ORM can transform performance.</li>
<li><a href="https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/"><strong>The Long Road from CGI to Containers</strong></a> (2026) — why software engineering is really the story of building better abstractions.</li>
</ul><p>The post <a href="https://perlhacks.com/2026/06/choosing-the-right-database-abstraction/">Choosing the Right Database Abstraction</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/06/choosing-the-right-database-abstraction/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2461</post-id>	</item>
		<item>
		<title>Public Identifiers, UUIDs and a Tiny SEO Fix</title>
		<link>https://perlhacks.com/2026/06/public-identifiers-uuids-and-a-tiny-seo-fix/</link>
					<comments>https://perlhacks.com/2026/06/public-identifiers-uuids-and-a-tiny-seo-fix/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sat, 06 Jun 2026 14:29:38 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Database Design]]></category>
		<category><![CDATA[line of succession]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[seo]]></category>
		<category><![CDATA[Software Architecture]]></category>
		<category><![CDATA[URL Design]]></category>
		<category><![CDATA[web development]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2452</guid>

					<description><![CDATA[<p>Public Identifiers, UUIDs and a Tiny SEO Fix A recent question from my friend and colleague Mohammad got me thinking about the way we identify data in web applications. While working on the DBIC component of a REST API, he came across the term enumeration attack. In this type of attack, an attacker systematically guesses [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/06/public-identifiers-uuids-and-a-tiny-seo-fix/">Public Identifiers, UUIDs and a Tiny SEO Fix</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<h1>Public Identifiers, UUIDs and a Tiny SEO Fix</h1>
<p>A recent question from my friend and colleague Mohammad got me thinking about the way we identify data in web applications.</p>
<p>While working on the DBIC component of a REST API, he came across the term <em>enumeration attack</em>. In this type of attack, an attacker systematically guesses resource identifiers in order to access data they shouldn&#8217;t be able to see.</p>
<p>For example, if your API exposes URLs like this:</p><pre class="urvanov-syntax-highlighter-plain-tag">GET /users/123
GET /users/124
GET /users/125</pre><p>then it&#8217;s easy for someone to try a large range of identifiers and see what they get back.</p>
<p>Mohammad&#8217;s question was simple:</p>
<blockquote><p>Should we replace sequential IDs with UUIDs? And if we do, should we index the UUID column?</p></blockquote>
<p>As is often the case, the answer turned out to be &#8220;it depends&#8221;.</p>
<h2>Two Different Types of Data</h2>
<p>The first thing I realised is that not all data objects have the same requirements.</p>
<p>Some objects are naturally public.</p>
<p>For example, books on a publishing website are intended to be discovered. In fact, you probably want people to be able to guess their URLs:</p><pre class="urvanov-syntax-highlighter-plain-tag">/books/design-patterns-in-modern-perl</pre><p>In this case, a human-readable slug makes perfect sense. Other objects are private by nature. User accounts, orders, invoices and API resources generally shouldn&#8217;t be enumerable. In those cases, a UUID is often a better choice:</p><pre class="urvanov-syntax-highlighter-plain-tag">/users/550e8400-e29b-41d4-a716-446655440000</pre><p>The important observation is that slugs and UUIDs solve different problems.</p>
<ul>
<li>Slugs are for humans (and, perhaps, search engines).</li>
<li>UUIDs are for machines.</li>
</ul>
<h2>Database Design</h2>
<p>A common question is whether a UUID should replace the primary key.</p>
<p>In most cases, I don&#8217;t think it should.</p>
<p>My preferred design is:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE users (
  id BIGINT PRIMARY KEY,
  uuid UUID NOT NULL UNIQUE
);</pre><p>The integer primary key remains the internal identifier used for joins and foreign keys.</p>
<p>The UUID becomes the public identifier exposed through APIs.</p>
<p>This gives you the best of both worlds:</p>
<ul>
<li>Small, efficient foreign keys.</li>
<li>Fast joins.</li>
<li>Unguessable public identifiers.</li>
</ul>
<p>If the application regularly searches by UUID then the UUID column should be indexed. In practice, declaring it <code inline="">UNIQUE</code> will usually create the appropriate index automatically.</p>
<h2>The Hybrid Approach</h2>
<p>Thinking about this reminded me that many large sites use a hybrid approach.</p>
<p>Amazon product URLs contain both a human-readable title and a stable identifier:</p><pre class="urvanov-syntax-highlighter-plain-tag">/Design-Patterns-Modern-Perl/dp/B0XXXXX123</pre><p>The ASIN is what really identifies the product.</p>
<p>The title is there for humans.</p>
<p>Stack Overflow does something similar:</p><pre class="urvanov-syntax-highlighter-plain-tag">/questions/12345/how-do-i-index-a-uuid-column</pre><p>Again, the question ID is authoritative. The title is helpful context.</p>
<p>My Line of Succession website uses the same idea.</p>
<p>A person page looks like this:</p><pre class="urvanov-syntax-highlighter-plain-tag">/p/2b5998-the-prince-william-prince-of-wales</pre><p>The important part is the identifier:</p><pre class="urvanov-syntax-highlighter-plain-tag">2b5998</pre><p>The rest is descriptive text.</p>
<p>This turns out to be particularly useful for royalty because titles change constantly. Someone might be &#8220;Prince William&#8221;, then &#8220;The Prince of Wales&#8221;, and eventually &#8220;King William V&#8221;.</p>
<p>By separating identity from presentation, old links continue to work regardless of title changes.</p>
<h2>A Tiny Bug</h2>
<p>While thinking about all of this, I discovered a small bug in Line of Succession.</p>
<p>The site allows any descriptive text after the identifier. These URLs all resolve to the same person:</p><pre class="urvanov-syntax-highlighter-plain-tag">/p/2b5998-the-prince-william-prince-of-wales
/p/2b5998-prince-billy
/p/2b5998-fred</pre><p>The application correctly ignores the descriptive text and uses only the identifier.</p>
<p>However, there was a problem.</p>
<p>The page was generating its canonical URL from the incoming request path rather than from the person record.</p>
<p>That meant a request for:</p><pre class="urvanov-syntax-highlighter-plain-tag">/p/2b5998-prince-billy</pre><p>generated:</p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;link rel="canonical"
      href="https://lineofsuccession.co.uk/p/2b5998-prince-billy"&gt;</pre><p>which is obviously not the canonical URL.</p>
<p>The fix was surprisingly small:</p><pre class="urvanov-syntax-highlighter-plain-tag">sub canonical( $self ) {
   if ($self-&gt;request-&gt;is_date_page) {
     return '/' . $self-&gt;canonical_date;
+  } elsif($self-&gt;request-&gt;is_person_page) {
+    return '/p/' . $self-&gt;request-&gt;person-&gt;slug;
   } else {
     return $self-&gt;request-&gt;path;
   }
 }</pre><p>At the same time I simplified another method by making it reuse the canonical URL logic.</p>
<p>The result was a six-line patch that fixed the SEO issue and made the code slightly cleaner.</p>
<p>Those are my favourite kinds of fixes.</p>
<h2>Future Improvements</h2>
<p>The fix also revealed an emerging abstraction in the code.</p>
<p>At the moment, various parts of the application know how to construct URLs for different object types.</p>
<p>A cleaner approach would be to give objects responsibility for generating their own URLs.</p>
<p>I&#8217;m considering a <code inline="">HasURL</code> role that would require an object to provide an identifier and optionally a prefix, and then build the URL automatically.</p>
<p>That&#8217;s a job for another day.</p>
<p>For now, a small question about UUIDs led to a useful discussion about public identifiers, a review of URL design, and a tiny production fix. Not bad for an afternoon&#8217;s work.</p><p>The post <a href="https://perlhacks.com/2026/06/public-identifiers-uuids-and-a-tiny-seo-fix/">Public Identifiers, UUIDs and a Tiny SEO Fix</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/06/public-identifiers-uuids-and-a-tiny-seo-fix/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2452</post-id>	</item>
		<item>
		<title>Teaching AI About the British Monarchy with MCP</title>
		<link>https://perlhacks.com/2026/05/teaching-ai-about-the-british-monarchy-with-mcp/</link>
					<comments>https://perlhacks.com/2026/05/teaching-ai-about-the-british-monarchy-with-mcp/#respond</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sat, 30 May 2026 09:58:02 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[dancer2]]></category>
		<category><![CDATA[line of succession]]></category>
		<category><![CDATA[mcp]]></category>
		<category><![CDATA[perl]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2445</guid>

					<description><![CDATA[<p>One of the more interesting additions I&#8217;ve made recently to the Line of Succession website is support for the Model Context Protocol (MCP). If you&#8217;ve spent any time around AI tooling recently, you&#8217;ve probably seen people talking about MCP. It&#8217;s often described as &#8220;USB for AI&#8221;, which is perhaps a little overblown, but the basic [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/05/teaching-ai-about-the-british-monarchy-with-mcp/">Teaching AI About the British Monarchy with MCP</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="isSelectedEnd">One of the more interesting additions I&#8217;ve made recently to <a href="https://lineofsuccession.co.uk/">the Line of Succession website</a> is support for the <a href="https://en.wikipedia.org/wiki/Model_Context_Protocol">Model Context Protocol (MCP)</a>.</p>
<p class="isSelectedEnd">If you&#8217;ve spent any time around AI tooling recently, you&#8217;ve probably seen people talking about MCP. It&#8217;s often described as &#8220;USB for AI&#8221;, which is perhaps a little overblown, but the basic idea is sound. MCP provides a standard way for AI assistants to discover and use external tools and data sources.</p>
<p class="isSelectedEnd">In practical terms, it means that instead of building bespoke integrations for ChatGPT, Claude, Gemini and whatever comes next, you expose a standard MCP endpoint and let the AI clients do the rest.</p>
<p class="isSelectedEnd">For a data-driven site like Line of Succession, that seemed like an obvious experiment.</p>
<h2>What is MCP?</h2>
<p class="isSelectedEnd">The Model Context Protocol was originally developed by Anthropic and has rapidly become one of the emerging standards in the AI ecosystem.</p>
<p class="isSelectedEnd">An MCP server exposes:</p>
<ul data-spread="false">
<li>Information about itself</li>
<li>A list of available tools</li>
<li>Schemas describing how those tools should be called</li>
<li>The results returned by those tools</li>
</ul>
<p class="isSelectedEnd">An AI client can connect to the server, discover the available tools and invoke them when needed.</p>
<p class="isSelectedEnd">Instead of scraping web pages or attempting to infer information from HTML, the AI gets access to structured data.</p>
<p class="isSelectedEnd">That&#8217;s exactly the kind of thing Line of Succession is good at.</p>
<h2>Why Add MCP?</h2>
<p class="isSelectedEnd">The site already exposes information through a traditional web interface and a JSON API.</p>
<p class="isSelectedEnd">But those interfaces were designed for humans and developers respectively.</p>
<p class="isSelectedEnd">MCP gives AI systems a much cleaner integration point.</p>
<p class="isSelectedEnd">For example, an AI assistant can now answer questions like:</p>
<ul data-spread="false">
<li>Who was the British sovereign on 14 November 1948?</li>
<li>What did the line of succession look like in 1980?</li>
<li>Who was next in line when Queen Victoria died?</li>
</ul>
<p class="isSelectedEnd">without having to scrape pages or understand the site&#8217;s internal URLs.</p>
<p class="isSelectedEnd">More importantly, it ensures that the information comes directly from the same database that powers the website.</p>
<p class="isSelectedEnd">The AI isn&#8217;t guessing.</p>
<p class="isSelectedEnd">It&#8217;s querying the source of truth.</p>
<p class="isSelectedEnd">As someone who runs a reference website, that&#8217;s a pretty attractive proposition.</p>
<h2>The Initial Design</h2>
<p class="isSelectedEnd">My first goal was to keep things simple.</p>
<p class="isSelectedEnd">Rather than exposing dozens of narrowly-focused tools, I started with just two:</p>
<ul data-spread="false">
<li><code dir="ltr">sovereign_on_date</code></li>
<li><code dir="ltr">line_of_succession</code></li>
</ul>
<p class="isSelectedEnd">Those two tools cover a surprisingly large proportion of the questions people are likely to ask.</p>
<p class="isSelectedEnd">The first returns the sovereign reigning on a given date. The second returns the line of succession for a specified date, with a configurable limit on the number of entries returned.</p>
<p class="isSelectedEnd">The implementation currently caps the list at thirty people. That&#8217;s enough for most use cases while preventing someone from accidentally asking for all six thousand people currently in the line of succession.</p>
<p class="isSelectedEnd">One thing I learned quite quickly is that MCP isn&#8217;t really about exposing huge amounts of data. It&#8217;s about exposing useful questions that can be answered from your data.</p>
<h2>MCP Is Mostly JSON-RPC</h2>
<p class="isSelectedEnd">One thing that surprised me when I first started reading the specification was how little protocol code is actually required.</p>
<p class="isSelectedEnd">At its core, MCP uses <a href="https://en.wikipedia.org/wiki/JSON-RPC">JSON-RPC</a>.</p>
<p class="isSelectedEnd">A client sends requests like:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}</pre><p></p>
<p class="isSelectedEnd">and the server responds with:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    ...
  }
}</pre><p></p>
<p class="isSelectedEnd">Once I&#8217;d written helper methods for creating standard JSON-RPC responses, most of the complexity disappeared.</p>
<p class="isSelectedEnd">The MCP module contains methods like:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">sub rpc_result ($self, $id, $result)</pre><p></p>
<p class="isSelectedEnd">and:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">sub rpc_error ($self, $id, $code, $message)</pre><p></p>
<p class="isSelectedEnd">which means the Dancer route handlers remain pleasantly small.</p>
<p class="isSelectedEnd">The protocol logic lives in one place and the web application simply delegates to it.</p>
<h2>Separating the MCP Logic</h2>
<p class="isSelectedEnd">I didn&#8217;t want protocol-specific code scattered throughout the web application.</p>
<p class="isSelectedEnd">Instead, I created a dedicated module:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">package Succession::MCP;</pre><p></p>
<p class="isSelectedEnd">This module is responsible for:</p>
<ul data-spread="false">
<li>Initialisation</li>
<li>Tool discovery</li>
<li>Tool execution</li>
<li>JSON-RPC response generation</li>
<li>Error handling</li>
</ul>
<p class="isSelectedEnd">That keeps the Dancer routes thin and makes the MCP implementation easier to test independently.</p>
<p class="isSelectedEnd">It also means that if I ever decide to expose the same MCP server through a different transport mechanism, most of the work is already done.</p>
<h2>Tool Calls Are Mostly Adapters</h2>
<p class="isSelectedEnd">One pleasant surprise was how little new application logic I actually had to write.</p>
<p class="isSelectedEnd">The MCP server needs to expose tools, but those tools ultimately just answer questions about the succession database. The code to answer those questions already existed.</p>
<p class="isSelectedEnd">For example, the application&#8217;s model layer already contained methods such as:</p>
<ul data-spread="false">
<li><code dir="ltr">sovereign_on_date()</code></li>
<li><code dir="ltr">line_of_succession()</code></li>
</ul>
<p class="isSelectedEnd">These methods power parts of the website itself, so they already encapsulate all of the business rules and database queries.</p>
<p class="isSelectedEnd">The MCP implementation simply acts as an adapter.</p>
<p class="isSelectedEnd">When a tool call arrives, the server extracts the arguments, validates them and passes them to the existing model methods:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">sub _call_tool ($self, $tool_name, $args) {
  my $tool = $self-&gt;_tool_dispatch-&gt;{$tool_name};

  return $tool-&gt;($args);
}</pre><p></p>
<p class="isSelectedEnd">The tool implementations themselves are deliberately thin:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">sub sovereign_on_date ($self, $args) {
  my $date = $args-&gt;{date};

  my $sovereign = $self-&gt;model-&gt;sovereign_on_date($date);

  ...

}</pre><p></p>
<p class="isSelectedEnd">That&#8217;s exactly how I wanted it to work.</p>
<p class="isSelectedEnd">The MCP layer doesn&#8217;t know how to calculate a line of succession or determine who was sovereign on a particular date. It simply knows how to expose those capabilities through the protocol.</p>
<p class="isSelectedEnd">This is one of the advantages of adding MCP to an existing application. If your business logic is already cleanly separated from your web interface, an MCP server often becomes surprisingly straightforward to implement.</p>
<p>In many ways, adding MCP feels less like building a new application and more like adding another interface alongside the website and API.</p>
<h2>The YAML Epiphany</h2>
<p class="isSelectedEnd">The most interesting design decision came a little later.</p>
<p class="isSelectedEnd">Initially, the tool definitions lived in Perl data structures.</p>
<p class="isSelectedEnd">That worked, but it quickly became obvious that I was duplicating information.</p>
<p class="isSelectedEnd">The MCP server needed tool descriptions.</p>
<p class="isSelectedEnd">The documentation page needed tool descriptions.</p>
<p class="isSelectedEnd">The schemas needed to be defined somewhere.</p>
<p class="isSelectedEnd">And every change required updating multiple places.</p>
<p class="isSelectedEnd">The obvious answer was to move all of the tool definitions into a YAML file.</p>
<p class="isSelectedEnd">The MCP module now loads its tool definitions at startup:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">sub _build__tools ($self) { return LoadFile($self-&gt;tools_file); }</pre><p></p>
<p class="isSelectedEnd">The result is a single source of truth.</p>
<p class="isSelectedEnd">The same YAML file drives:</p>
<ul data-spread="false">
<li>The <code dir="ltr">tools/list</code> response</li>
<li>Tool metadata</li>
<li>JSON schemas</li>
<li>Human-readable documentation</li>
</ul>
<p class="isSelectedEnd">Adding a new tool now involves updating one file and writing the code that implements it.</p>
<p class="isSelectedEnd">Everything else follows automatically.</p>
<p class="isSelectedEnd">Here&#8217;s the current YAML file:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag"># data/mcp-tools.yml

- name: sovereign_on_date
  description: Return the British sovereign on a given date.
  documentation: |
    Looks up the reigning British sovereign for the supplied date.

    Use this when answering questions such as “Who was sovereign on
    6 February 1952?”
  inputSchema:
    type: object
    properties:
      date:
        type: string
        description: Date in YYYY-MM-DD format.
    required:
      - date

- name: line_of_succession
  description: Return the line of succession on a given date.
  documentation: |
    Returns people in the line of succession.

    If no date is supplied, the current line of succession is returned.
  inputSchema:
    type: object
    properties:
      date:
        type: string
        description: Optional date in YYYY-MM-DD format. Omit for the current line of succession.
      limit:
        type: integer
        description: Maximum number of successors to return.
        minimum: 1
        maximum: 100
      required: []</pre><p></p>
<p class="isSelectedEnd">Looking back, this is probably the part of the design I&#8217;m happiest with. It feels very Perl-ish: keep configuration as data and avoid duplicating information wherever possible.</p>
<h2>Human Documentation Matters</h2>
<p class="isSelectedEnd">One thing I noticed while exploring other MCP servers is that many of them are effectively invisible to humans.</p>
<p class="isSelectedEnd">You know an endpoint exists.</p>
<p class="isSelectedEnd">You know it speaks MCP.</p>
<p class="isSelectedEnd">But unless you inspect the protocol responses manually, you don&#8217;t really know what it does.</p>
<p class="isSelectedEnd">I decided to add a conventional web page at <a href="https://lineofsuccession.co.uk/mcp"><code dir="ltr">/mcp</code></a>.</p>
<p class="isSelectedEnd">The page lists all available tools, their descriptions and their schemas.</p>
<p class="isSelectedEnd">The nice part is that there is no duplicated documentation.</p>
<p class="isSelectedEnd">The page is generated from the same YAML definitions used by the MCP server itself.</p>
<p class="isSelectedEnd">If I add a new tool tomorrow, both the machine-readable and human-readable views update automatically.</p>
<h2>Structured Data and Text Responses</h2>
<p class="isSelectedEnd">Another nice feature of MCP is that tool results can include both structured data and human-readable text.</p>
<p class="isSelectedEnd">For example, a tool response might contain:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  "content": [ {
    "type": "text",
    "text": "The sovereign on 14 November 1948 was George VI."
    } ],
  "structuredContent": {
    ...
  }
}</pre><p></p>
<p class="isSelectedEnd">The structured content is useful for software.</p>
<p class="isSelectedEnd">The text is useful for humans and language models.</p>
<p class="isSelectedEnd">Both are generated from the same underlying data.</p>
<p class="isSelectedEnd">That gives AI clients flexibility while ensuring consistency.</p>
<h2>Getting Listed</h2>
<p class="isSelectedEnd">Once everything was working, I submitted the server to the MCP directory at <a href="https://mcpservers.org">mcpservers.org</a>.</p>
<p class="isSelectedEnd">That might seem like a small step, but discoverability is important.</p>
<p class="isSelectedEnd">An MCP server hidden on a random website isn&#8217;t much use if nobody knows it exists.</p>
<p class="isSelectedEnd">Directories like that are rapidly becoming the equivalent of API catalogues for the AI era.</p>
<p class="isSelectedEnd">Being listed means developers and AI enthusiasts can find the service without first discovering the website.</p>
<h2>Was It Worth It?</h2>
<p class="isSelectedEnd">Absolutely.</p>
<p class="isSelectedEnd">The amount of code required was surprisingly small. Most of the work wasn&#8217;t implementing the protocol; it was deciding how best to expose the data.</p>
<p class="isSelectedEnd">More importantly, it opens the site up to an entirely new audience: AI agents.</p>
<p class="isSelectedEnd">Historically, websites were built for humans and APIs were built for developers.</p>
<p class="isSelectedEnd">MCP introduces a third category: services designed specifically for AI systems.</p>
<p class="isSelectedEnd">For a structured-data site like Line of Succession, that&#8217;s a natural fit.</p>
<p class="isSelectedEnd">Will MCP still be the dominant standard in five years&#8217; time? I have no idea. The AI industry changes too quickly to make confident predictions.</p>
<p class="isSelectedEnd">But right now it has significant momentum, broad industry support and a growing ecosystem of tools.</p>
<p>And if nothing else, it&#8217;s rather satisfying to ask an AI who was on the throne on a particular date and know that the answer came directly from my database rather than from whatever the model happened to remember.</p><p>The post <a href="https://perlhacks.com/2026/05/teaching-ai-about-the-british-monarchy-with-mcp/">Teaching AI About the British Monarchy with MCP</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/05/teaching-ai-about-the-british-monarchy-with-mcp/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2445</post-id>	</item>
		<item>
		<title>The Long Road from CGI to Containers</title>
		<link>https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/</link>
					<comments>https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Mon, 18 May 2026 15:36:11 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[cgi]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[psgi]]></category>
		<category><![CDATA[softwarearchitecture]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2437</guid>

					<description><![CDATA[<p>One of the defining characteristics of a good programmer is an instinct for keeping implementation details in the correct layer of an application. That sounds abstract, but it turns out to explain a huge amount of the progress we’ve made in software development over the last twenty-five years. And nowhere is that clearer than in [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/">The Long Road from CGI to Containers</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p data-start="69" data-end="215">One of the defining characteristics of a good programmer is an instinct for keeping implementation details in the correct layer of an application.</p>
<p data-start="217" data-end="364">That sounds abstract, but it turns out to explain a huge amount of the progress we’ve made in software development over the last twenty-five years.</p>
<p data-start="366" data-end="423">And nowhere is that clearer than in Perl web development.</p>
<p data-start="425" data-end="532">Many of us who built web applications during the dotcom boom spent years learning this lesson the hard way.</p>
<p data-start="534" data-end="561">We wrote CGI programs that:</p>
<ul data-start="562" data-end="828">
<li data-section-id="1hrkapv" data-start="562" data-end="584">parsed HTTP requests</li>
<li data-section-id="ia7w56" data-start="585" data-end="601">generated HTML by hand</li>
<li data-section-id="11qm9he" data-start="602" data-end="635">connected directly to databases</li>
<li data-section-id="oxywwx" data-start="636" data-end="657">embedded SQL inline</li>
<li data-section-id="10ssi1" data-start="658" data-end="698">mixed business logic with presentation</li>
<li data-section-id="1k0ef25" data-start="699" data-end="727">relied on Apache behaviour</li>
<li data-section-id="1o4kt96" data-start="728" data-end="765">assumed specific filesystem layouts</li>
<li data-section-id="1xpkcp2" data-start="766" data-end="828">and often only worked on one particular server configuration</li>
</ul>
<p data-start="830" data-end="861">It all worked. Until it didn’t.</p>
<p data-start="863" data-end="1015">The history of Perl web development is, in many ways, the history of gradually moving implementation details into more appropriate architectural layers.</p>
<hr data-start="1017" data-end="1020" />
<h2 data-section-id="el1fv2" data-start="1022" data-end="1044">The Early CGI Years</h2>
<p data-start="1046" data-end="1107">Early Perl CGI applications were often a single giant script.</p>
<p data-start="1109" data-end="1135">You’d open a file and see:</p>
<ul data-start="1136" data-end="1261">
<li data-section-id="vbdqpi" data-start="1136" data-end="1154">request handling</li>
<li data-section-id="gqcdm0" data-start="1155" data-end="1171">authentication</li>
<li data-section-id="v39dg3" data-start="1172" data-end="1189">HTML generation</li>
<li data-section-id="1wl0ikq" data-start="1190" data-end="1203">SQL queries</li>
<li data-section-id="pcs1uo" data-start="1204" data-end="1220">business logic</li>
<li data-section-id="xq775k" data-start="1221" data-end="1236">configuration</li>
<li data-section-id="1a36h93" data-start="1237" data-end="1261">deployment assumptions</li>
</ul>
<p data-start="1263" data-end="1309">…all mixed together in a glorious ball of mud.</p>
<p data-start="1311" data-end="1331">Something like this:</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="relative">
<div class="">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<pre class="urvanov-syntax-highlighter-plain-tag">#!/usr/bin/perl

use CGI;
use DBI;

my $cgi = CGI-&gt;new;

print $cgi-&gt;header;
print "&lt;html&gt;&lt;body&gt;";

my $dbh = DBI-&gt;connect(
  "dbi:mysql:test",
  "user",
  "pass"
);

my $sth = $dbh-&gt;prepare(
  "select * from users where id = ?"
);

$sth-&gt;execute($cgi-&gt;param('id'));

while (my $row = $sth-&gt;fetchrow_hashref) {
  print "&lt;h1&gt;$row-&gt;{name}&lt;/h1&gt;";
}

print "&lt;/body&gt;&lt;/html&gt;";</pre><br />
At the time, this felt perfectly normal.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p data-start="1759" data-end="1827">And to be fair, it <em data-start="1778" data-end="1783">was</em> a huge step forward from static HTML sites.</p>
<p data-start="1829" data-end="1870">But the design had a fundamental problem:</p>
<p data-start="1872" data-end="1919">Everything knew too much about everything else.</p>
<p data-start="1921" data-end="1948">The application logic knew:</p>
<ul data-start="1949" data-end="2086">
<li data-section-id="39oag0" data-start="1949" data-end="1966">how HTTP worked</li>
<li data-section-id="uiq751" data-start="1967" data-end="1984">how HTML worked</li>
<li data-section-id="15et8bv" data-start="1985" data-end="2018">how Apache launched CGI scripts</li>
<li data-section-id="1ugkrtw" data-start="2019" data-end="2044">how the database worked</li>
<li data-section-id="jts300" data-start="2045" data-end="2086">how the operating system was configured</li>
</ul>
<p data-start="2088" data-end="2134">Every concern leaked into every other concern.</p>
<p data-start="2136" data-end="2154">That made systems:</p>
<ul data-start="2155" data-end="2245">
<li data-section-id="1mg424a" data-start="2155" data-end="2169">hard to test</li>
<li data-section-id="az09iw" data-start="2170" data-end="2185">hard to reuse</li>
<li data-section-id="1ueutk7" data-start="2186" data-end="2202">hard to deploy</li>
<li data-section-id="b3tx90" data-start="2203" data-end="2218">hard to scale</li>
<li data-section-id="1lbws61" data-start="2219" data-end="2245">and terrifying to change</li>
</ul>
<hr data-start="2247" data-end="2250" />
<h2 data-section-id="196ob5l" data-start="2252" data-end="2299">The First Big Lesson: Put Logic in Libraries</h2>
<p data-start="2301" data-end="2448">One of the first signs of a developer maturing is the realisation that application logic should live in reusable modules, not in front-end scripts.</p>
<p data-start="2450" data-end="2466">Instead of this:</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="relative">
<div class="">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<pre class="urvanov-syntax-highlighter-plain-tag">if ($user-&gt;{status} eq 'gold') {
  $discount = 0.2;
}</pre><br />
being embedded directly in a CGI script, it becomes:</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="relative">
<div class="">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<pre class="urvanov-syntax-highlighter-plain-tag">my $discount = $user-&gt;discount_rate;</pre><br />
That sounds like a small change, but architecturally it’s enormous.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p data-start="2708" data-end="2750">Now the business logic lives in a library.</p>
<p data-start="2752" data-end="2816">And once that happens, several good things follow automatically.</p>
<h3 data-section-id="1sxaypq" data-start="2818" data-end="2857">Multiple Interfaces Become Possible</h3>
<p data-start="2859" data-end="2892">If the logic is in modules, then:</p>
<ul data-start="2893" data-end="2966">
<li data-section-id="17qbl7e" data-start="2893" data-end="2910">a web front-end</li>
<li data-section-id="oyv07r" data-start="2911" data-end="2923">a CLI tool</li>
<li data-section-id="12c0bb5" data-start="2924" data-end="2936">a REST API</li>
<li data-section-id="198xz7i" data-start="2937" data-end="2949">a cron job</li>
<li data-section-id="9ycnhq" data-start="2950" data-end="2966">a queue worker</li>
</ul>
<p data-start="2968" data-end="3006">…can all use the same underlying code.</p>
<p data-start="3008" data-end="3041">The interface layer becomes thin.</p>
<p data-start="3043" data-end="3116">The application itself becomes independent of how users interact with it.</p>
<p data-start="3118" data-end="3156">That’s a huge increase in flexibility.</p>
<h3 data-section-id="1pgnydl" data-start="3158" data-end="3184">Testing Becomes Easier</h3>
<p data-start="3186" data-end="3225">Testing CGI scripts was always awkward.</p>
<p data-start="3227" data-end="3262">Testing modules is straightforward.</p>
<p data-start="3264" data-end="3373">You can instantiate objects, call methods, and inspect results without needing a web server or HTTP requests.</p>
<p data-start="3375" data-end="3438">The easier code is to test, the more likely it is to be tested.</p>
<p data-start="3440" data-end="3480">And tested code tends to survive longer.</p>
<h3 data-section-id="1awrd8" data-start="3482" data-end="3510">Deployment Becomes Safer</h3>
<p data-start="3512" data-end="3621">Once the core behaviour is isolated from the interface layer, replacing the interface becomes far less risky.</p>
<p data-start="3623" data-end="3681">You can redesign the UI without rewriting the application.</p>
<p data-start="3683" data-end="3750">That separation is one of the foundations of maintainable software.</p>
<hr data-start="3752" data-end="3755" />
<h2 data-section-id="4h31i2" data-start="3757" data-end="3779">The PSGI Revolution</h2>
<p data-start="3781" data-end="3894">The next big architectural leap in Perl web development came with PSGI and <span class="hover:entity-accent entity-underline inline cursor-pointer align-baseline"><span class="whitespace-normal">Plack</span></span>.</p>
<p data-start="3896" data-end="3978">Younger developers may not fully appreciate how painful web deployment used to be.</p>
<p data-start="3980" data-end="4086">In the early 2000s, moving an application between hosting environments could require substantial rewrites.</p>
<ul>
<li data-start="4088" data-end="4121">A CGI application worked one way.</li>
<li data-start="4123" data-end="4165">A mod_perl application worked another way.</li>
<li data-start="4167" data-end="4194">FastCGI had its own quirks.</li>
<li data-start="4196" data-end="4247">Embedded Apache handlers behaved differently again.</li>
</ul>
<p data-start="4249" data-end="4363">Many Perl developers spent years repeatedly rewriting applications simply because deployment environments changed.</p>
<p data-start="4365" data-end="4382">That was madness.</p>
<p data-start="4384" data-end="4431">The deployment model is an operational concern.</p>
<p data-start="4433" data-end="4479">It should not affect application architecture.</p>
<p data-start="4481" data-end="4571">PSGI fixed this by defining a standard interface between web applications and web servers.</p>
<p data-start="4573" data-end="4610">The core idea was beautifully simple:</p>
<blockquote data-start="4612" data-end="4703">
<p data-start="4614" data-end="4703">A web application is just a function that receives an environment and returns a response.</p>
</blockquote>
<p data-start="4705" data-end="4791">Once that abstraction existed, applications no longer cared whether they were running:</p>
<ul data-start="4792" data-end="4921">
<li data-section-id="1843bvb" data-start="4792" data-end="4800">as CGI</li>
<li data-section-id="88tafm" data-start="4801" data-end="4817">under mod_perl</li>
<li data-section-id="yepid5" data-start="4818" data-end="4834">inside FastCGI</li>
<li data-section-id="15pdpy" data-start="4835" data-end="4850">under Starman</li>
<li data-section-id="12cjln6" data-start="4851" data-end="4865">behind nginx</li>
<li data-section-id="ytnv71" data-start="4866" data-end="4891">on a development laptop</li>
<li data-section-id="1tqpuce" data-start="4892" data-end="4921">or inside a cloud container</li>
</ul>
<p data-start="4923" data-end="4965">The deployment details moved down a layer.</p>
<p data-start="4967" data-end="4995">Exactly where they belonged.</p>
<p data-start="4997" data-end="5090">This was one of the most important architectural improvements Perl web development ever made.</p>
<p data-start="5092" data-end="5125">And it reflected a broader truth:</p>
<blockquote data-start="5127" data-end="5202">
<p data-start="5129" data-end="5202">Good abstractions stop lower-level implementation details leaking upward.</p>
</blockquote>
<hr />
<h2 data-section-id="141ej8e" data-start="477" data-end="526"><span role="text">The Transitional Era: FatPacker and <code data-start="516" data-end="526">cpanfile</code></span></h2>
<p data-start="528" data-end="640">There was also an interesting intermediate stage between traditional Perl deployments and full containerisation.</p>
<p data-start="642" data-end="735">For years, one of the hardest parts of deploying Perl applications was dependency management.</p>
<p data-start="737" data-end="792">You’d move an application to a new server and discover:</p>
<ul data-start="793" data-end="968">
<li data-section-id="hphz5e" data-start="793" data-end="819">the wrong module version</li>
<li data-section-id="1t55rzy" data-start="820" data-end="842">missing XS libraries</li>
<li data-section-id="1dwhnmz" data-start="843" data-end="871">incompatible Perl versions</li>
<li data-section-id="s0gapw" data-start="872" data-end="968">or an entire dependency tree that worked perfectly on the developer’s machine and nowhere else</li>
</ul>
<p data-start="970" data-end="1049">Large parts of Perl deployment culture evolved around coping with this problem.</p>
<p data-start="1051" data-end="1138">Tools like <code data-start="1062" data-end="1072">cpanfile</code> improved things by making dependencies explicit and reproducible.</p>
<p data-start="1140" data-end="1233">Instead of vaguely documenting requirements in a README, applications could formally declare:</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="relative">
<div class="">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<pre class="urvanov-syntax-highlighter-plain-tag">requires 'Dancer2';
requires 'DBIx::Class';
requires 'Template';</pre><br />
That may seem obvious now, but it was a major improvement in deployment reliability.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p data-start="1411" data-end="1541">Then tools like <span class="hover:entity-accent entity-underline inline cursor-pointer align-baseline"><span class="whitespace-normal">App::FatPacker</span></span> went even further by packaging dependencies directly alongside applications.</p>
<p data-start="1543" data-end="1668">Instead of relying on the target server’s Perl environment, applications could carry much of their runtime context with them.</p>
<p data-start="1670" data-end="1729">These tools didn’t completely solve deployment portability:</p>
<ul data-start="1730" data-end="1840">
<li data-section-id="1b7y3ls" data-start="1730" data-end="1763">system libraries still mattered</li>
<li data-section-id="wz1oqm" data-start="1764" data-end="1794">Perl versions still mattered</li>
<li data-section-id="9e0j1c" data-start="1795" data-end="1840">operating system differences still mattered</li>
</ul>
<p data-start="1842" data-end="1895">…but they represented an important shift in thinking.</p>
<p data-start="1897" data-end="1939">The industry was gradually realising that:</p>
<ul data-start="1940" data-end="2078">
<li data-section-id="40bngv" data-start="1940" data-end="1994">deployment environments were part of the application</li>
<li data-section-id="u5r428" data-start="1995" data-end="2021">reproducibility mattered</li>
<li data-section-id="1eq5k9r" data-start="2022" data-end="2078">and infrastructure assumptions needed to be controlled</li>
</ul>
<p data-start="2080" data-end="2221">Containers eventually pushed this idea to its logical conclusion by packaging not just Perl dependencies, but the entire runtime environment.</p>
<p data-start="2223" data-end="2342">In hindsight, tools like <code data-start="2248" data-end="2258">cpanfile</code> and FatPacker were stepping stones toward modern container-based deployment models.</p>
<hr data-start="5204" data-end="5207" />
<h2 data-section-id="avc5zh" data-start="5209" data-end="5246">Containers Are the Same Idea Again</h2>
<p data-start="5248" data-end="5339">Docker and containers are simply the same architectural principle repeated one layer lower.</p>
<p data-start="5341" data-end="5423">Before containers, deployments were often fragile and highly environment-specific.</p>
<p data-start="5425" data-end="5450">Applications depended on:</p>
<ul data-start="5451" data-end="5590">
<li data-section-id="oqa14g" data-start="5451" data-end="5483">particular Linux distributions</li>
<li data-section-id="y8i1ze" data-start="5484" data-end="5508">specific Perl versions</li>
<li data-section-id="1rbybas" data-start="5509" data-end="5537">installed system libraries</li>
<li data-section-id="aqka3g" data-start="5538" data-end="5563">hand-configured servers</li>
<li data-section-id="3iagcb" data-start="5564" data-end="5590">undocumented setup steps</li>
</ul>
<p data-start="5592" data-end="5643">Developers became experts in “works on my machine”.</p>
<p data-start="5645" data-end="5689">Operations teams became experts in swearing.</p>
<p data-start="5691" data-end="5720">Containers changed the model.</p>
<p data-start="5722" data-end="5743">Instead of deploying:</p>
<ul data-start="5744" data-end="5757">
<li data-section-id="10oh7ug" data-start="5744" data-end="5757">source code</li>
</ul>
<p data-start="5759" data-end="5771">…you deploy:</p>
<ul data-start="5772" data-end="5804">
<li data-section-id="4cejjd" data-start="5772" data-end="5804">a complete runtime environment</li>
</ul>
<p data-start="5806" data-end="5858">Now the application no longer cares whether it runs:</p>
<ul data-start="5859" data-end="5950">
<li data-section-id="113ihho" data-start="5859" data-end="5874">on bare metal</li>
<li data-section-id="1mrrym5" data-start="5875" data-end="5885">on a VPS</li>
<li data-section-id="149vzbh" data-start="5886" data-end="5901">in Kubernetes</li>
<li data-section-id="12u780q" data-start="5902" data-end="5910">in ECS</li>
<li data-section-id="4cz653" data-start="5911" data-end="5925">in Cloud Run</li>
<li data-section-id="suuk3c" data-start="5926" data-end="5950">or on someone’s laptop</li>
</ul>
<p data-start="5952" data-end="5958">Again:</p>
<ul data-start="5959" data-end="6033">
<li data-section-id="s7lqvb" data-start="5959" data-end="5998">infrastructure concerns move downward</li>
<li data-section-id="hitqb" data-start="5999" data-end="6033">application concerns stay upward</li>
</ul>
<p data-start="6035" data-end="6065">The boundaries become cleaner.</p>
<hr data-start="6067" data-end="6070" />
<h2 data-section-id="1fgmdxc" data-start="6072" data-end="6105">The Pattern Repeats Everywhere</h2>
<p data-start="6107" data-end="6180">Once you notice this pattern, you see it throughout software engineering.</p>
<h3 data-section-id="xbyhet" data-start="6182" data-end="6195">Templates</h3>
<p data-start="6197" data-end="6223">Template systems separate:</p>
<ul data-start="6224" data-end="6263">
<li data-section-id="61p7qa" data-start="6224" data-end="6243">presentation</li>
</ul>
<p>from</p>
<ul data-start="6224" data-end="6263">
<li data-section-id="1y7k1lo" data-start="6244" data-end="6263">application logic</li>
</ul>
<p data-start="6265" data-end="6303">HTML should not contain database code.</p>
<p data-start="6305" data-end="6359">Business logic should not contain giant blobs of HTML.</p>
<h3 data-section-id="6dh2fv" data-start="6361" data-end="6389">ORMs and Database Layers</h3>
<p data-start="6391" data-end="6440">DBI separates applications from database engines.</p>
<p data-start="6442" data-end="6492">ORMs separate applications from raw SQL structure.</p>
<p data-start="6494" data-end="6500">Again:</p>
<ul data-start="6501" data-end="6539">
<li data-section-id="1lr5kvv" data-start="6501" data-end="6539">implementation details move downward</li>
</ul>
<h3 data-section-id="k34kmu" data-start="6541" data-end="6558">Configuration</h3>
<p data-start="6560" data-end="6595">Configuration belongs outside code.</p>
<p data-start="6597" data-end="6663">Deployment-specific values should not be embedded in applications.</p>
<h3 data-section-id="ynhw6l" data-start="6665" data-end="6673">APIs</h3>
<p data-start="6675" data-end="6723">Clients should not care whether data comes from:</p>
<ul data-start="6724" data-end="6802">
<li data-section-id="6zn6pa" data-start="6724" data-end="6736">PostgreSQL</li>
<li data-section-id="179h5y9" data-start="6737" data-end="6744">Redis</li>
<li data-section-id="cg5dvi" data-start="6745" data-end="6762">another service</li>
<li data-section-id="1rclt94" data-start="6763" data-end="6772">a queue</li>
<li data-section-id="4lspky" data-start="6773" data-end="6785">flat files</li>
<li data-section-id="d24z0d" data-start="6786" data-end="6802">or magic elves</li>
</ul>
<p data-start="6804" data-end="6840">That’s the implementation’s problem.</p>
<hr data-start="6842" data-end="6845" />
<h2 data-section-id="1i68abl" data-start="6847" data-end="6894">The Goal Is Not Abstraction for Its Own Sake</h2>
<p data-start="6896" data-end="6980">Of course, experienced developers also know that abstractions can become ridiculous.</p>
<p data-start="6982" data-end="7017">Some abstractions simplify systems.</p>
<p data-start="7019" data-end="7086">Others merely hide complexity behind six additional layers of YAML.</p>
<p data-start="7088" data-end="7196">Joel Spolsky’s “Law of Leaky Abstractions” remains painfully relevant.</p>
<p data-start="7198" data-end="7233">The goal is not abstraction itself.</p>
<p data-start="7235" data-end="7285">The goal is to isolate genuinely volatile details.</p>
<p data-start="7287" data-end="7333">Good abstractions protect systems from change.</p>
<p data-start="7335" data-end="7375">Bad abstractions merely obscure reality.</p>
<hr data-start="7377" data-end="7380" />
<h2 data-section-id="1ja5yev" data-start="7382" data-end="7399">The Real Skill</h2>
<p data-start="7401" data-end="7480">The deeper lesson here is that software architecture is largely about deciding:</p>
<blockquote data-start="7482" data-end="7505">
<p data-start="7484" data-end="7505">“What belongs where?”</p>
</blockquote>
<p data-start="7507" data-end="7554">Experienced developers develop an instinct for:</p>
<ul data-start="7555" data-end="7675">
<li data-section-id="ost2yq" data-start="7555" data-end="7591">which details are likely to change</li>
<li data-section-id="cpoyga" data-start="7592" data-end="7639">which layers should know about which concerns</li>
<li data-section-id="1slr97u" data-start="7640" data-end="7675">and where boundaries should exist</li>
</ul>
<p data-start="7677" data-end="7770">That instinct is often more important than language choice, frameworks, or technology stacks.</p>
<p data-start="7772" data-end="7901" data-is-last-node="" data-is-only-node="">And if you spent the early 2000s rewriting CGI applications to run under mod_perl, you probably learned that lesson the hard way.</p>
<figure id="attachment_2442" aria-describedby="caption-attachment-2442" style="width: 300px" class="wp-caption aligncenter"><a href="https://perlhacks.com/wp-content/uploads/2026/05/perl-web-dev-infographic.png"><img fetchpriority="high" decoding="async" class="size-medium wp-image-2442" src="https://perlhacks.com/wp-content/uploads/2026/05/perl-web-dev-infographic-300x200.png" alt="Perl Web Development Over Time" width="300" height="200" srcset="https://perlhacks.com/wp-content/uploads/2026/05/perl-web-dev-infographic-300x200.png 300w, https://perlhacks.com/wp-content/uploads/2026/05/perl-web-dev-infographic-1024x683.png 1024w, https://perlhacks.com/wp-content/uploads/2026/05/perl-web-dev-infographic-768x512.png 768w, https://perlhacks.com/wp-content/uploads/2026/05/perl-web-dev-infographic.png 1536w" sizes="(max-width: 300px) 100vw, 300px" /></a><figcaption id="caption-attachment-2442" class="wp-caption-text">Perl Web Development Over Time</figcaption></figure><p>The post <a href="https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/">The Long Road from CGI to Containers</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/feed/</wfw:commentRss>
			<slash:comments>8</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2437</post-id>	</item>
		<item>
		<title>Summarising a Month of Git Activity with Perl (and a Little Help from AI)</title>
		<link>https://perlhacks.com/2026/04/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai/</link>
					<comments>https://perlhacks.com/2026/04/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai/#respond</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 12 Apr 2026 15:46:52 +0000</pubDate>
				<category><![CDATA[Miscellaneous]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2432</guid>

					<description><![CDATA[<p>Every month, I write a newsletter which (among other things) discusses some of the technical projects I’ve been working on. It’s a useful exercise — partly as a record for other people, but mostly as a way for me to remember what I’ve actually done. Because, as I’m sure you’ve noticed, it’s very easy to [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/04/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai/">Summarising a Month of Git Activity with Perl (and a Little Help from AI)</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p data-start="236" data-end="456">Every month, I write a newsletter which (among other things) discusses some of the technical projects I’ve been working on. It’s a useful exercise — partly as a record for other people, but mostly as a way for me to remember what I’ve actually done.</p>
<p data-start="458" data-end="520">Because, as I’m sure you’ve noticed, it’s very easy to forget.</p>
<p data-start="522" data-end="562">So this month, I decided to automate it.</p>
<p data-start="564" data-end="698">(And, if you’re interested in the end result, this is also a good excuse to mention that <a href="https://davecross.substack.com/">the newsletter</a> exists. Two birds, one stone.)</p>
<hr data-start="700" data-end="703" />
<h2 data-section-id="5120if" data-start="705" data-end="719">The Problem</h2>
<p data-start="721" data-end="998">All of my Git repositories live somewhere under <code data-start="769" data-end="780">/home/dave/git</code>. Over time, that’s become… less organised than it might be. Some repos are directly under that directory, others are buried a couple of levels down, and I’m fairly sure there are a few I’ve completely forgotten about.</p>
<p data-start="1000" data-end="1018">What I wanted was:</p>
<ul data-start="1020" data-end="1179">
<li data-section-id="15dtblb" data-start="1020" data-end="1046">Given a month and a year</li>
<li data-section-id="gd00pe" data-start="1047" data-end="1092">Find all Git repositories under that directory</li>
<li data-section-id="15pkg47" data-start="1093" data-end="1140">Identify which ones had commits in that month</li>
<li data-section-id="c2v37e" data-start="1141" data-end="1179">Summarise the work done in each repo</li>
</ul>
<p data-start="1181" data-end="1270">The first three are straightforward enough. The last one is where things get interesting.</p>
<hr data-start="1272" data-end="1275" />
<h2 data-section-id="olegil" data-start="1277" data-end="1304">Finding the Repositories</h2>
<p data-start="1306" data-end="1459">The first step is walking the directory tree and finding <code data-start="1363" data-end="1369">.git</code> directories. This is a classic Perl task — <code data-start="1413" data-end="1425">File::Find</code> still does exactly what you need.</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="w-full overflow-x-hidden overflow-y-auto">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<div class="cm-content q9tKkq_readonly">
<pre class="urvanov-syntax-highlighter-plain-tag">use v5.40;
use File::Find;

sub find_repos ($root) {
  my @repos;

  find(
    sub {
      return unless $_ eq '.git';
      push @repos, $File::Find::dir;
    },
    $root
  );

  return @repos;
}</pre><br />
This gives us a list of repository directories to inspect. It’s simple, robust, and doesn’t require any external dependencies.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p data-start="1832" data-end="1995">(There are, of course, other ways to do this — you could shell out to <code data-start="1902" data-end="1906">fd</code> or <code data-start="1910" data-end="1916">find</code>, for example — but keeping it in Perl keeps everything nicely self-contained.)</p>
<hr data-start="1997" data-end="2000" />
<h2 data-section-id="1kecnlb" data-start="2002" data-end="2032">Getting Commits for a Month</h2>
<p data-start="2034" data-end="2100">For each repo, we can run <code data-start="2060" data-end="2069">git log</code> with appropriate date filters.</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="pointer-events-none absolute inset-x-4 top-12 bottom-4">
<div class="pointer-events-none sticky z-40 shrink-0 z-1!">
<div class="sticky bg-token-border-light">
<pre class="urvanov-syntax-highlighter-plain-tag">sub commits_for_month ($repo, $since, $until) {
  my $cmd = sprintf(
    q{git -C %s log --since="%s" --until="%s" --pretty=format:"%%s"},
    $repo, $since, $until
  );

  my @commits = `$cmd`;
  chomp @commits;

  return @commits;
}</pre><br />
Where <code data-start="2374" data-end="2382">$since</code> and <code data-start="2387" data-end="2395">$until</code> define the month we’re interested in. I’ve been using something like:</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="w-full overflow-x-hidden overflow-y-auto">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<div class="cm-content q9tKkq_readonly">
<pre class="urvanov-syntax-highlighter-plain-tag">my $since = "$year-$month-01";
my $until = "$year-$month-31"; # good enough for this purpose</pre><br />
Yes, that’s a bit hand-wavy around month lengths. No, it doesn’t matter in practice. Sometimes “good enough” really is good enough.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<hr data-start="2706" data-end="2709" />
<h2 data-section-id="he36od" data-start="2711" data-end="2728">A Small Gotcha</h2>
<p data-start="2730" data-end="2868">It turns out I have a few repositories where I never got around to making a first commit. In that case, <code data-start="2834" data-end="2843">git log</code> helpfully explodes with:</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<blockquote>
<div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1">fatal: your current branch &#8216;master&#8217; does not have any commits yet</div>
</blockquote>
</div>
</div>
</div>
</div>
<div class="">
<div class="">Which is fair enough — but not helpful in a script that’s supposed to quietly churn through dozens of repositories.</div>
</div>
</div>
</div>
</div>
<p data-start="3062" data-end="3099">The fix is simply to ignore failures:</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="w-full overflow-x-hidden overflow-y-auto">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<div class="cm-content q9tKkq_readonly">
<pre class="urvanov-syntax-highlighter-plain-tag">my @commits = `$cmd 2&gt;/dev/null`;</pre><br />
If there are no commits, we just get an empty list and move on. No warnings, no noise.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p data-start="3236" data-end="3395">This is one of those little bits of defensive programming that makes the difference between a script you run once and a script you’re happy to run every month.</p>
<hr data-start="3397" data-end="3400" />
<h2 data-section-id="awll0y" data-start="3402" data-end="3425">Summarising the Work</h2>
<p data-start="3427" data-end="3489">Once we have a list of commit messages, we can summarise them.</p>
<p data-start="3491" data-end="3528">And this is where I cheated slightly.</p>
<p data-start="3530" data-end="3653">I used <a href="https://metacpan.org/pod/OpenAPI::Client"><span class="hover:entity-accent entity-underline inline cursor-pointer align-baseline"><span class="whitespace-normal">OpenAPI::Client::OpenAI</span></span></a> to feed the commit messages into an LLM and ask it to produce a short summary.</p>
<p data-start="3655" data-end="3683">Something along these lines:</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="w-full overflow-x-hidden overflow-y-auto">
<div class="relative z-0 flex max-w-full">
<div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr">
<div class="cm-scroller">
<div class="cm-content q9tKkq_readonly">
<pre class="urvanov-syntax-highlighter-plain-tag">use OpenAPI::Client::OpenAI;

sub summarise_commits ($commits) {
  my $client = OpenAPI::Client::OpenAI-&gt;new(
    api_key =&gt; $ENV{OPENAI_API_KEY},
  );

  my $text = join "\n", @$commits;

  my $response = $client-&gt;chat-&gt;completions-&gt;create({
    model =&gt; 'gpt-4.1-mini',
    messages =&gt; [{
      role =&gt; 'user',
      content =&gt; "Summarise the following commit messages:\n\n$text",
    }],
  });

<span class="ͼg">  return</span> <span class="ͼm">$response</span><span class="ͼg">-&gt;</span><span class="ͼf">choices</span><span class="ͼg">-&gt;</span>[<span class="ͼj">0</span>]<span class="ͼg">-&gt;</span><span class="ͼf">message</span><span class="ͼg">-&gt;</span><span class="ͼf">content</span>;
}</pre><br />
Is this overkill? Almost certainly.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p data-start="4262" data-end="4348">Could I have written some heuristics to group and summarise commit messages? Possibly.</p>
<p data-start="4350" data-end="4397">Would it have been as much fun? Definitely not.</p>
<p data-start="4399" data-end="4554">And in practice, it works remarkably well. Even messy, inconsistent commit messages tend to turn into something that looks like a coherent summary of work.</p>
<hr data-start="4556" data-end="4559" />
<h2 data-section-id="7axuzz" data-start="4561" data-end="4583">Putting It Together</h2>
<p data-start="4585" data-end="4599">For each repo:</p>
<ol data-start="4601" data-end="4720">
<li data-section-id="urgav0" data-start="4601" data-end="4631">Get commits for the month</li>
<li data-section-id="81uyn9" data-start="4632" data-end="4659">Skip if there are none</li>
<li data-section-id="1j7isda" data-start="4660" data-end="4683">Generate a summary</li>
<li data-section-id="1mhaa5b" data-start="4684" data-end="4720">Print the repo name and summary</li>
</ol>
<p data-start="4722" data-end="4754">The output looks something like:</p>
<div class="relative w-full mt-4 mb-1">
<div class="">
<div class="relative">
<div class="h-full min-h-0 min-w-0">
<div class="h-full min-h-0 min-w-0">
<div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl">
<div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback">
<div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1">
<pre class="urvanov-syntax-highlighter-plain-tag">my-project
-----------
Refactored database layer, added caching, and fixed several edge-case bugs.

another-project
---------------
Initial scaffolding, basic API endpoints, and deployment configuration.</pre><br />
Which is already a pretty good starting point for a newsletter.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<hr data-start="5129" data-end="5132" />
<h2 data-section-id="17mll61" data-start="5134" data-end="5155">A Nice Side Effect</h2>
<p data-start="5157" data-end="5246">One unexpected benefit of this approach is that it surfaces projects I’d forgotten about.</p>
<p data-start="5248" data-end="5428">Because the script walks the entire directory tree, it finds everything — including half-finished experiments, abandoned ideas, and repos I created at 11pm and never touched again.</p>
<p data-start="5430" data-end="5490">Sometimes that’s useful. Sometimes it’s mildly embarrassing.</p>
<p data-start="5492" data-end="5520">But it’s always interesting.</p>
<hr data-start="5522" data-end="5525" />
<h2 data-section-id="iju0gn" data-start="5527" data-end="5540">What Next?</h2>
<p data-start="5542" data-end="5578">This is very much a <strong data-start="5562" data-end="5577">first draft</strong>.</p>
<p data-start="5580" data-end="5727">It works, but it’s currently a script glued together with shell commands and assumptions about my directory structure. The obvious next step is to:</p>
<ul data-start="5729" data-end="5819">
<li data-section-id="1h509s5" data-start="5729" data-end="5761">Turn it into a proper module</li>
<li data-section-id="vtewgc" data-start="5762" data-end="5775">Add tests</li>
<li data-section-id="1pga30p" data-start="5776" data-end="5796">Clean up the API</li>
<li data-section-id="17ilcwr" data-start="5797" data-end="5819">Release it to CPAN</li>
</ul>
<p data-start="5821" data-end="5977">At that point, it becomes something other people might actually want to use — not just a personal tool with hard-coded paths and questionable date handling.</p>
<hr data-start="5979" data-end="5982" />
<h2 data-section-id="1ev3xsp" data-start="5984" data-end="6007">A Future Enhancement</h2>
<p data-start="6009" data-end="6088">One idea I particularly like is to run this automatically using GitHub Actions.</p>
<p data-start="6090" data-end="6102">For example:</p>
<ul data-start="6104" data-end="6230">
<li data-section-id="18b9lwk" data-start="6104" data-end="6119">Run monthly</li>
<li data-section-id="7q7o23" data-start="6120" data-end="6157">Generate summaries for that month</li>
<li data-section-id="j8aq" data-start="6158" data-end="6196">Commit the results to a repository</li>
<li data-section-id="bc5kpq" data-start="6197" data-end="6230">Publish them via GitHub Pages</li>
</ul>
<p data-start="6232" data-end="6326">Over time, that would build up a <strong data-start="6265" data-end="6296">permanent, browsable record</strong> of what I’ve been working on.</p>
<p data-start="6328" data-end="6355">It’s a nice combination of:</p>
<ul data-start="6357" data-end="6435">
<li data-section-id="tcan2v" data-start="6357" data-end="6371">automation</li>
<li data-section-id="1o0e7fe" data-start="6372" data-end="6389">documentation</li>
<li data-section-id="12ly6o5" data-start="6390" data-end="6435">and a gentle nudge towards accountability</li>
</ul>
<p data-start="6437" data-end="6486">Which is either a fascinating historical archive…</p>
<p data-start="6488" data-end="6563">…or a slightly alarming reminder of how many half-finished projects I have.</p>
<hr data-start="6565" data-end="6568" />
<h2 data-section-id="b03dx4" data-start="6570" data-end="6589">Closing Thoughts</h2>
<p data-start="6591" data-end="6737">This started as a small piece of automation to help me write a newsletter. But it’s turned into a nice example of what Perl is still very good at:</p>
<ul data-start="6739" data-end="6895">
<li data-section-id="s5ilte" data-start="6739" data-end="6766">Gluing systems together</li>
<li data-section-id="1wyovvn" data-start="6767" data-end="6798">Wrapping command-line tools</li>
<li data-section-id="zq59cj" data-start="6799" data-end="6833">Handling messy real-world data</li>
<li data-section-id="1tqzrvd" data-start="6834" data-end="6895">Adding just enough intelligence to make the output useful</li>
</ul>
<p data-start="6897" data-end="6959">And, occasionally, outsourcing the hard thinking to a machine.</p>
<p data-start="6897" data-end="6959">The code (such as it is currently is) is on GitHub at <a href="https://github.com/davorg/git-month-summary">https://github.com/davorg/git-month-summary</a>.</p>
<p data-start="6961" data-end="7080">If you’re interested in the kind of projects this helps summarise, you can find <a href="https://davecross.substack.com/">my monthly newsletter over on Substack</a>.</p>
<p data-start="7082" data-end="7155">And if I get round to turning this into a CPAN module, I’ll let you know &#8211; well, if you&#8217;re subscribed to the newsletter!</p>
<p data-start="7082" data-end="7155"><p>The post <a href="https://perlhacks.com/2026/04/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai/">Summarising a Month of Git Activity with Perl (and a Little Help from AI)</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/04/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2432</post-id>	</item>
	</channel>
</rss>
