<?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: Hamdi LAADHARI</title>
    <description>The latest articles on DEV Community by Hamdi LAADHARI (@hamdi_laadhari).</description>
    <link>https://dev.to/hamdi_laadhari</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%2F1377788%2F319892fa-dc63-4fdd-947d-e00f94681822.png</url>
      <title>DEV Community: Hamdi LAADHARI</title>
      <link>https://dev.to/hamdi_laadhari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hamdi_laadhari"/>
    <language>en</language>
    <item>
      <title>Put Your Docker Images on a Diet with Alpine Linux (2015)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Sat, 22 Aug 2026 09:13:11 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/put-your-docker-images-on-a-diet-with-alpine-linux-2015-jg</link>
      <guid>https://dev.to/hamdi_laadhari/put-your-docker-images-on-a-diet-with-alpine-linux-2015-jg</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on June 21, 2015, back when Alpine was still a dark-horse pick rather than the default. Already in English, lightly cleaned up for dev.to (fixed a few typos). Alpine did go on to become the mainstream slim-base choice — but it's not the automatic right answer anymore either: its musl libc trips up some native Node/Python modules, and DNS resolution has had known issues, which is why multi-stage builds and distroless images are now common alternatives for the same "smaller image" goal. My &lt;a href="https://github.com/killerwolf/Dockerfiles" rel="noopener noreferrer"&gt;Dockerfiles&lt;/a&gt; and &lt;a href="https://hub.docker.com/u/killerwolf/" rel="noopener noreferrer"&gt;Docker Hub images&lt;/a&gt; are still up if you want to see what this looked like in practice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;docker images&lt;/code&gt; and you'll typically see images weighing several hundred MBs — even a simple container providing a single service ends up heavy. Why? Because the base images people reach for — Ubuntu, CentOS, Debian — are themselves very heavy.&lt;/p&gt;

&lt;p&gt;Images based on BusyBox could have been an alternative, but they're too far from the full-featured Linux OSes we're used to, and building from BusyBox isn't straightforward — it often requires compiling from source.&lt;/p&gt;

&lt;p&gt;Let me introduce &lt;a href="https://www.alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine Linux&lt;/a&gt;, a lightweight Linux distribution. Its popularity is growing fast in the Docker community. Once people discover it, they rebase their Dockerfiles onto it and are amazed how simple the switch was. I started using Alpine well before it became official on Docker Hub, building my own PHP/nginx/MariaDB containers instead of relying on the heavy official ones.&lt;/p&gt;

&lt;p&gt;More and more images using Alpine as a base are showing up on Docker Hub. My own builds are public on &lt;a href="https://hub.docker.com/u/killerwolf/" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt; — feel free to use and fork them on &lt;a href="https://github.com/killerwolf/Dockerfiles" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>alpine</category>
      <category>docker</category>
      <category>devops</category>
      <category>retro</category>
    </item>
    <item>
      <title>Supercharging Dev VMs with Vagrant and Ansible (2014)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:31:23 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/supercharging-dev-vms-with-vagrant-and-ansible-2014-4c58</link>
      <guid>https://dev.to/hamdi_laadhari/supercharging-dev-vms-with-vagrant-and-ansible-2014-4c58</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on April 30, 2014. Already in English, lightly cleaned up for dev.to (fixed a few typos and reworded one sentence about Ansible's learning curve that read backwards in the original). Vagrant was the default way to spin up local dev VMs before Docker ate that space — most teams now reach for containers instead, but if you do need full VMs (kernel-level testing, OS-specific work), the Vagrant + Ansible combo below still works exactly like this. My &lt;a href="https://github.com/killerwolf/vagrant-lamp-ansible" rel="noopener noreferrer"&gt;example project&lt;/a&gt; is still up on GitHub.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Vagrant was the de facto standard for managing development machines. It lets you spin up a multi-VM development environment in seconds. For more details, see &lt;a href="http://www.vagrantup.com" rel="noopener noreferrer"&gt;Vagrant Up&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Vagrant isn't a provisioning tool itself — it leverages existing ones, like &lt;a href="http://puppetlabs.com" rel="noopener noreferrer"&gt;Puppet&lt;/a&gt; and &lt;a href="http://www.ansible.com" rel="noopener noreferrer"&gt;Ansible&lt;/a&gt;. Ansible was the new kid on the block at the time. Its learning curve isn't too steep: you just write your playbooks in YAML, and it's DevOps-friendly.&lt;/p&gt;

