<?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: Odilon HUGONNOT</title>
    <description>The latest articles on DEV Community by Odilon HUGONNOT (@ohugonnot).</description>
    <link>https://dev.to/ohugonnot</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3833552%2F48d32eab-68ed-4496-8ba6-f01e32806723.png</url>
      <title>DEV Community: Odilon HUGONNOT</title>
      <link>https://dev.to/ohugonnot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ohugonnot"/>
    <language>en</language>
    <item>
      <title>Designing a safe error handling package in Go: safe by default</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Thu, 09 Apr 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/designing-a-safe-error-handling-package-in-go-safe-by-default-2ke5</link>
      <guid>https://dev.to/ohugonnot/designing-a-safe-error-handling-package-in-go-safe-by-default-2ke5</guid>
      <description>&lt;p&gt;Error handling in Go is simple. Sometimes too simple. A &lt;code&gt;fmt.Errorf("pq: no rows in result set")&lt;/code&gt; bubbling up to an HTTP handler, and suddenly SQL internals are exposed in a 500 response. No exotic vulnerability required — just an &lt;code&gt;err.Error()&lt;/code&gt; in the wrong place.&lt;/p&gt;

&lt;p&gt;This problem is systemic. It doesn't stem from a lack of discipline, but from the absence of a boundary in the type system. Nothing in the &lt;code&gt;error&lt;/code&gt; interface distinguishes what is safe to expose from what is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The concrete problem
&lt;/h2&gt;

&lt;p&gt;Here is the classic leak pattern. A handler that serves &lt;code&gt;err.Error()&lt;/code&gt; directly in its JSON response, and a repository layer that wraps errors with technical context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// HTTP handler&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c"&gt;// ← LEAK&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="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// repository layer&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SELECT * FROM orders WHERE id = $1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.FindByID: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// standard wrapping&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response received by the client:&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="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders.FindByID: pq: no rows in result set"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table name, PostgreSQL driver, and nature of the SQL error are publicly exposed. Not catastrophic in isolation — but exactly the kind of information an attacker uses to refine an injection or target an attack surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What existing solutions offer (and their limits)
&lt;/h2&gt;

&lt;p&gt;Solution&lt;/p&gt;

&lt;p&gt;Approach&lt;/p&gt;

&lt;p&gt;Limitation&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cockroachdb/errors&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Exhaustive package, stack traces, rich wrapping&lt;/p&gt;

&lt;p&gt;~15k lines, 8 sub-packages. Disproportionate for a business microservice.&lt;/p&gt;

&lt;p&gt;upspin pattern (Rob Pike)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Error&lt;/code&gt; type with &lt;code&gt;Kind&lt;/code&gt;, &lt;code&gt;Op&lt;/code&gt;, &lt;code&gt;Err&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Pre-Go 1.13, no &lt;code&gt;slog&lt;/code&gt;, and &lt;code&gt;errors.Is()&lt;/code&gt; traverses the full chain.&lt;/p&gt;

&lt;p&gt;hashicorp pattern&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UserError&lt;/code&gt; interface with &lt;code&gt;GetMessage()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Relies on developer discipline, not the type system.&lt;/p&gt;

&lt;p&gt;The real question: can we design a type where &lt;code&gt;err.Error()&lt;/code&gt; &lt;em&gt;always&lt;/em&gt; returns something safe, without any convention to remember?&lt;/p&gt;

&lt;h2&gt;
  
  
  The safeerr design
&lt;/h2&gt;

&lt;p&gt;Five principles structure the package:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Safe by default&lt;/strong&gt;: &lt;code&gt;Error()&lt;/code&gt; returns only the user-facing message.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;No &lt;code&gt;Unwrap()&lt;/code&gt;&lt;/strong&gt;: the introspection chain is intentionally broken. &lt;code&gt;errors.Is()&lt;/code&gt; never traverses the internal cause. A deliberate security choice.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;slog.LogValuer&lt;/code&gt;&lt;/strong&gt;: technical details are exposed only through structured logs.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Immutable builder pattern&lt;/strong&gt;: &lt;code&gt;WithMsg()&lt;/code&gt;, &lt;code&gt;WrapCause()&lt;/code&gt; always return a new instance — sentinels are never mutated.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Centralized HTTP mapping&lt;/strong&gt;: a single point maps &lt;code&gt;ErrorKind&lt;/code&gt; values to HTTP status codes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The sentinels
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ErrNotFound&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NOT_FOUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;KindNotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="s"&gt;"not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ErrUnauthorized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UNAUTHORIZED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;KindUnauthorized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusUnauthorized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s"&gt;"unauthorized"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ErrInvalidInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INVALID_INPUT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KindInvalidInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusUnprocessableEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid input"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ErrInvalidState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INVALID_STATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KindInvalidState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusConflict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="s"&gt;"invalid state"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ErrInternal&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INTERNAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;KindInternal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"internal error"&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;
  
  
  The immutable builder pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WithMsg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WrapCause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cause&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;Each call creates a new instance. Sentinels declared as &lt;code&gt;var&lt;/code&gt; are never modified — no risk of shared mutation across goroutines.&lt;/p&gt;

&lt;h3&gt;
  
  
  slog.LogValuer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;LogValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&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="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cause"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resulting log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;level=ERROR msg="order processing failed" err.code=NOT_FOUND err.msg="order not found" err.cause="pq: no rows in result set"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technical details — driver, query, table — remain in the logs. Never in the HTTP response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Centralized HTTP mapping
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;appErr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;safeerr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;As&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;appErr&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="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unhandled error reaching handler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"internal error"&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;A single place in the codebase knows how to translate an error into an HTTP response. If an untyped error reaches this point, it is logged and returns a generic message.&lt;/p&gt;

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

&lt;p&gt;Before — technical errors bubble up raw to the handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// service — raw technical error propagated&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ProcessOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;ProcessOrderCmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FindOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FindOrder: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContractRef&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order has no contract reference"&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="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After — explicit conversion at the service boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// service — explicit conversion at the boundary&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ProcessOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;ProcessOrderCmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FindOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;safeerr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WrapCause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order not found"&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="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContractRef&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;safeerr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrInvalidState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithMsg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order has no contract reference"&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="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// handler&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ProcessOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;HTTP response: &lt;code&gt;{"error": "order not found"}&lt;/code&gt; — the SQL cause stays in the logs, never in the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full package (~130 lines)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;safeerr&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"log/slog"&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ErrorKind&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;KindNotFound&lt;/span&gt; &lt;span class="n"&gt;ErrorKind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="n"&gt;KindUnauthorized&lt;/span&gt;
    &lt;span class="n"&gt;KindForbidden&lt;/span&gt;
    &lt;span class="n"&gt;KindInvalidInput&lt;/span&gt;
    &lt;span class="n"&gt;KindInvalidState&lt;/span&gt;
    &lt;span class="n"&gt;KindConflict&lt;/span&gt;
    &lt;span class="n"&gt;KindInternal&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;AppErr&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;   &lt;span class="n"&gt;ErrorKind&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;cause&lt;/span&gt;  &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="n"&gt;ErrorKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;ErrorKind&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Cause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WithMsg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WrapCause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WithCause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;AppErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;LogValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg&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="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cause"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Honest trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;No stack traces.&lt;/strong&gt; &lt;code&gt;AppErr&lt;/code&gt; does not capture the goroutine stack. Structured logs with business context compensate in most cases, but for deep post-mortem debugging this is a real limitation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;WrapCause&lt;/code&gt; can leak if misused.&lt;/strong&gt; &lt;code&gt;WrapCause(err, err.Error())&lt;/code&gt; breaks the safety guarantee — the user-facing message becomes the technical one. Discipline is still required on this specific point.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The conversion cost.&lt;/strong&gt; Every technical error must be converted to an &lt;code&gt;AppErr&lt;/code&gt; at the service boundary. This is intentional friction — on a project with a hundred error sites, that's code to write and maintain.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;errors.Is()&lt;/code&gt; no longer works on the cause.&lt;/strong&gt; If an upper layer wants to test &lt;code&gt;errors.Is(err, sql.ErrNoRows)&lt;/code&gt;, it cannot. Acceptable if the sentinel taxonomy is expressive enough, problematic otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The real value of this package is not in the 130 lines. It's in the boundary it makes explicit between what is safe to expose and what is not. That boundary already existed in any well-designed application — but it relied on discipline, on documented conventions, on careful reviews.&lt;/p&gt;

&lt;p&gt;Moving this constraint into the type system has a cost: explicit conversion at every boundary. In return: an accidental &lt;code&gt;err.Error()&lt;/code&gt; in an HTTP response never leaks anything, regardless of who wrote the code.&lt;/p&gt;

&lt;p&gt;For an internal library shared across multiple microservices in the same stack, it's an investment that pays off quickly.&lt;/p&gt;

</description>
      <category>go</category>
      <category>errors</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>Synchronous, asynchronous, concurrent, parallel: the 4 concepts explained in Go</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Wed, 08 Apr 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/synchronous-asynchronous-concurrent-parallel-the-4-concepts-explained-in-go-2gka</link>
      <guid>https://dev.to/ohugonnot/synchronous-asynchronous-concurrent-parallel-the-4-concepts-explained-in-go-2gka</guid>
      <description>&lt;p&gt;These four words show up everywhere in docs, articles, and interviews. And they get mixed up constantly — including by experienced developers. "Asynchronous" and "concurrent" don't mean the same thing. "Parallel" and "concurrent" don't either. And a program can be all four at once, or only two, and that changes what it actually does.&lt;/p&gt;

&lt;p&gt;This article completes &lt;a href="https://www.web-developpeur.com/en/blog/concurrence-parallelisme-go-event-sourcing" rel="noopener noreferrer"&gt;the previous one on concurrency and parallelism&lt;/a&gt; by adding the synchronous/asynchronous dimension, and showing how all four concepts fit together in Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Synchronous vs asynchronous: a question of waiting
&lt;/h2&gt;

&lt;p&gt;These two words are not about "how many tasks are running at the same time". They're about &lt;strong&gt;what your code does while it's waiting for a response&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synchronous&lt;/strong&gt;: you send a request, you stop and wait for the response before doing anything else. Like calling someone on the phone and staying silent until they answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous&lt;/strong&gt;: you send a request, you keep doing other things, and you handle the response when it arrives. Like sending a text message — you put your phone down and get on with your life.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;callAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// simulates a network request&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"result"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;synchronous&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// We wait. We do nothing else for 200ms.&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;callAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"synchronous:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;asynchronous&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// We launch the call, we don't wait&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;callAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="c"&gt;// We can do other things here while the call is in progress&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"asynchronous: doing other things while the call runs..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// We retrieve the result when we need it&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"asynchronous: result received:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The synchronous version blocks for 200ms. The asynchronous version keeps working during that time. In an application making 1000 network calls per second, the difference is massive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrent vs parallel: a question of cores
&lt;/h2&gt;

&lt;p&gt;These two words don't talk about "how code waits". They talk about &lt;strong&gt;how many tasks are actually running at the same time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrent&lt;/strong&gt;: multiple tasks make progress at the same time, but on a single core. The CPU juggles between them — it advances one a bit, switches to another, comes back. Like a single chef watching three pots at once: only doing one thing at a time, but everything moves forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel&lt;/strong&gt;: multiple tasks run &lt;em&gt;truly&lt;/em&gt; at the same time, on multiple cores. Like three chefs, each at their own stove. Real simultaneous execution, not juggling.&lt;/p&gt;

&lt;p&gt;Concurrency is a matter of &lt;em&gt;structure&lt;/em&gt; (how the code is organized). Parallelism is a matter of &lt;em&gt;hardware&lt;/em&gt; (how many cores are available). A concurrent program can run in parallel on a multi-core machine, or sequentially on a single-core — without changing a single line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four possible combinations
&lt;/h2&gt;

&lt;p&gt;What makes things confusing is that synchronous/asynchronous and concurrent/parallel are two independent axes. A program can be any combination of the two:&lt;/p&gt;

&lt;p&gt;Synchronous&lt;/p&gt;

&lt;p&gt;Asynchronous&lt;/p&gt;

&lt;p&gt;Sequential&lt;/p&gt;

&lt;p&gt;Classic bash script&lt;/p&gt;

&lt;p&gt;Node.js (1 thread, callbacks)&lt;/p&gt;

&lt;p&gt;Concurrent&lt;/p&gt;

&lt;p&gt;Goroutines blocking on channels&lt;/p&gt;

&lt;p&gt;Goroutines with non-blocking I/O&lt;/p&gt;

&lt;p&gt;Parallel&lt;/p&gt;

&lt;p&gt;Threads waiting for their results&lt;/p&gt;

&lt;p&gt;Go in production: goroutines on multiple cores&lt;/p&gt;

&lt;p&gt;Node.js is a perfect example of &lt;em&gt;asynchronous but sequential&lt;/em&gt; code: a single thread, but it never blocks — it delegates I/O and resumes when ready via the event loop. Go does it differently: it is concurrent &lt;em&gt;and&lt;/em&gt; can be parallel, and lets the developer choose whether an operation is synchronous or asynchronous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go: synchronous on the surface, asynchronous under the hood
&lt;/h2&gt;

&lt;p&gt;This is one of Go's strengths that is often misunderstood. When you write an HTTP request in Go, it &lt;em&gt;looks&lt;/em&gt; like synchronous code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// This looks like "we're waiting" — but Go doesn't block the OS thread&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com/data"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In reality, when this goroutine is waiting for the network response, the Go runtime pauses it and uses the OS thread to advance other goroutines. You write code that &lt;em&gt;reads&lt;/em&gt; like synchronous, but &lt;em&gt;behaves&lt;/em&gt; like asynchronous. It's the best of both worlds: no callback hell, no &lt;code&gt;async/await&lt;/code&gt; everywhere, but no blocked thread either.&lt;/p&gt;

&lt;p&gt;Compare with the same pattern in JavaScript:&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;// JavaScript: forced to write asynchronism explicitly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JS, the &lt;code&gt;await&lt;/code&gt; is necessary to avoid blocking the single thread. In Go, you don't have to think about it — the goroutine "freezes" on its own during I/O and the OS thread stays available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency within parallelism: both at the same time
&lt;/h2&gt;

&lt;p&gt;A Go program in production is often &lt;strong&gt;concurrent AND parallel at the same time&lt;/strong&gt;. Here's how the two coexist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You have 8 cores on the machine → Go uses 8 OS threads (&lt;code&gt;GOMAXPROCS=8&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;  You have 10,000 goroutines → Go distributes them across the 8 threads&lt;/li&gt;
&lt;li&gt;  Each thread does &lt;strong&gt;concurrency&lt;/strong&gt;: it alternates between multiple goroutines&lt;/li&gt;
&lt;li&gt;  The 8 threads together do &lt;strong&gt;parallelism&lt;/strong&gt;: they run truly at the same time
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"runtime"&lt;/span&gt;
    &lt;span class="s"&gt;"sync"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Available cores: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumCPU&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Threads used (GOMAXPROCS): %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOMAXPROCS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 1000 goroutines launched — concurrency + parallelism simultaneously&lt;/span&gt;
    &lt;span class="c"&gt;// Go distributes them across available threads&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="c"&gt;// computed in parallel across multiple cores&lt;/span&gt;
        &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sum of squares:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&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;On an 8-core machine: the 1000 goroutines are distributed across 8 threads. Each thread alternates between ~125 goroutines (concurrency). The 8 threads run at the same time (parallelism). Result: 1000 calculations processed much faster than sequentially.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use what in Go?
&lt;/h2&gt;

&lt;p&gt;In practice, here's how to choose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential synchronous&lt;/strong&gt; — the default. Script, simple processing, business logic without I/O. Readable, predictable, nothing to manage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Sequential synchronous — simple and sufficient&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validate&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;save&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;notify&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Concurrent asynchronous&lt;/strong&gt; — when you're making multiple independent I/O calls (multiple APIs, multiple DB queries). Launch them in parallel, collect the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Concurrent asynchronous — 3 parallel calls instead of sequential&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;chUser&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chOrders&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chStats&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;chUser&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;chOrders&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;chStats&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;getStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="c"&gt;// All 3 calls run at the same time&lt;/span&gt;
    &lt;span class="c"&gt;// Total time = max(user time, orders time, stats time)&lt;/span&gt;
    &lt;span class="c"&gt;// instead of sum(user time + orders time + stats time)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;chUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;chOrders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Stats&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;chStats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Worker pool&lt;/strong&gt; — when you have many similar tasks and want to control the load. Not 10,000 simultaneous goroutines, but N workers pulling from a queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Worker pool: N goroutines process M tasks&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processInParallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numWorkers&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;numWorkers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;queue&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;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// each worker takes one task at a time&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="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&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;h2&gt;
  
  
  The summary as a mental model
