<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dave Cross</title>
    <description>The latest articles on DEV Community by Dave Cross (@davorg).</description>
    <link>https://dev.to/davorg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F114555%2F673c4753-2096-43a2-89fc-80ba2685b08b.png</url>
      <title>DEV Community: Dave Cross</title>
      <link>https://dev.to/davorg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davorg"/>
    <language>en</language>
    <item>
      <title>Choosing the Right Database Abstraction</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:24:43 +0000</pubDate>
      <link>https://dev.to/davorg/choosing-the-right-database-abstraction-4ge2</link>
      <guid>https://dev.to/davorg/choosing-the-right-database-abstraction-4ge2</guid>
      <description>&lt;p&gt;A &lt;span&gt;question&lt;/span&gt; came up recently in the Perl community asking whether, in a Mojolicious application, it’s better to use &lt;a href="https://metacpan.org/pod/DBIx::Class" rel="noopener noreferrer"&gt;DBIx::Class&lt;/a&gt; or a Mojolicious-specific module like &lt;a href="https://metacpan.org/pod/Mojo::Pg" rel="noopener noreferrer"&gt;Mojo::Pg&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s an interesting question, but I think it’s asking the wrong thing.&lt;/p&gt;

&lt;p&gt;I’ve spent almost forty years moving &lt;strong&gt;up&lt;/strong&gt; the stack of database abstractions. Every few years, someone invents a new layer, and every few years, experienced programmers explain that they don’t need it because they’re perfectly capable of working at the layer below. I’ve watched exactly the same argument play out over CGI, web frameworks, ORMs, containers and now AI-assisted coding.&lt;/p&gt;

&lt;p&gt;Sometimes the new abstraction turns out to be a dead end. More often, it’s simply another step that lets us spend less time on plumbing and more time solving the problems our users actually care about.&lt;/p&gt;

&lt;p&gt;I think that’s what’s really going on here.&lt;/p&gt;

&lt;h2&gt;
  
  
  You’re comparing different layers
&lt;/h2&gt;

&lt;p&gt;The first thing that struck me about the discussion was that it jumped straight from DBIx::Class to Mojo::Pg, as though they were equivalent choices. I don’t think they are.&lt;/p&gt;

&lt;p&gt;If you zoom out a little, there are several layers involved in database access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application&lt;/li&gt;
&lt;li&gt;DBIx::Class / DBIO (ORM)&lt;/li&gt;
&lt;li&gt;DBI&lt;/li&gt;
&lt;li&gt;DBD::Pg / DBD::SQLite / DBD::MariaDB&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each layer builds on the one below it. DBIx::Class sits on top of DBI, DBI sits on top of a database driver, and the driver talks to the database itself. Every layer gives you a slightly richer abstraction whilst hiding a little more of the mechanics.&lt;/p&gt;

&lt;p&gt;Mojo::Pg occupies a rather different place. It’s a PostgreSQL library designed specifically for Mojolicious. That’s a perfectly sensible design if you’ve already decided you’re using both PostgreSQL and Mojolicious, but it couples together decisions that I’d normally prefer to keep separate.&lt;/p&gt;

&lt;p&gt;Personally, I like choosing my web framework independently of my database, and my database independently of my data access layer. The more independent those decisions are, the easier it is to change one without affecting the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I moved up the stack
&lt;/h2&gt;

&lt;p&gt;I started writing embedded SQL in C in the late 1980s. When I moved to Perl in 1996, I used &lt;a href="https://metacpan.org/pod/sybperl" rel="noopener noreferrer"&gt;Sybperl&lt;/a&gt;, which was a thin layer over the proprietary Sybase database API. When I discovered &lt;a href="https://metacpan.org/pod/DBI" rel="noopener noreferrer"&gt;DBI&lt;/a&gt;, I adopted it enthusiastically because it removed an enormous amount of repetitive code whilst remaining database-independent. Later, when DBIx::Class became mature enough for production systems, I happily moved another level up the stack.&lt;/p&gt;

&lt;p&gt;None of those decisions happened because I couldn’t write SQL. I’ve been writing SQL since 1988 and I’m perfectly happy doing so.&lt;/p&gt;

&lt;p&gt;The point was that I no longer wanted to spend my time writing the same joins, the same foreign key lookups and the same bits of boilerplate over and over again. Those aren’t the interesting parts of my applications. They’re necessary, but they’re plumbing. Good abstractions let me write that plumbing once and then think about the domain I’m modelling instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn’t just about databases
&lt;/h2&gt;

&lt;p&gt;A few weeks ago, I wrote about &lt;a href="https://dev.to/davorg/the-long-road-from-cgi-to-containers-1kfh"&gt;&lt;em&gt;The Long Road from CGI to Containers&lt;/em&gt;&lt;/a&gt;. The point of that article was that the history of web development is largely the history of building better abstractions.&lt;/p&gt;

&lt;p&gt;We moved from CGI scripts to web frameworks. From hand-written deployment procedures to containers. Every step let us express our intent at a higher level whilst worrying less about the implementation details.&lt;/p&gt;

&lt;p&gt;Database programming has followed exactly the same path.&lt;/p&gt;

&lt;h2&gt;
  
  
  But I like writing SQL
&lt;/h2&gt;

&lt;p&gt;Whenever I recommend an ORM, someone inevitably assumes it’s because I don’t know SQL.&lt;/p&gt;

&lt;p&gt;Nothing could be further from the truth.&lt;/p&gt;

&lt;p&gt;SQL is probably the programming language I’ve been writing the longest. I enjoy writing SQL. But I don’t enjoy writing the same SQL repeatedly.&lt;/p&gt;

&lt;p&gt;I don’t want every controller containing another join between the same tables. I don’t want to duplicate the same &lt;code&gt;WHERE&lt;/code&gt; clause in half a dozen places. I don’t want to remember every foreign key relationship every time I touch the code.&lt;/p&gt;

&lt;p&gt;Those things aren’t business logic; they’re plumbing.&lt;/p&gt;

&lt;p&gt;An ORM lets me move that plumbing into one place and give it names.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DBIx::Class gives me
&lt;/h2&gt;

&lt;p&gt;For me, the biggest advantage of DBIx::Class isn’t that it generates SQL.&lt;/p&gt;

&lt;p&gt;It’s that it models my application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Person has many Titles.&lt;/li&gt;
&lt;li&gt;A Title belongs to a Person.&lt;/li&gt;
&lt;li&gt;A User&amp;gt; has many Orders.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those relationships become part of the vocabulary of the application. Queries become reusable methods. Business rules become object methods instead of comments beside SQL statements.&lt;/p&gt;

&lt;p&gt;Instead of thinking about joins, I’m thinking about people, titles and orders.&lt;/p&gt;

&lt;p&gt;That’s a much nicer level of abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isn’t it slower?
&lt;/h2&gt;

&lt;p&gt;One criticism often levelled at DBIx::Class is performance.&lt;/p&gt;

&lt;p&gt;That’s not entirely unfair. It’s perfectly possible to write inefficient DBIx::Class code. I know because I’ve done it.&lt;/p&gt;

&lt;p&gt;While recently carrying out some optimisation on my &lt;a href="https://lineofsuccession.co.uk/" rel="noopener noreferrer"&gt;Line of Succession&lt;/a&gt; application, I discovered one page was generating hundreds of unnecessary database queries. The solution wasn’t to abandon DBIx::Class and replace it with hand-written SQL. The solution was to understand the ORM better.&lt;/p&gt;

&lt;p&gt;Adding a couple of well-placed &lt;code&gt;prefetch&lt;/code&gt; clauses reduced the number of queries dramatically with almost no change to the surrounding code.&lt;/p&gt;

&lt;p&gt;The ORM wasn’t the problem; my use of the ORM was.&lt;/p&gt;

&lt;p&gt;That’s a useful lesson. Every abstraction has a learning curve. If you don’t understand how it’s working underneath, you’ll eventually hit performance problems. But once you do understand it, you can often achieve dramatic improvements without sacrificing the higher-level API that made you choose the abstraction in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if I just want to write SQL?
&lt;/h2&gt;

&lt;p&gt;One thing that struck me about the original discussion is that it jumped straight from DBIx::Class to Mojo::Pg, as though those were the only two choices.&lt;/p&gt;

&lt;p&gt;I don’t think they are.&lt;/p&gt;

&lt;p&gt;If all I wanted was to execute SQL from a Mojolicious application, I’d probably start by looking for a DBI-based solution rather than one that’s tied specifically to PostgreSQL. Something like &lt;a href="https://metacpan.org/pod/Mojolicious::Plugin::Database" rel="noopener noreferrer"&gt;Mojolicious::Plugin::Database&lt;/a&gt; seems like a more natural fit, as it lets me use the standard Perl database abstraction whilst integrating cleanly with Mojolicious.&lt;/p&gt;

&lt;p&gt;That still leaves me free to choose PostgreSQL, SQLite, MariaDB or whatever other database makes sense for the project.&lt;/p&gt;

&lt;p&gt;By contrast, choosing Mojo::Pg bakes the decision to use PostgreSQL into your application at a deeper level than I’d be comfortable with.&lt;/p&gt;

&lt;p&gt;Good abstractions should reduce coupling, not increase it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model layer
&lt;/h2&gt;

&lt;p&gt;Of course, in larger applications, there’s usually another abstraction on top of the ORM itself. My controllers don’t generally talk directly to DBIx::Class resultsets; they ask domain-level questions such as “Who was sovereign on this date?” The fact that the answer currently comes from DBIx::Class is an implementation detail. That’s another useful abstraction—but it’s probably a topic for another article.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Aside: What happened to DBIx::Class?
&lt;/h3&gt;

&lt;p&gt;For a long time, this was the awkward question.&lt;/p&gt;

&lt;p&gt;Around a decade ago, active development effectively stopped. This wasn’t  because the technology had failed or because the community lost interest. The maintainer had explicitly stopped development, and measures such as the CPAN &lt;code&gt;NOXFER&lt;/code&gt; co-maintainer flag prevented the normal open source succession process from happening.&lt;/p&gt;

&lt;p&gt;For years, that left DBIx::Class in an uncomfortable position. It remained stable and widely used, but there was understandable uncertainty about its long-term future.&lt;/p&gt;

&lt;p&gt;Fortunately, that situation finally appears to be changing. &lt;a href="https://metacpan.org/pod/DBIO" rel="noopener noreferrer"&gt;DBIO&lt;/a&gt; appeared on CPAN just a few days ago. It has taken the hard-fork route, preserving the ideas that made DBIx::Class successful whilst allowing active development to resume.&lt;/p&gt;

&lt;p&gt;Whether DBIO ultimately becomes the successor to DBIx::Class isn’t really the important point.&lt;/p&gt;

&lt;p&gt;The important point is that these ideas are still valuable enough that people are prepared to invest significant effort in carrying them forward.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Choosing your abstraction
&lt;/h2&gt;

&lt;p&gt;I don’t think this is fundamentally a question about Mojolicious.&lt;/p&gt;

&lt;p&gt;I’d happily use DBIx::Class with Mojolicious, Dancer2 or any other web framework. I’d happily use DBI with all of them too. The web framework and the data access layer are largely independent architectural decisions, and I generally prefer to keep them that way.&lt;/p&gt;

&lt;p&gt;The important decision isn’t the framework.&lt;/p&gt;

&lt;p&gt;It isn’t even the database.&lt;/p&gt;

&lt;p&gt;It’s deciding where you want your application to live.&lt;/p&gt;

&lt;p&gt;Do you want to think in terms of SQL?&lt;/p&gt;

&lt;p&gt;Do you want to think in terms of tables?&lt;/p&gt;

&lt;p&gt;Or do you want to think in terms of the concepts that make up your application’s domain?&lt;/p&gt;

&lt;p&gt;After nearly forty years of writing SQL, I know which one I’d rather spend my day thinking about.&lt;/p&gt;

&lt;p&gt;The best abstraction isn’t the one that hides the most detail.&lt;/p&gt;

&lt;p&gt;It’s the one that lets you spend most of your time thinking about the problem you’re actually trying to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;p&gt;I’ve written about different aspects of this topic before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://perlhacks.com/2012/10/dbic-vs-dbi/" rel="noopener noreferrer"&gt;&lt;strong&gt;DBIx::Class vs DBI&lt;/strong&gt;&lt;/a&gt; (2012) — why I prefer to use an ORM over raw SQL.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://perlhacks.com/2015/09/the-joy-of-prefetch/" rel="noopener noreferrer"&gt;&lt;strong&gt;The Joy of Prefetch&lt;/strong&gt;&lt;/a&gt; (2015) — how understanding your ORM can transform performance.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/davorg/the-long-road-from-cgi-to-containers-1kfh"&gt;&lt;strong&gt;The Long Road from CGI to Containers&lt;/strong&gt;&lt;/a&gt; (2026) — why software engineering is really the story of building better abstractions.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/06/choosing-the-right-database-abstraction/" rel="noopener noreferrer"&gt;Choosing the Right Database Abstraction&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>dbi</category>
      <category>dbixclass</category>
    </item>
    <item>
      <title>Public Identifiers, UUIDs and a Tiny SEO Fix</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sat, 06 Jun 2026 14:29:38 +0000</pubDate>
      <link>https://dev.to/davorg/public-identifiers-uuids-and-a-tiny-seo-fix-1i3n</link>
      <guid>https://dev.to/davorg/public-identifiers-uuids-and-a-tiny-seo-fix-1i3n</guid>
      <description>&lt;p&gt;A recent question from my friend and colleague Mohammad got me thinking about the way we identify data in web applications.&lt;/p&gt;

&lt;p&gt;While working on the DBIC component of a REST API, he came across the term &lt;em&gt;enumeration attack&lt;/em&gt;. In this type of attack, an attacker systematically guesses resource identifiers in order to access data they shouldn’t be able to see.&lt;/p&gt;

&lt;p&gt;For example, if your API exposes URLs like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /users/123
GET /users/124
GET /users/125
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then it’s easy for someone to try a large range of identifiers and see what they get back.&lt;/p&gt;

