<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl" media="all"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Roastidio.us Tagged with web</title>
<link>https://roastidio.us/tag/2691</link>
<atom:link href="https://roastidio.us/tagged_with/web" rel="self" type="application/rss+xml"></atom:link>
<description>Roastidio.us Tagged with web</description>
<item>
<title>URL parser tester</title>
<link>https://timothygu.me/urltester/</link>
<guid isPermaLink="false">TLANXrUKgNwD8wnno_fm5A27lsvZxKtuyZGaNA==</guid>
<pubDate>Sun, 07 Jun 2026 01:51:09 +0000</pubDate>
<description>Go net/url</description>
<content:encoded>&lt;dl&gt;
      &lt;div&gt;
        &lt;dt&gt;Go net/url&lt;/dt&gt;
        &lt;dt&gt;Go net/http&lt;/dt&gt;

        &lt;dd&gt;
          &lt;p&gt;
            This is Go&amp;#39;s built-in &lt;a href=&quot;https://golang.org/pkg/net/url/&quot;&gt;net/url package&lt;/a&gt;. The parser is based on &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc3986.html&quot;&gt;RFC 3986&lt;/a&gt;, with some compatibility fixes.
            We compiled it to WebAssembly using Go&amp;#39;s &lt;a href=&quot;https://github.com/golang/go/wiki/WebAssembly&quot;&gt;built-in compiler support&lt;/a&gt;.
            net/url itself does not support &lt;abbr&gt;IDNA&lt;/abbr&gt;, but the built-in &lt;a href=&quot;https://golang.org/pkg/net/http/&quot;&gt;net/http package&lt;/a&gt; does through the &lt;a href=&quot;https://pkg.go.dev/golang.org/x/net/idna&quot;&gt;golang.org/x/net/idna
            package&lt;/a&gt;.
            We added a &amp;quot;Go net/http&amp;quot; entry to emulate how net/http handles a URL.
          &lt;/p&gt;
          &lt;p&gt;
            The part mappings are as follows.
            Go&amp;#39;s url.URL object has multiple accessors for the path, query, and fragment components, each with a different level of encoded-ness;
            we choose the same fields/methods as the &lt;a href=&quot;https://pkg.go.dev/net/url#URL.String&quot;&gt;URL.String()&lt;/a&gt; serialization method.
          &lt;/p&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Property&lt;/th&gt;
                &lt;th&gt;&lt;a href=&quot;https://golang.org/pkg/net/url/#URL&quot;&gt;url.URL&lt;/a&gt; field/method&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;href&lt;/td&gt;&lt;td&gt;String()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;protocol&lt;/td&gt;&lt;td&gt;Scheme
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;User.Username()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;password&lt;/td&gt;&lt;td&gt;User.Password()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;td&gt;Hostname()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;port&lt;/td&gt;&lt;td&gt;Port()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pathname&lt;/td&gt;&lt;td&gt;Opaque || EscapedPath()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;search&lt;/td&gt;&lt;td&gt;RawQuery
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;EscapedFragment()
            &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;Node.js legacy&lt;/dt&gt;

        &lt;dd&gt;
          &lt;p&gt;
            This is the Node.js&amp;#39;s &lt;a href=&quot;https://nodejs.org/dist/latest-v17.x/docs/api/url.html#url_legacy_url_api&quot;&gt;legacy URL parser&lt;/a&gt;, written in JavaScript based on &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc3986.html&quot;&gt;RFC 3986&lt;/a&gt;.
            Developers have been encouraged to switch to the modern parser based on the &lt;abbr&gt;WHATWG&lt;/abbr&gt; URL Standard since version 8 (released in 2017).
            We copied the parser as well as some required internal Node.js source files and bundled them using &lt;a href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt; for use here.
          &lt;/p&gt;
          &lt;p&gt;
            Compared to the official Node.js binaries, the version presented here could have some slight differences when handling &lt;abbr&gt;IDNA&lt;/abbr&gt;.
            This is since Node.js generally uses ICU4C&amp;#39;s IDNA support (which is difficult to compile to WebAssembly), while here we have replaced it with a pure JavaScript implementation &lt;a href=&quot;https://github.com/jsdom/tr46&quot;&gt;tr46&lt;/a&gt;.
          &lt;/p&gt;
          &lt;p&gt;
            The part mappings are as follows:
          &lt;/p&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Property&lt;/th&gt;
                &lt;th&gt;&lt;a href=&quot;https://nodejs.org/dist/latest-v16.x/docs/api/url.html#url_legacy_urlobject&quot;&gt;Legacy urlObject&lt;/a&gt; property&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;href&lt;/td&gt;&lt;td&gt;href
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;protocol&lt;/td&gt;&lt;td&gt;protocol
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;auth.split(:)[0]
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;password&lt;/td&gt;&lt;td&gt;auth.split(:)[1…].join(:)
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;td&gt;hostname
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;port&lt;/td&gt;&lt;td&gt;port
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pathname&lt;/td&gt;&lt;td&gt;pathname
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;search&lt;/td&gt;&lt;td&gt;search
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;hash
            &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;Python urlparse&lt;/dt&gt;

        &lt;dd&gt;
          &lt;p&gt;
            This combines Python&amp;#39;s built-in &lt;a href=&quot;https://docs.python.org/3/library/urllib.parse.html&quot;&gt;urllib.parse&lt;/a&gt; module with Python library &lt;a href=&quot;https://docs.python-requests.org/en/master/&quot;&gt;Requests&lt;/a&gt;&amp;#39; &lt;code&gt;requote_uri()&lt;/code&gt; function.
            Python&amp;#39;s urllib uses various RFCs (primarily &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1738.html&quot;&gt;1738&lt;/a&gt; and &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1808.html&quot;&gt;1808&lt;/a&gt;) as the basis for its parser.
            To run Python in the browser, we use &lt;a href=&quot;https://pyodide.org/&quot;&gt;Pyodide&lt;/a&gt;, which compiles Python to WebAssembly.
          &lt;/p&gt;
          &lt;p&gt;
            Since the parser does no normalization by default, we use the popular Requests library&amp;#39;s &lt;code&gt;requote_uri()&lt;/code&gt; for parity with other parsers listed here.
            The part mappings are as follows:
          &lt;/p&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Property&lt;/th&gt;
                &lt;th&gt;&lt;a href=&quot;https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlparse&quot;&gt;ParseResult&lt;/a&gt; properties&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;href&lt;/td&gt;&lt;td&gt;geturl()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;protocol&lt;/td&gt;&lt;td&gt;scheme
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;username
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;password&lt;/td&gt;&lt;td&gt;password
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;td&gt;hostname
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;port&lt;/td&gt;&lt;td&gt;port
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pathname&lt;/td&gt;&lt;td&gt;path
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;search&lt;/td&gt;&lt;td&gt;query
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;fragment
            &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
          &lt;/table&gt;
          &lt;p&gt;
            Note: We ignore the &lt;code&gt;params&lt;/code&gt; part, which exists in &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1738.html&quot;&gt;RFC 1738&lt;/a&gt;
            but has no equivalent in other parsers and was removed in &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc3986&quot;&gt;RFC 3986&lt;/a&gt;.
          &lt;/p&gt;
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;Python requests&lt;/dt&gt;

        &lt;dd&gt;
          &lt;p&gt;
            This captures how Python library &lt;a href=&quot;https://docs.python-requests.org/en/master/&quot;&gt;Requests&lt;/a&gt;&amp;#39; deals with request URLs.
            Requests uses &lt;a href=&quot;https://urllib3.readthedocs.io/&quot;&gt;urllib3&lt;/a&gt;, which is based on &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc3986&quot;&gt;RFC 3986&lt;/a&gt;, to parse incoming URLs.
            However, it does some additional normalization on top of urllib3, such as applying the &lt;code&gt;requote_uri()&lt;/code&gt; function.
            &lt;abbr&gt;IDNA&lt;/abbr&gt; support in both Requests and urllib3 is provided through the &lt;a href=&quot;https://github.com/kjd/idna&quot;&gt;idna&lt;/a&gt; package.
            The part mappings are as follows:
          &lt;/p&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Property&lt;/th&gt;
                &lt;th&gt;&lt;a href=&quot;https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html#urllib3.util.Url&quot;&gt;urllib3.util.Url&lt;/a&gt; properties&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;href&lt;/td&gt;&lt;td&gt;url
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;protocol&lt;/td&gt;&lt;td&gt;scheme
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;auth.split(:)[0]
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;password&lt;/td&gt;&lt;td&gt;auth.split(:)[1…].join(:)
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;td&gt;host
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;port&lt;/td&gt;&lt;td&gt;port
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pathname&lt;/td&gt;&lt;td&gt;path
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;search&lt;/td&gt;&lt;td&gt;query
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;fragment
            &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;libcurl&lt;/dt&gt;

        &lt;dd&gt;
          &lt;p&gt;
            This is libcurl&amp;#39;s &lt;a href=&quot;https://everything.curl.dev/libcurl/url&quot;&gt;URL API&lt;/a&gt;.
            curl uses &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc3986.html&quot;&gt;RFC 3986&lt;/a&gt; as the basis for its parser,
            with some features of the &lt;abbr&gt;WHATWG&lt;/abbr&gt; URL Standard mixed in,
            as detailed on its &lt;a href=&quot;https://curl.se/docs/url-syntax.html&quot;&gt;URL Syntax&lt;/a&gt; documentation page.
            We created a simple C application &amp;quot;frontend&amp;quot; for the API and compiled it to WebAssembly using Emscripten.
            While curl does &lt;a href=&quot;https://curl.se/docs/url-syntax.html#idna&quot;&gt;support&lt;/a&gt; &lt;abbr&gt;IDNA&lt;/abbr&gt; using the libidn2 library,
            the functionality is not exposed through the URL API.
          &lt;/p&gt;
          &lt;p&gt;
            When parsing the URL, we use &lt;code&gt;CURLU_NON_SUPPORT_SCHEME&lt;/code&gt; and &lt;code&gt;CURLU_URLENCODE&lt;/code&gt; flags. When getting individual parts of the URL, we pass &lt;code&gt;0&lt;/code&gt; as flags.
            The part mappings are as follows:
          &lt;/p&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Property&lt;/th&gt;
                &lt;th&gt;&lt;a href=&quot;https://github.com/curl/curl/blob/curl-7_76_1/include/curl/urlapi.h#L53-L65&quot;&gt;CURLUPart&lt;/a&gt;&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;href&lt;/td&gt;&lt;td&gt;CURLUPART_URL
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;protocol&lt;/td&gt;&lt;td&gt;CURLUPART_SCHEME
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;CURLUPART_USER
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;password&lt;/td&gt;&lt;td&gt;CURLUPART_PASSWORD
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;td&gt;CURLUPART_HOST
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;port&lt;/td&gt;&lt;td&gt;CURLUPART_PORT
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pathname&lt;/td&gt;&lt;td&gt;CURLUPART_PATH
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;search&lt;/td&gt;&lt;td&gt;CURLUPART_QUERY
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;CURLUPART_FRAGMENT
            &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
          &lt;/table&gt;
          &lt;p&gt;
            Note: We ignore &lt;code&gt;CURLUPART_OPTIONS&lt;/code&gt;, &lt;a href=&quot;https://curl.se/docs/url-syntax.html#userinfo&quot;&gt;used&lt;/a&gt; for IMAP/POP3/SMTP &amp;quot;login options.&amp;quot; We also do not list &lt;code&gt;CURLUPART_ZONEID&lt;/code&gt; separately as it is included in &lt;code&gt;CURLUPART_HOST&lt;/code&gt;.
          &lt;/p&gt;
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;spec-url&lt;/dt&gt;
        &lt;dt&gt;spec-url absolute&lt;/dt&gt;

        &lt;dd&gt;
          &lt;p&gt;
            This is the JavaScript &lt;a href=&quot;https://github.com/alwinb/spec-url&quot;&gt;spec-url&lt;/a&gt; library, a reference implementation of Alwin Blok&amp;#39;s &lt;a href=&quot;https://alwinb.github.io/url-specification/&quot;&gt;URL Specification&lt;/a&gt;.
            Blok&amp;#39;s specification is designed to be a rephrasing of the &lt;abbr&gt;WHATWG&lt;/abbr&gt; URL Standard in more mathematical terms.
            We used &lt;a href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt; to generate a bundle for the library.
          &lt;/p&gt;
          &lt;p&gt;
            The actual parsing steps done by this tool is similar to the proposed &lt;a href=&quot;https://alwinb.github.io/url-specification/#concluding&quot;&gt;&lt;em&gt;parse-resolve-and-normalise&lt;/em&gt;&lt;/a&gt; algorithm in Blok&amp;#39;s specification.
            If no base URL is specified, &amp;quot;web-mode&amp;quot; is used, and the &amp;quot;force resolve&amp;quot; step in the algorithm is not done.
          &lt;/p&gt;
          &lt;p&gt;
            The &lt;em&gt;absolute&lt;/em&gt; variant optimizes for use of the input string as an &amp;quot;absolute URL,&amp;quot; at the risk of losing some information. Concretely, the absolute variant always &lt;a href=&quot;https://alwinb.github.io/url-specification/#reference-resolution&quot;&gt;forces&lt;/a&gt; the parser output.
            The absolute variant is closer to how the &lt;abbr&gt;WHATWG&lt;/abbr&gt; URL Standard operates, while the normal variant is closer to how Go&amp;#39;s net/url and Node.js&amp;#39; legacy parser operate.
          &lt;/p&gt;
          &lt;p&gt;
            The part mappings are derived from Blok&amp;#39;s specification:
          &lt;/p&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Property&lt;/th&gt;
                &lt;th&gt;Field/function&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;href&lt;/td&gt;&lt;td&gt;print()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;protocol&lt;/td&gt;&lt;td&gt;scheme
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;user
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;password&lt;/td&gt;&lt;td&gt;pass
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;td&gt;host
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;port&lt;/td&gt;&lt;td&gt;port
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pathname&lt;/td&gt;&lt;td&gt;root + (dirs &amp;amp;&amp;amp; (dirs.join(/) + /)) + file
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;search&lt;/td&gt;&lt;td&gt;query
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;hash
            &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;Rust url&lt;/dt&gt;

        &lt;dd&gt;
          &lt;p&gt;
            This is Rust&amp;#39;s &lt;a href=&quot;https://docs.rs/url/&quot;&gt;url&lt;/a&gt; crate, created by the Servo Project.
            It should be highly compatible with the &lt;abbr&gt;WHATWG&lt;/abbr&gt; URL Standard, with complete &lt;abbr&gt;IDNA&lt;/abbr&gt; support.
            We compiled it to WebAssembly using &lt;a href=&quot;https://rustwasm.github.io/wasm-pack/&quot;&gt;wasm-pack&lt;/a&gt; and &lt;a href=&quot;https://rustwasm.github.io/docs/wasm-bindgen/&quot;&gt;wasm-bindgen&lt;/a&gt;.
          &lt;/p&gt;
          &lt;p&gt;
            The part mappings are as follows:
          &lt;/p&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Property&lt;/th&gt;
                &lt;th&gt;&lt;a href=&quot;https://docs.rs/url/2.2.1/url/struct.Url.html&quot;&gt;url::Url&lt;/a&gt; method&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;href&lt;/td&gt;&lt;td&gt;as_str()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;protocol&lt;/td&gt;&lt;td&gt;scheme()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;username()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;password&lt;/td&gt;&lt;td&gt;password()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hostname&lt;/td&gt;&lt;td&gt;host_str()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;port&lt;/td&gt;&lt;td&gt;port()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;pathname&lt;/td&gt;&lt;td&gt;path()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;search&lt;/td&gt;&lt;td&gt;query()
              &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;fragment()
            &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;whatwg-url&lt;/dt&gt;

        &lt;dd&gt;
          This is the JavaScript &lt;a href=&quot;https://github.com/jsdom/whatwg-url&quot;&gt;whatwg-url&lt;/a&gt; library, designed from scratch to be a reference implementation of the &lt;abbr&gt;WHATWG&lt;/abbr&gt; URL Standard.
          We load the latest (nightly) bundle of the JavaScript &lt;a href=&quot;https://github.com/jsdom/whatwg-url&quot;&gt;whatwg-url&lt;/a&gt; library, which is also used for its own &lt;a href=&quot;https://jsdom.github.io/whatwg-url/&quot;&gt;URL Viewer&lt;/a&gt; program.
          This utility is, to a large extent, inspired by URL Viewer.
          URL part mapping is trivial, as whatwg-url exposes the same properties as a browser &lt;code&gt;URL&lt;/code&gt; object.
        &lt;/dd&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;dt&gt;your browser&lt;/dt&gt;

        &lt;dd&gt;
          For comparison, we also parse every URL with your own browser&amp;#39;s &lt;code&gt;URL&lt;/code&gt; class.
        &lt;/dd&gt;
      &lt;/div&gt;
    &lt;/dl&gt;</content:encoded>
