<?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>Deprecating My Symfony Profiler MCP Bundle: Right About the Problem, Half-Wrong About the Solution</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Wed, 16 Sep 2026 21:02:13 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/deprecating-my-symfony-profiler-mcp-bundle-right-about-the-problem-half-wrong-about-the-solution-567l</link>
      <guid>https://dev.to/hamdi_laadhari/deprecating-my-symfony-profiler-mcp-bundle-right-about-the-problem-half-wrong-about-the-solution-567l</guid>
      <description>&lt;p&gt;In March 2025 I built &lt;a href="https://github.com/killerwolf/mcp-profiler-bundle" rel="noopener noreferrer"&gt;mcp-profiler-bundle&lt;/a&gt;, a Symfony bundle that let AI coding agents read the Symfony Profiler: the requests your app just served, the queries they ran, the exceptions they threw. The bet was simple. An agent that can see what the app actually did should debug it better than one that can only read the source.&lt;/p&gt;

&lt;p&gt;This week I deprecated it. Symfony now ships the official version of that idea, and rereading my code for the deprecation notice left me with an uncomfortable verdict: I was right about the problem, and half-wrong about the solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bundle did
&lt;/h2&gt;

&lt;p&gt;MCP, the Model Context Protocol, is a JSON-RPC protocol that lets a client like Claude Desktop, Cursor or Cline call tools on a local server. My bundle added a &lt;code&gt;bin/console mcp:server:run&lt;/code&gt; command that spoke it over stdio and exposed four tools: &lt;code&gt;profiler:list&lt;/code&gt;, &lt;code&gt;profiler:get_by_token&lt;/code&gt;, &lt;code&gt;profiler:get_collectors&lt;/code&gt; and &lt;code&gt;profiler:get_collector&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There was no official PHP SDK back then. My first commit leaned on a community library, &lt;code&gt;james2037/mcp-php-server&lt;/code&gt;, which only existed as &lt;code&gt;dev-master&lt;/code&gt;. By the 0.1.0 tag on 31 March 2025 it was gone, and the whole protocol fit in one &lt;code&gt;match&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="c1"&gt;// Command/RunMCPServerCommand.php&lt;/span&gt;
&lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_THROW_ON_ERROR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'method'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'initialize'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendInitialize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'tools/list'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendToolsList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'tools/call'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'params'&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="s1"&gt;'notifications/initialized'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendProtocolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;\sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Method "%s" not found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$method&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;Hold on to that first line. It comes back later.&lt;/p&gt;

&lt;p&gt;It found some users: 11 stars, 2 forks and roughly 5,900 Packagist installs. Version 0.2.0 added multi-kernel (&lt;code&gt;APP_ID&lt;/code&gt;) support a week after launch, and in February 2026 an outside contributor, Rhodri Pugh, sent a PHP 8.4 fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right about the problem
&lt;/h2&gt;

&lt;p&gt;The idea is official now. In September 2025, Symfony announced the official PHP MCP SDK, built with The PHP Foundation and Anthropic's MCP team. On 23 December 2025, &lt;a href="https://symfony.com/doc/current/ai/components/mate.html" rel="noopener noreferrer"&gt;Symfony AI Mate&lt;/a&gt; shipped its first release, about nine months after my 0.1.0: an MCP dev server giving coding agents access to a Symfony app's internals. The container came first, profiler access landed in 0.3 in January 2026, and it handles multi-kernel apps too.&lt;/p&gt;

&lt;p&gt;Symfony's &lt;a href="https://symfony.com/blog/symfony-ai-spotlight-mate-an-agent-s-way-into-your-runtime" rel="noopener noreferrer"&gt;spotlight on Mate&lt;/a&gt; opens on the same premise: an agent can read all your code and still not know what happened on the last request. Mate is past 100,000 installs a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Half-wrong about the solution
&lt;/h2&gt;