&lt;p&gt;Mohammad’s question was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should we replace sequential IDs with UUIDs? And if we do, should we index the UUID column?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As is often the case, the answer turned out to be “it depends”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Different Types of Data
&lt;/h2&gt;

&lt;p&gt;The first thing I realised is that not all data objects have the same requirements.&lt;/p&gt;

&lt;p&gt;Some objects are naturally public.&lt;/p&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/books/design-patterns-in-modern-perl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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’t be enumerable. In those cases, a UUID is often a better choice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/users/550e8400-e29b-41d4-a716-446655440000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important observation is that slugs and UUIDs solve different problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slugs are for humans (and, perhaps, search engines).&lt;/li&gt;
&lt;li&gt;UUIDs are for machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Database Design
&lt;/h2&gt;

&lt;p&gt;A common question is whether a UUID should replace the primary key.&lt;/p&gt;

&lt;p&gt;In most cases, I don’t think it should.&lt;/p&gt;

&lt;p&gt;My preferred design is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integer primary key remains the internal identifier used for joins and foreign keys.&lt;/p&gt;

&lt;p&gt;The UUID becomes the public identifier exposed through APIs.&lt;/p&gt;

&lt;p&gt;This gives you the best of both worlds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small, efficient foreign keys.&lt;/li&gt;
&lt;li&gt;Fast joins.&lt;/li&gt;
&lt;li&gt;Unguessable public identifiers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the application regularly searches by UUID then the UUID column should be indexed. In practice, declaring it &lt;code&gt;UNIQUE&lt;/code&gt; will usually create the appropriate index automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Approach
&lt;/h2&gt;

&lt;p&gt;Thinking about this reminded me that many large sites use a hybrid approach.&lt;/p&gt;

&lt;p&gt;Amazon product URLs contain both a human-readable title and a stable identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Design-Patterns-Modern-Perl/dp/B0XXXXX123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ASIN is what really identifies the product.&lt;/p&gt;

&lt;p&gt;The title is there for humans.&lt;/p&gt;

&lt;p&gt;Stack Overflow does something similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/questions/12345/how-do-i-index-a-uuid-column
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the question ID is authoritative. The title is helpful context.&lt;/p&gt;

&lt;p&gt;My Line of Succession website uses the same idea.&lt;/p&gt;

&lt;p&gt;A person page looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/p/2b5998-the-prince-william-prince-of-wales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2b5998
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest is descriptive text.&lt;/p&gt;

&lt;p&gt;This turns out to be particularly useful for royalty because titles change constantly. Someone might be “Prince William”, then “The Prince of Wales”, and eventually “King William V”.&lt;/p&gt;

&lt;p&gt;By separating identity from presentation, old links continue to work regardless of title changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Tiny Bug
&lt;/h2&gt;

&lt;p&gt;While thinking about all of this, I discovered a small bug in Line of Succession.&lt;/p&gt;

&lt;p&gt;The site allows any descriptive text after the identifier. These URLs all resolve to the same person:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/p/2b5998-the-prince-william-prince-of-wales
/p/2b5998-prince-billy
/p/2b5998-fred
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application correctly ignores the descriptive text and uses only the identifier.&lt;/p&gt;

&lt;p&gt;However, there was a problem.&lt;/p&gt;

&lt;p&gt;The page was generating its canonical URL from the incoming request path rather than from the person record.&lt;/p&gt;

&lt;p&gt;That meant a request for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/p/2b5998-prince-billy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt;
      &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://lineofsuccession.co.uk/p/2b5998-prince-billy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which is obviously not the canonical URL.&lt;/p&gt;