</item>
<item>
<title>Is there a power law of category use?</title>
<link>https://jamesg.blog/2026/06/04/is-there-a-power-law-of-category-use</link>
<guid isPermaLink="false">Q4ftdvKfzowAewItoiKSdUyT3-QCSBzeCSWdKw==</guid>
<pubDate>Fri, 05 Jun 2026 11:12:54 +0000</pubDate>
<description>I have been thinking a lot about categories over the last week or so. It started by Thomas sharing the work he has been doing to build a category index page, whose design I love. I started to realise that I like to put a lot of my blog posts in one of few categories: moments of joy go in one category, slices of life go in another, posts about coffee go in another. As a result of all of this thinking about categories, I fixed my category index page, which has been broken for a few months. I ad...</description>
<content:encoded>&lt;p&gt;I have been thinking a lot about categories over the last week or so. It started by &lt;a href=&quot;https://vanderwal.net/random/catlist.php&quot;&gt;Thomas sharing the work he has been doing to build a category index page&lt;/a&gt;, whose design I love. I started to realise that I like to put a lot of my blog posts in one of few categories: moments of joy go in one category, slices of life go in another, posts about coffee go in another.&lt;/p&gt;&lt;p&gt;As a result of all of this thinking about categories, I fixed my &lt;a href=&quot;https://jamesg.blog/categories&quot;&gt;category index page&lt;/a&gt;, which has been broken for a few months. I added a count of how many posts are in each category so I could see how I use categories. I noticed what I had felt from how I use categories on a day-to-day basis was true in the data: there is a heavy concentration of posts in a few categories, and then a long tail of other categories.&lt;/p&gt;&lt;p&gt;I created a chart in Google Sheets to visualise the trend, charting all 63 categories I use at the time of writing this post:&lt;/p&gt;&lt;figure&gt;&lt;picture&gt;&lt;img src=&quot;https://editor.jamesg.blog/content/images/2026/06/categorychart.png&quot; alt=&quot;A chart showing a power law trend in the use of categories on my website. Almost all of my blog posts are categorised in one of the top 10 categories. &quot; title=&quot;&quot;/&gt;&lt;/picture&gt;&lt;div&gt;ALT&lt;div&gt;A chart showing a power law trend in the use of categories on my website. Almost all of my blog posts are categorised in one of the top 10 categories. &lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;Thomas confirmed the trend applies to his website, too, which has me interested in whether others, too, have long tails of categories.&lt;/p&gt;&lt;p&gt;I think there are a few possible reasons for why my category use is in this way.&lt;/p&gt;&lt;p&gt;First, I intentionally like to keep the number of categories I use to a minimum. I feel that expressiveness is an attribute of a category which can vary. A category can list posts that are part of a series, like my IndieWeb Carnival category, or be an umbrella for a whole genre of writing I do, like my Stories category (which include some of my poetic prose). Indeed, I think categories serve both an organisational and an editorial use on a page: they help me find my posts, and also add a label of where I think the post fits into the rest of the website.&lt;/p&gt;&lt;p&gt;Second, I don’t remember a lot of the categories I have created. It’s easier to remember fewer than many things.&lt;/p&gt;&lt;p&gt;Third, my category use is perhaps influenced by the fact that the tool I use to publish blog posts, Ghost, only has a limited history of the categories I have used.&lt;/p&gt;&lt;p&gt;While I try to minimise the number of categories I use, I do create new ones when I feel like having a page that lists posts in the category I want to create would be useful. For example, I recently created a category called Bear Blog Carnival so I can aggregate all my contributions to that event; I have a category called Artemis that groups all my blog posts in that project; I have a category called “New Things” that followed a challenge I did to try and do a new thing every day for a week.&lt;/p&gt;&lt;p&gt;An interesting side effect of having categories is that they remind me of different projects I have done, like my &lt;a href=&quot;https://jamesg.blog/steampunk-post&quot;&gt;guest writing for a Scottish coffee roaster&lt;/a&gt;, and my two &lt;a href=&quot;https://jamesg.blog/weeknotes&quot;&gt;separate one-off attempts to try week notes&lt;/a&gt;. It was also interesting to see that I have published almost an equal number of posts in the IndieWeb and Life categories. This matches what I think about my writing: I love writing about technology, but I love writing about the world around me too.&lt;/p&gt;&lt;p&gt;If you do any analysis on how you use categories, I’d love to read what you write! &lt;a href=&quot;https://jamesg.blog/email&quot;&gt;You can send me an email&lt;/a&gt; with any ideas or thoughts you have.&lt;/p&gt;&lt;a href=&quot;https://jamesg.blog/categories&quot;&gt;category index page&lt;/a&gt;&lt;a href=&quot;https://jamesg.blog/email&quot;&gt;You can send me an email&lt;/a&gt;&lt;a href=&quot;https://jamesg.blog/steampunk-post&quot;&gt;guest writing for a Scottish coffee roaster&lt;/a&gt;&lt;a href=&quot;https://jamesg.blog/weeknotes&quot;&gt;separate one-off attempts to try week notes&lt;/a&gt;&lt;a href=&quot;https://vanderwal.net/random/catlist.php&quot;&gt;Thomas sharing the work he has been doing to build a category index page&lt;/a&gt;</content:encoded>
</item>
<item>
<title>Hiding a list with no items in CSS</title>
<link>https://jamesg.blog/2026/06/04/hiding-a-list-with-no-items-in-css</link>
<guid isPermaLink="false">CSZpW3yRu6sJEbw0FtB4dOem4LmG0fpxk0Q3Bg==</guid>
<pubDate>Fri, 05 Jun 2026 11:12:54 +0000</pubDate>
<description>The Artemis reading interface consists of a section tag for each day for which there are blog posts to show. Each section contains a h2 denoting the date the posts were published, and a ul that lists each post published on that date. Here is what the interface looks like: The Artemis interface showing three posts, one published on May 26th and two published on May 25th. ALTThe Artemis interface showing three posts, one published on May 26th and two published on May 25th. There are some niche ...</description>
<content:encoded>&lt;p&gt;The &lt;a href=&quot;https://artemis.jamesg.blog&quot;&gt;Artemis&lt;/a&gt; reading interface consists of a &lt;code&gt;section&lt;/code&gt; tag for each day for which there are blog posts to show. Each &lt;code&gt;section&lt;/code&gt; contains a &lt;code&gt;h2&lt;/code&gt; denoting the date the posts were published, and a &lt;code&gt;ul&lt;/code&gt; that lists each post published on that date.&lt;/p&gt;&lt;p&gt;Here is what the interface looks like:&lt;/p&gt;&lt;figure&gt;&lt;picture&gt;&lt;img src=&quot;https://editor.jamesg.blog/content/images/2026/06/artemisinterface.png&quot; alt=&quot;The Artemis interface showing three posts, one published on May 26th and two published on May 25th.&quot; title=&quot;&quot;/&gt;&lt;/picture&gt;&lt;div&gt;ALT&lt;div&gt;The Artemis interface showing three posts, one published on May 26th and two published on May 25th.&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;There are some niche scenarios where a list may end up with no items but still retain the heading (and, as I later discovered, some practical scenarios too).&lt;/p&gt;&lt;p&gt;This got me thinking: could I write a CSS rule that would hide an entire section if the list in the section had no child &lt;code&gt;li&lt;/code&gt; elements? I first looked at the &lt;code&gt;:empty&lt;/code&gt; CSS psuedo-class (&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:empty&quot;&gt;MDN docs for :empty&lt;/a&gt;), but &lt;code&gt;:empty&lt;/code&gt; is sensitive to white-space. If a &lt;code&gt;ul&lt;/code&gt; had no child elements but a single space between its opening and closing tags, &lt;code&gt;:empty&lt;/code&gt; would not apply.&lt;/p&gt;&lt;p&gt;With that said, with the &lt;code&gt;:has&lt;/code&gt; selector I can achieve the effect I want!&lt;/p&gt;&lt;p&gt;I ended up with the following rule:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;section&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;has&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;list-entries&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;li&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This rule says that a &lt;code&gt;section&lt;/code&gt; should be hidden if it does not have an element with the class &lt;code&gt;.list-entries&lt;/code&gt; (which is a &lt;code&gt;ul&lt;/code&gt;) that contains at least one &lt;code&gt;li&lt;/code&gt;. &lt;em&gt;NB: If you use a similar rule, you may want to make this only apply in a particular context, i.e. your &lt;code&gt;main&lt;/code&gt; tag.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;One nice effect of this rule is that, because it is CSS, the rule will be applied as the page changes. For example, Artemis has a feature to hide posts. When you press the &lt;code&gt;hide&lt;/code&gt; button, an element appears saying a post is hidden and shows an &lt;code&gt;undo&lt;/code&gt; button that hides after a few seconds. This message shows using JavaScript. With the CSS rule, if you hide all posts, when the last &lt;code&gt;undo&lt;/code&gt; message goes away, the whole section can disappear without having to use JavaScript to find the parent &lt;code&gt;section&lt;/code&gt; and delete it. Indeed, the less JavaScript I have to write, the better.&lt;/p&gt;&lt;p&gt;Here is how the effect looks:&lt;/p&gt;&lt;p&gt;When the last post in the &lt;code&gt;ul&lt;/code&gt; in a &lt;code&gt;section&lt;/code&gt; is hidden, the whole &lt;code&gt;section&lt;/code&gt;, including the heading, disappears. When the page is refreshed, the section will also be hidden because the back-end will have been notified that the user wants to hide the post. &lt;/p&gt;&lt;p&gt;I thought I&amp;#39;d document this in case it is helpful for anyone else!&lt;/p&gt;&lt;a href=&quot;https://artemis.jamesg.blog&quot;&gt;Artemis&lt;/a&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:empty&quot;&gt;MDN docs for :empty&lt;/a&gt;</content:encoded>
</item>
<item>
<title>JUnited 2026</title>
<link>https://jamesg.blog/2026/06/01/junited-2026</link>
<guid isPermaLink="false">6zoDOh1sammxbgbEbLtYvAmoqGMOUO7OhIfzpg==</guid>
<pubDate>Fri, 05 Jun 2026 11:12:54 +0000</pubDate>
<description>In recent weeks, I have been talking with a lot of people about personal websites. In so many of my discussions, I mention that one of the reasons I love coming back to my personal website is the community around the indie web: people all over the world sharing what interests them: slices of life, hopes and dreams, tutorials on how to do something, and more. Websites aren’t islands. Websites are houses in a town. That brings me to JUnited. I first participated in JUnited, a challenge that i...</description>
<content:encoded>&lt;p&gt;In recent weeks, I have been talking with a lot of people about personal websites. In so many of my discussions, I mention that one of the reasons I love coming back to my personal website is the community around the indie web: people all over the world sharing what interests them: slices of life, hopes and dreams, tutorials on how to do something, and more. Websites aren’t islands. Websites are houses in a town.&lt;/p&gt;&lt;p&gt;That brings me to &lt;a href=&quot;https://robertbirming.com/junited-blog-love-letter/&quot;&gt;JUnited&lt;/a&gt;. &lt;a href=&quot;https://jamesg.blog/2024/06/07/junited-2024&quot;&gt;I first participated in JUnited&lt;/a&gt;, a challenge that invites participants to share links to “blog posts or blogs you think deserve more love,” in 2024. I found out about the challenge via &lt;a href=&quot;https://notes.jeddacp.com/junited2024/&quot;&gt;JCProbably&lt;/a&gt;. I missed last year – I think I forgot the challenge was going on! – but I was delighted this morning to wake up to a post by &lt;a href=&quot;https://kiko.io/post/Junited-2026/&quot;&gt;Kristof that mentioned the challenge&lt;/a&gt;. Inviting people to share links to blog posts they enjoy, JUnited so wonderfully embodies the essence of the indie web community.&lt;/p&gt;&lt;p&gt;With that in mind, I am excited to participate again in the challenge.&lt;/p&gt;&lt;p&gt;For each day in the month of June, I will update this post with a link to a blog post I really enjoyed reading. I invite you to peruse the links and see what interests you. And, if you have a website, I invite you to participate in the challenge, too.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://britthub.co.uk/a-love-letter-to-creative-spaces/&quot;&gt;A Love Letter To Creative Spaces&lt;/a&gt; by Britt&lt;/li&gt;&lt;li&gt;&amp;quot;there’s a sort of Gresham&amp;#39;s Law of developers (vs users): developers are willing to use a forum with a lot of users in it, but users aren’t willing to use a forum with a lot of developer-speak. [...]&amp;quot; by &lt;a href=&quot;https://tantek.com/2024/035/t1/greshams-law-developers-users-jargon&quot;&gt;Tantek&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://musings.martyn.berlin/to-have-a-moral-stance-on-ai-is-to-be-an-outcast-and-it-sucks&quot;&gt;To have a moral stance on AI is to be an outcast, and it sucks.&lt;/a&gt; by Martyn&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://bojidar-bg.dev/blog/2026-06-02-notetaking/&quot;&gt;A note on note-taking&lt;/a&gt; by Bojidar&lt;/li&gt;&lt;/ul&gt;&lt;a href=&quot;https://bojidar-bg.dev/blog/2026-06-02-notetaking/&quot;&gt;A note on note-taking&lt;/a&gt;&lt;a href=&quot;https://britthub.co.uk/a-love-letter-to-creative-spaces/&quot;&gt;A Love Letter To Creative Spaces&lt;/a&gt;&lt;a href=&quot;https://jamesg.blog/2024/06/07/junited-2024&quot;&gt;I first participated in JUnited&lt;/a&gt;&lt;a href=&quot;https://kiko.io/post/Junited-2026/&quot;&gt;Kristof that mentioned the challenge&lt;/a&gt;&lt;a href=&quot;https://musings.martyn.berlin/to-have-a-moral-stance-on-ai-is-to-be-an-outcast-and-it-sucks&quot;&gt;To have a moral stance on AI is to be an outcast, and it sucks.&lt;/a&gt;&lt;a href=&quot;https://notes.jeddacp.com/junited2024/&quot;&gt;JCProbably&lt;/a&gt;&lt;a href=&quot;https://robertbirming.com/junited-blog-love-letter/&quot;&gt;JUnited&lt;/a&gt;&lt;a href=&quot;https://tantek.com/2024/035/t1/greshams-law-developers-users-jargon&quot;&gt;Tantek&lt;/a&gt;</content:encoded>
</item>
<item>
<title>Distinguishing user and system content</title>
<link>https://jamesg.blog/2026/06/01/distinguishing-user-and-system-content</link>
<guid isPermaLink="false">VUaOxWMSI_bJqWyk3xVv68-Lx3XGYcUli6hpBQ==</guid>
<pubDate>Fri, 05 Jun 2026 11:12:54 +0000</pubDate>
<description>When I was designing the inline message to indicate a link in a user’s Artemis reader has been flagged as malicious, I intentionally designed the system message and state to be distinguished from the user-set author name. Here is what the final design looked like: The Artemis feed showing three entries. The first entry has been flagged as suspicious, and shows a clear, distinguished label indicating this, as well as being indented with a coloured border. This creates contrast between the tw...</description>
<content:encoded>&lt;p&gt;When I was designing the &lt;a href=&quot;https://jamesg.blog/2026/06/01/flagging-suspicious-websites-in-artemis&quot;&gt;inline message to indicate a link in a user’s Artemis reader has been flagged as malicious&lt;/a&gt;, I intentionally designed the system message and state to be distinguished from the user-set author name.&lt;/p&gt;&lt;p&gt;Here is what the final design looked like:&lt;/p&gt;&lt;figure&gt;&lt;picture&gt;&lt;img src=&quot;https://editor.jamesg.blog/content/images/2026/06/artemisflaggedlink.png&quot; alt=&quot;The Artemis feed showing three entries. The first entry has been flagged as suspicious, and shows a clear, distinguished label indicating this, as well as being indented with a coloured border. This creates contrast between the two posts below that have not been flagged which appear without the indent or suspicious link label.&quot; title=&quot;&quot;/&gt;&lt;/picture&gt;&lt;div&gt;ALT&lt;div&gt;The Artemis feed showing three entries. The first entry has been flagged as suspicious, and shows a clear, distinguished label indicating this, as well as being indented with a coloured border. This creates contrast between the two posts below that have not been flagged which appear without the indent or suspicious link label.&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;The message “This link has been flagged as suspicious. Learn why.” appears above the article title. This post that has been flagged as suspicious is visually indented compared to other posts. Both of these indicators are set by the system.&lt;/p&gt;&lt;p&gt;Because Artemis is designed to be text-heavy, I could have instead designed the feature to add a tag next to an author’s name, like this:&lt;/p&gt;&lt;figure&gt;&lt;picture&gt;&lt;img src=&quot;https://editor.jamesg.blog/content/images/2026/06/inlineflagexample.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/picture&gt;&lt;/figure&gt;&lt;p&gt;Because the link already goes to a wall that indicates to the user the link is malicious, this technically could have sufficed, but it wouldn&amp;#39;t be a good implementation. This implementation with the “flagged as malicious” label right next to the author name would set a bad precedent for including system messages immediately next to author names, which are customisable by the user.&lt;/p&gt;&lt;p&gt;When I designed the &lt;a href=&quot;https://jamesg.blog/2026/02/23/artemis-via&quot;&gt;“via” feature,&lt;/a&gt; for example, I put the “(via)” label in italics. Because users cannot add italics to author names, because the “via” feature only indicates a link points to another website, and because users set their own author names, I thought this would be a sufficient distinction. But for something related to security and integrity – a link being flagged as malicious – a clear visual distinction that cannot be confused with anything else was absolutely essential.&lt;/p&gt;&lt;p&gt;More broadly, having a system message in the same place as a user-controlled field is confusing at best, and potentially malicious at worst.&lt;/p&gt;&lt;p&gt;I thought I’d write this down since it isn’t necessarily obvious, but something that I think about when I am designing features. There are also other real-world examples of this pattern. Signal, for example, adds a little blue checkmark next to the “Note to Self” chat that is specifically for messaging yourself. This indicates that the user is an official chat.&lt;/p&gt;&lt;figure&gt;&lt;picture&gt;&lt;img src=&quot;https://editor.jamesg.blog/content/images/2026/06/signalnotetoself.png&quot; alt=&quot;The Signal chat for the official &amp;quot;Note to Self&amp;quot; feature shows a blue checkmark next to the name &amp;quot;Note to Self&amp;quot;.&quot; title=&quot;&quot;/&gt;&lt;/picture&gt;&lt;div&gt;ALT&lt;div&gt;The Signal chat for the official &amp;quot;Note to Self&amp;quot; feature shows a blue checkmark next to the name &amp;quot;Note to Self&amp;quot;.&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;p&gt;The official Signal chat also has the same checkmark.&lt;/p&gt;&lt;p&gt;In both the “Note to Self” and official Signal chat, the author name at the top of the chat has an “Official chat” label that is set by the system. This creates a clear distinction between chats between users and chats between system users (the Note to Self feature, the official Signal account):&lt;/p&gt;&lt;figure&gt;&lt;picture&gt;&lt;img src=&quot;https://editor.jamesg.blog/content/images/2026/06/signalofficialchat-1.png&quot; alt=&quot;The Signal &amp;quot;Note to Self&amp;quot; chat window with the words &amp;quot;Official chat&amp;quot; under the words &amp;quot;Note to Self&amp;quot; both in the persistent author indicator in the top left and in the big box that appears at the top of the chat logs.&quot; title=&quot;&quot;/&gt;&lt;/picture&gt;&lt;div&gt;ALT&lt;div&gt;The Signal &amp;quot;Note to Self&amp;quot; chat window with the words &amp;quot;Official chat&amp;quot; under the words &amp;quot;Note to Self&amp;quot; both in the persistent author indicator in the top left and in the big box that appears at the top of the chat logs.&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;a href=&quot;https://jamesg.blog/2026/02/23/artemis-via&quot;&gt;“via” feature,&lt;/a&gt;&lt;a href=&quot;https://jamesg.blog/2026/06/01/flagging-suspicious-websites-in-artemis&quot;&gt;inline message to indicate a link in a user’s Artemis reader has been flagged as malicious&lt;/a&gt;</content:encoded>
</item>
<item>
<title>L’inventeur du Web s’inquiète de l’essor de l’IA et appelle à protéger l’esprit ouvert des débuts d’Internet</title>
<link>https://siecledigital.fr/2026/06/04/linventeur-du-web-sinquiete-de-lessor-de-lia-et-appelle-a-proteger-lesprit-ouvert-des-debuts-dinternet/</link>
<guid isPermaLink="false">YT_nKaW_uRyJdiVCojWMEdON6qy9BxTdWvnZhQ==</guid>
<pubDate>Fri, 05 Jun 2026 08:35:55 +0000</pubDate>
<description>N’oublions pas que le personnage a du poids. Tim Berners-Lee n’est pas un commentateur de plateau. Il a créé le Web. En 1989, alors physicien au CERN près de Genève, il imaginait un système de partage d’informations entre scientifiques. Deux ans plus tard, il écrivait le premier navigateur internet et posait les fondations de ce […]</description>
<content:encoded>&lt;a href=&quot;https://siecledigital.fr/2026/06/04/linventeur-du-web-sinquiete-de-lessor-de-lia-et-appelle-a-proteger-lesprit-ouvert-des-debuts-dinternet/&quot;&gt;&lt;img src=&quot;https://siecledigital.fr/wp-content/uploads/2026/06/tim-berners-lee-600x350.jpg&quot; alt=&quot;L&amp;#39;inventeur du Web s&amp;#39;inquiète de l&amp;#39;essor de l&amp;#39;IA et appelle à protéger l&amp;#39;esprit ouvert des débuts d&amp;#39;Internet&quot; title=&quot;&quot;/&gt;&lt;/a&gt;N’oublions pas que le personnage a du poids. Tim Berners-Lee n’est pas un commentateur de plateau. Il a créé le Web. En 1989, alors physicien au CERN près de Genève, il imaginait un système de partage d’informations entre scientifiques. Deux ans plus tard, il écrivait le premier navigateur internet et posait les fondations de ce […]</content:encoded>
</item>
<item>
<title>IPv6 zones in URLs are a mistake - Xe Iaso</title>
<link>https://xeiaso.net/notes/2026/ipv6-zones-go-url/</link>
<guid isPermaLink="false">zOGjOu0uCNmtPMsmrlQKWRFs-XOkkYWrXjBkmw==</guid>
<pubDate>Fri, 05 Jun 2026 00:44:43 +0000</pubDate>
<description>Run away while you still can, it&#39;s not too late for you to avoid the curse of knowledge.</description>
<content:encoded>&lt;h1&gt;IPv6 zones in URLs are a mistake&lt;/h1&gt;&lt;div&gt;
        &lt;div&gt;
            &lt;p&gt;
                Published on &lt;time&gt;2026-06-05&lt;/time&gt;, 696 words, 3 minutes to read
            &lt;/p&gt;

            
                &lt;p&gt;Run away while you still can, it&amp;#39;s not too late for you to avoid the curse of knowledge.&lt;/p&gt;
            
        &lt;/div&gt;
        &lt;div&gt;
            
        &lt;/div&gt;
    &lt;/div&gt;&lt;p&gt;IPv6 is weird. One of the more strange parts of the standard is that every interface&amp;#39;s link local addresses are in &lt;code&gt;fe80::whatever&lt;/code&gt;. If you have a machine with two network interfaces, both of them will be in &lt;code&gt;fe80::&lt;/code&gt;, so if you have a packet destined to &lt;code&gt;fe80::4&lt;/code&gt;, how do you disambiguate it?&lt;/p&gt;&lt;p&gt;The answer is you use &lt;a href=&quot;https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses_(with_zone_index)&quot;&gt;IPv6 scopes/zones&lt;/a&gt;. The exact format of what goes into a zone is OS dependent, but on Linux it&amp;#39;s the interface name and on Windows it&amp;#39;s the interface ID. This lets the kernel&amp;#39;s routing table know how to handle an address range conflict.&lt;/p&gt;&lt;p&gt;On my tower, this would be represented like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text code-highlight&quot;&gt;fe80::4%eth0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where &lt;code&gt;eth0&lt;/code&gt; is the name of my tower&amp;#39;s ethernet device.&lt;/p&gt;&lt;p&gt;When you create a host:port bindhost, you normally separate the hostname and port with a colon. IPv6 uses colons to separate hex groups. In order to disambiguate what&amp;#39;s the host and what&amp;#39;s the port, you typically format the IPv6 address in square brackets, so &lt;code&gt;fe80::4&lt;/code&gt; on port 80 would look like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text code-highlight&quot;&gt;[fe80::4]:80&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And with the right scope it looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text code-highlight&quot;&gt;[fe80::4%eth0]:80&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now let&amp;#39;s get URL encoding into the mix. From high orbit, you can imagine a URL&amp;#39;s format as being something like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;code-highlight&quot;&gt;&amp;lt;scheme&amp;gt;:[//][&amp;lt;username&amp;gt;[:&amp;lt;password&amp;gt;]@][&amp;lt;hostname&amp;gt;][:&amp;lt;port&amp;gt;][/&amp;lt;path&amp;gt;][?&amp;lt;query&amp;gt;][#&amp;lt;fragment&amp;gt;]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An IPv6 zone would then be part of the hostname, just like with that &lt;code&gt;fe80::4&lt;/code&gt; port 80 example from earlier. So you&amp;#39;d think the URL would be something like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text code-highlight&quot;&gt;http://[fe80::4%eth0]:80&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But if you try to parse this as a URL in Go, you get an error:&lt;/p&gt;&lt;p&gt;Yields:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text code-highlight&quot;&gt;panic: parse &amp;quot;http://[fe80::4%eth0]:80&amp;quot;: invalid URL escape &amp;quot;%et&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This happens because URLs can&amp;#39;t represent all Unicode values, so any values that don&amp;#39;t fit into the grammar of a URL become &lt;a href=&quot;https://en.wikipedia.org/wiki/Percent-encoding&quot;&gt;percent-encoded&lt;/a&gt;. This is why sometimes you&amp;#39;ll see a &lt;code&gt;%20&lt;/code&gt; in URLs in the wild; that&amp;#39;s encoding the ascii space key, which is invalid in URLs.&lt;/p&gt;&lt;p&gt;In order to work around this, you need to percent-encode the percent sign in the IPv6 zone:&lt;/p&gt;&lt;p&gt;Yields:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text code-highlight&quot;&gt;fe80::4%eth0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In theory, there is guidance for how to properly handle IPv6 zones in user interfaces in &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9844.txt&quot;&gt;RFC 9844&lt;/a&gt;&lt;del&gt;, but there&amp;#39;s no such guidance for URLs&lt;/del&gt;. Go also does not seem to follow this RFC in &lt;a href=&quot;https://pkg.go.dev&quot;&gt;net/url&lt;/a&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;img src=&quot;https://stickers.xeiaso.net/sticker/cadey/coffee&quot; alt=&quot;Cadey is coffee&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;a href=&quot;https://xeiaso.net/characters#cadey&quot;&gt;Cadey&lt;/a&gt;&lt;/span&gt;&lt;div&gt;&lt;p&gt;EDIT: It seems that this behaviour is compliant with &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6874.html&quot;&gt;RFC 6874&lt;/a&gt; and that this is in fact how it is meant to be done.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text code-highlight&quot;&gt;IP-literal = &amp;quot;[&amp;quot; ( IPv6address / IPv6addrz / IPvFuture  ) &amp;quot;]&amp;quot;

      ZoneID = 1*( unreserved / pct-encoded )

      IPv6addrz = IPv6address &amp;quot;%25&amp;quot; ZoneID&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Our industry confounds me.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;So in the meantime in order for Anubis to point to IPv6 zoned addresses, you need to encode the &lt;code&gt;%&lt;/code&gt; with percent encoding. This is horrible, but it seems that this is an edge case that applies to other frameworks, programming languages, and libraries:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://trac.nginx.org/nginx/ticket/623&quot;&gt;https://trac.nginx.org/nginx/ticket/623&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/psf/requests/issues/6808&quot;&gt;https://github.com/psf/requests/issues/6808&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/draft-schinazi-httpbis-link-local-uri-bcp-03&quot;&gt;https://datatracker.ietf.org/doc/html/draft-schinazi-httpbis-link-local-uri-bcp-03&lt;/a&gt; -- Browsers don&amp;#39;t currently support IPv6 zones because it breaks the concept of an &amp;quot;origin&amp;quot; which is used for many subtle things, this RFC draft attempts to define an zone origin in IPv6 so that browsers have a leg to stand on&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Maybe some day in the future there will be a better option here. In the meantime my policy of not forking the Go standard library means that this somewhat terrible UX for an edge case is acceptable. I hate it, but what can you do?&lt;/p&gt;&lt;p&gt;TL;DR: computers were a mistake.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;Facts and circumstances may have changed since publication. Please contact me before jumping to conclusions if something seems wrong or unclear.&lt;/p&gt;&lt;p&gt;Tags: &lt;/p&gt;</content:encoded>
</item>
<item>
<title>Crashing cars and improving hover detection | Motion Magazine</title>
<link>https://motion.dev/magazine/collision-detection-in-hover-detection</link>
<enclosure type="image/jpeg" length="0" url="https://api.motion.dev/magazine/og/article/collision-detection-in-hover-detection.png?title=Crashing+cars+and+improving+hover+detection&amp;description=Fast+pointer+movements+can+skip+over+elements%2C+breaking+hover+detection.+The+surprising+solution+can+be+found+in+game+development.&amp;category=Feature&amp;author=Matt+Perry&amp;date=2026-06-04T00%3A00%3A00.000Z"></enclosure>
<guid isPermaLink="false">ad9IkGEKNs4tIlIJ_RqpgoF7d8LqtdOkKQx5Cw==</guid>
<pubDate>Fri, 05 Jun 2026 00:44:42 +0000</pubDate>
<description>Fast pointer movements can skip over elements, breaking hover detection. The surprising solution can be found in game development.</description>
<content:encoded>&lt;p&gt;I&amp;#39;m going to show you an effect that you&amp;#39;ll recognise immediately, perhaps without ever having paid it much attention.&lt;/p&gt;&lt;p&gt;Take any collection of elements that react to hover: a list of menu items, swatches in a colour picker, squares in a grid. Now quickly swipe your cursor across them:&lt;/p&gt;&lt;p&gt;In real life, your hand moves across your desk, or your finger across the screen, in a continuous, unbroken motion. But this isn&amp;#39;t reflected in the example above. Here, lights in the path of motion are switched on seemingly at random. Move slower, and you&amp;#39;ll see that every element lights up, and the faster you swipe, the more elements are skipped.&lt;/p&gt;&lt;p&gt;Now run your mouse over this version. Swipe it as fast as you like: every cell you cross lights up, with nothing skipped.&lt;/p&gt;&lt;p&gt;Honestly when I created this second example I couldn&amp;#39;t stop playing with it. It is weird how responsive it feels, why doesn&amp;#39;t it always work like this? By the end of this post you&amp;#39;ll know why, and how to build this improved hover yourself.&lt;/p&gt;&lt;p&gt;Surprisingly, this is the exact same problem that video game engines encounter when deciding whether a car has crashed, or any other type of collision has taken place. As such, a solution to our skipped elements was invented decades ago.&lt;/p&gt;&lt;h2&gt;Discrete vs continuous motion&lt;/h2&gt;&lt;p&gt;CSS selectors like &lt;code&gt;:hover&lt;/code&gt;, Motion events like &lt;code&gt;onHoverStart&lt;/code&gt;, and JS events like &lt;code&gt;pointerenter&lt;/code&gt; are all afflicted by this skipped element problem.&lt;/p&gt;&lt;p&gt;The reason being, pointer position is sampled discretely, rather than continuously. Streamed as a series of points, via events, to the browser and then by the browser to our code.&lt;/p&gt;&lt;p&gt;To illustrate, lets imagine a row of elements, with a pointer moving slowly across them. The pointer events always come in at the same rate, so with slower motion these events are closer together. Meaning that it&amp;#39;s more likely at least one pointer event lands on each element, triggering its hover state:&lt;/p&gt;&lt;p&gt;Faster movement means the gaps between these events increases. Which means any elements lying in these gaps are completely skipped.&lt;/p&gt;&lt;p&gt;If you&amp;#39;ve ever written a physics engine, this&amp;#39;ll feel familiar, because it&amp;#39;s a textbook collision detection problem with a specific name: &lt;strong&gt;tunnelling&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Physics engines run a loop. Every iteration, they calculate the next position of objects based on their position and velocity. Then, they check which objects hit which. It&amp;#39;s similar to pointer events in sense that you&amp;#39;re checking discrete snapshots of positions rather than continuous motion (the latter essentially requiring infinite computation).&lt;/p&gt;&lt;p&gt;Picture a car driving towards a thin wall. During Animation Frame A it&amp;#39;s just short of the wall, and then as it drives a little further, in Frame B it&amp;#39;s overlapping the wall. The engine spots the overlap and registers a hit.&lt;/p&gt;&lt;p&gt;In a game, it will probably do something like move the car back outside the wall and trigger a crashing animation, so they appear to impact.&lt;/p&gt;&lt;p&gt;But now, imagine a car that&amp;#39;s moving even faster. It&amp;#39;s moving so fast that, in &lt;strong&gt;Frame B,&lt;/strong&gt; it&amp;#39;s already out the other side of the wall. In no single frame does the car overlap the wall, so the collision check, which only ever looks once per frame, sees nothing. The car sails straight through.&lt;/p&gt;&lt;p&gt;This is tunnelling, and it&amp;#39;s the exact same problem as our pointer sampling. The pointer is the fast object, each hover target a thin wall.&lt;/p&gt;&lt;h2&gt;The fix&lt;/h2&gt;&lt;p&gt;Games fix tunnelling by, instead of asking &amp;quot;where is the object &lt;em&gt;this frame&lt;/em&gt;, and does that overlap anything?&amp;quot;, you ask &amp;quot;what path did the object take &lt;em&gt;since last frame&lt;/em&gt;, and did that path cross anything?&amp;quot;.&lt;/p&gt;&lt;p&gt;Rather than test a point, you test a line.&lt;/p&gt;&lt;p&gt;For pointers, that line is easy to calculate. You can measure the pointer&amp;#39;s position this frame, and look back at where it was in the previous frame. Draw a line between the two and you&amp;#39;ve got the route the cursor took. So instead of checking which element contains the current point, you check which elements the &lt;strong&gt;line intersects&lt;/strong&gt;.&lt;/p&gt;&lt;h2&gt;Building it with Motion&lt;/h2&gt;&lt;p&gt;To actually implement this, we need to:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Get the pointer&amp;#39;s previous and current position.&lt;/li&gt;
&lt;li&gt;Measure the elements we wish to check against.&lt;/li&gt;
&lt;li&gt;Perform a cheap geometric test.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;Reading the pointer&lt;/h3&gt;&lt;p&gt;In this post, we&amp;#39;re going to be using Motion APIs and concepts, but the basic procedure is of course replicable in plain JavaScript.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://motion.dev/plus&quot;&gt;Motion+&lt;/a&gt; ships a &lt;a href=&quot;https://motion.dev/docs/cursor&quot;&gt;&lt;code&gt;usePointerPosition&lt;/code&gt;&lt;/a&gt; hook that gives you pointer positions as &lt;a href=&quot;https://motion.dev/docs/react-motion-value&quot;&gt;motion values&lt;/a&gt;, in &lt;strong&gt;viewport-relative coordinates&lt;/strong&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;const pointer = usePointerPosition()&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The reason I default to &lt;code&gt;usePointerPosition&lt;/code&gt; is that it&amp;#39;s extremely composable. No matter how many components call it, it only ever registers a single pointer listener and a single pair of motion values. Likewise, motion values are easy to pass straight through our compositional hooks like &lt;code&gt;useTransform&lt;/code&gt;, &lt;code&gt;useSpring&lt;/code&gt; and &lt;code&gt;useVelocity&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;However, for this use case, the nice thing about motion values is they remember their final set value in the previous animation frame. &lt;code&gt;pointer.x.get()&lt;/code&gt; returns the current position and &lt;code&gt;pointer.x.getPrevious()&lt;/code&gt; is where it was left the frame before. There&amp;#39;s our line: previous position to current position.&lt;/p&gt;&lt;h3&gt;Measuring the element&lt;/h3&gt;&lt;p&gt;We want to test every element&amp;#39;s bounding box against the line each frame. &lt;code&gt;Element.getBoundingClientRect()&lt;/code&gt; is the browser&amp;#39;s built-in API for measuring the bounding box (and like &lt;code&gt;usePointerPosition&lt;/code&gt;, this method also returns coordinates relative to the viewport).&lt;/p&gt;&lt;p&gt;We can schedule this measurement using &lt;a href=&quot;https://motion.dev/docs/frame&quot;&gt;Motion&amp;#39;s frame loop&lt;/a&gt;&amp;#39;s &lt;code&gt;read&lt;/code&gt; step, which is like &lt;code&gt;requestAnimationFrame&lt;/code&gt; but removes &lt;a href=&quot;https://motion.dev/magazine/web-animation-performance-tier-list&quot;&gt;layout and style thrashing&lt;/a&gt; by grouping all the reads before any writes within each animation frame.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;frame.read(measureElement, true)&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Testing the geometry&lt;/h3&gt;&lt;p&gt;For each element, we want to answer one question: does the pointer&amp;#39;s path cross it? We have a cheap solution in the form of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Slab_method&quot;&gt;slab method&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The idea behind the slab method is to stop thinking of the rectangle as a shape and start thinking of it as the overlap between two infinite bands, called &lt;strong&gt;slabs&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Slab X fills the gap between the left and right edges, and Slab Y fills the gap between the top and bottom. Self-evidently, a point is inside the rectangle only when it&amp;#39;s inside &lt;em&gt;both&lt;/em&gt; slabs at once.&lt;/p&gt;&lt;p&gt;To figure this out, we can measure whether the line is inside each slab independently. So for the x-axis alone we ask: at what fraction (running from &lt;code&gt;0&lt;/code&gt; at the start of the line and &lt;code&gt;1&lt;/code&gt; at the end) of the way does the path &lt;em&gt;enter&lt;/em&gt; the vertical slab, and at what fraction does it &lt;em&gt;leave&lt;/em&gt;?&lt;/p&gt;&lt;p&gt;For instance, in the following diagram &lt;code&gt;enterX&lt;/code&gt; would be something like &lt;code&gt;0.33&lt;/code&gt; and &lt;code&gt;exitX&lt;/code&gt; something like &lt;code&gt;0.66&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;We can tell if a line is fully outside a slab, and therefore outside the box, if &lt;code&gt;enterX&lt;/code&gt; (or &lt;code&gt;Y&lt;/code&gt;) is more than &lt;code&gt;1&lt;/code&gt;, which means the line is fully to the left of the slab. Likewise, if &lt;code&gt;exitX&lt;/code&gt; is less than &lt;code&gt;0&lt;/code&gt; then the line is fully to the right of the slab.&lt;/p&gt;&lt;p&gt;If the line does intersect Slab X then we can do exactly the same for Slab Y.&lt;/p&gt;&lt;p&gt;Finally, we know the line isn&amp;#39;t inside the element unless it is inside both slabs &lt;em&gt;at the same time&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;We know it doesn&amp;#39;t &lt;code&gt;enter&lt;/code&gt; the bounding box until we&amp;#39;ve crossed both &lt;code&gt;enterX&lt;/code&gt; and &lt;code&gt;enterY&lt;/code&gt;. Or, &lt;code&gt;enter = max(enterX, enterY)&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Likewise, the path will &lt;code&gt;exit&lt;/code&gt; the bounding box when it leaves either &lt;code&gt;exitX&lt;/code&gt; or &lt;code&gt;exitY&lt;/code&gt;. Or, &lt;code&gt;exit = min(exitX, exitY)&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Finally, with final &lt;code&gt;enter&lt;/code&gt; and &lt;code&gt;exit&lt;/code&gt; values in hand, we can quickly calculate whether the line was within both slabs &lt;em&gt;at the same time&lt;/em&gt; &lt;strong&gt;by checking whether &lt;code&gt;enter &amp;lt; exit&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;A path can pass through both slabs and still miss the element, as long as it isn&amp;#39;t inside both &lt;em&gt;at the same time&lt;/em&gt;. Here the line crosses Slab X below the element, leaves it, and only then crosses Slab Y to its right. The two crossings never overlap, so the highest &lt;code&gt;enter&lt;/code&gt; is larger than the smallest &lt;code&gt;exit&lt;/code&gt;.&lt;/p&gt;&lt;h2&gt;See it run&lt;/h2&gt;&lt;p&gt;Here&amp;#39;s the same grid again, this time wired up with our collision detection instead of &lt;code&gt;onPointerEnter&lt;/code&gt;. Every cell the pointer crosses lights up, so even a fast flick leaves an unbroken trail with nothing missed.&lt;/p&gt;&lt;h2&gt;Next steps&lt;/h2&gt;&lt;p&gt;There are (probably?) good reasons browsers hit-test a single point rather than running this algorithm on every element, on every site. Clearly this feels better than the native implementation but it&amp;#39;s certainly more expensive. Although there are plenty of avenues for optimisation (caching measurements, algorithmic improvements), what we have is good enough for most things.&lt;/p&gt;&lt;p&gt;However, we&amp;#39;re not about &amp;quot;good enough&amp;quot;, we&amp;#39;re looking for something more. So for isolated showcase UIs this is a technique you can keep in mind.&lt;/p&gt;&lt;p&gt;You can take it further, too. In this post we&amp;#39;ve concentrated specifically on producing a &amp;quot;is hover&amp;quot; boolean check. But, the slab method gives us some valuable extra information, including &lt;em&gt;where&lt;/em&gt; along the path the hit happened.&lt;/p&gt;&lt;p&gt;There are some fun additional effects you could produce with this information that I&amp;#39;ll leave to the imagination. But, one idea is triggering an animation with a negative &lt;code&gt;delay&lt;/code&gt; based on this progress value. This ensures elements colliding with the cursor don&amp;#39;t all animate as a batch but with a subtle offset that reflects when they were &amp;quot;really&amp;quot; hovered.&lt;/p&gt;&lt;p&gt;Our new &lt;a href=&quot;https://motion.dev/examples/react-bobble-hover&quot;&gt;Bobble Hover&lt;/a&gt; example does exactly this, with each tile rippling in the order the cursor hit them, each launched with the speed it was struck at - all built on the same slab method we&amp;#39;ve just explored.&lt;/p&gt;&lt;p&gt;Would you like to see this kind of collision detection API in Motion? You know where to &lt;a href=&quot;https://x.com/motiondotdev&quot;&gt;let us know&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Designing a better podcast editor — Adam Solove</title>
<link>https://www.adamsolove.com/ui/ducking/2026/06/03/better-podcast-ui.html</link>
<guid isPermaLink="false">jSkDWYmKQTbDYl_8LiqcsxLVYrfbhnNYY2etsA==</guid>
<pubDate>Thu, 04 Jun 2026 14:56:23 +0000</pubDate>
<description>A better concept model and more efficient tools for editing spoken word audio.</description>
<content:encoded>&lt;p&gt;For the past few years, my partner has recorded and edited a niche podcast while I’ve helped a bit with selecting music and EQ setting. Our workflow was painful: one shared iCloud file, emails of notes coded to timestamps, carefully checking that our changes didn’t conflict.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Editing audio was like traveling back in time twenty years: no track changes, no comments, and no multiplayer editing.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The friction wasn’t just in the file-oriented workflow. The normal interaction model of Digital Audio Workstations (or DAWs) is not particularly well-suited for the task of editing spoken word audio.&lt;/p&gt;&lt;p&gt;So I decided to build the podcast editor we wanted: Ducking. It has a UI purpose-built for laying out spoken word audio, plus multiplayer editing, collaboration tools, and history management. In this post, I’ll talk about the improvements it makes to editing tools. Future posts will discuss the engineering challenges of multiplayer audio editing and the pleasure of building software for just a few users with design sketching techniques and LLM assistance.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 1.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  &lt;img src=&quot;https://www.adamsolove.com/making-of/assets/screenshot.png&quot; alt=&quot;Screenshot of Ducking — waveform timeline, transcript pane, history panel, two cursors on different clips&quot; title=&quot;&quot;/&gt;
  &lt;figcaption&gt;Screenshot of Ducking in use, with the comments and effects panels open.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;How does Ducking major editing podcasts easier? It focuses on providing better tools for the two most-common recurring tasks:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Audio layout: specifying how bits of audio should stick together as things around them change.&lt;/li&gt;