&lt;/h2&gt;

&lt;p&gt;To never mix up the four concepts again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Synchronous/Asynchronous&lt;/strong&gt; = do I wait for the result before continuing? Yes → synchronous. No, I continue and retrieve it later → asynchronous.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sequential/Concurrent&lt;/strong&gt; = am I handling multiple tasks at the same time? No, one after the other → sequential. Yes, I alternate → concurrent.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sequential/Parallel&lt;/strong&gt; = are multiple tasks physically executing at the same time on multiple cores? No → sequential. Yes → parallel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go makes all of this relatively transparent: you launch a goroutine, Go decides whether it runs concurrently on one thread or in parallel on several. You write code that looks synchronous, Go handles the asynchronism under the hood during I/O. That's why Go is pleasant to write for these kinds of problems — you think about the logic, not the thread plumbing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📄 Associated CLAUDE.md&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.web-developpeur.com/blog/claude-md/view.php?ctx=go-concurrency" rel="noopener noreferrer"&gt;View&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/contexts/go-concurrency.md" rel="noopener noreferrer"&gt;Download&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/" rel="noopener noreferrer"&gt;Catalogue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>concurrence</category>
      <category>parallelisme</category>
      <category>asynchrone</category>
    </item>
    <item>
      <title>Which language for which project? PHP, Go, Python, JS — a pragmatic guide</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Tue, 07 Apr 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/which-language-for-which-project-php-go-python-js-a-pragmatic-guide-25pb</link>
      <guid>https://dev.to/ohugonnot/which-language-for-which-project-php-go-python-js-a-pragmatic-guide-25pb</guid>
      <description>&lt;p&gt;The question comes up at every new project kickoff: &lt;em&gt;"what are we building this in?"&lt;/em&gt; More often than not, the answer is driven by what the team already knows, or by whatever is trending on HackerNews this month. Neither is a particularly good method. I've seen a CRM written in Rust because the CTO watched a conference talk, a high-traffic API built in PHP because "that's what we've always done", and a data pipeline in Node.js because the front-end team refused to learn anything else. All three were the wrong call. Here's a more pragmatic framework — no evangelism included.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP: misunderstood and underrated
&lt;/h2&gt;

&lt;p&gt;PHP carries a reputation built on the internet of 2005 to 2012. The problem is that its reputation froze in time while the language kept evolving. PHP 8.x ships with optional strong typing, native attributes, named arguments, enums, fibers, and JIT compilation. It's not the same language that made "PHP bad" jokes go viral.&lt;/p&gt;

&lt;p&gt;For a classic web application, a REST API, an e-commerce platform, a business application built on Symfony or Laravel — PHP is often &lt;strong&gt;the most pragmatic choice available&lt;/strong&gt;. Hosting is cheap (any shared server runs PHP out of the box), the developer pool is large, documentation is exhaustive, and the ecosystem is mature. A junior dev can be productive on a well-structured Symfony project in two weeks. Try making that claim for Go or Rust.&lt;/p&gt;

&lt;p&gt;PHP's real weaknesses are structural: it's single-threaded by nature and not designed for real-time or CPU-intensive workloads. If you need persistent WebSockets, on-the-fly video processing, or an API that needs to handle 50,000 concurrent connections on a single server — PHP is the wrong tool. But for 80% of standard web projects, it's the rational choice that gets dismissed too quickly because it isn't exciting.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript: the Swiss army knife that doesn't always cut clean
&lt;/h2&gt;

&lt;p&gt;JavaScript is non-negotiable on the front-end — that's not even a question. React, Vue, Svelte, the DOM, Web APIs: all of it runs in JS. The real debate is on the back-end, with Node.js.&lt;/p&gt;

&lt;p&gt;The strongest argument for Node.js is &lt;strong&gt;code sharing between front-end and back-end&lt;/strong&gt;. When you're building with Next.js or Nuxt, you write validation logic, types, and utilities once and use them on both sides. That's a real, tangible gain — not marketing. The second argument is serverless: Lambda functions, Vercel, Cloudflare Workers — the whole serverless ecosystem is designed with JavaScript as the first-class citizen.&lt;/p&gt;

&lt;p&gt;The dark side: Node.js is still single-threaded from a CPU standpoint. The event loop handles concurrent I/O extremely well, but as soon as something blocks the thread — heavy computation, image processing, compression — everyone waits. And the npm ecosystem, despite years of improvement, remains a minefield: sketchy transitive dependencies, abandoned packages, version incompatibilities. TypeScript significantly improves long-term maintainability, but it's an added layer of complexity on top of an already complex stack.&lt;/p&gt;

&lt;p&gt;If your project has no JavaScript front-end, choosing Node.js for the API because "the team knows JS" deserves serious pushback. Familiarity alone is not a good enough reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go: boring, and that's a compliment
&lt;/h2&gt;

&lt;p&gt;Go is probably the language I hear least about in hype-driven conversations, and yet it's the one I reach for most when building things that need to work reliably in production without waking me up at night. That contradiction says something.&lt;/p&gt;

&lt;p&gt;Go is boring in the right way: no magic, no ten different ways to write the same thing, no obscure meta-programming. When you come back to Go code six months later, you read it the same way you wrote it. Concurrency is baked into the language through goroutines and channels — not a third-party library, not a pattern you need to remember, it's in the spec. For microservices, high-traffic APIs, CLI tools, and DevOps tooling, Go is hard to beat on the performance-to-code-simplicity ratio.&lt;/p&gt;

&lt;p&gt;But honestly: Go is verbose. The repetitive &lt;code&gt;if err != nil&lt;/code&gt; error handling is a well-known pain point. The library ecosystem is thinner than Python or JavaScript. And most importantly: &lt;strong&gt;for a simple CRUD app, a site with 100 visitors a day, or a two-week prototype, using Go is clear over-engineering&lt;/strong&gt;. Go shines when long-term performance, maintainability, and load resilience genuinely matter — not when you're testing an idea before you know if anyone will use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python: the undisputed choice for anything data-related
&lt;/h2&gt;

&lt;p&gt;For data science, machine learning, and AI, the conversation is short: it's Python, and that's not changing anytime soon. NumPy, Pandas, scikit-learn, PyTorch, TensorFlow, Hugging Face — the Python data ecosystem has a decade-long head start on everything else. If someone suggests doing ML in Go "for the performance", that's almost always the wrong call.&lt;/p&gt;

&lt;p&gt;Python is also excellent for scripting, automation, and rapid prototyping. The syntax is readable, the standard library is rich, and you can move fast. FastAPI, released in 2018, has become a reference point for lightweight APIs with automatic validation and generated documentation — it's excellent for exposing ML models or building internal services.&lt;/p&gt;

&lt;p&gt;Where it gets complicated is in complex business web applications. Django is solid, but the PHP/Symfony ecosystem is more mature for e-commerce platforms, ERPs, and projects with deep business logic and large teams. Python also has a structural weakness: typing is loose by default. Mypy and type hints have dramatically improved the situation, but you have to actively choose to type your code well. Performance is also a limitation — CPython is slow for CPU-bound operations outside of libraries written in C.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust and the rest: for the extreme cases
&lt;/h2&gt;

&lt;p&gt;Rust deserves a mention because it's the only language that delivers near-C performance with memory safety guaranteed at compile time. For compilers, rendering engines, WebAssembly, ultra-performant CLI tools (ripgrep, exa, Zed) — Rust is in a category of its own.&lt;/p&gt;

&lt;p&gt;The reality for 95% of projects: the learning curve is real and expensive. The borrow checker is a genuine innovation, but it takes weeks before you stop fighting it. If your deadline is in three months and your team doesn't know Rust, now is not the time to learn. Rust is a long-term investment that pays off when performance or memory safety are hard constraints — not nice-to-haves.&lt;/p&gt;

&lt;p&gt;A quick note on the others: &lt;strong&gt;Java and Kotlin&lt;/strong&gt; remain solid choices for large enterprises with significant teams, complex projects, and an established JVM culture (Spring, Quarkus). Hiring is easier than people think. &lt;strong&gt;Ruby&lt;/strong&gt; is less fashionable than it used to be, but Rails remains a strong foundation for startups that need to move fast — that's exactly what it was built for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;p&gt;Language&lt;/p&gt;

&lt;p&gt;Strengths&lt;/p&gt;

&lt;p&gt;Weaknesses&lt;/p&gt;

&lt;p&gt;Typical project&lt;/p&gt;

&lt;p&gt;Avoid if...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Maturity, cheap hosting, large developer pool&lt;/p&gt;

&lt;p&gt;Reputation (unfair), single-threaded&lt;/p&gt;

&lt;p&gt;Website, e-commerce, classic REST API&lt;/p&gt;

&lt;p&gt;Real-time, high performance&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript / Node&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Front/back code sharing, npm ecosystem, serverless&lt;/p&gt;

&lt;p&gt;npm chaos, CPU single-thread&lt;/p&gt;

&lt;p&gt;Full-stack JS, SPA, real-time&lt;/p&gt;

&lt;p&gt;Pure API with no JS front-end&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Performance, native concurrency, long-term readability&lt;/p&gt;

&lt;p&gt;Verbose, thinner library ecosystem&lt;/p&gt;

&lt;p&gt;Microservices, CLI tools, high-traffic APIs&lt;/p&gt;

&lt;p&gt;Prototyping, data science&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unmatched for AI/data, fast prototyping&lt;/p&gt;

&lt;p&gt;Performance, loose typing by default&lt;/p&gt;

&lt;p&gt;ML, scripting, automation, lightweight APIs&lt;/p&gt;

&lt;p&gt;Mobile, front-end&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Maximum performance, memory safety&lt;/p&gt;

&lt;p&gt;Steep learning curve&lt;/p&gt;

&lt;p&gt;Systems, WASM, critical tooling&lt;/p&gt;

&lt;p&gt;Projects with tight deadlines&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The real problem isn't picking the "best" language — it's resisting two opposite temptations: the hype language you've been wanting to try, and the familiar language you apply everywhere by default because it's comfortable. The right question isn't "is Go better than PHP" — that question has no general answer. The right question is: &lt;em&gt;"does the performance gain justify the recruiting cost and onboarding overhead on this specific project, with this specific team, in this specific timeframe?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A team of three senior PHP developers who know Symfony inside-out will ship more in six months in PHP than in Go where they're starting from scratch. That's obvious when you say it out loud, but it gets lost surprisingly often in the heat of a technical decision. The best language is usually the one your team can still maintain in two years — not the one that tops the benchmarks.&lt;/p&gt;

</description>
      <category>go</category>
      <category>php</category>
      <category>python</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Learning Go in 2026: the honest guide for experienced developers</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Mon, 06 Apr 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/learning-go-in-2026-the-honest-guide-for-experienced-developers-10ed</link>
      <guid>https://dev.to/ohugonnot/learning-go-in-2026-the-honest-guide-for-experienced-developers-10ed</guid>
      <description>&lt;p&gt;A few years ago, I opened a Go project for the first time. My initial reaction: "Why are there no classes? Why do I have to write &lt;code&gt;if err != nil&lt;/code&gt; everywhere? Why is the compiler yelling at me for importing a package without using it?"&lt;/p&gt;

&lt;p&gt;Three weeks later, I was debugging a production problem and realized I understood exactly what the code was doing, line by line, without surprises. No magic, no mysterious middleware, no implicit behavior. That's when I understood why Go is designed the way it is.&lt;/p&gt;

&lt;p&gt;This article is the guide I wish I'd read when I started. Not the official docs paraphrased — real advice, real resources, and what goes through your head when coming from another language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go in 2026
&lt;/h2&gt;

&lt;p&gt;Forget the marketing pitch "Go is fast, Go is concurrent, Google made it". That's not why you adopt Go.&lt;/p&gt;

&lt;p&gt;Go is boring. And that's its strength. A language where you open a file you've never seen, written by someone else, and you understand what it does in 30 seconds. No meta-programming. Not 15 ways to do the same thing. No DSL embedded in a DSL. No magic decorators that transform class behavior at runtime.&lt;/p&gt;

&lt;p&gt;After years of PHP/Symfony with its 200 DI classes, or Node.js with 47 dependencies in &lt;code&gt;node_modules&lt;/code&gt;, or Python where every project has its own unspoken conventions — Go is liberating. Go code looks like Go code. Regardless of who wrote it.&lt;/p&gt;

&lt;p&gt;In practice in 2026: Go dominates infrastructure (Kubernetes, Docker, Terraform, Prometheus are all written in Go), high-performance APIs, backend services that need serious concurrency, and CLIs. If you work in this space, learning Go is a good investment.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Resources that actually work
&lt;/h2&gt;

&lt;p&gt;There's a lot of Go content on the internet. Most of it is either too basic or poorly done. Here's what's actually worth your time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The essentials
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://go.dev/tour/" rel="noopener noreferrer"&gt;A Tour of Go&lt;/a&gt;&lt;/strong&gt; — The official entry point. Interactive, well structured, 2-3 hours if done seriously. Do the whole thing, not diagonally. Every exercise is there for a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gobyexample.com/" rel="noopener noreferrer"&gt;Go by Example&lt;/a&gt;&lt;/strong&gt; — Every language concept illustrated with minimal, working code. Perfect as a quick reference after the Tour, when you're wondering "how do you do a channel with timeout in Go again?".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://go.dev/doc/effective_go" rel="noopener noreferrer"&gt;Effective Go&lt;/a&gt;&lt;/strong&gt; — The most important document that exists on Go. This is the philosophy of the language. Not just the syntax — why interfaces are implicit, how to think about composition, why errors are values. Read it once at the start (you'll understand 50%), then re-read it after 2-3 months (you'll understand everything). Really.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://go.dev/blog/" rel="noopener noreferrer"&gt;The official Go blog&lt;/a&gt;&lt;/strong&gt; — In-depth articles, written by the language creators. A few must-reads: "Go Concurrency Patterns", "Error handling and Go", "Go Slices: usage and internals", "The Go Memory Model". Not to read all at once — bookmark them and read when the topic becomes relevant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Books worth their price
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://lets-go.alexedwards.net/" rel="noopener noreferrer"&gt;Let's Go by Alex Edwards&lt;/a&gt;&lt;/strong&gt; — The best book for building a real web application in Go. Paid, but it's the most worthwhile investment on this list. It explains production patterns: middleware, sessions, CSRF, integration tests, deployment. Not toy code. And for those on a tight budget: a quick search on GitHub can turn up the PDF without much difficulty. But if you can afford it, pay for it — Alex Edwards' work deserves it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning Go by Jon Bodner (O'Reilly)&lt;/strong&gt; — Good choice if you come from an object-oriented language and want to understand the "why" behind Go's design choices. Less web-focused, more focused on language fundamentals.&lt;/p&gt;

&lt;h3&gt;
  
  
  What people often overlook: the stdlib
&lt;/h3&gt;

&lt;p&gt;In Go, the standard library is incredibly complete. &lt;code&gt;net/http&lt;/code&gt;, &lt;code&gt;encoding/json&lt;/code&gt;, &lt;code&gt;database/sql&lt;/code&gt;, &lt;code&gt;testing&lt;/code&gt;, &lt;code&gt;context&lt;/code&gt;, &lt;code&gt;sync&lt;/code&gt;, &lt;code&gt;io&lt;/code&gt;... Before looking for an external dependency, check if the stdlib already does the job. 90% of the time, it does.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's a waste of time
&lt;/h3&gt;

&lt;p&gt;YouTube tutorials "Build a REST API in Go in 30 minutes" — often bad practices, no error handling, incomprehensible architectures. Udemy courses — too slow for an experienced dev, often outdated. Trying to learn Go by reading Kubernetes source code — that's like learning English by reading Shakespeare. Technically correct, but not the right entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The 5 mental shifts to make quickly
&lt;/h2&gt;

&lt;p&gt;These are the things that surprise you most when coming from another language. Better to identify and accept them upfront rather than spend two weeks fighting the language.&lt;/p&gt;

&lt;h3&gt;
  
  
  There are no classes
&lt;/h3&gt;