&lt;p&gt;Give it a try — clone my example project, &lt;a href="https://github.com/killerwolf/vagrant-lamp-ansible" rel="noopener noreferrer"&gt;vagrant-lamp-ansible&lt;/a&gt;, and get started.&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>ansible</category>
      <category>devops</category>
      <category>retro</category>
    </item>
    <item>
      <title>Naming eZ Publish Web Transactions in New Relic (2013)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:29:02 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/naming-ez-publish-web-transactions-in-new-relic-2013-4inc</link>
      <guid>https://dev.to/hamdi_laadhari/naming-ez-publish-web-transactions-in-new-relic-2013-4inc</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on October 5, 2013. Already in English, lightly cleaned up for dev.to. New Relic's PHP agent and API have evolved a lot since, but the underlying idea — naming transactions manually instead of dumping everything into one bucket — is still exactly how you'd solve this today. The &lt;a href="https://github.com/killerwolf/ezpublish-newrelic" rel="noopener noreferrer"&gt;ezpublish-newrelic extension&lt;/a&gt; is still up on GitHub and Packagist if you want to see the actual code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When using the &lt;strong&gt;New Relic PHP agent&lt;/strong&gt; with an eZ Publish stack, every profiled transaction shows up under &lt;code&gt;/index.php&lt;/code&gt;. Not exactly helpful when you're trying to figure out what's actually making your app struggle.&lt;/p&gt;

&lt;p&gt;New Relic provides a self-explanatory &lt;a href="https://docs.newrelic.com/docs/php/the-php-api" rel="noopener noreferrer"&gt;PHP API&lt;/a&gt;. It lets you send additional metrics and data alongside whatever the agent already gathers — including a custom transaction name.&lt;/p&gt;

&lt;p&gt;The eZ Publish extension I wrote, &lt;a href="https://github.com/killerwolf/ezpublish-newrelic" rel="noopener noreferrer"&gt;ezpublish-newrelic&lt;/a&gt;, hooks into that API to track modules and their views under separate, meaningful transaction names instead of one big &lt;code&gt;/index.php&lt;/code&gt; bucket.&lt;/p&gt;

&lt;p&gt;The source is on &lt;a href="https://github.com/killerwolf/ezpublish-newrelic" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and installable via Composer from &lt;a href="https://packagist.org/packages/killerwolf/ezpublish-newrelic" rel="noopener noreferrer"&gt;Packagist&lt;/a&gt; — feel free to contribute.&lt;/p&gt;

</description>
      <category>php</category>
      <category>newrelic</category>
      <category>ezpublish</category>
      <category>retro</category>
    </item>
    <item>
      <title>Uninstalling BlueStacks from Mac OS X, the Undocumented Way (2013)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:20:52 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/uninstalling-bluestacks-from-mac-os-x-the-undocumented-way-2013-2f89</link>
      <guid>https://dev.to/hamdi_laadhari/uninstalling-bluestacks-from-mac-os-x-the-undocumented-way-2013-2f89</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on May 28, 2013. Already in English, lightly cleaned up for dev.to (I quoted the space-containing path, which the original left unquoted — it would have broken in a real shell). BlueStacks has changed a lot since 2013, and macOS's security model has too (notarization, System Integrity Protection), so these exact paths almost certainly won't match a current install. The instinct — dig into &lt;code&gt;~/Library&lt;/code&gt; for the app's own bundled uninstaller — is still the right one if a modern macOS app leaves debris behind.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For some reverse engineering needs, I installed a full-featured Android environment called BlueStacks. I decided to remove it after playing with it for a couple of hours. I searched the web and found no up-to-date way of getting rid of it — BlueStacks is a pretty intrusive app.&lt;/p&gt;

&lt;p&gt;After digging deep into &lt;code&gt;~/Library&lt;/code&gt;, I found out how.&lt;/p&gt;