&lt;li&gt;Navigation: finding the right bit of audio. This happens at a lot of levels of precision, from “roughly where does act 2 start?” down to “exactly which millisecond is the beginning of that background noise?”&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Ducking itself was built specifically for our podcast workflow, serves its purpose doing that, and won’t be public anytime soon. &lt;strong&gt;But I hope that some of these ideas will spread into other tools and be more broadly useful.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Throughout this post, I’ll show simplified animations of the features in action to avoid distracting with other parts of the editing UI.&lt;/p&gt;&lt;h2&gt;Audio layout&lt;/h2&gt;&lt;p&gt;Like laying out a newspaper or a webpage, one of the main challenges with audio editing is to start by roughly trying out how different parts fit together, then to carefully specify more precisely, without messing up the existing choices.&lt;/p&gt;&lt;p&gt;Ducking provides an audio layout concept model that is much faster to work with, by borrowing ideas from other DAWs, text editors, and even further afield.&lt;/p&gt;&lt;h3&gt;From absolute to magnetic time&lt;/h3&gt;&lt;p&gt;In a traditional &lt;abbr&gt;DAW&lt;/abbr&gt;, every clip has an absolute start time. When one clip is moved or edited for length, everything after drifts out of alignment.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 2.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;Absolute layout — trimming any clip leaves a silent gap or overlaps the next clip.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;Absolute layout is the right model for writing songs, where material in one measure should stay there. But it’s the wrong model for editing spoken word material, where the default is to reflow later material as earlier bits change.&lt;/p&gt;&lt;p&gt;The right layout model is a magnetic timeline, where clips are ordered, not positioned. Each clip’s place in time is computed from the lengths of the items before it. So when one clip is added, removed, or  edited, everything after just re-flows automatically.&lt;/p&gt;&lt;p&gt;Gap clips allow adding explicitly-timed silence when that is needed.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 3.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;Magnetic layout — clips and gaps reflow when you trim.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;This is the model used by many video editing tools as well as audio tools that focus on spoken word, like &lt;a href=&quot;https://hindenburg.com/&quot;&gt;Hindenburg&lt;/a&gt;. So the idea itself isn’t new. &lt;strong&gt;But it provides the first step and suggests that further playing with the idea of an automated layout model might be useful.&lt;/strong&gt;&lt;/p&gt;&lt;h3&gt;From splits to skip regions&lt;/h3&gt;&lt;p&gt;The vast majority of podcast editing is repeatedly removing tiny bits of unwanted material like filler words, long pauses, or a flubbed sentence. In most audio editors, that means splitting each recording clip into lots of tiny parts and adjusting their alignment. After doing that dozens of times, the timeline view becomes a huge set of disconnected clips that are hard to scan or reorganize.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 4.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;Without skip regions, every filler removal splits a clip in two. One more cut and you&amp;#39;re up to ten detached fragments — none of them carrying any indication that they belong to the same original take.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;Ducking uses “skip regions” as a better solution. The editor can leave a clip as a single unit while editing away part of it as not to be used. This keeps a single mostly-intact recording as a unit, so it’s easier to understand and rearrange, while still indicating where material has been removed.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 5.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;Skip regions — fold a portion of a clip without splitting it in two.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;The skip region acts like code folding in a text editor. It leaves a visible indication and can be unfolded to interact with the skipped audio or change the region’s start and end.&lt;/p&gt;&lt;h3&gt;Pin-based alignment&lt;/h3&gt;&lt;p&gt;So far the editor has only been working with a single track of audio. The problem gets harder as we add more parallel tracks.&lt;/p&gt;&lt;p&gt;Perhaps the trickiest case is transition music. The editor will usually want it to play gently underneath the end of one section, swell to be the main focus, and then duck back beneath the beginning of the next section.&lt;/p&gt;&lt;p&gt;Most audio tools allow laying out the second track either in absolute time or by connecting the start of a clip in one track to a specific place in another, which allows the second track to float along with the rest of the magnetic timeline.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 6.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;Single connector — music clip tied to the end of a specific speech clip.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;With the connector model, the editor can construct any particular transition, but they have to struggle a bit to translate between the creative vision they have and the set of tools that enables them to reach it.&lt;/p&gt;&lt;p&gt;Analyzing the creative decisions that they are trying to make, editors really care about:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;How the fade-in portion of the music corresponds to the outgoing speaking part: making sure it becomes noticable right as the section is reaching a dramatic or summarizing closing beat.&lt;/li&gt;
&lt;li&gt;How the swell of the music lets the last section gently fade out and how the music corresponds to the emotional or intellectual changes between the two sections.&lt;/li&gt;
&lt;li&gt;How the fade-out portion of the music ducks back down to fit beneath the next section’s spoken content, lingers for a bit, and then fades out smoothly.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;So I built a set of layout tools that exactly correspond to those set of creative decisions. Using the pin-based layout system, the editor gets to pick which part of the music should play at the same time as which part of the preceding and following spoken word clips. Then they can independently control the volume or other effects on each track so that they layer properly.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 7.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;Pins and constraints — two-tie constraint layout with fade points and automation.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;Combining all of these elements — magnetic timeline, skip regions, and constraints between tracks — removes the layout busywork from audio editing and lets the editor focus directly on the emotional experience they’re trying to achieve.&lt;/p&gt;&lt;h2&gt;Navigating audio&lt;/h2&gt;&lt;p&gt;Before any edit action can happen, first the editor has to understand and choose what to edit. Navigating audio happens in a few ways:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Actually listening to the audio, possibly scrubbing or jumping around the timeline. The playhead provides a visible correspondence between what’s playing now and where it is in the timeline. A common task is to just listen and try to pause at the right time to leave the playhead at a certain spot.&lt;/li&gt;
&lt;li&gt;Looking at the timeline and waveforms can sometimes help, both at very zoomed-out levels, where seeing the patterns of clips and tracks gives the high-level structure of the project, and at a very zoomed-in level, where the waveform shows exactly where a piece of speech or sound begins.&lt;/li&gt;
&lt;li&gt;Reviewing the transcript is helpful at intermediate levels, when looking for a specific bit of speech.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Ducking’s UI makes it easy to navigate in any of these manners by establishing correspondences between each of them. Rotating the timeline editing tools 90º lets the overview, waveform, and transcript view all move in the same alignment and scroll together.&lt;/p&gt;&lt;p&gt;Below is a low-fidelity interactive mockup that shows the core ideas.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 8.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;Scrolling through a project. The timeline, transcript, and overview thumb stay in lockstep — the golden playhead marks the current position in all three.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;With this high-level UI layout:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;The scrollbar shows a simplified and zoomed-out overview view of the entire project, so the scroll tab’s size and position shows us exactly where in the project we are and how far zoomed in we are. In this project, the purple music clips clearly establish the section breaks, allowing us to quickly see we’re looking at the transition from the intro into the first main section.&lt;/li&gt;
&lt;li&gt;The timeline view shows a traditional waveform with editing tools for each track and clip. But its rotated ninety degrees so that it scrolls vertically. This allows us to do detailed &lt;abbr&gt;DAW&lt;/abbr&gt; edit operations to the waveform, while benefiting from the natural alignment of the timeline to the transcript.&lt;/li&gt;
&lt;li&gt;The transcript view is neither an afterthought (like in most DAWs where it just hangs out and has no correspondence to the rest of the editing), nor is it the primary editing surface (like in Descript). It scrolls and zooms together with the timeline so that you can always clearly see the text of the audio being edited. Clicking on words moves the playhead there&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;The UI layout establishes a clear correspondence between the overview, the waveforms, the transcript, and any currently-playing audio:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;The playhead appears as a golden line in all three views to orient what we’re currently listening to.&lt;/li&gt;
&lt;li&gt;The current contents of the timeline view are indicated in the overview by its scroll tab and in the transcript view by the matching gray outline section.&lt;/li&gt;
&lt;li&gt;Scroll and zoom actions obviously take effect in all three places at once.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This same correspondence also ratchets up the power of other tools. When searching for text or looking at the history of edits to the project, those annotations can be overlayed onto all three views.&lt;/p&gt;&lt;p&gt;As an example, because the overview always shows the entire project, it’s a great way to see the overall structure and then orient where search results or tracked changes have happend in the document.&lt;/p&gt;&lt;figure&gt;
  &lt;div&gt;&lt;span&gt;Fig. 9.&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;/div&gt;
  
  &lt;figcaption&gt;The same scrollbar, three jobs. The overview&amp;#39;s whole-project context lets other tools — search, history compare — speak in the same affordance.&lt;/figcaption&gt;