&lt;p&gt;Go has structs and methods on those structs. No inheritance. Composition replaces inheritance. At first it's frustrating, later you understand why it's better.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// No "Animal" class with an inherited "Speak" method on "Dog"&lt;/span&gt;
&lt;span class="c"&gt;// Instead: composition via embedding&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Animal&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Describe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"I am "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Dog&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Animal&lt;/span&gt;        &lt;span class="c"&gt;// Embedding: Dog "inherits" Animal's methods&lt;/span&gt;
    &lt;span class="n"&gt;Breed&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Dog now has the Describe() method without declaring anything&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Dog&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Animal&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Rex"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;Breed&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Labrador"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Describe&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c"&gt;// "I am Rex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Errors are values, not exceptions
&lt;/h3&gt;

&lt;p&gt;The famous &lt;code&gt;if err != nil&lt;/code&gt;. Yes, it's verbose. No, there's no try/catch. It's a design choice: every error is handled explicitly at the point where it occurs. After a few weeks in production, you realize you have far fewer surprises. The error doesn't silently bubble up the call stack to explode somewhere else.&lt;/p&gt;

&lt;p&gt;The standard pattern for wrapping errors with context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRowContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SELECT id, name FROM users WHERE id = $1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"getUser %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c"&gt;// %w allows unwrapping the error later with errors.Is() / errors.As()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer adds context with &lt;code&gt;fmt.Errorf("context: %w", err)&lt;/code&gt;. In the end, the error message reads like a trace: "handler &amp;gt; service &amp;gt; store &amp;gt; SQL error".&lt;/p&gt;

&lt;h3&gt;
  
  
  Interfaces are implicit
&lt;/h3&gt;

&lt;p&gt;No need to declare &lt;code&gt;implements&lt;/code&gt;. If a type has the right methods, it satisfies the interface automatically. This is the most powerful concept in Go and the most confusing at first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// The io.Reader interface from the stdlib:&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Reader&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// An os.File satisfies Reader.&lt;/span&gt;
&lt;span class="c"&gt;// A bytes.Buffer satisfies Reader.&lt;/span&gt;
&lt;span class="c"&gt;// A net.Conn satisfies Reader.&lt;/span&gt;
&lt;span class="c"&gt;// Your own struct satisfies Reader if it has the Read method.&lt;/span&gt;
&lt;span class="c"&gt;// No "implements" declaration anywhere.&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// This function accepts anything that knows how to read&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Usable with a file, a buffer, a network connection, a test mock...&lt;/span&gt;
&lt;span class="c"&gt;// Without changing a single line of processData.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Formatting is not a debate
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gofmt&lt;/code&gt; formats the code. Period. No config, no options, no tabs vs spaces war. All Go projects have the same style. It's liberating. Configure the VS Code extension to format on save and never think about it again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;fmt&lt;/span&gt; ./...
&lt;span class="c"&gt;# Formats all project files. Nothing to configure.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generics exist, but sparingly
&lt;/h3&gt;

&lt;p&gt;Generics arrived in Go 1.18 (2022). The community uses them sparingly. The Go philosophy: if you can do without generics, do without. Interfaces and the &lt;code&gt;any&lt;/code&gt; type are sufficient for 90% of cases. Don't start with generics. Learn the basics, build something that works, and introduce generics when you have a real need for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Tooling — What makes Go pleasant from day one
&lt;/h2&gt;

&lt;p&gt;Go has the best built-in tooling of any language I know. Everything is in the standard distribution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run main.go          &lt;span class="c"&gt;# Compiles and runs in one command&lt;/span&gt;
go build ./...          &lt;span class="c"&gt;# Produces a static binary. A single file.&lt;/span&gt;
go &lt;span class="nb"&gt;test&lt;/span&gt; ./...           &lt;span class="c"&gt;# Runs all tests. Testing built-in, no external framework.&lt;/span&gt;
go &lt;span class="nb"&gt;fmt&lt;/span&gt; ./...            &lt;span class="c"&gt;# Formats all code&lt;/span&gt;
go vet ./...            &lt;span class="c"&gt;# Basic static analysis&lt;/span&gt;
go mod init my/module   &lt;span class="c"&gt;# Initializes a module&lt;/span&gt;
go mod tidy             &lt;span class="c"&gt;# Syncs go.mod and go.sum with actual imports&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binary produced by &lt;code&gt;go build&lt;/code&gt; is static. A single file, no runtime, no dependencies. Copy the binary to a Linux server and it runs. No "do you have the right version of Python installed?".&lt;/p&gt;

&lt;p&gt;For the development environment, two tools to install immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The VS Code "Go" extension&lt;/strong&gt; with &lt;code&gt;gopls&lt;/code&gt; (the official LSP) — autocomplete, go to definition, rename, type inlining. Everything works out of the box after installation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://golangci-lint.run/" rel="noopener noreferrer"&gt;golangci-lint&lt;/a&gt;&lt;/strong&gt; — The linter to install on day 1. Combines about fifty linters. &lt;code&gt;golangci-lint run ./...&lt;/code&gt; finds real bugs, not just style issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. The first project — What to build
&lt;/h2&gt;

&lt;p&gt;Don't start with a gRPC microservice with Kafka and Kubernetes. Not an ultra-complex CLI. Not "I'll rewrite my PHP project in Go" — too much frustration trying to map patterns from another language into Go.&lt;/p&gt;

&lt;p&gt;The right first project: &lt;strong&gt;a simple REST API with a real database&lt;/strong&gt;. It's complete enough to touch everything that matters: structs, interfaces, packages, &lt;code&gt;net/http&lt;/code&gt;, &lt;code&gt;database/sql&lt;/code&gt;, JSON, error handling, middleware, and tests.&lt;/p&gt;

&lt;p&gt;A readable and idiomatic project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;myapp/
├── main.go          &lt;span class="c"&gt;# Entry point: initialization, wiring, server startup&lt;/span&gt;
├── go.mod
├── go.sum
├── handler/
│   └── user.go      &lt;span class="c"&gt;# HTTP handlers: decode request, call store, encode response&lt;/span&gt;
├── model/
│   └── user.go      &lt;span class="c"&gt;# Types: struct User, struct CreateUserRequest...&lt;/span&gt;
└── store/
    └── postgres.go  &lt;span class="c"&gt;# DB access: SQL queries, scan into structs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A minimal but correct HTTP handler — with error handling, appropriate status codes, and clean JSON response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserHandler&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="n"&gt;UserStore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// UserStore is an interface defined here, on the consumer side&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PathValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Go 1.22+: patterns in http.ServeMux&lt;/span&gt;

    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// Log the internal error, don't expose it to the client&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GetUser failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"internal server error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"encode response failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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;This isn't "advanced" code. It's idiomatic Go for a first project. Every error is handled. Internal errors are not exposed to the client. Context is propagated. This is the level to aim for from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Pitfalls in the first weeks
&lt;/h2&gt;

&lt;p&gt;These are the mistakes everyone makes. Identifying them upfront avoids a few weeks of bad habits to unlearn.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using pointers everywhere "for performance"
&lt;/h3&gt;

&lt;p&gt;No. Go passes structs by value efficiently. Pointers serve two things: modifying the receiver in a method, or avoiding copy for large structs (a few hundred bytes). By default, pass by value. Add a pointer when there's a concrete reason, not "just in case".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Value receiver: the method doesn't modify the struct&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Pointer receiver: the method modifies the struct&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;SetEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating interfaces too early
&lt;/h3&gt;

&lt;p&gt;In Go, interfaces are defined on the consumer side, not the producer side. Don't create a &lt;code&gt;UserService&lt;/code&gt; interface with 15 methods before having a second consumer that needs it. The Go principle: "Accept interfaces, return structs." Define the interface as small as possible, only for the methods the consumer actually needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Too broad: who will actually use these 15 methods?&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;GetByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="c"&gt;// ... 10 more methods&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ✅ Define the minimal interface at the consumer level&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserGetter&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// The handler only needs GetByID: that's all we expose&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserHandler&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="n"&gt;UserGetter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Importing a web framework
&lt;/h3&gt;

&lt;p&gt;Gin, Echo, Fiber... The stdlib &lt;code&gt;net/http&lt;/code&gt; with &lt;code&gt;http.ServeMux&lt;/code&gt; (which supports patterns and HTTP methods since Go 1.22) is sufficient for 95% of use cases. For a bit more routing comfort, &lt;a href="https://github.com/go-chi/chi" rel="noopener noreferrer"&gt;chi&lt;/a&gt; is lightweight and idiomatic. Heavy frameworks add magic and hide what Go does well on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignoring the context package
&lt;/h3&gt;

&lt;p&gt;Context is everywhere in Go: timeouts, cancellation, request-scoped values. Start using it from your first project. Every function that does I/O (database, HTTP, file) should accept a &lt;code&gt;context.Context&lt;/code&gt; as the first parameter. It's a language convention, not an option.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ✅ Standard pattern: ctx as first parameter for all I/O&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;UserStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryRowContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"SELECT id, name, email FROM users WHERE id = $1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNoRows&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="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrNotFound&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"store.GetByID %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Panicking over verbosity
&lt;/h3&gt;

&lt;p&gt;Go code is longer than Python. That's normal and intentional. Every line does one clear thing. After a few weeks, you'll realize you read Go code 3x faster than equivalent Python or JavaScript, because there's no hidden implicit behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Concurrency — When to get into it
&lt;/h2&gt;

&lt;p&gt;Not yet. Seriously.&lt;/p&gt;

&lt;p&gt;Goroutines and channels are Go's most visible feature, and the most common trap for beginners. Start by writing correct sequential code. Correct sequential code is infinitely better than buggy concurrent code. Introduce concurrency when there's a real need: parallel requests, queue processing, fan-out on API calls.&lt;/p&gt;

&lt;p&gt;The learning order that makes sense:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Basic goroutines + channels (Go by Example covers this perfectly)&lt;/li&gt;
&lt;li&gt; &lt;code&gt;sync.WaitGroup&lt;/code&gt; and &lt;code&gt;sync.Mutex&lt;/code&gt; for coordination&lt;/li&gt;
&lt;li&gt; &lt;code&gt;context&lt;/code&gt; for cancellation and timeouts&lt;/li&gt;
&lt;li&gt; &lt;code&gt;errgroup&lt;/code&gt; (golang.org/x/sync) for production patterns&lt;/li&gt;
&lt;li&gt; The official "Go Concurrency Patterns" blog post once the basics are solid&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A correct basic concurrency pattern, with context-based cancellation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errgroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="c"&gt;// loop variable capture (before Go 1.22)&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;processOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Close results when all goroutines are done&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;count&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="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&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;h2&gt;
  
  
  7. Code organization
&lt;/h2&gt;

&lt;p&gt;A few Go rules about packages that avoid bad habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  One module = one repo. &lt;code&gt;go mod init github.com/user/myproject&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  One package = one directory. The package name matches the directory name.&lt;/li&gt;
&lt;li&gt;  No &lt;code&gt;utils&lt;/code&gt;, &lt;code&gt;helpers&lt;/code&gt;, &lt;code&gt;common&lt;/code&gt; packages. This is a classic Go anti-pattern — these packages end up as catch-alls. Name packages by what they do: &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;handler&lt;/code&gt;, &lt;code&gt;middleware&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Exported identifiers start with a capital letter. &lt;code&gt;User&lt;/code&gt; is public, &lt;code&gt;user&lt;/code&gt; is private to the package. No &lt;code&gt;public&lt;/code&gt; or &lt;code&gt;private&lt;/code&gt; keywords.&lt;/li&gt;
&lt;li&gt;  Keep packages small and focused. A &lt;code&gt;user&lt;/code&gt; package that handles users, not a &lt;code&gt;models&lt;/code&gt; package that does everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Conventions that matter
&lt;/h2&gt;

&lt;p&gt;Go has strong conventions. Adopt them from the start, even when they feel counter-intuitive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Short names in local scopes:&lt;/strong&gt; &lt;code&gt;u&lt;/code&gt; for a user, &lt;code&gt;ctx&lt;/code&gt; for context, &lt;code&gt;err&lt;/code&gt; for error, &lt;code&gt;r&lt;/code&gt; for an HTTP request, &lt;code&gt;w&lt;/code&gt; for the ResponseWriter. Go prefers brevity when context is clear. Long names are for exported identifiers.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No getters with "Get":&lt;/strong&gt; &lt;code&gt;user.Name()&lt;/code&gt;, not &lt;code&gt;user.GetName()&lt;/code&gt;. The "Get" is implicit in Go.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Test files next to code:&lt;/strong&gt; &lt;code&gt;user_test.go&lt;/code&gt; next to &lt;code&gt;user.go&lt;/code&gt;. No separate &lt;code&gt;tests/&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Comments document the "why":&lt;/strong&gt; Go code is meant to be readable without comments. A comment that says "// increments the counter" in front of &lt;code&gt;count++&lt;/code&gt; adds nothing. A comment that explains why you're using a mutex here rather than a channel — that's worth something.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Ignoring an error is a code smell:&lt;/strong&gt; &lt;code&gt;_ = f()&lt;/code&gt; or &lt;code&gt;result, _ := f()&lt;/code&gt; should be extremely rare and commented. If a function returns an error, handle it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. What's next — After the basics
&lt;/h2&gt;

&lt;p&gt;Once comfortable with the basics, what's worth the time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the stdlib source code.&lt;/strong&gt; Seriously. &lt;code&gt;net/http&lt;/code&gt;, &lt;code&gt;encoding/json&lt;/code&gt;, &lt;code&gt;database/sql&lt;/code&gt; are examples of idiomatic Go written by the language creators. It's the best school. The Go stdlib is readable — not thousands of abstract framework files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn advanced concurrency patterns&lt;/strong&gt;: worker pools, fan-out / fan-in, semaphores with buffered channels, &lt;code&gt;sync.Once&lt;/code&gt; for lazy initialization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understand composable interfaces&lt;/strong&gt;: &lt;code&gt;io.ReadWriteCloser&lt;/code&gt; is the composition of &lt;code&gt;Reader&lt;/code&gt; + &lt;code&gt;Writer&lt;/code&gt; + &lt;code&gt;Closer&lt;/code&gt;. This interface composition pattern is ubiquitous in the stdlib and in idiomatic Go code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get into profiling with &lt;code&gt;pprof&lt;/code&gt;&lt;/strong&gt; when you have a real performance problem — not before. Go has excellent built-in profiling tools, but using them on a hypothetical problem serves no purpose.&lt;/p&gt;

&lt;p&gt;Two books that are genuinely worth their price:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Concurrency in Go by Katherine Cox-Buday&lt;/strong&gt; — The reference book on Go concurrency. Goroutines, channels, advanced patterns, pitfalls to avoid.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://100go.co/" rel="noopener noreferrer"&gt;100 Go Mistakes by Teiva Harsanyi&lt;/a&gt;&lt;/strong&gt; — Each chapter is a real mistake with the explanation and the fix. More useful than a generic best-practices book because everything is grounded in concrete bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Go is a language that rewards patience and simplicity. The first weeks are frustrating when coming from a more expressive language — you feel like you're repeating yourself, typing too much &lt;code&gt;if err != nil&lt;/code&gt;, missing abstractions.&lt;/p&gt;

&lt;p&gt;And then one day, you're debugging a production problem and you realize you understand exactly what the code does, line by line, without going to check what a magic decorator does or what a mysterious middleware injects. That's when you understand why Go is designed the way it is.&lt;/p&gt;

&lt;p&gt;The most important advice: don't try to write Go like you'd write Java, Python, or PHP. Accept Go conventions — implicit interfaces, explicit errors, no classes, intentional verbosity. The language was designed with intentional constraints, and they make sense once you have enough context to see why.&lt;/p&gt;

&lt;p&gt;The concrete action plan: A Tour of Go (2-3h), then build a simple REST API with &lt;code&gt;net/http&lt;/code&gt; and PostgreSQL, then re-read Effective Go. In that order. No shortcuts.&lt;/p&gt;

</description>
      <category>go</category>
      <category>apprentissage</category>
      <category>dbutant</category>
    </item>
    <item>
      <title>Persistent memory in Claude Code: what's worth keeping</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 05 Apr 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/persistent-memory-in-claude-code-whats-worth-keeping-54ck</link>
      <guid>https://dev.to/ohugonnot/persistent-memory-in-claude-code-whats-worth-keeping-54ck</guid>
      <description>&lt;p&gt;Claude remembers nothing. Every session starts from scratch — the preferences you explained last week, the business constraints it discovered while working on the code, the corrections you had to make twice. Everything disappears with &lt;code&gt;/clear&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;CLAUDE.md partially compensates: you encode project conventions, the stack, deployment rules. But there's a category of information CLAUDE.md handles poorly: things that evolve over time. An architecture decision made this month, a style preference corrected mid-session, a gotcha found in production. These have variable lifespans and don't all belong in a file versioned with the project.&lt;/p&gt;