&lt;h3&gt;
  
  
  Here's how
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd "~/Library/BlueStacks App Player/Uninstall/Clear BlueStacks User Data.app/Contents/MacOS"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;sh uHD-ClearUserData&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hit &lt;strong&gt;Delete&lt;/strong&gt; in the confirmation popup&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You're all set.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>cli</category>
      <category>productivity</category>
      <category>retro</category>
    </item>
    <item>
      <title>Revamping the Blog... Done (2013)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:18:36 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/revamping-the-blog-done-2013-3nfc</link>
      <guid>https://dev.to/hamdi_laadhari/revamping-the-blog-done-2013-3nfc</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on May 27, 2013, right on the heels of the "in progress" post. Already in English, lightly cleaned up for dev.to. NodeJitsu shut down in 2015, so the hosting details here are historical only — the "ditch WordPress for a flat-file Node.js blog" idea still holds up fine today.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm done revamping the blog with &lt;a href="https://github.com/rgrove/lectroid" rel="noopener noreferrer"&gt;Lectroid&lt;/a&gt;. This blog engine offers all the basic features you'd need. It's currently hosted on &lt;a href="https://www.nodejitsu.com" rel="noopener noreferrer"&gt;NodeJitsu&lt;/a&gt; on their cheapest plan. The frontend is fully responsive, built with &lt;a href="http://twitter.github.io/bootstrap/" rel="noopener noreferrer"&gt;Twitter Bootstrap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So — what's your take on the new setup?&lt;/p&gt;

</description>
      <category>node</category>
      <category>git</category>
      <category>github</category>
      <category>retro</category>
    </item>
    <item>
      <title>Revamping the Blog... In Progress (2013)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:15:33 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/revamping-the-blog-in-progress-2013-57i8</link>
      <guid>https://dev.to/hamdi_laadhari/revamping-the-blog-in-progress-2013-57i8</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on April 6, 2013, already in English (this was the post announcing the blog would go English-only going forward). Lightly cleaned up for dev.to. Pagodabox shut down in 2015, and Cloud9 IDE was acquired by AWS and rebuilt into something very different, so neither is usable the way described here — Lectroid, the Node.js blog engine, still exists as historical software.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From now on this blog will be held in English only. Despite not being a native English speaker, I find it easier writing in the language of Shakespeare. I'm writing this post to announce the upcoming blog revamp.&lt;/p&gt;

&lt;p&gt;The blog was running WordPress with a full LAMP stack on my own dedicated server — a total overkill setup. On top of that, WordPress kept getting heavier release after release. I only needed a personal blog system, not a whole CMS engine.&lt;/p&gt;

&lt;p&gt;I went with a lightweight Node.js-based blog engine that does only what I need. It's called &lt;a href="https://github.com/rgrove/lectroid" rel="noopener noreferrer"&gt;Lectroid&lt;/a&gt; — no database required, posts are stored as markdown files.&lt;/p&gt;

&lt;p&gt;I also decided to fully lighten up the dev process and give a PaaS a try. I played with a few of them for a couple of days and settled on &lt;a href="https://pagodabox.com/" rel="noopener noreferrer"&gt;Pagodabox&lt;/a&gt;, which fit my needs. I'll be using &lt;a href="https://c9.io/" rel="noopener noreferrer"&gt;Cloud9 IDE&lt;/a&gt; and Git for versioning and deploying to Pagodabox. I'll keep you all posted.&lt;/p&gt;

</description>
      <category>node</category>
      <category>git</category>
      <category>github</category>
      <category>retro</category>
    </item>
    <item>
      <title>eZ Publish's Native RSS Import: Making It Actually Import &lt;enclosure&gt; Media (2012)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:11:51 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/ez-publishs-native-rss-import-making-it-actually-import-media-2012-2lk5</link>
      <guid>https://dev.to/hamdi_laadhari/ez-publishs-native-rss-import-making-it-actually-import-media-2012-2lk5</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on July 20, 2012. Translated from French, lightly cleaned up for dev.to — I also fixed smart-quote encoding issues throughout the PHP code blocks that would have made them fail to parse. eZ Publish is now Ibexa DXP, but the kernel mechanism described here (and the "quick and dirty vs. extension hook" tradeoff) is unchanged.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;eZ Publish lets you import content from an RSS feed into eZ Publish content objects without writing a single line of code. But that native feature doesn't import the media referenced in the &lt;code&gt;&amp;lt;enclosure&amp;gt;&lt;/code&gt; tag:&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;enclosure&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"http://www.example.com/images/voiture.jpg"&lt;/span&gt; &lt;span class="na"&gt;length=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/jpeg"&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;I'll show you how to fetch that media (image, or any other file like PDF, DOC, or FLV) and insert it transparently into your eZ Publish content. This feature is a bit hardcoded in eZ Publish and isn't cleanly extensible out of the box. I'll show you the &lt;em&gt;quick and dirty&lt;/em&gt; way to get there first, then the cleaner way — you decide how much time it's worth spending.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick and dirty implementation