&lt;p&gt;The fix was surprisingly small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;( $self ) {&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;is_date_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;canonical_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elsif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;is_person_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/p/&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;person&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the same time I simplified another method by making it reuse the canonical URL logic.&lt;/p&gt;

&lt;p&gt;The result was a six-line patch that fixed the SEO issue and made the code slightly cleaner.&lt;/p&gt;

&lt;p&gt;Those are my favourite kinds of fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;The fix also revealed an emerging abstraction in the code.&lt;/p&gt;

&lt;p&gt;At the moment, various parts of the application know how to construct URLs for different object types.&lt;/p&gt;

&lt;p&gt;A cleaner approach would be to give objects responsibility for generating their own URLs.&lt;/p&gt;

&lt;p&gt;I’m considering a &lt;code&gt;HasURL&lt;/code&gt; role that would require an object to provide an identifier and optionally a prefix, and then build the URL automatically.&lt;/p&gt;

&lt;p&gt;That’s a job for another day.&lt;/p&gt;

&lt;p&gt;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’s work.&lt;/p&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/06/public-identifiers-uuids-and-a-tiny-seo-fix/" rel="noopener noreferrer"&gt;Public Identifiers, UUIDs and a Tiny SEO Fix&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>databasedesign</category>
      <category>lineofsuccession</category>
      <category>perl</category>
    </item>
    <item>
      <title>Teaching AI About the British Monarchy with MCP</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sat, 30 May 2026 09:58:02 +0000</pubDate>
      <link>https://dev.to/davorg/teaching-ai-about-the-british-monarchy-with-mcp-43eh</link>
      <guid>https://dev.to/davorg/teaching-ai-about-the-british-monarchy-with-mcp-43eh</guid>
      <description>&lt;p&gt;One of the more interesting additions I’ve made recently to &lt;a href="https://lineofsuccession.co.uk/" rel="noopener noreferrer"&gt;the Line of Succession website&lt;/a&gt; is support for the &lt;a href="https://en.wikipedia.org/wiki/Model_Context_Protocol" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve spent any time around AI tooling recently, you’ve probably seen people talking about MCP. It’s often described as “USB for AI”, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For a data-driven site like Line of Succession, that seemed like an obvious experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol was originally developed by Anthropic and has rapidly become one of the emerging standards in the AI ecosystem.&lt;/p&gt;

&lt;p&gt;An MCP server exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Information about itself&lt;/li&gt;
&lt;li&gt;A list of available tools&lt;/li&gt;
&lt;li&gt;Schemas describing how those tools should be called&lt;/li&gt;
&lt;li&gt;The results returned by those tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An AI client can connect to the server, discover the available tools and invoke them when needed.&lt;/p&gt;

&lt;p&gt;Instead of scraping web pages or attempting to infer information from HTML, the AI gets access to structured data.&lt;/p&gt;

&lt;p&gt;That’s exactly the kind of thing Line of Succession is good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Add MCP?
&lt;/h2&gt;

&lt;p&gt;The site already exposes information through a traditional web interface and a JSON API.&lt;/p&gt;

&lt;p&gt;But those interfaces were designed for humans and developers respectively.&lt;/p&gt;

&lt;p&gt;MCP gives AI systems a much cleaner integration point.&lt;/p&gt;

&lt;p&gt;For example, an AI assistant can now answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who was the British sovereign on 14 November 1948?&lt;/li&gt;
&lt;li&gt;What did the line of succession look like in 1980?&lt;/li&gt;
&lt;li&gt;Who was next in line when Queen Victoria died?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without having to scrape pages or understand the site’s internal URLs.&lt;/p&gt;

&lt;p&gt;More importantly, it ensures that the information comes directly from the same database that powers the website.&lt;/p&gt;

&lt;p&gt;The AI isn’t guessing.&lt;/p&gt;

&lt;p&gt;It’s querying the source of truth.&lt;/p&gt;

&lt;p&gt;As someone who runs a reference website, that’s a pretty attractive proposition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Design
&lt;/h2&gt;

&lt;p&gt;My first goal was to keep things simple.&lt;/p&gt;

&lt;p&gt;Rather than exposing dozens of narrowly-focused tools, I started with just two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sovereign_on_date&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;line_of_succession&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those two tools cover a surprisingly large proportion of the questions people are likely to ask.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The implementation currently caps the list at thirty people. That’s enough for most use cases while preventing someone from accidentally asking for all six thousand people currently in the line of succession.&lt;/p&gt;

&lt;p&gt;One thing I learned quite quickly is that MCP isn’t really about exposing huge amounts of data. It’s about exposing useful questions that can be answered from your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Is Mostly JSON-RPC
&lt;/h2&gt;

&lt;p&gt;One thing that surprised me when I first started reading the specification was how little protocol code is actually required.&lt;/p&gt;

&lt;p&gt;At its core, MCP uses &lt;a href="https://en.wikipedia.org/wiki/JSON-RPC" rel="noopener noreferrer"&gt;JSON-RPC&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A client sends requests like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/list"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the server responds with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I’d written helper methods for creating standard JSON-RPC responses, most of the complexity disappeared.&lt;/p&gt;

&lt;p&gt;The MCP module contains methods like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;rpc_result&lt;/span&gt; &lt;span class="err"&gt;($&lt;/span&gt;&lt;span class="nf"&gt;self&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;rpc_error&lt;/span&gt; &lt;span class="err"&gt;($&lt;/span&gt;&lt;span class="nf"&gt;self&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which means the Dancer route handlers remain pleasantly small.&lt;/p&gt;

&lt;p&gt;The protocol logic lives in one place and the web application simply delegates to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separating the MCP Logic
&lt;/h2&gt;

&lt;p&gt;I didn’t want protocol-specific code scattered throughout the web application.&lt;/p&gt;

&lt;p&gt;Instead, I created a dedicated module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nb"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;Succession::&lt;/span&gt;&lt;span class="nv"&gt;MCP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This module is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initialisation&lt;/li&gt;
&lt;li&gt;Tool discovery&lt;/li&gt;
&lt;li&gt;Tool execution&lt;/li&gt;
&lt;li&gt;JSON-RPC response generation&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That keeps the Dancer routes thin and makes the MCP implementation easier to test independently.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool Calls Are Mostly Adapters
&lt;/h2&gt;

&lt;p&gt;One pleasant surprise was how little new application logic I actually had to write.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For example, the application’s model layer already contained methods such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sovereign_on_date()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;line_of_succession()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These methods power parts of the website itself, so they already encapsulate all of the business rules and database queries.&lt;/p&gt;

&lt;p&gt;The MCP implementation simply acts as an adapter.&lt;/p&gt;

&lt;p&gt;When a tool call arrives, the server extracts the arguments, validates them and passes them to the existing model methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;_call_tool&lt;/span&gt; &lt;span class="p"&gt;($self, $tool_name, $args) {&lt;/span&gt;
  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;_tool_dispatch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$tool_name&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$tool&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool implementations themselves are deliberately thin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;sovereign_on_date&lt;/span&gt; &lt;span class="p"&gt;($self, $args) {&lt;/span&gt;
  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;date&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$sovereign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;sovereign_on_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s exactly how I wanted it to work.&lt;/p&gt;

&lt;p&gt;The MCP layer doesn’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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;In many ways, adding MCP feels less like building a new application and more like adding another interface alongside the website and API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The YAML Epiphany
&lt;/h2&gt;

&lt;p&gt;The most interesting design decision came a little later.&lt;/p&gt;

&lt;p&gt;Initially, the tool definitions lived in Perl data structures.&lt;/p&gt;

&lt;p&gt;That worked, but it quickly became obvious that I was duplicating information.&lt;/p&gt;

&lt;p&gt;The MCP server needed tool descriptions.&lt;/p&gt;

&lt;p&gt;The documentation page needed tool descriptions.&lt;/p&gt;

&lt;p&gt;The schemas needed to be defined somewhere.&lt;/p&gt;

&lt;p&gt;And every change required updating multiple places.&lt;/p&gt;

&lt;p&gt;The obvious answer was to move all of the tool definitions into a YAML file.&lt;/p&gt;

&lt;p&gt;The MCP module now loads its tool definitions at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;_build__tools&lt;/span&gt; &lt;span class="p"&gt;($self) {&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;LoadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;tools_file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a single source of truth.&lt;/p&gt;

&lt;p&gt;The same YAML file drives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;tools/list&lt;/code&gt; response&lt;/li&gt;
&lt;li&gt;Tool metadata&lt;/li&gt;
&lt;li&gt;JSON schemas&lt;/li&gt;
&lt;li&gt;Human-readable documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a new tool now involves updating one file and writing the code that implements it.&lt;/p&gt;

&lt;p&gt;Everything else follows automatically.&lt;/p&gt;

&lt;p&gt;Here’s the current YAML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# data/mcp-tools.yml&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sovereign_on_date&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Return the British sovereign on a given date.&lt;/span&gt;
  &lt;span class="na"&gt;documentation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;Looks up the reigning British sovereign for the supplied date.&lt;/span&gt;

    &lt;span class="s"&gt;Use this when answering questions such as “Who was sovereign on&lt;/span&gt;
    &lt;span class="s"&gt;6 February 1952?”&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Date in YYYY-MM-DD format.&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;date&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;line_of_succession&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Return the line of succession on a given date.&lt;/span&gt;
  &lt;span class="na"&gt;documentation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;Returns people in the line of succession.&lt;/span&gt;

    &lt;span class="s"&gt;If no date is supplied, the current line of succession is returned.&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Optional date in YYYY-MM-DD format. Omit for the current line of succession.&lt;/span&gt;
      &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Maximum number of successors to return.&lt;/span&gt;
        &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;maximum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking back, this is probably the part of the design I’m happiest with. It feels very Perl-ish: keep configuration as data and avoid duplicating information wherever possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human Documentation Matters
&lt;/h2&gt;

&lt;p&gt;One thing I noticed while exploring other MCP servers is that many of them are effectively invisible to humans.&lt;/p&gt;

&lt;p&gt;You know an endpoint exists.&lt;/p&gt;

&lt;p&gt;You know it speaks MCP.&lt;/p&gt;

&lt;p&gt;But unless you inspect the protocol responses manually, you don’t really know what it does.&lt;/p&gt;

&lt;p&gt;I decided to add a conventional web page at &lt;a href="https://lineofsuccession.co.uk/mcp" rel="noopener noreferrer"&gt;&lt;code&gt;/mcp&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The page lists all available tools, their descriptions and their schemas.&lt;/p&gt;

&lt;p&gt;The nice part is that there is no duplicated documentation.&lt;/p&gt;

&lt;p&gt;The page is generated from the same YAML definitions used by the MCP server itself.&lt;/p&gt;

&lt;p&gt;If I add a new tool tomorrow, both the machine-readable and human-readable views update automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured Data and Text Responses
&lt;/h2&gt;

&lt;p&gt;Another nice feature of MCP is that tool results can include both structured data and human-readable text.&lt;/p&gt;

&lt;p&gt;For example, a tool response might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The sovereign on 14 November 1948 was George VI."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"structuredContent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structured content is useful for software.&lt;/p&gt;

&lt;p&gt;The text is useful for humans and language models.&lt;/p&gt;

&lt;p&gt;Both are generated from the same underlying data.&lt;/p&gt;

&lt;p&gt;That gives AI clients flexibility while ensuring consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Listed
&lt;/h2&gt;

&lt;p&gt;Once everything was working, I submitted the server to the MCP directory at &lt;a href="https://mcpservers.org" rel="noopener noreferrer"&gt;mcpservers.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That might seem like a small step, but discoverability is important.&lt;/p&gt;

&lt;p&gt;An MCP server hidden on a random website isn’t much use if nobody knows it exists.&lt;/p&gt;

&lt;p&gt;Directories like that are rapidly becoming the equivalent of API catalogues for the AI era.&lt;/p&gt;

&lt;p&gt;Being listed means developers and AI enthusiasts can find the service without first discovering the website.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was It Worth It?
&lt;/h2&gt;

&lt;p&gt;Absolutely.&lt;/p&gt;

&lt;p&gt;The amount of code required was surprisingly small. Most of the work wasn’t implementing the protocol; it was deciding how best to expose the data.&lt;/p&gt;

&lt;p&gt;More importantly, it opens the site up to an entirely new audience: AI agents.&lt;/p&gt;

&lt;p&gt;Historically, websites were built for humans and APIs were built for developers.&lt;/p&gt;

&lt;p&gt;MCP introduces a third category: services designed specifically for AI systems.&lt;/p&gt;

&lt;p&gt;For a structured-data site like Line of Succession, that’s a natural fit.&lt;/p&gt;

&lt;p&gt;Will MCP still be the dominant standard in five years’ time? I have no idea. The AI industry changes too quickly to make confident predictions.&lt;/p&gt;

&lt;p&gt;But right now it has significant momentum, broad industry support and a growing ecosystem of tools.&lt;/p&gt;

&lt;p&gt;And if nothing else, it’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.&lt;/p&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/05/teaching-ai-about-the-british-monarchy-with-mcp/" rel="noopener noreferrer"&gt;Teaching AI About the British Monarchy with MCP&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>dancer2</category>
      <category>lineofsuccession</category>
    </item>
    <item>
      <title>The Long Road from CGI to Containers</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Mon, 18 May 2026 15:36:11 +0000</pubDate>
      <link>https://dev.to/davorg/the-long-road-from-cgi-to-containers-1kfh</link>
      <guid>https://dev.to/davorg/the-long-road-from-cgi-to-containers-1kfh</guid>
      <description>&lt;p&gt;One of the defining characteristics of a good programmer is an instinct for keeping implementation details in the correct layer of an application.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;And nowhere is that clearer than in Perl web development.&lt;/p&gt;

&lt;p&gt;Many of us who built web applications during the dotcom boom spent years learning this lesson the hard way.&lt;/p&gt;

&lt;p&gt;We wrote CGI programs that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parsed HTTP requests&lt;/li&gt;
&lt;li&gt;generated HTML by hand&lt;/li&gt;
&lt;li&gt;connected directly to databases&lt;/li&gt;
&lt;li&gt;embedded SQL inline&lt;/li&gt;
&lt;li&gt;mixed business logic with presentation&lt;/li&gt;
&lt;li&gt;relied on Apache behaviour&lt;/li&gt;
&lt;li&gt;assumed specific filesystem layouts&lt;/li&gt;
&lt;li&gt;and often only worked on one particular server configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It all worked. Until it didn’t.&lt;/p&gt;

&lt;p&gt;The history of Perl web development is, in many ways, the history of gradually moving implementation details into more appropriate architectural layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Early CGI Years
&lt;/h2&gt;

&lt;p&gt;Early Perl CGI applications were often a single giant script.&lt;/p&gt;

&lt;p&gt;You’d open a file and see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request handling&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;HTML generation&lt;/li&gt;
&lt;li&gt;SQL queries&lt;/li&gt;
&lt;li&gt;business logic&lt;/li&gt;
&lt;li&gt;configuration&lt;/li&gt;
&lt;li&gt;deployment assumptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…all mixed together in a glorious ball of mud.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/perl&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;CGI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;DBI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$cgi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;CGI&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="nv"&gt;$cgi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;header&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$dbh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;DBI&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbi:mysql:test&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt;
  &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt;
  &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$sth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$dbh&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select * from users where id = ?&lt;/span&gt;&lt;span class="p"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$sth&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cgi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;param&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="p"&gt;'));&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sth&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;fetchrow_hashref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;$row&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;{name}&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time, this felt perfectly normal.&lt;/p&gt;

&lt;p&gt;And to be fair, it &lt;em&gt;was&lt;/em&gt; a huge step forward from static HTML sites.&lt;/p&gt;

&lt;p&gt;But the design had a fundamental problem:&lt;/p&gt;

&lt;p&gt;Everything knew too much about everything else.&lt;/p&gt;

&lt;p&gt;The application logic knew:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how HTTP worked&lt;/li&gt;
&lt;li&gt;how HTML worked&lt;/li&gt;
&lt;li&gt;how Apache launched CGI scripts&lt;/li&gt;
&lt;li&gt;how the database worked&lt;/li&gt;
&lt;li&gt;how the operating system was configured&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every concern leaked into every other concern.&lt;/p&gt;

&lt;p&gt;That made systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hard to test&lt;/li&gt;
&lt;li&gt;hard to reuse&lt;/li&gt;
&lt;li&gt;hard to deploy&lt;/li&gt;
&lt;li&gt;hard to scale&lt;/li&gt;
&lt;li&gt;and terrifying to change&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The First Big Lesson: Put Logic in Libraries
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;')&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;being embedded directly in a CGI script, it becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;discount_rate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a small change, but architecturally it’s enormous.&lt;/p&gt;

&lt;p&gt;Now the business logic lives in a library.&lt;/p&gt;

&lt;p&gt;And once that happens, several good things follow automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple Interfaces Become Possible
&lt;/h3&gt;

&lt;p&gt;If the logic is in modules, then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a web front-end&lt;/li&gt;
&lt;li&gt;a CLI tool&lt;/li&gt;
&lt;li&gt;a REST API&lt;/li&gt;
&lt;li&gt;a cron job&lt;/li&gt;
&lt;li&gt;a queue worker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…can all use the same underlying code.&lt;/p&gt;

&lt;p&gt;The interface layer becomes thin.&lt;/p&gt;

&lt;p&gt;The application itself becomes independent of how users interact with it.&lt;/p&gt;

&lt;p&gt;That’s a huge increase in flexibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Becomes Easier
&lt;/h3&gt;

&lt;p&gt;Testing CGI scripts was always awkward.&lt;/p&gt;

&lt;p&gt;Testing modules is straightforward.&lt;/p&gt;

&lt;p&gt;You can instantiate objects, call methods, and inspect results without needing a web server or HTTP requests.&lt;/p&gt;

&lt;p&gt;The easier code is to test, the more likely it is to be tested.&lt;/p&gt;

&lt;p&gt;And tested code tends to survive longer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Becomes Safer
&lt;/h3&gt;

&lt;p&gt;Once the core behaviour is isolated from the interface layer, replacing the interface becomes far less risky.&lt;/p&gt;

&lt;p&gt;You can redesign the UI without rewriting the application.&lt;/p&gt;

&lt;p&gt;That separation is one of the foundations of maintainable software.&lt;/p&gt;




&lt;h2&gt;
  
  
  The PSGI Revolution
&lt;/h2&gt;

&lt;p&gt;The next big architectural leap in Perl web development came with PSGI and Plack.&lt;/p&gt;

&lt;p&gt;Younger developers may not fully appreciate how painful web deployment used to be.&lt;/p&gt;

&lt;p&gt;In the early 2000s, moving an application between hosting environments could require substantial rewrites.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CGI application worked one way.&lt;/li&gt;
&lt;li&gt;A mod_perl application worked another way.&lt;/li&gt;
&lt;li&gt;FastCGI had its own quirks.&lt;/li&gt;
&lt;li&gt;Embedded Apache handlers behaved differently again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many Perl developers spent years repeatedly rewriting applications simply because deployment environments changed.&lt;/p&gt;

&lt;p&gt;That was madness.&lt;/p&gt;

&lt;p&gt;The deployment model is an operational concern.&lt;/p&gt;

&lt;p&gt;It should not affect application architecture.&lt;/p&gt;

&lt;p&gt;PSGI fixed this by defining a standard interface between web applications and web servers.&lt;/p&gt;

&lt;p&gt;The core idea was beautifully simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A web application is just a function that receives an environment and returns a response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that abstraction existed, applications no longer cared whether they were running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;as CGI&lt;/li&gt;
&lt;li&gt;under mod_perl&lt;/li&gt;
&lt;li&gt;inside FastCGI&lt;/li&gt;
&lt;li&gt;under Starman&lt;/li&gt;
&lt;li&gt;behind nginx&lt;/li&gt;
&lt;li&gt;on a development laptop&lt;/li&gt;
&lt;li&gt;or inside a cloud container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deployment details moved down a layer.&lt;/p&gt;

&lt;p&gt;Exactly where they belonged.&lt;/p&gt;

&lt;p&gt;This was one of the most important architectural improvements Perl web development ever made.&lt;/p&gt;

&lt;p&gt;And it reflected a broader truth:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Good abstractions stop lower-level implementation details leaking upward.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Transitional Era: FatPacker and &lt;code&gt;cpanfile&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There was also an interesting intermediate stage between traditional Perl deployments and full containerisation.&lt;/p&gt;

&lt;p&gt;For years, one of the hardest parts of deploying Perl applications was dependency management.&lt;/p&gt;

&lt;p&gt;You’d move an application to a new server and discover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the wrong module version&lt;/li&gt;
&lt;li&gt;missing XS libraries&lt;/li&gt;
&lt;li&gt;incompatible Perl versions&lt;/li&gt;
&lt;li&gt;or an entire dependency tree that worked perfectly on the developer’s machine and nowhere else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Large parts of Perl deployment culture evolved around coping with this problem.&lt;/p&gt;

&lt;p&gt;Tools like &lt;code&gt;cpanfile&lt;/code&gt; improved things by making dependencies explicit and reproducible.&lt;/p&gt;

&lt;p&gt;Instead of vaguely documenting requirements in a README, applications could formally declare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requires 'Dancer2';
requires 'DBIx::Class';
requires 'Template';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That may seem obvious now, but it was a major improvement in deployment reliability.&lt;/p&gt;

&lt;p&gt;Then tools like App::FatPacker went even further by packaging dependencies directly alongside applications.&lt;/p&gt;

&lt;p&gt;Instead of relying on the target server’s Perl environment, applications could carry much of their runtime context with them.&lt;/p&gt;

&lt;p&gt;These tools didn’t completely solve deployment portability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system libraries still mattered&lt;/li&gt;
&lt;li&gt;Perl versions still mattered&lt;/li&gt;
&lt;li&gt;operating system differences still mattered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…but they represented an important shift in thinking.&lt;/p&gt;

&lt;p&gt;The industry was gradually realising that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deployment environments were part of the application&lt;/li&gt;
&lt;li&gt;reproducibility mattered&lt;/li&gt;
&lt;li&gt;and infrastructure assumptions needed to be controlled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Containers eventually pushed this idea to its logical conclusion by packaging not just Perl dependencies, but the entire runtime environment.&lt;/p&gt;

&lt;p&gt;In hindsight, tools like &lt;code&gt;cpanfile&lt;/code&gt; and FatPacker were stepping stones toward modern container-based deployment models.&lt;/p&gt;




&lt;h2&gt;
  
  
  Containers Are the Same Idea Again
&lt;/h2&gt;

&lt;p&gt;Docker and containers are simply the same architectural principle repeated one layer lower.&lt;/p&gt;

&lt;p&gt;Before containers, deployments were often fragile and highly environment-specific.&lt;/p&gt;

&lt;p&gt;Applications depended on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;particular Linux distributions&lt;/li&gt;
&lt;li&gt;specific Perl versions&lt;/li&gt;
&lt;li&gt;installed system libraries&lt;/li&gt;
&lt;li&gt;hand-configured servers&lt;/li&gt;
&lt;li&gt;undocumented setup steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers became experts in “works on my machine”.&lt;/p&gt;

&lt;p&gt;Operations teams became experts in swearing.&lt;/p&gt;

&lt;p&gt;Containers changed the model.&lt;/p&gt;

&lt;p&gt;Instead of deploying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a complete runtime environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the application no longer cares whether it runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on bare metal&lt;/li&gt;
&lt;li&gt;on a VPS&lt;/li&gt;
&lt;li&gt;in Kubernetes&lt;/li&gt;
&lt;li&gt;in ECS&lt;/li&gt;
&lt;li&gt;in Cloud Run&lt;/li&gt;
&lt;li&gt;or on someone’s laptop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;infrastructure concerns move downward&lt;/li&gt;
&lt;li&gt;application concerns stay upward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The boundaries become cleaner.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern Repeats Everywhere
&lt;/h2&gt;

&lt;p&gt;Once you notice this pattern, you see it throughout software engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Templates
&lt;/h3&gt;

&lt;p&gt;Template systems separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;presentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;from&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HTML should not contain database code.&lt;/p&gt;

&lt;p&gt;Business logic should not contain giant blobs of HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  ORMs and Database Layers
&lt;/h3&gt;

&lt;p&gt;DBI separates applications from database engines.&lt;/p&gt;

&lt;p&gt;ORMs separate applications from raw SQL structure.&lt;/p&gt;

&lt;p&gt;Again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implementation details move downward&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;Configuration belongs outside code.&lt;/p&gt;

&lt;p&gt;Deployment-specific values should not be embedded in applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  APIs
&lt;/h3&gt;

&lt;p&gt;Clients should not care whether data comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;another service&lt;/li&gt;
&lt;li&gt;a queue&lt;/li&gt;
&lt;li&gt;flat files&lt;/li&gt;
&lt;li&gt;or magic elves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the implementation’s problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal Is Not Abstraction for Its Own Sake
&lt;/h2&gt;

&lt;p&gt;Of course, experienced developers also know that abstractions can become ridiculous.&lt;/p&gt;

&lt;p&gt;Some abstractions simplify systems.&lt;/p&gt;

&lt;p&gt;Others merely hide complexity behind six additional layers of YAML.&lt;/p&gt;

&lt;p&gt;Joel Spolsky’s “Law of Leaky Abstractions” remains painfully relevant.&lt;/p&gt;

&lt;p&gt;The goal is not abstraction itself.&lt;/p&gt;

&lt;p&gt;The goal is to isolate genuinely volatile details.&lt;/p&gt;

&lt;p&gt;Good abstractions protect systems from change.&lt;/p&gt;

&lt;p&gt;Bad abstractions merely obscure reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Skill
&lt;/h2&gt;

&lt;p&gt;The deeper lesson here is that software architecture is largely about deciding:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What belongs where?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Experienced developers develop an instinct for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which details are likely to change&lt;/li&gt;
&lt;li&gt;which layers should know about which concerns&lt;/li&gt;
&lt;li&gt;and where boundaries should exist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That instinct is often more important than language choice, frameworks, or technology stacks.&lt;/p&gt;

&lt;p&gt;And if you spent the early 2000s rewriting CGI applications to run under mod_perl, you probably learned that lesson the hard way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh29b76a3yje8c858lq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh29b76a3yje8c858lq2.png" alt="Perl Web Development Over Time" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/05/the-long-road-from-cgi-to-containers/" rel="noopener noreferrer"&gt;The Long Road from CGI to Containers&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>cgi</category>
      <category>docker</category>
      <category>perl</category>
    </item>
    <item>
      <title>How I Cross-Post Blog Articles (Without Making It Complicated)</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Tue, 28 Apr 2026 15:33:29 +0000</pubDate>
      <link>https://dev.to/davorg/how-i-cross-post-blog-articles-without-making-it-complicated-13gl</link>
      <guid>https://dev.to/davorg/how-i-cross-post-blog-articles-without-making-it-complicated-13gl</guid>
      <description>&lt;p&gt;Someone asked me on LinkedIn recently how to cross-post blog content from a &lt;a href="https://docs.github.com/en/pages" rel="noopener noreferrer"&gt;GitHub Pages&lt;/a&gt; site to places like &lt;a href="https://medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://dev.to/"&gt;dev.to&lt;/a&gt; and &lt;a href="https://linkedin.com/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I started writing a quick reply and, as often happens, it turned into something longer. So here’s the slightly more organised version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different problems
&lt;/h2&gt;

&lt;p&gt;There are actually &lt;em&gt;two&lt;/em&gt; separate things people mean when they ask this.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Sharing links on social media
&lt;/h2&gt;

&lt;p&gt;This is the easy one.&lt;/p&gt;

&lt;p&gt;When I publish a post, I’ll usually share it by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pasting the link&lt;/li&gt;
&lt;li&gt;adding a short description&lt;/li&gt;
&lt;li&gt;maybe tweaking the wording per platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only slightly technical thing worth doing here is making sure your site has &lt;a href="https://en.wikipedia.org/wiki/Open_Graph_protocol" rel="noopener noreferrer"&gt;&lt;strong&gt;Open Graph tags&lt;/strong&gt;&lt;/a&gt; set up properly.&lt;/p&gt;

&lt;p&gt;That ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the right title appears&lt;/li&gt;
&lt;li&gt;the right description is used&lt;/li&gt;
&lt;li&gt;and, importantly, the right image is shown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t do this, your carefully written post ends up looking like a random bare link.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about “Share to…” buttons?
&lt;/h3&gt;

&lt;p&gt;Many blogging platforms (and themes) offer “Share to Facebook/LinkedIn/etc.” buttons.&lt;/p&gt;

&lt;p&gt;They:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generate a pre-filled post&lt;/li&gt;
&lt;li&gt;save a bit of time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don’t tend to use them – I prefer writing a slightly different intro per platform – but they’re perfectly reasonable if you want something quick and consistent.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Reposting the article elsewhere
&lt;/h2&gt;

&lt;p&gt;This is where it gets more interesting.&lt;/p&gt;

&lt;p&gt;My usual workflow is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write on my own site (WordPress in my case, but GitHub Pages is fine)&lt;/li&gt;
&lt;li&gt;then repost to other platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two I use most are Medium and dev.to – and they take slightly different approaches.&lt;/p&gt;




&lt;h2&gt;
  
  
  First requirement: you need a web feed
&lt;/h2&gt;

&lt;p&gt;If you want any level of automation, your site needs an RSS/Atom feed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress: you get this automatically&lt;/li&gt;
&lt;li&gt;GitHub Pages: depends how your site is built&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of people use &lt;a href="https://jekyllrb.com/" rel="noopener noreferrer"&gt;Jekyll&lt;/a&gt; on GitHub Pages (it’s the default and best-supported option), but it’s not the only choice — you can also use other static site generators or even pre-built HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whichever approach you use, you’ll need to make sure it produces a web feed.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  If you’re using Jekyll
&lt;/h3&gt;

&lt;p&gt;Add the official feed plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;jekyll-feed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Pages supports this plugin natively, so there’s no extra build step needed.&lt;/p&gt;

&lt;p&gt;Your feed will usually be available at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/feed.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  If you’re using something else
&lt;/h3&gt;

&lt;p&gt;Most static site generators (&lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt;, &lt;a href="https://www.11ty.dev/" rel="noopener noreferrer"&gt;Eleventy&lt;/a&gt;, &lt;a href="https://astro.build/" rel="noopener noreferrer"&gt;Astro&lt;/a&gt;, etc.) can generate feeds, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it’s often not enabled by default&lt;/li&gt;
&lt;li&gt;the configuration varies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you’ll need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;check the documentation for your tool&lt;/li&gt;
&lt;li&gt;enable or add a feed generator&lt;/li&gt;
&lt;li&gt;confirm the feed URL works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That feed is what tools like dev.to use to discover your posts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Medium: “Import a story”
&lt;/h2&gt;

&lt;p&gt;Medium has a built-in &lt;em&gt;Import a story&lt;/em&gt; feature.&lt;/p&gt;

&lt;p&gt;You give it the URL of your original post and it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pulls in the content&lt;/li&gt;
&lt;li&gt;recreates the article&lt;/li&gt;
&lt;li&gt;lets you edit it before publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works surprisingly well.&lt;/p&gt;

&lt;p&gt;The only slight downside is that it always takes me a few minutes to find the option. It’s on &lt;a href="https://medium.com/me/stories" rel="noopener noreferrer"&gt;your Stories page&lt;/a&gt; (when you’re logged in). Look for the big button in the top right corner.&lt;/p&gt;




&lt;h2&gt;
  
  
  dev.to: RSS-based automation
&lt;/h2&gt;

&lt;p&gt;dev.to does something a bit cleverer.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;point it at your RSS/Atom feed&lt;/li&gt;
&lt;li&gt;and it will automatically create &lt;strong&gt;draft posts&lt;/strong&gt; for new articles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few notes from experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it can take a few hours for drafts to appear&lt;/li&gt;
&lt;li&gt;formatting is usually good but you’ll probably want to tweak it a bit&lt;/li&gt;
&lt;li&gt;it often misses the &lt;em&gt;featured image&lt;/em&gt;, so I add that manually&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The really important bit: canonical URLs
&lt;/h2&gt;

&lt;p&gt;This is the part that many people miss.&lt;/p&gt;

&lt;p&gt;When you repost content, you are technically creating &lt;strong&gt;duplicate content&lt;/strong&gt; – which search engines don’t love.&lt;/p&gt;

&lt;p&gt;However…&lt;/p&gt;

&lt;p&gt;If you use the &lt;em&gt;official&lt;/em&gt; import tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Medium&lt;/li&gt;
&lt;li&gt;dev.to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…they both add a &lt;strong&gt;canonical link&lt;/strong&gt; back to your original post.&lt;/p&gt;

&lt;p&gt;That’s the equivalent of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This content originally lives over here – treat that as the primary version.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your original site gets the SEO benefit&lt;/li&gt;
&lt;li&gt;you don’t get penalised for duplication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re manually copying and pasting, you need to be careful about this – or at least be aware of the trade-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about LinkedIn?
&lt;/h2&gt;

&lt;p&gt;LinkedIn is a bit different.&lt;/p&gt;

&lt;p&gt;As far as I’m aware, there isn’t a clean equivalent of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Medium’s import tool&lt;/li&gt;
&lt;li&gt;or dev.to’s RSS integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s worth checking – platforms change – but I haven’t found anything comparable.&lt;/p&gt;

&lt;p&gt;So my approach is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write a short summary&lt;/li&gt;
&lt;li&gt;link to the original post&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That also tends to fit LinkedIn better as a platform.&lt;/p&gt;

&lt;p&gt;I’ve seen various guides suggesting ways to automate LinkedIn reposting, but many of them rely on long-dead platforms or brittle integrations. As far as I can tell, there’s no clean, supported equivalent of Medium’s import tool or dev.to’s RSS integration. These days, I treat LinkedIn as a place to &lt;em&gt;promote&lt;/em&gt; posts rather than republish them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is any of this automated?
&lt;/h2&gt;

&lt;p&gt;A bit, but not completely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Medium: semi-automated (import tool)&lt;/li&gt;
&lt;li&gt;dev.to: semi-automated (RSS → draft)&lt;/li&gt;
&lt;li&gt;LinkedIn: manual&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; build a fully automated pipeline with APIs and scripts.&lt;/p&gt;

&lt;p&gt;But in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a small amount of manual editing is useful&lt;/li&gt;
&lt;li&gt;each platform benefits from slightly different positioning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I don’t try to over-automate it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;There’s no standard protocol for cross-posting blog content.&lt;/p&gt;

&lt;p&gt;What you actually have is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a few helpful platform features&lt;/li&gt;
&lt;li&gt;a bit of light automation&lt;/li&gt;
&lt;li&gt;and some copy-and-paste&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly, that works perfectly well.&lt;/p&gt;

&lt;p&gt;Like most things on the web, there’s no single ‘right’ way to do this — just a collection of tools that mostly work if you don’t try to be too clever.&lt;/p&gt;




&lt;p&gt;If you’d like help building this kind of thing without overcomplicating it, there’s a brief overview of what I do here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://davecross.co.uk/services/" rel="noopener noreferrer"&gt;https://davecross.co.uk/services/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The post &lt;a href="https://blog.dave.org.uk/2026/04/how-i-cross-post-blog-articles-without-making-it-complicated.html" rel="noopener noreferrer"&gt;How I Cross-Post Blog Articles (Without Making It Complicated)&lt;/a&gt; appeared first on &lt;a href="https://blog.dave.org.uk" rel="noopener noreferrer"&gt;Davblog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>blogging</category>
      <category>contentsyndication</category>
      <category>crossposting</category>
      <category>githubpages</category>
    </item>
    <item>
      <title>Summarising a Month of Git Activity with Perl (and a Little Help from AI)</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sun, 12 Apr 2026 15:46:52 +0000</pubDate>
      <link>https://dev.to/davorg/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai-2fpc</link>
      <guid>https://dev.to/davorg/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai-2fpc</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Because, as I’m sure you’ve noticed, it’s very easy to forget.&lt;/p&gt;

&lt;p&gt;So this month, I decided to automate it.&lt;/p&gt;

&lt;p&gt;(And, if you’re interested in the end result, this is also a good excuse to mention that &lt;a href="https://davecross.substack.com/" rel="noopener noreferrer"&gt;the newsletter&lt;/a&gt; exists. Two birds, one stone.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;All of my Git repositories live somewhere under &lt;code&gt;/home/dave/git&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;What I wanted was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Given a month and a year&lt;/li&gt;
&lt;li&gt;Find all Git repositories under that directory&lt;/li&gt;
&lt;li&gt;Identify which ones had commits in that month&lt;/li&gt;
&lt;li&gt;Summarise the work done in each repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first three are straightforward enough. The last one is where things get interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding the Repositories
&lt;/h2&gt;

&lt;p&gt;The first step is walking the directory tree and finding &lt;code&gt;.git&lt;/code&gt; directories. This is a classic Perl task — &lt;code&gt;File::Find&lt;/code&gt; still does exactly what you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;v5&lt;/span&gt;&lt;span class="mf"&gt;.40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;File::&lt;/span&gt;&lt;span class="nv"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;find_repos&lt;/span&gt; &lt;span class="p"&gt;($root) {&lt;/span&gt;
  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;@repos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vg"&gt;$_&lt;/span&gt; &lt;span class="ow"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.git&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
      &lt;span class="nb"&gt;push&lt;/span&gt; &lt;span class="nv"&gt;@repos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;File::Find::&lt;/span&gt;&lt;span class="nv"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nv"&gt;$root&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;@repos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us a list of repository directories to inspect. It’s simple, robust, and doesn’t require any external dependencies.&lt;/p&gt;

&lt;p&gt;(There are, of course, other ways to do this — you could shell out to &lt;code&gt;fd&lt;/code&gt; or &lt;code&gt;find&lt;/code&gt;, for example — but keeping it in Perl keeps everything nicely self-contained.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Commits for a Month
&lt;/h2&gt;

&lt;p&gt;For each repo, we can run &lt;code&gt;git log&lt;/code&gt; with appropriate date filters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;commits_for_month&lt;/span&gt; &lt;span class="p"&gt;($repo, $since, $until) {&lt;/span&gt;
  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sx"&gt;q{git -C %s log --since="%s" --until="%s" --pretty=format:"%%s"}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$since&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$until&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;@commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;&lt;span class="si"&gt;$cmd&lt;/span&gt;&lt;span class="p"&gt;`;&lt;/span&gt;
  &lt;span class="nb"&gt;chomp&lt;/span&gt; &lt;span class="nv"&gt;@commits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;@commits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;$since&lt;/code&gt; and &lt;code&gt;$until&lt;/code&gt; define the month we’re interested in. I’ve been using something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$year&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;$month&lt;/span&gt;&lt;span class="s2"&gt;-01&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$year&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;$month&lt;/span&gt;&lt;span class="s2"&gt;-31&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt; &lt;span class="c1"&gt;# good enough for this purpose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, that’s a bit hand-wavy around month lengths. No, it doesn’t matter in practice. Sometimes “good enough” really is good enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Small Gotcha
&lt;/h2&gt;

&lt;p&gt;It turns out I have a few repositories where I never got around to making a first commit. In that case, &lt;code&gt;git log&lt;/code&gt; helpfully explodes with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;fatal: your current branch ‘master’ does not have any commits yet&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is fair enough — but not helpful in a script that’s supposed to quietly churn through dozens of repositories.&lt;/p&gt;

&lt;p&gt;The fix is simply to ignore failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;@commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;&lt;span class="si"&gt;$cmd&lt;/span&gt;&lt;span class="sb"&gt; 2&amp;gt;/dev/null&lt;/span&gt;&lt;span class="p"&gt;`;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there are no commits, we just get an empty list and move on. No warnings, no noise.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summarising the Work
&lt;/h2&gt;

&lt;p&gt;Once we have a list of commit messages, we can summarise them.&lt;/p&gt;

&lt;p&gt;And this is where I cheated slightly.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://metacpan.org/pod/OpenAPI::Client" rel="noopener noreferrer"&gt;OpenAPI::Client::OpenAI&lt;/a&gt; to feed the commit messages into an LLM and ask it to produce a short summary.&lt;/p&gt;

&lt;p&gt;Something along these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;OpenAPI::Client::&lt;/span&gt;&lt;span class="nv"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;summarise_commits&lt;/span&gt; &lt;span class="p"&gt;($commits) {&lt;/span&gt;
  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;OpenAPI::Client::&lt;/span&gt;&lt;span class="nv"&gt;OpenAI&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ENV&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;join&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt; &lt;span class="nv"&gt;@$commits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4.1-mini&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="s"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
      &lt;span class="s"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Summarise the following commit messages:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this overkill? Almost certainly.&lt;/p&gt;

&lt;p&gt;Could I have written some heuristics to group and summarise commit messages? Possibly.&lt;/p&gt;

&lt;p&gt;Would it have been as much fun? Definitely not.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;For each repo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get commits for the month&lt;/li&gt;
&lt;li&gt;Skip if there are none&lt;/li&gt;
&lt;li&gt;Generate a summary&lt;/li&gt;
&lt;li&gt;Print the repo name and summary&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output looks something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-project
-----------
Refactored database layer, added caching, and fixed several edge-case bugs.

another-project
---------------
Initial scaffolding, basic API endpoints, and deployment configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is already a pretty good starting point for a newsletter.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Nice Side Effect
&lt;/h2&gt;

&lt;p&gt;One unexpected benefit of this approach is that it surfaces projects I’d forgotten about.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Sometimes that’s useful. Sometimes it’s mildly embarrassing.&lt;/p&gt;

&lt;p&gt;But it’s always interesting.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Next?
&lt;/h2&gt;

&lt;p&gt;This is very much a &lt;strong&gt;first draft&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn it into a proper module&lt;/li&gt;
&lt;li&gt;Add tests&lt;/li&gt;
&lt;li&gt;Clean up the API&lt;/li&gt;
&lt;li&gt;Release it to CPAN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Future Enhancement
&lt;/h2&gt;

&lt;p&gt;One idea I particularly like is to run this automatically using GitHub Actions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run monthly&lt;/li&gt;
&lt;li&gt;Generate summaries for that month&lt;/li&gt;
&lt;li&gt;Commit the results to a repository&lt;/li&gt;
&lt;li&gt;Publish them via GitHub Pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over time, that would build up a &lt;strong&gt;permanent, browsable record&lt;/strong&gt; of what I’ve been working on.&lt;/p&gt;

&lt;p&gt;It’s a nice combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automation&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;and a gentle nudge towards accountability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which is either a fascinating historical archive…&lt;/p&gt;

&lt;p&gt;…or a slightly alarming reminder of how many half-finished projects I have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gluing systems together&lt;/li&gt;
&lt;li&gt;Wrapping command-line tools&lt;/li&gt;
&lt;li&gt;Handling messy real-world data&lt;/li&gt;
&lt;li&gt;Adding just enough intelligence to make the output useful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, occasionally, outsourcing the hard thinking to a machine.&lt;/p&gt;

&lt;p&gt;The code (such as it is currently is) is on GitHub at &lt;a href="https://github.com/davorg/git-month-summary" rel="noopener noreferrer"&gt;https://github.com/davorg/git-month-summary&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’re interested in the kind of projects this helps summarise, you can find &lt;a href="https://davecross.substack.com/" rel="noopener noreferrer"&gt;my monthly newsletter over on Substack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And if I get round to turning this into a CPAN module, I’ll let you know – well, if you’re subscribed to the newsletter!&lt;/p&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/04/summarising-a-month-of-git-activity-with-perl-and-a-little-help-from-ai/" rel="noopener noreferrer"&gt;Summarising a Month of Git Activity with Perl (and a Little Help from AI)&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>github</category>
      <category>newsletter</category>
      <category>whathaveidonethismonth</category>
    </item>
    <item>
      <title>Writing a TOON Module for Perl</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sun, 29 Mar 2026 17:46:14 +0000</pubDate>
      <link>https://dev.to/davorg/writing-a-toon-module-for-perl-4bjo</link>
      <guid>https://dev.to/davorg/writing-a-toon-module-for-perl-4bjo</guid>
      <description>&lt;p&gt;Every so often, a new data serialisation format appears and people get excited about it. Recently, one of those formats is &lt;strong&gt;TOON&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;So the obvious Perl question is: “Ok, where’s the CPAN module?”&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I should point out that I knew about &lt;a href="https://metacpan.org/pod/Data::TOON" rel="noopener noreferrer"&gt;Data::Toon&lt;/a&gt; but I wanted something with an interface that was more like JSON.pm.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  What TOON Is
&lt;/h2&gt;

&lt;p&gt;TOON stands for &lt;strong&gt;Token-Oriented Object Notation&lt;/strong&gt;. It’s a textual format for representing structured data — the same data model as JSON:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Objects (hashes)
&lt;/li&gt;
&lt;li&gt;Arrays
&lt;/li&gt;
&lt;li&gt;Strings
&lt;/li&gt;
&lt;li&gt;Numbers
&lt;/li&gt;
&lt;li&gt;Booleans
&lt;/li&gt;
&lt;li&gt;Null&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea behind TOON is that it is designed to be &lt;strong&gt;easy for both humans and language models to read and write&lt;/strong&gt;. It tries to reduce punctuation noise and make the structure of data clearer.&lt;/p&gt;

&lt;p&gt;If you think of the landscape like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Human-friendly&lt;/th&gt;
&lt;th&gt;Machine-friendly&lt;/th&gt;
&lt;th&gt;Very common&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Very&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOON&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Not yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;TOON is trying to sit in the middle: simpler than YAML, more readable than JSON.&lt;/p&gt;

&lt;p&gt;Whether it succeeds at that is a matter of taste — but it’s an interesting idea.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  TOON vs JSON vs YAML
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;  
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arthur Dent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arthur@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"street"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"High Street"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Guildford"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phones"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"01234 567890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"07700 900123"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  YAML
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Arthur Dent&lt;/span&gt;  
&lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;42&lt;/span&gt;  
&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arthur@example.com&lt;/span&gt;  
&lt;span class="na"&gt;alive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  
&lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;High Street&lt;/span&gt;  
  &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Guildford&lt;/span&gt;  
&lt;span class="na"&gt;phones&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="s"&gt;– 01234 567890&lt;/span&gt;  
  &lt;span class="s"&gt;– 07700 &lt;/span&gt;&lt;span class="m"&gt;900123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TOON
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Arthur Dent&lt;/span&gt;
&lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;
&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arthur@example.com”&lt;/span&gt;
&lt;span class="na"&gt;alive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;High Street&lt;/span&gt; 
  &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Guildford&lt;/span&gt;
&lt;span class="s"&gt;phones[2]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;01234 567890,07700 &lt;/span&gt;&lt;span class="m"&gt;900123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see that TOON sits somewhere between JSON and YAML:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less punctuation and quoting than JSON
&lt;/li&gt;
&lt;li&gt;More explicit structure than YAML
&lt;/li&gt;
&lt;li&gt;Still very easy to parse
&lt;/li&gt;
&lt;li&gt;Still clearly structured for machines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the idea, anyway.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Why People Think TOON Is Useful
&lt;/h2&gt;

&lt;p&gt;The current interest in TOON is largely driven by AI/LLM workflows.&lt;/p&gt;

&lt;p&gt;People are using it because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It is easier for humans to read than JSON.
&lt;/li&gt;
&lt;li&gt;It is less ambiguous and complex than YAML.
&lt;/li&gt;
&lt;li&gt;It maps cleanly to the JSON data model.
&lt;/li&gt;
&lt;li&gt;It is relatively easy to parse.
&lt;/li&gt;
&lt;li&gt;It works well in prompts and generated output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;You may or may not buy that argument — but it’s an interesting niche.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Wrote a Perl Module
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But what I &lt;em&gt;do&lt;/em&gt; have a strong opinion about is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a data format exists, then Perl should have a CPAN module for it that works the way Perl programmers expect.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perl already has very good, very consistent interfaces for data serialisation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON
&lt;/li&gt;
&lt;li&gt;YAML
&lt;/li&gt;
&lt;li&gt;Storable
&lt;/li&gt;
&lt;li&gt;Sereal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They all tend to follow the same pattern, particularly the object-oriented interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;pretty&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I wanted a TOON module that worked the same way.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Goals
&lt;/h2&gt;

&lt;p&gt;When designing the module, I had a few simple goals.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Familiar OO Interface
&lt;/h3&gt;

&lt;p&gt;The primary interface should be object-oriented and feel like JSON.pm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;TOON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$toon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;TOON&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;  
               &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;pretty&lt;/span&gt;  
               &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;canonical&lt;/span&gt;  
               &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$toon&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$toon&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already know JSON, you already know how to use TOON.&lt;/p&gt;

&lt;p&gt;There are also convenience functions, but the OO interface is the main one.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pure Perl Implementation
&lt;/h3&gt;

&lt;p&gt;Version 0.001 is pure Perl. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to install&lt;/li&gt;
&lt;li&gt;No compiler required&lt;/li&gt;
&lt;li&gt;Works everywhere Perl works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If TOON becomes popular and performance matters, someone can always write an XS backend later.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Clean Separation of Components
&lt;/h3&gt;

&lt;p&gt;Internally, the module is split into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokenizer&lt;/strong&gt; – turns text into tokens
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parser&lt;/strong&gt; – turns tokens into Perl data structures
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emitter&lt;/strong&gt; – turns Perl data structures into TOON text
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; – reports line/column errors cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it easier to test and maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Do the Simple Things Well First
&lt;/h3&gt;

&lt;p&gt;Version 0.001 supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalars&lt;/li&gt;
&lt;li&gt;Arrayrefs&lt;/li&gt;
&lt;li&gt;Hashrefs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;undef&lt;/code&gt; → null&lt;/li&gt;
&lt;li&gt;Pretty printing&lt;/li&gt;
&lt;li&gt;Canonical key ordering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; (yet) try to serialise blessed objects or do anything clever. That can come later if people actually want it.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Usage (OO Style)
&lt;/h2&gt;

&lt;p&gt;Here’s a simple Perl data structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Arthur Dent&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt;
  &lt;span class="s"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;drinks&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;["&lt;/span&gt;&lt;span class="s2"&gt;tea&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;coffee&lt;/span&gt;&lt;span class="p"&gt;"],&lt;/span&gt;
  &lt;span class="s"&gt;alive&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Encoding
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;TOON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$toon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;TOON&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;pretty&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$toon&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Decoding
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;TOON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$toon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;TOON&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$toon&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="k"&gt;print&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Convenience Functions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nv"&gt;TOON&lt;/span&gt; &lt;span class="sx"&gt;qw(encode_toon decode_toon)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;encode_toon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;decode_toon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the OO interface is where most of the flexibility lives.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Command Line Tool
&lt;/h2&gt;

&lt;p&gt;There’s also a command-line tool, &lt;code&gt;toon_pp&lt;/code&gt;, similar to &lt;code&gt;json_pp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;data.toon | toon_pp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which will pretty-print TOON data.&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;And fundamentally, this is how CPAN has always worked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;See a problem. Write a module. Upload it. See if anyone else finds it useful.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So now Perl has a TOON module. And if you already know how to use JSON.pm, you already know how to use it.&lt;/p&gt;

&lt;p&gt;That was the goal.&lt;/p&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/03/writing-a-toon-module-for-perl/" rel="noopener noreferrer"&gt;Writing a TOON Module for Perl&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cpan</category>
      <category>toon</category>
      <category>serialisation</category>
    </item>
    <item>
      <title>Still on the [b]leading edge</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sat, 21 Mar 2026 11:14:06 +0000</pubDate>
      <link>https://dev.to/davorg/still-on-the-bleading-edge-40df</link>
      <guid>https://dev.to/davorg/still-on-the-bleading-edge-40df</guid>
      <description>&lt;p&gt;About eighteen months ago, I wrote a post called &lt;a href="https://dev.to/davorg/on-the-bleading-edge-1olf"&gt;&lt;em&gt;On the Bleading Edge&lt;/em&gt;&lt;/a&gt; about my decision to start using Perl’s new &lt;code&gt;class&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;I still think that’s true.&lt;/p&gt;

&lt;p&gt;But every now and then, the bleading edge reminds you why it’s called that.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This is the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom
&lt;/h2&gt;

&lt;p&gt;I was building a Docker image for &lt;a href="https://aphra.perlhacks.com/" rel="noopener noreferrer"&gt;Aphra&lt;/a&gt;. As part of the build, I needed to install &lt;a href="https://metacpan.org/pod/App::HTTPThis" rel="noopener noreferrer"&gt;App::HTTPThis&lt;/a&gt;, which depends on &lt;a href="https://metacpan.org/pod/Plack::App::DirectoryIndex" rel="noopener noreferrer"&gt;Plack::App::DirectoryIndex&lt;/a&gt;, which depends on &lt;a href="https://metacpan.org/pod/WebServer::DirIndex" rel="noopener noreferrer"&gt;WebServer::DirIndex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Docker build failed with this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;13 45.66 &lt;span class="nt"&gt;--&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Working on WebServer::DirIndex
&lt;span class="gp"&gt;#&lt;/span&gt;13 45.66 Fetching https://www.cpan.org/authors/id/D/DA/DAVECROSS/WebServer-DirIndex-0.1.3.tar.gz ... OK
&lt;span class="gp"&gt;#&lt;/span&gt;13 45.83 Configuring WebServer-DirIndex-v0.1.3 ... OK
&lt;span class="gp"&gt;#&lt;/span&gt;13 46.21 Building WebServer-DirIndex-v0.1.3 ... OK
&lt;span class="gp"&gt;#&lt;/span&gt;13 46.75 Successfully installed WebServer-DirIndex-v0.1.3
&lt;span class="gp"&gt;#&lt;/span&gt;13 46.84 &lt;span class="o"&gt;!&lt;/span&gt; Installing the dependencies failed: Installed version &lt;span class="o"&gt;(&lt;/span&gt;undef&lt;span class="o"&gt;)&lt;/span&gt; of WebServer::DirIndex is not &lt;span class="k"&gt;in &lt;/span&gt;range &lt;span class="s1"&gt;'v0.1.0'&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;13 46.84 &lt;span class="o"&gt;!&lt;/span&gt; Bailing out the installation &lt;span class="k"&gt;for &lt;/span&gt;Plack-App-DirectoryIndex-v0.2.1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, that’s a deeply confusing error message.&lt;/p&gt;

&lt;p&gt;It clearly says that WebServer::DirIndex was successfully installed. And then immediately says that the installed version is &lt;code&gt;undef&lt;/code&gt; and not in the required range.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But the version number in WebServer::DirIndex was fine. The module built. The tests passed. Everything looked normal.&lt;/p&gt;

&lt;p&gt;So why did the installer think the version was &lt;code&gt;undef&lt;/code&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Bug Appears
&lt;/h2&gt;

&lt;p&gt;This only shows up in a fairly specific situation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A module uses modern Perl &lt;code&gt;class&lt;/code&gt; syntax&lt;/li&gt;
&lt;li&gt;The module defines a &lt;code&gt;$VERSION&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Another module declares a prerequisite with a specific version requirement&lt;/li&gt;
&lt;li&gt;The installer tries to check the installed version without loading the module&lt;/li&gt;
&lt;li&gt;It uses &lt;a href="https://metacpan.org/pod/Module::Metadata" rel="noopener noreferrer"&gt;Module::Metadata&lt;/a&gt; to extract &lt;code&gt;$VERSION&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;And the version of Module::Metadata it is using doesn’t properly understand &lt;code&gt;class&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So this bug only appears when you combine:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;modern Perl syntax + version checks + older toolchain&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is pretty much the definition of “bleading edge”.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Culprit
&lt;/h2&gt;

&lt;p&gt;The problem turned out to be an older version of Module::Metadata that had been fatpacked into &lt;code&gt;cpanm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cpanm&lt;/code&gt; uses &lt;code&gt;Module::Metadata&lt;/code&gt; to inspect modules and extract &lt;code&gt;$VERSION&lt;/code&gt; without loading the module. But the older &lt;code&gt;Module::Metadata&lt;/code&gt; didn’t correctly understand the &lt;code&gt;class&lt;/code&gt; keyword, so it couldn’t work out which package the &lt;code&gt;$VERSION&lt;/code&gt; belonged to.&lt;/p&gt;

&lt;p&gt;So when it checked the installed version, it found… nothing.&lt;/p&gt;

&lt;p&gt;Hence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Installed version (undef) of WebServer::DirIndex is not in range ‘v0.1.0’&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The version wasn’t wrong. The installer just couldn’t see it.&lt;/p&gt;

&lt;p&gt;An aside, you may find it amusing to hear an anecdote from my attempts to debug this problem.&lt;/p&gt;

&lt;p&gt;I spun up a new Ubuntu Docker container, installed &lt;code&gt;cpanm&lt;/code&gt; and tried to install Plack::App::DirectoryIndex. Initially, this gave the same error message. At least the problem was easily reproducible.&lt;/p&gt;

&lt;p&gt;I then ran code that was very similar to the code &lt;code&gt;cpanm&lt;/code&gt; uses to work out what a module’s version is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;perl &lt;span class="nt"&gt;-MModule&lt;/span&gt;::Metadata &lt;span class="nt"&gt;-E&lt;/span&gt;&lt;span class="s1"&gt;'say Module::Metadata-&amp;gt;new_from_module("WebServer::DirIndex")-&amp;gt;version'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This displayed an empty string. I was really onto something here. Module::Metadata couldn’t find the version.&lt;/p&gt;

&lt;p&gt;I was using Module::Metadata version 1.000037 and, looking at the change log on CPAN, I saw this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;1.000038 2023-04-28 11:25:40Z&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-&lt;/code&gt; &lt;code&gt;detects "class" syntax&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I installed 1.000038 and reran my command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;perl &lt;span class="nt"&gt;-MModule&lt;/span&gt;::Metadata &lt;span class="nt"&gt;-E&lt;/span&gt;&lt;span class="s1"&gt;'say Module::Metadata-&amp;gt;new_from_module("WebServer::DirIndex")-&amp;gt;version'&lt;/span&gt;
&lt;span class="go"&gt;0.1.3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That seemed conclusive. Excitedly, I reran the Docker build.&lt;/p&gt;

&lt;p&gt;It failed again.&lt;/p&gt;

&lt;p&gt;You’ve probably worked out why. But it took me a frustrating half an hour to work it out.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cpanm&lt;/code&gt; doesn’t use the installed version of Module::Metadata. It uses its own, fatpacked version. Updating Module::Metadata wouldn’t fix my problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workaround
&lt;/h2&gt;

&lt;p&gt;I found a workaround. That was to add a redundant &lt;code&gt;package&lt;/code&gt; declaration alongside the &lt;code&gt;class&lt;/code&gt; declaration, so older versions of Module::Metadata can still identify the package that owns &lt;code&gt;$VERSION&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So instead of just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;class&lt;/span&gt; &lt;span class="nn"&gt;WebServer::&lt;/span&gt;&lt;span class="nv"&gt;DirIndex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;our&lt;/span&gt; &lt;span class="nv"&gt;$VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.1.3&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I now have this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nb"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;WebServer::&lt;/span&gt;&lt;span class="nv"&gt;DirIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;class&lt;/span&gt; &lt;span class="nn"&gt;WebServer::&lt;/span&gt;&lt;span class="nv"&gt;DirIndex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;our&lt;/span&gt; &lt;span class="nv"&gt;$VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.1.3&lt;/span&gt;&lt;span class="p"&gt;';&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks unnecessary. And in a perfect world, it would be unnecessary.&lt;/p&gt;

&lt;p&gt;But it allows older tooling to work out the version correctly, and everything installs cleanly again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Proper Fix
&lt;/h2&gt;

&lt;p&gt;Of course, the real fix was to update the toolchain.&lt;/p&gt;

&lt;p&gt;So I &lt;a href="https://github.com/miyagawa/cpanminus/issues/697" rel="noopener noreferrer"&gt;raised an issue against App::cpanminus&lt;/a&gt;, pointing out that the fatpacked Module::Metadata was too old to cope properly with modules that use &lt;code&gt;class&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tatsuhiko Miyagawa responded very quickly, and a new release of &lt;code&gt;cpanm&lt;/code&gt; appeared with an updated version of Module::Metadata.&lt;/p&gt;

&lt;p&gt;This is one of the nice things about the Perl ecosystem. Sometimes you report a problem and the right person fixes it almost immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Do I Remove the Workaround?
&lt;/h2&gt;

&lt;p&gt;This leaves me with an interesting question.&lt;/p&gt;

&lt;p&gt;The correct fix is “use a recent &lt;code&gt;cpanm&lt;/code&gt;”.&lt;/p&gt;

&lt;p&gt;But the workaround is “add a redundant &lt;code&gt;package&lt;/code&gt; line so older tooling doesn’t get confused”.&lt;/p&gt;

&lt;p&gt;So when do I remove the workaround?&lt;/p&gt;

&lt;p&gt;The answer is probably: not yet.&lt;/p&gt;

&lt;p&gt;Because although a fixed &lt;code&gt;cpanm&lt;/code&gt; 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 &lt;code&gt;cpanm&lt;/code&gt; lurking in them.&lt;/p&gt;

&lt;p&gt;And the workaround is harmless. It just offends my sense of neatness slightly.&lt;/p&gt;

&lt;p&gt;So for now, the redundant &lt;code&gt;package&lt;/code&gt; line stays. Not because modern Perl needs it, but because parts of the world around modern Perl are still catching up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Life on the Bleading Edge
&lt;/h2&gt;

&lt;p&gt;This is what life on the bleading edge actually looks like.&lt;/p&gt;

&lt;p&gt;Not dramatic crashes. Not language bugs. Not catastrophic failures.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;And then you lose two days proving that you are not, in fact, going mad.&lt;/p&gt;

&lt;p&gt;But I’m still using &lt;code&gt;class&lt;/code&gt;. And I’m still happy I am.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/03/still-on-the-bleading-edge/" rel="noopener noreferrer"&gt;Still on the [b]leading edge&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>cpan</category>
      <category>docker</category>
      <category>modulemetadata</category>
    </item>
    <item>
      <title>Your README Is Already a Website</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sat, 28 Feb 2026 17:42:48 +0000</pubDate>
      <link>https://dev.to/davorg/your-readme-is-already-a-website-dg7</link>
      <guid>https://dev.to/davorg/your-readme-is-already-a-website-dg7</guid>
      <description>&lt;h2&gt;
  
  
  Announcing &lt;code&gt;readme-to-index&lt;/code&gt; — My First GitHub Marketplace Release 🎉
&lt;/h2&gt;

&lt;p&gt;Today I published my first GitHub Action to the &lt;a href="https://github.com/marketplace/" rel="noopener noreferrer"&gt;Marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s called &lt;a href="https://github.com/marketplace/actions/readme-to-index-html" rel="noopener noreferrer"&gt;&lt;strong&gt;readme-to-index&lt;/strong&gt;&lt;/a&gt;, and it does something very simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It turns your &lt;code&gt;README.md&lt;/code&gt; into a clean, styled &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;No Jekyll.&lt;br&gt;
No Ruby.&lt;br&gt;
No themes.&lt;br&gt;
No &lt;code&gt;_config.yml&lt;/code&gt;.&lt;br&gt;
No implicit behaviour.&lt;/p&gt;

&lt;p&gt;Just Markdown → HTML → Done.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;I have a lot of small projects.&lt;/p&gt;

&lt;p&gt;Many of them already have good READMEs. In fact, for most of them, the README &lt;em&gt;is&lt;/em&gt; the documentation.&lt;/p&gt;

&lt;p&gt;So the obvious question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why create a separate site when the README already exists?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GitHub Pages + Jekyll is great. But for small libraries and utilities, it can feel like overkill.&lt;/p&gt;

&lt;p&gt;I wanted something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal&lt;/li&gt;
&lt;li&gt;Deterministic&lt;/li&gt;
&lt;li&gt;Easy to drop into any workflow&lt;/li&gt;
&lt;li&gt;Friendly to CI pipelines&lt;/li&gt;
&lt;li&gt;With zero hidden conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a GitHub Action that does exactly one thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;README.md → index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Styled using &lt;a href="https://simplecss.org/" rel="noopener noreferrer"&gt;Simple.css&lt;/a&gt; (by default, but you can use any stylesheet you like).&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Converts &lt;code&gt;README.md&lt;/code&gt; to &lt;code&gt;index.html&lt;/code&gt; using &lt;a href="https://pandoc.org/" rel="noopener noreferrer"&gt;Pandoc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Automatically sets the HTML &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; from the first &lt;code&gt;# Heading&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Applies Simple.css for clean, classless styling&lt;/li&gt;
&lt;li&gt;Suppresses Pandoc’s injected syntax-highlighting CSS&lt;/li&gt;
&lt;li&gt;Leaves your original README untouched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your README remains the canonical source of truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Add this to your workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;davorg/readme-to-index@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;_site/index.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then deploy using the standard GitHub Pages artifact flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available Options
&lt;/h2&gt;

&lt;p&gt;The action supports a few optional inputs:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;readme&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Path to the Markdown source file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; &lt;code&gt;README.md&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;output&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Path to the generated HTML file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; &lt;code&gt;index.html&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;css_url&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;CSS stylesheet to include in the generated HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; &lt;code&gt;https://cdn.simplecss.org/simple.min.css&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;install_pandoc&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Whether to install Pandoc automatically using &lt;code&gt;apt-get&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; &lt;code&gt;true&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Set this to &lt;code&gt;false&lt;/code&gt; if your workflow already installs Pandoc.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;extra_pandoc_args&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Additional arguments passed directly to the Pandoc command.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;davorg/readme-to-index@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;extra_pandoc_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--toc"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Enabling GitHub Pages
&lt;/h2&gt;

&lt;p&gt;For this to deploy as a website, you’ll need to enable GitHub Pages in your repository settings.&lt;/p&gt;

&lt;p&gt;Go to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository → Settings → Pages → Build and deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it. The workflow will handle the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example Complete Workflow
&lt;/h2&gt;

&lt;p&gt;Here’s a minimal working example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish README to GitHub Pages&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-pages&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;davorg/readme-to-index@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;_site/index.html&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/configure-pages@v5&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-pages-artifact@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;_site&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployment&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/deploy-pages@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Not Just Use Jekyll?
&lt;/h2&gt;

&lt;p&gt;You absolutely can.&lt;/p&gt;

&lt;p&gt;But this approach has two real advantages:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Simplicity
&lt;/h3&gt;

&lt;p&gt;There’s no Ruby toolchain.&lt;/p&gt;

&lt;p&gt;There are no implicit layouts.&lt;/p&gt;

&lt;p&gt;There are no theme conventions to understand.&lt;/p&gt;

&lt;p&gt;It takes one Markdown file and produces one HTML file.&lt;/p&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pipeline-Friendly
&lt;/h3&gt;

&lt;p&gt;Because it’s "just a step", you can use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As part of a release build&lt;/li&gt;
&lt;li&gt;In CI pipelines&lt;/li&gt;
&lt;li&gt;Before publishing documentation artifacts&lt;/li&gt;
&lt;li&gt;Outside GitHub entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It doesn’t rely on GitHub Pages’ default behaviour.&lt;/p&gt;

&lt;p&gt;It works wherever Pandoc runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Small Milestone
&lt;/h2&gt;

&lt;p&gt;Shipping something to the GitHub Marketplace feels surprisingly significant.&lt;/p&gt;

&lt;p&gt;It’s a tiny tool.&lt;br&gt;
It does one thing.&lt;br&gt;
But it does it cleanly.&lt;/p&gt;

&lt;p&gt;That’s the kind of tooling I like building.&lt;/p&gt;

&lt;p&gt;If you’re interested:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/marketplace/actions/readme-to-index-html" rel="noopener noreferrer"&gt;https://github.com/marketplace/actions/readme-to-index-html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome. Stars appreciated. Minimalism encouraged.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>githubpages</category>
      <category>readme</category>
      <category>website</category>
    </item>
    <item>
      <title>Treating GitHub Copilot as a Contributor</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sun, 22 Feb 2026 11:51:15 +0000</pubDate>
      <link>https://dev.to/davorg/treating-github-copilot-as-a-contributor-186m</link>
      <guid>https://dev.to/davorg/treating-github-copilot-as-a-contributor-186m</guid>
      <description>&lt;p&gt;For some time, we’ve talked about GitHub Copilot as if it were a clever autocomplete engine.&lt;/p&gt;

&lt;p&gt;It isn’t.&lt;/p&gt;

&lt;p&gt;Or rather, that’s not all it is.&lt;/p&gt;

&lt;p&gt;The interesting thing — the thing that genuinely changes how you work — is that you can assign GitHub issues to Copilot.&lt;/p&gt;

&lt;p&gt;And it behaves like a contributor.&lt;/p&gt;

&lt;p&gt;Over the past day, I’ve been doing exactly that on my new CPAN module, &lt;a href="https://metacpan.org/pod/WebServer::DirIndex" rel="noopener noreferrer"&gt;WebServer::DirIndex&lt;/a&gt;. 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.&lt;/p&gt;

&lt;p&gt;That still feels faintly futuristic. But it’s not “vibe coding”. It’s surprisingly structured.&lt;/p&gt;

&lt;p&gt;Let me explain how it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  It Starts With a Proper Issue
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add CPAN scaffolding&lt;/li&gt;
&lt;li&gt;Update the classes to use Feature::Compat::Class&lt;/li&gt;
&lt;li&gt;Replace DirHandle&lt;/li&gt;
&lt;li&gt;Add WebServer::DirIndex::File&lt;/li&gt;
&lt;li&gt;Move &lt;code&gt;render()&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;:reader&lt;/code&gt; attribute where useful&lt;/li&gt;
&lt;li&gt;Remove dependency on Plack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one was a focused, bounded piece of work. Each one had clear expectations.&lt;/p&gt;

&lt;p&gt;The key is this: Copilot works best when you behave like a maintainer, not a magician.&lt;/p&gt;

&lt;p&gt;You describe the change precisely. You state constraints. You mention compatibility requirements. You indicate whether tests need to be updated.&lt;/p&gt;

&lt;p&gt;Then you assign the issue to Copilot.&lt;/p&gt;

&lt;p&gt;And wait.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pull Request Arrives
&lt;/h2&gt;

&lt;p&gt;After a few minutes — sometimes ten, sometimes less — Copilot creates a branch and opens a pull request.&lt;/p&gt;

&lt;p&gt;The PR contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code changes&lt;/li&gt;
&lt;li&gt;Updated or new tests&lt;/li&gt;
&lt;li&gt;A descriptive PR message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because it’s a real PR, your CI runs automatically. The code is evaluated in the same way as any other contribution.&lt;/p&gt;

&lt;p&gt;This is already a major improvement over editor-based prompting. The work is isolated, reviewable, and properly versioned.&lt;/p&gt;

&lt;p&gt;But the most interesting part is what happens in the background.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watching Copilot Think
&lt;/h2&gt;

&lt;p&gt;If you visit the &lt;strong&gt;Agents&lt;/strong&gt; tab in the repository, you can see Copilot reasoning through the issue.&lt;/p&gt;

&lt;p&gt;It reads like a junior developer narrating their approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interpreting the problem&lt;/li&gt;
&lt;li&gt;Identifying the relevant files&lt;/li&gt;
&lt;li&gt;Planning changes&lt;/li&gt;
&lt;li&gt;Considering test updates&lt;/li&gt;
&lt;li&gt;Running validation steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you can interrupt it.&lt;/p&gt;

&lt;p&gt;If it starts drifting toward unnecessary abstraction or broad refactoring, you can comment and steer it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Please don’t change the public API.&lt;/li&gt;
&lt;li&gt;Avoid experimental Perl features.&lt;/li&gt;
&lt;li&gt;This must remain compatible with Perl 5.40.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It responds. It adjusts course.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Teaching Copilot About Your Project
&lt;/h2&gt;

&lt;p&gt;Out of the box, Copilot doesn’t really know how your repository works. It sees code, but it doesn’t know policy.&lt;/p&gt;

&lt;p&gt;That’s where repository-level configuration becomes useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Custom Repository Instructions
&lt;/h3&gt;

&lt;p&gt;GitHub allows you to provide a &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; file that gives Copilot repository-specific guidance. The documentation for this lives here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions" rel="noopener noreferrer"&gt;Adding repository custom instructions for GitHub Copilot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When GitHub offers to generate this file for you, say yes.&lt;/p&gt;

&lt;p&gt;Then customise it properly.&lt;/p&gt;

&lt;p&gt;In a CPAN module, I tend to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum supported Perl version&lt;/li&gt;
&lt;li&gt;Whether Feature::Compat::Class is preferred&lt;/li&gt;
&lt;li&gt;Whether experimental features are forbidden&lt;/li&gt;
&lt;li&gt;CPAN layout expectations (&lt;code&gt;lib/&lt;/code&gt;, &lt;code&gt;t/&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Test conventions (Test::More, no stray diagnostics)&lt;/li&gt;
&lt;li&gt;A strong preference for not breaking the public API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this file, Copilot guesses.&lt;/p&gt;

&lt;p&gt;With this file, Copilot aligns itself with your house style.&lt;/p&gt;

&lt;p&gt;That difference is impressive.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Customising the Copilot Development Environment
&lt;/h3&gt;

&lt;p&gt;There’s another piece that many people miss: Copilot can run a special workflow event called &lt;code&gt;copilot_agent_setup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can define a workflow that prepares the environment Copilot works in. GitHub documents this here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a&gt;Customizing the development environment for GitHub Copilot coding agent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my Perl projects, I use this standard setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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 .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Obviously, that was originally written for me by Copilot!)&lt;/p&gt;

&lt;p&gt;This does two important things.&lt;/p&gt;

&lt;p&gt;Firstly, it ensures Copilot is working with the correct Perl version.&lt;/p&gt;

&lt;p&gt;Secondly, it installs the distribution dependencies, meaning Copilot can reason in a context that actually resembles my real development environment.&lt;/p&gt;

&lt;p&gt;Without this workflow, Copilot operates in a kind of generic space.&lt;/p&gt;

&lt;p&gt;With it, Copilot behaves like a contributor who has actually checked out your code and run &lt;code&gt;cpanm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s a useful difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reviewing the Work
&lt;/h2&gt;

&lt;p&gt;This is the part where it’s important not to get starry-eyed.&lt;/p&gt;

&lt;p&gt;I still review the PR carefully.&lt;/p&gt;

&lt;p&gt;I still check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has it changed behaviour unintentionally?&lt;/li&gt;
&lt;li&gt;Has it introduced unnecessary abstraction?&lt;/li&gt;
&lt;li&gt;Are the tests meaningful?&lt;/li&gt;
&lt;li&gt;Has it expanded scope beyond the issue?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I check out the branch and run the tests. Exactly as I would with a PR from a human co-worker.&lt;/p&gt;

&lt;p&gt;You can request changes and reassign the PR to Copilot. It will revise its branch.&lt;/p&gt;

&lt;p&gt;The loop is fast. Faster than traditional asynchronous code review.&lt;/p&gt;

&lt;p&gt;But the responsibility is unchanged. You are still the maintainer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Feels Different
&lt;/h2&gt;

&lt;p&gt;What’s happening here isn’t just “AI writing code”. It’s AI integrated into the contribution workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issues&lt;/li&gt;
&lt;li&gt;Structured reasoning&lt;/li&gt;
&lt;li&gt;Pull requests&lt;/li&gt;
&lt;li&gt;CI&lt;/li&gt;
&lt;li&gt;Review cycles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That architecture matters.&lt;/p&gt;

&lt;p&gt;It means you can use Copilot in a controlled, auditable way.&lt;/p&gt;

&lt;p&gt;In my experience with WebServer::DirIndex, this model works particularly well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mechanical refactors&lt;/li&gt;
&lt;li&gt;Adding attributes (e.g. &lt;code&gt;:reader&lt;/code&gt; where appropriate)&lt;/li&gt;
&lt;li&gt;Removing dependencies&lt;/li&gt;
&lt;li&gt;Moving methods cleanly&lt;/li&gt;
&lt;li&gt;Adding new internal classes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is less strong when the issue itself is vague or architectural. Copilot cannot infer the intent you didn’t articulate.&lt;/p&gt;

&lt;p&gt;But given a clear issue, it’s remarkably capable — even with modern Perl using tools like Feature::Compat::Class.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Small but Important Point for the Perl Community
&lt;/h2&gt;

&lt;p&gt;I’ve seen people saying that AI tools don’t handle Perl well. That has not been my experience.&lt;/p&gt;

&lt;p&gt;With a properly described issue, repository instructions, and a defined development environment, Copilot works competently with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modern Perl syntax&lt;/li&gt;
&lt;li&gt;CPAN distribution layouts&lt;/li&gt;
&lt;li&gt;Test suites&lt;/li&gt;
&lt;li&gt;Feature::Compat::Class (or whatever OO framework I’m using on a particular project)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The constraint isn’t the language. It’s how clearly you explain the task.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Shift
&lt;/h2&gt;

&lt;p&gt;The most interesting thing here isn’t that Copilot writes Perl. It’s that GitHub allows you to treat AI as a contributor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You file an issue.&lt;/li&gt;
&lt;li&gt;You assign it.&lt;/li&gt;
&lt;li&gt;You supervise its reasoning.&lt;/li&gt;
&lt;li&gt;You review its PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;If you want to have a closer look at the issues and PRs I’m talking about, here are some links?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/davorg-cpan/webserver-dirindex" rel="noopener noreferrer"&gt;WebServer::DirIndex repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davorg-cpan/webserver-dirindex/issues?q=is%3Aissue%20state%3Aclosed" rel="noopener noreferrer"&gt;Closed issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/davorg-cpan/webserver-dirindex/pulls?q=is%3Apr+is%3Aclosed" rel="noopener noreferrer"&gt;Closed PRs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/02/treating-github-copilot-as-a-contributor/" rel="noopener noreferrer"&gt;Treating GitHub Copilot as a Contributor&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>perl</category>
      <category>github</category>
      <category>githubcopilot</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built a Tiny Domain Inventory Tool (Because I Used to Buy Too Many Domains)</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sun, 08 Feb 2026 12:22:22 +0000</pubDate>
      <link>https://dev.to/davorg/i-built-a-tiny-domain-inventory-tool-because-i-used-to-buy-too-many-domains-45lg</link>
      <guid>https://dev.to/davorg/i-built-a-tiny-domain-inventory-tool-because-i-used-to-buy-too-many-domains-45lg</guid>
      <description>&lt;p&gt;(Or: how I stopped losing track of the domains I swore I wouldn’t buy)&lt;/p&gt;

&lt;p&gt;I used to be a serial domain-buyer.&lt;/p&gt;

&lt;p&gt;Not in a dramatic, “lost a fortune” way — just lots of &lt;em&gt;“oh, that might be useful one day”&lt;/em&gt; moments spread over many years. At one point, I honestly couldn’t tell you exactly what I owned, where it was registered, or what half of it pointed at.&lt;/p&gt;

&lt;p&gt;I’m mostly better now. Mostly.&lt;/p&gt;

&lt;p&gt;But I still own more domains than I like to admit, and I wanted a &lt;strong&gt;simple, honest way to see what I’ve got&lt;/strong&gt; without:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;another SaaS subscription&lt;/li&gt;
&lt;li&gt;a spreadsheet that immediately goes stale&lt;/li&gt;
&lt;li&gt;or building a backend I’d then have to maintain forever&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built this instead:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://davorg.dev/mydomains/" rel="noopener noreferrer"&gt;https://davorg.dev/mydomains/&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
👉 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/davorg/mydomains" rel="noopener noreferrer"&gt;https://github.com/davorg/mydomains&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a small, static, browser-only tool for keeping track of domains, their DNS, and where they’re hosted — and it turns out to be surprisingly useful.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I wanted (and what I very deliberately didn’t)
&lt;/h2&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runs entirely in the browser&lt;/li&gt;
&lt;li&gt;stores its data locally&lt;/li&gt;
&lt;li&gt;can be hosted as static files&lt;/li&gt;
&lt;li&gt;gives me &lt;em&gt;useful facts&lt;/em&gt;, not a false sense of certainty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I explicitly didn’t want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accounts&lt;/li&gt;
&lt;li&gt;logins&lt;/li&gt;
&lt;li&gt;databases&lt;/li&gt;
&lt;li&gt;background jobs&lt;/li&gt;
&lt;li&gt;“AI-powered insights” (whatever that would even mean here)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a tool for &lt;em&gt;me&lt;/em&gt;. I trust myself with my own data.&lt;/p&gt;

&lt;p&gt;If that resonates with you, read on.&lt;/p&gt;


&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;At its core, it’s just an inventory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a list of domains you own&lt;/li&gt;
&lt;li&gt;the hostnames you care about (&lt;code&gt;@&lt;/code&gt;, &lt;code&gt;www&lt;/code&gt;, &lt;code&gt;mail&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;some notes and keywords so future-you knows &lt;em&gt;why&lt;/em&gt; you bought it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that, it can enrich each domain with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DNS data&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NS, MX, TXT for the domain&lt;/li&gt;
&lt;li&gt;A / AAAA / CNAME for each host&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RDAP data&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;registrar&lt;/li&gt;
&lt;li&gt;expiry date&lt;/li&gt;
&lt;li&gt;registration status&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Best-effort guesses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS provider&lt;/li&gt;
&lt;li&gt;hosting provider per host&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is opt-in and cached. Nothing happens unless you click “refresh”.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture: aggressively boring (by design)
&lt;/h2&gt;

&lt;p&gt;There are three files that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;index.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;style.css&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;script.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No build step. No framework. No external dependencies beyond public HTTP APIs.&lt;/p&gt;

&lt;p&gt;All state lives in &lt;code&gt;localStorage&lt;/code&gt; under a single key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORAGE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;domainInventory.v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you clear your browser data, it’s gone. Which is why import/export exists.&lt;/p&gt;

&lt;p&gt;That’s not a bug — that’s the trade-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  DNS and RDAP: best-effort, not gospel
&lt;/h2&gt;

&lt;p&gt;DNS is fetched using Google’s DNS-over-HTTPS endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://dns.google/resolve?name=example.com&amp;amp;type=A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RDAP is fetched via &lt;code&gt;rdap.org&lt;/code&gt;, with a couple of TLD-specific exceptions where needed.&lt;/p&gt;

&lt;p&gt;This immediately gives you two realities to accept:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Not everything has RDAP&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS sometimes says no&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code treats RDAP as optional. If it works, great. If not, nothing breaks.&lt;/p&gt;

&lt;p&gt;That pattern shows up a lot in this project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Provider guessing (aka “useful lies”)
&lt;/h2&gt;

&lt;p&gt;One of the more interesting parts was deciding how far to go with &lt;em&gt;interpretation&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  DNS provider detection
&lt;/h3&gt;

&lt;p&gt;This is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;guessDnsProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nsList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nsLower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nsList&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nsLower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudflare.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cloudflare&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nsLower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;awsdns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS Route 53&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nsLower&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gandi.net&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gandi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is it exhaustive? No.&lt;br&gt;
Is it useful for &lt;em&gt;my&lt;/em&gt; domains? Yes.&lt;/p&gt;

&lt;p&gt;That’s a recurring theme here.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hosting provider detection (and Cloudflare honesty)
&lt;/h3&gt;

&lt;p&gt;Hosting detection looks at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CNAME targets&lt;/li&gt;
&lt;li&gt;A / AAAA addresses&lt;/li&gt;
&lt;li&gt;known IP ranges&lt;/li&gt;
&lt;li&gt;known patterns (&lt;code&gt;.github.io&lt;/code&gt;, &lt;code&gt;.cloudfront.net&lt;/code&gt;, &lt;code&gt;.a.run.app&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But DNS can lie — or at least obscure.&lt;/p&gt;

&lt;p&gt;So if a host resolves to Cloudflare IPs, the tool does this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it recognised the origin → “GitHub Pages (via Cloudflare)”&lt;/li&gt;
&lt;li&gt;Otherwise → “Unknown (via Cloudflare)”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the data doesn’t support a confident claim, I’d rather say &lt;em&gt;“I don’t know”&lt;/em&gt; than pretend.&lt;/p&gt;


&lt;h2&gt;
  
  
  Caching with intent
&lt;/h2&gt;

&lt;p&gt;All fetched data is cached per domain, along with a timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastChecked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here’s the important bit: &lt;strong&gt;the cache is only cleared when it should be&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you edit notes or keywords, nothing is invalidated.&lt;/p&gt;

&lt;p&gt;If you change the domain name or host list, the cache is wiped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nameChanged&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;hostsChanged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one small decision makes the tool feel calm instead of twitchy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this tool is &lt;em&gt;not&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;It is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an auto-discovery system&lt;/li&gt;
&lt;li&gt;a real-time monitor&lt;/li&gt;
&lt;li&gt;a source of legal truth about domain ownership&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It assumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you know which domains you own&lt;/li&gt;
&lt;li&gt;you know which hosts matter&lt;/li&gt;
&lt;li&gt;you’ll refresh things when you care&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In return, it stays small, fast, and understandable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Possible future improvements (and non-improvements)
&lt;/h2&gt;

&lt;p&gt;Things I might add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more provider heuristics (DigitalOcean, Fly.io, etc.)&lt;/li&gt;
&lt;li&gt;configurable IP allow-lists per provider&lt;/li&gt;
&lt;li&gt;better handling of registrar quirks per TLD&lt;/li&gt;
&lt;li&gt;bulk refresh with throttling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things I probably won’t:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accounts&lt;/li&gt;
&lt;li&gt;sync&lt;/li&gt;
&lt;li&gt;background polling&lt;/li&gt;
&lt;li&gt;turning this into a product&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment it needs a backend, it stops being &lt;em&gt;this&lt;/em&gt; tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I’m happy with it
&lt;/h2&gt;

&lt;p&gt;This started as a way to get a handle on my own past enthusiasm for buying domains.&lt;/p&gt;

&lt;p&gt;It ended up as a reminder that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a browser is a perfectly good runtime&lt;/li&gt;
&lt;li&gt;not every problem needs infrastructure&lt;/li&gt;
&lt;li&gt;“good enough and honest” beats “comprehensive but fragile”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you own more domains than you’d like to admit, you might find it useful too.&lt;/p&gt;

&lt;p&gt;And if nothing else, it’s a nice excuse to build something small, tidy, and under your control.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>sideprojects</category>
      <category>dns</category>
    </item>
    <item>
      <title>App::HTTPThis: the tiny web server I keep reaching for</title>
      <dc:creator>Dave Cross</dc:creator>
      <pubDate>Sun, 04 Jan 2026 13:46:13 +0000</pubDate>
      <link>https://dev.to/davorg/apphttpthis-the-tiny-web-server-i-keep-reaching-for-2mf0</link>
      <guid>https://dev.to/davorg/apphttpthis-the-tiny-web-server-i-keep-reaching-for-2mf0</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For that job, I’ve been using &lt;a href="https://metacpan.org/dist/App-HTTPThis/view/bin/http_this" rel="noopener noreferrer"&gt;&lt;strong&gt;App::HTTPThis&lt;/strong&gt;&lt;/a&gt; for years.&lt;/p&gt;

&lt;p&gt;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: &lt;em&gt;run a command and you’ve got a website&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I’ve used it for years
&lt;/h2&gt;

&lt;p&gt;Static sites are deceptively simple… right up until they aren’t.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You want to check that relative links behave the way you think they do.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want to confirm your CSS and images are loading with the paths you expect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You want to reproduce “real HTTP” behaviour (caching headers, MIME types, directory handling) rather than viewing files directly from disk.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sure, you &lt;em&gt;can&lt;/em&gt; open &lt;code&gt;file:///.../index.html&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;http_this&lt;/code&gt;, the workflow is basically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cd&lt;/code&gt; into your site directory&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;run a single command&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;open a URL&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;get on with your life&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s the “tiny screwdriver” that’s always on my desk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I took it over
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But I was using App::HTTPThis regularly, and I had one small-but-annoying itch: when you visited a directory URL, it would always show a directory listing - even if that directory contained an &lt;code&gt;index.html.&lt;/code&gt; So instead of behaving like a typical web server (serve index.html by default), it treated &lt;code&gt;index.html&lt;/code&gt; as just another file you had to click.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;(If you want to read more on this story, I wrote a couple of &lt;a href="https://dev.to/davorg/the-story-behind-a-new-module-2gkp"&gt;blog&lt;/a&gt; &lt;a href="https://dev.to/davorg/mission-almost-accomplished-3ccg"&gt;posts&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’ve done since taking it over
&lt;/h2&gt;

&lt;p&gt;Most of the changes are about making the “serve a directory” experience smoother, without turning it into a kitchen-sink web server.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Serve index pages by default (autoindex)
&lt;/h3&gt;

&lt;p&gt;The first change was to make directory URLs behave like you’d expect: if &lt;code&gt;index.html&lt;/code&gt; exists, serve it automatically. If it doesn’t, you still get a directory listing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Prettier index pages
&lt;/h3&gt;

&lt;p&gt;Once autoindex was in place, I then turned my attention to the &lt;em&gt;fallback&lt;/em&gt; directory listing page. If there isn’t an &lt;code&gt;index.html&lt;/code&gt;, 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 do end up browsing raw directories.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) A config file
&lt;/h3&gt;

&lt;p&gt;Once you’ve used a tool for a while, you start to realise you run it &lt;em&gt;the same way&lt;/em&gt; most of the time.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) &lt;code&gt;--host&lt;/code&gt; option
&lt;/h3&gt;

&lt;p&gt;The ability to control the host binding sounds like an edge case until it isn’t.&lt;/p&gt;

&lt;p&gt;Sometimes you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;only &lt;code&gt;localhost&lt;/code&gt; access for safety;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;access from other devices on your network (phone/tablet testing);&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;behaviour that matches a particular environment.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;--host&lt;/code&gt; option gives you that control without adding complexity to the default case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bonjour feature (and what it’s for)
&lt;/h2&gt;

&lt;p&gt;This is the part I only really appreciated recently: App::HTTPThis can advertise itself on your local network using &lt;strong&gt;mDNS / DNS-SD&lt;/strong&gt; – commonly called &lt;em&gt;Bonjour&lt;/em&gt; on Apple platforms, &lt;em&gt;Avahi&lt;/em&gt; on Linux, and various other names depending on who you’re talking to.&lt;/p&gt;

&lt;p&gt;It’s switched on with the &lt;code&gt;--name&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http_this --name MyService&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When you do that, &lt;code&gt;http_this&lt;/code&gt; publishes an &lt;code&gt;_http._tcp&lt;/code&gt; service on your local network with the instance name you chose (&lt;code&gt;MyService&lt;/code&gt; 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 &lt;code&gt;http://192.168.1.23:7007/&lt;/code&gt;”.&lt;/p&gt;

&lt;p&gt;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: &lt;strong&gt;not Apple magic&lt;/strong&gt; , just &lt;strong&gt;local-network service discovery&lt;/strong&gt; with a branding problem.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://github.com/davorg-cpan/app-httpthis/blob/master/BONJOUR.md" rel="noopener noreferrer"&gt;&lt;code&gt;BONJOUR.md&lt;/code&gt;&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;(If you’re curious, look for &lt;code&gt;_http._tcp&lt;/code&gt; and your chosen service name.)&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related tools in the same family
&lt;/h2&gt;

&lt;p&gt;App::HTTPThis is part of a little ecosystem of “run a thing &lt;em&gt;here&lt;/em&gt; quickly” command-line apps. If you like the shape of &lt;code&gt;http_this&lt;/code&gt;, you might also want to look at these siblings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://metacpan.org/pod/https_this" rel="noopener noreferrer"&gt;&lt;strong&gt;https_this&lt;/strong&gt;&lt;/a&gt; : like &lt;code&gt;http_this&lt;/code&gt;, but served over HTTPS (useful when you need to test secure contexts, service workers, APIs that require HTTPS, etc.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://metacpan.org/pod/cgi_this" rel="noopener noreferrer"&gt;&lt;strong&gt;cgi_this&lt;/strong&gt;&lt;/a&gt; : for quick CGI-style testing without setting up a full web server stack&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://metacpan.org/pod/dav_this" rel="noopener noreferrer"&gt;&lt;strong&gt;dav_this&lt;/strong&gt;&lt;/a&gt; : serves content over WebDAV (handy for testing clients or workflows that expect DAV)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://metacpan.org/pod/ftp_this" rel="noopener noreferrer"&gt;&lt;strong&gt;ftp_this&lt;/strong&gt;&lt;/a&gt; : quick FTP server for those rare-but-real moments when you need one&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;And if you’ve got ideas, bug reports, or platform notes (especially around Bonjour/Avahi weirdness), I’m always &lt;a href="https://github.com/davorg-cpan/app-httpthis/issues" rel="noopener noreferrer"&gt;happy to hear them&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;The post &lt;a href="https://perlhacks.com/2026/01/apphttpthis-the-tiny-web-server-i-keep-reaching-for/" rel="noopener noreferrer"&gt;App::HTTPThis: the tiny web server I keep reaching for&lt;/a&gt; first appeared on &lt;a href="https://perlhacks.com" rel="noopener noreferrer"&gt;Perl Hacks&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>web</category>
      <category>httpthis</category>
      <category>perl</category>
      <category>staticsites</category>
    </item>
  </channel>
</rss>