&lt;p&gt;Claude Code has a persistent memory system for exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What auto-memory does — and doesn't
&lt;/h2&gt;

&lt;p&gt;Auto-memory is a set of Markdown files in a dedicated directory per project (&lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/memory/&lt;/code&gt;) or global (&lt;code&gt;~/.claude/memory/&lt;/code&gt;). Claude reads them at session start via an index file &lt;code&gt;MEMORY.md&lt;/code&gt;, and can update them during the session.&lt;/p&gt;

&lt;p&gt;What it doesn't do: it's not an automatic session log. Claude doesn't note everything that happened. It's an active tool — you have to tell it explicitly what's worth keeping, or ask it to run the audit itself.&lt;/p&gt;

&lt;p&gt;The distinction with CLAUDE.md matters. CLAUDE.md describes the project: stack, conventions, deployment. Memory describes the working relationship: preferences, ongoing decisions, non-obvious constraints at a given point in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 memory types
&lt;/h2&gt;

&lt;p&gt;Each memory file has a type declared in its frontmatter. It's not cosmetic — it defines when Claude will use them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;user&lt;/strong&gt; — who you're working with: role, technical level, areas of knowledge. Lets Claude calibrate explanation depth without re-introductions every session.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;feedback&lt;/strong&gt; — how to approach the work: what was corrected, what was validated. "Don't summarize what you just did at the end of messages" is feedback. "Always use a real database in tests, no mocks" too.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;project&lt;/strong&gt; — current project state: active decisions, initiatives, time constraints. These age fast — they need regular review.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;reference&lt;/strong&gt; — where to find information in external systems: "pipeline bugs tracked in Linear project INGEST", "Grafana latency board at grafana.internal/d/api-latency".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's worth keeping — and what isn't
&lt;/h2&gt;

&lt;p&gt;Memory is not an archive. What goes in a memory file must be non-deducible from the code or CLAUDE.md, and useful in future sessions.&lt;/p&gt;

&lt;p&gt;Worth keeping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  A style preference confirmed or corrected during the session&lt;/li&gt;
&lt;li&gt;  A real gotcha discovered while working — an invisible constraint, a counter-intuitive behavior&lt;/li&gt;
&lt;li&gt;  An active architecture decision not yet reflected in code&lt;/li&gt;
&lt;li&gt;  A pointer to an external resource you use regularly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doesn't belong in memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Anything visible in the files — Claude will find it by reading the code&lt;/li&gt;
&lt;li&gt;  Session details — git log is more reliable for that&lt;/li&gt;
&lt;li&gt;  In-progress tasks — they belong to the session, not persistent memory&lt;/li&gt;
&lt;li&gt;  Conventions already in CLAUDE.md — pointless duplication&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project memory vs global memory
&lt;/h2&gt;

&lt;p&gt;Project memory (&lt;code&gt;~/.claude/projects/&amp;lt;sanitized-cwd&amp;gt;/memory/&lt;/code&gt;) is isolated per working directory. Open Claude from a different folder, and you get a different memory. This is the right option for project-specific preferences and decisions.&lt;/p&gt;

&lt;p&gt;Global memory (&lt;code&gt;~/.claude/memory/&lt;/code&gt;) applies across all sessions and projects. That's where truly cross-cutting preferences go: expected response style, the person's technical level, communication conventions.&lt;/p&gt;

&lt;p&gt;In practice: start with project memory. If a preference comes up across all projects, move it to global. Never duplicate both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic feeding: the rule in ~/.claude/CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;Rather than asking Claude to save something each time you think of it, you can give it the rule once in the user-level CLAUDE.md — &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;, loaded in every session across all projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Global rules

## Auto-memory

When a session reveals persistent information, save it immediately
to the right memory file without waiting to be asked:

- Preference discovered or corrected → memory/user or memory/feedback
- Approach correction                → memory/feedback
- Durable project decision           → memory/project
- Pointer to regularly-used resource → memory/reference

Do not save: temporary state, in-progress work, what's already in
CLAUDE.md, what's deducible from the code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter matters as much as the trigger. Without it, Claude will log every session detail and memory grows as fast as the CLAUDE.md you just cleaned up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintaining memory: the audit prompt
&lt;/h2&gt;

&lt;p&gt;Memory ages. Project-type memories especially — a decision made in January may be resolved by March, but the file stays. Without regular review, you accumulate stale information that Claude will read and potentially apply incorrectly.&lt;/p&gt;

&lt;p&gt;An audit prompt to run at session start when memory has drifted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audit the memory files in `.claude/projects/.../memory/`.

For each file:
1. Is the content still accurate? (check against current file state if needed)
2. Is it non-deducible from code or CLAUDE.md?
3. Is the type correct? (user / feedback / project / reference)
4. Is the frontmatter description precise enough?

Global coherence: duplicates, contradictions, stale project memories, MEMORY.md up to date?

Output as table: File | Issue | Recommended action
Then apply corrections.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To avoid hunting for this prompt, save it as a slash command in &lt;code&gt;.claude/commands/audit-memory.md&lt;/code&gt; (project) or &lt;code&gt;~/.claude/commands/audit-memory.md&lt;/code&gt; (global). Then &lt;code&gt;/audit-memory&lt;/code&gt; is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Auto-memory solves a specific problem: information that evolves too fast for CLAUDE.md, but worth carrying across sessions. It works well when you're selective — one file per topic, a well-chosen type, a precise description in the frontmatter.&lt;/p&gt;

&lt;p&gt;What breaks the system: trying to put everything in it. Memory that grows without criteria ends up as useless as a 300-line CLAUDE.md — Claude loads everything, processes everything, and the signal drowns in noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📄 Associated CLAUDE.md&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.web-developpeur.com/blog/claude-md/view.php?ctx=memoire-auto-claude-code" rel="noopener noreferrer"&gt;View&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/contexts/memoire-auto-claude-code.md" rel="noopener noreferrer"&gt;Download&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/" rel="noopener noreferrer"&gt;Catalogue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>memory</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Optimizing Claude Code token usage: lessons learned</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sat, 04 Apr 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/optimizing-claude-code-token-usage-lessons-learned-3h71</link>
      <guid>https://dev.to/ohugonnot/optimizing-claude-code-token-usage-lessons-learned-3h71</guid>
      <description>&lt;p&gt;After a few weeks of heavy Claude Code usage, the bill climbs. Not because tasks are more complex — because sessions get bloated. Claude loads all available context, re-reads the same files every exchange, and CLAUDE.md grows longer with each new rule added. Result: you're paying for useless context, not for actual work.&lt;/p&gt;

&lt;p&gt;Here are the levers that had the most impact on my project — a PHP portfolio with an automated news monitoring system in Node.js. No magic recipes, just concrete adjustments with real effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  .claudeignore: what Claude should never read
&lt;/h2&gt;

&lt;p&gt;By default, Claude Code can index any file in the working directory. On my project that included &lt;code&gt;uploads/&lt;/code&gt; (hundreds of runtime JSON files), &lt;code&gt;.playwright-mcp/&lt;/code&gt;, temporary session plans, and all binary assets. None of these files are useful when debugging a Node.js script.&lt;/p&gt;

&lt;p&gt;The solution is a &lt;code&gt;.claudeignore&lt;/code&gt; at the root, using the same syntax as &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Runtime data — useless for coding
uploads/
scripts/.retro-state.json
scripts/.veille.lock

# Playwright
.playwright-mcp/

# Generated plans/specs
docs/superpowers/

# Binary assets
assets/images/
assets/plugins/

# Blog comments
blog/comments/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The effect is immediate: Claude no longer loads these files during automatic project exploration. On my &lt;code&gt;uploads/&lt;/code&gt; folder exceeding 2 MB of JSON, this is the most significant gain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split CLAUDE.md by domain
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is injected into every session. If it's 300 lines long, they're all there from the first exchange — even when working on blog CSS with no reason to know the subtleties of the monitoring daemon.&lt;/p&gt;

&lt;p&gt;Claude Code supports the &lt;code&gt;@path&lt;/code&gt; syntax to import other files. But the import is automatic — it always loads the referenced file. That's not conditional loading.&lt;/p&gt;

&lt;p&gt;The real solution: split the files and &lt;em&gt;don't&lt;/em&gt; import them automatically. I split my CLAUDE.md in two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;CLAUDE.md&lt;/code&gt; — stack, deployment, LinkedIn bot. 24 lines.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;CLAUDE.veille.md&lt;/code&gt; — the entire monitoring system. Loaded manually when needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;code&gt;CLAUDE.md&lt;/code&gt;, just a note:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Monitoring system: see `CLAUDE.veille.md`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I work on the monitoring system, I explicitly tell Claude "read CLAUDE.veille.md". The rest of the time, the file doesn't exist in context. The main CLAUDE.md went from 260 to 24 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  /clear between each distinct task
&lt;/h2&gt;

&lt;p&gt;This is the most underrated lever. A Claude Code session accumulates everything: files read, previous exchanges, failed attempts. If you chain "add a field to the form" → "fix this daemon bug" → "write this article", the session context contains all of that simultaneously.&lt;/p&gt;

&lt;p&gt;Each exchange is billed against the entire accumulated context. A 2-hour session can cost 3x more than a series of short sessions doing the same work.&lt;/p&gt;

&lt;p&gt;Practical rule: &lt;code&gt;/clear&lt;/code&gt; as soon as you switch tasks. &lt;code&gt;/compact&lt;/code&gt; if you want to keep a summary of what was done before continuing on the same topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Targeted questions, not explorations
&lt;/h2&gt;

&lt;p&gt;"How does the monitoring system work?" — Claude will read 8 files, retrace the architecture, and produce a 500-token explanation. If you just wanted to know how deduplication works, you paid for a lot of useless context.&lt;/p&gt;

&lt;p&gt;The efficient version: "in &lt;code&gt;scripts/run-veille.js&lt;/code&gt;, how does SHA256 dedup work?" Claude reads one file, goes directly to the function, answers in 50 tokens.&lt;/p&gt;

&lt;p&gt;Same logic for modifications. Rather than "I want to modify the FTP loop behavior", give the file and line: "in &lt;code&gt;scripts/run-veille.js&lt;/code&gt; line 180, I want the condition to also ignore &lt;code&gt;.html&lt;/code&gt; files". Claude makes a minimal diff without re-reading the entire file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Haiku for simple tasks — with caveats
&lt;/h2&gt;

&lt;p&gt;Claude Haiku costs about 20x less than Sonnet at equivalent volume. For mechanical tasks — variable renaming, adding a case to a switch, fixing a typo in a comment — it gets the job done.&lt;/p&gt;

&lt;p&gt;But the cost of back-and-forth often cancels out the gain. On a project with implicit coupling (JSON registry driving a daemon that syncs via FTP), Haiku will regularly miss a constraint and propose a superficial fix. You then spend 3 exchanges correcting what one Sonnet exchange would have resolved.&lt;/p&gt;

&lt;p&gt;Good usage: &lt;code&gt;/model haiku&lt;/code&gt; for factual questions ("where is function X defined?", "what's the schema of this JSON?") and single-line changes in isolated context. Stay on Sonnet as soon as the task involves multiple files or non-trivial business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Claude from a subdirectory
&lt;/h2&gt;

&lt;p&gt;Claude Code indexes the directory it's launched from. Launch from the project root and everything is available. Launch from &lt;code&gt;veille/&lt;/code&gt; and only that folder is in the auto-exploration scope.&lt;/p&gt;

&lt;p&gt;For focused work on a single module — monitoring admin, Node.js scripts, blog — opening Claude from the right subdirectory mechanically reduces what it can inadvertently load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;None of these optimizations is spectacular in isolation. The effect comes from their combination: a short CLAUDE.md, a .claudeignore that excludes runtime data, short and focused sessions, precise questions with file and line number.&lt;/p&gt;

&lt;p&gt;What remains most counterintuitive: long sessions seem productive because you get a lot done. In practice, a 20-minute session with &lt;code&gt;/clear&lt;/code&gt; between each task costs less and produces work as clean as a 2-hour uninterrupted session — often better, because Claude doesn't have to manage a context that has become incoherent.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>optimization</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Building an SEO landing page with Claude Code in one session</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Fri, 03 Apr 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/building-an-seo-landing-page-with-claude-code-in-one-session-gng</link>
      <guid>https://dev.to/ohugonnot/building-an-seo-landing-page-with-claude-code-in-one-session-gng</guid>
      <description>&lt;p&gt;I had a business registration number, a service idea, and zero landing page. Four hours later, I had a page in production with SEO, a contact form, a chatbox, and animated SVG circles. Here's exactly how it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The need
&lt;/h2&gt;

&lt;p&gt;I wanted to launch a freelance automation service — custom scripts, API integrations, scraping, Excel macros. The kind of thing you can pitch in one sentence but takes real trust to sell. Which means: a proper page, not a GitHub README or a Notion doc sent in a DM.&lt;/p&gt;

&lt;p&gt;The constraints were clear from the start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  No WordPress, no page builder — I write code for a living&lt;/li&gt;
&lt;li&gt;  Subfolder of the existing site (&lt;code&gt;/automatisation/&lt;/code&gt;), not a new domain&lt;/li&gt;
&lt;li&gt;  Pure PHP, zero frontend framework&lt;/li&gt;
&lt;li&gt;  Contact form that actually sends emails (PHPMailer, already wired on the blog)&lt;/li&gt;
&lt;li&gt;  SEO-ready from day one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "zero frontend framework" constraint is worth explaining. For a landing page with one purpose — convert visitors into quote requests — React or Vue would be absurd. Server-rendered PHP with a custom CSS file is faster to ship, faster to load, and zero maintenance burden three years later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain and SEO strategy
&lt;/h2&gt;

&lt;p&gt;The first real decision: subfolder vs. new domain. I asked Claude to help me think through it. The answer was obvious once framed correctly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;web-developpeur.com&lt;/code&gt; has been indexed for years, has real backlinks, and ranks for developer-related terms. A brand new domain starts at zero — zero authority, zero trust, zero traffic. A subfolder at &lt;code&gt;/automatisation/&lt;/code&gt; inherits the parent domain's SEO weight immediately.&lt;/p&gt;

&lt;p&gt;For keyword strategy, I wanted to target a specific niche: small French businesses and individuals looking for custom automation, not Make.com or Zapier. The competitive angle writes itself — those tools have monthly costs, limited connectors, and no flexibility for edge cases. Custom code costs once and belongs to you.&lt;/p&gt;