&lt;/h3&gt;

&lt;p&gt;Here are the core files affected by our changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;setObjectAttributeValue()&lt;/code&gt; function in &lt;code&gt;cronjobs/rssimport.php&lt;/code&gt; (the PHP script called by cron to fetch new items from the RSS feeds configured in the back office)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;rssFieldDefinition()&lt;/code&gt; method in &lt;code&gt;kernel/classes/ezrssimport.php&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;setObjectAttributeValue()&lt;/code&gt; handles fetching the different attribute types (text line, XML block), so that's naturally where we add handling for the &lt;code&gt;ezimage&lt;/code&gt; attribute (this also works with &lt;code&gt;ezfile&lt;/code&gt;). We add a &lt;code&gt;case 'ezimage':&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setObjectAttributeValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$objectAttribute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$dataType&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//...&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'ezimage'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;pathinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eZHTTPTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getDataByURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$value&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="nv"&gt;$image&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/tmp/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'basename'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'wb'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nb"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="nb"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$objectAttribute&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'basename'&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once eZ Publish knows how to handle &lt;code&gt;ezimage&lt;/code&gt;, we need to add &lt;code&gt;Item – Enclosure – Url&lt;/code&gt; to the mapping &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;. That's the &lt;code&gt;rssFieldDefinition()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'2.0'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'0.91'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'0.92'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'item'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'elements'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'link'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'author'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'comments'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'guid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'pubDate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                      &lt;span class="s1"&gt;'enclosure'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'attributes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'url'&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="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'channel'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'elements'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'link'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'copyright'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'managingEditor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'webMaster'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'pubDate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'lastBuildDate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'generator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'docs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'cloud'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                         &lt;span class="s1"&gt;'ttl'&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cleaner implementation
&lt;/h3&gt;

&lt;p&gt;Instead of editing the &lt;code&gt;rssimport.php&lt;/code&gt; cron script directly, you can copy it into your own extension, make the &lt;code&gt;ezimage&lt;/code&gt; changes there, and have that run instead of eZ Publish's default &lt;code&gt;rssimport.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rssFieldDefinition()&lt;/code&gt; is called from another method in the same class, and there's a hook there that lets you extend the definition from classes living in your custom extensions. Here's the hook, inside &lt;code&gt;fieldMap()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$fieldDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eZRSSImport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rssFieldDefinition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$ini&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eZINI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ini&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;variable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'RSSSettings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ActiveExtensions'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$activeExtension&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="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;eZExtension&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;baseDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$activeExtension&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/rss/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$activeExtension&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'rssimport.php'&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;include_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;eZExtension&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;baseDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$activeExtension&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/rss/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$activeExtension&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'rssimport.php'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$fieldDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eZRSSImport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;arrayMergeRecursive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$fieldDefinition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;call_user_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$activeExtension&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'rssimport'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rssFieldDefinition'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;array&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="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;So all we need is a class in &lt;code&gt;extension/myextension/rss/ezrssimage.php&lt;/code&gt; with a method of the same name, &lt;code&gt;rssFieldDefinition()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ezrssimagerssimport&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rssFieldDefinition&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="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'item'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'elements'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'enclosure'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'attributes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'url'&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="p"&gt;)&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;And declare that class in &lt;code&gt;myextension/settings/site.ini.append.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[RSSSettings]&lt;/span&gt;
&lt;span class="err"&gt;ActiveExtensions&lt;/span&gt;&lt;span class="nn"&gt;[]&lt;/span&gt;&lt;span class="err"&gt;=ezrssimage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for this one. If I ever find the time I'll package this as a proper extension and publish it on GitHub (unless someone beats me to it).&lt;/p&gt;

