<?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>Sat, 06 Jun 2026 17:29:33 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>
<site xmlns="com-wordpress:feed-additions:1">40678030</site>	<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/#respond</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>0</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>6</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>
		<item>
		<title>Writing a TOON Module for Perl</title>
		<link>https://perlhacks.com/2026/03/writing-a-toon-module-for-perl/</link>
					<comments>https://perlhacks.com/2026/03/writing-a-toon-module-for-perl/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 29 Mar 2026 17:46:14 +0000</pubDate>
				<category><![CDATA[CPAN]]></category>
		<category><![CDATA[cpan]]></category>
		<category><![CDATA[serialisation]]></category>
		<category><![CDATA[toon]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2417</guid>

					<description><![CDATA[<p>Every so often, a new data serialisation format appears and people get excited about it. Recently, one of those formats is **TOON** — Token-Oriented Object Notation. As the name suggests, it’s another way of representing the same kinds of data structures that you’d normally store in JSON or YAML: hashes, arrays, strings, numbers, booleans and [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/03/writing-a-toon-module-for-perl/">Writing a TOON Module for Perl</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Every so often, a new data serialisation format appears and people get excited about it. Recently, one of those formats is **TOON** — Token-Oriented Object Notation. As the name suggests, it’s another way of representing the same kinds of data structures that you’d normally store in JSON or YAML: hashes, arrays, strings, numbers, booleans and nulls.</p>
<p>So the obvious Perl question is: *“Ok, where’s the CPAN module?”*</p>
<p>This post explains what TOON is, why some people think it’s useful, and why I decided to write a Perl module for it — with an interface that should feel very familiar to anyone who has used JSON.pm.</p>
<p>I should point out  that I knew about [Data::Toon](https://metacpan.org/pod/Data::TOON) but I wanted something with an interface that was more like JSON.pm.</p>
<p>&#8212;</p>
<p>## What TOON Is</p>
<p>TOON stands for **Token-Oriented Object Notation**. It’s a textual format for representing structured data — the same data model as JSON:</p>
<p>* Objects (hashes)<br />
* Arrays<br />
* Strings<br />
* Numbers<br />
* Booleans<br />
* Null</p>
<p>The idea behind TOON is that it is designed to be **easy for both humans and language models to read and write**. It tries to reduce punctuation noise and make the structure of data clearer.</p>
<p>If you think of the landscape like this:</p>
<p>| Format | Human-friendly | Machine-friendly | Very common |<br />
| &#8212;&#8212; | &#8212;&#8212;&#8212;&#8212;&#8211; | &#8212;&#8212;&#8212;&#8212;&#8212;- | &#8212;&#8212;&#8212;&#8211; |<br />
| JSON   | Medium         | Very             | Yes         |<br />
| YAML   | High           | Medium           | Yes         |<br />
| TOON   | High           | High             | Not yet     |</p>
<p>TOON is trying to sit in the middle: simpler than YAML, more readable than JSON.</p>
<p>Whether it succeeds at that is a matter of taste — but it’s an interesting idea.</p>
<p>&#8212;</p>
<p>## TOON vs JSON vs YAML</p>
<p>It’s probably easiest to understand TOON by comparing it to JSON and YAML. Here’s the same “person” record written in all three formats.</p>
<p>### JSON</p>
<p>    {<br />
      &#8220;name&#8221;: &#8220;Arthur Dent&#8221;,<br />
      &#8220;age&#8221;: 42,<br />
      &#8220;email&#8221;: &#8220;arthur@example.com&#8221;,<br />
      &#8220;alive&#8221;: true,<br />
      &#8220;address&#8221;: {<br />
        &#8220;street&#8221;: &#8220;High Street&#8221;,<br />
        &#8220;city&#8221;: &#8220;Guildford&#8221;<br />
      },<br />
      &#8220;phones&#8221;: [<br />
        &#8220;01234 567890&#8221;,<br />
        &#8220;07700 900123&#8221;<br />
      ]<br />
    }</p>
<p>### YAML</p>
<p>    name: Arthur Dent<br />
    age: 42<br />
    email: arthur@example.com<br />
    alive: true<br />
    address:<br />
      street: High Street<br />
      city: Guildford<br />
    phones:<br />
      &#8211; 01234 567890<br />
      &#8211; 07700 900123</p>
<p>### TOON</p>
<p>    name: Arthur Dent<br />
    age: 42<br />
    email: arthur@example.com<br />
    alive: true<br />
    address:<br />
      street: High Street<br />
      city: Guildford<br />
    phones[2]: 01234 567890,07700 900123</p>
<p>You can see that TOON sits somewhere between JSON and YAML:</p>
<p>* Less punctuation and quoting than JSON<br />
* More explicit structure than YAML<br />
* Still very easy to parse<br />
* Still clearly structured for machines</p>
<p>That’s the idea, anyway.</p>
<p>&#8212;</p>
<p>## Why People Think TOON Is Useful</p>
<p>The current interest in TOON is largely driven by AI/LLM workflows.</p>
<p>People are using it because:</p>
<p>1. It is easier for humans to read than JSON.<br />
2. It is less ambiguous and complex than YAML.<br />
3. It maps cleanly to the JSON data model.<br />
4. It is relatively easy to parse.<br />
5. It works well in prompts and generated output.</p>
<p>In other words, it’s not trying to replace JSON for APIs, and it’s not trying to replace YAML for configuration files. It’s aiming at the space where humans and machines are collaborating on structured data.</p>
<p>You may or may not buy that argument — but it’s an interesting niche.</p>
<p>&#8212;</p>
<p>## Why I Wrote a Perl Module</p>
<p>I don’t have particularly strong opinions about TOON as a format. It might take off, it might not. We’ve seen plenty of “next big data format” ideas over the years.</p>
<p>But what I *do* have a strong opinion about is this:</p>
<p>> If a data format exists, then Perl should have a CPAN module for it that works the way Perl programmers expect.</p>
<p>Perl already has very good, very consistent interfaces for data serialisation:</p>
<p>* JSON<br />
* YAML<br />
* Storable<br />
* Sereal</p>
<p>They all tend to follow the same pattern, particularly the object-oriented interface:</p>
<p>    use JSON;<br />
    my $json = JSON->new->pretty->canonical;<br />
    my $text = $json->encode($data);<br />
    my $data = $json->decode($text);</p>
<p>So I wanted a TOON module that worked the same way.</p>
<p>&#8212;</p>
<p>## Design Goals</p>
<p>When designing the module, I had a few simple goals.</p>
<p>### 1. Familiar OO Interface</p>
<p>The primary interface should be object-oriented and feel like <code>JSON.pm</code>:</p>
<p>    use TOON;<br />
    my $toon = TOON->new<br />
                   ->pretty<br />
                   ->canonical<br />
                   ->indent(2);<br />
    my $text = $toon->encode($data);<br />
    my $data = $toon->decode($text);</p>
<p>If you already know JSON, you already know how to use TOON.</p>
<p>There are also convenience functions, but the OO interface is the main one.</p>
<p>### 2. Pure Perl Implementation</p>
<p>Version 0.001 is pure Perl. That means:</p>
<p>* Easy to install<br />
* No compiler required<br />
* Works everywhere Perl works</p>
<p>If TOON becomes popular and performance matters, someone can always write an XS backend later.</p>
<p>### 3. Clean Separation of Components</p>
<p>Internally, the module is split into:</p>
<p>* **Tokenizer** – turns text into tokens<br />
* **Parser** – turns tokens into Perl data structures<br />
* **Emitter** – turns Perl data structures into TOON text<br />
* **Error handling** – reports line/column errors cleanly</p>
<p>This makes it easier to test and maintain.</p>
<p>### 4. Do the Simple Things Well First</p>
<p>Version 0.001 supports:</p>
<p>* Scalars<br />
* Arrayrefs<br />
* Hashrefs<br />
* <code>undef</code> → null<br />
* Pretty printing<br />
* Canonical key ordering</p>
<p>It does **not** (yet) try to serialise blessed objects or do anything clever. That can come later if people actually want it.</p>
<p>&#8212;</p>
<p>## Example Usage (OO Style)</p>
<p>Here’s a simple Perl data structure:</p>
<p>    my $data = {<br />
      name   => &#8220;Arthur Dent&#8221;,<br />
      age    => 42,<br />
      drinks => [ &#8220;tea&#8221;, &#8220;coffee&#8221; ],<br />
      alive  => 1,<br />
    };</p>
<p>### Encoding</p>
<p>    use TOON;<br />
    my $toon = TOON->new->pretty->canonical;<br />
    my $text = $toon->encode($data);<br />
    print $text;</p>
<p>### Decoding</p>
<p>    use TOON;<br />
    my $toon = TOON->new;<br />
    my $data = $toon->decode($text);<br />
    print $data->{name};</p>
<p>### Convenience Functions</p>
<p>    use TOON qw(encode_toon decode_toon);<br />
    my $text = encode_toon($data);<br />
    my $data = decode_toon($text);</p>
<p>But the OO interface is where most of the flexibility lives.</p>
<p>&#8212;</p>
<p>## Command Line Tool</p>
<p>There’s also a command-line tool, <code>toon_pp</code>, similar to <code>json_pp</code>:</p>
<p>    cat data.toon | toon_pp</p>
<p>Which will pretty-print TOON data.</p>
<p>&#8212;</p>
<p>## Final Thoughts</p>
<p>I don’t know whether TOON will become widely used. Predicting the success of data formats is a fool’s game. But the cost of supporting it in Perl is low, and the potential usefulness is high enough to make it worth doing.</p>
<p>And fundamentally, this is how CPAN has always worked:</p>
<p>> See a problem. Write a module. Upload it. See if anyone else finds it useful.</p>
<p>So now Perl has a TOON module. And if you already know how to use <code>JSON.pm</code>, you already know how to use it.</p>
<p>That was the goal.</p><p>The post <a href="https://perlhacks.com/2026/03/writing-a-toon-module-for-perl/">Writing a TOON Module for Perl</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/03/writing-a-toon-module-for-perl/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2417</post-id>	</item>
		<item>
		<title>Still on the [b]leading edge</title>
		<link>https://perlhacks.com/2026/03/still-on-the-bleading-edge/</link>
					<comments>https://perlhacks.com/2026/03/still-on-the-bleading-edge/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sat, 21 Mar 2026 11:14:06 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[cpan]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[module metadata]]></category>
		<category><![CDATA[perl]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2413</guid>

					<description><![CDATA[<p>About eighteen months ago, I wrote a post called On the Bleading Edge about my decision to start using Perl’s new class feature in real code. I knew I was getting ahead of parts of the ecosystem. I knew there would be occasional pain. I decided the benefits were worth it. I still think that’s [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/03/still-on-the-bleading-edge/">Still on the [b]leading edge</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p data-start="349" data-end="680">About eighteen months ago, I wrote a post called <a class="decorated-link" href="https://perlhacks.com/2024/08/on-the-bleading-edge/" target="_new" rel="noopener" data-start="398" data-end="475"><em data-start="399" data-end="421">On the Bleading Edge</em></a> about my decision to start using Perl’s new <code data-start="520" data-end="527">class</code> feature in real code. I knew I was getting ahead of parts of the ecosystem. I knew there would be occasional pain. I decided the benefits were worth it.</p>
<p data-start="682" data-end="708">I still think that’s true.</p>
<p data-start="710" data-end="785">But every now and then, the bleading edge reminds you why it’s called that.</p>
<p data-start="787" data-end="1016">Recently, I lost a couple of days to a bug that turned out not to be in my code, not in the module I was installing, and not even in the module that module depended on — but in the installer’s understanding of modern Perl syntax.</p>
<p data-start="1018" data-end="1036">This is the story.</p>
<h2 data-section-id="vf7x19" data-start="1038" data-end="1052">The Symptom</h2>
<p data-start="1054" data-end="1265">I was building a Docker image for <a href="https://aphra.perlhacks.com/">Aphra</a>. As part of the build, I needed to install <a href="https://metacpan.org/pod/App::HTTPThis">App::HTTPThis</a>, which depends on <a href="https://metacpan.org/pod/Plack::App::DirectoryIndex">Plack::App::DirectoryIndex</a>, which depends on <a href="https://metacpan.org/pod/WebServer::DirIndex">WebServer::DirIndex</a>.</p>
<p data-start="1267" data-end="1307">The Docker build failed with this error:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">#13 45.66 --&gt; Working on WebServer::DirIndex
#13 45.66 Fetching https://www.cpan.org/authors/id/D/DA/DAVECROSS/WebServer-DirIndex-0.1.3.tar.gz ... OK
#13 45.83 Configuring WebServer-DirIndex-v0.1.3 ... OK
#13 46.21 Building WebServer-DirIndex-v0.1.3 ... OK
#13 46.75 Successfully installed WebServer-DirIndex-v0.1.3
#13 46.84 ! Installing the dependencies failed: Installed version (undef) of WebServer::DirIndex is not in range 'v0.1.0'
#13 46.84 ! Bailing out the installation for Plack-App-DirectoryIndex-v0.2.1.</pre><p></p>
<p data-start="1834" data-end="1879">Now, that’s a deeply confusing error message.</p>
<p data-start="1881" data-end="2046">It clearly says that WebServer::DirIndex was successfully installed. And then immediately says that the installed version is <code data-start="2008" data-end="2015">undef</code> and not in the required range.</p>
<p data-start="2048" data-end="2195">At this point you start wondering if you’ve somehow broken version numbering, or if there’s a packaging error, or if the dependency chain is wrong.</p>
<p data-start="2197" data-end="2316">But the version number in WebServer::DirIndex was fine. The module built. The tests passed. Everything looked normal.</p>
<p data-start="2318" data-end="2373">So why did the installer think the version was <code data-start="2365" data-end="2372">undef</code>?</p>
<h2 data-section-id="1qj34oz" data-start="2375" data-end="2399">When This Bug Appears</h2>
<p data-start="2401" data-end="2451">This only shows up in a fairly specific situation:</p>
<ul data-start="2453" data-end="2825">
<li data-section-id="14t2040" data-start="2453" data-end="2495">A module uses modern Perl <code data-start="2481" data-end="2488">class</code> syntax</li>
<li data-section-id="1t2er2a" data-start="2496" data-end="2529">The module defines a <code data-start="2519" data-end="2529">$VERSION</code></li>
<li data-section-id="14f6awz" data-start="2530" data-end="2606">Another module declares a prerequisite with a specific version requirement</li>
<li data-section-id="1noiju1" data-start="2607" data-end="2686">The installer tries to check the installed version without loading the module</li>
<li data-section-id="1713qy" data-start="2687" data-end="2737">It uses <a href="https://metacpan.org/pod/Module::Metadata">Module::Metadata</a> to extract <code data-start="2727" data-end="2737">$VERSION</code></li>
<li data-section-id="1jsln84" data-start="2738" data-end="2825">And the version of Module::Metadata it is using doesn’t properly understand <code data-start="2818" data-end="2825">class</code></li>
</ul>
<p data-start="2827" data-end="3100">If you don’t specify a version requirement, you’ll probably never see this. Which is why I hadn’t seen it before. I don’t often pin minimum versions of my own modules, but in this case, the modules are more tightly coupled than I’d like, and specific versions are required.</p>
<p data-start="3102" data-end="3144">So this bug only appears when you combine:</p>
<blockquote data-start="3146" data-end="3201">
<p data-start="3148" data-end="3201">modern Perl syntax + version checks + older toolchain</p>
</blockquote>
<p data-start="3203" data-end="3258">Which is pretty much the definition of “bleading edge”.</p>
<h2 data-section-id="1fxbvyb" data-start="3260" data-end="3279">The Real Culprit</h2>
<p data-start="3281" data-end="3386">The problem turned out to be an older version of Module::Metadata that had been fatpacked into <code data-start="3378" data-end="3385">cpanm</code>.</p>
<p data-start="3388" data-end="3637"><code data-start="3388" data-end="3395">cpanm</code> uses <code data-start="3401" data-end="3419">Module::Metadata</code> to inspect modules and extract <code data-start="3451" data-end="3461">$VERSION</code> without loading the module. But the older <code data-start="3504" data-end="3522">Module::Metadata</code> didn’t correctly understand the <code data-start="3555" data-end="3562">class</code> keyword, so it couldn’t work out which package the <code data-start="3614" data-end="3624">$VERSION</code> belonged to.</p>
<p data-start="3639" data-end="3699">So when it checked the installed version, it found… nothing.</p>
<p data-start="3701" data-end="3707">Hence:</p>
<blockquote data-start="3709" data-end="3784">
<p data-start="3711" data-end="3784">Installed version (undef) of WebServer::DirIndex is not in range &#8216;v0.1.0&#8217;</p>
</blockquote>
<p data-start="3786" data-end="3847">The version wasn’t wrong. The installer just couldn’t see it.</p>
<p data-start="3786" data-end="3847">An aside, you may find it amusing to hear an anecdote from my attempts to debug this problem.</p>
<p data-start="3786" data-end="3847">I spun up a new Ubuntu Docker container, installed <code>cpanm</code> and tried to install Plack::App::DirectoryIndex. Initially, this gave the same error message. At least the problem was easily reproducible.</p>
<p data-start="3786" data-end="3847">I then ran code that was very similar to the code <code>cpanm</code> uses to work out what a module&#8217;s version is.</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ perl -MModule::Metadata -E'say Module::Metadata-&gt;new_from_module("WebServer::DirIndex")-&gt;version'</pre><p>This displayed an empty string. I was really onto something here. Module::Metadata couldn&#8217;t find the version.</p>
<p>I was using Module::Metadata version 1.000037 and, looking at the change log on CPAN, I saw this:</p>
<blockquote>
<div class="line number3 index2 alt2"><code class="cpanchanges constants">1.000038  2023-04-28 11:25:40Z</code></div>
<div class="line number4 index3 alt1"><code class="cpanchanges functions">-</code> <code class="cpanchanges plain">detects "class" syntax</code></div>
</blockquote>
<div>I installed 1.000038 and reran my command.</div>
<div></div>
<div>
<pre class="urvanov-syntax-highlighter-plain-tag">$ perl -MModule::Metadata -E'say Module::Metadata-&gt;new_from_module("WebServer::DirIndex")-&gt;version'
0.1.3</pre><br />
That seemed conclusive. Excitedly, I reran the Docker build.</p>
<p>It failed again.</p>
<p>You&#8217;ve probably worked out why. But it took me a frustrating half an hour to work it out.</p>
<p><code>cpanm</code> doesn&#8217;t use the installed version of Module::Metadata. It uses its own, fatpacked version. Updating Module::Metadata wouldn&#8217;t fix my problem.</p>
</div>
<h2 data-section-id="1is82ri" data-start="3849" data-end="3866">The Workaround</h2>
<p data-start="3868" data-end="4067">I found a workaround. That was to add a redundant <code data-start="3918" data-end="3927">package</code> declaration alongside the <code data-start="3954" data-end="3961">class</code> declaration, so older versions of Module::Metadata can still identify the package that owns <code data-start="4056" data-end="4066">$VERSION</code>.</p>
<p data-start="4069" data-end="4093">So instead of just 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="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">class WebServer::DirIndex {
  our $VERSION = '0.1.3';
  ...
}</pre><br />
I now have this:</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="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">package WebServer::DirIndex;

class WebServer::DirIndex {
  our $VERSION = '0.1.3';
  ...
}</pre><br />
It looks unnecessary. And in a perfect world, it would be unnecessary.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p data-start="4373" data-end="4474">But it allows older tooling to work out the version correctly, and everything installs cleanly again.</p>
<h2 data-section-id="1qco1hd" data-start="4476" data-end="4493">The Proper Fix</h2>
<p data-start="4495" data-end="4547">Of course, the real fix was to update the toolchain.</p>
<p data-start="4549" data-end="4706">So I <a href="https://github.com/miyagawa/cpanminus/issues/697">raised an issue against App::cpanminus</a>, pointing out that the fatpacked Module::Metadata was too old to cope properly with modules that use <code data-start="4698" data-end="4705">class</code>.</p>
<p data-start="4708" data-end="4815">Tatsuhiko Miyagawa responded very quickly, and a new release of <code data-start="4772" data-end="4779">cpanm</code> appeared with an updated version of Module::Metadata.</p>
<p data-start="4817" data-end="4954">This is one of the nice things about the Perl ecosystem. Sometimes you report a problem and the right person fixes it almost immediately.</p>
<h2 data-section-id="16mnfbl" data-start="4956" data-end="4991">When Do I Remove the Workaround?</h2>
<p data-start="4993" data-end="5037">This leaves me with an interesting question.</p>
<p data-start="5039" data-end="5081">The correct fix is “use a recent <code data-start="5072" data-end="5079">cpanm</code>”.</p>
<p data-start="5083" data-end="5176">But the workaround is “add a redundant <code data-start="5122" data-end="5131">package</code> line so older tooling doesn’t get confused”.</p>
<p data-start="5178" data-end="5213">So when do I remove the workaround?</p>
<p data-start="5215" data-end="5247">The answer is probably: not yet.</p>
<p data-start="5249" data-end="5483">Because although a fixed <code data-start="5274" data-end="5281">cpanm</code> exists, that doesn’t mean everyone is using it. Old Docker base images, CI environments, bootstrap scripts, and long-lived servers can all have surprisingly ancient versions of <code data-start="5459" data-end="5466">cpanm</code> lurking in them.</p>
<p data-start="5485" data-end="5563">And the workaround is harmless. It just offends my sense of neatness slightly.</p>
<p data-start="5565" data-end="5720">So for now, the redundant <code data-start="5591" data-end="5600">package</code> line stays. Not because modern Perl needs it, but because parts of the world around modern Perl are still catching up.</p>
<h2 data-section-id="1ca3cnq" data-start="5722" data-end="5750">Life on the Bleading Edge</h2>
<p data-start="5752" data-end="5811">This is what life on the bleading edge actually looks like.</p>
<p data-start="5813" data-end="5880">Not dramatic crashes. Not language bugs. Not catastrophic failures.</p>
<p data-start="5882" data-end="6041">Just a tool, somewhere in the install chain, that looks at perfectly valid modern Perl code and quietly decides that your module doesn’t have a version number.</p>
<p data-start="6043" data-end="6115">And then you lose two days proving that you are not, in fact, going mad.</p>
<p data-start="6117" data-end="6171">But I’m still using <code data-start="6137" data-end="6144">class</code>. And I’m still happy I am.</p>
<p data-start="6173" data-end="6324">You just have to keep an eye on the whole toolchain — not just the language — when you decide to live a little closer to the future than everyone else.</p><p>The post <a href="https://perlhacks.com/2026/03/still-on-the-bleading-edge/">Still on the [b]leading edge</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/03/still-on-the-bleading-edge/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2413</post-id>	</item>
		<item>
		<title>Treating GitHub Copilot as a Contributor</title>
		<link>https://perlhacks.com/2026/02/treating-github-copilot-as-a-contributor/</link>
					<comments>https://perlhacks.com/2026/02/treating-github-copilot-as-a-contributor/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 22 Feb 2026 11:51:15 +0000</pubDate>
				<category><![CDATA[Miscellaneous]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2404</guid>

					<description><![CDATA[<p>For some time, we’ve talked about GitHub Copilot as if it were a clever autocomplete engine. It isn’t. Or rather, that&#8217;s not all it is. The interesting thing — the thing that genuinely changes how you work — is that you can assign GitHub issues to Copilot. And it behaves like a contributor. Over the [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/02/treating-github-copilot-as-a-contributor/">Treating GitHub Copilot as a Contributor</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p data-start="394" data-end="481">For some time, we’ve talked about GitHub Copilot as if it were a clever autocomplete engine.</p>
<p data-start="483" data-end="492">It isn’t.</p>
<p data-start="494" data-end="549">Or rather, that&#8217;s not all it is.</p>
<p data-start="551" data-end="671">The interesting thing — the thing that genuinely changes how you work — is that you can assign GitHub issues to Copilot.</p>
<p data-start="673" data-end="707">And it behaves like a contributor.</p>
<p data-start="709" data-end="1031">Over the past day, I’ve been doing exactly that on my new CPAN module, <a href="https://metacpan.org/pod/WebServer::DirIndex">WebServer::DirIndex</a>. I’ve opened issues, assigned them to Copilot, and watched a steady stream of pull requests land. Ten issues closed in about a day, each one implemented via a Copilot-generated PR, reviewed and merged like any other contribution.</p>
<p data-start="1033" data-end="1069">That still feels faintly futuristic. But it’s not “vibe coding”. It’s surprisingly structured.</p>
<p data-start="1131" data-end="1159">Let me explain how it works.</p>
<hr />
<h2 data-start="1166" data-end="1198">It Starts With a Proper Issue</h2>
<p data-start="1200" data-end="1236">This workflow depends on discipline. You don’t type “please refactor this” into a chat window. You create a proper GitHub issue. The sort you would assign to another human maintainer. For example, here are some of the recent issues Copilot handled in WebServer::DirIndex:</p>
<ul>
<li data-start="1200" data-end="1236">Add CPAN scaffolding</li>
<li data-start="1200" data-end="1236">Update the classes to use Feature::Compat::Class</li>
<li data-start="1200" data-end="1236">Replace DirHandle</li>
<li data-start="1200" data-end="1236">Add WebServer::DirIndex::File</li>
<li data-start="1200" data-end="1236">Move <code data-start="1625" data-end="1635">render()</code> method</li>
<li data-start="1200" data-end="1236">Use <code data-start="1651" data-end="1660">:reader</code> attribute where useful</li>
<li data-start="1200" data-end="1236">Remove dependency on Plack</li>
</ul>
<p data-start="1718" data-end="1764">Each one was a focused, bounded piece of work. Each one had clear expectations.</p>
<p data-start="1800" data-end="1886">The key is this: Copilot works best when you behave like a maintainer, not a magician.</p>
<p data-start="1888" data-end="2032">You describe the change precisely. You state constraints. You mention compatibility requirements. You indicate whether tests need to be updated.</p>
<p data-start="2034" data-end="2071">Then you assign the issue to Copilot.</p>
<p data-start="2073" data-end="2082">And wait.</p>
<hr />
<h2 data-start="2089" data-end="2116">The Pull Request Arrives</h2>
<p data-start="2118" data-end="2222">After a few minutes — sometimes ten, sometimes less — Copilot creates a branch and opens a pull request.</p>
<p data-start="2224" data-end="2240">The PR contains:</p>
<ul>
<li data-start="2224" data-end="2240">Code changes</li>
<li data-start="2224" data-end="2240">Updated or new tests</li>
<li data-start="2224" data-end="2240">A descriptive PR message</li>
</ul>
<p data-start="2314" data-end="2434">And because it’s a real PR, your CI runs automatically. The code is evaluated in the same way as any other contribution.</p>
<p data-start="2436" data-end="2558">This is already a major improvement over editor-based prompting. The work is isolated, reviewable, and properly versioned.</p>
<p data-start="2560" data-end="2628">But the most interesting part is what happens in the background.</p>
<hr />
<h2 data-start="2635" data-end="2660">Watching Copilot Think</h2>
<p data-start="2662" data-end="2761">If you visit the <strong data-start="2679" data-end="2689">Agents</strong> tab in the repository, you can see Copilot reasoning through the issue.</p>
<p data-start="2763" data-end="2821">It reads like a junior developer narrating their approach:</p>
<ul>
<li data-start="2763" data-end="2821">Interpreting the problem</li>
<li data-start="2763" data-end="2821">Identifying the relevant files</li>
<li data-start="2763" data-end="2821">Planning changes</li>
<li data-start="2763" data-end="2821">Considering test updates</li>
<li data-start="2763" data-end="2821">Running validation steps</li>
</ul>
<p data-start="2957" data-end="2982">And you can interrupt it.</p>
<p data-start="2984" data-end="3088">If it starts drifting toward unnecessary abstraction or broad refactoring, you can comment and steer it:</p>
<ul>
<li data-start="3092" data-end="3127">Please don’t change the public API.</li>
<li data-start="3092" data-end="3127">Avoid experimental Perl features.</li>
<li data-start="3092" data-end="3127">This must remain compatible with Perl 5.40.</li>
</ul>
<p data-start="3213" data-end="3244">It responds. It adjusts course.</p>
<p data-start="3246" data-end="3403">This ability to intervene mid-flight is one of the most useful aspects of the system. You are not passively accepting generated code — you’re supervising it.</p>
<hr />
<h2 data-start="3410" data-end="3448">Teaching Copilot About Your Project</h2>
<p data-start="3450" data-end="3562">Out of the box, Copilot doesn’t really know how your repository works. It sees code, but it doesn’t know policy.</p>
<p data-start="3564" data-end="3626">That’s where repository-level configuration becomes useful.</p>
<h3 data-start="3628" data-end="3665">1. Custom Repository Instructions</h3>
<p data-start="3667" data-end="3824">GitHub allows you to provide a <code data-start="3698" data-end="3731">.github/copilot-instructions.md</code> file that gives Copilot repository-specific guidance. The documentation for this lives here:</p>
<ul>
<li data-start="3826" data-end="3929">
<p id="title-h1" class="border-bottom-0"><a href="https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions">Adding repository custom instructions for GitHub Copilot</a></p>
</li>
</ul>
<p data-start="3931" data-end="3989">When GitHub offers to generate this file for you, say yes.</p>
<p data-start="3991" data-end="4018">Then customise it properly.</p>
<p data-start="4020" data-end="4056">In a CPAN module, I tend to include:</p>
<ul>
<li data-start="4020" data-end="4056">Minimum supported Perl version</li>
<li data-start="4020" data-end="4056">Whether Feature::Compat::Class is preferred</li>
<li data-start="4020" data-end="4056">Whether experimental features are forbidden</li>
<li data-start="4020" data-end="4056">CPAN layout expectations (<code data-start="4213" data-end="4219">lib/</code>, <code data-start="4221" data-end="4225">t/</code>, etc.)</li>
<li data-start="4020" data-end="4056">Test conventions (Test::More, no stray diagnostics)</li>
<li data-start="4020" data-end="4056">A strong preference for not breaking the public API</li>
</ul>
<p data-start="4342" data-end="4377">Without this file, Copilot guesses.</p>
<p data-start="4379" data-end="4439">With this file, Copilot aligns itself with your house style.</p>
<p data-start="4441" data-end="4469">That difference is impressive.</p>
<h3 data-start="4476" data-end="4530">2. Customising the Copilot Development Environment</h3>
<p data-start="4532" data-end="4647">There’s another piece that many people miss: Copilot can run a special workflow event called <code data-start="4625" data-end="4646">copilot_agent_setup</code>.</p>
<p data-start="4649" data-end="4750">You can define a workflow that prepares the environment Copilot works in. GitHub documents this here:</p>
<ul>
<li data-start="4649" data-end="4750">
<p id="title-h1" class="border-bottom-0"><a href="ttps://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/customize-the-agent-environment">Customizing the development environment for GitHub Copilot coding agent</a></p>
</li>
</ul>
<p data-start="4863" data-end="4910">In my Perl projects, I use this standard setup:</p>
<div class="w-full my-4">
<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 corner-superellipse/1.1 border-token-border-light bg-token-bg-elevated-secondary rounded-3xl">
<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">name: Copilot Setup Steps

on:
  workflow_dispatch:
  push:
    paths:
      - .github/workflows/copilot-setup-steps.yml
  pull_request:
    paths:
      - .github/workflows/copilot-setup-steps.yml

jobs:
  copilot-setup-steps:
    runs-on: ubuntu-latest
    permissions:
      contents: read
  steps:
    - name: Check out repository
      uses: actions/checkout@v4

    - name: Set up Perl 5.40
      uses: shogo82148/actions-setup-perl@v1
      with:
        perl-version: '5.40'

    - name: Install dependencies
      run: cpanm --installdeps --with-develop --notest .</pre><br />
(Obviously, that was originally written for me by Copilot!)</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="">
<div class="">This does two important things.</div>
</div>
</div>
</div>
</div>
<p data-start="5403" data-end="5470">Firstly, it ensures Copilot is working with the correct Perl version.</p>
<p data-start="5472" data-end="5621">Secondly, it installs the distribution dependencies, meaning Copilot can reason in a context that actually resembles my real development environment.</p>
<p data-start="5623" data-end="5690">Without this workflow, Copilot operates in a kind of generic space.</p>
<p data-start="5692" data-end="5791">With it, Copilot behaves like a contributor who has actually checked out your code and run <code data-start="5783" data-end="5790">cpanm</code>.</p>
<p data-start="5793" data-end="5824">That’s a useful difference.</p>
<hr />
<h2 data-start="5831" data-end="5852">Reviewing the Work</h2>
<p data-start="5854" data-end="5915">This is the part where it’s important not to get starry-eyed.</p>
<p data-start="5917" data-end="5951">I still review the PR carefully.</p>
<p data-start="5953" data-end="5969">I still check:</p>
<ul>
<li data-start="5953" data-end="5969">Has it changed behaviour unintentionally?</li>
<li data-start="5953" data-end="5969">Has it introduced unnecessary abstraction?</li>
<li data-start="5953" data-end="5969">Are the tests meaningful?</li>
<li data-start="5953" data-end="5969">Has it expanded scope beyond the issue?</li>
</ul>
<p>I check out the branch and run the tests. Exactly as I would with a PR from a human co-worker.</p>
<p data-start="6131" data-end="6213">You can request changes and reassign the PR to Copilot. It will revise its branch.</p>
<p data-start="6215" data-end="6282">The loop is fast. Faster than traditional asynchronous code review.</p>
<p data-start="6284" data-end="6320">But the responsibility is unchanged. You are still the maintainer.</p>
<hr />
<h2 data-start="6358" data-end="6385">Why This Feels Different</h2>
<p data-start="6387" data-end="6438">What’s happening here isn’t just “AI writing code”. It’s AI integrated into the contribution workflow:</p>
<ul>
<li data-start="6387" data-end="6438">Issues</li>
<li data-start="6387" data-end="6438">Structured reasoning</li>
<li data-start="6387" data-end="6438">Pull requests</li>
<li data-start="6387" data-end="6438">CI</li>
<li data-start="6387" data-end="6438">Review cycles</li>
</ul>
<p data-start="6572" data-end="6598">That architecture matters.</p>
<p data-start="6600" data-end="6660">It means you can use Copilot in a controlled, auditable way.</p>
<p data-start="6662" data-end="6746">In my experience with WebServer::DirIndex, this model works particularly well for:</p>
<ul>
<li data-start="6662" data-end="6746">Mechanical refactors</li>
<li data-start="6662" data-end="6746">Adding attributes (e.g. <code data-start="6799" data-end="6808">:reader</code> where appropriate)</li>
<li data-start="6662" data-end="6746">Removing dependencies</li>
<li data-start="6662" data-end="6746">Moving methods cleanly</li>
<li data-start="6662" data-end="6746">Adding new internal classes</li>
</ul>
<p data-start="6916" data-end="7037">It is less strong when the issue itself is vague or architectural. Copilot cannot infer the intent you didn’t articulate.</p>
<p data-start="7039" data-end="7177">But given a clear issue, it’s remarkably capable — even with modern Perl using tools like Feature::Compat::Class.</p>
<hr />
<h2 data-start="7184" data-end="7237">A Small but Important Point for the Perl Community</h2>
<p data-start="7239" data-end="7302">I&#8217;ve seen people saying that AI tools don’t handle Perl well. That has not been my experience.</p>
<p data-start="7338" data-end="7466">With a properly described issue, repository instructions, and a defined development environment, Copilot works competently with:</p>
<ul>
<li data-start="7338" data-end="7466">Modern Perl syntax</li>
<li data-start="7338" data-end="7466">CPAN distribution layouts</li>
<li data-start="7338" data-end="7466">Test suites</li>
<li data-start="7338" data-end="7466">Feature::Compat::Class (or whatever OO framework I&#8217;m using on a particular project)</li>
</ul>
<p data-start="7571" data-end="7605">The constraint isn’t the language. It’s how clearly you explain the task.</p>
<hr />
<h2 data-start="7652" data-end="7669">The Real Shift</h2>
<p data-start="7671" data-end="7734">The most interesting thing here isn’t that Copilot writes Perl. It’s that GitHub allows you to treat AI as a contributor.</p>
<ul>
<li data-start="7795" data-end="7813">You file an issue.</li>
<li data-start="7815" data-end="7829">You assign it.</li>
<li data-start="7831" data-end="7859">You supervise its reasoning.</li>
<li data-start="7861" data-end="7879">You review its PR.</li>
</ul>
<p data-start="7881" data-end="7903">It’s not autocomplete. It’s not magic. It’s just another developer on the project. One who works quickly, doesn’t argue, and reads your documentation very carefully.</p>
<p data-start="7881" data-end="7903">Have you been using AI tools to write or maintain Perl code? What successes (or failures!) have you had? Are there other tools I should be using?</p>
<hr />
<h2 data-start="7881" data-end="7903">Links</h2>
<p>If you want to have a closer look at the issues and PRs I&#8217;m talking about, here are some links?</p>
<ul>
<li><a href="https://github.com/davorg-cpan/webserver-dirindex">WebServer::DirIndex repo</a></li>
<li><a href="https://github.com/davorg-cpan/webserver-dirindex/issues?q=is%3Aissue%20state%3Aclosed">Closed issues</a></li>
<li><a href="https://github.com/davorg-cpan/webserver-dirindex/pulls?q=is%3Apr+is%3Aclosed">Closed PRs</a></li>
</ul><p>The post <a href="https://perlhacks.com/2026/02/treating-github-copilot-as-a-contributor/">Treating GitHub Copilot as a Contributor</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/02/treating-github-copilot-as-a-contributor/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2404</post-id>	</item>
		<item>
		<title>App::HTTPThis: the tiny web server I keep reaching for</title>
		<link>https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/</link>
					<comments>https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 04 Jan 2026 13:46:13 +0000</pubDate>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[http_this]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[static sites]]></category>
		<category><![CDATA[webdev]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2393</guid>

					<description><![CDATA[<p>Whenever I’m building a static website, I almost never start by reaching for Apache, nginx, Docker, or anything that feels like “proper infrastructure”. Nine times out of ten I just want a directory served over HTTP so I can click around, test routes, check assets, and see what happens in a real browser. For that [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/">App::HTTPThis: the tiny web server I keep reaching for</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p data-start="144" data-end="472">Whenever I’m building a static website, I almost never start by reaching for Apache, nginx, Docker, or anything that feels like “proper infrastructure”. Nine times out of ten I just want a directory served over HTTP so I can click around, test routes, check assets, and see what happens in a real browser.</p>
<p data-start="474" data-end="532">For that job, I’ve been using <a href="https://metacpan.org/dist/App-HTTPThis/view/bin/http_this"><strong data-start="504" data-end="521">App::HTTPThis</strong></a> for years.</p>
<p data-start="534" data-end="768">It’s a simple local web server you run from the command line. Point it at a directory, and it serves it. That’s it. No vhosts. No config bureaucracy. No “why is this module not enabled”. Just: <em data-start="727" data-end="767">run a command and you’ve got a website</em>.</p>
<h2 data-start="770" data-end="799">Why I’ve used it for years</h2>
<p data-start="801" data-end="865">Static sites are deceptively simple… right up until they aren’t.</p>
<ul data-start="867" data-end="1163">
<li data-start="867" data-end="940">
<p data-start="869" data-end="940">You want to check that relative links behave the way you think they do.</p>
</li>
<li data-start="941" data-end="1021">
<p data-start="943" data-end="1021">You want to confirm your CSS and images are loading with the paths you expect.</p>
</li>
<li data-start="1022" data-end="1163">
<p data-start="1024" data-end="1163">You want to reproduce “real HTTP” behaviour (caching headers, MIME types, directory handling) rather than viewing files directly from disk.</p>
</li>
</ul>
<p data-start="1165" data-end="1371">Sure, you <em data-start="1175" data-end="1180">can</em> open <code data-start="1186" data-end="1210">file:///.../index.html</code> in a browser, but that’s not the same thing as serving it over HTTP. And setting up Apache (or friends) feels like bringing a cement mixer to butter some toast.</p>
<p data-start="1373" data-end="1417">With <code data-start="1378" data-end="1389">http_this</code>, the workflow is basically:</p>
<ul data-start="1419" data-end="1510">
<li data-start="1419" data-end="1450">
<p data-start="1421" data-end="1450"><code data-start="1421" data-end="1425">cd</code> into your site directory</p>
</li>
<li data-start="1451" data-end="1473">
<p data-start="1453" data-end="1473">run a single command</p>
</li>
<li data-start="1474" data-end="1486">
<p data-start="1476" data-end="1486">open a URL</p>
</li>
<li data-start="1487" data-end="1510">
<p data-start="1489" data-end="1510">get on with your life</p>
</li>
</ul>
<p data-start="1512" data-end="1565">It’s the “tiny screwdriver” that’s always on my desk.</p>
<h2 data-start="1567" data-end="1588">Why I took it over</h2>
<p data-start="1590" data-end="1776">A couple of years ago, the original maintainer had (entirely reasonably!) become too busy elsewhere and the distribution wasn’t getting attention. That happens. Open source is like that.</p>
<p data-start="1880" data-end="2162">But I was using App::HTTPThis regularly, and I had one small-but-annoying itch: when you visited a directory URL, it would <em data-start="541" data-end="549">always</em> show a directory listing &#8211; even if that directory contained an <code data-start="613" data-end="625">index.html</code>. So instead of behaving like a typical web server (serve <code data-start="683" data-end="695">index.html</code> by default), it treated <code data-start="720" data-end="732">index.html</code> as just another file you had to click.</p>
<p data-start="1880" data-end="2162">That’s exactly the sort of thing you notice when you’re using a tool every day, and it was irritating enough that I volunteered to take over maintenance.</p>
<p data-start="1880" data-end="2162">(If you want to read more on this story, I wrote a couple of <a href="https://dev.to/davorg/the-story-behind-a-new-module-2gkp">blog</a> <a href="https://perlhacks.com/2023/05/mission-almost-accomplished/">posts</a>.)</p>
<h2 data-start="2164" data-end="2202">What I’ve done since taking it over</h2>
<p data-start="2204" data-end="2336">Most of the changes are about making the “serve a directory” experience smoother, without turning it into a kitchen-sink web server.</p>
<h3 data-start="2338" data-end="2368">1) Serve index pages by default (autoindex)</h3>
<p data-start="245" data-end="630">The first change was to make directory URLs behave like you’d expect: <strong data-start="1124" data-end="1174">if <code data-start="1129" data-end="1141">index.html</code> exists, serve it automatically</strong>. If it doesn’t, you still get a directory listing.</p>
<h3 data-start="2635" data-end="2662">2) Prettier index pages</h3>
<p data-start="2664" data-end="2731">Once autoindex was in place, I then turned my attention to the <em data-start="1106" data-end="1116">fallback</em> directory listing page. If there isn’t an <code data-start="1159" data-end="1171">index.html</code>, you still need a useful listing — but it doesn’t have to look like it fell out of 1998. So I cleaned up the listing output and made it a bit nicer to read when you <em data-start="1337" data-end="1341">do</em> end up browsing raw directories.</p>
<h3 data-start="2963" data-end="2983">3) A config file</h3>
<p data-start="2985" data-end="3086">Once you’ve used a tool for a while, you start to realise you run it <em data-start="3054" data-end="3068">the same way</em> most of the time.</p>
<p data-start="3088" data-end="3261">A config file lets you keep your common preferences in one place instead of re-typing options. It keeps the “one command” feel, but gives you repeatability when you want it.</p>
<h3 data-start="3263" data-end="3285">4) <code data-start="3270" data-end="3278">--host</code> option</h3>
<p data-start="3287" data-end="3367">The ability to control the host binding sounds like an edge case until it isn’t.</p>
<p data-start="3369" data-end="3388">Sometimes you want:</p>
<ul data-start="3390" data-end="3546">
<li data-start="3390" data-end="3427">
<p data-start="3392" data-end="3427">only <code data-start="3397" data-end="3408">localhost</code> access for safety;</p>
</li>
<li data-start="3428" data-end="3495">
<p data-start="3430" data-end="3495">access from other devices on your network (phone/tablet testing);</p>
</li>
<li data-start="3496" data-end="3546">
<p data-start="3498" data-end="3546">behaviour that matches a particular environment.</p>
</li>
</ul>
<p data-start="3548" data-end="3635">A <code data-start="3550" data-end="3558">--host</code> option gives you that control without adding complexity to the default case.</p>
<h2 data-start="3637" data-end="3679">The Bonjour feature (and what it’s for)</h2>
<p data-start="270" data-end="529">This is the part I only really appreciated recently: App::HTTPThis can advertise itself on your local network using <strong data-start="386" data-end="403">mDNS / DNS-SD</strong> &#8211; commonly called <em data-start="422" data-end="431">Bonjour</em> on Apple platforms, <em data-start="452" data-end="459">Avahi</em> on Linux, and various other names depending on who you’re talking to.</p>
<p data-start="531" data-end="592"><span data-start="546" data-end="591">I</span>t’s switched on with the <code data-start="574" data-end="582">--name</code> option.</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-bash">http_this --name MyService</code></div>
</div>
<p data-start="631" data-end="966">When you do that, <code data-start="649" data-end="660">http_this</code> publishes an <code data-start="674" data-end="686">_http._tcp</code> service on your local network with the instance name you chose (<code data-start="751" data-end="759">MyService</code> in this case). Any device on the same network that understands mDNS/DNS-SD can then discover it and resolve it to an address and port, without you having to tell anyone, “go to <code data-start="937" data-end="964">http://192.168.1.23:7007/</code>”.</p>
<p data-start="968" data-end="1389">Confession time: I ignored this feature for ages because I’d mentally filed it under “Apple-only magic” (Bonjour! very shiny! probably proprietary!). It turns out it’s not Apple-only at all; it’s a set of standard networking technologies that are supported on pretty much everything, just under a frankly ridiculous number of different names. So: <strong data-start="1303" data-end="1322">not Apple magic</strong>, just <strong data-start="1329" data-end="1364">local-network service discovery</strong> with a branding problem.</p>
<p data-start="1391" data-end="1708">Because I’d never really used it, I finally sat down and tested it properly after someone emailed me about it last week, and it worked nicely, nicely enough that I’ve now added a <a href="https://github.com/davorg-cpan/app-httpthis/blob/master/BONJOUR.md"><code data-start="1554" data-end="1566">BONJOUR.md</code></a> file to the repo with a practical explanation of what’s going on, how to enable it, and a few ways to browse/discover the advertised service.</p>
<p data-start="1710" data-end="1782">(If you’re curious, look for <code data-start="1739" data-end="1751">_http._tcp</code> and your chosen service name.)</p>
<p data-start="1710" data-end="1782">It’s a neat quality-of-life feature if you’re doing cross-device testing or helping someone else on the same network reach what you’re running.</p>
<h2 data-start="4801" data-end="4836">Related tools in the same family</h2>
<p data-start="4838" data-end="5017">App::HTTPThis is part of a little ecosystem of “run a thing <em data-start="4898" data-end="4904">here</em> quickly” command-line apps. If you like the shape of <code data-start="4958" data-end="4969">http_this</code>, you might also want to look at these siblings:</p>
<ul data-start="5019" data-end="5445">
<li data-start="5019" data-end="5172">
<p data-start="5021" data-end="5172"><a href="https://metacpan.org/pod/https_this"><strong data-start="5021" data-end="5035">https_this</strong></a> : like <code data-start="5043" data-end="5054">http_this</code>, but served over HTTPS (useful when you need to test secure contexts, service workers, APIs that require HTTPS, etc.)</p>
</li>
<li data-start="5173" data-end="5260">
<p data-start="5175" data-end="5260"><a href="https://metacpan.org/pod/cgi_this"><strong data-start="5175" data-end="5187">cgi_this</strong></a> : for quick CGI-style testing without setting up a full web server stack</p>
</li>
<li data-start="5261" data-end="5361">
<p data-start="5263" data-end="5361"><a href="https://metacpan.org/pod/dav_this"><strong data-start="5263" data-end="5275">dav_this</strong></a> : serves content over WebDAV (handy for testing clients or workflows that expect DAV)</p>
</li>
<li data-start="5362" data-end="5445">
<p data-start="5364" data-end="5445"><a href="https://metacpan.org/pod/ftp_this"><strong data-start="5364" data-end="5376">ftp_this</strong></a> : quick FTP server for those rare-but-real moments when you need one</p>
</li>
</ul>
<p data-start="5447" data-end="5586">They all share the same basic philosophy: remove the friction between “I have a directory” and “I want to interact with it like a service”.</p>
<h2 data-start="5588" data-end="5602">Wrapping up</h2>
<p data-start="5604" data-end="5789">I like tools that do one job, do it well, and get out of the way. App::HTTPThis has been that tool for me for years and it’s been fun (and useful) to nudge it forward as a maintainer.</p>
<p data-start="5791" data-end="5939">If you’re doing any kind of static site work — docs sites, little prototypes, generated output, local previews — it’s worth keeping in your toolbox.</p>
<p data-start="5941" data-end="6072">And if you’ve got ideas, bug reports, or platform notes (especially around Bonjour/Avahi weirdness), I’m always <a href="https://github.com/davorg-cpan/app-httpthis/issues">happy to hear them</a>.</p><p>The post <a href="https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/">App::HTTPThis: the tiny web server I keep reaching for</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2393</post-id>	</item>
		<item>
		<title>Behind the scenes at Perl School Publishing</title>
		<link>https://perlhacks.com/2025/12/behind-the-scenes-at-perl-school-publishing/</link>
					<comments>https://perlhacks.com/2025/12/behind-the-scenes-at-perl-school-publishing/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 14 Dec 2025 17:46:53 +0000</pubDate>
				<category><![CDATA[Books]]></category>
		<category><![CDATA[epub]]></category>
		<category><![CDATA[linter]]></category>
		<category><![CDATA[pandoc]]></category>
		<category><![CDATA[perlschool]]></category>
		<category><![CDATA[wkhtmltopdf]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2376</guid>

					<description><![CDATA[<p>We’ve just published a new Perl School book: Design Patterns in Modern Perl by Mohammad Sajid Anwar. It’s been a while since we last released a new title, and in the meantime, the world of eBooks has moved on – Amazon don’t use .mobi any more, tools have changed, and my old “it mostly works [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2025/12/behind-the-scenes-at-perl-school-publishing/">Behind the scenes at Perl School Publishing</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p data-start="150" data-end="255">We’ve just published a new <a href="https://perlschool.com/">Perl School</a> book: <a href="https://perlschool.com/books/design-patterns/"><em data-start="195" data-end="227">Design Patterns in Modern Perl</em></a> by Mohammad Sajid Anwar.</p>
<p data-start="257" data-end="501">It’s been a while since we last released a new title, and in the meantime, the world of eBooks has moved on – Amazon don’t use <code data-start="383" data-end="390">.mobi</code> any more, tools have changed, and my old “it mostly works if you squint” build pipeline was starting to creak.</p>
<p data-start="503" data-end="803">On top of that, we had a hard deadline: we wanted the book ready in time for the London Perl Workshop. As the date loomed, last-minute fixes and manual tweaks became more and more terrifying. We really needed a reliable, reproducible way to go from manuscript to “good quality PDF + EPUB” every time.</p>
<p data-start="805" data-end="1023">So over the last couple of weeks, I’ve been rebuilding the Perl School book pipeline from the ground up. This post is the story of that process, the tools I ended up using, and how you can steal it for your own books.</p>
<hr data-start="1025" data-end="1028" />
<h2 data-start="1030" data-end="1077">The old world, and why it wasn’t good enough</h2>
<p data-start="1079" data-end="1148">The original Perl School pipeline dates back to a very different era:</p>
<ul data-start="1150" data-end="1287">
<li data-start="1150" data-end="1180">
<p data-start="1152" data-end="1180">Amazon wanted <code data-start="1166" data-end="1173">.mobi</code> files.</p>
</li>
<li data-start="1181" data-end="1207">
<p data-start="1183" data-end="1207">EPUB support was patchy.</p>
</li>
<li data-start="1208" data-end="1287">
<p data-start="1210" data-end="1287">I was happy to glue things together with shell scripts and hope for the best.</p>
</li>
</ul>
<p data-start="1289" data-end="1528">It worked… until it didn’t. Each book had slightly different scripts, slightly different assumptions, and a slightly different set of last-minute manual tweaks. It certainly wasn’t something I’d hand to a new author and say, “trust this”.</p>
<p data-start="1530" data-end="1733">Coming back to it for <em data-start="1552" data-end="1584">Design Patterns in Modern Perl</em> made that painfully obvious. The book itself is modern and well-structured; the pipeline that produced it shouldn’t feel like a relic.</p>
<hr data-start="1735" data-end="1738" />
<h2 data-start="1740" data-end="1806">Choosing tools: Pandoc and <code data-start="1770" data-end="1783">wkhtmltopdf</code> (and no LaTeX, thanks)</h2>
<p data-start="1808" data-end="1856">The new pipeline is built around two main tools:</p>
<ul data-start="1858" data-end="2088">
<li data-start="1858" data-end="1993">
<p data-start="1860" data-end="1993"><a href="https://pandoc.org/"><strong data-start="1860" data-end="1870">Pandoc</strong></a> – the Swiss Army knife of document conversion. It can take Markdown/Markua plus metadata and produce HTML, EPUB, and much, much more.</p>
</li>
<li data-start="1994" data-end="2088">
<p data-start="1996" data-end="2088"><a href="https://wkhtmltopdf.org/"><strong data-start="1996" data-end="2013"><code data-start="1998" data-end="2011">wkhtmltopdf</code></strong></a> – which turns HTML into a print-ready PDF using a headless browser engine.</p>
</li>
</ul>
<p data-start="2090" data-end="2340">Why not LaTeX? Because I’m allergic. LaTeX is enormously powerful, but every time I’ve tried to use it seriously, I end up debugging page breaks in a language I don’t enjoy. HTML + CSS I can live with; browsers I can reason about. So the PDF route is:</p>
<ul>
<li data-start="2344" data-end="2398">Markdown → HTML (via Pandoc) → PDF (via <code data-start="2384" data-end="2397">wkhtmltopdf</code>)</li>
</ul>
<p data-start="2400" data-end="2422">And the EPUB route is:</p>
<ul>
<li data-start="2426" data-end="2483">Markdown → EPUB (via Pandoc) → validated with <code data-start="2472" data-end="2483">epubcheck</code></li>
</ul>
<p data-start="2485" data-end="2699">The front matter (cover page, title page, copyright, etc.) is generated with Template Toolkit from a simple <code data-start="2593" data-end="2612">book-metadata.yml</code> file, and then stitched together with the chapters to produce a nice, consistent book.</p>
<p data-start="2701" data-end="2755">That got us a long way… but then a reader found a bug.</p>
<hr data-start="2757" data-end="2760" />
<h2 data-start="2762" data-end="2786">The iBooks bug report</h2>
<p data-start="2788" data-end="3027">Shortly after publication, I got an email from a reader who’d bought the Leanpub EPUB and was reading it in Apple Books (iBooks). Instead of happily flipping through <em data-start="2949" data-end="2981">Design Patterns in Modern Perl</em>, they were greeted with a big pink error box.</p>
<p data-start="3029" data-end="3066">Apple’s error message boiled down to:</p>
<blockquote data-start="3068" data-end="3122">
<p data-start="3070" data-end="3122">There’s something wrong with the XHTML in this EPUB.</p>
</blockquote>
<p data-start="3124" data-end="3198">That was slightly worrying. But, hey, every day is a learning opportunity. And, after a bit of digging, this is what I found out.</p>
<p data-start="3124" data-end="3198">EPUB 3 files are essentially a ZIP containing:</p>
<ul data-start="3200" data-end="3274">
<li data-start="3200" data-end="3223">
<p data-start="3202" data-end="3223">XHTML content files</p>
</li>
<li data-start="3224" data-end="3249">
<p data-start="3226" data-end="3249">a bit of XML metadata</p>
</li>
<li data-start="3250" data-end="3274">
<p data-start="3252" data-end="3274">CSS, images, and so on</p>
</li>
</ul>
<p data-start="3276" data-end="3390">Apple Books is quite strict about the “X” in XHTML: it expects well-formed XML, not just “kind of valid HTML”. So when working with EPUB, you need to forget all of that nice HTML5 flexibility that you&#8217;ve got used to over the last decade or so.</p>
<p data-start="3392" data-end="3487">The first job was to see if we could reproduce the error and work out where it was coming from.</p>
<hr data-start="3489" data-end="3492" />
<h2 data-start="3494" data-end="3520">Discovering <code data-start="3509" data-end="3520">epubcheck</code></h2>
<p data-start="3522" data-end="3540">Enter <code data-start="3528" data-end="3539">epubcheck</code>.</p>
<p data-start="3542" data-end="3735"><code data-start="3542" data-end="3553">epubcheck</code> is the reference validator for EPUB files. Point it at an <code data-start="3612" data-end="3619">.epub</code> and it will unpack it, parse all the XML/XHTML, check the metadata and manifest, and tell you exactly what’s wrong.</p>
<p data-start="3737" data-end="3786">Running it on the book immediately produced this:</p>
<blockquote data-start="3788" data-end="3895">
<p data-start="3790" data-end="3895">Fatal Error while parsing file: The element type <code data-start="3839" data-end="3843">br</code> must be terminated by the matching end-tag <code data-start="3887" data-end="3894">&lt;/br&gt;</code>.</p>
</blockquote>
<p data-start="3897" data-end="3935">That’s the XML parser’s way of saying:</p>
<ul data-start="3937" data-end="4043">
<li data-start="3937" data-end="3963">
<p data-start="3939" data-end="3963">In HTML, <code data-start="3948" data-end="3954">&lt;br&gt;</code> is fine.</p>
</li>
<li data-start="3964" data-end="4043">
<p data-start="3966" data-end="4043">In XHTML (which is XML), you must use <code data-start="4004" data-end="4012">&lt;br /&gt;</code> (self-closing) or <code data-start="4031" data-end="4042">&lt;br&gt;&lt;/br&gt;</code>.</p>
</li>
</ul>
<p data-start="4045" data-end="4109">And there were a number of these scattered across a few chapters.</p>
<p data-start="4111" data-end="4309">In other words: perfectly reasonable raw HTML in the manuscript had been passed straight through by Pandoc into the EPUB, but that HTML was not strictly valid XHTML, so Apple Books rejected it. I should note at this point that the documentation for Pandoc&#8217;s EPUB creation explicitly says that it won&#8217;t touch HTML fragments it finds in a Markdown file when converting it to EPUB. It&#8217;s down to the author to ensure they&#8217;re using valid XHTML</p>
<hr data-start="4311" data-end="4314" />
<h2 data-start="4316" data-end="4349">A quick (but not scalable) fix</h2>
<p data-start="4351" data-end="4418">Under time pressure, the quickest way to confirm the diagnosis was:</p>
<ol data-start="4420" data-end="4615">
<li data-start="4420" data-end="4448">
<p data-start="4423" data-end="4448">Unzip the generated EPUB.</p>
</li>
<li data-start="4449" data-end="4482">
<p data-start="4452" data-end="4482">Open the offending XHTML file.</p>
</li>
<li data-start="4483" data-end="4543">
<p data-start="4486" data-end="4543">Manually turn <code data-start="4500" data-end="4506">&lt;br&gt;</code> into <code data-start="4512" data-end="4520">&lt;br /&gt;</code> in a couple of places.</p>
</li>
<li data-start="4544" data-end="4563">
<p data-start="4547" data-end="4563">Re-zip the EPUB.</p>
</li>
<li data-start="4564" data-end="4589">
<p data-start="4567" data-end="4589">Run <code data-start="4571" data-end="4582">epubcheck</code> again.</p>
</li>
<li data-start="4590" data-end="4615">
<p data-start="4593" data-end="4615">Try it in Apple Books.</p>
</li>
</ol>
<p data-start="4617" data-end="4741">That worked. The errors vanished, <code data-start="4651" data-end="4662">epubcheck</code> was happy, and the reader confirmed that the fixed file opened fine in iBooks.</p>
<p data-start="4743" data-end="4755">But clearly:</p>
<blockquote data-start="4757" data-end="4821">
<p data-start="4759" data-end="4821"><em data-start="4759" data-end="4819">Open the EPUB in a text editor and fix the XHTML by hand</em></p>
</blockquote>
<p data-start="4823" data-end="4864">is not a sustainable publishing strategy.</p>
<p data-start="4866" data-end="4973">So the next step was to move from “hacky manual fix” to “the pipeline prevents this from happening again”.</p>
<hr data-start="4975" data-end="4978" />
<h2 data-start="4980" data-end="5020">HTML vs XHTML, and why linters matter</h2>
<p data-start="5022" data-end="5083">The underlying issue is straightforward once you remember it:</p>
<ul data-start="5085" data-end="5366">
<li data-start="5085" data-end="5167">
<p data-start="5087" data-end="5167">HTML is very forgiving. Browsers will happily fix up all kinds of broken markup.</p>
</li>
<li data-start="5168" data-end="5366">
<p data-start="5170" data-end="5210">XHTML is XML, so it’s not forgiving:</p>
<ul data-start="5213" data-end="5366">
<li data-start="5213" data-end="5288">
<p data-start="5215" data-end="5288">empty elements must be self-closed (<code data-start="5251" data-end="5259">&lt;br /&gt;</code>, <code data-start="5261" data-end="5270">&lt;img /&gt;</code>, <code data-start="5272" data-end="5280">&lt;hr /&gt;</code>, etc.),</p>
</li>
<li data-start="5291" data-end="5335">
<p data-start="5293" data-end="5335">tags must be properly nested and balanced,</p>
</li>
<li data-start="5338" data-end="5366">
<p data-start="5340" data-end="5366">attributes must be quoted.</p>
</li>
</ul>
</li>
</ul>
<p data-start="5368" data-end="5499">EPUB 3 content files are XHTML. If you feed them sloppy HTML, some readers (like Apple Books) will just refuse to load the chapter.</p>
<p data-start="5501" data-end="5603">So I added a manuscript HTML linter to the toolchain, before we ever get to Pandoc or <code data-start="5591" data-end="5602">epubcheck</code>.</p>
<p data-start="5605" data-end="5625">Roughly, the linter:</p>
<ul data-start="5627" data-end="5915">
<li data-start="5627" data-end="5730">
<p data-start="5629" data-end="5730">Reads the manuscript (ignoring fenced code blocks so it doesn’t complain about <code data-start="5708" data-end="5711">&lt;</code> in Perl examples).</p>
</li>
<li data-start="5731" data-end="5762">
<p data-start="5733" data-end="5762">Extracts any raw HTML chunks.</p>
</li>
<li data-start="5763" data-end="5812">
<p data-start="5765" data-end="5812">Wraps those chunks in a temporary root element.</p>
</li>
<li data-start="5813" data-end="5867">
<p data-start="5815" data-end="5867">Uses <code data-start="5820" data-end="5833">XML::LibXML</code> to check they’re well-formed XML.</p>
</li>
<li data-start="5868" data-end="5915">
<p data-start="5870" data-end="5915">Reports any errors with file and line number.</p>
</li>
</ul>
<p data-start="5917" data-end="6043">It’s not trying to be a full HTML validator; it’s just checking: “If this HTML ends up in an EPUB, will the XML parser choke?”</p>
<p data-start="6045" data-end="6124">That would have caught the <code data-start="6072" data-end="6078">&lt;br&gt;</code> problem before the book ever left my machine.</p>
<hr data-start="6126" data-end="6129" />
<h2 data-start="6131" data-end="6181">Hardening the pipeline: <code data-start="6158" data-end="6169">epubcheck</code> in the loop</h2>
<p data-start="6183" data-end="6310">The linter catches the obvious issues in the manuscript; <code data-start="6244" data-end="6255">epubcheck</code> is still the final authority on the finished EPUB.</p>
<p data-start="6312" data-end="6348">So the pipeline now looks like this:</p>
<ol data-start="6350" data-end="6818">
<li data-start="6350" data-end="6433">
<p data-start="6353" data-end="6433"><strong data-start="6353" data-end="6381">Lint the manuscript HTML</strong><br data-start="6381" data-end="6384" />Catch broken raw HTML/XHTML before conversion.</p>
</li>
<li data-start="6434" data-end="6670">
<p data-start="6437" data-end="6475"><strong data-start="6437" data-end="6473">Build PDF + EPUB via <code data-start="6460" data-end="6471">make_book</code></strong></p>
<ul data-start="6479" data-end="6670">
<li data-start="6479" data-end="6549">
<p data-start="6481" data-end="6549">Generate front matter from metadata (cover, title pages, copyright).</p>
</li>
<li data-start="6553" data-end="6594">
<p data-start="6555" data-end="6594">Turn Markdown + front matter into HTML.</p>
</li>
<li data-start="6598" data-end="6640">
<p data-start="6600" data-end="6640">Use <code data-start="6604" data-end="6617">wkhtmltopdf</code> for a print-ready PDF.</p>
</li>
<li data-start="6644" data-end="6670">
<p data-start="6646" data-end="6670">Use Pandoc for the EPUB.</p>
</li>
</ul>
</li>
<li data-start="6671" data-end="6756">
<p data-start="6674" data-end="6756"><strong data-start="6674" data-end="6705">Run <code data-start="6680" data-end="6691">epubcheck</code> on the EPUB</strong><br data-start="6705" data-end="6708" />Ensure the final file is standards-compliant.</p>
</li>
<li data-start="6757" data-end="6818">
<p data-start="6760" data-end="6818">Only then do we upload it to Leanpub and Amazon, making it available to eager readers.</p>
</li>
</ol>
<p data-start="6820" data-end="7033">The nice side-effect of this is that <em data-start="6857" data-end="6862">any</em> future changes (new CSS, new template, different metadata) still go through the same gauntlet. If something breaks, the pipeline shouts at me long before a reader has to.</p>
<hr data-start="7035" data-end="7038" />
<h2 data-start="7040" data-end="7092">Docker and GitHub Actions: making it reproducible</h2>
<p data-start="7094" data-end="7209">Having a nice Perl script and a list of tools installed on my laptop is fine for a solo project; it’s not great if:</p>
<ul data-start="7211" data-end="7317">
<li data-start="7211" data-end="7267">
<p data-start="7213" data-end="7267">other authors might want to build their own drafts, or</p>
</li>
<li data-start="7268" data-end="7317">
<p data-start="7270" data-end="7317">I want the build to happen automatically in CI.</p>
</li>
</ul>
<p data-start="7319" data-end="7414">So the next step was to package everything into a Docker image and wire it into GitHub Actions.</p>
<p data-start="7416" data-end="7472">The Docker image is based on a slim Ubuntu and includes:</p>
<ul data-start="7474" data-end="7666">
<li data-start="7474" data-end="7536">
<p data-start="7476" data-end="7536">Perl + <code data-start="7483" data-end="7490">cpanm</code> + all CPAN modules from the repo’s <code data-start="7526" data-end="7536">cpanfile</code></p>
</li>
<li data-start="7537" data-end="7547">
<p data-start="7539" data-end="7547"><code data-start="7539" data-end="7547">pandoc</code></p>
</li>
<li data-start="7548" data-end="7563">
<p data-start="7550" data-end="7563"><code data-start="7550" data-end="7563">wkhtmltopdf</code></p>
</li>
<li data-start="7564" data-end="7584">
<p data-start="7566" data-end="7584">Java + <code data-start="7573" data-end="7584">epubcheck</code></p>
</li>
<li data-start="7585" data-end="7666">
<p data-start="7587" data-end="7666">The Perl School utility scripts themselves (<code data-start="7631" data-end="7642">make_book</code>, <code data-start="7644" data-end="7659">check_ms_html</code>, etc.)</p>
</li>
</ul>
<p data-start="7668" data-end="7706">The workflow in a book repo is simple:</p>
<ul data-start="7708" data-end="7920">
<li data-start="7708" data-end="7749">
<p data-start="7710" data-end="7749">Mount the book’s Git repo into <code data-start="7741" data-end="7748">/work</code>.</p>
</li>
<li data-start="7750" data-end="7795">
<p data-start="7752" data-end="7795">Run <code data-start="7756" data-end="7771">check_ms_html</code> to lint the manuscript.</p>
</li>
<li data-start="7796" data-end="7856">
<p data-start="7798" data-end="7856">Run <code data-start="7802" data-end="7813">make_book</code> to build <code data-start="7823" data-end="7836">built/*.pdf</code> and <code data-start="7841" data-end="7855">built/*.epub</code>.</p>
</li>
<li data-start="7857" data-end="7887">
<p data-start="7859" data-end="7887">Run <code data-start="7863" data-end="7874">epubcheck</code> on the EPUB.</p>
</li>
<li data-start="7888" data-end="7920">
<p data-start="7890" data-end="7920">Upload the <code data-start="7901" data-end="7909">built/</code> artefacts.</p>
</li>
</ul>
<p data-start="7922" data-end="8174">GitHub Actions then uses that same image as a <code data-start="7968" data-end="7980">container</code> for the job, so every push or pull request can build the book in a clean, consistent environment, without needing each author to install Pandoc, <code>wkhtmltopdf</code>, Java, and a large chunk of CPAN locally.</p>
<hr data-start="8176" data-end="8179" />
<h2 data-start="8181" data-end="8210">Why I’m making this public</h2>
<p data-start="8212" data-end="8246">At this point, the pipeline feels:</p>
<ul data-start="8248" data-end="8397">
<li data-start="8248" data-end="8291">
<p data-start="8250" data-end="8291">modern (Pandoc, HTML/CSS layout, EPUB 3),</p>
</li>
<li data-start="8292" data-end="8322">
<p data-start="8294" data-end="8322">robust (lint + <code data-start="8309" data-end="8320">epubcheck</code>),</p>
</li>
<li data-start="8323" data-end="8357">
<p data-start="8325" data-end="8357">reproducible (Docker + Actions),</p>
</li>
<li data-start="8358" data-end="8397">
<p data-start="8360" data-end="8397">and not tied to Perl in any deep way.</p>
</li>
</ul>
<p data-start="8399" data-end="8618">Yes, <em data-start="8404" data-end="8436">Design Patterns in Modern Perl</em> is a Perl book, and the utilities live under the “Perl School” banner, but nothing is stopping you from using the same setup for your own book on whatever topic you care about.</p>
<p data-start="8620" data-end="8764">So I’ve made the utilities available in a public repository (the <a href="https://github.com/davorg/perlschool-util"><code data-start="8685" data-end="8702">perlschool-util</code></a> repo on GitHub). There you’ll find:</p>
<ul data-start="8766" data-end="8907">
<li data-start="8766" data-end="8786">
<p data-start="8768" data-end="8786">the build scripts,</p>
</li>
<li data-start="8787" data-end="8822">
<p data-start="8789" data-end="8822">the Dockerfile and helper script,</p>
</li>
<li data-start="8823" data-end="8862">
<p data-start="8825" data-end="8862">example GitHub Actions configuration,</p>
</li>
<li data-start="8863" data-end="8907">
<p data-start="8865" data-end="8907">and notes on how to structure a book repo.</p>
</li>
</ul>
<p data-start="8909" data-end="8932">If you’ve ever thought:</p>
<blockquote data-start="8934" data-end="9055">
<p data-start="8936" data-end="9055">I’d like to write a small technical book, but I don’t want to fight with LaTeX or invent a build system from scratch…</p>
</blockquote>
<p data-start="9057" data-end="9104">then you’re very much the person I had in mind.</p>
<p data-start="9106" data-end="9251">eBook publishing really is pretty easy once you’ve got a solid pipeline. If these tools help you get your ideas out into the world, that’s a win.</p>
<p data-start="9253" data-end="9447">And, of course, if you’d like to write a book <em data-start="9299" data-end="9304">for</em> Perl School, I’m still <a href="https://perlschool.com/write/">very interested in talking to potential authors</a> &#8211; especially if you’re doing interesting modern Perl in the real world.</p><p>The post <a href="https://perlhacks.com/2025/12/behind-the-scenes-at-perl-school-publishing/">Behind the scenes at Perl School Publishing</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2025/12/behind-the-scenes-at-perl-school-publishing/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2376</post-id>	</item>
		<item>
		<title>Dotcom Survivor Syndrome – How Perl’s Early Success Created the Seeds of Its Downfall</title>
		<link>https://perlhacks.com/2025/11/dotcom-survivor-syndrome-how-perls-early-success-created-the-seeds-of-its-downfall/</link>
					<comments>https://perlhacks.com/2025/11/dotcom-survivor-syndrome-how-perls-early-success-created-the-seeds-of-its-downfall/#comments</comments>
		
		<dc:creator><![CDATA[Dave Cross]]></dc:creator>
		<pubDate>Sun, 30 Nov 2025 15:50:59 +0000</pubDate>
				<category><![CDATA[Perl]]></category>
		<category><![CDATA[developer bias]]></category>
		<category><![CDATA[dotcom]]></category>
		<category><![CDATA[survivor syndrome]]></category>
		<category><![CDATA[web development]]></category>
		<guid isPermaLink="false">https://perlhacks.com/?p=2368</guid>

					<description><![CDATA[<p>If you were building web applications during the first dot-com boom, chances are you wrote Perl. And if you&#8217;re now a CTO, tech lead, or senior architect, you may instinctively steer teams away from it—even if you can’t quite explain why. This reflexive aversion isn’t just a preference. It’s what I call Dotcom Survivor Syndrome: [&#8230;]</p>
<p>The post <a href="https://perlhacks.com/2025/11/dotcom-survivor-syndrome-how-perls-early-success-created-the-seeds-of-its-downfall/">Dotcom Survivor Syndrome – How Perl’s Early Success Created the Seeds of Its Downfall</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></description>
										<content:encoded><![CDATA[<p data-start="403" data-end="642">If you were building web applications during the first dot-com boom, chances are you wrote Perl. And if you&#8217;re now a CTO, tech lead, or senior architect, you may instinctively steer teams away from it—even if you can’t quite explain why.</p>
<p data-start="644" data-end="894">This reflexive aversion isn’t just a preference. It’s what I call <strong data-start="710" data-end="743">Dotcom Survivor Syndrome</strong>: a long-standing bias formed by the messy, experimental, high-pressure environment of the early web, where Perl was both a lifeline and a liability.</p>
<p data-start="896" data-end="1111">Perl wasn’t the problem. The conditions under which we used it were. And unfortunately, those conditions, combined with a separate, prolonged misstep over versioning, continue to distort Perl’s reputation to this day.</p>
<hr data-start="1113" data-end="1116" />
<h2 data-start="1118" data-end="1177"><strong data-start="1121" data-end="1175">The Glory Days: Perl at the Heart of the Early Web</strong></h2>
<p data-start="1179" data-end="1237">In the mid- to late-1990s, Perl was the web’s duct tape.</p>
<ul data-start="1238" data-end="1435">
<li data-start="1238" data-end="1283">
<p data-start="1240" data-end="1283">It powered CGI scripts on Apache servers.</p>
</li>
<li data-start="1284" data-end="1338">
<p data-start="1286" data-end="1338">It automated deployments before DevOps had a name.</p>
</li>
<li data-start="1339" data-end="1435">
<p data-start="1341" data-end="1435">It parsed logs, scraped data, processed form input, and glued together whatever needed glueing.</p>
</li>
</ul>
<p data-start="1437" data-end="1614"><strong data-start="1437" data-end="1447">Perl 5</strong>, released in 1994, introduced real structure: references, modules, and the birth of <strong data-start="1532" data-end="1540">CPAN</strong>, which became one of the most effective software ecosystems in the world.</p>
<p data-start="1616" data-end="1694">Perl wasn’t just part of the early web—it was <strong data-start="1662" data-end="1693">instrumental in creating it</strong>.</p>
<hr data-start="1696" data-end="1699" />
<h2 data-start="1701" data-end="1764"><strong data-start="1704" data-end="1762">The Dotcom Boom: Shipping Fast and Breaking Everything</strong></h2>
<p data-start="1766" data-end="1876">To understand the long shadow Perl casts, you have to understand the speed and pressure of the dot-com boom.</p>
<p data-start="1878" data-end="1961">We weren’t just building websites.<br data-start="1912" data-end="1915" />We were inventing <strong data-start="1933" data-end="1940">how</strong> to build websites.</p>
<p data-start="1963" data-end="2090">Best practices? Mostly unwritten.<br data-start="1996" data-end="1999" />Frameworks? Few existed.<br data-start="2023" data-end="2026" />Code reviews? Uncommon.<br data-start="2049" data-end="2052" />Continuous integration? Still a dream.</p>
<p data-start="2092" data-end="2228">The pace was frantic. You built something overnight, demoed it in the morning, and deployed it that afternoon. And Perl let you do that.</p>
<p data-start="2230" data-end="2403">But that same flexibility—its greatest strength—became its greatest weakness in that environment. With deadlines looming and scalability an afterthought, we ended up with:</p>
<ul data-start="2404" data-end="2603">
<li data-start="2404" data-end="2454">
<p data-start="2406" data-end="2454">Thousands of lines of unstructured CGI scripts</p>
</li>
<li data-start="2455" data-end="2480">
<p data-start="2457" data-end="2480">Minimal documentation</p>
</li>
<li data-start="2481" data-end="2512">
<p data-start="2483" data-end="2512">Global variables everywhere</p>
</li>
<li data-start="2513" data-end="2554">
<p data-start="2515" data-end="2554">Inline HTML mixed with business logic</p>
</li>
<li data-start="2555" data-end="2603">
<p data-start="2557" data-end="2603">Security holes you could drive a truck through</p>
</li>
</ul>
<p data-start="2605" data-end="2845">When the crash came, these codebases didn’t age gracefully. The people who inherited them, often the same people who now run engineering orgs, remember Perl not as a powerful tool, but as the source of late-night chaos and technical debt.</p>
<hr data-start="2847" data-end="2850" />
<h2 data-start="2852" data-end="2913"><strong data-start="2855" data-end="2911">Dotcom Survivor Syndrome: Bias with a Backstory</strong></h2>
<p data-start="2915" data-end="3000">Many senior engineers today carry these memories with them. They associate Perl with:</p>
<ul data-start="3001" data-end="3103">
<li data-start="3001" data-end="3025">
<p data-start="3003" data-end="3025">Fragile legacy systems</p>
</li>
<li data-start="3026" data-end="3059">
<p data-start="3028" data-end="3059">Inconsistent, “write-only” code</p>
</li>
<li data-start="3060" data-end="3103">
<p data-start="3062" data-end="3103">The bad old days of early web development</p>
</li>
</ul>
<p data-start="3105" data-end="3258">And that’s understandable. But it also creates a bias—often unconscious—that prevents Perl from getting a fair hearing in modern development discussions.</p>
<hr data-start="3260" data-end="3263" />
<h2 data-start="3265" data-end="3317"><strong data-start="3268" data-end="3315">Version Number Paralysis: The Perl 6 Effect</strong></h2>
<p data-start="3319" data-end="3437">If Dotcom Boom Survivor Syndrome created the emotional case against Perl, then Perl 6 created the optical one.</p>
<p data-start="3439" data-end="3617">In 2000, Perl 6 was announced as a ground-up redesign of the language. It promised modern syntax, new paradigms, and a bright future. But it didn’t ship—not for a very long time.</p>
<p data-start="3619" data-end="3635">In the meantime:</p>
<ul data-start="3636" data-end="3879">
<li data-start="3636" data-end="3749">
<p data-start="3638" data-end="3749"><strong data-start="3638" data-end="3676">Perl 5 continued to evolve quietly, </strong>but with the <em data-start="3690" data-end="3711">implied expectation</em> that it would eventually be replaced.</p>
</li>
<li data-start="3750" data-end="3879">
<p data-start="3752" data-end="3879"><strong data-start="3752" data-end="3781">Years turned into decades</strong>, and confusion set in. Was Perl 5 deprecated? Was Perl 6 compatible? What was the future of Perl?</p>
</li>
</ul>
<p data-start="3881" data-end="4132">To outsiders—and even many Perl users—it looked like the language was stalled. Perl 5 releases were labelled 5.8, 5.10, 5.12&#8230; but never 6. Perl 6 finally emerged in 2015, but as an entirely different language, not a successor.</p>
<p data-start="4134" data-end="4250">Eventually, the community admitted what everyone already knew: Perl 6 wasn’t Perl. In 2019, it was renamed Raku.</p>
<p data-start="4252" data-end="4442">But the damage was done. For nearly two decades, the version number “6” hung over Perl 5 like a storm cloud &#8211; a constant reminder that its future was uncertain, even when that wasn’t true.</p>
<p data-start="4444" data-end="4495">This is what I call <strong data-start="4464" data-end="4492">Version Number Paralysis</strong>:</p>
<ul data-start="4496" data-end="4714">
<li data-start="4496" data-end="4561">
<p data-start="4498" data-end="4561">A stalled major version that made the language look obsolete.</p>
</li>
<li data-start="4562" data-end="4631">
<p data-start="4564" data-end="4631">A missed opportunity to signal continued relevance and evolution.</p>
</li>
<li data-start="4632" data-end="4714">
<p data-start="4634" data-end="4714">A marketing failure that deepened the sense that Perl was a thing of the past.</p>
</li>
</ul>
<p data-start="4716" data-end="4869">Even today, many developers believe Perl is “stuck at version 5,” unaware that modern Perl is actively maintained, well-supported, and quite capable.</p>
<p data-start="4716" data-end="4869">While Dotcom Survivor Syndrome left many people with an aversion to Perl, Version Number Paralysis gave them an excuse not to look closely at Perl to see if it had changed.</p>
<hr data-start="4871" data-end="4874" />
<h2 data-start="4876" data-end="4920"><strong data-start="4879" data-end="4918">What They Missed While Looking Away</strong></h2>
<p data-start="4922" data-end="4989">While the world was confused or looking elsewhere, Perl 5 gained:</p>
<ul data-start="4990" data-end="5224">
<li data-start="4990" data-end="5028">
<p data-start="4992" data-end="5028">Modern object systems (Moo, Moose)</p>
</li>
<li data-start="5029" data-end="5077">
<p data-start="5031" data-end="5077">A mature testing culture (Test::More, Test2)</p>
</li>
<li data-start="5078" data-end="5145">
<p data-start="5080" data-end="5145">Widespread use of best practices (Perl::Critic, perltidy, etc.)</p>
</li>
<li data-start="5146" data-end="5189">
<p data-start="5148" data-end="5189">Core team stability and annual releases</p>
</li>
<li data-start="5190" data-end="5224">
<p data-start="5192" data-end="5224">Huge CPAN growth and refinements</p>
</li>
</ul>
<p data-start="5226" data-end="5378">But those who weren’t paying attention, especially those still carrying dotcom-era baggage, never saw it. They still think Perl looks like it did in 2002.</p>
<hr data-start="5380" data-end="5383" />
<h2 data-start="5385" data-end="5409"><strong data-start="5388" data-end="5407">Can We Move On?</strong></h2>
<p data-start="5411" data-end="5576">Dotcom Survivor Syndrome is real. So is Version Number Paralysis. Together, they’ve unfairly buried a language that remains fast, expressive, and battle-tested.</p>
<p data-start="5578" data-end="5615">We can’t change the past. But we can:</p>
<ul data-start="5616" data-end="5844">
<li data-start="5616" data-end="5668">
<p data-start="5618" data-end="5668">Acknowledge the emotional and historical baggage</p>
</li>
<li data-start="5669" data-end="5731">
<p data-start="5671" data-end="5731">Celebrate the role Perl played in inventing the modern web</p>
</li>
<li data-start="5732" data-end="5786">
<p data-start="5734" data-end="5786">Educate developers about what Perl really is today</p>
</li>
<li data-start="5787" data-end="5844">
<p data-start="5789" data-end="5844">Push back against the assumption that old == obsolete</p>
</li>
</ul>
<hr data-start="5846" data-end="5849" />
<h2 data-start="5851" data-end="5870"><strong data-start="5854" data-end="5868">Conclusion</strong></h2>
<p data-start="5872" data-end="6127">Perl’s early success was its own undoing. It became the default tool for the first web boom, and in doing so, it took the brunt of that era’s chaos. Then, just as it began to mature, its versioning story confused the industry into thinking it had stalled.</p>
<p data-start="6129" data-end="6285">But the truth is that modern Perl is thriving quietly in the margins &#8211; maintained by a loyal community, used in production, and capable of great things.</p>
<p data-start="6287" data-end="6451">The only thing holding it back is a generation of developers still haunted by memories of CGI scripts, and a version number that suggested a future that never came.</p>
<p data-start="6453" data-end="6487">Maybe it’s time we looked again.</p><p>The post <a href="https://perlhacks.com/2025/11/dotcom-survivor-syndrome-how-perls-early-success-created-the-seeds-of-its-downfall/">Dotcom Survivor Syndrome – How Perl’s Early Success Created the Seeds of Its Downfall</a> first appeared on <a href="https://perlhacks.com">Perl Hacks</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://perlhacks.com/2025/11/dotcom-survivor-syndrome-how-perls-early-success-created-the-seeds-of-its-downfall/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2368</post-id>	</item>
	</channel>
</rss>