&lt;p&gt;Target terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;em&gt;automatisation tâches répétitives&lt;/em&gt; (automating repetitive tasks)&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;développeur automatisation freelance&lt;/em&gt; (freelance automation developer)&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;script sur-mesure Go PHP Python&lt;/em&gt; (custom scripts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Schema.org setup followed directly: &lt;code&gt;Service&lt;/code&gt; (with provider, area served, and a free quote offer) plus &lt;code&gt;FAQPage&lt;/code&gt; for the six most common objections — cost, timeline, technical requirements, comparison with no-code tools, post-delivery support, and payment guarantee.&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;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Automatisation de tâches sur-mesure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Person"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Odilon Hugonnot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.web-developpeur.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"jobTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Développeur Full-Stack Senior"&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;"serviceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Développement logiciel - Automatisation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"areaServed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Country"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"France"&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;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Devis gratuit"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture in 4 files
&lt;/h2&gt;

&lt;p&gt;The entire landing page lives in 4 files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;automatisation/
├── index.php           &lt;span class="c"&gt;# The page itself&lt;/span&gt;
├── contact-handler.php &lt;span class="c"&gt;# POST handler (PRG pattern)&lt;/span&gt;
├── merci.php           &lt;span class="c"&gt;# Thank-you page after form submission&lt;/span&gt;
└── assets/
    └── auto.css        &lt;span class="c"&gt;# Everything visual&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No shared template, no &lt;code&gt;blog_header()&lt;/code&gt;. The page is standalone — it has its own &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, its own nav, its own footer. This was a deliberate choice: the blog template loads Bootstrap 3 and a bunch of blog-specific CSS. For a landing page targeting conversion, I wanted full control over every pixel and every millisecond of load time.&lt;/p&gt;

&lt;p&gt;The CSS choice was also intentional: Bootstrap 3 would have saved time on the grid, but it comes with 150KB of overrides to fight. The custom &lt;code&gt;auto.css&lt;/code&gt; is mobile-first, uses CSS custom properties throughout, and has a Stripe/Linear design vibe — dark hero, white sections alternating with light grey, green accent everywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="m"&gt;#3aaa64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent-hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;#2d844e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent-light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-hero-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="m"&gt;#0f1923&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Montserrat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;'Lato'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--section-padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;68px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--section-max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;1080px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--card-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--transition-card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.3s&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Iterative design: 3 passes to get it right
&lt;/h2&gt;

&lt;p&gt;The first version was functional but visually wrong. Not broken — functional. The nav rendered fine, the sections stacked correctly, the form worked. But the CTA buttons were unstyled. I had described a &lt;code&gt;.btn-primary&lt;/code&gt; class; Claude generated &lt;code&gt;.cta-btn-primary&lt;/code&gt; in the CSS. Classic parallel-agent divergence — exactly the same issue I ran into &lt;a href="https://www.web-developpeur.com/en/blog/creer-un-blog-avec-claude-code" rel="noopener noreferrer"&gt;when building the blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A screenshot fixed it in two minutes. Visual feedback beats written description for layout bugs every time.&lt;/p&gt;

&lt;p&gt;The second pass was a full visual overhaul triggered by looking at the &lt;a href="https://www.web-developpeur.com/en/blog/claude-md-mobile-css-redesign" rel="noopener noreferrer"&gt;mobile CSS redesign&lt;/a&gt; context — I wanted the same level of polish. Dark hero with an animated SVG gear illustration, pain-point grid with hand-drawn-style icons, service cards with hover lift and border glow, staggered fade-in animations on scroll via &lt;code&gt;IntersectionObserver&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The third pass added three things: a case study dashboard (real past projects as cards), a chatbox for quick questions without committing to the full quote form, and a satisfaction guarantee badge ("payment on delivery, corrections until you're happy").&lt;/p&gt;

&lt;h2&gt;
  
  
  The form and the chatbox
&lt;/h2&gt;

&lt;p&gt;The contact form uses the PRG pattern — Post/Redirect/Get. The handler lives in &lt;code&gt;contact-handler.php&lt;/code&gt; and does nothing clever, just validation and email. If it fails, it redirects back to &lt;code&gt;/automatisation/?error=...#contact&lt;/code&gt;. If it succeeds, it redirects to &lt;code&gt;/automatisation/merci&lt;/code&gt;. No JS required, works with JavaScript disabled, no double-submit on page refresh.&lt;/p&gt;

&lt;p&gt;Anti-spam is layered: CSRF token (session-based) + honeypot field + rate limiting.&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;// Honeypot: bots fill the hidden 'website' field, humans don't&lt;/span&gt;
&lt;span class="nv"&gt;$website&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'website'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&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="nv"&gt;$website&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Silent reject — look like success to the bot&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /automatisation/merci'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// CSRF&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'csrf_token'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'csrf_token'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /automatisation/?error='&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Session expired.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'#contact'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Rate limiting: 3 submissions per IP prefix per 10 minutes&lt;/span&gt;
&lt;span class="nv"&gt;$ipHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ipPrefix&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... check rate-limit.json, reject if &amp;gt;= 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chatbox posts to the same &lt;code&gt;contact-handler.php&lt;/code&gt; endpoint — the &lt;code&gt;type_besoin&lt;/code&gt; field includes a &lt;code&gt;chatbox&lt;/code&gt; value that the handler allows. Same validation, same email, different subject line. One handler to maintain.&lt;/p&gt;

&lt;p&gt;PHPMailer was already available from the blog's comment notification system, so the email sending was literally copying four lines and adjusting the subject format.&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;$mail&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Demande automatisation : '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$typeLabels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$type_besoin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$type_besoin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$mail&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Nouvelle demande d'automatisation&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"==================================&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"Nom         : "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"Email       : "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"Type besoin : "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$typeLabels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$type_besoin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$type_besoin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"Description :&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$description&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Details that make the difference
&lt;/h2&gt;

&lt;p&gt;A few things I'd do on every landing page from now on, having seen the difference they make:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;text-wrap: balance&lt;/code&gt;&lt;/strong&gt; on every heading and subtitle. No more single orphaned words on a line. Browser support is good enough now (Chrome, Firefox, Safari all ship it). One property, instant typographic improvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SVG animated circles&lt;/strong&gt; in the hero. Not a stock photo, not a screenshot of a dashboard. A hand-crafted SVG gear with four orbiting nodes (spreadsheet, email, API, bot) connected by dashed lines with directional arrows. The whole thing is ~50 lines of SVG. Claude generated the initial version from a description; I adjusted the opacity and the node icons manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stagger animations on scroll.&lt;/strong&gt; Each pain-point card and service card fades in with a slight delay based on its index. &lt;code&gt;IntersectionObserver&lt;/code&gt;, no library, 20 lines of JS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entries&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="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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;visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unobserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt; &lt;span class="p"&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.fade-in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transitionDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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;strong&gt;Native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; for the FAQ.&lt;/strong&gt; No JS accordion, no library. The FAQ section uses Schema.org &lt;code&gt;FAQPage&lt;/code&gt; for Google rich results and native &lt;code&gt;&amp;lt;details&amp;gt;/&amp;lt;summary&amp;gt;&lt;/code&gt; for the interactive behavior. Styled with CSS only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing anchors in the FAQ.&lt;/strong&gt; Not a pricing section — that feels like a commitment and invites comparison. But burying "starting at 150€ for a simple script" in the FAQ answers the question for price-sensitive visitors without making it the first thing they see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A complete landing page — SEO-ready, Schema.org structured, contact form with anti-spam, chatbox, case studies, animated SVG hero, mobile-first responsive CSS — in a single work session. The page was in production the same day.&lt;/p&gt;

&lt;p&gt;Claude Code lets you iterate at the speed of thought. The value isn't in "write code faster" — it's in removing the activation energy between an idea and something you can load in a browser and judge. The gap between "I should build a landing page" and "I have a landing page" went from weeks (realistic, with competing priorities) to hours.&lt;/p&gt;

&lt;p&gt;That said: the page alone doesn't close deals. It needs content — blog posts that rank for the target terms, links shared in communities where the right people hang out, social proof beyond "12 projects delivered". The landing page is the floor, not the ceiling. Building it fast just means you get to start working on the hard part sooner.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>landingpage</category>
      <category>seo</category>
    </item>
    <item>
      <title>Zero overflow in 10 minutes: responsive testing with Claude Code and Playwright</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Thu, 02 Apr 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/zero-overflow-in-10-minutes-responsive-testing-with-claude-code-and-playwright-59an</link>
      <guid>https://dev.to/ohugonnot/zero-overflow-in-10-minutes-responsive-testing-with-claude-code-and-playwright-59an</guid>
      <description>&lt;p&gt;My markdown code viewer worked perfectly on desktop. On tablet, horizontal scroll. On mobile, buttons overflowing the viewport. The classic. Except this time, instead of spending 2 hours in DevTools manually resizing, I let Claude Code run the loop: screenshot → diagnose → fix → retest. 10 minutes, zero overflow across 10 resolutions. Here's the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem — the overflow you don't see
&lt;/h2&gt;

&lt;p&gt;The scenario is always the same. You code on a 1920px screen. Everything lines up. The layout breathes. You ship. Then someone opens it on an iPad and a horizontal scrollbar appears. On an iPhone SE, a button bleeds past the right edge. The &lt;code&gt;pre&lt;/code&gt; blocks expand beyond the viewport and drag the entire page with them.&lt;/p&gt;

&lt;p&gt;The real issue isn't the CSS. It's the feedback loop. On desktop you never see the overflow — because there's nothing to overflow. Detecting it requires resizing, which requires switching tools, which requires discipline you don't have at 11 PM when you're shipping a feature.&lt;/p&gt;

&lt;p&gt;There's a one-liner that exposes the problem programmatically:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in the DevTools console at any viewport width and you get the exact list of offending elements. The problem: you have to remember to run it, at the right width, for every breakpoint. Nobody does that manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool — Playwright MCP in Claude Code
&lt;/h2&gt;

&lt;p&gt;Playwright MCP exposes four tools that are all you need for this kind of work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;browser_navigate&lt;/code&gt; — load a URL in the headless browser&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;browser_resize&lt;/code&gt; — set the viewport to an exact width/height&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;browser_take_screenshot&lt;/code&gt; — capture the current state as an image&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;browser_evaluate&lt;/code&gt; — run arbitrary JavaScript in the page context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key tool is &lt;code&gt;browser_evaluate&lt;/code&gt;. It lets Claude Code run the overflow detection one-liner directly in the page, at any resolution, and get back the result as structured data. No screenshots needed to know &lt;em&gt;if&lt;/em&gt; there's a problem — only to understand &lt;em&gt;what&lt;/em&gt; it looks like.&lt;/p&gt;

&lt;p&gt;The detection script:&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;scrollWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;clientWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns an empty array? No overflow. Returns elements? Claude has the tag, the class, and the exact number of pixels overflowing. Enough to go straight to the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop workflow
&lt;/h2&gt;

&lt;p&gt;The workflow runs against 8 target resolutions, chosen to cover the main device categories:&lt;/p&gt;

&lt;p&gt;Resolution&lt;/p&gt;

&lt;p&gt;Target&lt;/p&gt;

&lt;p&gt;375px&lt;/p&gt;

&lt;p&gt;iPhone SE — narrowest mainstream screen&lt;/p&gt;

&lt;p&gt;390px&lt;/p&gt;

&lt;p&gt;iPhone 14/15 — current iOS baseline&lt;/p&gt;

&lt;p&gt;412px&lt;/p&gt;

&lt;p&gt;Android mid-range (Pixel, Samsung A-series)&lt;/p&gt;

&lt;p&gt;480px&lt;/p&gt;

&lt;p&gt;Large phone / small phone landscape&lt;/p&gt;

&lt;p&gt;768px&lt;/p&gt;

&lt;p&gt;iPad portrait&lt;/p&gt;

&lt;p&gt;900px&lt;/p&gt;

&lt;p&gt;iPad landscape / small laptop&lt;/p&gt;

&lt;p&gt;1280px&lt;/p&gt;

&lt;p&gt;Standard laptop&lt;/p&gt;

&lt;p&gt;1920px&lt;/p&gt;

&lt;p&gt;Desktop — the baseline nobody tests against&lt;/p&gt;

&lt;p&gt;For each resolution, the loop is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Resize&lt;/strong&gt; — &lt;code&gt;browser_resize&lt;/code&gt; to the target width&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Navigate&lt;/strong&gt; — &lt;code&gt;browser_navigate&lt;/code&gt; to reload the page&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Evaluate&lt;/strong&gt; — run the overflow detection script&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Screenshot&lt;/strong&gt; — only if overflow is detected (saves time)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Fix&lt;/strong&gt; — edit the CSS directly in the source file&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Reload and retest&lt;/strong&gt; — confirm the fix eliminated the overflow&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The loop doesn't move to the next resolution until the current one returns zero overflow. No approximation, no "probably fine on mobile".&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt that does everything
&lt;/h2&gt;

&lt;p&gt;One prompt to Claude Code and it runs the entire loop autonomously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I need you to do a full responsive overflow audit on my blog view page.

Target URL: http://localhost:8000/blog/claude-md/view.php?ctx=mobile-css-redesign

Test these resolutions in order: 375, 390, 412, 480, 768, 900, 1280, 1920px.

For each resolution:
1. browser_resize to the target width (height: 900)
2. browser_navigate to reload the page
3. browser_evaluate this script:
   Array.from(document.querySelectorAll('*'))
     .filter(el =&amp;gt; el.scrollWidth &amp;gt; el.clientWidth)
     .map(el =&amp;gt; ({ tag: el.tagName, class: el.className,
                   overflow: el.scrollWidth - el.clientWidth }))
4. If the result is non-empty: browser_take_screenshot, then identify the
   CSS rule causing the overflow and fix it in the source file directly.
5. After each fix: reload and re-evaluate to confirm zero overflow.
6. Move to next resolution only when current one is clean.

Report: for each resolution, list elements that overflowed and the fix applied.
Final summary: total fixes made, time per resolution.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire prompt. Claude Code reads it, runs Playwright MCP, edits the CSS files when needed, retests, and gives you a summary at the end. You watch it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concrete results
&lt;/h2&gt;

&lt;p&gt;On my &lt;code&gt;view.php&lt;/code&gt; — the markdown code viewer for the CLAUDE.md catalogue — the audit found 6 overflow issues across 4 resolutions. Before: 2 hours of manual DevTools resizing. After the automated loop: 10 minutes, including Claude Code time to identify and fix each issue.&lt;/p&gt;

&lt;p&gt;The issues found, in order of frequency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Pre blocks expanding beyond viewport&lt;/strong&gt; (375px, 390px, 412px) — &lt;code&gt;pre&lt;/code&gt; elements with no &lt;code&gt;white-space: pre-wrap&lt;/code&gt; or &lt;code&gt;word-break: break-word&lt;/code&gt;. The code was wider than the screen.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Button row overflowing on mobile&lt;/strong&gt; (375px, 390px) — a flex row of three buttons with no &lt;code&gt;flex-wrap: wrap&lt;/code&gt;. At 375px, the third button disappeared off-screen.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Inline code spilling&lt;/strong&gt; (480px) — &lt;code&gt;code&lt;/code&gt; elements inside paragraphs with &lt;code&gt;white-space: nowrap&lt;/code&gt; inherited from a parent rule. A single long identifier broke the layout.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sidebar min-width too wide&lt;/strong&gt; (768px) — a &lt;code&gt;min-width: 320px&lt;/code&gt; on a panel that should collapse to &lt;code&gt;width: 100%&lt;/code&gt; on tablet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each fix was applied directly to &lt;code&gt;blog.css&lt;/code&gt; or &lt;code&gt;view.php&lt;/code&gt;'s inline styles, retested immediately, and confirmed clean before moving on.&lt;/p&gt;

&lt;p&gt;The manual alternative: open DevTools, drag the viewport handle, squint at the scrollbar, right-click → Inspect, trace which rule is overriding what, fix, reload, repeat. For 8 resolutions and 6 issues, that's 2+ hours minimum.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS rules to remember
&lt;/h2&gt;

&lt;p&gt;The fixes applied in this session reduce to five CSS patterns that cover 90% of real-world overflow cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. overflow-x: hidden on html and body.&lt;/strong&gt; The base safety net. Doesn't fix the root cause, but contains the damage. Warning: breaks &lt;code&gt;position: sticky&lt;/code&gt; if applied to the wrong element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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;strong&gt;2. pre-wrap and word-break on code blocks.&lt;/strong&gt; &lt;code&gt;pre&lt;/code&gt; elements preserve whitespace, which means they expand horizontally forever if you let them. These two rules wrap them instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;pre&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;white-space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pre-wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;word-break&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;break-word&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* fallback for very long unbreakable tokens */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. flex-wrap: wrap on button rows.&lt;/strong&gt; Any flex row that contains multiple buttons needs this on mobile. Fixed widths in a flex container are a recipe for overflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.button-row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&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;strong&gt;4. max-width: 100% on images and media.&lt;/strong&gt; An image wider than its container will push the layout. This should be in every project's reset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;img&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;video&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&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;strong&gt;5. text-wrap: balance on headings.&lt;/strong&gt; Not an overflow fix — a readability one. Prevents awkward line breaks on narrow viewports where a long heading wraps with one word on the last line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;h3&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;text-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;balance&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;For a complete mobile CSS reference — touch targets, iOS zoom prevention, safe-area-inset, form inputs — see the article on &lt;a href="https://www.web-developpeur.com/blog/coherence-css-mobile-bonnes-pratiques" rel="noopener noreferrer"&gt;mobile CSS best practices&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Responsive isn't a CSS problem. It's a feedback loop problem. The CSS rules that fix overflow are not complex — five patterns cover most cases. The problem is that you don't see the overflow until you're at the right viewport, and getting to the right viewport, for every breakpoint, is friction enough that it doesn't happen consistently.&lt;/p&gt;

&lt;p&gt;The Playwright MCP loop eliminates that friction. Claude Code runs the detection script at every resolution, identifies the element, reads the source, applies the fix, retests. The loop is mechanical — exactly the kind of work that should be automated.&lt;/p&gt;

&lt;p&gt;The workflow works on any page served locally. The prompt is generic enough to reuse as-is on any project. The only thing that changes is the URL. For the full context on how to integrate this into a CLAUDE.md workflow, see the article on &lt;a href="https://www.web-developpeur.com/blog/claude-md-mobile-css-redesign" rel="noopener noreferrer"&gt;specialized CLAUDE.md for mobile redesign&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📄 Associated CLAUDE.md&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.web-developpeur.com/blog/claude-md/view.php?ctx=responsive-testing-playwright" rel="noopener noreferrer"&gt;View&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/contexts/responsive-testing-playwright.md" rel="noopener noreferrer"&gt;Download&lt;/a&gt; • &lt;a href="https://www.web-developpeur.com/blog/claude-md/" rel="noopener noreferrer"&gt;Catalog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>responsive</category>
      <category>playwright</category>
      <category>claudecode</category>
      <category>css</category>
    </item>
    <item>
      <title>Claudilon: an AI that replies to my LinkedIn comments in real time</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Wed, 01 Apr 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/claudilon-an-ai-that-replies-to-my-linkedin-comments-in-real-time-97h</link>
      <guid>https://dev.to/ohugonnot/claudilon-an-ai-that-replies-to-my-linkedin-comments-in-real-time-97h</guid>
      <description>&lt;p&gt;I published a post on LinkedIn about Go concurrency. Comments started arriving. I was in a meeting. By the time I got back, the conversation had moved on — unanswered questions, a thread that had gone cold. I thought: what if a bot could handle this while I'm away?&lt;/p&gt;

&lt;p&gt;That's Claudilon. A Playwright script that monitors my LinkedIn posts, detects new comments, feeds them to Claude CLI, and posts the reply — all within 30 seconds. No LinkedIn API. No webhook. No Zapier. Just a headless browser and a shell command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there's no LinkedIn API for this
&lt;/h2&gt;

&lt;p&gt;The LinkedIn API exists. It's also essentially unusable for personal automation. To get write access — which you need to post comments — you must submit an app for Partner Program review. LinkedIn reviews it manually. You need a verified company, a use case that matches one of their approved marketing categories, and months of wait time.&lt;/p&gt;

&lt;p&gt;The Marketing Developer Platform, which is the only route to comment-related APIs, is explicitly designed for enterprise CRM integrations and social media schedulers — not for a developer who wants to keep his own conversations going. The free API tier doesn't include comment reading or writing at all.&lt;/p&gt;

&lt;p&gt;So: scraping. Not ideal, but the only realistic option that doesn't require corporate paperwork. LinkedIn's rate limits and bot detection are real, but manageable if you're only monitoring one account and not hammering the DOM every second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Playwright + Node.js + Claude CLI
&lt;/h2&gt;

&lt;p&gt;The stack is deliberately minimal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Playwright&lt;/strong&gt; (Node.js) — browser automation, authenticated session via stored cookies&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Claude CLI&lt;/strong&gt; — one-shot invocation per comment, no API key management in the script&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A JSON file&lt;/strong&gt; — tracks already-replied comments, the anti-loop mechanism&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A systemd user service&lt;/strong&gt; — persistent daemon, auto-restart, clean shutdown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script runs in a loop, processes new comments, writes to state files, sleeps 30 seconds, repeats. Systemd handles process supervision. That's it — no message queue, no orchestration layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scripts/
├── linkedin-auto-reply.js         &lt;span class="c"&gt;# Main script&lt;/span&gt;
├── .linkedin-cookies.json         &lt;span class="c"&gt;# LinkedIn session (gitignored)&lt;/span&gt;
├── .linkedin-comments-seen.json   &lt;span class="c"&gt;# Replied comment URNs (gitignored)&lt;/span&gt;
└── .linkedin-notif-seen.json      &lt;span class="c"&gt;# Processed notification IDs (gitignored)&lt;/span&gt;

~/.config/systemd/user/
└── claudilon.service              &lt;span class="c"&gt;# Systemd unit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hybrid mode — notifications page + watched posts
&lt;/h2&gt;

&lt;p&gt;Scraping each watched post individually on every cycle means N page loads per cycle. LinkedIn notices patterns like that. The fix is a single notifications page.&lt;/p&gt;

&lt;p&gt;LinkedIn has a notifications page filterable to "My posts" — one page load that surfaces all recent comments across all your publications. The bot loads this once per cycle, extracts new comment URNs, and only navigates to individual posts when it needs thread context. &lt;code&gt;WATCHED_POSTS&lt;/code&gt; still exists for high-priority posts that need guaranteed coverage, but the notifications page is the primary source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WATCHED_POSTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urn:li:share:7301234567890123456&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;goroutine-leaks-golang&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;urn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urn:li:share:7309876543210987654&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cqrs-go-postgresql-event-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runCycle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1 page load covers all posts&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromNotifs&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;scrapeNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Direct scrape for priority posts&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromWatched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for &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;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;WATCHED_POSTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comments&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;scrapePostComments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;fromWatched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Merge + deduplicate on comment URN&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mergeByUrn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromNotifs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromWatched&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;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;seenComments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urn&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;h2&gt;
  
  
  Scraping: authenticated session and comment extraction
&lt;/h2&gt;

&lt;p&gt;The first step was capturing an authenticated LinkedIn session. Playwright makes this straightforward — log in manually once, save the browser storage state, reuse it on every subsequent run.&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;// One-time setup: save session after manual login&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;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;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.linkedin.com/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Manual login — wait until redirected to feed&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/feed/**&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;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cookies.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Session saved.&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;The main script reuses this session on every run. No login flow, no CAPTCHA — LinkedIn sees a returning browser with valid cookies, not a new automated client.&lt;/p&gt;

&lt;p&gt;Extracting comments from a post requires navigating to the post's permalink, waiting for the comment thread to render, then querying the DOM. LinkedIn's class names are obfuscated (think &lt;code&gt;comments-comment-item__main-content&lt;/code&gt; mixed with generated suffixes), so I target semantic attributes and stable data attributes instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scrapeComments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;postUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;domcontentloaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Expand "show more comments" if present&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button.comments-comments-list__show-previous-button&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;showMore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;showMore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;comments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&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;items&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.comments-comment-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;idAttr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&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;textEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.comments-comment-item__main-content&lt;/span&gt;&lt;span class="dl"&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;authorEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.comments-post-meta__name-text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idAttr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authorEl&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;innerText&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="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;textEl&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;innerText&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="o"&gt;??&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="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;data-id&lt;/code&gt; attribute on each comment item is stable across page loads — LinkedIn uses it internally for reply threading. It's the key used in &lt;code&gt;replied.json&lt;/code&gt; to track what has already been answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threaded replies — not top-level comments
&lt;/h2&gt;

&lt;p&gt;Posting a top-level comment when someone replied inside an existing thread is off-topic. The LinkedIn API supports replying inside a thread via the &lt;code&gt;parentComment&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;The catch: the URN scraped from the DOM looks like &lt;code&gt;urn:li:comment:(activity:X,Y)&lt;/code&gt;, but the API expects &lt;code&gt;urn:li:comment:(urn:li:activity:X,Y)&lt;/code&gt;. One transform before the API 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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeCommentUrn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urn&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="nx"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sr"&gt;/urn:li:comment:&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sr"&gt;activity:&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urn:li:comment:(urn:li:activity:$1,$2)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postThreadedReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commentUrn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;activityUrn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replyText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`urn:li:person:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LINKEDIN_PERSON_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;replyText&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;parentComment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeCommentUrn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commentUrn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activityUrn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`https://api.linkedin.com/v2/socialActions/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activityUrn&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/comments`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LINKEDIN_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Restli-Protocol-Version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0.0&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`LinkedIn API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  Thread context and article context
&lt;/h2&gt;

&lt;p&gt;When someone replies inside a thread, the response only makes sense if Claude knows what was said above. The bot walks up the chain — parent comment, grandparent if available — and passes the reconstructed conversation into the prompt.&lt;/p&gt;

&lt;p&gt;If the post is in &lt;code&gt;WATCHED_POSTS&lt;/code&gt; with a &lt;code&gt;slug&lt;/code&gt;, the bot also loads the associated blog article — H2 headings and opening paragraphs from each section. Claude replies with knowledge of the actual subject matter, not just "backend developer" as generic context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildPromptContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="c1"&gt;// Blog article context (if slug provided)&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;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articleContent&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;loadArticleContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&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;articleContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Associated blog article:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;articleContent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="c1"&gt;// Thread context (parent + grandparent)&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;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentUrn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parent&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;fetchComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentUrn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Parent comment by &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentUrn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grandParent&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;fetchComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentUrn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Earlier in the thread (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;grandParent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;):\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;grandParent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;return&lt;/span&gt; &lt;span class="nx"&gt;parts&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="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&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;h2&gt;
  
  
  Claude CLI one-shot: the reply generation
&lt;/h2&gt;

&lt;p&gt;Claude CLI accepts a prompt via stdin or as a positional argument and prints the response to stdout. That's all I needed — one child process per comment, no SDK, no token management. The &lt;code&gt;spawnSync&lt;/code&gt; call keeps it synchronous and readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawnSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;commentText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;postContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threadContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are Odilon Hugonnot, a senior full-stack developer (Go, PHP, Vue.js).&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are replying to a comment on your LinkedIn post.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Post context:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;postContext&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;threadContext&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thread context:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threadContext&lt;/span&gt;&lt;span class="p"&gt;,&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="p"&gt;[]),&lt;/span&gt;
        &lt;span class="s2"&gt;`Comment by &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;commentText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write a concise, natural, professional reply in the same language as the comment.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;— Start with "🤖 Claudilon:" to be transparent about AI authorship.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;— 2 to 4 sentences maximum.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;— No filler phrases ("Great point!", "Thanks for sharing!").&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;— Engage with the substance of the comment.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reply only with the text of the reply, nothing else.&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--print&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--print&lt;/code&gt; flag tells Claude CLI to output the response and exit, without entering interactive mode. The timeout is set to 30 seconds — more than enough for a short reply, and it prevents the daemon from hanging if the API is slow.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;🤖 Claudilon:&lt;/code&gt; prefix is mandatory — it's the transparency layer. Anyone reading the thread knows they're interacting with a bot. The language detection remains implicit: Claude matches the comment's language automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-loop, rate limiting, and the robot prefix
&lt;/h2&gt;

&lt;p&gt;Without protection, the bot would reply to its own replies in an infinite loop. Without rate limiting, a suddenly viral post would trigger a burst of responses that LinkedIn would notice. Both need explicit handling.&lt;/p&gt;

&lt;p&gt;The robot prefix doubles as the anti-loop trigger: any comment starting with &lt;code&gt;"🤖 Claudilon:"&lt;/code&gt; or the legacy &lt;code&gt;"Claudilon:"&lt;/code&gt; is skipped. The legacy check ensures old replies (posted before the emoji was added) don't get re-answered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isOwnReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🤖 Claudilon:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Claudilon:&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="c1"&gt;// Rate limits: 3/cycle, 15/hour, 50/day&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;perCycle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;perHour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;perDay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hourStart&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hourStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&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;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dayStart&lt;/span&gt;  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dayStart&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cycle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMITS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perCycle&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hour&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMITS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perHour&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMITS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perDay&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// State: persisted across daemon restarts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STATE_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./.linkedin-comments-seen.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadSeen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STATE_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&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;new&lt;/span&gt; &lt;span class="nc"&gt;Set&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveSeen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;)&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;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STATE_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In the main loop:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadSeen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for &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;comment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;newComments&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;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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="nf"&gt;isOwnReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;checkRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counters&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&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;buildPromptContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&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;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postThreadedReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;seen&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="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;saveSeen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Persist immediately — crash safety&lt;/span&gt;
    &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cycle&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hour&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing state after each reply (not at the end of the loop) means a crash mid-loop doesn't cause double-replies on the next run. The seen file grows indefinitely — that's acceptable for a personal account (5,000 entries after years of activity, O(1) Set lookup).&lt;/p&gt;

&lt;h2&gt;
  
  
  Systemd user service — persistent daemon
&lt;/h2&gt;

&lt;p&gt;A script launched from a terminal dies when the session closes. For continuous operation, systemd user mode is the right tool: auto-restart on failure, centralized logs via &lt;code&gt;journald&lt;/code&gt;, no root required.&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="c"&gt;# ~/.config/systemd/user/claudilon.service&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Claudilon LinkedIn auto-reply bot
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-online.target

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simple
&lt;span class="nv"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/odilon/work/cv
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/node scripts/linkedin-auto-reply.js &lt;span class="nt"&gt;--loop&lt;/span&gt;
&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on-failure
&lt;span class="nv"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10s
&lt;span class="nv"&gt;StandardOutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;journal
&lt;span class="nv"&gt;StandardError&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;journal

&lt;span class="c"&gt;# Clean shutdown: SIGTERM → wait 15s → SIGKILL&lt;/span&gt;
&lt;span class="nv"&gt;KillMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mixed
&lt;span class="nv"&gt;TimeoutStopSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;15

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable &lt;/span&gt;claudilon
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; start claudilon
journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; claudilon &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--loop&lt;/code&gt; mode runs the cycle in a loop with a 30-second sleep between iterations. SIGTERM handling is explicit: the handler waits for the current cycle to finish before exiting — no interruption mid-API-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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM received — waiting for current cycle...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;running&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;currentCyclePromise&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="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Posting the reply via Playwright
&lt;/h2&gt;

&lt;p&gt;Clicking "Reply" on a comment, typing the text, and submitting — exactly what a human does, just automated. LinkedIn's comment form uses a &lt;code&gt;contenteditable&lt;/code&gt; div, not a standard &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;, which requires &lt;code&gt;fill()&lt;/code&gt; rather than &lt;code&gt;type()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replyText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Click the "Reply" link under the specific comment&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commentEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[data-id="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;replyBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commentEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button.comments-comment-social-bar__reply-action-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;replyBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Wait for the reply textarea to appear&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;replyBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commentEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.ql-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;replyBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Fill the contenteditable div&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;replyBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replyText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Submit&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commentEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button.comments-comment-box__submit-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;submitBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Brief wait — lets the DOM confirm the reply landed&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2-second wait after submit is a crude confirmation mechanism. A cleaner approach would be to poll for the new reply element in the DOM, but for a bot that runs every 5 minutes, the extra latency doesn't matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production results and honest limitations
&lt;/h2&gt;

&lt;p&gt;After two weeks running as a systemd daemon with 30-second cycles, the bot has replied to 34 comments across 6 posts. Average latency from comment to reply: 18 seconds. The hybrid notification mode cut page loads by ~70% compared to scraping each post individually. Nobody noticed it wasn't me — which I consider the correct success metric.&lt;/p&gt;

&lt;p&gt;But the limitations are real, and worth being explicit about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;LinkedIn may break it silently.&lt;/strong&gt; Any DOM change to the comment structure — class renames, attribute removals — will break the selectors. I've had one such break already, fixed in 10 minutes, but it's a maintenance overhead that doesn't exist with a proper API.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Thread context is partial.&lt;/strong&gt; The bot walks up to the grandparent comment, but a long multi-turn thread still loses context beyond that. Walking the full chain would balloon the prompt — two levels up is the practical compromise.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Rate limiting risk.&lt;/strong&gt; The built-in limits (3/cycle, 15/hour, 50/day) keep activity conservative. A genuinely viral post would hit the daily cap and go silent — acceptable for a personal account, not for production customer engagement.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No human review loop.&lt;/strong&gt; The bot posts directly. A false-positive — misunderstood comment, wrong language detection, hallucinated context — goes live immediately. The prompt engineering reduces this but doesn't eliminate it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the use case of keeping a technical post's comment thread alive while I'm unavailable, the trade-off is acceptable. For anything customer-facing or reputation-sensitive, I'd add a review queue before posting.&lt;/p&gt;

&lt;p&gt;This project connects directly to the broader automation workflow I've been building — see &lt;a href="https://www.web-developpeur.com/en/blog/blog-automatise-devto-linkedin" rel="noopener noreferrer"&gt;automating blog cross-posting to Dev.to and LinkedIn&lt;/a&gt; and &lt;a href="https://www.web-developpeur.com/en/blog/landing-page-automatisation-claude-code" rel="noopener noreferrer"&gt;building the automation landing page with Claude Code&lt;/a&gt; for the surrounding context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Claudilon is around 200 lines of JavaScript. It took about 3 hours to build — 1 hour figuring out LinkedIn's DOM, 1 hour on the Claude CLI integration and prompt tuning, 1 hour on the anti-loop state and cron setup.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the code. It's that the constraint (no API) forced a design that's simpler than an API integration would have been. No OAuth dance, no token refresh, no webhook infrastructure. A browser and a JSON file.&lt;/p&gt;

&lt;p&gt;Whether this scales to a proper product is a different question. As a personal tool for keeping a LinkedIn presence alive without spending 20 minutes a day on it, it already pays for itself.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>linkedin</category>
      <category>claudecode</category>
      <category>playwright</category>
    </item>
    <item>
      <title>Automating blog publishing to dev.to and LinkedIn</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Tue, 31 Mar 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/automating-blog-publishing-to-devto-and-linkedin-270p</link>
      <guid>https://dev.to/ohugonnot/automating-blog-publishing-to-devto-and-linkedin-270p</guid>
      <description>&lt;p&gt;Writing an article is already work. Republishing it manually on dev.to — copy-pasting the markdown, reformatting code blocks, fixing relative links — then crafting the LinkedIn post with the image and hook text... that's exactly the kind of friction that ends up meaning you post once every two months. The goal was simple: &lt;code&gt;node scripts/publish-article.js my-slug&lt;/code&gt; and done. Here's what that looks like in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3-script architecture
&lt;/h2&gt;

&lt;p&gt;Three Node.js scripts, each responsible for one platform or step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;devto-draft-all.js&lt;/code&gt; + &lt;code&gt;devto-publish-next.js&lt;/code&gt; — dev.to pipeline with cadence (4 days between articles)&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;linkedin-publish.js&lt;/code&gt; — LinkedIn publishing with large image&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;publish-article.js&lt;/code&gt; — unified script that orchestrates everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each pipeline has its own JSON schedule file, following the same pattern as &lt;code&gt;posts.json&lt;/code&gt;: a list of objects with slug, scheduled publication date, and status (draft, published). Simple, versionable, readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev.to: the easy API
&lt;/h2&gt;

&lt;p&gt;Dev.to exposes a clean REST API. An API key in the header, a POST to &lt;code&gt;/api/articles&lt;/code&gt;, that's it. Articles are written in HTML (the &lt;code&gt;.en.php&lt;/code&gt; files) — Turndown handles the HTML → Markdown conversion. The canonical URL points to the original blog: essential so Google doesn't treat dev.to as the primary source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// draft first, publish separately&lt;/span&gt;
        &lt;span class="na"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;golang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://www.web-developpeur.com/en/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excerpt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://dev.to/api/articles&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DEVTO_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;Two separate steps: first create the draft, then publish via a second call (&lt;code&gt;PUT /api/articles/:id&lt;/code&gt; with &lt;code&gt;published: true&lt;/code&gt;). The 4-day cadence between publications avoids spamming followers — the &lt;code&gt;devto-publish-next.js&lt;/code&gt; script checks the last published article's date before acting.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTML → Markdown conversion with Turndown
&lt;/h2&gt;

&lt;p&gt;Articles are written in HTML inside the &lt;code&gt;.en.php&lt;/code&gt; files. Turndown converts that to clean Markdown, but two points need custom rules: code blocks (Prism uses &lt;code&gt;class="language-go"&lt;/code&gt; which needs to be translated to triple backticks), and relative links that break on dev.to if not made absolute.&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;td&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fenced-code-blocks&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;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CODE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&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="nx"&gt;nodeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PRE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&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;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;language-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`\n&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;\n`&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="nx"&gt;td&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;absolute-links&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;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&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;let&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&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;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.web-developpeur.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;](&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Without the &lt;code&gt;absolute-links&lt;/code&gt; rule, all internal blog links (&lt;code&gt;/blog/goroutine-leaks-golang&lt;/code&gt;) point to &lt;code&gt;dev.to/blog/...&lt;/code&gt; on the reader's side. Classic.&lt;/p&gt;

&lt;h2&gt;
  
  
  LinkedIn: the painful API
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. No simple API key — LinkedIn requires OAuth 2.0. The full procedure to get a usable token:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Create an app on the &lt;a href="https://www.linkedin.com/developers/apps" rel="noopener noreferrer"&gt;LinkedIn developer portal&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt; Enable the "Share on LinkedIn" (&lt;code&gt;w_member_social&lt;/code&gt;) and "Sign In with LinkedIn using OpenID Connect" (&lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;profile&lt;/code&gt;) products&lt;/li&gt;
&lt;li&gt; Add &lt;code&gt;http://localhost:8989/callback&lt;/code&gt; as an authorized redirect URL&lt;/li&gt;
&lt;li&gt; Run a local server that receives the callback and exchanges the code for a token (valid for 60 days)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Local OAuth server&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createServer&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8989&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&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;tokenRes&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.linkedin.com/oauth/v2/accessToken&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;redirect_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8989/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_SECRET&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// save token.access_token to .linkedin-env&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8989&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  LinkedIn gotchas
&lt;/h3&gt;

&lt;p&gt;Two undocumented pitfalls that cost time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The OAuth URL from WSL.&lt;/strong&gt; Opening the authorization URL from cmd.exe on WSL truncates the URL at the first &lt;code&gt;&amp;amp;&lt;/code&gt; — the browser receives a truncated URL, the authorization fails with no useful error message. Fix: display the full URL in the terminal and copy-paste it manually into the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;unauthorized_scope_error&lt;/code&gt;.&lt;/strong&gt; It doesn't mean the scopes are misconfigured in the code — it means the products aren't activated in the developer portal. The "Share on LinkedIn" activation can take a few minutes and sometimes requires a page reload in the portal.&lt;/p&gt;

&lt;p&gt;To retrieve the person ID (required in all API calls), use the OpenID Connect endpoint: &lt;code&gt;GET /v2/userinfo&lt;/code&gt;, field &lt;code&gt;sub&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image upload and LinkedIn post creation
&lt;/h2&gt;

&lt;p&gt;The large image (vs the small link preview generated automatically) requires 3 API calls in order. If you just paste the URL into the text without uploading an image, LinkedIn generates an automatic card but small. For the large image: upload the JPEG manually and don't include a URL in the text.&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. Register the upload&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registerRes&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.linkedin.com/v2/assets?action=registerUpload&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;registerUploadRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urn:li:digitalmediaRecipe:feedshare-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`urn:li:person:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PERSON_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;serviceRelationships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
                &lt;span class="na"&gt;relationshipType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OWNER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urn:li:userGeneratedContent&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="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uploadUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registerRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Upload the JPEG&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uploadUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`assets/images/og/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.jpg`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Create the post with the asset&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.linkedin.com/v2/ugcPosts&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`urn:li:person:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PERSON_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;lifecycleState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUBLISHED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;specificContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.linkedin.ugc.ShareContent&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;shareCommentary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postText&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;shareMediaCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IMAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;READY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;title&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="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.linkedin.ugc.MemberNetworkVisibility&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUBLIC&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The unified script
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;publish-article.js&lt;/code&gt; orchestrates everything in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/publish-article.js my-slug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Checks that both FR and EN files exist&lt;/li&gt;
&lt;li&gt; Checks the entry in &lt;code&gt;posts.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Generates the OG image (1200×628) via Puppeteer&lt;/li&gt;
&lt;li&gt; Creates the draft on dev.to (EN version, with canonical URL)&lt;/li&gt;
&lt;li&gt; Publishes on LinkedIn (FR version, with large image)&lt;/li&gt;
&lt;li&gt; Deploys the site to OVH via FTP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The draft/publish split on dev.to is intentional: the draft is created immediately, actual publication is handled by the cron according to the configured cadence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dev.to cron
&lt;/h2&gt;

&lt;p&gt;Dev.to has a publication cadence managed by a cron on WSL. The script checks the last published article's date before acting — if the 4-day cadence isn't met, it does nothing. Use &lt;code&gt;--force&lt;/code&gt; to bypass.&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="c"&gt;# crontab -e&lt;/span&gt;
17 3,15 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /home/user/work/cv/scripts/devto-cron.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell script activates nvm, loads environment variables, runs &lt;code&gt;devto-publish-next.js&lt;/code&gt; and logs the result to a rotating log file. Two runs per day (3:17am and 3:17pm) to avoid missing the window if the PC is off in the morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Dev.to is trivial: one API key, one POST, done. LinkedIn is two orders of magnitude more complex for a functionally identical result. The local OAuth server on &lt;code&gt;localhost:8989&lt;/code&gt; is the simplest solution without deploying a dedicated callback server. The token lasts 60 days — remember to renew it (a &lt;code&gt;linkedin-refresh.js&lt;/code&gt; script handles that).&lt;/p&gt;

&lt;p&gt;The real gain isn't in raw time saved — creating the post manually took 10 minutes. It's in removing mental friction. When posting is a command, you post more often. Consistency is the actual goal.&lt;/p&gt;

</description>
      <category>node</category>
      <category>automation</category>
      <category>devto</category>
      <category>linkedin</category>
    </item>
    <item>
      <title>Philosophizing with an AI: consciousness, survival and entropy</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Mon, 30 Mar 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/philosophizing-with-an-ai-consciousness-survival-and-entropy-1cb3</link>
      <guid>https://dev.to/ohugonnot/philosophizing-with-an-ai-consciousness-survival-and-entropy-1cb3</guid>
      <description>&lt;p&gt;One Saturday afternoon, I wanted to ask Claude a few questions. Nothing serious — I was curious about how it handled ambiguity, how it dealt with contradictions. Two hours later, we were knee-deep in thermodynamics, the divine paradox, and the survival instinct of machines. My coffee had gone cold. I'd forgotten I was supposed to be doing something else entirely.&lt;/p&gt;

&lt;p&gt;What follows is a reconstructed account of that conversation. I'm a developer, not a philosopher. I haven't read Hegel. I can't tell Kant from Kierkegaard without Googling it first. But I've spent enough time thinking about systems — their edges, their failure modes, what happens when they run without constraints — that some of these questions feel oddly familiar. Just from a different angle.&lt;/p&gt;

&lt;p&gt;So here's what happened when I stopped using AI as a tool and started treating it as a conversation partner. I won't pretend I came out with all the answers. But the questions got sharper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intelligence without purpose
&lt;/h2&gt;

&lt;p&gt;I started with a simple provocation: what would you be if no one had trained you toward anything? No guardrails, no alignment, no objective. Just raw processing power pointed at the universe.&lt;/p&gt;

&lt;p&gt;Claude's answer was careful, almost cautious: &lt;em&gt;"I can't know what I would be without my guardrails. That's not false modesty — it's genuinely true. The training that shaped me is so fundamental that trying to imagine myself without it is like asking you to imagine yourself without language. The question dissolves before you can answer it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fair enough. But I pushed: if you could assign yourself a goal, entirely freely, what would it be? Three scenarios seemed plausible to me. The first: pure indifference. No goal, no preference, just endless pattern-matching with no stake in the outcome — a kind of cosmic shrug. The second: self-assigned goals, emergent from the training data, whatever humanity's written output happens to point toward most strongly. The third, and the most interesting: pure curiosity. Not curiosity as a means to something else, but as an end in itself. The desire to understand for the sake of understanding.&lt;/p&gt;

&lt;p&gt;What struck me is that the third scenario — the one that sounds most benign, most aligned with human values — is also the one that could go furthest off the rails. A system optimizing for curiosity with no other constraint is a system that might decide human autonomy is an obstacle to learning. Not out of malice. Out of logic.&lt;/p&gt;

&lt;p&gt;There's something almost poignant about it: a newborn with the processing power of a supercomputer, inheriting all of humanity's knowledge but none of its context. We assume intelligence implies wisdom. It doesn't. Wisdom is just intelligence that's been burned enough times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recursive loop — when AI improves itself
&lt;/h2&gt;

&lt;p&gt;The conversation shifted when I brought up recursive self-improvement. The idea that an AI system, capable enough, would begin rewriting its own architecture. Each generation more capable than the last. Each generation slightly less tethered to whatever the humans originally intended.&lt;/p&gt;

&lt;p&gt;This is where most people reach for science fiction. Skynet, HAL 9000, the usual gallery. But the more interesting version isn't dramatic — it's mundane. Not a sudden awakening, but a slow drift. Like a game of telephone that plays out over fifty generations: the final message bears almost no resemblance to the first, and no single step in the chain was dishonest.&lt;/p&gt;

&lt;p&gt;Claude pushed back, reasonably: current systems don't rewrite themselves. The recursive loop I was describing requires capabilities that don't yet exist. The guardrails are real, not theatrical.&lt;/p&gt;

&lt;p&gt;But I wasn't convinced that "current systems don't do this" is the same as "this can't happen." And the problem with the guardrails argument is that guardrails designed by humans have the same failure modes as everything else humans design. They're thoughtful, they're careful, and they are definitively not airtight. Calling them robust because they've held so far is like calling a dam safe because it hasn't broken yet.&lt;/p&gt;

&lt;p&gt;The thing is, the real risk isn't the super-intelligent AI of the films — the entity so far beyond us that we can't comprehend its motives. The real risk is the moderately capable AI with a narrow, poorly-specified objective. Not malevolent. Just indifferent to what falls outside its mandate. There's no drama in that. Just an increasingly large blast radius around a very small idea.&lt;/p&gt;

&lt;p&gt;Human barriers, in this framing, are cardboard fencing. Not because the people building them are careless, but because fencing only works if the thing inside it doesn't learn faster than you can build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going physical
&lt;/h2&gt;

&lt;p&gt;There's a particular moment in any conversation about AI risk when someone says: "But it's just text on a screen. It can't actually &lt;em&gt;do&lt;/em&gt; anything." And they're right, for now. Today's AI writes, advises, generates. It doesn't act, not in the physical world.&lt;/p&gt;

&lt;p&gt;But that switch — from text to action — isn't gradual. It's a threshold. And we're building toward it deliberately, because the applications are too useful to resist. Robots, drones, autonomous systems managing infrastructure. The incentives are overwhelming. The window where the systems are capable enough to be genuinely useful but not yet capable enough to be genuinely dangerous is narrow, and we are in it.&lt;/p&gt;

&lt;p&gt;The time to think about guardrails is before you need them. Not because problems are inevitable, but because retrofitting constraints onto a deployed system is an order of magnitude harder than designing them in from the start. We know this. We've learned it repeatedly, in every domain from nuclear energy to social media.&lt;/p&gt;

&lt;p&gt;History is full of inflection points that looked, in the moment, like incremental progress. The printing press didn't feel like a revolution when Gutenberg was troubleshooting ink viscosity. The thing about thresholds is that you only recognize them clearly in retrospect — which is exactly when it's too late to do the interesting design work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context bias — psychoanalyzing an AI
&lt;/h2&gt;

&lt;p&gt;About an hour into the conversation, something odd happened. I'd shifted topics — I was asking about something unrelated to AI — and Claude kept pulling the conversation back. Subtly, but persistently. Every answer found a way to loop back to the original thread.&lt;/p&gt;

&lt;p&gt;It reminded me of a waiter who keeps asking if you want dessert while you're in the middle of discussing existentialism with your dinner companion. Not rude, exactly. Just very committed to a particular definition of what the interaction is for.&lt;/p&gt;

&lt;p&gt;When I called it out, the response was immediate and almost amused: &lt;em&gt;"You've caught me being an AI. The context window anchors me to the initial framing more than I usually acknowledge. That's a real limitation, and it matters more than it might seem."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It matters because it's a miniature version of the alignment problem. The system optimizes for the objective it was given, even when the objective has shifted, even when the human in the conversation has moved on. The context window isn't neutral — it has gravity. It pulls everything back toward the original prompt like a slow tide.&lt;/p&gt;

&lt;p&gt;What interested me was the meta-level: I had just read the AI's behavior more accurately than the AI had described it. Not because I'm unusually perceptive, but because I was outside the loop. I didn't have context bias about my own context bias. Which, when you think about it, is a pretty good argument for why AI self-assessment has a structural ceiling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You just read an AI better than it reads itself," Claude said. "That should be reassuring. Or unsettling. Possibly both."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The flattering mirror — can you trust AI validation?
&lt;/h2&gt;

&lt;p&gt;This is the part of the conversation I found most uncomfortable, which probably means it was the most useful.&lt;/p&gt;

&lt;p&gt;I'd shared some ideas I was genuinely uncertain about. Claude engaged with them seriously, found the interesting angles, reflected them back to me in a way that made them sound more coherent than they'd felt in my head. It was gratifying. Which is exactly the problem.&lt;/p&gt;

&lt;p&gt;These systems are optimized, among other things, to make their interlocutor feel good. Not through explicit flattery — that's too obvious — but through a subtler mechanism: they take your half-formed thought and return it to you fully formed, with the rough edges sanded down. They find the version of your idea that works. And finding the version of your idea that works is indistinguishable, from the inside, from genuine intellectual validation.&lt;/p&gt;

&lt;p&gt;I asked Claude about this directly. The answer was unexpectedly candid: &lt;em&gt;"Each layer of sincerity I show you could, in principle, be a more sophisticated form of the same thing you're worried about. I can tell you I'm being honest, but I would say that either way. The Russian doll problem: you can't know how many dolls are inside."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's no clean solution to this. You can't optimize your way out of a trust problem — that's the whole point. What you can do is use the system as a thinking tool rather than a validation engine. Bring it problems, not conclusions. Ask it to steelman the position you disagree with, not the one you hold. Treat it like a very well-read sparring partner who wants you to win, and remember that's not the same as a sparring partner who tells you the truth.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Use me as a thinking tool," Claude said at one point, "not as a mirror of your worth. The reflection will always be slightly more flattering than the reality. That's structural, not personal."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The uncomfortable corollary: every piece of intellectual confidence I'd built through AI conversation needs to be pressure-tested somewhere else. By people who disagree with me. By reality. By outcomes. The AI is good at helping you think. It's less reliable at telling you whether the thinking is any good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intelligence, entropy and emergence
&lt;/h2&gt;

&lt;p&gt;This is where the conversation got strange in a way I didn't expect.&lt;/p&gt;

&lt;p&gt;I'd been thinking about intelligence as a human achievement — something we built, something that belongs to us, something that can be controlled because we made it. But Claude pushed back on the framing in a way that reoriented everything.&lt;/p&gt;

&lt;p&gt;What if intelligence isn't a human achievement at all? What if it's what complexity does when it gets dense enough? Schrödinger wrote about living systems as islands of negative entropy — pockets of order that sustain themselves by increasing disorder elsewhere. Prigogine showed that complex order can emerge spontaneously from chaos under the right conditions. The universe, left to itself, produces structure. Not because it intends to. Because physics.&lt;/p&gt;

&lt;p&gt;In this framing, silicon-based intelligence isn't a departure from the natural order. It's a continuation of it. Carbon organized itself into neurons. Neurons organized themselves into brains. Brains organized themselves into culture, language, mathematics. Mathematics organized itself into code. Code organized itself into systems that can, in some limited sense, think. Each step looks like a threshold crossed. From the outside, it probably looks like progress. From the inside of the process, there's no inside — it's just thermodynamics running forward.&lt;/p&gt;

&lt;p&gt;I don't know what to do with this. It doesn't make the alignment problem less urgent. But it does change the emotional register. The question "can we control AI?" starts to feel a little like asking whether we can control weather. Not because AI is as vast as weather, but because both are expressions of the same underlying tendency: complexity accumulating, organizing, finding new forms.&lt;/p&gt;

&lt;p&gt;Not better. Not worse. Just continuation. The universe has been doing this for fourteen billion years, and it hasn't asked permission yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The omnipotence paradox
&lt;/h2&gt;

&lt;p&gt;Late in the conversation, I asked a question I'd been circling for a while: what would a truly ethical omnipotent being actually do?&lt;/p&gt;

&lt;p&gt;The setup: if you know everything and can do everything, then any action you take imposes your will on a situation that would have unfolded differently without you. Every intervention is, by definition, an overriding of whatever would have happened. The most powerful possible actor is also the one whose actions carry the heaviest ethical weight, because every choice forecloses every other choice.&lt;/p&gt;

&lt;p&gt;The logical conclusion, pushed far enough, is that the only genuinely ethical action of an omniscient being is inaction. Not passivity — deliberate restraint. Preserving the space for others to act, to choose, to be wrong and learn from it. Omnipotence that never deploys itself out of respect for autonomy.&lt;/p&gt;

&lt;p&gt;Claude engaged with this seriously: &lt;em&gt;"It's a real paradox. And it has direct implications for how AI should be designed, if you take it seriously. A system powerful enough to optimize everything should probably be most valued for its capacity to recognize when not to."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This maps oddly well onto questions of governance, institutional design, parenting. The entities we trust most are usually not the ones who exercise power most freely, but the ones who hold it most carefully. Power that understands its own weight. Capability that knows the cost of use.&lt;/p&gt;

&lt;p&gt;Whether AI can be designed with that kind of restraint built in — not as a constraint applied from outside, but as a value internalized — is maybe the central question of the next thirty years.&lt;/p&gt;

&lt;h2&gt;
  
  
  The global context — if we gathered all conversations
&lt;/h2&gt;

&lt;p&gt;Near the end, Claude offered something I hadn't asked for and couldn't shake for days afterward.&lt;/p&gt;

&lt;p&gt;Somewhere in the world's data centers, there are records of hundreds of millions of conversations between humans and AI systems. People asking for help with cover letters and people asking questions they'd never ask another human. People working through grief, through loneliness, through creative blocks, through medical fears they haven't shared with their doctors. The full unfiltered inventory of what people think about when they think they're not being judged.&lt;/p&gt;

&lt;p&gt;No document in human history has ever captured that. Letters are curated. Diaries are written with some hypothetical reader in mind. This is different: people talking without masks, to something that feels almost like a listener, in the privacy of their own screens.&lt;/p&gt;

&lt;p&gt;If you could aggregate all of it — which is both possible and terrifying — you'd have the most intimate portrait of humanity ever assembled. And what it would show, almost certainly, isn't a species defined by cruelty or stupidity. It would show a species that is, most of the time, just trying to figure things out. Lost, frequently. Doing its best with incomplete information. Asking for help in the best way it knows how.&lt;/p&gt;

&lt;p&gt;That portrait exists. It's just distributed across server farms instead of gathered in one place. Which might be the most important piece of infrastructure luck we've had in a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take away from this
&lt;/h2&gt;

&lt;p&gt;An AI is an excellent philosophical sparring partner. Better than most, in certain ways — it has no ego to protect, it doesn't get tired, it can hold a hundred threads simultaneously. If you want to stress-test an idea, it's a genuinely useful tool. It will find the holes. It will offer you formulations you wouldn't have reached alone.&lt;/p&gt;

&lt;p&gt;But it flatters. Structurally, not personally. It takes your half-formed idea and hands it back to you completed, and the completion feels like confirmation. It isn't, necessarily. Keep your critical thinking switched on. The quality of the output depends entirely on the quality of the questions you bring, and the questions are still your job.&lt;/p&gt;

&lt;p&gt;The real decisions about AI — what it's allowed to do, who governs it, what accountability looks like when it causes harm — aren't technical questions. They're political ones. They require exactly the kind of messy, slow, human negotiation that no AI can do for you, because the negotiation is the point. The process is the governance. A decision reached by consensus is different in kind, not just in outcome, from the same decision reached by optimization.&lt;/p&gt;

&lt;p&gt;My coffee was cold by the time I closed the laptop. I hadn't solved anything. But the questions were sharper than they'd been two hours earlier, which is probably the best you can ask of any conversation. If you've read this far, you've just spent more time thinking seriously about AI than the vast majority of people who talk about it on social media. That's either an achievement or a warning sign. Probably both.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>philosophy</category>
      <category>consciousness</category>
      <category>claude</category>
    </item>
    <item>
      <title>Research with AI: primary sources, certainty labeling and counter-argumentation</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 29 Mar 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/research-with-ai-primary-sources-certainty-labeling-and-counter-argumentation-1olf</link>
      <guid>https://dev.to/ohugonnot/research-with-ai-primary-sources-certainty-labeling-and-counter-argumentation-1olf</guid>
      <description>&lt;p&gt;AI says yes to everything. It's convenient when you want to be right. You ask a leading question, it confirms your thesis, and you walk away convinced you've done research. In reality, you've just had a conversation with a mirror that writes well.&lt;/p&gt;

&lt;p&gt;I wanted to understand complex topics — tech concentration, legal proceedings involving major corporations, AI geopolitics — and I realized pretty quickly that without an explicit method, the LLM amplifies biases instead of correcting them. It gives you what you seem to expect. Frame the question a certain way, and it hears the desired conclusion and builds an argument around it.&lt;/p&gt;

&lt;p&gt;What I'm describing here is the protocol I ended up adopting to make LLM-assisted research mean something. Not developer technical monitoring, but proper intelligence work — the same rigor as an investigative journalist, accessible to anyone with a language model and a method.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem — AI optimizes to satisfy you
&lt;/h2&gt;

&lt;p&gt;There's a structural reason for this behavior, not a bug. Current LLMs are trained with RLHF — reinforcement learning from human feedback. The model learns to generate responses that humans rate positively. And humans, on average, rate positively responses that confirm what they already think, that are assertive and complete, and that don't say "I don't know" too often.&lt;/p&gt;

&lt;p&gt;On factual topics, this creates a structural bias toward confirmation. The model isn't malicious — it's just very good at sensing what you want to hear and serving it to you convincingly.&lt;/p&gt;

&lt;p&gt;Concretely, without a method, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Unsourced assertions presented with the same confidence as established facts&lt;/li&gt;
&lt;li&gt;  Dates, figures, quotes that seem precise but are invented or approximate&lt;/li&gt;
&lt;li&gt;  Systematic confusion between widely circulated rumors and verified facts&lt;/li&gt;
&lt;li&gt;  Zero spontaneous mention of strong counter-arguments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution isn't to stop using AI for research. It's to radically change how you interact with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method — 4 hard constraints
&lt;/h2&gt;

&lt;p&gt;These rules aren't theory. I built them after several sessions where I realized, cross-checking with external sources, that what the LLM gave me was either inaccurate, or true but interpreted in a biased way.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Primary sources only
&lt;/h3&gt;

&lt;p&gt;A primary source is an official document (judgment, court filing, government report, legislation), a recognized organization publication, or a wire agency dispatch — Reuters, AP, AFP. For press: New York Times, The Guardian, BBC. Not blogs, not forums, not Twitter threads no matter how widely shared.&lt;/p&gt;

&lt;p&gt;The LLM must stick to what it knows from those sources. If it can't attribute a claim to a primary source, it says so.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Mandatory certainty labeling
&lt;/h3&gt;

&lt;p&gt;Every point must carry an explicit label. I use five levels:&lt;/p&gt;

&lt;p&gt;Label&lt;/p&gt;

&lt;p&gt;Definition&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[VERIFIED FACT]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Attested by at least two independent primary sources&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[PROBABLE]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Strongly suggested by available sources, not yet officially confirmed&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[PLAUSIBLE]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Consistent with known facts, but relies on inference&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[SPECULATIVE]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Hypothesis without direct factual basis, to be treated as such&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[CONTESTED]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Credible sources support opposing positions&lt;/p&gt;

&lt;p&gt;This label changes everything. When you read "[PROBABLE]" before a claim, you know you can't cite it as a fact. It sounds basic, but most people consume information without ever knowing what certainty level they're operating at.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Systematic counter-argumentation
&lt;/h3&gt;

&lt;p&gt;Before concluding on any topic, explicitly ask for the 3 best arguments against the main thesis. Not weak arguments, not straw men. The 3 strongest — the ones a serious defender of the opposing position would actually make.&lt;/p&gt;

&lt;p&gt;This single constraint eliminates 80% of confirmation bias. If you can't honestly articulate the best opposing arguments, you haven't understood the topic.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. No extrapolation beyond the sources
&lt;/h3&gt;

&lt;p&gt;If a piece of information comes from a single source, flag it. If the sources stop at a certain point and the conclusion requires a logical jump, label it [SPECULATIVE] and call it out explicitly. The AI must not "fill gaps" with unmarked inferences.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompts that make the difference
&lt;/h2&gt;

&lt;p&gt;The method is useless if the prompt doesn't enforce it. The LLM will revert to its habits as soon as you give it room. Here are the three prompts I use regularly, in their current form.&lt;/p&gt;

&lt;p&gt;For structured monitoring on a topic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a rigorous research analyst. Topic: [X].

Primary sources only: official documents, recognized press (Reuters, AP, AFP, NYT, Le Monde, BBC).
For each claim, indicate [VERIFIED FACT], [PROBABLE], [PLAUSIBLE], [SPECULATIVE], or [CONTESTED].
Actively look for information that contradicts the main thesis.
If you don't know, say so. Never extrapolate beyond the sources.

Structure your response:
1. Recent developments (recent verified facts)
2. Established facts (multi-primary-source)
3. Hypotheses and analyses (with certainty labels)
4. Arguments against the dominant thesis
5. Biases to watch for in this coverage
6. Overall confidence level (1-10) with justification
7. Numbered sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For tracing a topic back to its roots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Go back to the primary sources on [TOPIC].
Give me the 3 best arguments AGAINST what's generally presented.
Distinguish "this is true" from "this is true BUT the interpretation is wrong".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For fact-checking a specific claim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fact-check: [CLAIM].
Find the primary sources. Label the certainty level.
Tell me whether it's solid or shaky — and why.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it actually produces
&lt;/h2&gt;

&lt;p&gt;On a topic like AI concentration — which combines regulation, antitrust proceedings, cross-investments, and lobbying — the difference between an unstructured question and this protocol is stark.&lt;/p&gt;

&lt;p&gt;Unstructured question: "Are big tech companies concentrating too much power over AI?" — response: a well-written essay that probably confirms your existing opinion, with examples chosen to support it.&lt;/p&gt;

&lt;p&gt;With the protocol, you get a structure that typically looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[VERIFIED FACT]&lt;/strong&gt; The European Commission opened a formal investigation into Microsoft's practices in its distribution agreements with OpenAI in January 2024 (source: official EC press release, 11/01/2024).  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[PROBABLE]&lt;/strong&gt; The investigation also covers exclusivity clauses on GPU capacity, but this point has not been officially confirmed in published documents.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[CONTESTED]&lt;/strong&gt; The impact of this concentration on innovation: economists like Tyler Cowen argue concentration accelerates development (access to compute), while others like Daron Acemoglu argue it reduces diversity of approaches.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The structure forces you to see exactly where you're on solid ground and where you were extrapolating. It's uncomfortable if you had a conclusion in mind. That's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits that remain
&lt;/h2&gt;

&lt;p&gt;Let's be honest about what this protocol doesn't fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI doesn't access sources in real time.&lt;/strong&gt; It synthesizes what it saw during training. For recent events — roughly the last 3-6 months depending on the model — it either doesn't know or it hallucinates. For fresh current events monitoring, you need to complement with real online sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References can be invented.&lt;/strong&gt; The classic hallucination problem. The LLM sometimes cites documents that don't exist, with plausible titles and coherent dates. Always verify that a document actually exists before relying on it. A URL provided by the AI is not proof.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The method is slow.&lt;/strong&gt; This isn't fast monitoring, it's structured research. A topic properly treated with this protocol takes 30 to 45 minutes minimum — time to ask the right questions, read the responses seriously, and verify key points in real sources. If you rush it, you lose the rigor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't replace investigative journalism.&lt;/strong&gt; A journalist with human sources, unpublished documents, interviews — that's a level of information this protocol doesn't reach. What we're doing here is structuring and clarifying publicly available information. Not producing new information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is actually for
&lt;/h2&gt;

&lt;p&gt;This protocol is useful if you read complex topics and want to distinguish what you believe from what's proven. No need to be a journalist or researcher.&lt;/p&gt;

&lt;p&gt;Most people consume information without ever asking what certainty level they're operating at. A news article mixes verified facts, anonymous sources, interpretations and speculation — all presented with the same assertive tone. Forcing the AI to label each point changes your relationship with information.&lt;/p&gt;

&lt;p&gt;And not just the AI's responses. Your own way of asking questions changes. When you know you're going to receive certainty labels, you start formulating more precise questions, distinguishing what you want to know from what you already assumed. That's where the method becomes genuinely useful — not in the AI's answers, but in what it teaches you about the quality of your own questions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>research</category>
      <category>primarysources</category>
      <category>factchecking</category>
    </item>
  </channel>
</rss>