&lt;p&gt;Let me know if you spot errors or improvements. The PHP snippets above are also up on &lt;a href="https://gist.github.com/killerwolf/5430167" rel="noopener noreferrer"&gt;this gist&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>ezpublish</category>
      <category>webdev</category>
      <category>retro</category>
    </item>
    <item>
      <title>Custom ImageMagick Filters for eZ Publish Thumbnails (2011)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:08:12 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/custom-imagemagick-filters-for-ez-publish-thumbnails-2011-4pim</link>
      <guid>https://dev.to/hamdi_laadhari/custom-imagemagick-filters-for-ez-publish-thumbnails-2011-4pim</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on July 13, 2011. Translated from French, lightly cleaned up for dev.to. eZ Publish has since been rebranded/rebuilt as Ibexa DXP, but the &lt;code&gt;image.ini&lt;/code&gt; filter mechanism and the underlying ImageMagick flags below are unchanged.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;eZ Publish's default filters don't let you crop images down to precise dimensions. The &lt;code&gt;image.ini&lt;/code&gt; config file lets you hook into eZ Publish anything the ImageMagick CLI tools can do.&lt;/p&gt;

&lt;p&gt;Here are two filters that do the same job — crop and resize an image to an exact size — in two different ways. Use whichever fits your image aliases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;Filters&lt;/span&gt;&lt;span class="nn"&gt;[]&lt;/span&gt;&lt;span class="err"&gt;=geometry/&lt;/span&gt;&lt;span class="py"&gt;thumbnailize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-thumbnail "%1x%2^" -gravity center -extent %1x%2&lt;/span&gt;
&lt;span class="err"&gt;Filters&lt;/span&gt;&lt;span class="nn"&gt;[]&lt;/span&gt;&lt;span class="err"&gt;=geometry/&lt;/span&gt;&lt;span class="py"&gt;thumbnailize2&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-geometry %1x%2^ -gravity center -crop %1x%2+0+0 +repage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>php</category>
      <category>imagemagick</category>
      <category>webdev</category>
      <category>retro</category>
    </item>
    <item>
      <title>My First UWA Widget for Netvibes and iGoogle (2009)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:04:16 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/my-first-uwa-widget-for-netvibes-and-igoogle-2009-2mln</link>
      <guid>https://dev.to/hamdi_laadhari/my-first-uwa-widget-for-netvibes-and-igoogle-2009-2mln</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on January 18, 2009. Lightly cleaned up for dev.to (translated from French; I also dropped my personal TVRage feed hash from the example URL rather than repost a token — TVRage itself shut down years ago anyway). Netvibes and iGoogle are both long gone as platforms and the UWA widget format died with them, but the "aggregate a messy feed into something clean" approach still holds up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Originally this project was meant for the same rabbit from &lt;a href="https://dev.to/hamdi_laadhari/hacking-my-nabaztag-reading-msn-messages-out-loud-with-a-talking-rabbit-2008-4ifj"&gt;my Nabaztag hack&lt;/a&gt;, but I ended up wanting to play with Netvibes' UWA widget framework instead. This widget shows the air times for the TV shows I watch regularly.&lt;/p&gt;

&lt;p&gt;I'm a sucker for mashups, so instead of manually listing out every show, I used a service from TVRage. Once you created a TVRage account and added your favorite shows to it, TVRage gave you a personalized RSS feed of everything airing during the current week.&lt;/p&gt;

&lt;p&gt;The feed was pretty messy — hard to work with, the items were inconsistent in structure — so aggregating it into the shape I wanted took more effort than expected.&lt;/p&gt;

&lt;p&gt;Wanting to get the widget working as fast as possible, I wrote a small PHP5 API that aggregates the feed and sorts the data properly. You could also use the UWA/JS parser that Netvibes provided directly, and that's honestly the better approach — as it stood, the widget depended on an API I'd written and hosted on one of my own sites. But it didn't matter much: I wasn't planning to distribute the widget, so the PHP solution worked fine for my own use.&lt;/p&gt;

&lt;p&gt;The widget takes three parameters: the &lt;code&gt;tid&lt;/code&gt; and &lt;code&gt;hash&lt;/code&gt; you'd find in your own personalized feed URL, and a third one pointing at my API.&lt;/p&gt;