&lt;/figure&gt;&lt;h2&gt;Conclusion and what’s next&lt;/h2&gt;&lt;p&gt;Taken together, the more powerful audio layout model and the new UI navigation make it much faster for us to produce podcast episodes from raw recordings. The software is definitely tailored just for our needs, but these ideas may be more broadly applicable, which is why I am sharing them here.&lt;/p&gt;&lt;p&gt;Where this post focused on the UI, I plan to publish two future posts on other parts of the project:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;The experience of building a local-first, multiplayer experience on top of Automerge, especially focusing on ideas around collaboration and change management with non-textual data.&lt;/li&gt;
&lt;li&gt;The texture of working with AI coding assistants outside of a business environment, using the leverage not to intensify work but to enjoy more sketching and hammock time to decide what’s next. Plus the pleasure of building narrowcast software that only has to please two people.&lt;/li&gt;
&lt;/ol&gt;&lt;hr/&gt;&lt;h2&gt;About me&lt;/h2&gt;&lt;p&gt;I’m Adam Solove, a product engineer who loves to build great products in complicated domains. I’m just wrapping up a six month sabbatical that focused on my local community and some building deeply personal tech experiments like the one above.&lt;/p&gt;&lt;p&gt;I’m starting to look for projects or my next role. If you’re building something interesting, please &lt;a href=&quot;mailto:asolove+recruiting@gmail.com&quot;&gt;get in touch&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Face à la contestation interne, Meta revoit son logiciel de surveillance des salariés</title>
<link>https://www.sciencesetavenir.fr/high-tech/web/face-a-la-contestation-interne-meta-revoit-son-logiciel-de-surveillance-des-salaries_192886?xtor=RSS-12</link>
<enclosure type="image/jpeg" length="0" url="https://www.sciencesetavenir.fr/assets/img/2026/06/03/cover-r4x3w1200-6a1fea4556871-le-logo-de-meta-au-meta-lab-de-los-angeles.jpg"></enclosure>
<guid isPermaLink="false">WeYRhlHe_j8J-lsjd1wBcWp5KNHAM9AoVJKiuw==</guid>
<pubDate>Thu, 04 Jun 2026 04:58:32 +0000</pubDate>
<description>Critiqué par ses employés, Meta modifie son dispositif de collecte de données sur les ordinateurs professionnels destiné à entraîner ses modèles d’intelligence artificielle. Le groupe autorise désormais des pauses dans la collecte et ouvre la voie à des demandes d’exemption.</description>
<content:encoded>Critiqué par ses employés, Meta modifie son dispositif de collecte de données sur les ordinateurs professionnels destiné à entraîner ses modèles d’intelligence artificielle. Le groupe autorise désormais des pauses dans la collecte et ouvre la voie à des demandes d’exemption.</content:encoded>
</item>
<item>
<title>Sarbacane devient Positive User : la plateforme d’emailing change de nom et d’ambition</title>
<link>https://www.blogdumoderateur.com/sarbacane-devient-positive-user/</link>
<enclosure type="image/jpeg" length="0" url="https://f.hellowork.com/blogdumoderateur/2026/05/Sarbacane-Positive-2026-294x147.jpg"></enclosure>
<guid isPermaLink="false">pLpyLcj1UpiPT3rfn4Fkctc7TL8K0jd_cx2Udg==</guid>
<pubDate>Thu, 04 Jun 2026 02:52:32 +0000</pubDate>
<description>Après vingt-cinq ans d&#39;existence sous la marque Sarbacane, la plateforme française de marketing digital change de nom et annonce une ambition européenne plus marquée. Le nouveau nom, Positive User, est entré en vigueur fin mai 2026.</description>
<content:encoded>&lt;img src=&quot;https://f.hellowork.com/blogdumoderateur/2026/05/Sarbacane-Positive-2026-294x147.jpg&quot; alt=&quot;Sarbacane devient Positive User : la plateforme d’emailing change de nom et d’ambition&quot; title=&quot;&quot;/&gt;Après vingt-cinq ans d&amp;#39;existence sous la marque Sarbacane, la plateforme française de marketing digital change de nom et annonce une ambition européenne plus marquée. Le nouveau nom, Positive User, est entré en vigueur fin mai 2026.</content:encoded>
</item>
<item>
<title>Ce qu’on a lu, vu, aimé et entendu en mai 2026 : les recos de la rédaction</title>
<link>https://www.blogdumoderateur.com/recos-redaction-mai-2026/</link>
<enclosure type="image/jpeg" length="0" url="https://f.hellowork.com/blogdumoderateur/2026/05/recos-redac-mai-2026-294x154.jpg"></enclosure>
<guid isPermaLink="false">9Xer2iLcBAiBEhanQdY5JDsnTlZQlEOrb3gcLw==</guid>
<pubDate>Thu, 04 Jun 2026 02:52:32 +0000</pubDate>
<description>Au programme : une chaîne YouTube qui réinvente le reportage de terrain, un jeu vidéo loufoque à l&#39;ère du web des années 90, une vidéo sur les dérives des paris sportifs au tennis, une déclaration de Sam Altman et un musée dédié à l&#39;art génératif.</description>
<content:encoded>&lt;img src=&quot;https://f.hellowork.com/blogdumoderateur/2026/05/recos-redac-mai-2026-294x154.jpg&quot; alt=&quot;Ce qu’on a lu, vu, aimé et entendu en mai 2026 : les recos de la rédaction&quot; title=&quot;&quot;/&gt;Au programme : une chaîne YouTube qui réinvente le reportage de terrain, un jeu vidéo loufoque à l&amp;#39;ère du web des années 90, une vidéo sur les dérives des paris sportifs au tennis, une déclaration de Sam Altman et un musée dédié à l&amp;#39;art génératif.</content:encoded>
</item>
<item>
<title>12 événements à ne pas manquer en juin 2026</title>
<link>https://www.blogdumoderateur.com/evenements-digital-juin-2026/</link>
<enclosure type="image/jpeg" length="0" url="https://f.hellowork.com/blogdumoderateur/2026/05/evenements-digital-juin-2026-294x154.jpg"></enclosure>
<guid isPermaLink="false">35FsAYwtZ0-SQ7Wgl6DMVOWAvnQ_JeGCkRTJGw==</guid>
<pubDate>Thu, 04 Jun 2026 02:52:32 +0000</pubDate>
<description>Retrouvez dans cette sélection : des webinars sur l&#39;IA-réputation, la gouvernance de l&#39;IA, le marketing d&#39;influence, les compétences à maîtriser en marketing digital à l&#39;ère de l&#39;IA, les piliers pour performer sur Meta Ads, l&#39;événement 2036, Intelligence Marketing Day, VivaTech, mais aussi Apple WWDC, French Tech Night à Nantes, DevLille et Figma Config.</description>
<content:encoded>&lt;img src=&quot;https://f.hellowork.com/blogdumoderateur/2026/05/evenements-digital-juin-2026-294x154.jpg&quot; alt=&quot;12 événements à ne pas manquer en juin 2026&quot; title=&quot;&quot;/&gt;Retrouvez dans cette sélection : des webinars sur l&amp;#39;IA-réputation, la gouvernance de l&amp;#39;IA, le marketing d&amp;#39;influence, les compétences à maîtriser en marketing digital à l&amp;#39;ère de l&amp;#39;IA, les piliers pour performer sur Meta Ads, l&amp;#39;événement 2036, Intelligence Marketing Day, VivaTech, mais aussi Apple WWDC, French Tech Night à Nantes, DevLille et Figma Config.</content:encoded>
</item>
<item>
<title>Avez-vous bien suivi l’actualité digitale du mois de mai 2026 ?</title>
<link>https://www.blogdumoderateur.com/quiz-actualite-mai-2026/</link>
<enclosure type="image/jpeg" length="0" url="https://f.hellowork.com/blogdumoderateur/2026/05/quiz-actu-digitale-mai-2026-294x154.jpg"></enclosure>
<guid isPermaLink="false">tW8VUu78RFB6YW4jiuboPFVuVGfmctDJcHj8Og==</guid>
<pubDate>Thu, 04 Jun 2026 02:52:32 +0000</pubDate>
<description>LinkedIn traque certains contenus, Instagram dévoile une nouvelle fonctionnalité, Google repense un élément emblématique de son moteur de recherche... Avez-vous bien suivi l&#39;actualité, entre deux jours fériés ?</description>
<content:encoded>&lt;img src=&quot;https://f.hellowork.com/blogdumoderateur/2026/05/quiz-actu-digitale-mai-2026-294x154.jpg&quot; alt=&quot;Avez-vous bien suivi l’actualité digitale du mois de mai 2026 ?&quot; title=&quot;&quot;/&gt;LinkedIn traque certains contenus, Instagram dévoile une nouvelle fonctionnalité, Google repense un élément emblématique de son moteur de recherche... Avez-vous bien suivi l&amp;#39;actualité, entre deux jours fériés ?</content:encoded>
</item>
<item>
<title>Sarah El Haïry (Haute-commissaire à l’enfance) : “Ce n’est pas à l’enfant de se protéger de la technologie”</title>
<link>https://www.blogdumoderateur.com/sarah-el-hairy-enfant-proteger-technologie/</link>
<enclosure type="image/jpeg" length="0" url="https://f.hellowork.com/blogdumoderateur/2026/05/sarah-el-hairy-294x154.jpg"></enclosure>
<guid isPermaLink="false">Uu_s9nEjcdfK9YKKwfGwQnlftf_1tcdszJI4Mw==</guid>
<pubDate>Thu, 04 Jun 2026 02:52:32 +0000</pubDate>
<description>Sarah El Haïry, Haute-commissaire à l&#39;Enfance, revient pour BDM sur les chantiers ouverts depuis un an et leurs enjeux.</description>
<content:encoded>&lt;img src=&quot;https://f.hellowork.com/blogdumoderateur/2026/05/sarah-el-hairy-294x154.jpg&quot; alt=&quot;Sarah El Haïry (Haute-commissaire à l’enfance) : “Ce n’est pas à l’enfant de se protéger de la technologie”&quot; title=&quot;&quot;/&gt;Sarah El Haïry, Haute-commissaire à l&amp;#39;Enfance, revient pour BDM sur les chantiers ouverts depuis un an et leurs enjeux.</content:encoded>
</item>
<item>
<title>RustRadio UI improved</title>
<link>https://blog.habets.se/2026/05/RustRadio-UI-improved.html</link>
<guid isPermaLink="false">v_R8qyLd6Vx68PLKHbzFAo97eEYzB_re2_Z3Rg==</guid>
<pubDate>Wed, 03 Jun 2026 16:12:15 +0000</pubDate>
<description>This is just a short followup to the last RustRadio post. If you came for more rants about C, you’ll be disappointed. I’ve never been that interested in writing UI code, including HTML. You can see the “programmer art” in the screenshots linked from www.habets.pp.se. And then the slightly different tech section, that doesn’t serve much of a purpose now that we have github. I’ve not been happier with GTK, QT, and the others either. But [RustRadio][rustradio] needs a UI. I feel like...</description>
<content:encoded>&lt;p&gt;This is just a short followup to the last RustRadio post. If you came for more &lt;a href=&quot;https://blog.habets.se/2026/05/Everything-in-C-is-undefined-behavior.html&quot;&gt;rants about C&lt;/a&gt;, you’ll be disappointed.&lt;/p&gt;&lt;p&gt;I’ve never been that interested in writing UI code, including HTML. You can see the “programmer art” in the screenshots linked from &lt;a href=&quot;https://www.habets.pp.se/&quot;&gt;www.habets.pp.se&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;And then the slightly different &lt;a href=&quot;https://www.habets.pp.se/synscan/&quot;&gt;tech section&lt;/a&gt;, that doesn’t serve much of a purpose now that we have github.&lt;/p&gt;&lt;p&gt;I’ve not been happier with GTK, QT, and the others either.&lt;/p&gt;&lt;p&gt;But [RustRadio][rustradio] needs a UI.&lt;/p&gt;&lt;p&gt;I feel like the browser is the most stable and portable UI. So I’d already decided on that. So now I have to manually do a bunch of &lt;a href=&quot;https://en.wikipedia.org/wiki/Document_Object_Model&quot;&gt;DOM&lt;/a&gt; manipulation, to create an interactive UI? Or worse, learn the React/Angular/Whatever flavor of the day, that will be obsolete by next afternoon? Gag me with a spoon.&lt;/p&gt;&lt;h2&gt;LLM to the rescue&lt;/h2&gt;&lt;p&gt;For now I’m just continuing to focus on the SDR and architectural parts of RustRadio, and I’m letting the LLM-written code do the HTML manipulation.&lt;/p&gt;&lt;p&gt;Yeah, it’s kinda vibe coding. But doesn’t use &lt;code class=&quot;highlighter-rouge&quot;&gt;unsafe&lt;/code&gt;, and it demonstrably outputs what I want. (I mean, sure it may require some follow-up prompts), so who cares?&lt;/p&gt;&lt;p&gt;The vibe coding is isolated to the files doing the drawing. If I want to artisanally craft better code in the future, that’s the file that needs to be rewritten. Until then, it works.&lt;/p&gt;&lt;h2&gt;The demo, in all its glory&lt;/h2&gt;&lt;p&gt;&amp;lt;iframe width=”560” height=”315” src=”https://www.youtube.com/embed/7k0JNT6itaI frameborder=”0” allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;&lt;h2&gt;You can run the demo too.&lt;/h2&gt;&lt;p&gt;See the &lt;a href=&quot;https://github.com/ThomasHabets/ruwasm&quot;&gt;quick start instructions in the ruwasm repo&lt;/a&gt; for how to run this UI live with an RTL-SDR.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Acessos ao buscador sem IA do DuckDuckGo triplicaram desde o Google I/O</title>
<link>https://manualdousuario.net/duckduckgo-buscador-sem-ia/</link>
<guid isPermaLink="false">xAXHikXjf8KTxuY8kawCuSUU-uV9aa-4X9ZJ4Q==</guid>
<pubDate>Wed, 03 Jun 2026 02:16:43 +0000</pubDate>
<description>Desde o anúncio do Google da “maior reformulação da caixa de busca da sua história, no dia 28/5, os acessos à variante sem IA do buscador do DuckDuckGo (DDG) triplicaram — e continuam aumentando, segundo a empresa. O endereço é noai.duckduckgo.com. Se preferir, há extensões gratuitas para Chrome e Firefox. O DuckDuckGo convencional pode ser […]</description>
<content:encoded>&lt;p&gt;Desde &lt;a href=&quot;https://manualdousuario.net/google-io-2026-interface-web/&quot;&gt;o anúncio do Google&lt;/a&gt; da “maior reformulação da caixa de busca da sua história, no dia 28/5, os acessos à variante sem IA do buscador do DuckDuckGo (DDG) &lt;a href=&quot;https://xcancel.com/DuckDuckGo/status/2060418373318553902&quot;&gt;triplicaram&lt;/a&gt; — e continuam aumentando, segundo a empresa.&lt;/p&gt;&lt;p&gt;O endereço é &lt;a href=&quot;https://noai.duckduckgo.com&quot;&gt;noai.duckduckgo.com&lt;/a&gt;. Se preferir, há extensões gratuitas para &lt;a href=&quot;https://chromewebstore.google.com/detail/duckduckgo-no-ai-search/faoilnlkccdjdkpljainiiimmijofmpd&quot;&gt;Chrome&lt;/a&gt; e &lt;a href=&quot;https://addons.mozilla.org/pt-BR/firefox/addon/duckduckgo-no-ai-search/&quot;&gt;Firefox&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;O DuckDuckGo convencional pode ser configurado como um buscador sem recursos de inteligência artificial. Para isso, acesse &lt;a href=&quot;https://duckduckgo.com/settings#aifeatures&quot;&gt;as configurações de IA&lt;/a&gt; e desmarque as três opções.&lt;/p&gt;&lt;p&gt;Não é como se o DuckDuckGo fosse uma empresa anti-IA. O buscador tem um equivalente ao “AI Overviews” do Google, o Search Assist, que por padrão aparece “às vezes”, ou seja, “quando é altamente relevante” segundo critérios não divulgados.&lt;/p&gt;&lt;p&gt;Outra iniciativa em IA é o &lt;a href=&quot;https://duck.ai&quot;&gt;Duck.ai&lt;/a&gt;, uma interface para grandes modelos de linguagem (LLMs) de terceiros: Anthropic (Haiku 4.5), OpenAI (GPT-5 mini, GPT-4o mini e gpt-oss 120B), Meta (Llama 4 Scout) e Mistral (Small 4). O diferencial dessa interface é uma configuração mais privada. As conversas são anonimizadas pelo DDG, a retenção do conteúdo é limitada e elas não são usadas para treinar modelos de IA.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>O fediverso valoriza a acessibilidade nas descrições de imagens — e isso é bom para todos ⁄ Manual do Usuário</title>
<link>https://manualdousuario.net/fediverso-descricao-imagens/</link>
<enclosure type="image/jpeg" length="0" url="https://manualdousuario.net/wp-content/uploads/2024/10/plushtodon.jpg"></enclosure>
<guid isPermaLink="false">ukQ9_oqg2OW1tNQXxXwRNIBq5Ijww9zakokdYw==</guid>
<pubDate>Wed, 03 Jun 2026 02:16:42 +0000</pubDate>
<description>Nota do editor: Há quase exatos três anos, em maio de 2023, publiquei um texto meio rabugento reclamando da “polícia da descrição de imagens” no fediverso/Mastodon. Eu sempre defendi a prática e…</description>
<content:encoded>&lt;p&gt;por Augusto Campos&lt;/p&gt;&lt;div&gt;
&lt;p&gt;&lt;strong&gt;Nota do editor:&lt;/strong&gt; Há quase exatos três anos, em maio de 2023, publiquei um texto meio rabugento &lt;a href=&quot;https://manualdousuario.net/policia-descricao-imagens-alt/&quot;&gt;reclamando da “polícia da descrição de imagens” no fediverso/Mastodon&lt;/a&gt;. Eu sempre defendi a prática e &lt;a href=&quot;https://manualdousuario.net/web-acessibilidade/&quot;&gt;descrevo imagens no blog do &lt;strong&gt;Manual&lt;/strong&gt; há muitos anos&lt;/a&gt;. Minha rusga, na ocasião, era com a natureza quase persecutória de alguns participantes proeminentes, incluindo donos de grandes instâncias brasileiras, com quem não descrevia imagens, mesmo que por esquecimento ou desconhecimento.&lt;/p&gt;
&lt;p&gt;Dia desses, trocando uma ideia (pelo Mastodon) com o &lt;a href=&quot;https://augustocampos.net&quot;&gt;Augusto Campos&lt;/a&gt;, ele se lembrou daquele texto meu e pediu para revisitar o tema aqui no blog, contando o que mudou nesse intervalo de três anos e, nas palavras dele, “remover um espinho atravessado na garganta” desde 2023 (o espinho, no caso, a minha opinião). Fiquei feliz com a proposta! Feita essa devida contextualização, segue o texto do Augusto.&lt;/p&gt;
&lt;/div&gt;&lt;hr/&gt;&lt;p&gt;Descrever imagens para pessoas com algum tipo de deficiência visual é um recurso de acessibilidade valioso, que demanda pouco esforço e é suportado em boa parte das plataformas sociais da atualidade.&lt;/p&gt;&lt;p&gt;Mas ser suportado não basta: para uma rede ser acessível às pessoas com deficiência visual, a oferta do recurso de acessibilidade precisa ter adesão ampla. &lt;/p&gt;&lt;p&gt;É como na mobilidade urbana. Pouco adianta ter rampa de acesso ao meio-fio de um dos lados da rua mas não do outro, pouco adianta um morador instalar em sua calçada o pavimento tátil padronizado se o vizinho ao lado não fizer o mesmo, e assim por diante.&lt;/p&gt;&lt;p&gt;É que acessibilidade não tem seu fundamento na tecnologia. É uma questão humana e social, cuja solução vem quando uma população ou comunidade cria a cultura de assumir coletivamente o esforço e o custo de incluir as pessoas que não tem o mesmo acesso aos recursos ou ambientes.&lt;/p&gt;&lt;p&gt;Com as descrições de imagem, a questão é similar à das rampas de meio-fio: as pessoas que tem demanda de seu uso ficam acolhidas quando a presença é frequente e a qualidade é suficiente.&lt;/p&gt;&lt;h2&gt;Descrevendo suas imagens&lt;/h2&gt;&lt;p&gt;É melhor uma descrição curta e super resumida (acessibilidade limitada, mas presente) do que a ausência de descrição. Em especial, é melhor essa descrição curta do que deixar de compartilhar a imagem (acessibilidade negativa) por não se achar apto a dedicar o esforço e tempo necessários a incluir a descrição.&lt;/p&gt;&lt;p&gt;Se não souber o que escrever, pense em como descreveria essa mesma imagem para uma pessoa com quem está falando sobre ela numa ligação por áudio — personagens, objetos, cenário, a dinâmica, o texto que consta na imagem, as impressões que causa, as conclusões que provoca.&lt;/p&gt;&lt;p&gt;Em geral, essa operação é feita durante a criação ou edição do seu post. Clique na imagem e selecione a opção de preencher a descrição.&lt;/p&gt;&lt;p&gt;⚠️ Caso você vá usar algum recurso automatizado de geração de texto a partir da imagem, tenho uma sugestão adicional: revise! É muito frequente (e aqui falo como alguém que acompanha as descrições atentamente) o texto gerado por ferramentas automatizadas incluir informações erradas, ou até mesmo opostas, em relação ao que um humano vê na mesma imagem. Essa dica vale especialmente para imagens expressando dinâmicas e interações, para fotos de bichinhos, e para memes.&lt;/p&gt;&lt;h2&gt;No fediverso, acessibilidade nas imagens é compromisso coletivo&lt;/h2&gt;&lt;p&gt;Já vimos que o sucesso da inclusão depende de esforço coletivo. No fediverso isso é política da rede, a começar pelos desenvolvedores dos softwares. &lt;/p&gt;&lt;p&gt;&lt;strong&gt;Nota do editor:&lt;/strong&gt; Entenda por “fediverso” o conjunto de plataformas federadas pelo protocolo ActivityPub, incluindo (mas não só) Mastodon, PeerTube, Pixelfed e tantos outros serviços.&lt;/p&gt;&lt;p&gt;No caso do Mastodon, que é o mais visível desses softwares (ao menos aqui no Ocidente), desde o início de 2025 passou a &lt;a href=&quot;https://mastodon.social/@Gargron/113964897388501103&quot;&gt;ser padrão exibir um lembrete ao usuário&lt;/a&gt; que tenta postar uma imagem sem a descrição.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://manualdousuario.net/wp-content/uploads/2026/05/alerta-ausencia-texto-alternativo-mastodon.png&quot; alt=&quot;Print do popup que o Mastodon exibe para posts com imagem sem descrição: “Adicionar texto alternativo? Seu post contém mídia sem texto alternativo. Adicionar descrições ajuda a tornar seu conteúdo acessível para mais pessoas.”, com as opções “Cancelar”, “Publicar mesmo assim” e “Adicione texto alternativo”.&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;Popup que o Mastodon exibe antes de um post com imagem sem descrição ser publicado.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Além do cliente oficial do Mastodon, o mesmo recurso está presente (mas nem sempre ativado por padrão) em vários dos apps e clientes alternativos, e também em outros serviços do fediverso.&lt;/p&gt;&lt;p&gt;Quando o criador do Mastodon, Eugen Rochko, anunciou essa nova configuração, ele também justificou: “Descrição textual é crucial para a acessibilidade, mas também tem outras vantagens, como tornar muito mais fácil pesquisar sua postagem ou filtrá-la.”&lt;/p&gt;&lt;p&gt;Não era uma novidade. A defesa intensa, concreta, clara e ativa da inclusão e da acessibilidade por meio das descrições de imagem faz parte da cultura do fediverso (embora com variações, afinal é uma comunidade plural) há bastante tempo, o que é especialmente verdadeiro no caso das instâncias que reúnem a comunidade de brasileiros federados, muitas das quais tem esse ponto incluído em suas regras e políticas oficiais.&lt;/p&gt;&lt;p&gt;As formas de reforço positivo a essas políticas são várias, e incluem a publicação de &lt;a href=&quot;https://alt.disquete.online&quot;&gt;tutoriais&lt;/a&gt;, o destaque a esse tópico em &lt;a href=&quot;https://kit-mastodon.space&quot;&gt;guias voltados a usuários recém-chegados&lt;/a&gt;, o uso de recursos de leiaute para alertar sobre imagens publicadas sem descrição, e até a publicação de estatísticas sobre o percentual de posts da instância contendo imagens sem descrição, cuja meta é, naturalmente, zero (e frequentemente chega bem perto disso).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Nota do autor:&lt;/strong&gt; No interesse da transparência, informo que os dois links no parágrafo anterior são de páginas mantidas por mim.&lt;/p&gt;&lt;p&gt;Quando os reforços positivos (incluindo os lembretes dos apps) não bastam, a comunidade também atua na promoção ativa da conformidade dos posts que chegam às suas linhas do tempo, incluindo medidas como a rejeição espontânea, mas persistente e bastante difundida, a promover posts sem acessibilidade (tanto em nível dos reposts individuais, como em perfis como o &lt;a href=&quot;https://arram.senta-la.cloud/@TrendsBR&quot;&gt;@TrendsBR&lt;/a&gt;), ou alguns usuários que se dedicam a buscar o contato de esclarecimento com quem é contumaz em não usar o recurso de acessibilidade.&lt;/p&gt;&lt;p&gt;Esse tratamento das exceções é, naturalmente, bem menos eficaz do que a prevenção. Inclusive porque frequentemente desagrada o envolvido, assim como acontece com o motorista que decide parar seu carro “só cinco minutinhos” em frente a uma rampa de meio-fio. Ele raramente vai ter reação positiva imediata a qualquer abordagem a respeito.&lt;/p&gt;&lt;p&gt;Mas a sociedade (ou a comunidade) decidiu que aquele recurso de acessibilidade é para estar à disposição de quem tem a demanda, e prefere que o motorista fique insatisfeito do que se sinta validado para continuar a dar preferência a alguma outra conveniência sua. &lt;/p&gt;&lt;p&gt;Exceções a essa regra? Existem, e podem ser tratadas, mas não são muitas. Prosseguindo na analogia com a guia rebaixada do meio-fio, não seria razoável pensar que ela deva ser priorizada em relação a um veículo de emergência (ambulância, bombeiros) em atendimento naquele local, ou ao posicionamento de uma equipe de cinegrafistas que dali conseguiria transmitir ao vivo uma emergência de evidente interesse público. E seria de esperar que todos os envolvidos procurarão prestar apoio a quem tenha demanda da guia bem naquele momento.&lt;/p&gt;&lt;p&gt;O mesmo raciocínio se aplica à acessibilidade de imagens: as exceções merecem ser validadas espontaneamente, mas o que as caracteriza não é a conveniência, a identidade ou a pressa de quem publica, e sim uma eventual incompatibilidade entre os dois interesses públicos ou da comunidade: compartilhar aquele conteúdo imediatamente e incluir imediatamente o recurso de acessibilidade.&lt;/p&gt;&lt;p&gt;No caso da acessibilidade de imagens, há uma alternativa que o bloqueio da guia rebaixada não permitiria: publicar “ao vivo” as imagens urgentes sem a descrição e logo depois retornar para inclui-las. &lt;/p&gt;&lt;p&gt;Que fica ainda melhor quando, como já vi acontecer, pessoas da comunidade se unem espontaneamente para redigir sugestões de descrição conforme os posts “ao vivo” vão sendo inseridos, para o autor depois retornar e inclui-los — sendo que as pessoas com necessidade de acesso que também estejam acompanhando o desenrolar do processo já se beneficiam desde o momento das sugestões publicadas.&lt;/p&gt;&lt;h2&gt;Você pode ajudar&lt;/h2&gt;&lt;p&gt;Felizmente, o sucesso das iniciativas de reforço positivo e de disseminação da cultura vêm reduzindo a demanda pelas abordagens voltadas a oferecer desincentivo a quem já deixou de incluir a descrição. &lt;/p&gt;&lt;p&gt;Acessibilidade é um quadro que demora a evoluir. As pessoas envolvidas trabalham anos a fio para conseguir tornar cada vez mais natural e fácil o ato de prover as medidas de inclusão a quem precisa. &lt;/p&gt;&lt;p&gt;Você pode ajudar, tanto na adesão individual, quanto no apoio à disseminação dessa ideia!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>1-Click GitHub Token Stealing via a VSCode Bug – Ammar&#39;s Blog</title>
<link>https://blog.ammaraskar.com/github-token-stealing/</link>
<guid isPermaLink="false">TDKqRq4IMFzu-7Dg1d2HVullc9g53fY1TiXaJw==</guid>
<pubDate>Wed, 03 Jun 2026 01:55:26 +0000</pubDate>
<description>My blog, mostly about programming</description>
<content:encoded>&lt;p&gt;Just by clicking a link, it’s possible for an attacker to steal a GitHub
token that can read and &lt;strong&gt;write&lt;/strong&gt; to your repos, including &lt;strong&gt;private ones&lt;/strong&gt;.&lt;/p&gt;&lt;h3&gt;Table of Contents&lt;/h3&gt;&lt;h1&gt;Background&lt;/h1&gt;&lt;p&gt;Did you know GitHub has this really cool feature
&lt;a href=&quot;https://docs.github.com/en/codespaces/the-githubdev-web-based-editor#about-the-githubdev-editor&quot;&gt;called github.dev&lt;/a&gt;?&lt;/p&gt;&lt;p&gt;On any repository you have access to, if you can change the url from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;github.com&lt;/code&gt;
to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;github.dev&lt;/code&gt; &lt;em&gt;or&lt;/em&gt; you click this little menu item:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/github-dev-dropdown-option.webp&quot; alt=&quot;Dropdown option to use github.dev on github file viewer&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;You’ll be launched into a little light-weight version of VSCode that runs
entirely in your browser (I guess that’s one advantage of having your app
written with electron).&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/github-dev-demo.png&quot; alt=&quot;CPython file opened in github.dev, a VSCode web interface&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;This browser instance of VSCode is pretty powerful, you can view all the files
in the repo (even if it’s a private one), you can send out pull requests and
even make commits.&lt;/p&gt;&lt;p&gt;This functionality is achieved by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;github.com&lt;/code&gt; POSTing over an OAuth token to
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;github.dev&lt;/code&gt; that allows it to interact with GitHub on your behalf. The token is
&lt;strong&gt;not scoped to the particular repo you interacted with&lt;/strong&gt;, meaning it has full
access to every other repo that you have access to.&lt;/p&gt;&lt;p&gt;The presence of this token and the fact that this web-app is running almost the
entire brunt of VSCode’s million line Typescript codebase makes it a great
target for anyone looking into VSCode bugs. That sort of bug is what we’ll
explore here and show how an attacker can use it to exfiltrate your GitHub token.&lt;/p&gt;&lt;h1&gt;VSCode Webview Security Model&lt;/h1&gt;&lt;p&gt;Being an electron app on the desktop, executing arbitrary Javascript inside of
VSCode would be tantamount to full remote code execution. This is why VSCode
implements some sandboxing approaches, the one we’ll focus on here is
&lt;a href=&quot;https://code.visualstudio.com/api/extension-guides/webview&quot;&gt;VSCode’s webviews&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Webviews use an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Same-origin_policy&quot;&gt;different origin&lt;/a&gt;
to the main VSCode window to ensure that any JavaScript executed inside of them
is fully isolated. These webviews are used for features such as Markdown
previews or editing Jupyter notebooks:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/iframe-jupyter-notebook.png&quot; alt=&quot;HTML render inside an iframe in a Jupyter notebook&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The output of the cell is rendered into an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; from the origin
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vscode-webview://...&lt;/code&gt;, as opposed to the main electron window which has the
origin &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vscode-file://...&lt;/code&gt;. This means that even if the Jupyter notebook uses
the built-in features of &lt;a href=&quot;https://stackoverflow.com/a/35760941&quot;&gt;displaying HTML&lt;/a&gt;
or &lt;a href=&quot;https://ipywidgets.readthedocs.io/en/latest/&quot;&gt;using Javascript for interactive widgets&lt;/a&gt;,
the actual core VScode application is protected from it. One cannot use
Electron’s integration with Node.js APIs inside this iframe or call into
VSCode’s APIs from this frame.&lt;/p&gt;&lt;p&gt;Great, that gives us the ability to render content, but just static content is
boring. How do we implement features like having the Markdown preview show you
which source line you currently have highlighted or updating the preview live
as we edit it?&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/markdown-preview-selection-marker.png&quot; alt=&quot;Markdown preview showing corresponding source line&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The same cross-origin policy that gives us security also prevents our main
editor window from interacting with the DOM in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vscode-webview://...&lt;/code&gt; frame.
After all, you wouldn’t want someone who used an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;iframe src=&amp;quot;google.com&amp;quot;&amp;gt;&lt;/code&gt; to
be able to interact with the google page to steal your cookies or change that
website’s behavior.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&amp;gt; document.getElementsByTagName(&amp;#39;iframe&amp;#39;)[0].contentWindow.findElementById(&amp;#39;foo&amp;#39;)
Uncaught SecurityError: Failed to read a named property &amp;#39;findElementById&amp;#39; from &amp;#39;Window&amp;#39;: 
Blocked a frame with origin &amp;quot;vscode-file://vscode-app&amp;quot; from accessing a cross-origin frame.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The only way to allow this behavior is to have the two web pages in the
different origins cooperate with each other using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage&quot;&gt;Window.postMessage() API&lt;/a&gt;.
This method allows sending JavaScript objects across the different
windows. So in that example of showing which rendered Markdown line corresponds
to what editor line, the main editor window posts a little message like this:&lt;/p&gt;&lt;p&gt;and then the corresponding code running inside of the webview has a listener for
this message &lt;a href=&quot;https://github.com/microsoft/vscode/blob/e4074382086b61bdfb0b6738e0853b2510660d57/extensions/markdown-language-features/preview-src/index.ts#L250&quot;&gt;that adds the highlight&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: VSCode in the browser uses a similar sandboxing model. VSCode developer Matt
Bierner has a &lt;a href=&quot;https://blog.mattbierner.com/vscode-webview-web-learnings/&quot;&gt;great blogpost about the challenges of porting it over from
Electron worth checking out&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;&lt;h2&gt;The Bug&lt;/h2&gt;&lt;p&gt;So our security boundary for webviews roughly looks like this:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/postmessage-boundary.png&quot; alt=&quot;Webview security boundary&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;but in terms of UI, our webview sits right here in the window. People expect
basic things like clicking links, drag and or pressing &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;F&lt;/kbd&gt;
to work inside of them:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/webview-shown-in-ui.png&quot; alt=&quot;Webviews as they appear in the layout of VSCode&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Hence, VSCode implements a bunch of basic functionality through the message
passing mechanism to enable these features.
Speaking of keyboard shortcuts, the astute reader who has dealt with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;s
may have already picked up on the issue.&lt;/p&gt;&lt;p&gt;As with most things cross-origin, the browser offers a good amount of isolation
between the two frames.
If you had a page on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hackerman.com&lt;/code&gt; and you
iframed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google.com/login&lt;/code&gt;, you would not want the hackerman page to be able to
attach a keyboard listener onto the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iframe&lt;/code&gt;. That would let them see all your
keystrokes on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;google.com&lt;/code&gt;, allowing them snoop your password.&lt;/p&gt;&lt;p&gt;Okay given that information, try clicking inside a VSCode webview and then
pressing &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;Shift&lt;/kbd&gt;+&lt;kbd&gt;P&lt;/kbd&gt; to bring up the command
palette.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/command-palette.png&quot; alt=&quot;VSCode command palette&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Oh yay, that works. Wait. Oh. Oh no.&lt;/strong&gt; So, to avoid the terrible user
experience of your keyboard shortcuts not working when you happen to be clicked
inside of a webview, the default set of webview message handlers &lt;a href=&quot;https://github.com/microsoft/vscode/blob/e4074382086b61bdfb0b6738e0853b2510660d57/src/vs/workbench/contrib/webview/browser/webviewElement.ts#L249-L254&quot;&gt;have an event&lt;/a&gt;
called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;did-keydown&lt;/code&gt;. When you load a webview, the following code runs inside
the webview to register a handler for it:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;contentWindow.addEventListener(&amp;#39;keydown&amp;#39;, handleInnerKeydown);

/**
 * @param {KeyboardEvent} e
 */
const handleInnerKeydown = (e) =&amp;gt; {
    // ...
    hostMessaging.postMessage(&amp;#39;did-keydown&amp;#39;, {
        key: e.key,
        keyCode: e.keyCode,
        code: e.code,
        shiftKey: e.shiftKey,
        altKey: e.altKey,
        ctrlKey: e.ctrlKey,
        metaKey: e.metaKey,
        repeat: e.repeat
    });
};&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;How convenient, so webviews just bubble up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; events so the main VSCode
window can treat them seamlessly as user keyboard events.&lt;/p&gt;&lt;p&gt;But…there’s
nothing preventing our script running in the untrusted web view from pretending
like it’s the user and pressing a bunch of keys on their behalf.&lt;/p&gt;&lt;p&gt;We could, say, bring up the command palette and start running dangerous commands
such as installing an attacker-controlled extension. All we’d need is a bit of
javascript that emits the correct events to simulate the keystrokes…&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;&lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;Shift&lt;/kbd&gt;+&lt;kbd&gt;P&lt;/kbd&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;developer: install extension from location&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;kbd&gt;Enter&lt;/kbd&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;attacker controlled extension&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;kbd&gt;Enter&lt;/kbd&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;In reality it’s not quite that simple. While we can certainly send
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; events corresponding to that sequence, the browser will not treat
it as if it’s the user typing it in. So VSCode will pop up the command palette
but unless VScode is intercepting all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; events to handle each character
being typed manually, our events will not actually type text into the palette.
Unfortunately, in this case here, it is not listening to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; events, the
command palette widget just uses an HTML &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;input&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;&lt;p&gt;We can scroll up and down in the command palette if we emit up-arrow &lt;kbd&gt;↑&lt;/kbd&gt;,
down-arrow &lt;kbd&gt;↓&lt;/kbd&gt; presses and can press &lt;kbd&gt;Enter&lt;/kbd&gt; to select commands but
arbitrary keystrokes are off the table.&lt;/p&gt;&lt;p&gt;Luckily, VSCode comes with a massive set of default keyboard shortcuts, all of
which listen directly on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; that we can try to make use of. After
a bunch of tinkering, the easiest way I found is to make use of the
&lt;em&gt;“Notifications: Accept Notification Primary Action”&lt;/em&gt; action.
This default keybind of &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;Shift&lt;/kbd&gt;+&lt;kbd&gt;A&lt;/kbd&gt; will
hit the primary button on whatever notification popped up last in VSCode.&lt;/p&gt;&lt;p&gt;Which notification are we accepting?&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/notification-recommended-extension.png&quot; alt=&quot;Recommended extension notification in VSCode&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;VSCode has this feature where your workspace can &lt;a href=&quot;https://code.visualstudio.com/docs/configure/extensions/extension-marketplace#_recommended-extensions&quot;&gt;recommend extensions&lt;/a&gt;
by putting them in a file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vscode/extensions.json&lt;/code&gt; that looks something
like&lt;/p&gt;&lt;p&gt;And then we can use &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;Shift&lt;/kbd&gt;+&lt;kbd&gt;A&lt;/kbd&gt;
to accept that notification and install the malicious extension giving us full
code execution? Shrimple as?&lt;/p&gt;&lt;p&gt;Again, not quite, VSCode as of 1.97 has this &lt;a href=&quot;https://code.visualstudio.com/docs/configure/extensions/extension-runtime-security#_extension-publisher-trust&quot;&gt;new publisher trust system&lt;/a&gt;
whereby installing an extension from a new publisher for the first time gives
you this dialog, even if we hit the &lt;em&gt;Install&lt;/em&gt; button in that notification:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/trusted-publisher-dialog.png&quot; alt=&quot;Publisher trust dialog in VSCode&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;While we can send &lt;kbd&gt;Tab&lt;/kbd&gt; key presses to navigate the buttons here,
pressing &lt;kbd&gt;Enter&lt;/kbd&gt; on the &lt;em&gt;Trust Publisher &amp;amp; Install&lt;/em&gt; button is impossible
as it listens for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; events specifically on the button and not the entire
window.&lt;/p&gt;&lt;p&gt;Instead, we can make use of another VSCode feature called &lt;strong&gt;local workspace extensions&lt;/strong&gt;.
As long as you are inside of a trusted workspace (which github.dev/web workspaces
always are),
then it’s possible to &lt;a href=&quot;https://code.visualstudio.com/updates/v1_89#_local-workspace-extensions&quot;&gt;install an extension directly present in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vscode/extensions&lt;/code&gt;&lt;/a&gt;.
Extensions installed in this way skip the trusted publisher check with the
trusted workspace check acting as the trust check. So now we can just put our
evil payload in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vscode/extensions/extension.js&lt;/code&gt; and execute our own code, right?&lt;/p&gt;&lt;p&gt;Well almost, doing this causes a Content Security Policy (CSP) error because the
extension worker that loads extensions is expecting them to be from
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vscode-cdn.net&lt;/code&gt;. Local workspace extensions probably weren’t well tested with
the web version of VSCode.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/csp-error-local-ext.png&quot; alt=&quot;Content security policy violation in local extension&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;This is just a small hiccup though, one of the things that extensions can do
as part of their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; is to contribute extra keybindings to VSCode.
Since we can reliably trigger keybindings, we can just add a keybind for
whatver VSCode command we want. Such as…installing an extension while skipping
the trusted publisher check. So our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; ends up looking like this
to call into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workbench.extensions.installExtension&lt;/code&gt; while skipping the
publisher trust check.&lt;/p&gt;&lt;p&gt;To put it all together, what we need is a repo with a &lt;strong&gt;Jupyter notebook&lt;/strong&gt; and a
&lt;strong&gt;local workspace extension&lt;/strong&gt;. The Jupyter notebook needs to execute a little bit
of Javascript which we can do with a markdown cell containing the following:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&amp;quot;data:foobar&amp;quot; onerror=&amp;quot;javascript(); goes(); here();&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;For our Javascript payload, we need to do the following:&lt;/p&gt;&lt;ol&gt;
  &lt;li&gt;Wait a little bit for VSCode to pop-up asking us if we want to install the
recommended extensions.&lt;/li&gt;
  &lt;li&gt;Emit a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; event for &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;Shift&lt;/kbd&gt;+&lt;kbd&gt;A&lt;/kbd&gt; to
accept the notification.&lt;/li&gt;
  &lt;li&gt;Wait a little bit for the extension to install and active, putting in our
custom keybind.&lt;/li&gt;
  &lt;li&gt;Emit a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; event for &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;F1&lt;/kbd&gt; triggering the
installation of our chosen extension.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;This payload ends up looking like:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;// Wait for VSCode to load and pop open the notification.
await sleep(10 * 1000);

// ctrl+shift+a, accept the primary notification asking if we want to install
// the recommended extension
window.dispatchEvent(
  new KeyboardEvent(&amp;quot;keydown&amp;quot;, {key: &amp;quot;a&amp;quot;, code: &amp;quot;KeyA&amp;quot;, keyCode: 65, 
                                ctrlKey: true, shiftKey: true})
);
// Wait a little for the extension to install...
await sleep(500);

// ctrl+f1, the custom keybind to install the chosen extension.
window.dispatchEvent(
  new KeyboardEvent(&amp;quot;keydown&amp;quot;, {key: &amp;quot;F1&amp;quot;, code: &amp;quot;F1&amp;quot;, keyCode: 112, ctrlKey: true})
);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h1&gt;PoC and Protecting Yourself&lt;/h1&gt;&lt;p&gt;Now that we’ve seen the details, let’s take a look at the proof-of-concept.
For the bravest among you, go ahead and just directly click:&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://github.dev/ammaraskar/github-dev-token-steal-poc/blob/main/README.ipynb&quot;&gt;https://github.dev/ammaraskar/github-dev-token-steal-poc/blob/main/README.ipynb&lt;/a&gt;&lt;/p&gt;&lt;p&gt;This will launch the github.dev editor directly to the notebook. You’ll see a
little status message of what the Javascript payload is doing.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/poc-status-page.png&quot; alt=&quot;Proof-of-concept initial page&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Once the payload runs, the newly installed extension will grab your GitHub API
token and then query &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://api.github.com/user/repos&lt;/code&gt; to get private repos
you have access to. It then prints them out and your token in a little information box.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/poc-info-box.png&quot; alt=&quot;Private repos printed by proof-of-concept&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The code for both the repos used is here:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Installed extension&lt;/strong&gt;: &lt;a href=&quot;https://github.com/ammaraskar/vscode-github-token-grab-extension/blob/main/src/extension.ts&quot;&gt;https://github.com/ammaraskar/vscode-github-token-grab-extension/blob/main/src/extension.ts&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Notebook JS&lt;/strong&gt;: &lt;a href=&quot;https://github.com/ammaraskar/github-dev-token-steal-poc/blame/main/README.ipynb&quot;&gt;https://github.com/ammaraskar/github-dev-token-steal-poc/blame/main/README.ipynb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;If you run the PoC, remember to either clear your github.dev data (see below) or at
the very least uninstall the proof-of-concept extension otherwise it will follow
you on all github.dev pages.&lt;/p&gt;&lt;p&gt;This vulnerability also exists in the desktop version of VSCode, though it’s a
bit harder to exploit since you would need to convince the victim to clone your
repo and open the notebook with the webview script payload. Of course, if you
had some other XSS in a webview that you can get a victim to open, you get
effectively full RCE on their computer.&lt;/p&gt;&lt;h2&gt;Protecting Yourself&lt;/h2&gt;&lt;p&gt;By a stroke of luck, if you have never used github.dev in the past, there is
one dialog to click through when landing on the website. This didn’t used to
happen before but some changing of VSCode’s GitHub plugins has caused this.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/initial-sign-in-dialog.png&quot; alt=&quot;Initial sign in dialog&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;This means that if you clear your cookies and local site data for github.dev,
you can take action and navigate away from the page if someone tries to use
this attack on you. I strongly recommend you clear site data for github.dev,
in Chrome this can be done by clicking the little icon in the URL bar,
clicking &lt;strong&gt;Cookies and site data&lt;/strong&gt; &amp;gt; &lt;strong&gt;Manage on-device site data&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/manage-site-data.png&quot; alt=&quot;Manage site data in Chrome&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;and then deleting data for all the domains with the trash can icons:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/delete-site-data.png&quot; alt=&quot;Delete site data in Chrome&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Unfortunately, if you’ve ever been past that dialog on github.dev and haven’t
cleared your browser’s local storage, you’re completely screwed. There are no
CSRF tokens or anything for github.dev so any link on the internet can redirect
you to this attack.&lt;/p&gt;&lt;h1&gt;What VSCode Did Well&lt;/h1&gt;&lt;p&gt;VSCode’s approach of not just solely relying on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; but using defense-in-depth
security measures like a strict &lt;a href=&quot;https://en.wikipedia.org/wiki/Content_Security_Policy&quot;&gt;Content Security Policy&lt;/a&gt;
and using &lt;a href=&quot;https://github.com/cure53/dompurify&quot;&gt;DOMPurify&lt;/a&gt; for rendered markdown
pays dividends here. If there was a way to execute arbitrary Javascript inside
the Markdown preview shown on an extension’s page, you can imagine how this
vulnerability could have even more impact (1-click RCE on desktop by linking
someone your extension). However, by using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;script-src &amp;#39;none&amp;#39;&lt;/code&gt; this is
effectively nipped in the bud.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://blog.ammaraskar.com/images/vscode/extension-page-script-src-csp.png&quot; alt=&quot;Content security policy in extension view&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h1&gt;Why Full Disclosure?&lt;/h1&gt;&lt;p&gt;To summarize the last time I interacted with &lt;a href=&quot;https://blog.ammaraskar.com/vscode-rce/#microsoft-security-and-vscode&quot;&gt;MSRC regarding reporting a VSCode
bug&lt;/a&gt;, it was a horrible experience where
they silently fixed the bug I pointed out without any credit. They also marked
it as not having any security impact. As I mentioned in that post, going forward
I would be doing full public disclosure for any security bugs I found in VSCode.
Taking a look at a &lt;a href=&quot;https://starlabs.sg/blog/2025/05-breaking-out-of-restricted-mode-xss-to-rce-in-visual-studio-code/&quot;&gt;recent report by Starlabs on a VSCode XSS bug&lt;/a&gt;
marked as ineligible and low severity, it doesn’t look like MSRC has gotten any
better about VSCode bugs.&lt;/p&gt;&lt;p&gt;I’m sure the VSCode team would have appreciated a longer heads up on this to come
up with solutions. There is legitimately a UI/UX balance here that needs to be
struck with the security concerns. To those folks, I am sorry, but this is one
of the few levers I have to try to influence MSRC and the security posture of
VSCode. Finding and fully developing security bugs into proof-of-concepts
like this takes time and effort on the part of security researchers that should
not be disrespected or taken for granted.&lt;/p&gt;&lt;h1&gt;Timeline&lt;/h1&gt;&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Jun 2, 2026&lt;/strong&gt; - An hour before posting I gave a heads up to an old contact
                  at GitHub security that I would be disclosing this bug.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Jun 2, 2026&lt;/strong&gt; - I disclosed the bug here and on the &lt;a href=&quot;https://github.com/microsoft/vscode/issues/319593&quot;&gt;VSCode issue tracker&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;</content:encoded>
</item>
<item>
<title>【Tenorshare PixPretty】AIポートレート写真編集ツールを試してみた</title>
<link>https://www.haurin-zatunenlife.com/entry/tenorshare-pixpretty?utm_source=feed</link>
<enclosure type="image/jpeg" length="0" url="https://cdn.image.st-hatena.com/image/scale/18868620226f60c065683fcd460b8beaec3db5ac/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Ft%2Ftwistinhaurin%2F20260520%2F20260520182323.jpg"></enclosure>
<guid isPermaLink="false">MiRo4XcIb3UBnOdla5EYLa1IXc6Ko2wRyO4C_Q==</guid>
<pubDate>Wed, 03 Jun 2026 01:24:46 +0000</pubDate>
<description>AIポートレート写真編集ツールの「Tenorshare PixPretty」を試めしてみました。こちらはSNSに載せる写真や、証明写真、ウェディング写真など肌の色合いやニキビやシミなんかをいい感じに編集出来るツールです。初心者からプロのフォトグラファーまで幅広く活用できる機能を搭載。AI による自然な肌補正・顔 / ボディの細かい調整に対応し、ワンクリックプリセットとバ...</description>
<content:encoded>&lt;center&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260520/20260520182323.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/center&gt;&lt;p&gt;SNSに載せる写真や、証明写真、ウェディング写真など肌の色合いやニキビやシミなんかをいい感じに編集出来たらいいのになぁと思ったことないですか？&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;そんな編集ができる&lt;strong&gt;AIポートレート写真編集ツール&lt;/strong&gt;の&lt;span&gt;&lt;strong&gt;「&lt;a href=&quot;https://x.gd/4gvfG&quot;&gt;Tenorshare PixPretty&lt;/a&gt;」&lt;/strong&gt;&lt;/span&gt;を試す機会をいただいたので、どんな事が出来るのか詳しく試してみました。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;こちらは&lt;span&gt;初心者からプロのフォトグラファーまで幅広く活用できる機能を搭載&lt;/span&gt;しています。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;AI による自然な肌補正・顔 / ボディの細かい調整に対応し、ワンクリックプリセットとバッチ処理で編集効率を大幅に向上させる次世代画像加工ツール。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;編集出来るのはいいけど、加工し過ぎて本来とは別人ではちょっとどうなのかと思いますが、&lt;strong&gt;「&lt;a href=&quot;https://x.gd/7rFj2X&quot;&gt;Tenorshare PixPretty&lt;/a&gt;」&lt;/strong&gt;は、加工し過ぎず自然な感じに仕上げる事ができるんです。&lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;Tenorshareとは&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;・「Tenorshare PixPretty」を開発しているのは香港に拠点を置く&lt;/p&gt;&lt;p&gt;Tenorshare(Hongkong)Limited」です。&lt;/p&gt;&lt;p&gt;・ 2007年に設立され、データ復元やシステム修復、AIツールなど多様なソフトウェアをグローバルに展開する企業で、日本語の公式サイトや日本法人によるサポートも提供されています。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;Tenorshare PixPrettyの特徴&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;・肌や顔の輪郭を簡単に整え、ニキビやシミなども、やりすぎず自然に仕上げる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・高度なAIによるカラー調整と豊富なフィルターを搭載し、写真の雰囲気を思い通りに演出できる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・豊富なアーティスティックフィルターライブラリを掲載しており、様々な撮影シーンにピッタリな雰囲気を演出できる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・写真はワンクリック、ドラッグ＆ドロップでアップロード可能。複数の写真に対して一括で編集やプリセット適用などができ、初心者でも手軽に操作できるので写真編集時間を大幅短縮。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・主要カメラのRAW画像処理に完全対応。写真のもつ本来のポテンシャルを最大限に引き出せる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・編集の強さを自在に調整できるカスタマイズ機能を掲載。全ての写真でプロフェッショナルな仕上がりを実現できる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・継続的なアップデートにより、最新技術を常に利用でき優れた写真編集体験を実現。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・自分好みにレタッチ機能を自由に組み合わせてプリセットとして保存、適用可能。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;・カーズ調整やスマート露出など、包括的なカラーグレーディング機能を掲載。プロレベルの色彩やライティング補正を簡単に実現。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;Tenorshare PixPrettyの機能&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;あらゆるシーンをプロ並みに補正&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・各クライアントの個性を正確に引き出すポートレート作成や、自然でロマンティックな瞬間を手軽に美しく残せるウェディング写真、完璧な印象を残せる証明写真、究極の商業ビジュアルを実現できる広告、ファッション写真など作成できる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;気になる肌悩みを整えて滑らかな素肌へ&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・1クリックで顔や体のニキビ、シミ、ほくろ、シワなどの肌の欠点を自動補正し、 自然な質感を保ちながら滑らかな美肌を作り出せる。&lt;/p&gt;&lt;p&gt;・肌色補正とレタッチにより肌の輝きを引き出し、明るく透明感のある均一な仕上がりに整える。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260519/20260519213306.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;・肌の質感を残しながら目元の細かいシワやほうれい線、首のシワを自然に補正でき、顔だけでなく、年齢が出やすいデコルテや首元を簡単に若々しく見せたい場合に非常に強力なツールです。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260519/20260519213302.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;顔をリシェイプして理想のフェイスラインを実現&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・フェイスリキファイ機能を使い顔の輪郭を細くしたり、鼻を高くしたり目を大きくしたり、眉を整えたり、唇をふっくらさせたりと、各パーツを細かく調整可能。自然でリアルな理想のフェイスラインを作り出せる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;AIボディスカルプティングで理想のプロポーションを実現&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・ボディ調整機能を使って、写真の体型を自由に補正可能。&lt;/p&gt;&lt;p&gt;・バストやヒップをふっくらさせたり、ウエスト・脚・腕を細くしたりして、理想的な黄金比のボディラインを瞬時に作り出す。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;自然で美しい分け目へ、わずか数秒でふんわりボリュームアップ&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・まばらになりがちな分け目も、数秒で手軽に補正。&lt;/p&gt;&lt;p&gt;・AIレタッチにより、気になる隙間を自然になじませながら、髪にほどよい密度とボリューム感をプラス。&lt;/p&gt;&lt;p&gt;・スタイリングや特別なケアをしなくても、より豊かで健やかな印象の髪へ仕上げれる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;Tenorshare PixPretty ダウンロード＆インストール&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、Tenorshare PixPrettyのサイトを開く。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://x.gd/4gvfG&quot;&gt;【公式】Tenorshare PixPretty ― プロフェッショナルAIポートレートレタッチツール&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、「無料ダウンロード」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504151750.jpg&quot; alt=&quot;Tenorshare PixPrettyのサイトの画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、ダウンロードされたアイコンをWクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504151755.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトのアイコン&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、「インストール」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;※「インストール設定」から言語とインストール先を選べます&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504151759.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトをインストールする時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;5&lt;/span&gt;、インストールされたらログインします。アカウントを持っていたら「メールアドレス」入れて「次へ」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504151806.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトを登録する時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;持っていなかったら、アカウントをお持ちでないですか？の「登録」を開いて下さい。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;「メールアドレス」を入力して「認証」をクリックすると、アドレスに認証コードが届くので30秒以内に入力して、好きな「パスワード」を入れて「登録」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504151811.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトに登録する時の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;これで「Tenorshare PixPretty」が使える様になりました(^^)/&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;Tenorshare PixPrettyを試してみる&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、Tenorshare PixPrettyを開き、＋マークをクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504154548.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトのトップ画面（ほぼ真っ暗）&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、プロジェクト名を入れて「プロジェクトを作成する」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504195208.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトの画像編集でタイトルをつける時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、画像やフォルダをインポート、またはドラック＆ドロップします。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260504/20260504195212.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトで画像編集で画像を取り込む時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、ここで、いろいろな調整をしていきます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260506/20260506180343.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトで画像を編集する時の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;①&lt;/span&gt; 表示倍率の調整、 ドラッグ操作、プリセットの3つが並んでいます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;②&lt;/span&gt; このエリアではレタッチ後の画像を確認できます。右下の「比較表示」ボタンをクリックすると加工前後の比較も可能。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;③&lt;/span&gt; プロジェクト内の全画像をサムネイルで表示します。「＋」をクリックすると画像の追加が可能です。また、フィルターや並び替え機能で画像の管理も行えます。スター付きマークでの画像分類にも対応しています。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;④&lt;/span&gt; プラン購入、カスタマーサポートや、マイページからは残り枚数の確認、ソフトのアップデート、ログアウトなどが行えます。他に、現在書き出し中または完了済みのタスクの確認や、レタッチが完了した画像をこちらから書き出せます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;⑤&lt;/span&gt; 各機能モジュールの切り替えや、選択中モジュールに属するサブ機能の利用ができます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;5&lt;/span&gt;、上記でいろいろ調整した結果がこちらです。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;満足した編集ができたら&lt;span&gt;④&lt;/span&gt;にある「エクスポート」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;輪郭を絞って、後はシミなどを消してみたんですがキレイな地上がりなんじゃないでしょうか？&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260506/20260506180352.jpg&quot; alt=&quot;左を向いている女性&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260506/20260506180359.jpg&quot; alt=&quot;左を向いている女性&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;肌がかなりキレイですよね。&lt;/p&gt;&lt;p&gt;目のクマとか、瞳の調整、唇、眉毛などそれぞれ細かく調整できますよ！&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;6&lt;/span&gt;、保存先や画像フォーマットなどを選んで「エクスポート」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260506/20260506213854.jpg&quot; alt=&quot;Tenorshare PixPrettyソフトで画像を保存する時の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;これで完了です(^^)/&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;男性バージョンも試してみました。&lt;/p&gt;&lt;p&gt;簡単な編集しかしてないのですが、もっと細かい編集も出来るので是非試してみて下さい。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260506/20260506214144.jpg&quot; alt=&quot;男性が正面を向いている&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260506/20260506214148.jpg&quot; alt=&quot;男性が正面を向いている&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;操作の詳しいガイドは&lt;a href=&quot;https://x.gd/0dx1q&quot;&gt;こちら&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;Tenorshare PixPretty料金価格&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://x.gd/7rFj2X&quot;&gt;【公式】Tenorshare PixPretty を購入&lt;/a&gt;&lt;/p&gt;&lt;p&gt;一般の人が使うにはちょっと高いですね。&lt;/p&gt;&lt;p&gt;仕事で継続的に使う人に向いてそうです。&lt;/p&gt;&lt;p&gt;また、公式サイトでは頻繁にプロモーション活動が行われているので、価格をチェックしておくと良いかと思います。&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260518/20260518205353.jpg&quot; alt=&quot;pixprettyソフトの価格表&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;とはいえ、無料で新規ユーザーに7日間の無料トライアルがついていて、この期間は全ての機能を利用する事ができ、最大1,000枚の写真をインポートでき、最大10枚の写真を正常にエクスポートできます。&lt;/p&gt;&lt;p&gt;30日間返金保証で24時間365日のサポートが受けられます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;今回試してみて、かなり細かい調整が出来るのでビックリしました。&lt;/p&gt;&lt;p&gt;少しでもキレイに仕上げたい人にはとてもいいツールなのではないでしょうか。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;ただ上にも書いたとおり、輪郭とか凄く変わるワケではないのですが、ほどほどにしておくのがいいと思います。&lt;/p&gt;&lt;p&gt;ほうれい線がなくなるのは嬉しいですが！&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;それでは、最後まで読んで頂いてありがとうございました。&lt;/p&gt;&lt;p&gt;いつもありがとうございます。&lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;</content:encoded>
</item>
<item>
<title>便利で使える【HIX.AI】オールインワンAIエージェントを使ってみた</title>
<link>https://www.haurin-zatunenlife.com/entry/hix-ai?utm_source=feed</link>
<enclosure type="image/jpeg" length="0" url="https://cdn.image.st-hatena.com/image/scale/69d03cb1261fdb7c20837ebc47f0db3ec3ecb9fc/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Ft%2Ftwistinhaurin%2F20260404%2F20260404161054.jpg"></enclosure>
<guid isPermaLink="false">xJ1RAQVM7ROYrgCLU7cmO-P1SfkHnvfVEhIXWg==</guid>
<pubDate>Wed, 03 Jun 2026 01:24:46 +0000</pubDate>
<description>AIを使ったツールは沢山ありますが、いろんなツールを使っているとお金もかかるし面倒くさい。そんな面倒くさい状態から解放してくれるのが「HIX.AI」です。「HIX.AI」を開くだけで文章、画像、動画、スライド生成、ライティングなどなどを網羅できます。オールインワンAIエージェント。しかも高速、高精度。初めて使ってみましたが3ステップで作れてしまい簡単で...</description>
<content:encoded>&lt;center&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260404/20260404161054.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/center&gt;&lt;p&gt;AIを使ったツールいろいろありますよね。&lt;/p&gt;&lt;p&gt;あちこちいろんなツールを使っているとお金もかかるし面倒くさい。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;そんな面倒くさい状態から解放してくれるのが&lt;span&gt;&lt;strong&gt;「HIX.AI」&lt;/strong&gt;&lt;/span&gt;です。&lt;/p&gt;&lt;p&gt;文章、画像、動画、スライド生成、高度な検索（ディープリサーチ）を網羅するオールインワンAIエージェント。&lt;/p&gt;&lt;p&gt;しかも高速、高精度なコンテンツとの事。&lt;/p&gt;&lt;p&gt;今回試す機会があったのでいつもの事ながら素人でも簡単で使いやすいか試してみたんですが、かなり使えます。&lt;/p&gt;&lt;p&gt;凄くシンプルな画面で、操作も3ステップで難しい事抜きで使える。&lt;/p&gt;&lt;p&gt;どんな機能があるのかなど少し紹介できればと思います。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;HIX.AIの特徴&lt;ul&gt;&lt;li&gt;多様なAIモデルの統合利用 &lt;/li&gt;&lt;li&gt;高性能なAIライティングと編集&lt;/li&gt;&lt;li&gt;高度な情報収集&lt;/li&gt;&lt;li&gt;オールインワンのメディア生成（画像・動画）&lt;/li&gt;&lt;li&gt;高い利便性とマルチ対応&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;AI画像生成を試してみた&lt;ul&gt;&lt;li&gt;手持ちの画像を変換してみよう&lt;/li&gt;&lt;li&gt;画像を生成してみよう&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;PPTを作成してみよう&lt;/li&gt;&lt;li&gt;プラント価格&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;HIX.AIの特徴&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;複雑で複数ステップにわたるタスクを自律的に処理する、賢いオールインワンAIエージェントで、深掘りリサーチから資料作成、データ分析まで、仕事や学習のパフォーマンスを最大化するために必要なすべてのAIツールが揃っています。&lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;多様なAIモデルの統合利用 &lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;Gemini 3.1 ProやGPT-5.4 Proといった最新モデルをサポート。市場の主要なAIチャットモデルを一括で利用でき、 使用目的に合わせてモデルを切り替えられる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;高性能なAIライティングと編集&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;ブログ、記事、広告文、ビジネスメールなど、高品質なコンテンツを瞬時に自動生成できる。&lt;/p&gt;&lt;p&gt;Googleドキュメントのような文書作成エディタを搭載しており、 AIによる文章の書き直し、拡張、要約、修正が可能。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;高度な情報収集&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;最新のアップデートで強化された「ディープリサーチ」機能により、オンライン上の膨大な情報を検索・精査しレポートとして瞬時に生成。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;オールインワンのメディア生成（画像・動画）&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;AI画像生成：チャットベースの指示で高精度な画像を生成&lt;/p&gt;&lt;p&gt;AI動画生成：テキストや画像からプレゼン資料やビデオ用素材の動画を高速に作成&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;高い利便性とマルチ対応&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;日本語を含む多言語で高性能な処理が可能。また、ブラウザ上で直接HIX.AIの機能が利用でき、SNSやメール作成をサポート。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;AI画像生成を試してみた&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;まず、簡単そうなので手持ちの画像を変換してみます。&lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;手持ちの画像を変換してみよう&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、HIX.AIのサイトを開きログイン。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://hix.ai/ja&quot;&gt;HIX.AI エージェントワークスペース&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、赤枠の＋の所から画像を選択 ⇨ プロンプトを入力 ⇨ 矢印マークをクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;「画像をジブリ風にして」と入力してみました。&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402143139.jpg&quot; alt=&quot;HIX.AIサイトの画像生成の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、少し待つと上の画像が見事なジブリ風になりました！&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402143359.jpg&quot; alt=&quot;赤い服装の象使い3人と象&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402143142.jpg&quot; alt=&quot;赤い服装象使い3人と象のジブリ風の絵&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;凄いですね(^^)/&lt;/p&gt;&lt;p&gt;自分が写ってる画像も変換したくなりました。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;こんな感じで変換してくれたみたいです。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402143145.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;もっと手の込んだ事も出来るみたいなのでかなり面白そうです。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;今度は自分で考えた内容を入力し、汎用エージェントに対し生成したい内容（画像、動画、PPT、レポートなど）を伝えるだけで、エージェントがユーザーの意図を識別し、スタイルが統一されたコンテンツをステップごとに生成します。&lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;画像を生成してみよう&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、&lt;/strong&gt;&lt;strong&gt;「&lt;a href=&quot;https://hix.ai/ja/ai-image&quot;&gt;AI画像&lt;/a&gt;」をクリックします。&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162314.jpg&quot; alt=&quot;HIX.AIサイトの画像生成の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、生成したい画像の内容を入力 ⇨ 矢印マークをクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;「芝生の上で遊んでいるマンチカンの子猫3匹。親は見守っている。天気は晴れていて周りには花が咲き誇っている」とファンタジーな内容を入力。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;※生成モデルや画面の比率など選べます&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162318.jpg&quot; alt=&quot;HIX.AIサイトの画像生成の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、ビックリする位想像の上をいっている画像が出来上がりました。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;簡単な文章でこれだけの画像が生成できます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162321.jpg&quot; alt=&quot;芝生の上で遊んでいるマンチカンの子猫3匹と、見守っている親猫。周りは花に囲まれている&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;出来上がった画像は編集できるので細かい調整も可能です。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;PPTを作成してみよう&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;次にPPTを作成してみました。&lt;/p&gt;&lt;p&gt;テキストを入力するだけで、プロ品質のプレゼン資料を瞬時に自動生成。構成の悩みや面倒なレイアウト調整をなくし、作業時間を大幅に削減します。最新の画像AINano Banana Proを搭載。AI特有の不自然な配図を解消し、文脈に完全にマッチした美しいビジュアルで説得力のあるスライドに仕上げます。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、下のサイトを開きます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://hix.ai/ja/ai-slides&quot;&gt;HIX.AIでAIスライド自動作成&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、「AIスライド作成」の所に作成したい内容を入れる ⇨ 矢印マークをクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;私は下の内容を入れてみました。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;「チョコレート専門店の新商品をアピールしたい。高級感がある感じで、どの世代にも伝わるように。新商品はミルクチョコレートに苺を使用した物と、ダークチョコレートにヘーゼルナッツが入っている。スライド4枚に納まるように。」&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162704.jpg&quot; alt=&quot;HIX.AIのPPTを作成する時の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、すると、直ぐに分析が始まります。&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162707.jpg&quot; alt=&quot;HIX.AIサイトでPPTを作成する時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162711.jpg&quot; alt=&quot;HIX.AIサイトでPPTを作成する時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;ポイントなどを確認した後、アウトラインが作成され生成に入ります。&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162714.jpg&quot; alt=&quot;HIX.AIサイトでPPTを作成する時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、出来上がるまでに少し時間はかかりましたが、もうAIにお願いしたら簡単に大体の事がお願いしたとおりに出来てしまいますね。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;商品のイメージに合わせたイラスト入りでプレゼン資料が出来上がりました！&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162717.jpg&quot; alt=&quot;ダークブラウンの背景にチョコレートを紹介する画像（文章のみ）&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162721.jpg&quot; alt=&quot;ダークブラウンの背景に苺ミルクチョコレートを紹介する文章と絵&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162724.jpg&quot; alt=&quot;ダークブラウンの背景にヘーゼルナッツダークチョコレートを紹介する文章と絵&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260402/20260402162727.jpg&quot; alt=&quot;ダークブラウンの背景にチョコレートを宣伝する内容の文章&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;プラント価格&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;無料と有料プランがあります。&lt;/p&gt;&lt;p&gt;年払い？にするとHIX.AI無制限が50％OFFになるっぽい。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260404/20260404201441.jpg&quot; alt=&quot;HIX.AIの料金プラン表&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://hix.ai/ja/pricing&quot;&gt;プランと価格 | HIX AI&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;span&gt;&lt;strong&gt;あとがき&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;使ってみた感じ、HIX.AIがこちらの意図を読み取ってくれて思っている以上の画像を作ってくれました。&lt;/p&gt;&lt;p&gt;もっと詳細なプロンプトを入力して楽しんでみたくなります。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;また、プレゼン資料に困っている人にもかなり使えそうです。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;それでは、最後まで読んで頂いてありがとうございました。&lt;/p&gt;&lt;p&gt;いつもありがとうございます。&lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;</content:encoded>
</item>
<item>
<title>画像を高画質へ【HitPaw FotorPea】AI画像編集ソフトは古い画像を高画質にするのに最適</title>
<link>https://www.haurin-zatunenlife.com/entry/hitpaw-fotopea?utm_source=feed</link>
<enclosure type="image/jpeg" length="0" url="https://cdn.image.st-hatena.com/image/scale/e5af4a547e60ffd663dd61e12d70f1f57ef58b81/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Ft%2Ftwistinhaurin%2F20260207%2F20260207133327.jpg"></enclosure>
<guid isPermaLink="false">7dI9A5eY3xRSizlbnqr7ioEa3x8z_qRhMKeX5w==</guid>
<pubDate>Wed, 03 Jun 2026 01:24:46 +0000</pubDate>
<description>最近はAIを使用したソフトが多いですよね。似た様なソフトが多いので実際どれがいいのか分からない。今回、HitPawさんのAI画像編集ソフト「HitPawFotorPea」を試めす機会があったので使ってみました。素人でも分かりやすく操作は簡単なのか、元画像との違いなど見てみたのですが試してみた結果古いぼやけた写真などが高画質化で簡単にキレイに生まれ変わったのでか...</description>
<content:encoded>&lt;center&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260207/20260207133327.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/center&gt;&lt;p&gt;最近はAIを使用したソフトが多いですよね。&lt;/p&gt;&lt;p&gt;最先端のAI技術とはどれほどのものなのか？&lt;/p&gt;&lt;p&gt;似た様なソフトが多いので実際どれがいいのか分からない。&lt;/p&gt;&lt;p&gt;今回、&lt;strong&gt;HitPaw&lt;/strong&gt;さんのAI画像編集ソフト&lt;span&gt;&lt;strong&gt;&lt;a href=&quot;https://www.hitpaw.jp/fotorpea-photo-enhancer.html&quot;&gt;「HitPawFotorPea」&lt;/a&gt;&lt;/strong&gt;&lt;/span&gt;を試めす機会があったので使ってみました。&lt;/p&gt;&lt;p&gt;昔のボケてる画像などAIの力でキレイな画像にできたら嬉しいですよね。&lt;/p&gt;&lt;p&gt;素人でも分かりやすく操作は簡単なのか、元画像との違いなど試してみたのですが、使ってみた結果簡単に古い画像がクリアでハッキリと蘇りました。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;HitPawとは&lt;/li&gt;&lt;li&gt;HitPaw FotorPeaとは&lt;ul&gt;&lt;li&gt;画像高画質化&lt;/li&gt;&lt;li&gt;AIで画像を生成&lt;/li&gt;&lt;li&gt;オブジェクト削除&lt;/li&gt;&lt;li&gt;背景の削除&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;HitPaw FotorPea 料金価格&lt;/li&gt;&lt;li&gt;HitPaw FotorPeaをインストールする&lt;/li&gt;&lt;li&gt;画像高画質化の方法&lt;/li&gt;&lt;li&gt;AI画像生成の方法&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;HitPawとは&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;2007年創業の香港に本社があるソフトウェアを開発・販売する会社です。&lt;/p&gt;&lt;p&gt;あらゆるプラットフォームで、あらゆるデバイスで、素晴らしいビデオ・音楽・写真を作成し楽しみ共有できるように最高のマルチメディアソフトを提供することに取り組んでいます。&lt;/p&gt;&lt;p&gt;AIを利用した画像・動画の高画質化ソフトや拡張子変換ソフトなどを提供。&lt;/p&gt;&lt;p&gt;世界中で1000万人以上のユーザーがいます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;HitPaw FotorPeaとは&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;画像の高画質化、背景やオブジェクトの削除、AI画像生成のためのAI画像編集ソフト。&lt;/p&gt;&lt;p&gt;・ワンクリックでぼやけた画像を鮮明に&lt;/p&gt;&lt;p&gt;・画質を落とさずに写真を拡大・縮小できる&lt;/p&gt;&lt;p&gt;・テキストや画像からAI絵・AIイラストを自動生成&lt;/p&gt;&lt;p&gt;・画像の背景を簡単に削除したり置き換えたりできる&lt;/p&gt;&lt;p&gt;・写真からオブジェクト（いらないもの・見知らぬ人など）を削除&lt;/p&gt;&lt;p&gt;・最先端のAIポートレートジェネレーターで自然な仕上がりに&lt;/p&gt;&lt;p&gt;・ワンクリックで証明写真を作成&lt;/p&gt;&lt;p&gt;・AIモデルの数が多く、今後も更新され続ける&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;画像高画質化&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・AI画像高画質化が画像のアップスケールと高解像度化を実現&lt;/p&gt;&lt;p&gt;・フェースモデル、色補正、カラー化、スクラッチ修復、ノイズ除去など写真を鮮やかにするための様々な選択枠を提供。ぼけた画像を鮮明にするだけではなく、破損した写真や古い家族写真のトラブルを解決。&lt;/p&gt;&lt;p&gt;・肌、布地、影など、あらゆる画素をAIが自動で最適化。&lt;/p&gt;&lt;p&gt;・荒い画像やぼやけを綺麗にし自然な質感を保ちながら画質を改善。&lt;/p&gt;&lt;p&gt;・RAW画像やスマホ撮影の写真も対応。 解像度を上げてスタジオ品質の鮮明な仕上がりに。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320183559.jpg&quot; alt=&quot;同じゴールデンレトリバーの左にボケた画像と右に高画質化した画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;AIで画像を生成&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・テキストから画像へ言葉で説明するだけで、AIイラストがアイデアをビジュアル化。驚くほど美しい作品をAI自動生成。&lt;/p&gt;&lt;p&gt;・既存の画像をアーティスティックに再構築し、新しい魅力を生み出せます。&lt;/p&gt;&lt;p&gt;・Gemini Nano Bananaなど複数の高精度AIモデルを搭載で、100種類以上のスタイルテンプレートから自由に選択可能。&lt;/p&gt;&lt;p&gt;・生成したAIイラストは、各プラットフォームに最適化された形式で簡単にエクスポートできる。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320184051.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;オブジェクト削除&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・写真からいらない物を自動で一瞬にして削除。&lt;/p&gt;&lt;p&gt;・人物や文字、肌のシミやニキビ、キズなど消す事ができる。&lt;/p&gt;&lt;p&gt;・数回クリックするだけで簡単にオブジェクトを消去でき、複雑な編集スキルは一切不要。初心者でも直感的に使える。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;背景の削除&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・最高のAI背景削除ツールでワンクリックで背景を消去&lt;/p&gt;&lt;p&gt;・背景のごちゃごちゃや余計な要素を取り除くことで、スッキリとしたプロフェッショナルな画像を作成できる。&lt;/p&gt;&lt;p&gt;・様々なカラーオプションにより背景を入れ替えたりして証明写真などをデザインできる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;HitPaw FotorPea 料金価格&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;１ヶ月やる位なら１年版か永久版をオススメしているのですが結構高いです。&lt;/p&gt;&lt;p&gt;ですが、使える機能は多いのでかなり役立ちそう。&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260207/20260207141617.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの料金価格表&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;無料版で2～5回ほど試す事ができます。&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320203952.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの機能比較表&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;30日間の返金保証と、24時間年中無休のサポートがあるので安心ですね。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;HitPaw FotorPeaをインストールする&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、HitPaw FotorPeaのサイトを開く。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.hitpaw.jp/fotorpea-photo-enhancer.html&quot;&gt;[公式] HitPaw Photo Enhancer :写真を高画質化・画像アップスケールソフト&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、「無料でダウンロード」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260208/20260208143857.jpg&quot; alt=&quot;HitPaw FotorPeaのホームページのトップ画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、ダウンロードされたアイコンをWクリック。&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260208/20260208143901.jpg&quot; alt=&quot;HitPaw FotorPeaのダウンロードアイコン&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、「インストール」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;※「インストール設定」から「言語」、「インストール先」を選ぶ事ができます&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260208/20260208143904.jpg&quot; alt=&quot;HitPaw FotorPeaをインストールする画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;5&lt;/span&gt;、あっという間にHitPaw FotorPeaが使える様になりました。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320205608.jpg&quot; alt=&quot;HitPaw FotorPeaソフトを開いた時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;ライセンス（ログインID、パスワード）がある人は右上の方にある「アカウント情報」から登録できます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;画像高画質化の方法&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;昔撮ったぼやけた画像を高画質にしてみます。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、HitPaw FotorPeaを開きます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320205608.jpg&quot; alt=&quot;HitPaw FotorPeaソフトを開いた時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、「AI高画質化」から「高品質な修復」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320214358.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、「ファイルを選択」から高画質化したい画像を選びます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;※HitPawが提供するサンプル画像をクリックして、効果を確認することができます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214140718.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの高画質化する時の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、最適なモデルが自動的に選択されるので「プレビュー」で確認します。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214141653.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの高画質化する時の画面（どう設定して高画質化するか）&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;5&lt;/span&gt;、色や輪郭がハッキリしたかと思います。プレビュー結果が良ければ「1枚エクスポート」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214141658.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの高画質化する時の画面（古い画像と高画質化された画像）&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;※プレビュー画面に納得がいかない場合は右側の所からいろいろ調節できます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214145603.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像を調節する画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;顔復元&lt;/span&gt;：&lt;/strong&gt;顔のディテールを自然に保ちながら強化することができます。このモデルは特徴を際立たせ、肌の質感を改善し画像の欠点を修正するのに役立つ。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;ノイズ除去&lt;/span&gt;：&lt;/strong&gt;画像の不要なノイズを除去しつつ、画質を保つことができます。このモデルは、高ISO設定や暗所で撮影された写真をクリアにするのに役立ちます。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;テキスト&lt;/span&gt;：&lt;/strong&gt;クリエイティブなポスターや書類、標識などのテキストの鮮明さとシャープさを強調し、画像全体の品質を損なうことなく、テキストを際立たせることができる。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;アニメ&lt;/span&gt;：&lt;/strong&gt;エッジを滑らかにし色の鮮やかさを向上させ、デジタルアートやイラストレーターのファンに最適。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;修復と着色&lt;/span&gt;：&lt;/strong&gt;白黒またはグレースケールの画像に色を付けるAIツールです。画像の内容を解析し、写真やパターンに基づいてリアルな色を適用します。&lt;/p&gt;&lt;p&gt;上記の他にもいろいろ選ぶ事ができます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;※赤枠の中を操作して画像を確認する事もできます&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214142950.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像高画化の確認画面&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;div&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214142954.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像高画化の確認画面&quot; title=&quot;&quot;/&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;6&lt;/span&gt;、最終確認したい場合は👁のマークをクリック。良ければファイルのマークをクリックで保存され終了です。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214145559.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;あっという間に古い画像がくっきりキレイな画像になりました。&lt;/p&gt;&lt;p&gt;「画像アップスケジュール」からも花の画像でも試してみたのですが、花の輪郭が凄くキレイになってます。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320214915.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像高画質化の画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260214/20260214160211.jpg&quot; alt=&quot;ピンクのブーゲンビリア（左は古い画像、右は高画質化した後の画像）&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;これはいいですね。&lt;/p&gt;&lt;p&gt;古い画像全部高画質化したくなってきました！&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;AI画像生成の方法&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、HitPaw FotorPeaソフトを開き、「AI自動生成」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260305/20260305215927.jpg&quot; alt=&quot;HitPaw FotorPeaソフトのトップ画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、上の画像の左の「AI画像生成」を開くとこの様な表示になり「画像から画像生成」または、「テキストから画像生成」、既にあるスタイルから選ぶ事もできます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260320/20260320215438.jpg&quot; alt=&quot;HitPaw FotorPeaソフトのAI画像生成の時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、まず、「テキストから画像へ」を選択して試して見ます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;プロンプトに「世界中を旅している20代の女性」と入力してみました。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260307/20260307150355.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像生成する時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;「モデルを選択」を開くと「Gemini」や「Flux」とか選択できて面白いです。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260307/20260307150358.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像生成する時の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、「生成」をクリックして出来上がった画像がこちらです。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;「世界中を旅している20代の女性」という簡単な文章でも充分伝わる画像が出来上がったかと思います。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;なかなかに魅力的な女性の画像です(^^)/&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260323/20260323215411.jpg&quot; alt=&quot;海をバックに上半身だけ写っているブロンドの若い女性&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;プレビューして良かったらダウンロードして完了です。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;他にも「画像から画像へ」や「テンプレート」からなど、画像を選んだりプロンプトを入力して「生成」できます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260215/20260215155211.jpg&quot; alt=&quot;HitPaw FotorPeaソフトの画像生成の画像&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;初めての人は沢山あるスタイルから試してみるといいかもしれません。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;strong&gt;あとがき&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;とにかく画像編集がかなりぼやけていても、セピア色だったとしても高画質に生まれ変わるので使いやすい！と関心しました。&lt;/p&gt;&lt;p&gt;過去の古ぼけた写真を全部高画質にしたいです。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;画像生成は、プロンプトに日本語で入力しても内容通りの画像が作れたので簡単でした。&lt;/p&gt;&lt;p&gt;もっと細かい指示でも大丈夫そうなので、具体的な背景や髪の長さや髪の色、人種、又は有名アーティスト風などでも作れるかと思います。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;それでは、最後まで読んで頂いてありがとうございました。&lt;/p&gt;&lt;p&gt;いつもありがとうございます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;cite&gt;&lt;a href=&quot;https://www.haurin-zatunenlife.com/entry/hitpaw-voice-changer&quot;&gt;www.haurin-zatunenlife.com&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;cite&gt;&lt;a href=&quot;https://www.haurin-zatunenlife.com/entry/hitpaw-video-converter&quot;&gt;www.haurin-zatunenlife.com&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;</content:encoded>
</item>
<item>
<title>データ消去フリーソフト【4DDiG Partition Manager】を試してみた！データ復元は本当に不可能か？操作感レビュー</title>
<link>https://www.haurin-zatunenlife.com/entry/4ddig-partition-manager-data?utm_source=feed</link>
<enclosure type="image/jpeg" length="0" url="https://cdn.image.st-hatena.com/image/scale/3b0227b075418ce446f7833812c4c6faee09ebd6/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Ft%2Ftwistinhaurin%2F20260111%2F20260111135130.jpg"></enclosure>
<guid isPermaLink="false">T-KSO01p_w0RnBecvwxMVpZRfV426zi-UkxEdA==</guid>
<pubDate>Wed, 03 Jun 2026 01:24:46 +0000</pubDate>
<description>データの消去を使った事はありますか？「4DDiG Partition Manager」はディスクやパーティションの管理ができるソフトです。パソコンを廃棄したり、中古で販売する際、 データを完全に消去しないと個人情報が漏洩する危険性があるので怖いですよね。初期化だけでは十分ではなく、復元ソフトを使えば簡単にデータが復元されてしまうことも珍しくないので、ちゃんと出...</description>
<content:encoded>&lt;center&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260111/20260111135130.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/center&gt;&lt;p&gt;データの消去や復元フリーソフトを使った事はありますか？&lt;/p&gt;&lt;p&gt;今回は沢山あるソフトの中から&lt;span&gt;&lt;strong&gt;&lt;a href=&quot;https://4ddig.tenorshare.com/jp/4ddig-partition-manager.html&quot;&gt;「4DDiG Partition Manager」&lt;/a&gt;&lt;/strong&gt;&lt;/span&gt;を紹介します。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;「4DDiG Partition Manager」&lt;/strong&gt;はディスクやパーティションの管理ができるソフトです。&lt;/p&gt;&lt;p&gt;パソコンを廃棄したり、中古で販売する際、 データを完全に消去しないと個人情報が漏洩する危険性があるので怖いですよね。&lt;/p&gt;&lt;p&gt;初期化だけでは十分ではなく、復元ソフトを使えば簡単にデータが復元されてしまうことも珍しくないので、ちゃんと出データを消去したい。&lt;/p&gt;&lt;p&gt;そして、間違えて消去してしまっても復元できるのか？？&lt;/p&gt;&lt;p&gt;&lt;strong&gt;「4DDiG Partition Manager」&lt;/strong&gt;は削除されたパーティションを簡単かつ安全に復元できるという事なので、こういう事には相変わらず素人なんですが、初めて使う人でも分かりやすく簡単か試してみました。&lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;4DDiG Partition Manageとは、2007年に香港で設立されたITソリューション企業の&lt;span&gt;Tenorshare社から出ているソフトで、Windowsに対応しています。&lt;/span&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;4DDiG Partition Managerの特徴&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;div&gt;&lt;p&gt;・ワンクリックでWindows OSをSSDに移行、システム再インストールは不要&lt;/p&gt;&lt;p&gt;・ハードドライブのクローンを作成しデータをバックアップ&lt;/p&gt;&lt;p&gt;・簡単にWinPEブータブルディスクを作成し、ブートできないコンピュータでSSDのクローン作成やハードドライブの修復が可能&lt;/p&gt;&lt;p&gt;・コンピュータや他のストレージデバイスから、パーティションの修復やデータの回復を簡単に実行&lt;/p&gt;&lt;p&gt;・PCが無料でWindows 11にアップグレードできるかを手軽にチェックし、データを失うことなくMBRをGPTに変換してアップグレード&lt;/p&gt;&lt;p&gt;・ディスクスペースを最適化するために、パーティションのサイズ変更、分割、作成、削除、フォーマットを柔軟に調整でき、回復パーティションの削除にも対応&lt;/p&gt;&lt;/div&gt;&lt;p&gt;4DDiG Partition Managerを利用すれば、Windows OSのSSDへの移行に加え、ディスク全体またはパーティ ションのアップグレードやコピー作成、パーティションのサイズ変更・拡張・分割・作成・削除、さらにデ ータを失うことなくMBRからGPTへの変換を実現することができます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;4DDiG Partition Managerの機能&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;クローン作成&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・ディスクまたはパーティション全体のコピーを作成しデータのバックアップを行う事が出来る。&lt;/p&gt;&lt;p&gt;・既存のドライブから別のドライブへ、簡単かつ安全にデータをクローン作成または転送します。この機能によりデータを失うことなくデータをバックアップし、簡単に大容量のHDDにアップグレードできます。&lt;/p&gt;&lt;p&gt;・パーティションのデータクローン作成のために設計されたこのモードは、データを消失することなく、簡単にドライブ上のパーティションのデータを別のパーティションに転送またはバックアップすることができます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;OS移行&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・高速システム・データ移行ソリューションの4DDiG Partition Managerで、ワンクリックでディスクパーティションからより大容量のパーティションにシステムや大容量のファイルやフォルダを移行する事ができる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;パーティション管理&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・高速システム・データ移行ソリューション4DDiG Partition Managerは、ワンクリックでディスクパーティションからより大容量のパーティションにシステムや大容量のファイルやフォルダを移行することができます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;パーティションの復元&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・信頼性の高いパーティション回復ソフトウェアで、削除されたパーティションの回復、フォーマットされたパーティションの復元、RAW パーティションの修復を簡単かつ安全に行うことができます。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;MBRとGPTの変換&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・データを失うことなく、Windows 11 アップグレードのために MBR を GPT に簡単に変換します。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;大容量SSDにアップデート&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・小容量のドライブを大容量ドライブまたはSSDディスクに移行またはアップグレードできる。OS移行はインストールされたソフト、システムアップデート、ドライバ、カスタム設定などシステムドライブ上のすべてのデータを別のPCへ安全に移行できる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h4&gt;&lt;span&gt;&lt;strong&gt;OSの再インストールが不要&lt;/strong&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p&gt;・Windowsを再インストールすることなく、OSを新しいドライブに転送し時間と労力を節約できる。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;4DDiG Partition Managerをインストールする&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、4DDiG Partition Managerのサイトを開く。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://4ddig.tenorshare.com/jp/4ddig-partition-manager.html?utm_source=www.haurin-zatunenlife.com&amp;amp;utm_medium=partner&amp;amp;utm_campaign=4DDiG%20Partition%20Manager&amp;amp;utm_term=www.haurin-zatunenlife.com-hl-20241230&amp;amp;utm_content=hdd%20%E3%81%8B%E3%82%89%20ssd%20os%20%E7%A7%BB%E8%A1%8C&quot;&gt;【公式】4DDiG Partition Manager：Windows向けの無料パーティション管理ソフト&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、「無料でダウンロード」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20241222/20241222151245.jpg&quot; alt=&quot;4DDiG Partition Managerのホームページのトップ画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、ダウンロードされたアイコンをクリック。&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20241222/20241222151249.jpg&quot; alt=&quot;4DDiG Partition Managerのアイコン&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;イオンストールが始まるので画面の指示に従って進めていきます。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、最後に「完了」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20241222/20241222151253.jpg&quot; alt=&quot;4DDiG Partition Managerのインストール完了画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;5&lt;/span&gt;、4DDiG Partition Managerが起動してインストールは完了です。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260115/20260115204833.jpg&quot; alt=&quot;4DDiG Partition Managerソフトの画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;ライセンスを持っている人は右上の鍵のマークからメールアドレスとライセンスを入力しましょう。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;4DDiG Partition Managerを使ってパソコンデータを消去&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;データ消去を行う前に以下の点を確認しましょう&lt;/p&gt;&lt;p&gt;・バックアップを取る。一度消去するとデータを復元することは非常に困難になります。重要なファイルや写真などは事前に別の場所へ移動しましょう。&lt;/p&gt;&lt;p&gt;・消去範囲を明確にする ハードディスク全体を消去するのか、それとも特定のパーティションやフォルダだけを消去するのかを事前に決めておきましょう。&lt;/p&gt;&lt;p&gt;・消去後の用途を確認する パソコンを廃棄する場合や、中古販売する場合など、目的に応じた消去方法を選びましょう。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;※4DDiG Partition Managerは「ゼロフィル」と「NIST800－88パージ方式」二つの消去方法を提供しています。一度消去すると復元する事ができないので安心かと思います。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;1&lt;/span&gt;、4DDiG Partition Managerをインストールして起動する。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;外付けハードドライブのパーティションを消去する必要がある場合は、コンピュータに接続する。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;左のナビゲーションバーから「ツールボックス」を選択し、「データ消去」をクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260115/20260115205337.jpg&quot; alt=&quot;4DDiG Partition Managerソフトのデータ消去画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;2&lt;/span&gt;、&lt;span&gt;削除するディスクまたはパーティションを選択し、「消去の結果をプレビュー」します。&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260115/20260115210650.jpg&quot; alt=&quot;4DDiG Partition Managerソフトのデータ消去画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;3&lt;/span&gt;、&lt;span&gt;削除するディスクまたはパーティションを正しく選択したことを確認したら、「はい」ボタンをクリック。&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260111/20260111135121.jpg&quot; alt=&quot;4DDiG Partition Managerソフトでデータを消去する画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;span&gt;4&lt;/span&gt;、ディスクデータの消去には時間がかかります。ディスクデータの消去が成功したら、「完了」ボタンをクリック。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;※ディスクの損傷を防ぐため、ディスクの抜き取りやプログラムの終了は避けましょう&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260111/20260111135126.jpg&quot; alt=&quot;4DDiG Partition Managerソフトでデータを消去する画面&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;2026年版 データ消去フリーソフトお勧め&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://x.gd/UjalJ&quot;&gt;https://x.gd/UjalJ&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;パソコンのデータを完全に消去する方法&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://x.gd/47Gwz&quot;&gt;https://x.gd/47Gwz&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;4DDiG Partition Managerの料金プラン&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;月間ライセンスを使ってみて良かったらすぐ永久ライセンスにした方がお得ですね。&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn-ak.f.st-hatena.com/images/fotolife/t/twistinhaurin/20260104/20260104142842.jpg&quot; alt=&quot;4DDiG Partition Managerソフトの料金プラン&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;3日間無料トライアルはこちらから&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://x.gd/Ktuzh&quot;&gt;https://x.gd/Ktuzh&lt;/a&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;h3&gt;&lt;span&gt;&lt;strong&gt;4DDiG Partition Managerの口コミ&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;口コミを見てみると、「PCの整理に役立った」や、「パーティションを過て消す事無く初心者でも簡単に使えた」など高い評価が多かったです。&lt;/p&gt;&lt;p&gt;Windows 11へのアップグレードも問題なく出来たという声も多かったです。&lt;/p&gt;&lt;p&gt;いろんな機能があるので気になる方はユーザーレビューに目を通してみるのもいいかと思います。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;strong&gt;あとがき&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://4ddig.tenorshare.com/jp/4ddig-partition-manager.html&quot;&gt;4DDiG Partition Manager&lt;/a&gt;はデータのバックアップや、Windows 11へのアップグレード、誤って消してしまったデータの復元など初心者でも簡単に操作できます。&lt;/p&gt;&lt;p&gt;前回はHDDからSSDに変換を試してみたんですが、こちらは初心者には少し難しく感じましたが、データ復元など他の機能は分かり安いです。&lt;/p&gt;&lt;p&gt;仕事で大量のデータを扱う人、会社には凄く使えるソフトではないでしょうか？&lt;/p&gt;&lt;p&gt;値段も仕事で使うなら良心的です。&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;&lt;cite&gt;&lt;a href=&quot;https://www.haurin-zatunenlife.com/entry/4ddig-partition-manager&quot;&gt;www.haurin-zatunenlife.com&lt;/a&gt;&lt;/cite&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt;それでは、最後まで読んで頂いてありがとうございました。&lt;/p&gt;&lt;p&gt;いつもありがとうございます。&lt;/p&gt;&lt;p&gt;&lt;ins&gt; &lt;/ins&gt;&lt;/p&gt;&lt;p&gt; &lt;/p&gt;&lt;p&gt; &lt;/p&gt;</content:encoded>
</item>
<item>
<title>Learning Protocol Handler | Blog</title>
<link>https://u1f383.github.io/web/2026/01/18/learning-protocol-handler.html</link>
<guid isPermaLink="false">ZDg2zqrkeR3juht6AxoXKhhERGzUgx9bLW5c2Q==</guid>
<pubDate>Tue, 02 Jun 2026 18:14:25 +0000</pubDate>
<description>0. Murmur</description>
<content:encoded>&lt;h2&gt;0. Murmur&lt;/h2&gt;&lt;p&gt;It has been four months since I last wrote a post… pretty long, lol. The reason is not only that I took a longer break after a whole busy year, like playing the game, doing more exercise, and thinking about the meaning of life, but also that I tried to step out of my comfort zone (in every aspect).&lt;/p&gt;&lt;p&gt;At the end of October, I randomly asked Faith (@farazsth98) if he wanted to participate in the first-year &lt;a href=&quot;https://www.zeroday.cloud&quot;&gt;zeroday.cloud competition&lt;/a&gt;, and maybe we could team up to target Ubuntu. I viewed it as a side project to push myself to do more research on the Linux kernel, and I also wanted to know what it’s like to do research with researchers more senior than me. However, I didn’t expect things to turn out like that. It only took us about three weeks – from finding some unused bugs and one exploitable vulnerability to finishing the exploitation – which is crazy and unimaginable. After that, we spent some time optimizing it, and in the end, we successfully archived LPE on latest Ubuntu Server!&lt;/p&gt;&lt;p&gt;This journey sounds great and should have made me even more passionate about security research, right? But after coming back from Landon (zeroday.cloud was held with BHEU, which was in Landon), I felt burned out and had no energy to read code for no reason. I started thinking about why I do security research and what I am actually chasing. The bad feeling lasted for three weeks. During this period, I read blogs (not limited to security) and did some non-heavy work, like organizing notes. In my free time, I spent more time thinking about what I was stuck on. As I read more and thought more, I gradually found my passion back, because I could see the enjoyment of sharing in those posts. They were pure happiness, learning new things, sharing cool techniques and stuff like that, and that was what I had lost.&lt;/p&gt;&lt;p&gt;Now, I still have a big project on Linux kernel research, but I also read blogs and do research in areas that I am not familiar with, just for fun. That’s why there were no post on the blog for months, and why this new post is about web security.&lt;/p&gt;&lt;p&gt;I am neither an expert in web security nor someone with deep research experience in protocol handlers. As a result, I will only provide an overview of protocol handlers along with some of my research notes.&lt;/p&gt;&lt;p&gt;I also want to thank maple (@maple3142) for answering my question and sharing his knowledge!&lt;/p&gt;&lt;h2&gt;1. Introduction&lt;/h2&gt;&lt;p&gt;If you click a link that looks like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;quot;XXXX://&amp;quot;&lt;/code&gt; – where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;XXXX&lt;/code&gt; is not a common protocol such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https&lt;/code&gt; – you may see a prompt on the screen asking whether you allow a specific program to open it. For example, if I try to navigate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slack://XXXX&lt;/code&gt; in Safari, macOS will ask me: &lt;strong&gt;“do you allow this website to open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Slack&lt;/code&gt;”?&lt;/strong&gt; The “Slack” here is the specific program I mentioned earlier.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260117174451735.png&quot; alt=&quot;image-20260117174451735&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;protocol handler&lt;/strong&gt; describes the situation in which user clicks a custom procol link and then operating system attempts to forward the URL request to the corresponding program. On different operating systems, the relationship between a protocol and a program is defined in different ways.&lt;/p&gt;&lt;p&gt;On macOS, this relationship is defined in the file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist&lt;/code&gt;, which is also the preference file for the domain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;com.apple.launchservices.secure&lt;/code&gt;. You can easily read it in a human-readable format using the command &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defaults read &amp;lt;domain&amp;gt;&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ defaults read com.apple.LaunchServices/com.apple.launchservices.secure
{
    LSHandlers =     (
                {
            LSHandlerModificationDate = 0;
            LSHandlerPreferredVersions =             {
                LSHandlerRoleAll = &amp;quot;-&amp;quot;;
            };
            LSHandlerRoleAll = &amp;quot;com.apple.gamecenter.gamecenteruiservice&amp;quot;;
            LSHandlerURLScheme = &amp;quot;itms-gcs&amp;quot;;
        },
        ...
    );
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;On Windows, protocol handlers are defined in the registry, with the following format:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;HKEY_CLASSES_ROOT\&amp;lt;protocol_name&amp;gt;\shell\open\command&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;In my Windows VM, the handler for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vscode://&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Code.exe&lt;/code&gt; as shown in the screenshot below:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260117180033526.png&quot; alt=&quot;image-20260117180033526&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;On Linux, different distributions may use different mechanisms, so here I will take Ubuntu as an example. On Ubuntu, there is another concept called &lt;strong&gt;MIME types (Media Types)&lt;/strong&gt;. MIME types are used to identify file types and determine which applications should open them by default. You can use the following commands to find MIME handlers:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;# for per-user
grep -ir &amp;quot;MimeType=x-scheme-handler&amp;quot; ~/.local/share/applications/
# for system
grep -ir &amp;quot;MimeType=x-scheme-handler&amp;quot; /usr/share/applications/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;You will see file content like the following:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;MimeType=x-scheme-handler/XXXXX&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Here, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;XXXXX&lt;/code&gt; is protocol name. By opening the corresponding configuration file, you can further identify the program and the command format associated with that protocol. For example, the description of the protocol &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snap://&lt;/code&gt; is defined in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/share/applications/snap-handle-link.desktop&lt;/code&gt;. By reading the file, you can know that its handler is program &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/bin/snap&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;[Desktop Entry]
...
Exec=/usr/bin/snap handle-link %U
MimeType=x-scheme-handler/snap;
...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;2. Electron&lt;/h2&gt;&lt;p&gt;As &lt;a href=&quot;https://www.electronjs.org/docs/latest/&quot;&gt;its documentation&lt;/a&gt; described, &lt;strong&gt;Electron&lt;/strong&gt; is a framework for building desktop applications using JavaScript, HTML, and CSS, and it embeds &lt;strong&gt;Chromium&lt;/strong&gt; and &lt;strong&gt;Node.js&lt;/strong&gt; into its binary. By using &lt;strong&gt;Electron&lt;/strong&gt;, you only need to maintain one JS codebase to create cross-platform apps that work on Windows, macOS, and Linux!&lt;/p&gt;&lt;p&gt;A diagram from &lt;a href=&quot;https://blog.logrocket.com/advanced-electron-js-architecture/&quot;&gt;Advanced Electron.js architecture&lt;/a&gt; clearly shows the architecture of Electron.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260117202131871.png&quot; alt=&quot;image-20260117202131871&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The main process (blue one) of Electron is &lt;strong&gt;Node.js&lt;/strong&gt;, which provides delevopers with abundant APIs to use. If you have some knowledge of Chrome, I think its role is similar to the browser process, handling those requests that require high privileges from render processes.&lt;/p&gt;&lt;p&gt;The renderer process runs inside &lt;strong&gt;Chromium&lt;/strong&gt;, and it is responsible for rendering web pages by parsing HTML and CSS and running Javascript. Since Electron is used to build applications, it can be imagined that each application needs to control how the web pages are rendered and behave.&lt;/p&gt;&lt;p&gt;Electron exposes many JavaScript APIs that allow developers to hook into. When starting an application, a main script will be executed by Node.js (main process) to set up the environment. Later, when Chromium is loaded, the browser context has already been configured with application-specific behaviors.&lt;/p&gt;&lt;p&gt;One of the features Electron supports is &lt;strong&gt;custom protocol handling&lt;/strong&gt;. By using the API &lt;a href=&quot;https://www.electronjs.org/docs/latest/api/app#appsetasdefaultprotocolclientprotocol-path-args&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.setAsDefaultProtocolClient()&lt;/code&gt;&lt;/a&gt;, you can register an application as the handler of a specific protocol. For example, if I want to register my application to be the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myapp://&lt;/code&gt; protocol handler, I can run the following JS code in the main script:&lt;/p&gt;&lt;p&gt;On Windows and Linux, you can write code like the following to handle startup requests triggered by a deeplink:&lt;/p&gt;&lt;p&gt;Instead of handling requests inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.whenReady()&lt;/code&gt; callback, on macOS you must define an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;#39;open-url&amp;#39;&lt;/code&gt; event handler:&lt;/p&gt;&lt;p&gt;If a deeplink is triggered from within the application or from a browser while an existing instance is already running, Electron (which typically allows only a single application process) will first launch a second instance. This second instance sends a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;#39;second-instance&amp;#39;&lt;/code&gt; event to the main instance and then exits. As a result, you may need to define a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;#39;second-instance&amp;#39;&lt;/code&gt; event handler to handle this scenario.&lt;/p&gt;&lt;h2&gt;3. Obsidian&lt;/h2&gt;&lt;h3&gt;3.1. File Extraction&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://obsidian.md&quot;&gt;Obsidian&lt;/a&gt; is a free note-taking app based on Electron (btw, I’ve used this app to take research notes for two years, so you should give it a try!).&lt;/p&gt;&lt;p&gt;After installation, it registers the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;obsidian://&lt;/code&gt; protocol handler, which invokes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Obsidian.exe&lt;/code&gt; to handle the URL requests.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260117234251391.png&quot; alt=&quot;image-20260117234251391&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Obsidian is not an open-source project, but you can download its ASAR file from the &lt;a href=&quot;https://github.com/obsidianmd/obsidian-releases/releases/tag/v1.11.4&quot;&gt;GitHub release&lt;/a&gt;. &lt;strong&gt;ASAR (Atom Shell Archive)&lt;/strong&gt; is a file format used by Electron to package applications. This file is generated by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asar&lt;/code&gt; Node.js package, and you can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extract&lt;/code&gt; command to unpack it.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;npx asar extract obsidian-1.11.4.asar out&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Once the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;obsidian-XXXX.asar&lt;/code&gt; is extracted, you will find the following files:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ tree -L 1
.
...
├── app.js
..
├── main.js
├── package.json
...
└── worker.js&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The attribute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;quot;main&amp;quot;&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; defines which JS file is executed first. However, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.js&lt;/code&gt; is missing from the extracted directory. Why?&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;{
    ...
    &amp;quot;main&amp;quot;: &amp;quot;index.js&amp;quot;,
    ...
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;If you directly install Obsidian from the released &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.deb&lt;/code&gt; package on Ubuntu (my VM is Ubuntu haha), you’ll find that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/opt/Obsidian/resources&lt;/code&gt; contains not only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;obsidian.asar&lt;/code&gt; but also &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.asar&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;aaa@aaa:~$ ls -al /opt/Obsidian/resources
...
-rw-rw-r-- 1 root root    86730 Jan 12 22:46 app.asar
...
-rwxrwxr-x 1 root root 25878062 Jan 12 22:46 obsidian.asar&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;According to the &lt;a href=&quot;https://github.com/electron/electron/blob/5bd2938f6af2ef9060772796f02c3ac9c80d5cdb/lib/browser/init.ts#L199&quot;&gt;Electron source code&lt;/a&gt; and related posts, it appears that the archive named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.asar&lt;/code&gt; is the  one actually loaded. Inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.asar&lt;/code&gt;, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; file defines &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.js&lt;/code&gt; as the main JS script.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;{
    ...
    &amp;quot;main&amp;quot;: &amp;quot;main.js&amp;quot;,
    ...
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Its content looks more like what I would expect from the entry point of an Electron application. By reading the code, we can also see that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;obsidian.asar&lt;/code&gt; is loaded after the first stage of initialization.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;let asarPath = path.join(APP_PATH, &amp;#39;obsidian.asar&amp;#39;);

// [...]

function loadApp(asarPath) {
    // Execute asar content
    let main = path.join(asarPath, &amp;#39;main.js&amp;#39;);

    let fn;
    try {
        fn = require(main);
    } catch (e) {
        return false;
    }

    if (fn) {
        fn(asarPath, updateEvents);
        return true;
    }
    return false;
}

// [...]

if (!success) {
    log(&amp;#39;Loading main app package&amp;#39;, asarPath);
    success = loadApp(asarPath);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;3.2. Debugging&lt;/h3&gt;&lt;h3&gt;3.2.1. Runtime Patch&lt;/h3&gt;&lt;p&gt;Here I want to share how I debug Obsidian. To be honest, this is also the main reason why I wrote this post. It includes the basic Electron application debugging (which I didn’t know before) and runtime patches to enable Obsidian’s inspector.&lt;/p&gt;&lt;p&gt;Normally, an Electron application supports two ways to debug: &lt;strong&gt;DevTools&lt;/strong&gt; and &lt;strong&gt;Inspector&lt;/strong&gt;. I believe everyone has used DevTools before, but you may not expect that it is also embedded inside an Electron app.&lt;/p&gt;&lt;p&gt;For Obsidian, you can use the shortcut &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;option + command + I&lt;/code&gt; on macOS or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift + control + I&lt;/code&gt; on Ubuntu to open the DevTools.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260118112354908.png&quot; alt=&quot;image-20260118112354908&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;By opening the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sources&lt;/code&gt; tab, you can see which code is executed on this page. You can also set breakpoints and debug it directly.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260118112733927.png&quot; alt=&quot;image-20260118112733927&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;It is straightforward, right? However, this only debugs the current page running in the &lt;strong&gt;renderer process&lt;/strong&gt;. What about the main process, which is the &lt;strong&gt;Node.js process&lt;/strong&gt;? That is where the second method comes in: &lt;strong&gt;Inspector&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;If we mirror its role into gdb toolchain, Inspector is more like the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gdbserver&lt;/code&gt;, allowing us attach and debug a running renderer. A process that is not a renderer can also implement the Inspector protocol to support debugging, and this is exactly what Electron’s Node.js process does. The inspector is not enabled by default, but in most cases you only need to pass &lt;a href=&quot;https://www.electronjs.org/docs/latest/tutorial/debugging-main-process&quot;&gt;additional parameters&lt;/a&gt; to the application to enable it. For example:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;# expose inspector port at 9229 (default port)
app --inspect=9229

# break in the first line of code
app --inspect-brk&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;After that, you can open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chrome://inspect&lt;/code&gt; in Chrome and debug the Node.js process.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260118122235921.png&quot; alt=&quot;image-20260118122235921&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;However, when I run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;obsidian --inspect&lt;/code&gt; or similar commands, no inspector is launched. After some investigation, I suspect that Obsidian either modified Node.js code (or perhaps just set some options, not sure) to disable the Inspector.&lt;/p&gt;&lt;p&gt;I then opened my IDA to reverse the Obsidian ELF. By searching for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;quot;--inspect&amp;quot;&lt;/code&gt;, I found that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node::options_parser::DebugOptionsParser::DebugOptionsParser()&lt;/code&gt; is responsible for parsing debug-related parameters. By mapping the function to the &lt;a href=&quot;https://github.com/nodejs/node/blob/9bcfbeb236307c5a9cc558477598b4338ed398b6/src/node_options.cc#L433&quot;&gt;Nodejs source code&lt;/a&gt;, it clearly shows that this function parses debug arguments, including &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--inspect&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;DebugOptionsParser::DebugOptionsParser() {
  // [...]
  AddOption(&amp;quot;--inspect&amp;quot;,
            &amp;quot;activate inspector on host:port (default: 127.0.0.1:9229)&amp;quot;,
            &amp;amp;DebugOptions::inspector_enabled, // offset: 9
            kAllowedInEnvvar);
  AddAlias(&amp;quot;--inspect=&amp;quot;, { &amp;quot;--inspect-port&amp;quot;, &amp;quot;--inspect&amp;quot; });
  // [...]
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;But if you set a breakpoint at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DebugOptions::CheckOptions()&lt;/code&gt; and inspect &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argv&lt;/code&gt;, you will find that &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--inspect&lt;/code&gt; is missing, which means Obsidian does not pass the parameters to Electron at all!&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;One possible solution is a runtime patch. You can break at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node::inspector::Agent::Start()&lt;/code&gt;, which determines whether the Inspector should be started. One of the condition check is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;options.inspector_enabled&lt;/code&gt; flag &lt;a href=&quot;https://github.com/nodejs/node/blob/9bcfbeb236307c5a9cc558477598b4338ed398b6/src/inspector_agent.cc#L864&quot;&gt;must be true&lt;/a&gt;. This is the same flag that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--inspect&lt;/code&gt; is supposed to set. Here, we can simply set it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt; manually and then continue execution – the Inspector will start successfully!&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;bool Agent::Start(const std::string&amp;amp; path,
                  const DebugOptions&amp;amp; options,
                  std::shared_ptr&amp;lt;ExclusiveAccess&amp;lt;HostPort&amp;gt;&amp;gt; host_port,
                  bool is_main) {
  // [...]
  if (!parent_handle_ &amp;amp;&amp;amp;
      (!options.inspector_enabled || !options.allow_attaching_debugger ||
       !StartIoThread())) {
    return false;
  }
  // [...]
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The following GDB commands are what I used:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;b Agent::Start
set follow-fork-mode parent
r
# hit the breakpoint
set *(char *)($rdx + 9)=1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Log out:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;...
pwndbg&amp;gt; set *(char *)($rdx + 9)=1
pwndbg&amp;gt; c
Continuing.
[New Thread 0x76bd653f36c0 (LWP 44357)]
Debugger listening on ws://127.0.0.1:9229/6f96cdd8-bb69-4f55-b36f-bfffc2eb2ca8
For help, see: https://nodejs.org/en/docs/inspector
2026-01-18 05:27:15 Loading main app package /opt/Obsidian/resources/obsidian.asar
[New Thread 0x76bcd19ff6c0 (LWP 44358)]
...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Great! We can now debug the main process!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260118133123479.png&quot; alt=&quot;image-20260118133123479&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;But what if we want to debug the initialization of the main script? Is there any way to pause the Node.js process right at startup? Going back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DebugOptionsParser::DebugOptionsParser()&lt;/code&gt;, we can see another parameter, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--inspect-brk&lt;/code&gt;, which sets the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;break_first_line&lt;/code&gt; flag.&lt;/p&gt;&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;break_first_line&lt;/code&gt; flag is used in &lt;a href=&quot;https://github.com/nodejs/node/blob/9bcfbeb236307c5a9cc558477598b4338ed398b6/src/inspector_agent.cc#L1189&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node::inspector::Agent::WaitForConnectByOptions()&lt;/code&gt;&lt;/a&gt; to determine whether the Inspector should break at the first line and wait for the debugger to attach.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;bool Agent::WaitForConnectByOptions() {
  // [...]
  bool should_break_first_line = debug_options_.should_break_first_line();
  // [...]
  if (wait_for_connect || should_break_first_line) {
    // Patch the debug options to implement waitForDebuggerOnStart for
    // the NodeWorker.enable method.
    if (should_break_first_line) {
      CHECK(!parent_env_-&amp;gt;has_serialized_options());
      debug_options_.EnableBreakFirstLine();
      parent_env_-&amp;gt;options()-&amp;gt;get_debug_options()-&amp;gt;EnableBreakFirstLine();
    }
    client_-&amp;gt;waitForFrontend();
    return true;
  }
  return false;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;To enable it, we just need to run one additional GDB command:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;set *(char *)($rdx + 12)=1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Now the Inspector will pause execution and wait for us to attach the debugger!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260118134544951.png&quot; alt=&quot;image-20260118134544951&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h3&gt;3.2.2. Static Patch&lt;/h3&gt;&lt;p&gt;When I was writing this post, I found an easier way to start the Inspector lol. Since we can repack the ASAR file, we just need to add the following two lines of JS code to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.js&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;Repacking ASAR files is straightforward:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;cd /opt/Obsidian/resources
npx asar extract app.asar ~/Downloads/app.unpacked
# ... patch file
npx asar pack ~/app.unpacked app.asar
cp app.asar /opt/Obsidian/resources/app.asar&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The Inspector will be launched as well.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260118140737247.png&quot; alt=&quot;image-20260118140737247&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;WOW, I feel like a stupid guy XD&lt;/p&gt;&lt;h3&gt;3.3. Find Vulnerabilities&lt;/h3&gt;&lt;p&gt;By searching for the string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;quot;second-instance&amp;quot;&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;quot;open-url&amp;quot;&lt;/code&gt;, you can easily locate the handler and start analyzing the minified JS code.&lt;/p&gt;&lt;p&gt;In fact, I don’t really have any experience finding protocol handler vulnerabilities, and I didn’t even find any web bugs, so… there are that many things to share in this part :p. However, the protocol handlers have been a widely known attack surface for a long time, and you can find plenty of resources discussing them. For example, Obsidian previously had &lt;a href=&quot;https://forum.obsidian.md/t/possible-remote-code-execution-through-obsidian-uri-scheme/39743&quot;&gt;a potential RCE vulnerability&lt;/a&gt;, which happened in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hook-get-address&lt;/code&gt; command handler.&lt;/p&gt;&lt;p&gt;If you can execute arbitrary JS code or HTML (via XSS, markdown features, …) in an Electron application, it may lead to unexpected problems. In worst case, an attacker can run call arbitrary Node.js APIs and run system commands. &lt;a href=&quot;https://lsgeurope.com/post/0-click-rce-in-electron-applications&quot;&gt;This post&lt;/a&gt; explains several scenarios where unsafe Electron configurations can result in pretty bad problems.&lt;/p&gt;&lt;p&gt;There are three relatively important attributes in the Electron &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webPreferences&lt;/code&gt; configuration:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nodeIntegration&lt;/code&gt;: whether the renderer process can call Node.js APIs.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sandbox&lt;/code&gt;: whether the renderer process runs in OS-level sandbox and can only access limited resources.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;contextIsolation&lt;/code&gt;: whether the web page’s JS code is prevented from polluting the global JS environment, such as hijacking preloaded JS code.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;I draw a simple map to show where we should focus when looking at an Electron application:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://u1f383.github.io/assets/image-20260118215012083.png&quot; alt=&quot;image-20260118215012083&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;I’m working on it and hope to share somethings interesting in the future!&lt;/p&gt;&lt;h2&gt;4. Conclusion&lt;/h2&gt;&lt;p&gt;I think writing blogs is still beneficial, not only for sharing technical ideas, but also for organizing what I’ve learned. Hope I can keep doing this throughout the year!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Five Years of Trying to Add Recursion to lychee | Matthias Endler</title>
<link>https://endler.dev/2026/lychee-recursion/</link>
<enclosure type="image/jpeg" length="0" url="https://endler.dev/default.png"></enclosure>
<guid isPermaLink="false">w2iMfPvgG6t13yCgLYC0Z84ctgchkyJI9IWCnw==</guid>
<pubDate>Mon, 01 Jun 2026 21:03:59 +0000</pubDate>
<description>Recursion has been</description>
<content:encoded>&lt;p&gt;Recursion has been &lt;a href=&quot;https://github.com/lycheeverse/lychee&quot;&gt;lychee&lt;/a&gt;’s longest-standing open issue. It’s been sitting there, unresolved, for over five years now.&lt;/p&gt;&lt;p&gt;If you haven’t come across it before, lychee is a fast, async link checker written in Rust (BTW).
I started it in 2020 because I got bored at home. By now, around 40k GitHub repositories depend on it.
You point it at your website, your docs, your README, your Markdown files.
Google, AWS, Microsoft, Cloudflare, and many others use it to check links in their documentation.&lt;/p&gt;&lt;p&gt;I gave &lt;a href=&quot;https://www.youtube.com/watch?v=BIguvia6AvM&quot;&gt;talks&lt;/a&gt; and &lt;a href=&quot;https://www.youtube.com/watch?v=plEz4l7HwhY&quot;&gt;podcasts&lt;/a&gt; about it, in case you’d like to learn more.&lt;/p&gt;&lt;figure&gt;
    

    
    
    

    

    &lt;img src=&quot;https://endler.dev/2026/lychee-recursion/screencast.svg&quot; alt=&quot;lychee goes weeeee...&quot; title=&quot;&quot;/&gt;
    

    

    
        &lt;figcaption&gt;
            
            lychee goes weeeee…
            
            
            
        &lt;/figcaption&gt;
    
&lt;/figure&gt;&lt;p&gt;lychee got &lt;a href=&quot;https://nlnet.nl/&quot;&gt;funded by NLnet&lt;/a&gt; through their &lt;a href=&quot;https://nlnet.nl/NGI0/&quot;&gt;NGI Zero program&lt;/a&gt; for open, trustworthy infrastructure.&lt;/p&gt;&lt;p&gt;That funding allowed us to spend serious, focused time on the project instead of coding late at night.&lt;sup&gt;1&lt;/sup&gt;
The funding is now coming to an end, which feels like the right moment to write this post.&lt;/p&gt;&lt;p&gt;And the most honest thing I can say is this: the single most requested feature, recursion, still isn’t shipped. :,(
But there are good reasons! Of course, the gist is “it’s hard,” but let’s go deeper than that.&lt;/p&gt;&lt;h2&gt;
    
Where It Started&lt;/h2&gt;&lt;p&gt;On December 14, 2020, a user named &lt;a href=&quot;https://github.com/styfle&quot;&gt;&lt;strong&gt;@styfle&lt;/strong&gt;&lt;/a&gt; opened &lt;a href=&quot;https://github.com/lycheeverse/lychee/issues/78&quot;&gt;issue #78&lt;/a&gt;:&lt;/p&gt;&lt;figure&gt;
    

    
    
    

    
        &lt;picture&gt;
            
    

    &lt;img src=&quot;https://endler.dev/2026/lychee-recursion/issue78.jpg&quot; alt=&quot;The original recursion issue&quot; title=&quot;&quot;/&gt;
    
        &lt;/picture&gt;
    

    

    
        &lt;figcaption&gt;
            
            The original recursion issue
            
            
            
        &lt;/figcaption&gt;
    
&lt;/figure&gt;&lt;p&gt;Very reasonable! At that point, lychee was already a fast, concurrent link checker with a lot of features. Surely adding a little &lt;code&gt;--recursive&lt;/code&gt; flag to follow links within a domain could be done in an honest day’s work, no?&lt;/p&gt;&lt;p&gt;But five years, four serious implementation attempts, and several abandoned pull requests later, recursion still isn’t merged. The issue is tagged for the v1.0 milestone and we still want to ship it before that. But somewhere along the way it became lychee’s white whale.&lt;/p&gt;&lt;h2&gt;
    
My Initial Architecture Made It Hard&lt;/h2&gt;&lt;p&gt;To understand why recursion is so difficult to add, you need to understand how lychee processes things.
Here’s the flow from back in late 2020:&lt;/p&gt;&lt;figure&gt;
    

    
    
    

    

    &lt;img src=&quot;https://endler.dev/2026/lychee-recursion/initial-architecture.svg&quot; alt=&quot;lychee&amp;#39;s initial architecture&quot; title=&quot;&quot;/&gt;
    

    

    
        &lt;figcaption&gt;
            
            lychee’s initial architecture
            
            
            
        &lt;/figcaption&gt;
    
&lt;/figure&gt;&lt;p&gt;Basically one big pipeline, from input URLs over link extraction, to link checking, to output formatting.&lt;/p&gt;&lt;p&gt;When @styfle opened the issue, I &lt;a href=&quot;https://github.com/lycheeverse/lychee/issues/78#issuecomment-744720356&quot;&gt;spotted the core problem almost immediately&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;
&lt;p&gt;There is no connection back to the extractor.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;That missing feedback loop (from checked responses back to the input queue) is the whole problem in a nutshell. lychee’s pipeline was designed as a one-shot, unidirectional flow: inputs go in one end, results come out the other, and the program stops when the input stream stops.
Recursion needs a &lt;em&gt;cycle&lt;/em&gt;: responses have to be able to create new inputs. And cycles in async, channel-based pipelines are where the dragons live. 🐲&lt;/p&gt;&lt;p&gt;I knew this on day one. I just badly underestimated how many ways we’d find to get the cycle wrong.&lt;/p&gt;&lt;h2&gt;
    
Attempt 1: A Simple Counter (February - December 2021)&lt;/h2&gt;&lt;p&gt;My &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/165&quot;&gt;first attempt&lt;/a&gt; was deliberately small. I didn’t want to rearchitect anything; I just wanted recursion to work!
So I added the handling directly in &lt;code&gt;main.rs&lt;/code&gt;. The idea was:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;After receiving a response, extract links from it if it came from one of the original input domains.&lt;/li&gt;
&lt;li&gt;Push those new links back into the request channel.&lt;/li&gt;
&lt;li&gt;Keep a running count of total expected requests vs. completed requests.&lt;/li&gt;
&lt;li&gt;Stop when &lt;code&gt;completed == total&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;I added a &lt;code&gt;recurse()&lt;/code&gt; function that called &lt;code&gt;collector::collect_links()&lt;/code&gt; on successful responses, spawned a task to send the new requests into the channel, and returned how many new requests it created. A plain &lt;code&gt;HashSet&amp;lt;String&amp;gt;&lt;/code&gt; acted as a “seen” cache so I wouldn’t re-check the same URL twice.&lt;/p&gt;&lt;p&gt;On top of that:&lt;/p&gt;&lt;p&gt;Straightforward, right?&lt;/p&gt;&lt;h3&gt;
    
Wrong&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;The program wouldn’t terminate.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The termination logic was a &lt;code&gt;while curr &amp;lt; total_requests&lt;/code&gt; loop:&lt;/p&gt;&lt;p&gt;When responses arrive and generate new requests, &lt;code&gt;total_requests&lt;/code&gt; goes up.
So far so good.  But extraction, sending, and receiving all happen &lt;strong&gt;concurrently&lt;/strong&gt; across different tasks, so the count can get out of sync.&lt;/p&gt;&lt;p&gt;I wasn’t happy about it even at the time:&lt;/p&gt;&lt;blockquote&gt;
&lt;p&gt;TBH I’m not super happy with the current impl anymore as I count the links in the queue and then close the channel after all links got checked. It can lead to subtle bugs I think. There must be a better way.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;Yes, Matthias from the past, the counter is fragile because:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;New links are discovered asynchronously, so &lt;code&gt;total_requests&lt;/code&gt; can be bumped &lt;em&gt;after&lt;/em&gt; the loop has already decided to exit.&lt;/li&gt;
&lt;li&gt;If the count is off by even one, you either hang forever (count too high) or quit too early (count too low).&lt;/li&gt;
&lt;li&gt;And to add insult to injury, every edge case made the counting logic gnarlier. Cached responses, failed responses, empty pages,…&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/pawroman&quot;&gt;&lt;strong&gt;@pawroman&lt;/strong&gt;&lt;/a&gt; gave me a genuinely thorough review here, including a careful analysis of memory usage for the &lt;code&gt;HashSet&lt;/code&gt; cache (fine for up to millions of links), a suggestion to use signed depth values to express infinite recursion, and a nudge for integration tests. It was good feedback. It just couldn’t fix the thing that was actually wrong, which was the whole approach to termination.&lt;/p&gt;&lt;h3&gt;
    
The Death Blow&lt;/h3&gt;&lt;p&gt;In September 2021 we decided to do a bigger rewrite: a stream-based architecture (&lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/330&quot;&gt;PR #330&lt;/a&gt;) to improve concurrency. It changed &lt;code&gt;Collector::collect_links&lt;/code&gt; from returning a &lt;code&gt;Vec&lt;/code&gt; to returning a &lt;code&gt;Stream&lt;/code&gt;, removed the &lt;code&gt;ClientPool&lt;/code&gt; abstraction, and reshaped how tasks talked to each other. That was a great improvement as it meant that the collector was lazy and we wouldn’t allocate big &lt;code&gt;Vec&lt;/code&gt;s of requests anymore. But it also meant that the recursion branch was borked and got its rug pulled from underneath.&lt;/p&gt;&lt;blockquote&gt;
&lt;p&gt;Will put this on hold once again as we started implementing a stream-based approach in #330, which might supersede this branch soon. Sorry to everyone waiting on recursion support to land, but I’d like to get this right instead of merging a buggy solution prematurely.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/165&quot;&gt;PR #165&lt;/a&gt; was closed in December 2021. The stream refactor landed and gave us a 35–50% speedup. Nice! Tradeoffs, I guess.&lt;/p&gt;&lt;div&gt;
  &lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Counting outstanding work in an async pipeline is fragile.&lt;/strong&gt; An off-by-one in distributed counting means a deadlock or an early exit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Big refactors and feature branches don’t get along.&lt;/strong&gt; The stream rewrite made the recursion branch stale before it was ever ready.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recursion touches almost every layer.&lt;/strong&gt; This isn’t something you bolt on.&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;&lt;p&gt;And one honest aside on the language question, because I get asked it a lot: the counting problem here is &lt;strong&gt;not Rust’s fault&lt;/strong&gt;. A Go version with goroutines and channels, or a Python asyncio version, would hit the same off-by-one bugs. The race between “response processed” and “new requests discovered” is inherent to any concurrent recursive crawler. Rust’s &lt;code&gt;Stream&lt;/code&gt; trait and the way it plays with ownership made a streaming architecture feel natural, and that’s what invalidated the work. So that’s perhaps a Rust-specific point.&lt;/p&gt;&lt;h2&gt;
    
Attempt 2: Feed It Back Through a Channel (January - July 2022)&lt;/h2&gt;&lt;p&gt;Now that the stream architecture was in place, I took another stab at it. This time, instead of counting requests by hand, I’d &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/465&quot;&gt;feed discovered URLs back through a &lt;em&gt;channel&lt;/em&gt; connected to the collector&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The collector would read from an input channel and turn what it received into a stream of requests.
Recursion would just mean sending newly discovered URLs into that channel. (Look, a feedback loop!)
The stream would close naturally when the channel closed.&lt;/p&gt;&lt;p&gt;I also played with unifying the input type so one method could take either a &lt;code&gt;Vec&lt;/code&gt; or a &lt;code&gt;Stream&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;It hung. Again.&lt;/strong&gt; But for a completely different reason this time.&lt;/p&gt;&lt;p&gt;The feedback loop created a circular dependency:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;collector&lt;/strong&gt; reads from an input channel and produces a stream of requests.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;checker&lt;/strong&gt; reads requests and produces responses.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;recursion handler&lt;/strong&gt; reads responses and sends new inputs back to the collector’s channel.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Do you see the problem?&lt;/p&gt;&lt;p&gt;For the collector’s stream to end, the input channel has to close.
For the channel to close, all senders have to be dropped.
But the recursion handler holds a sender; it needs one to push discovered URLs back.
And the recursion handler only stops when there are no more responses, which only happens when there are no more requests, which only happens when the collector’s stream ends.
Another circular dependency causing a deadlock.&lt;/p&gt;&lt;p&gt;I said as much at the time:&lt;/p&gt;&lt;blockquote&gt;
&lt;p&gt;I had very little time to look at the issue so far, but it hangs because the input channel does not get dropped, leading to a dangling connection. I thought that the channel would be closed (and dropped) automatically once &lt;code&gt;futures::StreamExt::for_each_concurrent&lt;/code&gt; finishes.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/untitaker&quot;&gt;&lt;strong&gt;@untitaker&lt;/strong&gt;&lt;/a&gt; confirmed it and could reproduce the deadlock in even trivial cases:&lt;/p&gt;&lt;blockquote&gt;
&lt;p&gt;You want to drop the &lt;code&gt;sender&lt;/code&gt; once there’s nothing to process anymore right? But won’t &lt;code&gt;for_each_concurrent&lt;/code&gt; hang forever because you didn’t do that yet? (and can’t, because you need the sender for more cloning)&lt;/p&gt;
&lt;/blockquote&gt;&lt;blockquote&gt;
&lt;p&gt;I can repro a deadlock even with &lt;code&gt;time lychee --offline -b . &amp;#39;**/*.htm*&amp;#39; -T1&lt;/code&gt; on an empty directory.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;This is the heart of using channels for cyclic data flow: &lt;strong&gt;channels use sender-drop as their termination signal, but in a cycle you can never drop all the senders, because each stage needs to hold one to keep the cycle alive.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I took the problem to the Tokio Discord, and the advice that came back was: “Stop using channels for this. Use semaphores with &lt;code&gt;tokio::spawn&lt;/code&gt; instead.”&lt;/p&gt;&lt;h3&gt;
    
The Performance Problem Too&lt;/h3&gt;&lt;p&gt;Even ignoring the deadlock, there was a second issue. The new &lt;code&gt;from_chan&lt;/code&gt; method benchmarked roughly 30% slower than the existing &lt;code&gt;from&lt;/code&gt; method. The extra channel indirection cost something, and it cost it even in the non-recursive case, which is the case basically everyone uses.&lt;/p&gt;&lt;div&gt;
  &lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Channels are the wrong tool for cyclic pipelines.&lt;/strong&gt; Their close-on-last-sender-drop semantics are fundamentally at odds with a feedback loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;for_each_concurrent&lt;/code&gt; looks perfect and isn’t.&lt;/strong&gt; It processes a stream concurrently but gives you no way to feed items back in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The common path can’t get slower.&lt;/strong&gt; Recursion support is worthless if it taxes everyone who never uses it.&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;&lt;p&gt;The channel-cycle deadlock is &lt;strong&gt;inherent to any channel-based system&lt;/strong&gt;. Go channels have the same problem. Closing one means knowing nobody will send again, and a cycle makes that impossible. Erlang/OTP sidesteps it with process monitoring instead of channel semantics. The 30% regression, though, has a Rust angle. Rust’s zero-cost-abstraction culture means people (me included) expect to pay nothing for features they don’t use. In a runtime-heavy language, a 30% regression on an unused path might slide. In Rust, “you don’t pay for what you don’t use” is practically a moral position, and it made that regression a non-starter for me.&lt;/p&gt;&lt;h2&gt;
    
Attempt 3: Semaphores (February 2022)&lt;/h2&gt;&lt;h3&gt;
    
What I Tried&lt;/h3&gt;&lt;p&gt;I dropped channels for the recursion loop entirely and reached for:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Arc&amp;lt;Semaphore&amp;gt;&lt;/code&gt; to cap concurrency (replacing the channel’s natural backpressure)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tokio::spawn&lt;/code&gt; for each unit of work (replacing &lt;code&gt;for_each_concurrent&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OwnedSemaphorePermit&lt;/code&gt; handed to each task, so work could be “transferred” when spawning a recursive sub-task&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;The &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/489&quot;&gt;prototype&lt;/a&gt; was pretty clean, honestly:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;const MAX_CONCURRENCY: usize = 10;

fn recurse(permit: OwnedSemaphorePermit, i: usize) -&amp;gt; JoinHandle&amp;lt;()&amp;gt; {
    tokio::spawn(async move {
        handle_input(permit, i).await;
    })
}

async fn handle_input(permit: OwnedSemaphorePermit, i: usize) {
    println!(&amp;quot;got = {i}&amp;quot;);
    if i % 9 == 0 {
        recurse(permit, 10).await.unwrap();
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But I guess you can tell what the problem with it was: &lt;strong&gt;it still locked up.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;When I tried to bring this model into the real codebase, the ownership requirements got ugly fast. The link checker needs the client config, the cache, the progress bar, the stats, and a handful of other things. To share all of that across spawned tasks, it all wanted to be wrapped in &lt;code&gt;Arc&amp;lt;RwLock&amp;lt;State&amp;gt;&amp;gt;&lt;/code&gt;.
I tried this model on the branch, but it gets quite ugly because of ownership and &lt;code&gt;Send&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;
    
Semaphores Aren’t Enough&lt;/h3&gt;&lt;p&gt;A semaphore solves the concurrency-limiting problem. It does nothing for the &lt;em&gt;termination&lt;/em&gt; problem. With &lt;code&gt;tokio::spawn&lt;/code&gt;, there’s no built-in way to know when all spawned tasks — including the ones spawned recursively — have finished. You’d need a separate coordination mechanism, which is to say: you’d be reinventing the counter from Attempt 1, except now spread across an unbounded number of spawned tasks. We’d come full circle to the very thing I was trying to escape.&lt;/p&gt;&lt;p&gt;There’s a subtlety with the permits, too. Swapping &lt;code&gt;for_each_concurrent&lt;/code&gt; for raw &lt;code&gt;tokio::spawn&lt;/code&gt; loses the bounded concurrency that channels gave us for free. The semaphore adds it back, but you have to manage permits carefully. If a task acquires a permit, spawns a child, and transfers the permit, the parent can’t do more work. If it clones the permit, you can blow past your concurrency limit. Getting the permit lifecycle exactly right is fiddly.&lt;/p&gt;&lt;div&gt;
  &lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Semaphores solve concurrency, not termination.&lt;/strong&gt; You still need something to tell you “all the work is done.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Arc&amp;lt;RwLock&amp;lt;State&amp;gt;&amp;gt;&lt;/code&gt; is a code smell in async Rust.&lt;/strong&gt; When you start wrapping everything in locks, you’re fighting the ownership model instead of working with it. That can leave a lot of performance on the table since every access is a lock acquisition across all threads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The real question was never “how do I recurse?”&lt;/strong&gt; It was “how do I know when I’m done recursing.”&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;&lt;p&gt;This was the most Rust-specific failure of the bunch.
The semaphore approach is idiomatic in Go. A &lt;code&gt;sync.WaitGroup&lt;/code&gt; plus a semaphore channel, with state shared across goroutines via &lt;code&gt;sync.Mutex&lt;/code&gt; is how you’d do that in Golang because it has green threads and a runtime that manages goroutine lifecycles for you.&lt;/p&gt;&lt;p&gt;But in Rust, the &lt;code&gt;Send + &amp;#39;static&lt;/code&gt; bounds on &lt;code&gt;tokio::spawn&lt;/code&gt;, the borrow checker’s aversion to shared mutable state, and the cost of &lt;code&gt;Arc&amp;lt;RwLock&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; get in the way. Rust made the “just wrap everything in Arc and Mutex” escape hatch painful enough that it became a dead end.&lt;/p&gt;&lt;h2&gt;
    
2022–2024 😴&lt;/h2&gt;&lt;p&gt;For more than two years, the recursion issue kept collecting comments from people who wanted it.
People suggested workarounds (piping sitemap URLs through &lt;code&gt;xargs&lt;/code&gt; was a popular one). The person who originally filed it built &lt;a href=&quot;https://github.com/styfle/links-awakening&quot;&gt;their own tool&lt;/a&gt; and moved on, which I completely understood.&lt;/p&gt;&lt;p&gt;I was honest about it whenever it came up:&lt;/p&gt;&lt;p&gt;Someone offered a €100 bounty. Others pointed to &lt;a href=&quot;https://github.com/raviqqe/muffet&quot;&gt;muffet&lt;/a&gt;, which already does recursive checking. lychee wasn’t standing still during these years; a lot of work went into performance, caching, rate limiting, and other features. But recursion was the elephant in the room.&lt;/p&gt;&lt;h2&gt;
    
Attempt 4: Gwenn Takes a Swing (January – March 2025)&lt;/h2&gt;&lt;p&gt;In late 2024, a community contributor, &lt;a href=&quot;https://github.com/gwennlbh&quot;&gt;&lt;strong&gt;@gwennlbh&lt;/strong&gt;&lt;/a&gt;, &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/1603&quot;&gt;picked up the gauntlet&lt;/a&gt;. Their plan went back to the channel-based model but with a twist: instead of trying to close channels for termination, they used an &lt;code&gt;Arc&amp;lt;AtomicUsize&amp;gt;&lt;/code&gt; counter. Like Attempt 1, but atomic and shared across tasks!&lt;/p&gt;&lt;p&gt;And it looked so elegant:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;Keep the two existing mpsc channels (requests and responses).&lt;/li&gt;
&lt;li&gt;After receiving a response, extract links from the body and send them as new requests.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;Arc&amp;lt;AtomicUsize&amp;gt;&lt;/code&gt; to track remaining work — increment when new requests are sent (recursive ones included), decrement when a response is processed, and break out of the receive loop when it hits zero.&lt;/li&gt;
&lt;li&gt;Lean on the existing cache to avoid cycles (don’t re-check URLs already seen).&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;This was the most functional attempt yet. It &lt;em&gt;actually worked&lt;/em&gt; on real websites:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;lychee -R https://endler.dev \
       --recursed-domains endler.dev&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I was really excited watching it come together, and I tried to give useful design guidance along the way:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Default recursion depth of 5&lt;/li&gt;
&lt;li&gt;Strict domain matching (no subdomain checking)&lt;/li&gt;
&lt;li&gt;Rate limiting deferred to a separate PR&lt;/li&gt;
&lt;li&gt;Breaking changes to &lt;code&gt;lychee-lib&lt;/code&gt;’s public API accepted&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;
    
Where It Broke&lt;/h3&gt;&lt;p&gt;And then it hit the same wall, from several directions at once.&lt;/p&gt;&lt;h4&gt;
    
1. Channel Backpressure Deadlock&lt;/h4&gt;&lt;p&gt;When recursion discovered a lot of links, the response handler tried to send new requests into the request channel. But if that channel was full (bounded by &lt;code&gt;max_concurrency&lt;/code&gt;), the send blocked. A blocked response handler means no responses get processed, which means no request slots free up. &lt;strong&gt;Classic backpressure deadlock.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;@gwennlbh worked around it by spawning the “send new requests” work in a separate &lt;code&gt;tokio::spawn&lt;/code&gt;, decoupling response processing from request sending. It worked, but it meant there was no longer a limit on how many of these background tasks could pile up (and with that, use unbounded memory).&lt;/p&gt;&lt;h4&gt;
    
2. Duplicate Requests&lt;/h4&gt;&lt;p&gt;Because requests are processed in parallel, the same URL could be discovered by multiple pages and sent into the channel before any of them got cached. The cache check happened too late: after the request was already in flight. There was no per-URL synchronization to stop concurrent duplicates:&lt;/p&gt;&lt;blockquote&gt;
&lt;p&gt;Because of the parallel nature of the request-to-response task, it seems to me that sending the same request twice to the channel is hard to prevent. I tried adding guards basically everywhere […] and I still seem to get duplicates.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;As a stopgap, a dedup check went into &lt;code&gt;Stats::insert&lt;/code&gt;, but that only stopped duplicate &lt;em&gt;reporting&lt;/em&gt;, not duplicate &lt;em&gt;checking&lt;/em&gt;. The real fix would arrive much later, with the &lt;code&gt;HostPool&lt;/code&gt;’s per-URI &lt;code&gt;active_requests&lt;/code&gt; mutex, but that machinery didn’t exist yet.&lt;/p&gt;&lt;h4&gt;
    
3. The Counter, Yet Again&lt;/h4&gt;&lt;p&gt;The &lt;code&gt;Arc&amp;lt;AtomicUsize&amp;gt;&lt;/code&gt; counter is, at heart, the same idea as Attempt 1 — and it brought the same fragility. With &lt;code&gt;Ordering::Relaxed&lt;/code&gt; (the weakest memory ordering), increments and decrements across threads could be reordered, so the counter could briefly read zero before the work was actually done. On Wikipedia with &lt;code&gt;--max-depth=0&lt;/code&gt;, it would lock up on the very last URL.&lt;/p&gt;&lt;h4&gt;
    
4. Changes Everywhere&lt;/h4&gt;&lt;p&gt;Adding &lt;code&gt;subsequent_uris&lt;/code&gt; (the list of discovered links) to the &lt;code&gt;Response&lt;/code&gt; type meant touching nearly every file that builds or consumes a &lt;code&gt;Response&lt;/code&gt;. Every &lt;code&gt;Response::new()&lt;/code&gt; call needed two new arguments (&lt;code&gt;vec![]&lt;/code&gt; and &lt;code&gt;0&lt;/code&gt; for the non-recursive case).&lt;/p&gt;&lt;h4&gt;
    
5. The Collector Got Bypassed&lt;/h4&gt;&lt;p&gt;To extract links from response bodies, the code built a fresh &lt;code&gt;Collector&lt;/code&gt; inline in the checker, sidestepping the configured collector that respects user flags like &lt;code&gt;--exclude&lt;/code&gt;, &lt;code&gt;--include&lt;/code&gt;, and fragment checking.&lt;/p&gt;&lt;h3&gt;
    
The End of That Road&lt;/h3&gt;&lt;p&gt;After a burst of energy in January 2025, things slowed. Merge conflicts piled up. CI linting rules changed underneath the branch. @gwennlbh switched to Windows and couldn’t get the OpenSSL dependency to build. In March 2025 they wrote, honestly:&lt;/p&gt;&lt;blockquote&gt;
&lt;p&gt;even though I was kinda denying it, it’s pretty clear that I’ve lost motivation to keep working on this […] I’m sorry T_T&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;I didn’t want them to apologize. They got further than anyone, on a hard feature, in a complex async codebase, as a volunteer. My own note on the PR a while later was just the sober truth:&lt;/p&gt;&lt;div&gt;
  &lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The atomic counter is a manual counter in a trenchcoat.&lt;/strong&gt; It had the same failure modes.&lt;/li&gt;
&lt;li&gt;When you’re adding &lt;code&gt;vec![]&lt;/code&gt; and &lt;code&gt;0&lt;/code&gt; to every &lt;code&gt;Response::new()&lt;/code&gt; call, that’s a leaky abstraction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outside contributors face extra friction.&lt;/strong&gt; Build-environment differences, conflicts with a moving target, and the sheer cognitive load of a big async codebase make this an especially brutal feature to contribute.&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;&lt;p&gt;How much of the issues were Rust-specific?
I’d say around half.
The backpressure is simply part of the problem space.
Any concurrent crawler in any language meets that.
The &lt;code&gt;Ordering::Relaxed&lt;/code&gt; trap is somewhat Rust-specific in that Rust makes you &lt;em&gt;choose&lt;/em&gt; a memory ordering (Go’s &lt;code&gt;sync/atomic&lt;/code&gt; does too, but most Go folks reach for &lt;code&gt;sync.WaitGroup&lt;/code&gt; instead).&lt;/p&gt;&lt;h2&gt;
    
So Why Is This Actually Hard?&lt;/h2&gt;&lt;p&gt;Four attempts in five years.
If we take a step back, I think the difficulties can be grouped into a few categories:&lt;/p&gt;&lt;h3&gt;
    
Knowing When You’re Done&lt;/h3&gt;&lt;p&gt;Every implementation faced the same question: &lt;strong&gt;how do you know when you’re finished?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;In a non-recursive pipeline the answer is easy. You’re done when the input stream is exhausted and the in-flight requests have completed.
Close the channel sender, drain the receiver, and Bob’s your uncle.&lt;/p&gt;&lt;p&gt;In a recursive pipeline the input stream is never truly exhausted, because every response might create new inputs. You need a separate way to detect &lt;em&gt;quiescence&lt;/em&gt;: the state where nothing is in progress and nothing new will be generated.&lt;/p&gt;&lt;p&gt;Turns out, the problem has a name in distributed systems: ✨ &lt;strong&gt;distributed termination detection&lt;/strong&gt;. ✨&lt;/p&gt;&lt;p&gt;The classic solutions (&lt;a href=&quot;https://en.wikipedia.org/wiki/Dijkstra%E2%80%93Scholten_algorithm&quot;&gt;Dijkstra–Scholten&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/Token_passing&quot;&gt;token passing&lt;/a&gt;) just don’t map well onto Tokio’s channel-based world.&lt;/p&gt;&lt;h3&gt;
    
The Cycle&lt;/h3&gt;&lt;p&gt;lychee’s architecture is fundamentally a DAG.
Inputs flow one direction through the stages.
Recursion introduces a cycle.
And cycles in channel-based systems deadlock, because channels use “all senders dropped” as their done signal, and in a cycle that condition is never met on its own.&lt;/p&gt;&lt;h3&gt;
    
Backpressure&lt;/h3&gt;&lt;p&gt;Bounded channels give you natural backpressure: if the checker is slow, the sender blocks until there’s room.
Which is lovely, until you want recursion.
Now the &lt;em&gt;response handler&lt;/em&gt; needs to send into the &lt;em&gt;request channel&lt;/em&gt;.
If that channel is full, the response handler blocks; if it blocks, no responses are consumed; if no responses are consumed, no request slots free up.&lt;/p&gt;&lt;h3&gt;
    
Deduplication Races&lt;/h3&gt;&lt;p&gt;We check links concurrently, which means multiple pages can hold the same link.
Without synchronization, several tasks discover the same URL and submit it before any of them can mark it “seen.”
Through attempts 1–4 the cache didn’t save us, because cache entries were written &lt;em&gt;after&lt;/em&gt; checking, not before submission.&lt;/p&gt;&lt;h3&gt;
    
Leaky Abstraction&lt;/h3&gt;&lt;p&gt;Recursion-awareness wants to live “everywhere.”
Responses need to carry discovered links, Requests need a depth, the collector needs to understand recursive inputs, stats and formatters need to handle duplicates.&lt;/p&gt;&lt;h3&gt;
    
How Much of This Is Rust’s Fault?&lt;/h3&gt;&lt;p&gt;I think this is the question people reading my blog really want answered, so let me be direct.
My honest estimate is… &lt;strong&gt;about 30%?&lt;/strong&gt;
The termination problem, the cycle problem, and the backpressure problem are all just part of the problem space.
Any concurrent recursive crawler, be it written in Go, Python, Java, or Erlang, has to solve that.
At some point, &lt;a href=&quot;https://www.scrapy.org/&quot;&gt;Scrapy&lt;/a&gt;, &lt;a href=&quot;https://github.com/gocolly/colly&quot;&gt;Colly&lt;/a&gt;, and the other mature crawling frameworks all had to do distributed termination detection and backpressure management.&lt;/p&gt;&lt;p&gt;What Rust adds is friction at the implementation level:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Ownership and &lt;code&gt;Send&lt;/code&gt; bounds make it harder to share state across spawned tasks. In Go you capture variables in a goroutine closure and move on. In Rust everything in async-land wants to be &lt;code&gt;Arc&lt;/code&gt;-wrapped and &lt;code&gt;Send + &amp;#39;static&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Explicit memory ordering on atomics forces you to think about concurrency correctness and also makes “eh, just use relaxed” a tempting but dangerous choice.&lt;/li&gt;
&lt;li&gt;Channel termination semantics in Tokio are stricter than in some other ecosystems. Go’s &lt;code&gt;context.Context&lt;/code&gt; gives you an orthogonal cancellation mechanism that Tokio channels don’t natively have. (In Tokio, you’d use a &lt;a href=&quot;https://tokio.rs/tokio/topics/shutdown&quot;&gt;CancellationToken&lt;/a&gt; for that.)&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;But on the other side, Rust also prevented a lot of issues:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;The compiler caught every unsafe attempt to share mutable state. In Go those would’ve been subtle runtime bugs I’d find in production or maybe with the race detector.&lt;/li&gt;
&lt;li&gt;Using the type system in our favor, we can make the right thing be the ergonomic thing.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Put another way, Rust made the &lt;em&gt;wrong&lt;/em&gt; approaches fail loudly and painfully e.g. with compiler errors (but also deadlocks in tests) and made the &lt;em&gt;right&lt;/em&gt; approach more solid and ergonomic.&lt;/p&gt;&lt;h2&gt;
    
A New Hope&lt;/h2&gt;&lt;p&gt;Despite all the failed attempts, the ground has quietly shifted under this problem in 2025–2026. A bunch of work, most of it not even about recursion, has made a real implementation finally look within reach.&lt;/p&gt;&lt;h3&gt;
    
Per-Host Rate Limiting (PR #1929, Merged December 2025)&lt;/h3&gt;&lt;p&gt;Recursion without rate limiting is dangerous.
Gwenn found that out firsthand by accidentally DDoS’ing their own WiFi router while recursively checking Wikipedia. 😬
Per-host rate limiting, which got merged in &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/1929&quot;&gt;PR #1929&lt;/a&gt;, makes recursive crawling respect server limits.
I previously waved this off as “out of scope” but it’s super important in practice.&lt;/p&gt;&lt;p&gt;The underlying issue (&lt;a href=&quot;https://github.com/lycheeverse/lychee/issues/1605&quot;&gt;#1605&lt;/a&gt;) was one I opened on January 6, 2025 — the same week &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/1603&quot;&gt;PR #1603&lt;/a&gt; (Attempt 4) opened. That timing was no accident. The moment we tried recursion for real, the lack of per-host rate limiting showed up as a glaring gap. It caused concurrent requests to the same host to throw 429s, the cache to be ineffective under high concurrency due to races (issue &lt;a href=&quot;https://github.com/lycheeverse/lychee/issues/1593&quot;&gt;#1593&lt;/a&gt;), and global concurrency settings being too coarse for a workload spread across many hosts at once.&lt;/p&gt;&lt;p&gt;The fix introduced a &lt;code&gt;HostPool&lt;/code&gt;, which is a per-host request queue with configurable rate limits, delays, and concurrent-request caps.
Each host gets its own bucket with its own settings, configurable via &lt;code&gt;lychee.toml&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;The &lt;code&gt;HostPool&lt;/code&gt; would later become a central abstraction. It’s the very same &lt;code&gt;HostPool&lt;/code&gt; that &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/2100&quot;&gt;PR #2100&lt;/a&gt; reused to unify input fetching with link checking, which means it’s now the single entrypoint that all HTTP requests flow through.&lt;/p&gt;&lt;p&gt;It’s important for recursion because the &lt;code&gt;HostPool&lt;/code&gt; gives us per-host rate limiting, deduplication (via each &lt;code&gt;Host&lt;/code&gt;’s per-URI &lt;code&gt;active_requests&lt;/code&gt; mutex and &lt;code&gt;HostCache&lt;/code&gt;), and caching at the right granularity, which lets recursive crawling stay a good web citizen (respecting rate-limit headers, backing off on 429s).&lt;/p&gt;&lt;h3&gt;
    
The WaitGroup (February 2026)&lt;/h3&gt;&lt;p&gt;The single most important recent thing is the &lt;code&gt;WaitGroup&lt;/code&gt; primitive, contributed by &lt;a href=&quot;https://github.com/katrinafyi&quot;&gt;Kait&lt;/a&gt; and merged in &lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/2046&quot;&gt;PR #2046&lt;/a&gt;.
It is one step towards solving the termination problem.&lt;/p&gt;&lt;p&gt;&lt;code&gt;WaitGroup&lt;/code&gt; is a mechanism for waiting on a dynamic set of tasks that can themselves spawn more tasks. It’s two pieces:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WaitGroup&lt;/code&gt;, a single waiter that fires when all the work is done.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WaitGuard&lt;/code&gt;, a cloneable guard held by each task. When the last guard is dropped, the waiter completes.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;The key move is that a &lt;code&gt;WaitGuard&lt;/code&gt; can be cloned. A task can spawn sub-tasks (recursion!) while preserving the invariant that the &lt;code&gt;WaitGroup&lt;/code&gt; only completes once &lt;em&gt;every&lt;/em&gt; guard — including the ones held by recursive sub-tasks — has been dropped.&lt;/p&gt;&lt;p&gt;That cleanly solves the termination problem:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;let (waiter, guard) = WaitGroup::new();

// Each request carries a guard clone
send_req.send((guard.clone(), request)).await;

// In the response handler, if recursing:
// the guard is cloned for each new request
for new_request in discovered_links {
    send_req.send((guard.clone(), new_request)).await;
}

// The original guard is dropped when the response is fully processed.
// When ALL guards are dropped (no more work), waiter.wait() returns.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It’s already wired into lychee’s main check loop. The &lt;code&gt;collect_responses&lt;/code&gt; function uses &lt;code&gt;take_until(waiter.wait())&lt;/code&gt; to stop receiving when the work is done. There’s even a comment in the current code anticipating exactly this:&lt;/p&gt;&lt;p&gt;That’s the missing piece that our previous attempts lacked.&lt;/p&gt;&lt;h3&gt;
    
Unified Request Handling (PR #2100, Merged March 2026)&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/2100&quot;&gt;PR #2100&lt;/a&gt; unified input URL fetching with the link checker’s &lt;code&gt;HostPool&lt;/code&gt;. Before this, CLI input URLs went through a separate &lt;code&gt;reqwest::Client&lt;/code&gt; that didn’t share config (user-agent, rate limiting, TLS settings) with the checker. That caused real bugs (Wikipedia returning 403 for input URLs because no user-agent was set).&lt;/p&gt;&lt;p&gt;After it, input fetching and link checking go through the same pool. For recursion this matters because recursively discovered pages need to be fetched and parsed, and they should use the same client config as everything else.&lt;/p&gt;&lt;h3&gt;
    
Sitemap Support (PR #2062)&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/lycheeverse/lychee/pull/2062&quot;&gt;Sitemap support&lt;/a&gt; is a partial solution to a lot of recursion use cases. By parsing &lt;code&gt;sitemap.xml&lt;/code&gt;, lychee can discover every page on a site without crawling recursively at all. It’s not a replacement for true recursion (it doesn’t help sites without sitemaps, and it won’t find dynamically linked pages), but it unblocks a lot of use-cases.&lt;/p&gt;&lt;h2&gt;
    
What Proper Recursion &lt;em&gt;Could&lt;/em&gt; Look Like&lt;/h2&gt;&lt;p&gt;With all that in place, here’s what’s left. The striking part is how much of it is already done:&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Knowing when the crawl is done is solved by the &lt;code&gt;WaitGroup&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deadlocks are avoided by spawning the follow-up work instead of blocking on a full channel.&lt;/li&gt;
&lt;li&gt;The per-host pool already paces requests, so we don’t hammer a server.&lt;/li&gt;
&lt;li&gt;lychee already skips URLs it has seen, which matters when every page links to the same nav and footer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Getting the page back is the one open problem.&lt;/strong&gt; lychee throws the page away after checking it, but recursion needs the HTML to find more links. It’s still in cache from the check that just happened, so we can grab it again for free. (Assuming the request method is GET, not a HEAD, which doesn’t return a body.)&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Once those are in, the actual recursion is just a handful of lines. When a checked page is on an allowed domain and under the depth limit, grab its content from cache, pull out the links, and send them back through the same pipeline as fresh requests:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;if recursive &amp;amp;&amp;amp; is_same_domain(&amp;amp;response, &amp;amp;recursion_domains) &amp;amp;&amp;amp; depth &amp;lt; max_depth {
    let content = resolver.url_contents(response.url()).await?;  // cache hit
    let links = extractor.extract(&amp;amp;content);
    for req in request::create(links, ...) {
        send_req.send((guard.clone(), Ok(req))).await;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The hard parts (knowing when to stop, not deadlocking, not flooding a server) are already solved by work that was never about recursion in the first place.
Recursion becomes a by-product of good architecture, not a special case bolted onto a pipeline that was never built for it.&lt;/p&gt;&lt;h2&gt;
    
So, Did We Fail…?&lt;/h2&gt;&lt;p&gt;I promised at the start I’d come back to this.&lt;/p&gt;&lt;p&gt;For a long time I told myself we’d failed. Four attempts, five years, seemingly nothing shipped.&lt;/p&gt;&lt;p&gt;But writing it all out changed how I see it. Every attempt hit some mix of channel termination semantics, backpressure deadlocks, ownership ergonomics, and distributed termination detection. None of those are lychee problems. They’re hard concurrent-systems problems.
We just lacked the vocabulary to talk about them, and while I wasn’t looking, those primitives got built. Sometimes the most important code you write for a feature is the code that never mentions the feature at all.&lt;/p&gt;&lt;p&gt;So no, I don’t think we failed. We made progress by stumbling into the right direction.&lt;/p&gt;&lt;p&gt;Thanks to NLnet for funding the work on lychee, and to everyone who contributed to the recursion effort over the years, whether in code, design feedback, or moral support. It’s been a long road, but we’re closer than ever to the finish line.&lt;/p&gt;&lt;section&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Well, to be fair, I still code late at night. But that’s just how I’m wired. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded>
</item>
</channel>
</rss>