&lt;p&gt;In August 2026, Mate 0.13 deleted its MCP server (&lt;a href="https://github.com/symfony/ai/pull/2380" rel="noopener noreferrer"&gt;PR #2380&lt;/a&gt;). It no longer depends on &lt;code&gt;mcp/sdk&lt;/code&gt;, and the agent runs plain commands instead, like these from Mate's docs and its profiler skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vendor/bin/mate tools:list
vendor/bin/mate tools:call symfony-profiler-list &lt;span class="nt"&gt;--statusCode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 &lt;span class="nt"&gt;--limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
vendor/bin/mate resources:read symfony-profiler://profile/&amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare CLI has a problem an MCP server doesn't: nothing tells the agent it exists. Mate's answer is Agent Skills — &lt;code&gt;SKILL.md&lt;/code&gt; files installed into &lt;code&gt;.agents/skills/&lt;/code&gt; (mirrored into &lt;code&gt;.claude/skills/&lt;/code&gt;) that say when to reach for which tool. The profiler skill reads like a triage procedure: filter for the failing request instead of scrolling, check which collectors it has, then read them in symptom order. &lt;code&gt;exception&lt;/code&gt; first for a 5xx; &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;, then &lt;code&gt;memory&lt;/code&gt; for a slow request.&lt;/p&gt;

&lt;p&gt;Johannes Wachter, who made that change, then &lt;a href="https://johanneswachter.dev/blog/mate-lab/" rel="noopener noreferrer"&gt;measured it&lt;/a&gt;: Claude Haiku, two tasks with injected performance bugs, ten runs per setup, counting whether the agent touched Mate at all. The bare CLI: 0 runs out of 20. The MCP server: 5 out of 20. The CLI with Skills and project instructions: 10 out of 10 on one task, 5 out of 10 on the harder one. He calls it a pattern from a small sample, not a proven effect, and concludes that "transport alone wasn't the deciding variable. Visibility was."&lt;/p&gt;

&lt;p&gt;That's the half I got wrong. I treated exposing the data as the product. Tools give the agent eyes; skills tell it where to look. My bundle handed over four tools with one-line descriptions ("Lists available profiler tokens.") and left the agent to work out the rest.&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;
&lt;code&gt;mcp-profiler-bundle&lt;/code&gt; (2025)&lt;/th&gt;
&lt;th&gt;Symfony AI Mate 0.13 (2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How the agent calls it&lt;/td&gt;
&lt;td&gt;MCP over stdio, hand-written JSON-RPC&lt;/td&gt;
&lt;td&gt;plain CLI, no server process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs the app to boot&lt;/td&gt;
&lt;td&gt;yes, it's a &lt;code&gt;bin/console&lt;/code&gt; command&lt;/td&gt;
&lt;td&gt;no, reads the profiler and the dumped container from disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finding a profile&lt;/td&gt;
&lt;td&gt;the last N (&lt;code&gt;limit&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;method, URL, IP, status, date range, kernel context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collector output&lt;/td&gt;
&lt;td&gt;raw collector data or VarDumper dumps&lt;/td&gt;
&lt;td&gt;formatters for Doctrine, exceptions, logs, mail, memory, requests, timing and translations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;no redaction&lt;/td&gt;
&lt;td&gt;cookies, session data, auth headers and secret env vars redacted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt injection&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;output wrapped and marked as untrusted data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telling the agent what to do&lt;/td&gt;
&lt;td&gt;four one-line tool descriptions&lt;/td&gt;
&lt;td&gt;Agent Skills plus &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beyond the profiler&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;service container, Monolog logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The catch: my own bundle
&lt;/h2&gt;

&lt;p&gt;Rereading the code was humbling. PHPStan flags the first two of these problems in CI, on a step I'd marked &lt;code&gt;continue-on-error: true&lt;/code&gt; the day I set up CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ------ -----------------------------------------------------------------------
  Line   Command/RunMCPServerCommand.php
 ------ -----------------------------------------------------------------------
  40     While loop condition is always true.
         🪪  while.alwaysTrue
  61     Unreachable statement - code above always terminates.
         🪪  deadCode.unreachable
  68     Constant JSON_THROW_ON_ERROR is not allowed for parameter #3 $depth
         of function json_decode.
         🪪  argument.invalidConstant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It never exits.&lt;/strong&gt; The MCP spec expects a stdio server to quit when the client closes its input. Mine can't: on EOF, &lt;code&gt;fgets()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;, and the loop sleeps a millisecond and tries again, forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It gets JSON-RPC wrong.&lt;/strong&gt; &lt;code&gt;JSON_THROW_ON_ERROR&lt;/code&gt; sits in &lt;code&gt;json_decode()&lt;/code&gt;'s &lt;code&gt;$depth&lt;/code&gt; parameter, not &lt;code&gt;$flags&lt;/code&gt;, so bad input decodes to &lt;code&gt;null&lt;/code&gt;. Instead of a &lt;code&gt;-32700&lt;/code&gt; parse error with a &lt;code&gt;null&lt;/code&gt; id, the client gets &lt;code&gt;-32601 Method "" not found&lt;/code&gt; with id &lt;code&gt;0&lt;/code&gt;. And any other notification, like &lt;code&gt;notifications/cancelled&lt;/code&gt;, gets an error reply, which JSON-RPC 2.0 forbids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool names don't travel.&lt;/strong&gt; They all contain a colon, which MCP's &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/server/tools" rel="noopener noreferrer"&gt;2025-11-25 spec&lt;/a&gt; says to avoid. The Cursor screenshot in my own README shows them as &lt;code&gt;profilerlist&lt;/code&gt; and &lt;code&gt;profilerget_by_token&lt;/code&gt;, and the README documented different names altogether.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The filters got lost.&lt;/strong&gt; My first version exposed &lt;code&gt;ip&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt; and &lt;code&gt;status_code&lt;/code&gt;. The rewrite kept them in &lt;code&gt;ProfilerList::execute()&lt;/code&gt; but put only &lt;code&gt;limit&lt;/code&gt; in the tool schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing was redacted.&lt;/strong&gt; The request collector holds cookies, headers and the server bag, and since Symfony's Dotenv copies &lt;code&gt;.env&lt;/code&gt; values into &lt;code&gt;$_SERVER&lt;/code&gt;, that includes &lt;code&gt;APP_SECRET&lt;/code&gt; and &lt;code&gt;DATABASE_URL&lt;/code&gt;. &lt;code&gt;profiler:get_collector&lt;/code&gt; dumped whatever the agent asked for, unfiltered, and the optional &lt;code&gt;bin/run-mcp.sh&lt;/code&gt; wrapper (adapted from &lt;a href="https://jolicode.com/blog/mcp-the-open-protocol-that-turns-llm-chatbots-into-intelligent-agents" rel="noopener noreferrer"&gt;JoliCode's MCP article&lt;/a&gt;) appended every response to &lt;code&gt;var/mcp/stdout.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It needed a booting app.&lt;/strong&gt; If the container didn't compile, &lt;code&gt;bin/console&lt;/code&gt; didn't start, and neither did the server. Mate reads everything from disk, so it still answers when the app won't boot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The test suite is one test.&lt;/strong&gt; It checks that the bundle class is a bundle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protocol has moved on, too. MCP's &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" rel="noopener noreferrer"&gt;2026-07-28 revision&lt;/a&gt; removed the &lt;code&gt;initialize&lt;/code&gt; handshake my &lt;code&gt;match&lt;/code&gt; is built around and made a new &lt;code&gt;server/discover&lt;/code&gt; method mandatory.&lt;/p&gt;

&lt;p&gt;None of this is a verdict against MCP, by the way. &lt;code&gt;symfony/mcp-bundle&lt;/code&gt;, for building your own MCP servers, does over 600,000 installs a month. The shift is narrower: for a dev tool the agent runs on your machine, a CLI plus skills got picked up at least as reliably, without a server process to babysit.&lt;/p&gt;

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

&lt;p&gt;So I retired it properly. &lt;a href="https://github.com/killerwolf/mcp-profiler-bundle/pull/14" rel="noopener noreferrer"&gt;PR #14&lt;/a&gt; puts a deprecation notice at the top of the README, with the migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; symfony/ai-mate
vendor/bin/mate init
composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; symfony/ai-symfony-mate-extension symfony/ai-monolog-mate-extension
vendor/bin/mate discover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mate discover&lt;/code&gt; also installs the skills. For frontend work, Simon André's &lt;a href="https://github.com/smnandre/symfony-ux-skills" rel="noopener noreferrer"&gt;Symfony UX skills&lt;/a&gt; cover Stimulus, Turbo, Twig Components and Live Components.&lt;/p&gt;

&lt;p&gt;Even deprecating it took a fix. CI had been red since June, failing on the PHP CS Fixer step, and the log on the deprecation PR showed the culprit: with no config file, PHP CS Fixer 3.95 opens an interactive &lt;code&gt;init&lt;/code&gt; wizard, which crashes in CI with &lt;code&gt;Failed to read template file.&lt;/code&gt; A &lt;code&gt;.php-cs-fixer.dist.php&lt;/code&gt; turned it green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/killerwolf/mcp-profiler-bundle/releases/tag/0.2.1" rel="noopener noreferrer"&gt;0.2.1&lt;/a&gt; is the final release, Rhodri's fix included, and the repository stays public. The lesson I'm keeping: access was the easy half. The half that makes it useful is deciding what the agent should look at, in what order, and what it must never see.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://symfony.com/doc/current/ai/components/mate.html" rel="noopener noreferrer"&gt;Symfony AI Mate documentation&lt;/a&gt; and &lt;a href="https://ai.symfony.com/" rel="noopener noreferrer"&gt;Symfony AI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://symfony.com/blog/symfony-ai-spotlight-mate-an-agent-s-way-into-your-runtime" rel="noopener noreferrer"&gt;Symfony AI Spotlight: Mate, an Agent's Way into Your Runtime&lt;/a&gt; — Symfony blog&lt;/li&gt;
&lt;li&gt;Mate repositories: &lt;a href="https://github.com/symfony/ai-mate" rel="noopener noreferrer"&gt;symfony/ai-mate&lt;/a&gt; (&lt;a href="https://github.com/symfony/ai-mate/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;), &lt;a href="https://github.com/symfony/ai-symfony-mate-extension" rel="noopener noreferrer"&gt;symfony/ai-symfony-mate-extension&lt;/a&gt;, &lt;a href="https://github.com/symfony/ai-monolog-mate-extension" rel="noopener noreferrer"&gt;symfony/ai-monolog-mate-extension&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://johanneswachter.dev/blog/mate-lab/" rel="noopener noreferrer"&gt;The Mate Lab: does an agent actually use a dev tool?&lt;/a&gt; — Johannes Wachter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://symfony.com/blog/introducing-ai-skills-for-symfony-ux" rel="noopener noreferrer"&gt;Introducing AI Skills for Symfony UX&lt;/a&gt; — Symfony blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/killerwolf/mcp-profiler-bundle" rel="noopener noreferrer"&gt;killerwolf/mcp-profiler-bundle&lt;/a&gt; and its &lt;a href="https://github.com/killerwolf/mcp-profiler-bundle/releases/tag/0.2.1" rel="noopener noreferrer"&gt;final release&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From 289 MB to 15 MB: What Migrating from Electron to Tauri Actually Cost</title>
      <dc:creator>Hamdi LAADHARI</dc:creator>
      <pubDate>Fri, 11 Sep 2026 09:48:39 +0000</pubDate>
      <link>https://dev.to/hamdi_laadhari/from-289-mb-to-15-mb-what-migrating-from-electron-to-tauri-actually-cost-n46</link>
      <guid>https://dev.to/hamdi_laadhari/from-289-mb-to-15-mb-what-migrating-from-electron-to-tauri-actually-cost-n46</guid>
      <description>&lt;p&gt;WatchMe is a small macOS menu-bar app I built: it lists your running processes, you tick the ones you care about, and it notifies you the moment one of them exits. Start a long build, go do something else, get told when it's done. That's the whole product.&lt;/p&gt;

&lt;p&gt;It was an Electron app for about two years. Last week I moved it to Tauri 2, and the number that started the whole thing was this one:&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;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; dist/mac-arm64/WatchMe.app
289M    dist/mac-arm64/WatchMe.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;289 MB. To list processes and show a notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;I want to be precise about the motivation, because "Electron is bloated" is a lazy reason to rewrite something that works.&lt;/p&gt;

&lt;p&gt;The app does three things that touch the OS: enumerate processes, own a tray icon, and post a notification. Everything else is a table and two checkboxes. Electron was shipping an entire Chromium build so that I could render that table — and on macOS, there is already a perfectly good webview sitting in the OS that every other app uses for free.&lt;/p&gt;

&lt;p&gt;That's the actual Tauri pitch, and it's narrower than the marketing suggests. Tauri doesn't make your app fast. It makes your app &lt;em&gt;small&lt;/em&gt;, by not shipping a browser. For something that lives in the menu bar and runs all day, small was the thing I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed in the code
&lt;/h2&gt;

&lt;p&gt;The Electron version had a three-part IPC dance. A JSON file of channel names, a preload script bridging them, and handlers in the main process:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ipc-channels.json&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;"GET_PROCESSES"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get-processes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"GET_PREFERENCES"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get-preferences"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SAVE_PREFERENCES"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"save-preferences"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"UPDATE_TRAY_TOOLTIP"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update-tray-tooltip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"QUIT_APP"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quit-app"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// preload.js&lt;/span&gt;
&lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exposeInMainWorld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electronAPI&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;getProcesses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IPC_CHANNELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GET_PROCESSES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...four more of these&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IPC_CHANNELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GET_PROCESSES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;processes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;psList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;processes&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;In Tauri, &lt;code&gt;ipc-channels.json&lt;/code&gt; and &lt;code&gt;preload.js&lt;/code&gt; are both gone. The channel name is the function name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_processes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="nf"&gt;.refresh_all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;
        &lt;span class="nf"&gt;.processes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.iter&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="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="n"&gt;ProcessInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="nf"&gt;.as_u32&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_string_lossy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into_owned&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="nf"&gt;.cmd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.iter&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="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="nf"&gt;.to_string_lossy&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="py"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;.collect&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// renderer.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getProcesses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_processes&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;The &lt;code&gt;ps-list&lt;/code&gt; npm package went away too — &lt;code&gt;sysinfo&lt;/code&gt; does it in the Rust core. My frontend still reads the same three fields it always did (&lt;code&gt;pid&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;cmd&lt;/code&gt;), so the renderer barely noticed.&lt;/p&gt;

&lt;p&gt;The other thing that vanished was the test harness. Testing a packaged Electron app meant 579 lines of scaffolding — booting the app, driving it, tearing it down. All of it deleted. The logic worth testing was always plain JavaScript; it just hadn't been separated from Electron.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&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;Electron&lt;/th&gt;
&lt;th&gt;Tauri 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.app&lt;/code&gt; bundle&lt;/td&gt;
&lt;td&gt;289 MB&lt;/td&gt;
&lt;td&gt;15 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.dmg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;3.7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm packages in lockfile&lt;/td&gt;
&lt;td&gt;309&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main process&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;main.js&lt;/code&gt;, 220 lines JS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lib.rs&lt;/code&gt;, 371 lines Rust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IPC plumbing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;preload.js&lt;/code&gt; + &lt;code&gt;ipc-channels.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaged-app test harness&lt;/td&gt;
&lt;td&gt;579 lines&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;18.9× smaller. A 94.7% reduction in what a user downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI actually helped — and where it didn't
&lt;/h2&gt;

&lt;p&gt;The last Electron commit in the repo is dated 9 September. The migration landed on the 10th, as a single commit: 34 files, +12,154 / −4,875. Claude Code did most of the typing. Worth being specific about which parts that actually sped up, because it wasn't uniform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it was genuinely good at&lt;/strong&gt; was the mechanical translation. Porting 220 lines of Electron main-process JavaScript into Rust is exactly the kind of work where a model earns its keep: the shape is known, the target API is well documented, and every mistake shows up as a compiler error in seconds. Rust's type checker is a brutal reviewer, and having one is what makes AI-written Rust tolerable — it can't quietly hand you something that half-works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What surprised me&lt;/strong&gt; was that the most valuable thing it did had nothing to do with the migration. While rebuilding the UI afterwards, it read the renderer and pointed out this:&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;highlighted-row&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 class marks a process as being watched. It has never been defined in any stylesheet. I checked every commit in the repository's history — it was added on 19 September 2024 and no CSS rule for it has ever existed. For 721 days, roughly two years, the only feedback that a process was being watched was the checkbox you'd just ticked yourself. I shipped releases with that bug and never noticed, because I wrote the code and my eyes slid straight over it.&lt;/p&gt;

&lt;p&gt;That's the part I'd point at if someone asked what AI is actually for. Not the typing. Reading code nobody has read in two years, without any of the assumptions that made it invisible to me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it didn't help&lt;/strong&gt; is worth saying just as plainly. It verified the redesigned UI by driving it in a Chromium-based browser. But Tauri doesn't render in Chromium on macOS — it renders in WKWebView. So the verification covered a browser the app never actually runs in, and I had to close that gap by hand. An agent that can't see the real target can only tell you a thing probably works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;p&gt;Here's the part the "Tauri is smaller" posts tend to skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dependency count didn't go down. It went up.&lt;/strong&gt; I removed 236 npm packages and added 499 Rust crates:&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;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'^\[\[package\]\]'&lt;/span&gt; src-tauri/Cargo.lock
499
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;309 npm packages became 73 npm packages plus 499 crates. What shrank is what I &lt;em&gt;ship&lt;/em&gt;, not what I &lt;em&gt;depend on&lt;/em&gt;. If your reason for migrating is supply-chain surface area, look at that number again before you start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The build cost moved onto the developer.&lt;/strong&gt; &lt;code&gt;node_modules&lt;/code&gt; is 105 MB. The Rust build directory is 5.8 GB:&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;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; node_modules src-tauri/target
105M    node_modules
5.8G    src-tauri/target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean release builds take minutes rather than seconds. I traded a big download for my users against a big build for me — which is the right trade for an app with more users than developers, but it is a trade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't ship a browser, so you don't control the browser.&lt;/strong&gt; Tauri uses WKWebView on macOS, WebView2 on Windows and WebKitGTK on Linux. On macOS the webview is a core OS component, which means, per Tauri's own docs, unsupported macOS versions don't get WebKit updates. My &lt;code&gt;minimumSystemVersion&lt;/code&gt; is 13.0, so my CSS floor is whatever Safari shipped with macOS 13 — not whatever's current. Every &lt;code&gt;appearance: none&lt;/code&gt; and &lt;code&gt;:focus-visible&lt;/code&gt; in my stylesheet is a bet on that floor. On Electron I never had to think about it, because I was shipping the renderer.&lt;/p&gt;

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

&lt;p&gt;WatchMe is on Tauri 2.11.5, &lt;code&gt;wry&lt;/code&gt; 0.55.1 and &lt;code&gt;sysinfo&lt;/code&gt; 0.39.6, built with &lt;code&gt;rustc&lt;/code&gt; 1.98.1 and Node 22. The app is 15 MB instead of 289 MB, and the code that does the interesting work is smaller and better tested than it was.&lt;/p&gt;

&lt;p&gt;Would I do it again? For a menu-bar utility, yes, without hesitating. For anything with a complex canvas-based UI, I'd think much harder — three webview engines means three sets of rendering quirks, and the more unusual your rendering is, the more of them you'll find. That's the same trade as above, with a lot more at stake.&lt;/p&gt;

&lt;p&gt;The repo is at &lt;a href="https://github.com/killerwolf/watchme" rel="noopener noreferrer"&gt;github.com/killerwolf/watchme&lt;/a&gt; if you want to see what 371 lines of Rust and a table of checkboxes look like.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://v2.tauri.app/reference/webview-versions/" rel="noopener noreferrer"&gt;Tauri 2 — Webview Versions&lt;/a&gt; — which engine on which platform, and how versions track the OS&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tauri-apps/wry" rel="noopener noreferrer"&gt;tauri-apps/wry&lt;/a&gt; — the webview abstraction underneath Tauri&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://crates.io/crates/sysinfo" rel="noopener noreferrer"&gt;sysinfo&lt;/a&gt; — the crate that replaced &lt;code&gt;ps-list&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>ai</category>
      <category>tauri</category>
    </item>
    <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>
  </channel>
</rss>