&lt;p&gt;Nothing about this stack survives today — Netvibes, iGoogle, TVRage, and UWA are all gone — but "aggregate a messy feed into a clean personal widget" is a shape I still reach for.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>api</category>
      <category>retro</category>
    </item>
    <item>
      <title>GreaseMonkey, or How to Enhance Your Web Experience (2008)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:00:58 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/greasemonkey-or-how-to-enhance-your-web-experience-2008-2003</link>
      <guid>https://dev.to/hamdi_laadhari/greasemonkey-or-how-to-enhance-your-web-experience-2008-2003</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on December 21, 2008. Lightly cleaned up for dev.to (translated from French, and I fixed a smart-quote encoding issue in the code block that would have made it fail). GreaseMonkey itself has since been discontinued for Firefox — Tampermonkey and Violentmonkey are the modern equivalents — and Google's markup has changed completely since 2008, so the exact selectors below won't match today's page. The userscript technique itself still works exactly the same way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://addons.mozilla.org/fr/firefox/addon/748" rel="noopener noreferrer"&gt;GreaseMonkey&lt;/a&gt; is a Firefox extension that lets you take control of the web. You find a site interesting despite it being plastered with ads. Thanks to GreaseMonkey and a script that's just a few lines long, you can change the visual rendering of your favorite site however you want.&lt;/p&gt;

&lt;p&gt;To illustrate, here's a small JS script I wrote — it removes the "Sponsored Links" that show up in Google search results. Knowing JavaScript is obviously a requirement, but so is knowing the DOM and/or XPath.&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="c1"&gt;// ==UserScript==&lt;/span&gt;
&lt;span class="c1"&gt;// @name           Google Sponsored Links remover&lt;/span&gt;
&lt;span class="c1"&gt;// @namespace      http://www.laadhari.fr&lt;/span&gt;
&lt;span class="c1"&gt;// @description    remove google sponsored links from SERPS&lt;/span&gt;
&lt;span class="c1"&gt;// @include        http://www.google.com/*&lt;/span&gt;
&lt;span class="c1"&gt;// ==/UserScript==&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;sidebarads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mbEnd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;skyads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tads&lt;/span&gt;&lt;span class="dl"&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;sidebarads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sidebarads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sidebarads&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;skyads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;skyads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;skyads&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="p"&gt;)();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hopefully this convinces you to write your own scripts — if you do, consider sharing them on &lt;a href="http://userscripts.org/" rel="noopener noreferrer"&gt;UserScripts.org&lt;/a&gt; in case they're useful to someone else. My &lt;a href="http://userscripts.org/scripts/show/38984" rel="noopener noreferrer"&gt;Google Sponsored Links remover&lt;/a&gt; is up there, open to any criticism.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>firefox</category>
      <category>webdev</category>
      <category>retro</category>
    </item>
    <item>
      <title>Hacking My Nabaztag: Reading MSN Messages Out Loud With a Talking Rabbit (2008)</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 21:57:22 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/hacking-my-nabaztag-reading-msn-messages-out-loud-with-a-talking-rabbit-2008-4ifj</link>
      <guid>https://dev.to/hamdi_laadhari/hacking-my-nabaztag-reading-msn-messages-out-loud-with-a-talking-rabbit-2008-4ifj</guid>
      <description>&lt;p&gt;&lt;em&gt;Archival repost — originally published on my old blog on December 12, 2008. Lightly cleaned up for dev.to; most of the tech mentioned here (Nabaztag, MSN Messenger, Messenger Plus! Live) has long since been discontinued, but I'm keeping the content as-is for the history.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A week ago I got my hands on a &lt;strong&gt;Nabaztag:tag&lt;/strong&gt; — one of the first "smart" connected objects, years before "smart speaker" was even a category. It was a Wi-Fi rabbit made by a French company called Violet: it moved its ears, lit up in different colors, and had a text-to-speech voice.&lt;/p&gt;

&lt;p&gt;Violet shipped an API with it, so you could script your own rabbit and make it do all sorts of things. My idea: have the rabbit read out messages people sent me on MSN Messenger while I was away from my keyboard ("AFK" for the non-geeks), so I could still hear what my friends were saying without being in front of the screen.&lt;/p&gt;

&lt;p&gt;The rabbit has a text-to-speech feature built in — send it some text through the API and it reads it back as soon as it receives it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring MSN events to the API
&lt;/h2&gt;

&lt;p&gt;The last piece was getting MSN to trigger the API based on its own internal events. That's where &lt;strong&gt;Messenger Plus! Live&lt;/strong&gt; came in. Everyone knew this MSN add-on for its UI tweaks, but fewer people knew it also shipped a scripting environment: you could write your own commands using the JavaScript engine embedded in the add-on. On top of the basic MSN events, Messenger Plus! Live exposed a whole set of extra event hooks, so all I had to do was implement the event handlers I cared about in my script.&lt;/p&gt;

&lt;p&gt;Here's a short video of my first tests with &lt;strong&gt;NabKiller&lt;/strong&gt; — that's the name I gave my rabbit:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4BKnJzcEGio"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Ideas I never got around to
&lt;/h2&gt;

&lt;p&gt;A few things I planned to build on top of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have NabKiller warn me as soon as a new episode of a show I was watching came out, and tell me whether subtitles were already available — or ping me the moment they were.&lt;/li&gt;
&lt;li&gt;Give the rabbit some actual intelligence by hooking it up to a Pandorabot. &lt;a href="http://sycophante.vhost.pandorabots.com/pandora/talk-oddcast?botid=a77514392e359d3d" rel="noopener noreferrer"&gt;Voldo&lt;/a&gt; was an example bot with that kind of AI at the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I like keeping this one around if only as a reminder that "smart home hack with a scripting API and a talking rabbit" predates the current wave of IoT and AI toys by over a decade.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>iot</category>
      <category>retro</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Node.js SEA Just Got Way Simpler — Updating My node-sea Boilerplate for Node 26</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:38:05 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/nodejs-sea-just-got-way-simpler-updating-my-node-sea-boilerplate-for-node-26-1efl</link>
      <guid>https://dev.to/hamdi_laadhari/nodejs-sea-just-got-way-simpler-updating-my-node-sea-boilerplate-for-node-26-1efl</guid>
      <description>&lt;p&gt;A while back I put together &lt;a href="https://github.com/killerwolf/node-sea" rel="noopener noreferrer"&gt;node-sea&lt;/a&gt;, a small boilerplate showing how to package a TypeScript app into a Single Executable Application (SEA) with Node.js — no runtime install required on the target machine, just one binary.&lt;/p&gt;

&lt;p&gt;I hadn't touched it in a while, so I went back to check if it still held up. Short answer: the &lt;em&gt;concept&lt;/em&gt; is more relevant than ever, but the &lt;em&gt;build process&lt;/em&gt; I wrote was already legacy. Node core absorbed most of it. Here's what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick recap: what's a Node.js SEA
&lt;/h2&gt;

&lt;p&gt;SEA lets you compile a Node app into a single native binary that runs without Node, npm, or &lt;code&gt;node_modules&lt;/code&gt; anywhere in sight. Useful for CLIs you want to hand to someone who doesn't have Node installed, or just for shipping one file instead of a runtime + a tree of dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old way
&lt;/h2&gt;

&lt;p&gt;My original &lt;code&gt;build-sea.js&lt;/code&gt; did this, by hand, in five steps:&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="c1"&gt;// 1. Generate the blob&lt;/span&gt;
&lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node --experimental-sea-config sea-config.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Copy the node binary itself&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;command -v node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Strip its existing signature (macOS)&lt;/span&gt;
&lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codesign --remove-signature dist/hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Inject the blob with an external tool&lt;/span&gt;
&lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npx --yes postject dist/hello NODE_SEA_BLOB dist/sea/sea-prep.blob &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;
  --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;
  --macho-segment-name NODE_SEA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 5. Re-sign it&lt;/span&gt;
&lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codesign --sign - dist/hello&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;That &lt;code&gt;postject&lt;/code&gt; step was the annoying one — an external WASM-based tool you had to &lt;code&gt;npx&lt;/code&gt; in, with a sentinel fuse string you copy-pasted from the docs and just trusted. It worked, but it wasn't exactly pleasant, and the Node.js team has since said openly that postject had become unmaintained with issues piling up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed: &lt;code&gt;--build-sea&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Node 25.5 (January 2026) folded all of that directly into Node core, behind one flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--build-sea&lt;/span&gt; sea-config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Blob generation, binary copy, and injection now happen internally — Node vendors the same binary-patching logic postject used to provide (via the LIEF library) instead of shelling out to an npm package. My entire &lt;code&gt;build-sea.js&lt;/code&gt; shrank from five manual steps to one command plus the platform-specific codesign call:&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="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node --build-sea sea-config.json&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="na"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inherit&lt;/span&gt;&lt;span class="dl"&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;darwin&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="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;codesign --sign - dist/hello&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;&lt;code&gt;sea-config.json&lt;/code&gt; changed slightly too — &lt;code&gt;output&lt;/code&gt; now points straight at the final executable instead of an intermediate &lt;code&gt;.blob&lt;/code&gt; file:&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;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/bundled/bundle.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"disableExperimentalSEAWarning"&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;"useCodeCache"&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="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;That &lt;code&gt;useCodeCache&lt;/code&gt; flag is new as well — it pre-compiles the bundle to V8 bytecode at build time, so the binary starts faster. One boolean, free win, no downside for a CommonJS bundle like this one (it does disable dynamic &lt;code&gt;import()&lt;/code&gt;, worth knowing if your app uses it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Before / after
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Old way&lt;/th&gt;
&lt;th&gt;&lt;code&gt;--build-sea&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Steps&lt;/td&gt;
&lt;td&gt;5 manual steps&lt;/td&gt;
&lt;td&gt;1 command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External deps&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;postject&lt;/code&gt; via &lt;code&gt;npx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blob handling&lt;/td&gt;
&lt;td&gt;you generate + inject it yourself&lt;/td&gt;
&lt;td&gt;handled internally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentinel fuse&lt;/td&gt;
&lt;td&gt;copy-pasted magic string&lt;/td&gt;
&lt;td&gt;not your problem anymore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codesigning&lt;/td&gt;
&lt;td&gt;still manual (macOS/Windows)&lt;/td&gt;
&lt;td&gt;still manual (macOS/Windows)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The catch: it's not on LTS yet
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised me. I assumed &lt;code&gt;--build-sea&lt;/code&gt; had landed on the current LTS by now, so I installed Node 24 "Krypton" (Active LTS as of today) to test — and the flag simply isn't there:&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;node &lt;span class="nt"&gt;--build-sea&lt;/span&gt; sea-config.json
&lt;span class="go"&gt;node: bad option: --build-sea
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;--experimental-sea-config&lt;/code&gt;, the old blob-generation flag, is available on 24.x. &lt;code&gt;--build-sea&lt;/code&gt; currently only exists on Node 26 (the Current release line). The silver lining: Node 26 is an even-numbered release, which means it graduates to Active LTS in October 2026 — a few weeks out at the time of writing. Still, if you're targeting LTS &lt;em&gt;today&lt;/em&gt;, you're stuck with the manual flow for a little longer.&lt;/p&gt;

&lt;p&gt;It's also worth noting SEA is still officially listed as &lt;strong&gt;Experimental (stability 1.1 – "Active development")&lt;/strong&gt; in the Node.js docs, not Stable, despite what a few blog posts floating around now claim. It's functional and usable, just not API-frozen yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;I bumped &lt;a href="https://github.com/killerwolf/node-sea" rel="noopener noreferrer"&gt;node-sea&lt;/a&gt; to Node 26, rewrote the build script around &lt;code&gt;--build-sea&lt;/code&gt;, and refreshed the dependencies (TypeScript, esbuild, commander) while I was at it — kept TypeScript on 5.9 rather than jumping to the new 7.0 Go-native compiler, since &lt;code&gt;ts-node&lt;/code&gt; hasn't caught up to its programmatic API yet.&lt;/p&gt;

&lt;p&gt;If you're maintaining anything that shells out to &lt;code&gt;postject&lt;/code&gt;, this is your sign to simplify it. Repo's here if you want to see the whole thing end to end: &lt;a href="https://github.com/killerwolf/node-sea" rel="noopener noreferrer"&gt;github.com/killerwolf/node-sea&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/api/single-executable-applications.html" rel="noopener noreferrer"&gt;Node.js Single Executable Applications docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://joyeecheung.github.io/blog/2026/01/26/improving-single-executable-application-building-for-node-js/" rel="noopener noreferrer"&gt;Improving Single Executable Application Building for Node.js — Joyee Cheung&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
