<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Dirk Holtwick</title>
        <link>https://holtwick.de</link>
        <description>Software developer for web, mobile and other platforms. Strong focus on privacy enhancement.</description>
        <lastBuildDate>Tue, 12 May 2026 10:05:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Dirk Holtwick</title>
            <url>https://holtwick.de/icon-1024.png</url>
            <link>https://holtwick.de</link>
        </image>
        <copyright>Copyright (c) Dirk Holtwick &lt;dirk.holtwick@gmail.com&gt;</copyright>
        <atom:link href="https://holtwick.de/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[PhotoFolder]]></title>
            <link>https://holtwick.de/blog/2018-08-24-photos</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2018-08-24-photos</guid>
            <pubDate>Fri, 24 Aug 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>The holiday is over and the camera full of photos and with this there comes the question of <strong>where to store the images?</strong></p><h3 id="existing-solutions" tabindex="-1"><a class="header-anchor" href="#existing-solutions">Existing Solutions</a></h3><p>You’d say, in <strong>iCloud or Photos app</strong> and that’s indeed a good place. I personally do not like to place my private photos on servers out of my full control, and I was afraid of using Photos app due to the closed box library iPhotos used to create. The later seems to become less of a problem since it stores the original files in the <code>Masters</code> folder ordered by import date inside of its library.</p><p>So I’m using Photos app now for my memories again stored on local devices. But before that I used <strong>Image Capture</strong>.</p><h3 id="command-line-tool" tabindex="-1"><a class="header-anchor" href="#command-line-tool">Command Line Tool</a></h3><p>Image Capture is a solid tool for syncing pictures to a local folder that comes with macOS. But files will keep their original file names. That’s why I created a little command line tool I’d like to share with you now:</p><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Source Code</p><p><a href="https://github.com/holtwick/photofolder" rel="noopener" target="_blank" class="external">https://github.com/holtwick/photofolder</a></p></div><p>The tool is rather simple. By passing a source and a destination folder it will copy or move images. It then has options to put the files in folder e.g. by year and add some additional information like picture dimensions. By creating hash values it will ignore already handled files. This is great to make sure you did not miss any picture in case you saved your images over multiple places over the years ;)</p><h3 id="backup-from-photos-library" tabindex="-1"><a class="header-anchor" href="#backup-from-photos-library">Backup from Photos Library</a></h3><p>To get back to our Photos library, this would be an example call to copy all originals from your library to a separate folder on the desktop:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">photofolder</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> --smart-copy</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -p</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -o</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> ~/Desktop/MyPhotosBackupByYear</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> ~/Pictures/Photos</span><span style="color:#005cc5;--shiki-dark:#79B8FF">\ </span><span style="color:#032f62;--shiki-dark:#9ECBFF">Library.photoslibrary/Masters/</span></span></code></pre></div><p><em>Published on August 24, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Receipts Blog]]></title>
            <link>https://holtwick.de/blog/2018-08-31-receipts-blog</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2018-08-31-receipts-blog</guid>
            <pubDate>Fri, 31 Aug 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Just a short note, that I started a <a href="https://www.receipts-app.com/blog" rel="noopener" target="_blank" class="external">separate blog</a> for news related to my product <a href="https://www.receipts-app.com/blog?ref=holtwick" rel="noopener" target="_blank" class="external">Receipts</a>. I hope you’ll subscribe there as well and learn more about the future of this product.</p><p>I had fun making the new website by using my Open Source project <a href="https://github.com/holtwick/seasite" rel="noopener" target="_blank" class="external"><strong>SeaSite</strong></a> about which I already wrote earlier in this blog: <a href="static-jquery">“Static websites the jQuery way”</a>. Even though the whole project is made of static pages, it was easy to do with those helper.</p><p>For SeaSite I added a localization plugin, to be able to reuse templates for English and German. In order to better respect the GDPR, I added features for load stuff lazy “on click” into the pages like YouTube videos or Disqus UI.</p><p>Another very useful feature of SeaSite is the outline generated when parsing Markdown. This way the <a href="https://www.receipts-app.com/help" rel="noopener" target="_blank" class="external">help pages</a> got a nice table of contents as a side info. The help document itself was edited in Typora, I wrote about the <a href="typora">approach earlier in this blog</a>.</p><p>Finally the search is also a nice feature that does not require any dynamic code on the server. SeaSite extracts all headers and contents from the created static pages a final step and puts the data into a JSON file. That is loaded lazily only if the user starts typing. The <a href="http://fusejs.io" rel="noopener" target="_blank" class="external">Fuse.js</a> library adds the search logic and fuzzy string support.</p></div><p><em>Published on August 31, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[UI States in Cocoa]]></title>
            <link>https://holtwick.de/blog/2018-09-17-nsdocstate</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2018-09-17-nsdocstate</guid>
            <pubDate>Mon, 17 Sep 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p><strong>A super simple approach</strong></p><p>In the web world, especially in <a href="https://reactjs.org/docs/state-and-lifecycle.html" rel="noopener" target="_blank" class="external">React</a> projects, the <code>state</code> of the app is an essential ingredient. The idea is that all visual representation originates from the current state i.e. if the state is changed the UI is likely to do as well.</p><p>In the Cocoa world this pattern is usually does not matter if you start a new project. There is some API like <a href="https://developer.apple.com/documentation/uikit/uistaterestoring?language=objc" rel="noopener" target="_blank" class="external">UIStateRestoring</a>, but it will require some extra work.</p><p>I sat down and tried to build something that is super simple and maintainable. It should provide the following features:</p><ol><li><strong>UI should update</strong> if state changes</li><li>It should work well with <strong>bindings</strong></li><li>There should be a way to <strong>back and forward navigation</strong> by using states</li><li>It should be possible to <strong>store and restore states</strong> e.g. for app relaunch</li><li>It should work <strong>per NSDocument</strong></li></ol><p>Ok, so first of all we need the state itself, I used my <code>SeaObject</code> implementation I <a href="seaobject">wrote about before</a>. Therefore it already covers the requirement “store and restore states” out of the box. Here an example:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@interface</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> State</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> : </span><span style="color:#6f42c1;--shiki-dark:#B392F0">SeaObject</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">searchString;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">currentViewID;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@end</span></span></code></pre><p>In this simple example we would store a search string and a pointer to the currently visible view controller.</p><p>Now we need to put that state somewhere. <code>NSDocument</code> seems to be ideal for that. To access it from any view controller we create a sub class of <code>NSViewController</code> we will use throughout the project and add a property called <code>document</code> to it. The following code will set it for us:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)viewWillAppear {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">super</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> viewWillAppear</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.document </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.view.window.windowController.document;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)viewDidDisappear {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.document </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> nil</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">super</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> viewDidDisappear</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>We can now access the state via <code>self.document.state</code> or even bind values to it. In the demo code we added a little helper for observing state changes, which can be used like this (for the <code>keyPath</code> trick see <a href="keypath-refatoring">this blog article</a>) :</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">[</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> observeKeyPath:keyPath</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.document.state.searchString) </span><span style="color:#005cc5;--shiki-dark:#79B8FF">action:</span><span style="color:#d73a49;--shiki-dark:#F97583">^</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> newValue) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> performCustomSearchWithString:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">document.state.searchString];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}];</span></span></code></pre><p>That’s basically it, just the navigation is missing and this is super easy if we start to store the states into a custom <code>NSUndoManager</code>. These are the two methods needed:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)restoreState:(State </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)state {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> storeState</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.state </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> state;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)storeState {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">!</span><span style="color:#24292e;--shiki-dark:#E1E4E8">[</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.state </span><span style="color:#005cc5;--shiki-dark:#79B8FF">isEqual:self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.lastStoredState]) {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">        self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.lastStoredState </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.state.copy;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.stateStack </span><span style="color:#005cc5;--shiki-dark:#79B8FF">registerUndoWithTarget:self</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">         selector:</span><span style="color:#d73a49;--shiki-dark:#F97583">@selector</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">restoreState:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">         object:self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.lastStoredState];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Now calling <code>[self.stateStack undo]</code> will restore the state to what it was when you called <code>storeState</code> the last time. This way you can have your marks for when it makes sense to store a state.</p><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Source Code</p><p>In the <strong><a href="https://github.com/holtwick/seakit-nsdocument-state" rel="noopener" target="_blank" class="external">example code on GitHub</a></strong> you will find some more additional stuff like <code>setupController</code> and <code>cleanupController</code> methods where to put the observers and do other stuff once the state becomes available or goes away. The example also contains code that shows how <code>firstResponder</code> can be restored.</p></div><p>I would love to get your feedback on that approach. Of course I’m not the first to reason about states, see e.g. <a href="https://www.objc.io/books/app-architecture/" rel="noopener" target="_blank" class="external">obj.c App Architecture</a> for other approaches.</p></div><p><em>Published on September 17, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[CSS from Mojave Colors]]></title>
            <link>https://holtwick.de/blog/2018-09-25-mojave-css</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2018-09-25-mojave-css</guid>
            <pubDate>Tue, 25 Sep 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Since <a href="https://www.apple.com/macos/mojave/" rel="noopener" target="_blank" class="external">macOS 10.14 Mojave</a> different <strong>visual modes like <a href="https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/" rel="noopener" target="_blank" class="external">dark</a> and light</strong> are available for the desktop. Native app developers can use <strong>named system colors</strong> to get the best fit for both <a href="https://developer.apple.com/documentation/appkit/nscolor/ui_element_colors" rel="noopener" target="_blank" class="external"><strong>semantic colors</strong></a> (like “window background”, “secondary label”) or <a href="https://developer.apple.com/documentation/appkit/nscolor/standard_colors" rel="noopener" target="_blank" class="external"><strong>plain colors</strong></a> (like “red”, “green”).</p><p>To adopt these <a href="https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/color/" rel="noopener" target="_blank" class="external">colors</a> for <strong>web development</strong> it is useful to get access to their <strong>color values for use in CSS</strong>. That’s what my new project <a href="https://github.com/holtwick/DesertColors" rel="noopener" target="_blank" class="external">DesertColor at GitHub</a> is providing.</p><p><code>dessertcolor.css</code> contains these extracted colors. With <code>&lt;html data-mode=&quot;dark&quot;&gt;</code> the dark variant is used, otherwise the light one. The colors can be accessed by their name:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-css"><span class="line"><span style="color:#22863a;--shiki-dark:#85E89D">body</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    color</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">var</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#e36209;--shiki-dark:#FFAB70">--label-color</span><span style="color:#24292e;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    background</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">var</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#e36209;--shiki-dark:#FFAB70">--control-background-color</span><span style="color:#24292e;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>But be aware, that <a href="https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/" rel="noopener" target="_blank" class="external">Dark Mode</a> in Mojave is not only about plain colors and that there is more like “Desktop Tinting” or “Translucency”.</p><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Source Code</p><p><a href="https://github.com/holtwick/DesertColors" rel="noopener" target="_blank" class="external">https://github.com/holtwick/DesertColors</a></p></div></div><p><em>Published on September 25, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Lovely Frameworks]]></title>
            <link>https://holtwick.de/blog/2018-10-11-frameworks</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2018-10-11-frameworks</guid>
            <pubDate>Thu, 11 Oct 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Certainly any macOS or iOS developers have used a framework in their project and <a href="https://cocoapods.org" rel="noopener" target="_blank" class="external">Cocoapods</a> makes it even easier to use 3rd party ones.</p><p><strong>But did you create one yourself?</strong></p><p>You really should, because it has some great advantages:</p><ol><li>Structure</li><li>Reusability</li><li>Stability</li></ol><h2 id="structure" tabindex="-1"><a class="header-anchor" href="#structure">Structure</a></h2><p>The benefit I like the most is that creating frameworks of certain areas of your code base forces you to precisely separate it from the rest of the code. Often the code - especially old and long grown one - has references to different parts of your project. Creating a framework is giving the opportunity to clean that up and ask yourself: “Does this still make sense this way?”</p><p>The outcome of creating a framework should also be to get a clean and minimal API. Minimal in the sense of: “What is it that is really required to execute that functionality?” You’ll be surprised how simple things may become.</p><h2 id="reusability" tabindex="-1"><a class="header-anchor" href="#reusability">Reusability</a></h2><p>The main purpose of a framework is to reuse it over again in other projects. This indeed makes a lot of sense and can pay out for the upfront investment of creating a framework in the first place.</p><p>For example I have some code that provides logging and debugging features I like to use as my basic toolbox. It also contains categories for objects I need all day, like string manipulation or date parsing and formatting. That is my first import on a new project. And also other frameworks I wrote use this base framework as well.</p><h2 id="stability" tabindex="-1"><a class="header-anchor" href="#stability">Stability</a></h2><p>A project should always be free of warnings and errors, otherwise you’ll not notice if some little problem that already showed up as a warning - which you ignored because it was hidden under dozens of others - causes you headaches a while after. But sometimes it is hard to get rid of warnings, because they would require a lot of workarounds or live in third party code you don’t want to touch. In your own frameworks just turn these warnings off, once your code ist stable and will not get changed any more.</p><p>Writing test suits for frameworks make sure you cover their functionality completely. You can focus on the single thing it is supposed to do and tests will not float around in a list of others in one big main project.</p><h2 id="real-life-example" tabindex="-1"><a class="header-anchor" href="#real-life-example">Real Life Example</a></h2><p>As a real life example I would like to talk about my projects <a href="https://pdfify.app" rel="noopener" target="_blank" class="external">PDFify</a> and <a href="https://www.receipts-app.com" rel="noopener" target="_blank" class="external">Receipts</a>. <a href="https://pdfify.app" rel="noopener" target="_blank" class="external">PDFify</a> was actually made to have a test environment for my OCR and PDF features I offer in <a href="https://www.receipts-app.com" rel="noopener" target="_blank" class="external">Receipts</a>. I separated OCR, PDF extraction and creation, Mail Parsing and the Scanner Interface and made a framework for each of them. I was able to write test cases specific to the different topics thus having a minimal functionality I can focus on. I can even reuse most stuff for iOS as well or for future projects to come. Sharing the code with other developers is also much easier this way.</p><p class="img-wrapper"><img src="/assets/cpug7l54xotjwz.png" alt="Screen Shot" width="368" height="194" loading="lazy"></p><h3 id="macos" tabindex="-1"><a class="header-anchor" href="#macos">macOS</a></h3><p>I found it easier to add all frameworks to the main project, even if a framework used sub-frameworks. The problems I ran into were related to signing.</p><h3 id="ios" tabindex="-1"><a class="header-anchor" href="#ios">iOS</a></h3><p>Frameworks finally got better support on iOS as in the early days, but there are still some pitfalls to care about:</p><ul><li>If you use categories you should set <code>OTHER_LDFLAGS = -all_load -ObjC</code></li><li>Dynamic frameworks don’t work well, set <code>MACH_O_TYPE = staticlib</code></li></ul><h3 id="debugging" tabindex="-1"><a class="header-anchor" href="#debugging">Debugging</a></h3><p>Frameworks also work great with HockeyApp (I know, this service is going to get replaced, but it is still first choice for me right now). Get more info here: <a href="https://web.archive.org/web/20190606064807/https://support.hockeyapp.net/kb/client-integration-ios-mac-os-x-tvos/how-to-solve-symbolication-problems" rel="noopener" target="_blank" class="external">https://web.archive.org/web/20190606064807/https://support.hockeyapp.net/kb/client-integration-ios-mac-os-x-tvos/how-to-solve-symbolication-problems</a></p></div><p><em>Published on October 11, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Website Performance]]></title>
            <link>https://holtwick.de/blog/2018-10-25-website-performance</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2018-10-25-website-performance</guid>
            <pubDate>Thu, 25 Oct 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>After putting some energy into optimizing my websites <a href="https://www.receipts-app.com" rel="noopener" target="_blank" class="external">receipts-app.com</a>, <a href="https://pdfify.app" rel="noopener" target="_blank" class="external">pdfify.app</a> and <a href="https://holtwick.de" rel="noopener" target="_blank" class="external">holtwick.de</a> I’d like to share my experiences/.</p><h2 id="defer-scripts" tabindex="-1"><a class="header-anchor" href="#defer-scripts">Defer Scripts</a></h2><p>Putting <code>defer</code> into script loading tags will load the scripts when the DOM is ready and in the order they appeared in your HTML. Put them in the <code>head</code> so the browser gets to know about them early and can already start loading while the rest of the page is still to be loaded and prepared.</p><p>But be aware that JS object might not be available for inline scripts. For example if loading <code>jQuery</code> using <code>defer</code> the <code>$</code> will only be available <strong>after DOM loaded</strong>! So best approach is to wrap inline script like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-js"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">window.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">addEventListener</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;load&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, () </span><span style="color:#d73a49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">  // Your code</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">})</span></span></code></pre><h2 id="inline-css" tabindex="-1"><a class="header-anchor" href="#inline-css">Inline CSS</a></h2><p>The page should start rendering only when the CSS is loaded to avaiod flickering effects. It is hard to tell which CSS styles are really used by a page. Usually the custom CSS is not super large, so consider inlining it to avoid an additional request and have it ready eraly.</p><h2 id="cdn" tabindex="-1"><a class="header-anchor" href="#cdn">CDN</a></h2><p>Even though you are not using a CDN (Content Delivery Network) for your website itself it can speed up if you use it for common CSS and JS like jQuery or Bootstrap. But if the rendering does not depend on JS code, it is also absolutely ok to serve from the same server as the page.</p><p>Remember the <code>defer</code> tip from the beginning, it will also work for those</p><h2 id="minification" tabindex="-1"><a class="header-anchor" href="#minification">Minification</a></h2><p>Of course reducing the size of files is always a good idea for production sites. UglifyJS does a good job for JS and</p><p>If the resulting CSS isn’t to big it absolutely makes sense to integrate it into the HTML header, which will improve the “first meaningful paint” benchmark and thus giving the user the impression the page loaded much faster.</p><h2 id="image-optimization" tabindex="-1"><a class="header-anchor" href="#image-optimization">Image Optimization</a></h2><p>Dropping image resource on <a href="https://imageoptim.com/mac" rel="noopener" target="_blank" class="external">ImageOptim</a> will usually have good results on reducing their file sizes which also helps speeding up page load.</p><p>Using more modern image types like WEBP is also possible but requires more changes in the HTML like using <code>&lt;picture&gt;</code> element etc. and will not be supported by all browsers yet. The work might not we worth the benefit right now.</p><h2 id="lazy-loading" tabindex="-1"><a class="header-anchor" href="#lazy-loading">Lazy Loading</a></h2><p>Not everything needs to be available on the first step. With some Javascript magic image loading might be a good thing to defer. But also 3rd party integrations like embedded YouTube videos or Disqus discussion groups don’t need to be loaded immediately. On <a href="https://www.receipts-app.com" rel="noopener" target="_blank" class="external">receipts-app.com</a> for example I provide an offline search, which also only loads if the user types some words. The user experience is usually still the same.</p><h2 id="google-analytics" tabindex="-1"><a class="header-anchor" href="#google-analytics">Google Analytics</a></h2><p>Tracking etc. should not slow down you page loading. Put it just in front of the closing <code>&lt;/body&gt;</code> tag.</p><h2 id="reduce-waiting-on-server-side" tabindex="-1"><a class="header-anchor" href="#reduce-waiting-on-server-side">Reduce Waiting on Server Side</a></h2><p>While optimizing all the details of the page you might forget about the server side. If you see a big “Waiting Time (TTFB)” in your Network Inspector this might be due to things going on on the server.</p><p>For example this could be a PHP script connecting to a database. I recommend profiling the PHP calls, which is simple using XDebug and and IDE like IntelliJ. In my case I opened the database without needing to. Another quick fix was to open MySQL via <code>127.0.0.1</code> instead of <code>localhost</code>.</p><p>Don’t forget configure a caching policy for your site that makes sense. Also compress the outgoing traffic, especially the HTML.</p><h2 id="check-the-results" tabindex="-1"><a class="header-anchor" href="#check-the-results">Check the Results</a></h2><p>To see if the optimizations had an effect you could e.g. use the following tools for measuring, just to name a few:</p><ul><li><a href="https://developers.google.com/web/tools/lighthouse/" rel="noopener" target="_blank" class="external">Google Lighthouse</a></li><li><a href="https://tools.pingdom.com" rel="noopener" target="_blank" class="external">Pingdom Tools</a></li></ul><p class="img-wrapper"><img src="/assets/nukjzaqt1fw8g2k.png" alt="image-20181025125841736" width="880" height="306" loading="lazy"></p><h2 id="static-website-builder" tabindex="-1"><a class="header-anchor" href="#static-website-builder">Static Website Builder</a></h2><p>For my own projects I use my own static web site builder: <a href="https://github.com/holtwick/seasite" rel="noopener" target="_blank" class="external">SeaSite</a>. It does all the repeating tasks for me and is easy to maintain. Read more about it in this <a href="static-jquery">blog post</a>.</p></div><p><em>Published on October 25, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[The most obvious Electron alternative on macOS/iOS]]></title>
            <link>https://holtwick.de/blog/2018-12-20-web-technologies</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2018-12-20-web-technologies</guid>
            <pubDate>Thu, 20 Dec 2018 07:00:00 GMT</pubDate>
            <description><![CDATA[An alternative way to do web based apps on macOS and iOS without Electron]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Recently I see some discussion about <a href="https://electronjs.org" rel="noopener" target="_blank" class="external">Electron</a> on macOS (<a href="https://daringfireball.net/2018/12/electron_and_the_decline_of_native_apps" rel="noopener" target="_blank" class="external">John Gruber</a>, <a href="https://twitter.com/daveverwer/status/1075538571069665281" rel="noopener" target="_blank" class="external">Dave Verwer</a>). I have some experience in the field and will share my view on the topic and a practical introduction here.</p><h2 id="why-web-technologies%3F" tabindex="-1"><a class="header-anchor" href="#why-web-technologies%3F">Why web technologies?</a></h2><p>In general web technologies are a <em>de facto</em> standard, not only in classic web browsers. There are some valid reasons for that:</p><ul><li>It is <strong>cross platform</strong></li><li>It is <strong>simple to learn</strong> and the majority of developers had contact with HTML, CSS and Javascript</li><li>It is a <strong>mature, stable, powerful and feature complete</strong>** environment</li><li>It is <strong>“the internet”</strong></li><li>It is <strong>cheap</strong></li></ul><p>So it takes no wonder to understand that developers, but also product managers, have some sympathy for the platform.</p><h2 id="about-electron" tabindex="-1"><a class="header-anchor" href="#about-electron">About Electron</a></h2><p>As soon as server driven service wants to create a desktop application it seems natural to start an <a href="https://electronjs.org" rel="noopener" target="_blank" class="external">Electron</a> project and point to the service’s URL. This of course has no additional value for the user besides having the app in the Dock.</p><p>But <a href="https://electronjs.org" rel="noopener" target="_blank" class="external">Electron</a> can do more, as <a href="https://code.visualstudio.com" rel="noopener" target="_blank" class="external">Visual Studio Code</a> and others demonstrate. As soon as the app makes use of the file system, native menus and other features, it comes closer to a <em>real</em> desktop app.</p><p>The look and feel these days isn’t that important anymore since the skeuomorphic design world transformed into a flat one.</p><p>But there are some disadvantages as well:</p><ul><li><strong>Size</strong>: The distributed app package comes with the whole browser engine included</li><li><strong>Security</strong>: You need to trust Electron which builds on <a href="https://www.chromium.org" rel="noopener" target="_blank" class="external">Chromium</a> which is maintained by Google</li><li><strong>Performance</strong>: Even though web technologies became ridiculous fast they are still slow in some area. You can add native modules for such tasks to Electron, but then you already left the comfort zone.</li><li><strong>Desktop</strong>: It only works on macOS, Windows and Linux. For iOS and Android you need to look out for other solutions, which of course exist.</li></ul><h2 id="how-to-use-wkwebview" tabindex="-1"><a class="header-anchor" href="#how-to-use-wkwebview">How to use WKWebView</a></h2><p>I recently started a new project and also had to make a decision of which technologies I’d like to use. I had written many pure native apps like my latest ones <a href="https://www.receipts-app.com/?ref=holtwick" rel="noopener" target="_blank" class="external">Receipts</a> and <a href="https://pdfify.app/?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a>, but I have to admit I like the latest Javascript Syntax very very much.</p><p class="img-wrapper"><img src="/assets/ehx1wfivn6z5c.png" alt="image-20181220111209125" width="1390" height="855" loading="lazy"></p><p>Since I have a lot of code I would like to reuse (OCR, Scanner Dialog, Share Extensions, PDF Logic) I really just need it for the UI. So Instead of taking Electron and trying to put my stuff in there, I have chosen to use <a href="https://developer.apple.com/documentation/webkit/wkwebview?language=objc#" rel="noopener" target="_blank" class="external">WKWebView</a> and here is my story:</p><h3 id="bridging" tabindex="-1"><a class="header-anchor" href="#bridging">Bridging</a></h3><p>First of all I need to set up a connection between the native (Objective-C) and web view (Javascript) sides, which I named: <em>Bridge</em>.</p><p>Sending to Javascript is easy as calling <a href="https://developer.apple.com/documentation/webkit/wkwebview/1415017-evaluatejavascript?language=objc#" rel="noopener" target="_blank" class="external">evaluateJavaScript:completionHandler:</a> For the other way I use <a href="https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc" rel="noopener" target="_blank" class="external">WKScriptMessageHandler</a>.</p><p>As you can see there is some asynchronous code involved and this is, where my <em>bridge</em> code comes into play. It defines some loose protocol of how the data that is send needs so be structured, for example:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;action&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;fetchData&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;responseAction&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;tmp-123&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;payload&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: { </span><span style="color:#005cc5;--shiki-dark:#79B8FF">&quot;skip&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#005cc5;--shiki-dark:#79B8FF">&quot;limit&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">100</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>This would basically work in both directions and mean, that <code>fetchData</code> should be called and the response be send to <code>responseAction</code>. These actions will registered with the <em>bridge</em> code. Their return value will be the payload for the response call. The response actions will temporarily be registered and deleted after successfully having received a response or having reached a timeout.</p><h3 id="native-ui" tabindex="-1"><a class="header-anchor" href="#native-ui">Native UI</a></h3><p>Using this technique it is quite easy to trigger native things like menus or popovers. I can also easily access my existing components. Also, all the database handling is done on the native side.</p><h3 id="custom-schemas" tabindex="-1"><a class="header-anchor" href="#custom-schemas">Custom schemas</a></h3><p>This works well for data that does not exceed some Kilobytes, but for providing images it would slow down the process. Therefore, I also implemented <a href="https://developer.apple.com/documentation/webkit/wkurlschemehandler?language=objc#" rel="noopener" target="_blank" class="external">WKURLSchemeHandler</a>, not only for images but also for the HTML pages that make the basis for the web app part.</p><p>Another nice benefit from it is that it can be used to trigger heavy computation resulting in large data. In the case of OnePile I use it to render PDF on the fly to PNG.</p><h3 id="jscontext" tabindex="-1"><a class="header-anchor" href="#jscontext">JSContext</a></h3><p>But Apple does not only offer <a href="https://developer.apple.com/documentation/webkit/wkwebview?language=objc#" rel="noopener" target="_blank" class="external">WKWebView</a> for hosting Javascript, it also provides the slick <a href="https://developer.apple.com/documentation/javascriptcore/jscontext?language=objc#" rel="noopener" target="_blank" class="external">JSContext</a> for headless code. I also make use of that for example to extract data from the notes to build the full text index on the native side.</p><p>With some fine-tuning you can nicely catch exceptions and pass logging to the native log for better debugging experience. I was also able to add support for <code>require</code> to dynamically load modules into the <a href="https://developer.apple.com/documentation/javascriptcore/jscontext?language=objc#" rel="noopener" target="_blank" class="external">JSContext</a>.</p><h3 id="macos-%26-ios" tabindex="-1"><a class="header-anchor" href="#macos-%26-ios">macOS &amp; iOS</a></h3><p>And the best: all this applies to iOS as well as to macOS. You can actually reuse the whole glue code. With Electron it is also all or nothing where with this approach you may decide view by view if you want to use it or not.</p><p class="img-wrapper"><img src="/assets/ocpexql82a86ml.png" alt="Screen Shot 2018-12-20 at 11.15.15" width="584" height="1048" loading="lazy"></p><h3 id="vue.js" tabindex="-1"><a class="header-anchor" href="#vue.js">Vue.js</a></h3><p>For the discussion about Electron and <a href="https://developer.apple.com/documentation/webkit/wkwebview?language=objc#" rel="noopener" target="_blank" class="external">WKWebView</a> it does not matter, which frameworks to use on the web side, but I just want to express my love for <a href="https://vuejs.org" rel="noopener" target="_blank" class="external">Vue</a>.js here 😍 For a macOS developer used to bindings this is a nice and powerful tool to feel right in the shiny web world.</p><h2 id="try-yourself" tabindex="-1"><a class="header-anchor" href="#try-yourself">Try yourself</a></h2><p>Would you like to see it in action? No problem, you can, by subscribing to the early preview of my new app Collect:</p><p><a href="https://onepile.app?ref=holtwick" rel="noopener" target="_blank" class="external"><strong>Subscribe for preview of OnePile App</strong></a></p><p>&nbsp;</p><p><strong>Update 2018-12-27:</strong></p><p>This article has received great <a href="https://news.ycombinator.com/item?id=18761840" rel="noopener" target="_blank" class="external">feedback on Hacker News</a> with very interesting and often profound discussions.</p><p>In the previous version of this article I said, <em>Electron is based on Chrome which is owned by Google</em>, which was not correct. It is based on <a href="https://www.chromium.org" rel="noopener" target="_blank" class="external">Chromium</a> which itself is the basis of Chrome, but the influence of Google on the code is still strong. Even though I don’t think Google is intentionally putting bad code into the project my intention by mentioning this point was, that the code basis is so large that usually nobody using it for a web project would ever do a code review before using it. The same of course applies to WKWebView, but since it is part of the operating system Apple ships, I think if you trust the OS at this point you can trust this component as well.</p><p>One argument I often heard has been: <em>Your approach is not cross platform</em>. And indeed this particular native implementation only works in the Apple ecosystem. Similar approaches in other environments can be achieved with reasonable effort. Projects like <a href="https://cordova.apache.org" rel="noopener" target="_blank" class="external">Cordova</a> or <a href="https://github.com/ionic-team/capacitor" rel="noopener" target="_blank" class="external">Capacitor</a> are built this way and there are more examples available to start with. But this is just a thin layer and the web code is the one that will grow and this is truly cross platform. In case of <a href="https://onepile.app?ref=holtwick" rel="noopener" target="_blank" class="external">OnePile</a> I might even build Windows and Linux clients on Electron first, but the web code will still stay the same for all destinations apart from some glue code for UI like menus.</p></div><p><em>Published on December 20, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Crash reporting: Sentry as HockeyApp alternative]]></title>
            <link>https://holtwick.de/blog/2019-01-07-crash-reporting</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2019-01-07-crash-reporting</guid>
            <pubDate>Mon, 07 Jan 2019 07:00:00 GMT</pubDate>
            <description><![CDATA[A crash and error reporting alternative to HockeyApp is Sentry; examples for macOS, iOS and Javascript]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Since the beginning I have been using and loving <a href="https://www.hockeyapp.net/#s" rel="noopener" target="_blank" class="external">HockeyApp</a>, because it was made by very skilled developers who exactly knew what makes the live of a developer easier. It started with symbolicated crash reports, which previously were difficult to get and improved the overall quality of software products that integrated and still integrate with it.</p><p>Success attracts buyers and so <a href="https://www.hockeyapp.net/#s" rel="noopener" target="_blank" class="external">HockeyApp</a> was acquired by Microsoft and is now transitioning to their own platform called <a href="https://appcenter.ms" rel="noopener" target="_blank" class="external">AppCenter</a>, which will certainly do a good job in the future but isn’t yet where HockeyApp is right now.</p><h2 id="sentry" tabindex="-1"><a class="header-anchor" href="#sentry">Sentry</a></h2><p>I took that as an opportunity to look out for alternatives and found <a href="https://sentry.io/welcome/" rel="noopener" target="_blank" class="external">Sentry</a>. First of all it is <a href="https://github.com/getsentry" rel="noopener" target="_blank" class="external">OpenSource</a> and can be installed on premise. But usually you will go with their service which comes with a <a href="https://sentry.io/pricing/" rel="noopener" target="_blank" class="external">reasonable pricing</a>.</p><p>It has a nice and clear web interface and supports everything I need, especially Objective-C and Javascript, which is a great fit for my <a href="2018-12-20-web-technologies">Electron alternative</a>.</p><h2 id="integration-with-macos-%2F-ios" tabindex="-1"><a class="header-anchor" href="#integration-with-macos-%2F-ios">Integration with macOS / iOS</a></h2><p><a href="https://docs.sentry.io/clients/cocoa/" rel="noopener" target="_blank" class="external">The integration is easy</a> e.g. by using their CocoaPod. With a few lines of code and providing the DSN of your project you can send <em>Events</em>. In my case I also wanted to let the user decide if he is ready to share the data and therefore added an <code>NSAlert</code> in the <code>setShouldSendEvent</code> block, which looks similar to this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">[SentryClient.sharedClient </span><span style="color:#005cc5;--shiki-dark:#79B8FF">setShouldSendEvent:</span><span style="color:#d73a49;--shiki-dark:#F97583">^</span><span style="color:#005cc5;--shiki-dark:#79B8FF">BOOL</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(SentryEvent </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _Nonnull event) {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    __block</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSInteger</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> ret </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">    dispatch_sync</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#6f42c1;--shiki-dark:#B392F0">dispatch_get_main_queue</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(), </span><span style="color:#d73a49;--shiki-dark:#F97583">^</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">       NSAlert</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">alert;</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">       // ...</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">       ret </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [alert </span><span style="color:#005cc5;--shiki-dark:#79B8FF">runModal</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    });</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> ret </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSAlertFirstButtonReturn</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}];</span></span></code></pre><p>With some more code the user sees this:</p><p class="img-wrapper"><img src="/assets/odxv68ms160gm70.png" alt="Sentry Dialog" width="605" height="266" loading="lazy"></p><h3 id="support" tabindex="-1"><a class="header-anchor" href="#support">Support</a></h3><p>What I liked a lot at HockeyApp was their feedback dialog which allowed the user to give information about what situation lead to the problem and that I could begin a dialog with them and involve them into beta testing the fix. Mainly I did this using the great support tool from my friends at <a href="https://replies.io/?ref=holtwick" rel="noopener" target="_blank" class="external">replies.io</a>. I wrote about <a href="replies">Replies earlier in this blog</a>.</p><p>So my goal was to integrate <a href="https://replies.io/?ref=holtwick" rel="noopener" target="_blank" class="external">replies.io</a> again with my new Sentry setup and this is what the <code>Send and Support</code> button is doing. It opens the support for of <a href="https://replies.io/?ref=holtwick" rel="noopener" target="_blank" class="external">replies.io</a> and the user has the chance to get in contact and provide more info. The link between the ticket on Sentry and the one on <a href="https://replies.io/?ref=holtwick" rel="noopener" target="_blank" class="external">replies.io</a> is the <code>userId</code> set for both services, which is a unique ID per client.</p><h3 id="errors" tabindex="-1"><a class="header-anchor" href="#errors">Errors</a></h3><p>Another aspect I changed while integrating the new service, was the tracking of <em>errors</em>. I’m referring to those errors you would usually write to the log file and only see if the user sends the log. This is very valuable information that you should use to improve your software.</p><p>I’m using a logging framework, which I described earlier <a href="logging">here in this blog</a>. So the most natural thing to do to hook into the <code>logError</code> part and create an event for Sentry:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)logLevel:(SeaLogFlag)level file:(</span><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#d73a49;--shiki-dark:#F97583"> char</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)file function:(</span><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#d73a49;--shiki-dark:#F97583"> char</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)function line:(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSUInteger</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)line context:(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)context message:(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)message {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    int</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> slevel </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (level </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> SeaLogFlagInfo) slevel </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> kSentrySeverityInfo</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (level </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> SeaLogFlagWarning) slevel </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> kSentrySeverityWarning</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (level </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> SeaLogFlagError) slevel </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> kSentrySeverityError</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (slevel) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        SentryBreadcrumb </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">bc </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [[SentryBreadcrumb </span><span style="color:#005cc5;--shiki-dark:#79B8FF">alloc</span><span style="color:#24292e;--shiki-dark:#E1E4E8">] </span><span style="color:#005cc5;--shiki-dark:#79B8FF">initWithLevel:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">slevel </span><span style="color:#005cc5;--shiki-dark:#79B8FF">category:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;log&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        bc.data </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> @{ </span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;message&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: message </span><span style="color:#d73a49;--shiki-dark:#F97583">?:</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> @&quot;&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">                     @&quot;file&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSString</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> stringWithFormat:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;&lt;</span><span style="color:#005cc5;--shiki-dark:#79B8FF">%@</span><span style="color:#032f62;--shiki-dark:#9ECBFF">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF">%@</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&gt;&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, @(file </span><span style="color:#d73a49;--shiki-dark:#F97583">?:</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">), @(line)]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                     };</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        [[SentryClient.sharedClient </span><span style="color:#005cc5;--shiki-dark:#79B8FF">breadcrumbs</span><span style="color:#24292e;--shiki-dark:#E1E4E8">] </span><span style="color:#005cc5;--shiki-dark:#79B8FF">addBreadcrumb:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">bc];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (level </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> SeaLogFlagError) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        [SentryClient.sharedClient </span><span style="color:#005cc5;--shiki-dark:#79B8FF">snapshotStacktrace:</span><span style="color:#d73a49;--shiki-dark:#F97583">^</span><span style="color:#24292e;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            SentryEvent </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">event </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [[SentryEvent </span><span style="color:#005cc5;--shiki-dark:#79B8FF">alloc</span><span style="color:#24292e;--shiki-dark:#E1E4E8">] </span><span style="color:#005cc5;--shiki-dark:#79B8FF">initWithLevel:kSentrySeverityError</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            event.message </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> message;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            [SentryClient.sharedClient </span><span style="color:#005cc5;--shiki-dark:#79B8FF">appendStacktraceToEvent:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">event];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            [SentryClient.sharedClient </span><span style="color:#005cc5;--shiki-dark:#79B8FF">sendEvent:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">event </span><span style="color:#005cc5;--shiki-dark:#79B8FF">withCompletionHandler:</span><span style="color:#d73a49;--shiki-dark:#F97583">^</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSError</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _Nullable error) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                ;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            }];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        }];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    }</span></span></code></pre><p>You might notice the <em>breadcrumb</em> object. This is the way to pass logging info to Sentry, which I do by creating breadcrumb objects for log on the <em>info</em> and <em>warning</em> level.</p><h3 id="symbolication" tabindex="-1"><a class="header-anchor" href="#symbolication">Symbolication</a></h3><p>In order to get the most out of the stack trace the service should symbolicate the crash report and show the line numbers, where the problem originated. HockeyApp had its own awesome macOS app doing the job. But Sentry also provides a <a href="https://docs.sentry.io/clients/cocoa/dsym/" rel="noopener" target="_blank" class="external">command line tool that integrates well</a> with the <em>archive step</em> in Xcode and confirms the upload with a notification:</p><p class="img-wrapper"><img src="/assets/g6cba6f8oo4d3r.png" alt="Upload confirmation" width="382" height="101" loading="lazy"></p><h2 id="integration-with-javascript" tabindex="-1"><a class="header-anchor" href="#integration-with-javascript">Integration with Javascript</a></h2><p>The same <a href="https://docs.sentry.io/platforms/javascript/" rel="noopener" target="_blank" class="external">applies to Javascript</a>. The integration again is straight forward, but I encountered one problem, because I was using it in WKWebView. But setting the <code>transport</code> option fixed the issue for me:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-js"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">Sentry.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">init</span><span style="color:#24292e;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  transport: Sentry.Transports.FetchTransport,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">})</span></span></code></pre><p>It is also important to get the <code>release</code> right. With some wepback and <code>process.env</code> tricks it worked quite well to get the version info from <code>package.json</code> as release information in the distributed code.</p><p>Of course you should upload your SourceMaps to Sentry to get the most out of the errors. Also the breadcrumbs will automatically be generated from the <code>console.xyz</code> calls.</p><h2 id="what-i-learned" tabindex="-1"><a class="header-anchor" href="#what-i-learned">What I learned</a></h2><p>I enjoyed modernizing my crash and error handling code and infrastructure and finally get more valuable info out of the logging data. I am also happy to have found a good way to respect my users privacy and leave the decision to them if they would like to share their data or not, since my products may contain sensitive data and therefore it is important to win the users trust.</p><p>Sentry seems to be a good alternative to HockeyApp. I would have stayed with HockeyApp, but since they are migrating to a new platform, I felt like some effort to put into migration for my projects would have come anyway, so why not doing it right now. That Sentry is OpenSource is another plus on the list.</p><p>If you would like to see it in action - although I hope you’ll never need to see the dialog 😉 - please try my products:</p><ul><li><a href="https://www.receipts-app.com?ref=holtwick" rel="noopener" target="_blank" class="external">Receipts</a></li><li><a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a></li></ul></div><p><em>Published on January 7, 2019</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Tiny UUID]]></title>
            <link>https://holtwick.de/blog/2019-02-21-uuid</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2019-02-21-uuid</guid>
            <pubDate>Mon, 07 Jan 2019 07:00:00 GMT</pubDate>
            <description><![CDATA[About UUIDs in Objective-C and Javascript]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Universally unique IDs are great. Used in a database in place of the primary key you don’t need to care for conflicts anymore, if more than one application instance is involved. But also inside an app they are comfortable object identifiers.</p><p>In general the version 4 described in <a href="https://tools.ietf.org/html/rfc4122" rel="noopener" target="_blank" class="external">RFC 4122</a> is used, which is composed by time related data and random bytes. For macOS and iOS <a href="https://developer.apple.com/documentation/foundation/nsuuid?language=objc#" rel="noopener" target="_blank" class="external">NSUUID</a> provides a simple-to-use interface. For Javascript projects <a href="https://github.com/kelektiv/node-uuid" rel="noopener" target="_blank" class="external">node-uuid</a> is widely used.</p><p>A drawback is that it is usually is used in form of a <strong>string</strong>. An incrementing <strong>integer</strong> instead is very likely managed more efficient and faster by databases and systems.</p><p>UUIDs are mostly presented in a hexadecimal notation like <code>f81d4fae-7dec-11d0-a765-00a0c91e6bf6</code> which is a <strong>36 character</strong>s long string. In binary representation it is <strong>16 bytes</strong> or <strong>128 bits</strong> long. For most environments this does not fit into a regular integer type anymore.</p><p>Since each byte counts if you have lot of data it makes sense to put some effort into reducing the size. Stripping the <code>-</code> already saves <strong>4 bytes</strong>. But to safe most, without getting into difficulties about string encoding and the like, <strong>Base64</strong> looks like a good choice.</p><p>But characters like <code>+</code> and <code>/</code> and <code>=</code> become painful when used in URLs or as file names. So let’s replace <code>+</code> by <code>-</code> and <code>/</code> by <code>_</code> as <a href="https://docs.python.org/2/library/base64.html#base64.urlsafe_b64encode" rel="noopener" target="_blank" class="external">Python does</a> for ages already. Then strip the trailing <code>==</code> and we get a <strong>22 characters</strong> long string, which is <strong>1.375 times</strong> the size of the most compact representation in binary form and significantly smaller than the original hex representation.</p><p>Here is the implementation in Objective-C:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)uuid {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    uuid_t</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> uuid;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    [NSUUID.UUID </span><span style="color:#005cc5;--shiki-dark:#79B8FF">getUUIDBytes:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">uuid];</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [[[[[</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSData</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> dataWithBytes:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">uuid </span><span style="color:#005cc5;--shiki-dark:#79B8FF">length:16</span><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">               base64EncodedStringWithOptions:</span><span style="color:#005cc5;--shiki-dark:#79B8FF">0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">              stringByReplacingOccurrencesOfString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;=&quot;</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> withString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">             stringByReplacingOccurrencesOfString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;+&quot;</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> withString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;-&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            stringByReplacingOccurrencesOfString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;/&quot;</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> withString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;_&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>And this one if for Javascript:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-js"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">import</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> uuid </span><span style="color:#d73a49;--shiki-dark:#F97583">from</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &apos;uuid&apos;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> rxUUIDReplace</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> /</span><span style="color:#005cc5;--shiki-dark:#79B8FF">[+=</span><span style="color:#22863a;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\/</span><span style="color:#005cc5;--shiki-dark:#79B8FF">]</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/</span><span style="color:#d73a49;--shiki-dark:#F97583">g</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> mapUUIDReplace</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">  &apos;+&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;-&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">  &apos;/&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;_&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">  &apos;=&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;&apos;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">export</span><span style="color:#d73a49;--shiki-dark:#F97583"> function</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> UUID</span><span style="color:#24292e;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> array</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  uuid.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">v4</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">null</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, array)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  return</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> toBase64</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(array).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">replace</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(rxUUIDReplace, </span><span style="color:#e36209;--shiki-dark:#FFAB70">m</span><span style="color:#d73a49;--shiki-dark:#F97583"> =&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> mapUUIDReplace[m])</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Happy uniqueness ;)</p></div><p><em>Published on January 7, 2019</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Hybrid Apps - No Marzipan]]></title>
            <link>https://holtwick.de/blog/2019-03-14-hybrid-apps</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2019-03-14-hybrid-apps</guid>
            <pubDate>Thu, 14 Mar 2019 07:00:00 GMT</pubDate>
            <description><![CDATA[Hybrid Apps - No Marzipan]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Everybody is talking about <a href="https://www.highcaffeinecontent.com/blog/20190301-Bringing-iOS-Apps-to-macOS-Using-Marzipanify" rel="noopener" target="_blank" class="external">Marzipan</a> and the amalgamation of UIKit and AppKit. This step is a logical consequence for Apple. But this is still just beneficial for the Apple universe and cross platform is much more than iOS, macOS, tvOS and watchOS.</p><p>I write macOS and iOS apps for over a decade now, but for my latest project I decided to choose a different approach. I decided to use web technologies in the front end, as I described in this <a href="2018-12-20-web-technologies">previous article</a>. You might say: <em>Oh, you are using <a href="https://electronjs.org/" rel="noopener" target="_blank" class="external">Electron</a> and <a href="https://cordova.apache.org/" rel="noopener" target="_blank" class="external">Cordova</a></em>. Well, almost.</p><p>Instead of using existing web app wrappers I wrote my own. This might sound stupid, but it has some benefits. First of all the footprint on disc of these apps is much smaller. Then I have full control over native features, even the newest ones. And I can reuse a lot of existing native code as well.</p><p>For the front end I use HTML, CSS and JS. If you did not use it for a long time you will be surprised how much it has matured over time. Javascript in its current form of <a href="https://en.wikipedia.org/wiki/ECMAScript" rel="noopener" target="_blank" class="external">ECMAScript7</a> (ES7) and later has evolved to a super powerful yet sexy language. All the packaging is greatly performed by <a href="https://webpack.js.org/" rel="noopener" target="_blank" class="external">webpack</a>, which you might refer to as the <em>compiler of web apps.</em></p><p>But what indeed is a big difference is the fresh wind blowing in user interface concepts. <a href="https://reactjs.org/" rel="noopener" target="_blank" class="external">ReactJS</a> and <a href="https://vuejs.org/" rel="noopener" target="_blank" class="external">VueJS</a> are certainly the most production ready frameworks. You need to change your mindset in order to not try to reach out to a certain view and manipulate its properties. Instead you have a state and derive rendering of everything from it. The framework is doing updates in a smart and efficient way in a 60fps manner. This allows to split large project into smaller easier to handle pieces. And these little pieces can be reused on all platforms, even if the visual appearance has to be different.</p><p>For mobile devices I found a great UI framework called <a href="http://framework7.io/" rel="noopener" target="_blank" class="external">Framework7</a>, which does a fantastic job. It comes really close to how the native UI looks and feels. But you benefit from the full flexibility of a web app. And you just need to set up the UI once and it looks great on both iOS and Android out of the box.</p><p>So for now I’m super happy with this approach.</p></div><p><em>Published on March 14, 2019</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[CSS - Single Page App Stack Layout]]></title>
            <link>https://holtwick.de/blog/2020-03-15-css-single-page-app-stack-layout</link>
            <guid isPermaLink="false">https://holtwick.de/blog/2020-03-15-css-single-page-app-stack-layout</guid>
            <pubDate>Sun, 15 Mar 2020 07:00:00 GMT</pubDate>
            <description><![CDATA[CSS Frameworks for a single page web app have different requirements than regular content based websites.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Most CSS Frameworks offer a layout system based on columns for creating text and content based websites. But for a single page web app the requirements are different. App development frameworks offer various techniques to structure the contents of a window, a simple yet powerful one is <strong>stack based view layout.</strong></p><p>Let’s start with a common example that you might find in a mail or notes app. You have multiple columns like a sidebar, a document list, a content area. The list might also have a search field. An additional column to the right gives detailed information.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-html"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;app&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;sidebar&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Sidebar&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;middle&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Search&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;list&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Item1&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Item2&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Item3&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;content&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;menu&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            Content related menu</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;text&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            Text</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;info&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Info&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span></code></pre><p>Each group has a different orientation for its children. All children of <code>app</code> are placed <strong>horizontally.</strong> Those of <code>middle</code> and <code>content</code> are <strong>vertically.</strong> We will introduce <code>hstack</code> and <code>vstack</code> CSS classes that will define the appropriate layout by using <a href="https://css-tricks.com/snippets/css/a-guide-to-flexbox/" rel="noopener" target="_blank" class="external">flex box</a>. This is the code in SCSS:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-scss"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">.app</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#6f42c1;--shiki-dark:#B392F0">.hstack</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#6f42c1;--shiki-dark:#B392F0">.vstack</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  display</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">flex</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863a;--shiki-dark:#85E89D">  &amp;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, &gt; </span><span style="color:#22863a;--shiki-dark:#85E89D">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    flex</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">none</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    overflow</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">hidden</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">.app</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#6f42c1;--shiki-dark:#B392F0">.hstack</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  flex-direction</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">row</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">.vstack</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  flex-direction</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">column</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>The default for <code>flex</code> is <code>auto</code> which means, everything grows and shrinks as required. But we instead want to give some columns a fixed width or height and <code>flex: none</code> will do that. For those elements that we want to grow or even be scrollable, we will add the following options:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-css"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">.-grow</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  flex</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">auto</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">.-scroll</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  overflow</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">auto</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Now we can add these options to <code>list</code> and <code>content</code> like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-html"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> class</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;content -grow -scroll&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span></code></pre><p>That’s basically it. You will find a <a href="https://jsfiddle.net/perenzo/sg1q5auj/" rel="noopener" target="_blank" class="external">demo at JSFiddle here</a>. It also shows how to add separators and how to prepare <code>html</code> and <code>body</code> correctly.</p></div><p><em>Published on March 15, 2020</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Implementing Crypto]]></title>
            <link>https://holtwick.de/blog/apple-webcrypto</link>
            <guid isPermaLink="false">https://holtwick.de/blog/apple-webcrypto</guid>
            <pubDate>Tue, 13 Feb 2018 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>I’d like to share some interesting findings I made while implementing a cross platform crypto format.</p><h2 id="basics" tabindex="-1"><a class="header-anchor" href="#basics">Basics</a></h2><p>Usually you will need these features from a crypto implementation:</p><ol><li>Random data generation</li><li>Checksum and HMAC calculations, like using <code>SHA2</code></li><li>Key derivation from a password, like using <code>PBKDF2</code></li><li>Encryption and decryption, like using <code>AES</code></li></ol><p>This is a pretty basic set of requirements and most crypto implementations support these. But as always the devil is in the details, because if you make a small mistake you will just get a wrong result and this result will not help you find the cause of the bug. It is wrong or it is right. Basta.</p><h2 id="binary-data" tabindex="-1"><a class="header-anchor" href="#binary-data">Binary Data</a></h2><p>So cryptography is applied on raw binary data. This may already be the source of troubles:</p><ul><li>Did you use the correct encoding, like <code>UTF-8</code>?</li><li>Is the implementation of your <code>Base64</code> and <code>Hex</code> helpers correctly working?</li><li>Is there a good way to concatenate data or slice it?</li></ul><p>On iOS and macOS you will usually use <code>NSData</code> which is fine. In the Javascript world you will soon be in trouble. If your target is <em>node.js</em> you can use <code>Buffer</code> which is quite nice, but on Web you will find yourself using <code>Uint8Array</code> pretty soon, which has no nice native toolset for converting between UTF-8 strings, hex and base64 presentations.</p><h2 id="iv-and-salt" tabindex="-1"><a class="header-anchor" href="#iv-and-salt">IV and Salt</a></h2><p>Random data makes it harder for an attacker to crack encrypted data and passwords, so using initialization vectors (IV) for encryption and a salt for password generation is a good practice.</p><p>But IV support on node.js is totally broken and ends in an exception for certain circumstances. I was pretty surprised about that, but I was not able to find a workaround, which made me switch to WebCryptography.</p><p>Another trap you can fall into eventually while testing is exactly this randomness of IV and salt if you forget to preset those in your test cases, because the result obviously will always differ if different random elements are created. Sounds obvious, but will happen ;)</p><h2 id="choosing-algorithms" tabindex="-1"><a class="header-anchor" href="#choosing-algorithms">Choosing Algorithms</a></h2><p>You will read a lot about which algorithms are the best, but in real life you’ll end up with taking what is there and implemented on all platforms you are targeting.</p><p>For checksums <code>SHA256</code> and <code>SHA512</code> are standard. For keyword derivation you will usually only find support for <code>PBKDF2</code> everywhere. For symmetric encryption the standard is <code>AES</code>. But it comes with different flavors. I found <code>CBC</code> was a good compromise that is available almost everywhere. <code>CTR</code> is also pretty widely supported.</p><h2 id="package-the-secret" tabindex="-1"><a class="header-anchor" href="#package-the-secret">Package the Secret</a></h2><p>Ok, you now have what you need and start happy encryption. It would be great, if all you need were self-contained, but usually it is not. You will need to store the salt somewhere to rebuild your key and you will also need the IV to decrypt your data.</p><p>So let’s store the IV with the encrypted data, like: <code>IV + cipherText = package</code>.</p><p>But we should also make sure the data cannot be manipulated, therefore we add an <code>HMAC</code> over the data of the previous package and add it as well: <code>IV + cipherText + HMAC = package</code>.</p><p>That’s nice. Now you can use the handy tools I mentioned in <em>Binary Data</em> to deconstruct it for decryption ;)</p><h2 id="warning" tabindex="-1"><a class="header-anchor" href="#warning">Warning</a></h2><p>I’m not a crypto expert and I’ll be very happy to hear your feedback about it e.g. on Twitter <a href="https://twitter.com/holtwick" rel="noopener" target="_blank" class="external">@holtwick</a>.</p></div><p><em>Published on February 13, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Why I developed a static Website Builder 🤦🏻‍♂️]]></title>
            <link>https://holtwick.de/blog/birth-of-hostic</link>
            <guid isPermaLink="false">https://holtwick.de/blog/birth-of-hostic</guid>
            <pubDate>Wed, 02 Sep 2020 06:00:00 GMT</pubDate>
            <description><![CDATA[I developed the static website generator Hostic. Here I explain why and how I did it.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>I love <a href="https://vuejs.org/" rel="noopener" target="_blank" class="external">Vue.js</a> from <a href="https://evanyou.me/" rel="noopener" target="_blank" class="external">Evan You</a> and I like static websites. Of course there are already solutions to combine these two passions like <a href="https://vuepress.vuejs.org/" rel="noopener" target="_blank" class="external">VuePress</a> or <a href="https://nuxtjs.org/" rel="noopener" target="_blank" class="external">Nuxt</a>. But would I be a programmer if I would choose this simple way?</p><p>Of course I wanted to get to the <em>bleeding edge</em> and was quickly inspired by Evans newest coup: <a href="https://github.com/vitejs/vite" rel="noopener" target="_blank" class="external">vite</a>. It throws the ballast of the <a href="https://webpack.js.org/" rel="noopener" target="_blank" class="external">webpack</a> overboard and does everything right. First I tried my luck with it and <a href="https://github.com/vuejs/vitepress" rel="noopener" target="_blank" class="external">vitepress</a>, but unfortunately that was not quite what I was looking for.</p><p>So I took a step back and looked at the classics of static website generation: <a href="https://www.gatsbyjs.com/" rel="noopener" target="_blank" class="external">Gatsby</a>, <a href="https://gohugo.io/" rel="noopener" target="_blank" class="external">Hugo</a>, <a href="https://jekyllrb.com" rel="noopener" target="_blank" class="external">Jekyll</a> and <a href="https://www.11ty.dev/" rel="noopener" target="_blank" class="external">11ty</a>. They did everything right too, but everything didn’t come off the shelf as I would like it to. Especially since I had already built my own solution for <a href="static-jquery">SeaSite</a>, with which all my websites were generated.</p><h2 id="what-do-i-want%3F" tabindex="-1"><a class="header-anchor" href="#what-do-i-want%3F">What do I want?</a></h2><p>But what was it that I wanted? I have found out the following points for me:</p><ol><li><strong>Speed:</strong> I want to make changes in the code like I did with Vue.js and see the result immediately in the browser.</li><li><strong>Flexibility:</strong> I would like to be able to influence every aspect of the code myself and be able to program. Preferably in Javascript.</li><li><strong>Post processing:</strong> I would like to be able to easily adjust content after it has already been calculated. This was the core principle of SeaSite, which allowed me to optimize images and videos afterwards, but also to run translations of text passages for different language versions.</li></ol><h2 id="how-do-i-do-it%3F" tabindex="-1"><a class="header-anchor" href="#how-do-i-do-it%3F">How do I do it?</a></h2><p>Well, at point 1 I had already discovered <a href="https://github.com/evanw/esbuild" rel="noopener" target="_blank" class="external">esbuild</a> in <a href="https://github.com/vitejs/vite" rel="noopener" target="_blank" class="external">vite</a>. It is so incredibly fast that I could not believe it. The result is also reliable and exactly as it should be. <a href="https://github.com/evanw/esbuild" rel="noopener" target="_blank" class="external">esbuild</a> was set as a tool that I wanted to use.</p><p>So I first built a small Node.js script that transpiled a Javascript file. I also built a small library to register routes. The generation of the content should be done on-demand when the website is requested by a simple Express.js webserver. To generate the static pages I would simply generate and save the content for all registered routes. This worked great and took only milliseconds.</p><p>Quickly I wanted to have the comfort of <a href="https://github.com/vitejs/vite" rel="noopener" target="_blank" class="external">vite</a>, i.e. when files change, the browser reloads immediately. With Chokidar I could watch the folder with the JS files and recompile everything via <a href="https://github.com/evanw/esbuild" rel="noopener" target="_blank" class="external">esbuild</a>. With a little trick, the import cache of Node.js could be bypassed and the new JS could be loaded and executed. With socket.io a reload mechanism for the browser was quickly assembled.</p><h2 id="now-everything-should-become-more-beautiful!" tabindex="-1"><a class="header-anchor" href="#now-everything-should-become-more-beautiful!">Now everything should become more beautiful!</a></h2><p>I had now finally caught fire and there was no turning back. Then it could also become more beautiful :) Unfortunately I didn’t succeed in integrating Vue.js at the first go, but I also doubted if this would make sense at all. In SeaSite I had already used JSX and JSDOM. For another project I had already written a DOM abstraction, which is very lean. I now extended it in a way that HTML and XML could be generated easily with JSX.</p><p>This made it possible to manipulate the content with simple DOM actions. But how much nicer it would be, if the corresponding nodes could be found by CSS selectors. So I also implemented the css-parse and it worked fine.</p><p>Also a markdown parser was already available from SeaSite and was only extended to provide meta data for the registration of routes while maintaining the pleasant speed.</p><h2 id="open-source!" tabindex="-1"><a class="header-anchor" href="#open-source!">Open Source!</a></h2><p>So now everything was on board that was needed and it was time to create a simple unified structure to publish the project. A first goal was to describe the routes with simple data structures to get maximum flexibility. For common formats like HTML, XML, JSON, text and assets convenient methods were created.</p><p>Since everything had the appearance of a web server anyway, which can also spit out static pages, it was obvious to adopt the smart middleware pattern of <a href="https://koajs.com/" rel="noopener" target="_blank" class="external">Koa.js</a>. This way, templates and plugins are easy to realize. A copy of the data structure mentioned serves then as context and the result is expected in the property <code>ctx.body</code>.</p><p>Here it is now, the final project. I would be very happy about help and ideas. Maybe it is not the greatest tool to create static websites, but maybe it is the basis for an even smarter solution that builds on it.</p><p><strong><a href="https://github.com/holtwick/hostic" rel="noopener" target="_blank" class="external">github.com/holtwick/hostic</a></strong></p><p><strong><a href="https://www.npmjs.com/package/hostic" rel="noopener" target="_blank" class="external">https://www.npmjs.com/package/hostic</a></strong></p><p>In the coming posts I will further explore some of the issues that arise when creating a website and how they can be solved with Hostic. The list of current ideas on topics:</p><ul><li>Building a simple static website with Hostic</li><li>Building a blog with Markdown</li><li>Building a multilingual website and localization</li><li>Optimizations for search engines and accessibility</li><li>Hosting: Beaker Browser, see…</li></ul><p>These websites are already driven by Hostic:</p><ul><li><a href="https://pdfify.app" rel="noopener" target="_blank" class="external">https://pdfify.app</a></li><li><a href="https://holtwick.de" rel="noopener" target="_blank" class="external">https://holtwick.de</a></li></ul></div><p><em>Published on September 2, 2020</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Video Chat Briefing - More features]]></title>
            <link>https://holtwick.de/blog/briefing</link>
            <guid isPermaLink="false">https://holtwick.de/blog/briefing</guid>
            <pubDate>Fri, 16 Apr 2021 06:00:00 GMT</pubDate>
            <description><![CDATA[News feature in Briefing video chat app]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Last year I started video chat projects. First <a href="https://peer-school.apperdeck.com" rel="noopener" target="_blank" class="external">peer.school</a> with which I wanted to contribute to the education of younger children. Then from that <a href="https://brie.fi/ng" rel="noopener" target="_blank" class="external">Brie.fi/ng</a>, which focuses on secure communication.</p><p>Briefing in particular has found many friends on <a href="https://github.com/holtwick/briefing/" rel="noopener" target="_blank" class="external">Github</a> and is now available in several languages, including Chinese and very recently Russian.</p><p>The project has a very simple design and uses the Javascript framework <a href="https://vuejs.org/" rel="noopener" target="_blank" class="external">Vue</a>. All the necessary functions are available, plus even such exciting features as hiding or swapping a person’s background.</p><p>Some native apps are also available, but among them the <a href="https://apps.apple.com/app/briefing-video-chat/id1510803601" rel="noopener" target="_blank" class="external">iOS App</a> with support for AppClips (enter rooms via QR code or NFC transmitter) is especially worth mentioning.</p><h2 id="embedding" tabindex="-1"><a class="header-anchor" href="#embedding">Embedding</a></h2><p>Besides sharing a URL to enter a shared room, there is also the possibility to embed Briefing directly into your own website. A simple <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe" rel="noopener" target="_blank" class="external">IFrame</a> makes it possible, like this one:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-html"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">iframe</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  src</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;https://brie.fi/ng/cool-call-62?audio=1&amp;video=1&amp;fs=0&amp;invite=0&amp;prefs=0&amp;share=0&quot;</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  allow</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;camera; microphone; fullscreen; speaker; display-capture&quot;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">iframe</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span></code></pre><p>For easy configuration, there is now an <a href="https://brie.fi/ng/embed-demo" rel="noopener" target="_blank" class="external">IFrame Tool</a> where anyone can quickly configure their code themselves with a few clicks:</p><p class="img-wrapper"><a href="https://brie.fi/ng/embed-demo" rel="noopener" target="_blank" class="external"><img src="/assets/e2h6ou5ggjt1r8.png" alt="Screenshot" width="555" height="728" loading="lazy"></a></p><p>If you want to have full control, you can also install all the components that are necessary for operation yourself. Instructions can be found in the <a href="https://github.com/holtwick/briefing/#readme" rel="noopener" target="_blank" class="external">README on Github</a>.</p><h2 id="commercial-license" tabindex="-1"><a class="header-anchor" href="#commercial-license">Commercial license</a></h2><p>Briefing is open source, but under the <a href="https://eupl.eu/" rel="noopener" target="_blank" class="external">EUPL v1.2 license</a>, which is a European answer to the <a href="https://www.gnu.org/licenses/gpl-3.0.en.html" rel="noopener" target="_blank" class="external">GPL</a>. This means that changes have to be published under the same license.</p><p>However, if it is desired not to publish changes, it is certainly fair to acquire a commercial license that offers these freedoms. Everything about this can also be found in the <a href="https://github.com/holtwick/briefing/#commercial-license" rel="noopener" target="_blank" class="external">README</a>.</p><h2 id="future" tabindex="-1"><a class="header-anchor" href="#future">Future</a></h2><p>Currently I’m working on a third product that should follow in the footsteps of Briefing. The focus is on better scalability, privacy and encryption, and relatime collaboration. The development is already well advanced. More about that soon here…</p></div><p><em>Published on April 16, 2021</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Me, the AI, and Our Sandbox]]></title>
            <link>https://holtwick.de/blog/bx-review</link>
            <guid isPermaLink="false">https://holtwick.de/blog/bx-review</guid>
            <pubDate>Thu, 16 Apr 2026 06:00:00 GMT</pubDate>
            <description><![CDATA[A hands-on report about using the bx sandbox for macOS, with a special focus on AI and VSCode.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><a class="app-codeberg-badge" href="https://codeberg.org/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-codeberg-badge-left"><svg viewBox="0 0 512 512" height="16" xmlns="http://www.w3.org/2000/svg"><circle cx="256" cy="256" r="248" fill="none" stroke="currentColor" stroke-width="36"></circle><path fill="currentColor" d="M255.3 71.8a192 192 0 0 0-162 294l160.1-207c.5-.6 1.5-1 2.6-1s2 .4 2.6 1l160 207a192 192 0 0 0 29.4-102c0-106-86-192-192-192a192 192 0 0 0-.7 0z"></path></svg> Codeberg </span><span class="app-codeberg-badge-right">holtwick/bx-mac</span></a> <a class="app-github-badge" href="https://github.com/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-github-badge-left"><svg viewBox="0 0 98 96" height="16" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="currentColor"></path></svg> GitHub </span><span class="app-github-badge-right">holtwick/bx-mac</span></a><p>Three weeks ago, I <a href="bx-sandbox">published a sandbox</a> to keep the AI somewhat in check. The built-in safeguards that Claude Code offers, for example, seemed untrustworthy to me, since they could usually be overridden by my instructions to the app. On top of that, I had a very uneasy feeling because sensitive information like SSH keys or project-specific confidential data in <code>.env.local</code> wasn’t inherently protected from the tools’ access.</p><p class="img-wrapper"><img src="/assets/lchg45gdf5lyd6.png" alt="Image" width="828" height="520" loading="lazy"></p><p>When I looked around at the available options, most of them seemed quite complex or restricted my workflow — for example, working inside a Docker environment. I had something in mind that would be as simple as using <code>.gitignore</code>. So I got straight to work — ironically with the AI — building a cage for it.</p><p>We decided to use Apple’s macOS sandbox functionality. In principle, the restrictions I had specified in <code>.bxignore</code> simply needed to be translated into sandbox instructions. One feature that was important to me was the ability to also protect my development environment, since sensitive operations usually happen within the IDE — in my case, Visual Studio Code.</p><p>It quickly became clear that the ignore file alone wasn’t enough. I added a configuration that could accommodate the specifics of each application being used. This also allowed me to grant access to multiple directories, so I could work on several projects simultaneously. For this reason, I introduced a <code>~/.bxconfig.toml</code>.</p><p>I can now type <code>bx projects</code> to launch my development environment. Visual Studio Code opens with access to my projects. Within those projects, all sensitive data is protected by <code>.bxignore</code>. On top of that, a global baseline configuration protects areas like <code>.ssh</code> and other sensitive locations. Neither the IDE nor the tools invoked within it — such as Claude Code — can access this data.</p><p>After a while, I decided to use Visual Studio Code exclusively for AI work and installed the more open <a href="https://vscodium.com/" rel="noopener" target="_blank" class="external">VSCodium</a> alongside it for everything I didn’t want to do with AI, because it’s really hard to fully “clean up” VSCode.</p><p>Of course, none of this provides 100% security, but it doesn’t have to. It offers enough protection to not feel completely exposed, and it barely restricts the usual workflows. For me personally, it’s a great fit, and I’d be happy if others found it useful too.</p><p>Give it a try:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">brew</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> install</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> holtwick/tap/bx</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> </span></span></code></pre><hr><a class="app-codeberg-badge" href="https://codeberg.org/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-codeberg-badge-left"><svg viewBox="0 0 512 512" height="16" xmlns="http://www.w3.org/2000/svg"><circle cx="256" cy="256" r="248" fill="none" stroke="currentColor" stroke-width="36"></circle><path fill="currentColor" d="M255.3 71.8a192 192 0 0 0-162 294l160.1-207c.5-.6 1.5-1 2.6-1s2 .4 2.6 1l160 207a192 192 0 0 0 29.4-102c0-106-86-192-192-192a192 192 0 0 0-.7 0z"></path></svg> Codeberg </span><span class="app-codeberg-badge-right">holtwick/bx-mac</span></a> <a class="app-github-badge" href="https://github.com/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-github-badge-left"><svg viewBox="0 0 98 96" height="16" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="currentColor"></path></svg> GitHub </span><span class="app-github-badge-right">holtwick/bx-mac</span></a></div><p><em>Published on April 16, 2026</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[AI Coding Tools in a Sandbox: Why Your File System Needs Protection]]></title>
            <link>https://holtwick.de/blog/bx-sandbox</link>
            <guid isPermaLink="false">https://holtwick.de/blog/bx-sandbox</guid>
            <pubDate>Sun, 29 Mar 2026 06:00:00 GMT</pubDate>
            <description><![CDATA[AI coding tools like Claude Code or Copilot have full access to your file system. bx protects everything except the current project directory with a single command.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p class="img-wrapper"><img src="/assets/0nwy5x8g1772fo0.jpg" alt="Photo by Kanhaiya Sharma on Unsplash" width="1684" height="575" loading="lazy"></p><a class="app-codeberg-badge" href="https://codeberg.org/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-codeberg-badge-left"><svg viewBox="0 0 512 512" height="16" xmlns="http://www.w3.org/2000/svg"><circle cx="256" cy="256" r="248" fill="none" stroke="currentColor" stroke-width="36"></circle><path fill="currentColor" d="M255.3 71.8a192 192 0 0 0-162 294l160.1-207c.5-.6 1.5-1 2.6-1s2 .4 2.6 1l160 207a192 192 0 0 0 29.4-102c0-106-86-192-192-192a192 192 0 0 0-.7 0z"></path></svg> Codeberg </span><span class="app-codeberg-badge-right">holtwick/bx-mac</span></a> <a class="app-github-badge" href="https://github.com/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-github-badge-left"><svg viewBox="0 0 98 96" height="16" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="currentColor"></path></svg> GitHub </span><span class="app-github-badge-right">holtwick/bx-mac</span></a><p>If you work with AI-powered coding tools today — be it Claude Code, Copilot, or Cursor — you quickly get used to the productivity boost. What’s easy to overlook: these tools have full access to your entire file system. Every command the AI executes runs with the same permissions as you.</p><p>That might be fine for a hobby project. But if you’re handling client data, have license keys on disk, SSH keys for production servers, or simply private photos in your home directory — at some point, you start feeling uneasy.</p><h2 id="the-existing-options" tabindex="-1"><a class="header-anchor" href="#the-existing-options">The Existing Options</a></h2><p>Of course, there are ways to protect yourself:</p><p><strong>Docker containers</strong> offer good isolation, but they’re cumbersome for typical macOS development workflows. You need to mount volumes, configure IDE integration, and the workflow never feels quite native.</p><p><strong>Claude’s built-in restrictions</strong> — the tool politely asks before modifying files. But “asking” isn’t protection. A hallucinated file path, a misinterpreted command, and suddenly the AI reads something it shouldn’t have seen. The trust is based on the model’s behavior, not on a technical barrier.</p><p><strong>VSCode Workspace Trust</strong> protects against automatic code execution, but not against file system access from a terminal or an extension.</p><p>None of these solutions really convinced me.</p><h2 id="the-idea%3A-use-the-macos-sandbox" tabindex="-1"><a class="header-anchor" href="#the-idea%3A-use-the-macos-sandbox">The Idea: Use the macOS Sandbox</a></h2><p>macOS ships with <code>sandbox-exec</code>, a kernel-level sandbox that Apple itself uses for App Store apps. The idea: a tool that launches any application so it can only see the current project directory — and nothing else.</p><p>The result is <strong>bx</strong>. A CLI tool you put in front of your actual command:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">bx</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> claude</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> ~/work/my-project</span></span></code></pre><p>That’s it. Claude Code starts with full access to <code>~/work/my-project</code> — but <code>~/Documents</code>, <code>~/Desktop</code>, <code>~/.ssh</code>, other projects, and everything else in the home directory is invisible.</p><h2 id="built-in-two-days-%E2%80%94-with-claude-code-itself" tabindex="-1"><a class="header-anchor" href="#built-in-two-days-%E2%80%94-with-claude-code-itself">Built in Two Days — with Claude Code Itself</a></h2><p>The ironic part: bx was built entirely with the tool it’s designed to protect against. In two intensive days, Claude Code wrote the bulk of the code — from sandbox profile generation to CLI argument parsing to app discovery via macOS Spotlight.</p><p>It worked surprisingly well. Claude knew the (undocumented!) Apple Sandbox Profile Language and correctly handled its quirks — for instance, the fact that <code>deny</code> rules in SBPL always take precedence over <code>allow</code> rules, regardless of order. This means you can’t simply lock down the entire home directory and then add exceptions. bx solves this by scanning the home directory and selectively blocking only sibling directories.</p><h2 id="more-than-just-claude-code" tabindex="-1"><a class="header-anchor" href="#more-than-just-claude-code">More Than Just Claude Code</a></h2><p>bx isn’t limited to one tool. It supports VSCode, Xcode, Terminal, and arbitrary commands out of the box. Any app can be added via a TOML configuration file:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-toml"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">[</span><span style="color:#6f42c1;--shiki-dark:#B392F0">apps</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">cursor</span><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">bundle = </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;com.todesktop.230313mzl4w4u92&quot;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">binary = </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;Contents/MacOS/Cursor&quot;</span></span></code></pre><p>The tool finds apps automatically via their macOS bundle ID — no hardcoded paths needed. And if you regularly work with the same projects, you can configure default directories per app:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-toml"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">[</span><span style="color:#6f42c1;--shiki-dark:#B392F0">apps</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">code</span><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">workdirs = [</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;~/work/project-a&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;~/work/shared-lib&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span></code></pre><p>A simple <code>bx code</code> then opens VSCode with exactly those directories — sandboxed.</p><h2 id="fine-grained-control" tabindex="-1"><a class="header-anchor" href="#fine-grained-control">Fine-Grained Control</a></h2><p>What makes bx especially practical: you can hide files even within an allowed project directory. A <code>.bxignore</code> file in the project works like <code>.gitignore</code>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>.env</span></span>
<span class="line"><span>.env.*</span></span>
<span class="line"><span>*.pem</span></span>
<span class="line"><span>secrets/</span></span></code></pre><p>This keeps environment variables and certificates invisible, even when the project directory itself is fully accessible.</p><p>With <code>bx --dry</code> you can preview exactly what will be protected — without launching anything. It gives you a clear picture of the actual isolation.</p><h2 id="the-growing-attack-surface" tabindex="-1"><a class="header-anchor" href="#the-growing-attack-surface">The Growing Attack Surface</a></h2><p>What many people underestimate: modern AI coding tools are far more than chat interfaces. Claude Code, for example, can execute shell commands, create and delete files, and access external services via MCP servers (Model Context Protocol) — databases, APIs, cloud infrastructure. On top of that, there are skills and hooks that can trigger actions automatically.</p><p>All of this happens in the user’s context, with their full permissions. bx’s sandbox operates at the kernel level: whether it’s an <code>rm</code> command, an MCP tool, or an automated hook trying to access <code>~/Documents</code> — the operating system blocks the access before it even happens. This is fundamentally different from a software-level restriction that could be bypassed.</p><p>Important to note: within the allowed project directory, everything is permitted — that’s intentional, otherwise you couldn’t work. If you have sensitive files there, you can selectively exclude them via <code>.bxignore</code>.</p><h2 id="being-honest" tabindex="-1"><a class="header-anchor" href="#being-honest">Being Honest</a></h2><p>bx is not a high-security vault. <code>sandbox-exec</code> is an undocumented Apple API that could change with any macOS update. There’s no network protection — API calls, git push, and npm install all work normally. And sandbox rules are generated once at launch; directories created afterwards are not automatically protected.</p><p>But as a pragmatic security layer for everyday development, it works remarkably well. It’s the difference between “the AI can theoretically read everything” and “the AI can only see this one project.”</p><p>That said, to be clear: bx was built with the best of intentions and to the best of my knowledge, but it comes with no guarantees. It’s not a replacement for a professional security solution, and it doesn’t absolve anyone from thinking for themselves. If you’re working with truly critical data, don’t rely blindly on any single tool — no matter how well it works. bx is an additional layer of protection, not a substitute for common sense.</p><h2 id="try-it-out" tabindex="-1"><a class="header-anchor" href="#try-it-out">Try It Out</a></h2><p>bx can be installed via Homebrew or npm:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-bash"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">brew</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> install</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> holtwick/tap/bx</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"># or</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">npm</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> install</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -g</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> bx-mac</span></span></code></pre><p>The code is open source: <a class="app-codeberg-badge" href="https://codeberg.org/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-codeberg-badge-left"><svg viewBox="0 0 512 512" height="16" xmlns="http://www.w3.org/2000/svg"><circle cx="256" cy="256" r="248" fill="none" stroke="currentColor" stroke-width="36"></circle><path fill="currentColor" d="M255.3 71.8a192 192 0 0 0-162 294l160.1-207c.5-.6 1.5-1 2.6-1s2 .4 2.6 1l160 207a192 192 0 0 0 29.4-102c0-106-86-192-192-192a192 192 0 0 0-.7 0z"></path></svg> Codeberg </span><span class="app-codeberg-badge-right">holtwick/bx-mac</span></a> <a class="app-github-badge" href="https://github.com/holtwick/bx-mac" target="_blank" rel="noopener"><span class="app-github-badge-left"><svg viewBox="0 0 98 96" height="16" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="currentColor"></path></svg> GitHub </span><span class="app-github-badge-right">holtwick/bx-mac</span></a></p><p>I’d love to hear your feedback, feature requests, and of course stars. And if you have interesting use cases — let me know!</p><hr><p><em>This blog post was written by Claude (Anthropic’s AI) and reviewed and approved by me. Fitting, given the topic, I’d say.</em></p></div><p><em>Published on March 29, 2026</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Write your first e-invoice...]]></title>
            <link>https://holtwick.de/blog/einvoice</link>
            <guid isPermaLink="false">https://holtwick.de/blog/einvoice</guid>
            <pubDate>Mon, 06 Jan 2025 07:00:00 GMT</pubDate>
            <description><![CDATA[Convert PDF invoices to e-invoices instantly. Create EN 16931, XRechnung, and ZUGFeRD compliant electronic invoices online. Free tool for small businesses and freelancers.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p><img src="/assets/dwp75m1o10jr3lk.jpeg" alt="right" width="533" height="800" loading="lazy">…with our new <a href="https://e-invoice.space?ref=holtwick" rel="noopener" target="_blank" class="external">Swiss army knife for e-invoices</a>. Following the latest update, e-invoices can now be easily created online. Even with your own stationery and fonts. The result is a PDF that is compatible with all required standards, including EN 16931, XRechnung, ZUGFeRD and PDF/A.</p><p>The highlight: invoices that were previously created using Word or similar programs can be converted into an e-invoice in no time at all. Simply upload the PDF of the existing invoice, the values are read out as if by magic and the result is a standard-compliant e-invoice in ZUGFeRD format. This means that the electronic data has been embedded in the PDF as XML and the other technical requirements have also been met.</p><p>The result can be quickly saved and sent. But also in our Mac tool <a href="https://receipts-app.com?ref=holtwick" rel="noopener" target="_blank" class="external">Receipts</a>.</p><p>But that’s not all. Received e-invoices, whether XML or PDF, can simply be dragged into the XML2Invoice window and everything is readable and ready for payment or further processing.</p><p><em>As I said, the Swiss army knife of e-invoicing</em> 😎</p><p class="action"><a href="https://e-invoice.space?ref=holtwick" class="button oui-button external" target="_blank" rel="noopener noreferrer">Write your first e-invoice now!</a></p><p><strong>Update 2025-01-29:</strong> XML2Invoice is now called <a href="https://e-invoice.space" rel="noopener" target="_blank" class="external">E-Invoice Space</a></p></div><p><em>Published on January 6, 2025</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[E-Invoice Space]]></title>
            <link>https://holtwick.de/blog/einvoice-space</link>
            <guid isPermaLink="false">https://holtwick.de/blog/einvoice-space</guid>
            <pubDate>Wed, 29 Jan 2025 07:00:00 GMT</pubDate>
            <description><![CDATA[Transform your invoicing process with E-Invoice Space. Create, convert and manage EN 16931 compliant e-invoices with our powerful web application. Free PDF to XML conversion.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>After the successful launch of <strong>XML2Invoice</strong>, many new features and improvements have been added, so the renaming to <a href="https://e-invoice.space?ref=holtwick" rel="noopener" target="_blank" class="external"><strong>E-Invoice Space</strong></a> is now taking place.</p><h2 id="new-layout" tabindex="-1"><a class="header-anchor" href="#new-layout">New layout</a></h2><p>The space is now used more sensibly, with only two areas: the PDF preview and the input area. It is also possible to adjust the size ratio between the two areas. There is now a general division between <strong>reading</strong> and <strong>writing</strong> to better highlight the strengths of <a href="https://e-invoice.space?ref=holtwick" rel="noopener" target="_blank" class="external"><strong>E-Invoice Space</strong></a> and simplify access to the functions.</p><h2 id="create-your-own-invoices" tabindex="-1"><a class="header-anchor" href="#create-your-own-invoices">Create your own invoices</a></h2><p>In the <strong>Write</strong> area, in addition to enriching an existing simple PDF with e-invoice data, a completely new invoice can now be created. All the necessary data can be conveniently entered and a preview is generated immediately. In the PRO version, it is also possible to select an individual background design (stationery) and upload your own fonts in TTF format.</p><h2 id="en-16931-compatibility" tabindex="-1"><a class="header-anchor" href="#en-16931-compatibility">EN 16931 compatibility</a></h2><p><a href="https://e-invoice.space?ref=holtwick" rel="noopener" target="_blank" class="external"><strong>E-Invoice Space</strong></a> now also meets the requirements of the EN 16931 standard, not just BASIC and MINIMAL as before.</p><p>Overall, the application has become more comprehensive and user-friendly, enabling quick results. As usual, everything takes place locally, so that the web application behaves like a locally installed application. No data is stored or transferred to a cloud service.</p></div><p><em>Published on January 29, 2025</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[How to transpile at the speed of light 💫]]></title>
            <link>https://holtwick.de/blog/esbuild</link>
            <guid isPermaLink="false">https://holtwick.de/blog/esbuild</guid>
            <pubDate>Thu, 20 Aug 2020 06:00:00 GMT</pubDate>
            <description><![CDATA[For every developer there comes the moment where speed matters. It saves you a relevant amount of time and keeps the flow going.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>For every developer there comes the moment where speed matters. It saves you a relevant amount of time and keeps the flow going.</p><p><a href="https://github.com/evanw/esbuild" rel="noopener" target="_blank" class="external">esbuild</a> is definitely fast and reduces the built time significantly. And it is nice and simple too, when it comes to set up.</p><h2 id="build" tabindex="-1"><a class="header-anchor" href="#build">Build</a></h2><p>It can be started from command line or nicely being integrated in a node.js script like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-js"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> esbuild</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> require</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;esbuild&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> options</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  target: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;node12&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  platform: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;node&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  jsxFactory: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;h&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  jsxFragment: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;hh&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  bundle: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">true</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  outfile: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;out.js&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  sourcemap: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;inline&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  loader: {</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">    &apos;.js&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;jsx&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">    &apos;.css&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;text&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  entryPoints: [</span><span style="color:#032f62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292e;--shiki-dark:#E1E4E8">sitePath</span><span style="color:#032f62;--shiki-dark:#9ECBFF">}/index.js`</span><span style="color:#24292e;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">await</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> service.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">build</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(options)</span></span></code></pre><p>This will build a single JS file containing everything that is needed to run. It also translates JSX and uses the function <code>h</code> to create elements. It also loads files ending on <code>.css</code> as plain text. A source map will be written as well. All this is done in a fragment of a second! This is because esbuild is written in Go instead of Javascript, because speed matters sometimes.</p><h2 id="sitemaps" tabindex="-1"><a class="header-anchor" href="#sitemaps">Sitemaps</a></h2><p>Speaking of source maps the same author of esbuild also wrote a module to support them on node: <a href="https://github.com/evanw/node-source-map-support" rel="noopener" target="_blank" class="external">node-source-map-support</a>.</p><h2 id="testing" tabindex="-1"><a class="header-anchor" href="#testing">Testing</a></h2><p>Now the setup is almost complete, but how about testing? I usually use <a href="https://jestjs.io/" rel="noopener" target="_blank" class="external">jest</a> for testing and therefore I wanted to get it working here as well. The solutions available did not fit my case, therefore I wrote my own transform:</p><p>First make sure to tell Jest what to do in a <code>package.json</code> section:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;jest&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    &quot;transform&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">      &quot;^.+\\.jsx?$&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;./src/jest-transform.js&quot;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    &quot;testEnvironment&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;node&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    &quot;testPathIgnorePatterns&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">      &quot;node_modules/&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">      &quot;dist/&quot;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    ]</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>The transformer looks like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-js"><span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">// Inspired by https://github.com/aelbore/esbuild-jest#readme</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> fs</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> require</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;node:fs&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> esbuild</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> require</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;esbuild&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> pkg</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> require</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;../package.json&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> external</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  ...</span><span style="color:#24292e;--shiki-dark:#E1E4E8">Object.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">keys</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(pkg.dependencies </span><span style="color:#d73a49;--shiki-dark:#F97583">??</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {}),</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  ...</span><span style="color:#24292e;--shiki-dark:#E1E4E8">Object.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">keys</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(pkg.devDependencies </span><span style="color:#d73a49;--shiki-dark:#F97583">??</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {}),</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  ...</span><span style="color:#24292e;--shiki-dark:#E1E4E8">Object.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">keys</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(pkg.peerDependencies </span><span style="color:#d73a49;--shiki-dark:#F97583">??</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {}),</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">module</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.</span><span style="color:#005cc5;--shiki-dark:#79B8FF">exports</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  getCacheKey</span><span style="color:#24292e;--shiki-dark:#E1E4E8">() { </span><span style="color:#6a737d;--shiki-dark:#6A737D">// Forces to ignore Jest cache</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> Math.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">random</span><span style="color:#24292e;--shiki-dark:#E1E4E8">().</span><span style="color:#6f42c1;--shiki-dark:#B392F0">toString</span><span style="color:#24292e;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  process</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#e36209;--shiki-dark:#FFAB70">content</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#e36209;--shiki-dark:#FFAB70">filename</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    esbuild.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">buildSync</span><span style="color:#24292e;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      target: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;node14&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      platform: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;node&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      jsxFactory: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;h&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      jsxFragment: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;h&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      bundle: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">true</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      outfile: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;out-jest.js&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      sourcemap: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;inline&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      loader: {</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">        &apos;.js&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;jsx&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">        &apos;.css&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;text&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      },</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      entryPoints: [filename],</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      external,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    })</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> js</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> fs.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">readFileSync</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(file, </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;utf-8&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    fs.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">unlinkSync</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(file)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> js</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  },</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="competition" tabindex="-1"><a class="header-anchor" href="#competition">Competition</a></h2><p>Why would you want to you use esbuild and not webpack, babel, rollup, etc.? Well, because it is fast and easy to use. The other solutions are blown up and become pretty complex after a while. They have many 3rd party dependencies, which can cause troubles as well.</p><p>If you want to experience the blatant acceleration, then try <a href="https://github.com/evanw/esbuild" rel="noopener" target="_blank" class="external">esbuild</a>.</p><hr><p><span>Photo by <a href="https://unsplash.com/@traf?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer" class="external">Traf</a> on <a href="https://unsplash.com/s/photos/fast?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener noreferrer" class="external">Unsplash</a></span></p></div><p><em>Published on August 20, 2020</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[EU funding for open source software through a performance-based platform]]></title>
            <link>https://holtwick.de/blog/eu-opensource</link>
            <guid isPermaLink="false">https://holtwick.de/blog/eu-opensource</guid>
            <pubDate>Mon, 02 Feb 2026 07:00:00 GMT</pubDate>
            <description><![CDATA[Proposal for an EU platform that funds open-source projects using measurable criteria, benefiting developers, the EU, and society.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Info</p><p>The EU is asking for <a href="https://netzpolitik.org/2026/konsultation-eu-kommission-arbeitet-an-neuer-open-source-strategie/" rel="noopener" target="_blank" class="external">feedback on its open source strategy</a>. Here is my suggestion. Please send your feedback via <a href="https://mastodon.social/@holtwick" rel="noopener" target="_blank" class="external">Mastodon</a>.</p></div><h2 id="initial-situation" tabindex="-1"><a class="header-anchor" href="#initial-situation">Initial situation</a></h2><p>As a software developer, I face a fundamental dilemma: developing high-quality open-source software—such as my end-to-end encrypted video conferencing solution <a href="https://brie.fi/ng" rel="noopener" target="_blank" class="external">Briefing</a>—requires a considerable investment of time and capital. Without sustainable funding, my only option is commercial exploitation, even though I would prefer to make the software completely free and continue to develop it intensively. This problem affects numerous developers in Europe.</p><h2 id="core-idea" tabindex="-1"><a class="header-anchor" href="#core-idea">Core idea</a></h2><p>The EU should establish a permanent funding system that promotes open source development through performance-based remuneration. Specifically, I propose an EU-operated platform (comparable to GitHub or Codeberg.org) that distributes funding based on measurable success criteria.</p><h2 id="how-it-works" tabindex="-1"><a class="header-anchor" href="#how-it-works">How it works</a></h2><p>The platform would evaluate the usefulness and quality of projects using objective metrics:</p><ul><li>Community engagement (ratings, stars)</li><li>Technical reach (integration into other projects, downloads)</li><li>Maintenance quality (responding to issues, fixing bugs)</li><li>Documentation and support</li></ul><p>Funding would be distributed proportionally, with upper limits per project/person to prevent abuse.</p><h2 id="benefits-for-all-involved" tabindex="-1"><a class="header-anchor" href="#benefits-for-all-involved">Benefits for all involved</a></h2><ul><li><em>For developers</em>: Direct link between performance and remuneration; opportunity to focus entirely on open source development</li><li><em>For the EU</em>: Building digital sovereignty; control over quality standards (e.g., EUPL licensing, developer verification); promotion of European innovation; reduction of dependence on non-European platforms</li><li><em>For society</em>: Access to secure, transparent software; strengthening of the European tech ecosystem</li></ul><h2 id="conclusion" tabindex="-1"><a class="header-anchor" href="#conclusion">Conclusion</a></h2><p>This initiative would create systematic incentives for the development of high-quality open source software while promoting European values such as transparency, data protection, and digital sovereignty.</p></div><p><em>Published on February 2, 2026</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Preventing fake news]]></title>
            <link>https://holtwick.de/blog/fake-news</link>
            <guid isPermaLink="false">https://holtwick.de/blog/fake-news</guid>
            <pubDate>Wed, 07 Feb 2024 07:00:00 GMT</pubDate>
            <description><![CDATA[How to prevent fake news by signing messages.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>I recently read about a “massive fake news machine” that the German Foreign Ministry claims to have uncovered (<a href="#sources">see below</a>). It can hardly be distinguished from legitimate news.</p><p>It occurred to me spontaneously that if you are not able to <em>stop</em> manipulated news, you should at least be able to <em>identify</em> legitimate news.</p><p><strong>The idea is simple: let’s create a digital network of trust using cryptographic means.</strong></p><h3 id="from-here-it-gets-technical%E2%80%A6" tabindex="-1"><a class="header-anchor" href="#from-here-it-gets-technical%E2%80%A6">From here it gets technical…</a></h3><p>The simplest form of signing is the calculation of a hash values. So let’s assume I want to tweet as <a href="https://twitter.com/holtwick" rel="noopener" target="_blank" class="external">@holtwick</a>: “I don’t like fake news!” and my secret would be “Gurkensalat” (usually called “salt”). In the terminal it would go like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">echo</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -n</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;@holtwick I don&apos;t like fake news! Gurkensalat&quot;</span><span style="color:#d73a49;--shiki-dark:#F97583"> |</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> shasum</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -a</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 256</span></span></code></pre><p>Alternatively with OpenSSL:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">echo</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -n</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;@holtwick I don&apos;t like fake news! Gurkensalat&quot;</span><span style="color:#d73a49;--shiki-dark:#F97583"> |</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> openssl</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> dgst</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -sha256</span></span></code></pre><p>The result is:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>55bc8fe5493377787bfc0be29417fd692070dc8446b855d62a9fedca5b536d53</span></span></code></pre><p>This can also be calculated before I tweet it. Then I take part of the result, e.g. the first 8 characters, and send it with the message, perhaps with a distinguishing feature such as a preceding <code>~</code> character:</p><div class="markdown-alert markdown-alert-example"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-table-of-contents"><path d="M16 12H3"></path><path d="M16 18H3"></path><path d="M16 6H3"></path><path d="M21 12h.01"></path><path d="M21 18h.01"></path><path d="M21 6h.01"></path></svg>@holtwick</p><p>I don’t like fake news! ~55bc8fe5</p></div><p>Now it’s time to verify something like this. A small service on your own website would be conceivable. Here is a small example in PHP of what an online check could look like:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-php"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">&lt;?</span><span style="color:#005cc5;--shiki-dark:#79B8FF">php</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> </span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">// This one is top secret :)</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">$salt </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;Gurkensalat&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">// Normalize whitespace to avoid copy paste issues</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">$sample </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> trim</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">preg_replace</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;/</span><span style="color:#032f62;--shiki-dark:#DBEDFF">[\t\n\r\s]</span><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos; &apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, $_GET[</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;text&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">])) </span><span style="color:#d73a49;--shiki-dark:#F97583">.</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot; &quot;</span><span style="color:#d73a49;--shiki-dark:#F97583"> .</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> $salt;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">// Calculate SHA256 and only use the first 8 chars</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">$hash </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> substr</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">hash</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;sha256&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,  $sample), </span><span style="color:#005cc5;--shiki-dark:#79B8FF">0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#005cc5;--shiki-dark:#79B8FF">8</span><span style="color:#24292e;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">// Compare hashes and return result</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">echo</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> strcmp</span><span style="color:#24292e;--shiki-dark:#E1E4E8">($_GET[</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;hash&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">], $hash) </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#d73a49;--shiki-dark:#F97583"> ?</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;ok&quot;</span><span style="color:#d73a49;--shiki-dark:#F97583"> :</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;invalid&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">?&gt;</span></span></code></pre><p>The resulting URL would then be: <code>https://holtwick.de/experiments/id.php?text=@holtwick%20I%20don%27t%20like%20fake%20news!&amp;hash=55bc8fe5</code></p><h3 id="try-it-out%3A" tabindex="-1"><a class="header-anchor" href="#try-it-out%3A">Try it out:</a></h3><p>Ok, that was a relatively primitive implementation to illustrate the idea. For a serious application, other techniques would certainly be used, such as a public key procedure or blockchains. Of course, there are already services that do something similar, as this <a href="https://www.elektronische-vertrauensdienste.de/EVD/DE/Uebersicht_eVD/start.html" rel="noopener" target="_blank" class="external">overview from the Federal Network Agency</a> shows.</p><div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>Warning</p><p>I’m no cryptography expert and I’m sure I’ve overlooked some attack vectors, but I still think that there are technical possibilities that can at least make it more difficult to claim things in a false name.</p></div><h2 id="sources" tabindex="-1"><a class="header-anchor" href="#sources">Source</a></h2><ul><li><a href="https://www.dw.com/en/how-russian-fake-news-paints-the-germans/a-64394917" rel="noopener" target="_blank" class="external">DW</a> (engl.)</li><li><a href="https://www.theguardian.com/world/2024/jan/26/germany-unearths-pro-russia-disinformation-campaign-on-x" rel="noopener" target="_blank" class="external">The Guardian</a> (engl.)</li><li><a href="https://www.spiegel.de/politik/deutschland/desinformation-aus-russland-auswaertiges-amt-deckt-pro-russische-kampagne-auf-a-765bb30e-8f76-4606-b7ab-8fb9287a6948" rel="noopener" target="_blank" class="external">Spiegel</a> (dt.)</li><li><a href="https://www.tagesschau.de/inland/desinformation-kampagne-russland-100.html" rel="noopener" target="_blank" class="external">Tagesschau</a> (dt.)</li></ul></div><p><em>Published on February 7, 2024</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[An IT strategy for Europe 🇪🇺]]></title>
            <link>https://holtwick.de/blog/it-strategy-for-europe</link>
            <guid isPermaLink="false">https://holtwick.de/blog/it-strategy-for-europe</guid>
            <pubDate>Wed, 26 Aug 2020 06:00:00 GMT</pubDate>
            <description><![CDATA[Europe needs a strategy and political will to change this trend. But the potential and the resources are there to make Europe more independent.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><h2 id="starting-point" tabindex="-1"><a class="header-anchor" href="#starting-point">Starting point</a></h2><p>The <a href="https://moneyinc.com/richest-companies-in-the-world-in-2019/" rel="noopener" target="_blank" class="external">most valuable</a> and most successful companies are IT companies: Apple, Google, Amazon and Microsoft. All based in the USA. China, another huge market, is the production site of most hardware, but also the seat of companies that successfully take the place of US companies: Alibaba, Tencent and <a href="https://en.wikipedia.org/wiki/List_of_largest_Internet_companies" rel="noopener" target="_blank" class="external">more</a>. Emerging markets also have their own market, where online business takes place. <a href="https://www.kaiostech.com/company/our-story/" rel="noopener" target="_blank" class="external">KaiOS</a> is an example of an innovative model that is hardly known in the first world.</p><p>Where does Europe rank? There are no significant online platforms. Businesses and administrations make themselves dependent on software from the USA and hardware from China. Creative entrepreneurs and talented developers are migrating to the USA. In the long run, Europe will become less relevant as an innovator and remain an importer of technology. This is disastrous.</p><p>Europe needs a strategy and the political will to change this trend. But the potential and the resources are there to make Europe more independent in a changing global world. In the following I would like to present my personal thoughts on such a strategy.</p><h2 id="data" tabindex="-1"><a class="header-anchor" href="#data">Data</a></h2><p>Europe should under no circumstances try to copy the USA or China. The existing successful solutions are basically <strong>centralized solutions</strong>. This means that <strong>services</strong> are offered, where the <strong>data</strong> of the customers are in the access of the operating companies. No matter how well the <em>privacy</em> promises on the websites can be read, de facto there is no protection of the data from the companies or the authorities of the country where the company is located - usually the USA or China.</p><p>Data is the commodity of the information age. The first difference that Europe should make in its strategy is the absolute protection of this data. Users should have full access and control over their data so as not to become dependent on individual companies. This can be achieved through <strong>decentralized solutions</strong> and <strong>strong encryption</strong>.</p><h2 id="platform" tabindex="-1"><a class="header-anchor" href="#platform">Platform</a></h2><p>It must be possible to display and edit data. Computers, tablets, smartphones and, over time, other devices will be used for this purpose. It is important which operating system is used, because this is the foundation on which all solutions are built. Microsoft, Google and Apple have a [monopoly] (<a href="https://de.statista.com/themen/783/betriebssysteme/" rel="noopener" target="_blank" class="external">https://de.statista.com/themen/783/betriebssysteme/</a>), all of them based in the USA. Europe should focus on its own <strong>free platform</strong>, with <a href="https://de.wikipedia.org/wiki/Linux" rel="noopener" target="_blank" class="external">Linux</a> and <a href="https://de.wikipedia.org/wiki/Android" rel="noopener" target="_blank" class="external">Android</a> as a first starting point.</p><p>But besides the operating system, another <em>secret</em> operating system has emerged: the web browser. Originally a European invention, it has revolutionized the access to information on the Internet. In recent years, complexity has increased and most computer services can now be accessed using web technologies. The programming is easy to learn and the knowledge of it is widely spread. Now with <a href="https://webassembly.org/" rel="noopener" target="_blank" class="external">WebAssembly </a>there are no real performance problems anymore.</p><p>Europe should take advantage of the fact that there is the proven web platform. However, after the weakening of Firefox only one browser engine is relevant and this is controlled by <a href="https://de.statista.com/statistik/daten/studie/158095/umfrage/meistgenutzte-browser-im-internet-weltweit/" rel="noopener" target="_blank" class="external">Google and Apple</a>. Europe should develop its own open engine in the short term, for which <a href="https://servo.org/" rel="noopener" target="_blank" class="external">Servo</a> is the obvious choice. In the long term the operating system as a whole could be replaced by a web engine, following the example of <a href="https://de.wikipedia.org/wiki/Google_Chrome_OS" rel="noopener" target="_blank" class="external">Google Chrome OS</a>.</p><h2 id="hardware" tabindex="-1"><a class="header-anchor" href="#hardware">Hardware</a></h2><p>It is also important to be able to trust the devices themselves. In many chips there are <a href="https://www.zdnet.com/article/minix-intels-hidden-in-chip-operating-system/" rel="noopener" target="_blank" class="external">own small operating systems</a>, which take over important functions and normal developers do not have access to the internals. It is important to become more independent in development and production, preferably with an open approach. Why shouldn’t several hardware manufacturers produce the same type of chips? <a href="https://de.wikipedia.org/wiki/ARM-Architektur" rel="noopener" target="_blank" class="external">ARM</a> is a successful model in this area. Unfortunately I don’t know enough about this topic to be able to go into more detail. But I think it is important to create a basis that can be trusted and that is not monopolized.</p><h2 id="software" tabindex="-1"><a class="header-anchor" href="#software">Software</a></h2><p>The public sector, i.e. administrations and authorities, should be obliged to use and develop open source software. Any solution developed with government funds must be freely and openly accessible and must be able to be operated without limits and free of license costs. The money currently spent on licenses for Windows in Europe alone should be enough to promote software development. Contracts for development and maintenance should in turn be awarded in Europe in order to stimulate and build up expertise and markets here.</p><h2 id="education" tabindex="-1"><a class="header-anchor" href="#education">Education</a></h2><p>Both education in dealing with IT and the transfer of knowledge through IT should be strengthened. Especially in times of Corona, the deficits have clearly become apparent. The needs for an educational infrastructure are certainly comparable throughout Europe, but there is no common effort to find a solution as far as I know.</p><p>In the area of learning materials there are now various solutions, for example in Germany <a href="https://anton.app/en_us/" rel="noopener" target="_blank" class="external">Anton App</a>, <a href="https://presse.funk.net/format/musstewissen/" rel="noopener" target="_blank" class="external">Musste Wissen</a> or <a href="https://simpleclub.com/" rel="noopener" target="_blank" class="external">SimpleClub</a>. But also internationally like <a href="https://de.khanacademy.org/" rel="noopener" target="_blank" class="external">Khan Academy</a>. Nevertheless, each teacher prepares his lessons individually and experiences are not effectively shared. A better promotion of such content could improve both traditional and virtual teaching and thus position Europe better, as the level of education increases and becomes more comparable.</p><h2 id="innovation" tabindex="-1"><a class="header-anchor" href="#innovation">Innovation</a></h2><p>European citizens have repeatedly shown that they are innovative and recognize future issues. <a href="https://de.wikipedia.org/wiki/Solarindustrie#Krise_seit_2012" rel="noopener" target="_blank" class="external">Renewable Energy</a> is a good example of this, but at the same time it is also a bad example of the short breath in political support. In the field of mobility, Europe was at the forefront with fossil fuels by optimizing cars, but then missed out on developments, even though many innovations for the engines of the future also [took place] in Europe [<a href="https://de.wikipedia.org/wiki/Transrapid" rel="noopener" target="_blank" class="external">https://de.wikipedia.org/wiki/Transrapid</a>].</p><p>The great challenges of our time are also opportunities that must be taken advantage of. The potential is there, now we need the courage and passion to go our own way and to do so as quickly as possible.</p><hr><p>Links to similar topics:</p><ul><li><a href="https://www.heise.de/meinung/Kommentar-Digitale-Souveraenitaet-zum-Schnaeppchenpreis-von-Europa-und-Mozilla-4874038.html?wt_mc=rss.red.ho.ho.atom.beitrag.beitrag" rel="noopener" target="_blank" class="external">Kommentar: Digitale Souveränität zum Schnäppchenpreis – von Europa und Mozilla</a> by Felix von Leitner, Heise.</li><li><a href="https://macwright.com/2020/08/22/clean-starts-for-the-web.html" rel="noopener" target="_blank" class="external">A clean start for the web</a> by Tom MacWright</li></ul></div><p><em>Published on August 26, 2020</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Using JSX in Typescript without React]]></title>
            <link>https://holtwick.de/blog/jsx-without-react</link>
            <guid isPermaLink="false">https://holtwick.de/blog/jsx-without-react</guid>
            <pubDate>Thu, 11 Aug 2016 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p><strong>JSX</strong> became a defacto standard for mixing in XML markup in JS or TypeScript source files. With a little trick it can be used for quickly creating DOM elements or for templating.</p><p>The following snippet can be dropped into a JSX file and will then make a HTMLElement of the XML markup:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">var</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> React </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">    createElement</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#d73a49;--shiki-dark:#F97583">function</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#e36209;--shiki-dark:#FFAB70">tag</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#e36209;--shiki-dark:#FFAB70">attrs</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#e36209;--shiki-dark:#FFAB70">children</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        var</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> e </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(tag);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">        // Add attributes</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        for</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">var</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> name </span><span style="color:#d73a49;--shiki-dark:#F97583">in</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> attrs) {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">            if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (name </span><span style="color:#d73a49;--shiki-dark:#F97583">&amp;&amp;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> attrs.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">hasOwnProperty</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(name)) {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">                var</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> v </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> attrs[name];</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">                if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (v </span><span style="color:#d73a49;--shiki-dark:#F97583">===</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> true</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                    e.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">setAttribute</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(name, name);</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                } </span><span style="color:#d73a49;--shiki-dark:#F97583">else</span><span style="color:#d73a49;--shiki-dark:#F97583"> if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (v </span><span style="color:#d73a49;--shiki-dark:#F97583">!==</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> false</span><span style="color:#d73a49;--shiki-dark:#F97583"> &amp;&amp;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> v </span><span style="color:#d73a49;--shiki-dark:#F97583">!=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> null</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                    e.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">setAttribute</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(name, v.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">toString</span><span style="color:#24292e;--shiki-dark:#E1E4E8">());</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">        // Append children</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        for</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">var</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> i </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 2</span><span style="color:#24292e;--shiki-dark:#E1E4E8">; i </span><span style="color:#d73a49;--shiki-dark:#F97583">&lt;</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> arguments</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.</span><span style="color:#005cc5;--shiki-dark:#79B8FF">length</span><span style="color:#24292e;--shiki-dark:#E1E4E8">; i</span><span style="color:#d73a49;--shiki-dark:#F97583">++</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">            var</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> child </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> arguments</span><span style="color:#24292e;--shiki-dark:#E1E4E8">[i];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">            e.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">appendChild</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                child.nodeType </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> null</span><span style="color:#d73a49;--shiki-dark:#F97583"> ?</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                doc.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">createTextNode</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(child.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">toString</span><span style="color:#24292e;--shiki-dark:#E1E4E8">()) </span><span style="color:#d73a49;--shiki-dark:#F97583">:</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                child);</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> e;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>The same approach could be used to directly generate a HTML string for templating.</p><p>It is then easy to do something like:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-typescript"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">document.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">querySelector</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;#menu&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">appendChild</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    &lt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">li class</span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8">{active ? </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;active&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> : </span><span style="color:#005cc5;--shiki-dark:#79B8FF">false</span><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span><span style="color:#d73a49;--shiki-dark:#F97583">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        {title}</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    &lt;/</span><span style="color:#24292e;--shiki-dark:#E1E4E8">li</span><span style="color:#d73a49;--shiki-dark:#F97583">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">);</span></span></code></pre></div><p><em>Published on August 11, 2016</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[KeyPath Trick]]></title>
            <link>https://holtwick.de/blog/keypath-refatoring</link>
            <guid isPermaLink="false">https://holtwick.de/blog/keypath-refatoring</guid>
            <pubDate>Wed, 20 Jun 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>I’m still on Objective-C but I like the idea of swift having the <code>#keyPath(property name)</code> <a href="https://docs.swift.org/swift-book/ReferenceManual/Expressions.html#ID549" rel="noopener" target="_blank" class="external">String-Expression</a>. Less can go wrong and autocompletion and refactoring also works.</p><p>In good old Objective-C we only have <code>@selector</code> doing similar things, but nothing that works with key paths. So here is a little trick that I use to fix this problem in my code. First I define this macro:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">#define</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> keyPath</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#e36209;--shiki-dark:#FFAB70">k</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#005cc5;--shiki-dark:#79B8FF">YES</span><span style="color:#d73a49;--shiki-dark:#F97583"> ?</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> @#k </span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (k </span><span style="color:#d73a49;--shiki-dark:#F97583">?</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> @&quot;&quot;</span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> nil</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span></code></pre><p>Actually the <code>@#k</code> part is already doing the job, but refactoring and autocompletion does not work, if the argument isn’t also treated as an expression, therefore I added the little <code>?:</code> dance.</p><p>Now you can use it like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">[</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> valueForKeyPath:keyPath</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.sample.greeting)];</span></span></code></pre><p>It is important also to add the <code>self</code> in front. If <code>self</code> is not appropriate in your situation because you are using the key path from another origin, then use this slightly extended version:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">#define</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> keyPathFromObject</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#e36209;--shiki-dark:#FFAB70">o, k</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#005cc5;--shiki-dark:#79B8FF">YES</span><span style="color:#d73a49;--shiki-dark:#F97583"> ?</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> @#k </span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> ((o </span><span style="color:#d73a49;--shiki-dark:#F97583">?:</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> o.k) </span><span style="color:#d73a49;--shiki-dark:#F97583">?</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> @&quot;&quot;</span><span style="color:#d73a49;--shiki-dark:#F97583"> :</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> nil</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span></code></pre><p>Once in place it will not be missed on refactoring any more:</p><p class="img-wrapper"><img src="/assets/gf6bky8y1tk4lkr.png" alt="image-20180620111144643" width="578" height="688" loading="lazy"></p><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Source Code</p><p>Demo code is available as <a href="https://gist.github.com/holtwick/27321fa97231fcba3ea3fb8621517865" rel="noopener" target="_blank" class="external">Gist</a>. I’m not the first one having this idea, <a href="http://nshipster.com/key-value-observing/#better-key-paths" rel="noopener" target="_blank" class="external">here</a> and <a href="https://gist.github.com/quinntaylor/d3f124dd4f812d6cc6de" rel="noopener" target="_blank" class="external">here</a> and <a href="https://github.com/Tricertops/Valid-KeyPath" rel="noopener" target="_blank" class="external">here</a> are examples of prior works on the topic.</p></div><h4 id="update-2018-06-20%3A" tabindex="-1"><a class="header-anchor" href="#update-2018-06-20%3A">Update 2018-06-20:</a></h4><p>The macro was always evaluating the expression. I modified it to never reach the expression part of the code.</p><h4 id="update-2018-06-21%3A" tabindex="-1"><a class="header-anchor" href="#update-2018-06-21%3A">Update 2018-06-21:</a></h4><p>The previous implementation did only work for <code>NSString</code> properties, fixed that by adding another <code>?:</code> round.</p><h4 id="update-2018-06-26%3A" tabindex="-1"><a class="header-anchor" href="#update-2018-06-26%3A">Update 2018-06-26:</a></h4><p>Haha, I have been blind and reinvented the wheel: <a href="https://github.com/jspahrsummers/libextobjc/blob/master/extobjc/EXTKeyPathCoding.h#L38" rel="noopener" target="_blank" class="external">This implementation</a> is really nice. They even figured out how to prepend the <code>@</code> to get <code>@keypath()</code>. I’m happy to see that the implementation is similar although it is full of extra super magic macro power. That said maybe my implementation is still good to take a look at due to it’s compactness ;)</p></div><p><em>Published on June 20, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[LanguageTool]]></title>
            <link>https://holtwick.de/blog/languagetool</link>
            <guid isPermaLink="false">https://holtwick.de/blog/languagetool</guid>
            <pubDate>Thu, 15 Apr 2021 06:00:00 GMT</pubDate>
            <description><![CDATA[Install LanguageTool yourself for better privacy protection]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p><a href="https://languagetool.org/" rel="noopener" target="_blank" class="external">LanguageTool</a> is the secret weapon for producing error-free texts in different languages. Personally, I’m at war with many a grammar rule and am glad when the machine warns me before it gets embarrassing 😅</p><p>However, I believe not everything I wrote needs to be sent to a service that is great, but not under my control.</p><p>So here are some tips to set up and use an appropriate server yourself.</p><h2 id="setting-up-your-own-server" tabindex="-1"><a class="header-anchor" href="#setting-up-your-own-server">Setting up your own server</a></h2><p>The easiest way to set up your own server is via Docker. This package offers itself: <a href="https://hub.docker.com/r/erikvl87/languagetool?ref=login" rel="noopener" target="_blank" class="external">erikvl87/languagetool</a>. And this is how it is loaded and started:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">docker</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> pull</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> erikvl87/languagetool</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">docker</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> run</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -d</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> --restart</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> unless-stopped</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -p</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> 8010:8010</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> erikvl87/languagetool</span></span></code></pre><p>Great, now the server can be reached at <code>http://example.com:8010</code>. There is more background information at <a href="https://dev.languagetool.org/http-server" rel="noopener" target="_blank" class="external">https://dev.languagetool.org/http-server</a></p><h2 id="use-in-visual-studio-code" tabindex="-1"><a class="header-anchor" href="#use-in-visual-studio-code">Use in Visual Studio Code</a></h2><p>The extension <a href="https://marketplace.visualstudio.com/items?itemName=davidlday.languagetool-linter" rel="noopener" target="_blank" class="external">LanguageTool Linter</a> provides all necessary functions to work with LanguageTool. Here the server URL can be entered now. The editing of texts and Markdown becomes much more comfortable.</p><p>The following adjustments to the settings have proven useful:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-json"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;languageToolLinter.external.url&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;http://example.com:8010&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;languageToolLinter.hideDiagnosticsOnChange&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">true</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;languageToolLinter.hideRuleIds&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">true</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;languageToolLinter.lintOnChange&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">true</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">  &quot;languageToolLinter.lintOnOpen&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#005cc5;--shiki-dark:#79B8FF">true</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="use-in-firefox" tabindex="-1"><a class="header-anchor" href="#use-in-firefox">Use in Firefox</a></h2><p>There are numerous plugins available for browsers and other apps, see <a href="https://languagetool.org/#plugins" rel="noopener" target="_blank" class="external">https://languagetool.org/#plugins</a>. In the Firefox extension, you can enter your own server under “Experimental settings (only for advanced users)”. The URL must be added to the version path, in our example: <code>http://example.com:8010/v2/</code>.</p></div><p><em>Published on April 15, 2021</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Resilient Sync for Local First]]></title>
            <link>https://holtwick.de/blog/localfirst-resilient-sync</link>
            <guid isPermaLink="false">https://holtwick.de/blog/localfirst-resilient-sync</guid>
            <pubDate>Mon, 24 Jun 2024 06:00:00 GMT</pubDate>
            <description><![CDATA[Resilient sync for local-first apps by using existing technologies such as file systems]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><div class="markdown-alert markdown-alert-success"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><path d="M20 6 9 17l-5-5"></path></svg>Update 2025-02-11</p><p>With <a href="https://receipts-app.com?ref=holtwick" rel="noopener" target="_blank" class="external">Receipts Space</a>, I have now published the first app based on the <a href="https://receipts-app.com/docs?ref=holtwick" rel="noopener" target="_blank" class="external">technology</a> described. It has proven itself and more will follow.</p></div><p>When I started programming in the 80s, the situation was simple: data was written to a file on a local disk. If files needed to be exchanged, a floppy disk was simply inserted into another computer and edited.</p><p>Later, floppy disks and CD drives disappeared and files were either exchanged by e-mail or the data was stored directly on a server. Even Microsoft Word moved to the cloud at some point.</p><p>However, moving data from the local computer to the cloud created new problems. What if the provider closed its service? However, it was not only data loss, but also being trapped by the provider because the data could no longer be retrieved from the service that increasingly became a problem for some and a business model for others.</p><h2 id="local-first" tabindex="-1"><a class="header-anchor" href="#local-first">Local-First</a></h2><p>The <a href="https://localfirstweb.dev/" rel="noopener" target="_blank" class="external">local-first movement</a> wants to bring the data back to the user while retaining the advantages of the internet. A robust and widely accepted solution has been found for the simultaneous processing of data with <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type" rel="noopener" target="_blank" class="external">CRDT</a>, but not yet for the exchange of data. This is because it is not (yet) possible to do completely without services on a server. Even with peer-to-peer, where the devices mainly communicate directly with each other, the connection must be mediated by servers at the beginning.</p><p>In an ideal local-first world, the following criteria should be met with regard to the exchange of data:</p><ol><li>data is processed and stored locally (offline).</li><li>the sync can be interrupted for a longer period of time, but data can be processed locally during this time and still be identical everywhere after a new sync (CRDT).</li><li>data should not be visible on the move (E2EE).</li></ol><p>And I would also like to add:</p><ol start="4"><li>the whole thing should also work with the technology of the 80s.</li></ol><p>Why? For reasons of resilience, because there are many reasons why things could go offline.</p><h2 id="resilient-sync" tabindex="-1"><a class="header-anchor" href="#resilient-sync">Resilient Sync</a></h2><p>For this reason, I propose a data exchange format that works both in the file system and on the Internet. It should be so simple that the implementation can work on any underlying data carrier or service.</p><h3 id="log-of-changes" tabindex="-1"><a class="header-anchor" href="#log-of-changes">Log of changes</a></h3><p>To start with, each client writes a kind of simple, continuous log with the changes. We call the position of the changes <code>index</code> and it starts at <code>0</code> and continues in whole numbers: <code>0, 1, 2, 3, ...</code>. Usually these changes are CRDT compliant, but this is not significant for the protocol. This data can also be stored in encrypted form. Let’s call this <code>data</code>.</p><p>Each client is given a unique identifier, usually a unique ID (UUID), we call it <code>clientId</code>. The change logs are stored linked to the <code>clientId</code>. We will call a collection of such logs <code>workspace</code>.</p><p>We can enrich further data, such as a <code>timestamp</code>, which can be useful if we want to process the data changes historically or implement a kind of endless “undo”.</p><p>It is also conceivable that each new entry contains a hash of the previous entry in order to ensure data consistency in the style of a blockchain. Further refinements in the direction of the Merkle tree are also conceivable. However, a hash on the content of the current entry can also contribute to data security.</p><h3 id="assets%2C-blobs%2C-the-big-binary-chunks" tabindex="-1"><a class="header-anchor" href="#assets%2C-blobs%2C-the-big-binary-chunks">Assets, blobs, the big binary chunks</a></h3><p>The reality is that there is also larger data that rarely changes: Images, videos, audios, i.e. files. These are not part of the content changes and would quickly bring data synchronization to a standstill. In addition, they are not always needed immediately and could also be loaded “on demand”. I therefore suggest treating these files separately from the changes. Let’s call them <code>asset</code>.</p><p>But here, too, the data should be stored according to <code>clientId</code> and in ascending order with <code>index</code>. The data sets can thus refer to an asset, e.g. with a data entry in the form of a URL that contains all the necessary information:<br><code>asset:///&lt;clientId&gt;/&lt;index&gt;/&lt;filename&gt;?size=&lt;sizeInBytes&gt;&amp;type=&lt;mimeType&gt;&amp;hash=&lt;checksumOfContents&gt;</code>. An example could look like this <code>asset:///abc/1/test.txt?size=100&amp;type=text%2Fplain&amp;hash=1a2b3c</code>.</p><h3 id="benefits" tabindex="-1"><a class="header-anchor" href="#benefits">Benefits</a></h3><p>The key advantage of this method is that we always know where the next data will appear. Because if a client has just written the index ‘123’, the next one will be ‘124’.</p><p>Why is this important? For the following reasons:</p><ol><li>we are not dependent on being notified of new data (“push”), but can also ask for it ourselves (“pull”).</li><li>we can load the individual entries even without knowing anything about their content. A sync can even take place without an intermediary having to “understand” the data.</li><li>we notice immediately if data is missing and can request it again.</li><li>the data can be replicated as often as required, there does not have to be just one sync source. It therefore makes sense to use it as a backup.</li><li>clients that do not have a direct connection to each other can use “stupid copying processes”.</li><li>data exchange can be easily and comprehensibly documented, which can be important when recognizing legally compliant stored data for tax and compliance purposes.</li><li>there are no conflicts when storing the data because it is only updated but never deleted (“append only”).</li></ol><h3 id="databases" tabindex="-1"><a class="header-anchor" href="#databases">Databases</a></h3><p>Let’s start with the implementation as a database. As described, a table with the following fields would be sufficient:</p><ul><li><code>index</code>: Integer</li><li><code>clientId</code>: String or integer</li><li><code>data</code>: String or binary</li><li><code>timestamp</code>: Integer (optional)</li><li><code>prevHash</code>: String or integer (optional)</li></ul><p>This would look similar for the assets.</p><p>Database can refer to both the local storage of the client, e.g. in the IndexedDB, and the storage on a synchronization server.</p><h3 id="filesystems" tabindex="-1"><a class="header-anchor" href="#filesystems">Filesystems</a></h3><p>The data is stored in a directory in a file system. Metadata, such as the <code>clientId</code> of the creator and details such as the encryption used, are stored in a JSON file called <code>index.json</code>.<br>Furthermore, there is a directory for each client that is named after the <code>clientId</code>.</p><p>These in turn each contain two directories:</p><ol><li><code>changes</code></li><li><code>assets</code></li></ol><p>The changes are recorded in <code>changes</code> and numbered consecutively. The same is done in <code>assets</code> with the binary data described.</p><p>Modern file systems have no significant restrictions on the number of files per directory, but it can’t hurt to limit the number anyway. The following algorithm in TypeScript ensures an even distribution:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-ts"><span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">/**</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"> * Distribute file, named by natural numbers, in a way that each folder only</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"> * contains `maxEntriesPerFolder` subfolders or files. Returns a list of</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"> * names, where the last one is the file name, all others are folder names.</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"> * </span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"> * Example: `distributedFilePath(1003)` results in `[&apos;2&apos;, &apos;1&apos;, &apos;3&apos;]` which </span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"> * could be translated to the file path `2/1/3.json`.</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D"> */</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">export</span><span style="color:#d73a49;--shiki-dark:#F97583"> function</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> distributedFilePath</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#e36209;--shiki-dark:#FFAB70">index</span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> number</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#e36209;--shiki-dark:#FFAB70">maxEntriesPerFolder</span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> number</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 1000</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292e;--shiki-dark:#E1E4E8">[] {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (index </span><span style="color:#d73a49;--shiki-dark:#F97583">&lt;</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    throw</span><span style="color:#d73a49;--shiki-dark:#F97583"> new</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> Error</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;Only numbers &gt;= 0 supported&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> names</span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> string</span><span style="color:#24292e;--shiki-dark:#E1E4E8">[] </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  do</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    names.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">unshift</span><span style="color:#24292e;--shiki-dark:#E1E4E8">((index </span><span style="color:#d73a49;--shiki-dark:#F97583">%</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> maxEntriesPerFolder).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">toString</span><span style="color:#24292e;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    index </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> Math.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">floor</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(index </span><span style="color:#d73a49;--shiki-dark:#F97583">/</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> maxEntriesPerFolder)</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  } </span><span style="color:#d73a49;--shiki-dark:#F97583">while</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (index </span><span style="color:#d73a49;--shiki-dark:#F97583">&gt;</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  names.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">unshift</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(names.</span><span style="color:#005cc5;--shiki-dark:#79B8FF">length</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">toString</span><span style="color:#24292e;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> names</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Source: <a href="https://github.com/holtwick/zeed/blob/master/src/common/data/distributed.ts" rel="noopener" target="_blank" class="external">zeed Framework</a></p><h3 id="online-services-and-peer-to-peer" tabindex="-1"><a class="header-anchor" href="#online-services-and-peer-to-peer">Online Services and Peer-To-Peer</a></h3><p>A suitable mix of the database or file system approach can be selected for online services, depending on which is better suited to the selected service. For Dropbox or WebDAV, for example, this would be the file system approach. For Apple CloudKit, the database approach.</p><p>But a simple service of your own is also conceivable, with a REST, WebSocket or other useful interfaces.</p><p>There is also no reason not to synchronize the data via peer-to-peer (P2P) or another local communication channel. After all, the data is identical and can therefore be synchronized with other clients via the fastest route. Redundancy is therefore an advantage and does not make things any more complicated. Theoretically, the sequence and multiple application of changes is also unproblematic with CRDT.</p><h2 id="refinements" tabindex="-1"><a class="header-anchor" href="#refinements">Refinements</a></h2><p>There is still potential for improvement in some areas:</p><ul><li>Control of data size per log entry. Collecting several changes for larger packages or splitting a change into several packages if the scope becomes too large.</li><li>Compression or summarization of data.</li><li>Announcement of new clients, e.g. through special log entries. Evaluation using cryptographic methods.</li><li>By adding a logical clock, such as the Lamport clock, entries can be sorted logically, thereby improving the chronology of an entry.</li><li>Writing the data in a single file per client for reasons of resource optimization.</li><li>Through the clever use of cryptographic means, it may even be possible to implement rights management (<a href="https://schmiste.github.io/srds06.pdf" rel="noopener" target="_blank" class="external">Cryptree</a>).</li></ul><h2 id="outlook" tabindex="-1"><a class="header-anchor" href="#outlook">Outlook</a></h2><p>I have been using this technique in my apps for several years, for example in the now completed project <a href="https://onepile.apperdeck.com/en/help/internal-file-format" rel="noopener" target="_blank" class="external">Onepile</a>. New projects using this approach will be published soon.</p><p>Simplicity and flexibility seem to me to be the biggest advantages of this approach. As a result, it should also be future-proof and be able to adapt quickly to new technical conditions.</p><p>The following diagram is an example of an ecosystem for a web app:</p><p class="img-wrapper"><img src="/assets/ued9nzrc325q2v.png" alt="Image" width="2976" height="2536" loading="lazy"></p><h2 id="related" tabindex="-1"><a class="header-anchor" href="#related">Related</a></h2><ul><li><strong>Discussion of the article on <a href="https://news.ycombinator.com/item?id=40772955" rel="noopener" target="_blank" class="external">HackerNews</a></strong></li><li><a href="https://www.youtube.com/watch?v=NMq0vncHJvU&amp;t=2s" rel="noopener" target="_blank" class="external">Martin Kleppmann’s Talk on the Local-First Conference 2024 in Berlin</a></li><li><a href="https://elk.zone/mastodon.social/@martin@nondeterministic.computer/112639441984059657" rel="noopener" target="_blank" class="external">Mastodon comment of Martin Kleppmann</a></li><li>List of similar approaches:<ul><li><a href="https://github.com/MichaelMure/git-bug/blob/master/doc/model.md" rel="noopener" target="_blank" class="external">git-bug</a></li><li><a href="https://remotestorage.io" rel="noopener" target="_blank" class="external">remotestorage.io</a></li><li><a href="https://tonsky.me/blog/crdt-filesync/" rel="noopener" target="_blank" class="external">Local, first, forever</a></li><li><a href="https://jack-vanlightly.com/analyses/2024/4/29/understanding-delta-lakes-consistency-model" rel="noopener" target="_blank" class="external">Understanding Delta Lake’s consistency model</a></li><li><a href="http://archagon.net/blog/2018/03/24/data-laced-with-history/" rel="noopener" target="_blank" class="external">Data Laced with History: Causal Trees &amp; Operational CRDTs</a></li></ul></li></ul></div><p><em>Published on June 24, 2024</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Code Quality - Logging]]></title>
            <link>https://holtwick.de/blog/logging</link>
            <guid isPermaLink="false">https://holtwick.de/blog/logging</guid>
            <pubDate>Thu, 25 Jan 2018 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>In complex projects, the time usually comes when it becomes indispensable for quality assurance to take further measures. I would like to discuss some of these aspects in a loose series:</p><ul><li>Logging</li><li>Crash Reporting</li><li><a href="replies">Support and Feedback</a></li></ul><p>In this article I’ll take a look at <strong>Logging</strong>.</p><h2 id="level" tabindex="-1"><a class="header-anchor" href="#level">Level</a></h2><p>The simplest form of a log entry is a <strong>message</strong>. But it soon becomes clear that this alone will not be enough and a little more <strong>context</strong> is needed. Usually, the <strong>Timestamp</strong> and <strong>Level</strong> are added quickly. This can be implemented in this way, for example:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>log.error()</span></span>
<span class="line"><span>log.warn()</span></span>
<span class="line"><span>log.info()</span></span>
<span class="line"><span>log.debug()</span></span></code></pre><p>In environments like the browser these can be easily filtered, but in a log file it may look different. That is why I like to resort to a trick that I learned from my colleagues when working on a major project, namely to highlight these levels:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>E|***</span></span>
<span class="line"><span>W|**</span></span>
<span class="line"><span>I|*</span></span>
<span class="line"><span>D|</span></span></code></pre><p>Okay, that looks nice and clear, but what’s better?</p><p>It is the possibility to filter, because filtering to <code>|*</code> will show all messages including and above <code>I|*</code>, thus also warnings and errors. <code>|**</code> and <code>|***</code> filter accordingly on higher levels. This works very well on macOS e. g. in Xcode or the Console App.</p><h2 id="time" tabindex="-1"><a class="header-anchor" href="#time">Time</a></h2><p>A log entry usually starts with a time stamp. This makes sense for a program that runs in full productivity at the customer or on the server. During development, however, this information is rather superfluous and also shortens the visible area of the message. If time is of any interest at all, then it’s more like <strong>how much time has passed</strong> until this or that happens. Therefore, it may make sense to specify the time since the start of the program:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>I|*       0ms App launch</span></span>
<span class="line"><span>D|      123ms Open file abc.xyz</span></span>
<span class="line"><span>E|***   412ms Could not open file abc.xyz because: Does not exist</span></span></code></pre><h2 id="colors-and-symbols" tabindex="-1"><a class="header-anchor" href="#colors-and-symbols">Colors and Symbols</a></h2><p>Another aspect that is helpful in the quick perception of information is the color of the entry. The browser does this for us by displaying the errors in red. But in some environments, such as Xcode, the use of colors is not possible or difficult to achieve, emojis can be used as an alternative:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>🔷 I|*       0ms App launch</span></span>
<span class="line"><span>◽️ D|      123ms Open file abc.xyz</span></span>
<span class="line"><span>❌ E|***   412ms Could not open file abc.xyz because: Does not exist</span></span></code></pre><p>The red symbol spotlights immediately and the error message is located so quickly.</p><h2 id="message-origin" tabindex="-1"><a class="header-anchor" href="#message-origin">Message Origin</a></h2><p>Okay, we noticed there was an error, but where did it occur? The most amazing log entry is useless if we can’t locate the cause. For this reason, the file name and the line number should be logged as well. Many IDEs allow you to jump directly to a location through a specific formatting. In Xcode e. g. by <code>CMD + SHIFT + O</code> and then specify the file and the line number separated by a colon. This could look like this in the log:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>🔷 I|*       0ms &lt;main.c:33&gt; App launch</span></span>
<span class="line"><span>◽️ D|      123ms &lt;AppDelegate.m:54&gt; Open file abc.xyz</span></span>
<span class="line"><span>❌ E|***   412ms &lt;AppDelegate.m:62&gt; Could not open file abc.xyz because: Does not exist</span></span></code></pre><p><strong>But!</strong> This is no information that should be used in a productive environment.</p><h2 id="concurrency" tabindex="-1"><a class="header-anchor" href="#concurrency">Concurrency</a></h2><p>And yet another feature is important in modern programs, namely whether the message comes from an asynchronous code block or the main thread. A visualization with Emojis can be helpful here as well, like here with a rocket:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-text"><span class="line"><span>◽️ 🔷 I|*       0ms &lt;main.c:33&gt; App launch</span></span>
<span class="line"><span>🚀 ❌ E|***   412ms &lt;MyCache.m:12&gt; Expected files were missing</span></span></code></pre><h2 id="conclusion" tabindex="-1"><a class="header-anchor" href="#conclusion">Conclusion</a></h2><p>Even such an everyday topic as logging can still offer room for optimizations and thus save time in some places, because relevant information can be collected quickly and it is easier to locate the origin of the problem.</p></div><p><em>Published on January 25, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[PDFify 4.0]]></title>
            <link>https://holtwick.de/blog/pdfify4</link>
            <guid isPermaLink="false">https://holtwick.de/blog/pdfify4</guid>
            <pubDate>Thu, 12 Sep 2024 06:00:00 GMT</pubDate>
            <description><![CDATA[Upgrade to PDFify 4.0 for PDF/A support, batch conversions, faster web display, and standardized page sizes. Try it now!]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p><a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a> can now be updated to the latest version 4.0. Many <a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a> users are looking forward to this major version update with improvements and new functions.</p><h2 id="pdf%2Fa-support" tabindex="-1"><a class="header-anchor" href="#pdf%2Fa-support">PDF/A Support</a></h2><p>Many <a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a> users keep electronic documents that they need to archive for the long term.</p><p><a href="https://en.wikipedia.org/wiki/PDF/A" rel="noopener" target="_blank" class="external">PDF/A</a> - Portable Document Format Archivable - enables standardized long-term archiving of documents. As of macOS 11, <a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a> now offers to save the document as PDF/A-2u by default in the settings.</p><p class="img-wrapper"><img src="/assets/dduxhx5g18q6yfe.png" alt="Image" width="592" height="537" loading="lazy"></p><p>The “classic” PDF, which supports the embedding of links, graphics, audio and video files as well as optional encryption, is of course retained in <a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a>.</p><h2 id="finder-extensions-%2F-quick-actions" tabindex="-1"><a class="header-anchor" href="#finder-extensions-%2F-quick-actions">Finder Extensions / Quick Actions</a></h2><p>To quickly convert several existing PDFs “in batch” to PDF/A, the quick actions in the Finder Extension can be supplemented with “Convert to PDF/A”.</p><p>There is also a new Finder extension for converting documents into a PDF with PDF/A format. However, the existing extensions also generate PDF/A if the option has been set in the settings.</p><div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lightbulb"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path><path d="M9 18h6"></path><path d="M10 22h4"></path></svg>Other file formats</p><p>Not only PDF can be optimized using the Finder Extensions, but also documents in various formats can be converted directly to PDF:</p><ul><li>Image files and scans such as JPG, PNG, TIF or GIF</li><li>HTML files</li><li>Emails</li></ul></div><h2 id="faster-display-of-pdfs-on-the-web" tabindex="-1"><a class="header-anchor" href="#faster-display-of-pdfs-on-the-web">Faster display of PDFs on the web</a></h2><p>Furthermore, you can now also select the linearized PDF, which is optimized for fast display on the Internet. The first page is then already displayed during the download, while the following pages are still being loaded.</p><h2 id="standardized-maximum-page-size" tabindex="-1"><a class="header-anchor" href="#standardized-maximum-page-size">Standardized maximum page size</a></h2><p><a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a> now allows you to limit the maximum page size so that you do not end up with unsightly huge pages. By default, the page size is set to DIN A4 with the update. In the settings, you can adjust the value to the original size or US letter.</p><p class="img-wrapper"><img src="/assets/gq7xpta81829c4t.png" alt="Image" width="592" height="537" loading="lazy"></p><h2 id="sequence-during-import" tabindex="-1"><a class="header-anchor" href="#sequence-during-import">Sequence during import</a></h2><p>Sometimes the individual pages of a document are available as separate image files. In the new version of <a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external">PDFify</a>, these are now reliably imported in the sort order familiar from the Apple Finder. This allows individual scans of pages to be quickly merged into one PDF document.</p><p><a href="https://pdfify.app?ref=holtwick" rel="noopener" target="_blank" class="external"><strong>Try now for free</strong></a></p></div><p><em>Published on September 12, 2024</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Receipts Space 3.0]]></title>
            <link>https://holtwick.de/blog/receipts3</link>
            <guid isPermaLink="false">https://holtwick.de/blog/receipts3</guid>
            <pubDate>Wed, 21 Jan 2026 07:00:00 GMT</pubDate>
            <description><![CDATA[Receipts Space 3.0 brings audit compliance, end-to-end encryption, a new dashboard and many workflow improvements.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Receipts Space 3.0 is here – a major update with lots of new features. Here’s a quick overview.</p><a href="https://video.holtwick.de/w/mAkY1dP2tnPp2FnYNZ1Swk" target="_blank">https://video.holtwick.de/w/mAkY1dP2tnPp2FnYNZ1Swk</a><h2 id="audit-compliance-(gobd)" tabindex="-1"><a class="header-anchor" href="#audit-compliance-(gobd)">Audit Compliance (GoBD)</a></h2><p>For anyone who needs to store receipts in compliance with regulations, there’s now a dedicated mode. Confirmed entries become read-only, and all changes are logged. A history timeline shows at a glance who changed what and when.</p><h2 id="end-to-end-encryption" tabindex="-1"><a class="header-anchor" href="#end-to-end-encryption">End-to-End Encryption</a></h2><p>If you store your library in the cloud or on a USB stick, you can now encrypt it with a password. Synchronization between multiple Macs still works – as long as all devices know the key.</p><h2 id="new-dashboard" tabindex="-1"><a class="header-anchor" href="#new-dashboard">New Dashboard</a></h2><p>The dashboard has been completely redesigned. In addition to income and expenses, it now shows open amounts and trends compared to the previous year. A click on contacts, categories or tags filters the view.</p><p class="img-wrapper"><img src="/assets/fnsa6jeb1470f9w.png" alt="Image" width="3488" height="2264" loading="lazy"></p><h2 id="folder-instead-of-package" tabindex="-1"><a class="header-anchor" href="#folder-instead-of-package">Folder Instead of Package</a></h2><p>The file format has been extended with a new option: The library can now also be saved as a regular folder. This solves synchronization issues with iCloud and other services.</p><p>Of course, there are many more changes. All details are available at <a href="https://receipts-app.com/en/blog/release-3" rel="noopener" target="_blank" class="external">receipts-app.com</a>.</p></div><p><em>Published on January 21, 2026</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Support, but efficiently]]></title>
            <link>https://holtwick.de/blog/replies</link>
            <guid isPermaLink="false">https://holtwick.de/blog/replies</guid>
            <pubDate>Tue, 16 Jan 2018 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Along with selling a product there comes support. Customers have questions and demands about the software and those are often repeating. This is where <a href="https://replies.io?ref=holtwick" rel="noopener" target="_blank" class="external">replies.io</a> comes in, a service provided by my friends <a href="https://www.mediaatelier.com/?ref=holtwick" rel="noopener" target="_blank" class="external">Stefan Fürst</a> and <a href="https://mailplaneapp.com/about.html?ref=holtwick" rel="noopener" target="_blank" class="external">Lars Steiger</a>, originally initiated by <a href="https://mailplaneapp.com/about.html?ref=holtwick" rel="noopener" target="_blank" class="external">Ruben Bakker</a>.</p><h2 id="support-eats-up-your-time" tabindex="-1"><a class="header-anchor" href="#support-eats-up-your-time">Support eats up your time</a></h2><p>Support can be time-consuming and easily eat up some hours of your work day. But at the same time it is usually not that much work, that it is worth outsourcing it, especially when you take into account that most of the answers still need your attention, since the support person might not be able to overview the specific technical details.</p><h2 id="replies.io%2C-the-smart-time-saver" tabindex="-1"><a class="header-anchor" href="#replies.io%2C-the-smart-time-saver"><em>Replies.io</em>, the smart time saver</a></h2><p>Therefore, it makes sense to cut down the time spent on support by automating and simplifying the tasks. This is what replies does:</p><h3 id="channels-and-faq" tabindex="-1"><a class="header-anchor" href="#channels-and-faq">Channels and FAQ</a></h3><p>First of all the users need to be able to get in contact with you. The most obvious way is through email. But what if you could also offer a support form that is already trying to cover the most frequently asked questions by analyzing what the user is typing? Replies.io has those for in form of <strong>web forms</strong> and a <strong>macOS framework</strong>. The later one is magic and provides even more benefit, since you get details about the OS, the installed version. The user can also send log files and screenshots and -recordings with a few clicks.</p><p class="img-wrapper"><img src="/assets/kiezzwqby4ixbm.png" alt="Dialog" width="712" height="761" loading="lazy"></p><h3 id="suggestions" tabindex="-1"><a class="header-anchor" href="#suggestions">Suggestions</a></h3><p>But even if the user did not find the right answer to his question in the FAQ or the suggestions, you still might already have answered a similar question. While typing your answer you usually get a proposal for an answer that fits well. This gets even better over time, with a growing set of answers.</p><h3 id="text-formatting" tabindex="-1"><a class="header-anchor" href="#text-formatting">Text Formatting</a></h3><p>If you are somewhat like me, you will fiddle with the look of the text a bit, but at least you’ll try to get the quoted text separated from your answers nicely, which can be a pain in Apple Mail. With replies.io you click at the last word of the sentence you like to answer and the text box show up waiting for you to type. The best is, it will generate a perfect looking email for you. It also has the personalized salutation and the footer ready for you.</p><h3 id="defer-your-answers" tabindex="-1"><a class="header-anchor" href="#defer-your-answers">Defer your answers</a></h3><p>One essential trick to save time is to slow down the conversation. In a regular email app, the mail is out when you hit “send”. Replies.io instead has some smart presets for deferring the sending. This is useful, because if you answer very quickly, the user is very likely starting a conversation, since he feels like being in a “chat mode”.</p><p class="img-wrapper"><img src="/assets/m3st5ug9wdgwjm.png" alt="Defer" width="257" height="225" loading="lazy"></p><p>Also consider weekends. You might have time to answer a question, but you shouldn’t send it out directly to avoid an unprofessional impression. You also don’t want to get more mails on your weekend from the user. The user will still be super happy to get the answer early Monday morning and you saved your weekend.</p><h3 id="react-to-crashes" tabindex="-1"><a class="header-anchor" href="#react-to-crashes">React to crashes</a></h3><p>And last but not the least replies.io integrates with <a href="http://hockeyapp.net/" rel="noopener" target="_blank" class="external">Hockey App</a>. It is super useful for the user to get a feedback and understand that you are working on a fix. It is also super useful for the developer to start a conversation of what did lead to the crash and finally have the user test the fix of that crash.</p><h2 id="understand-where-you%E2%80%99ve-spent-time" tabindex="-1"><a class="header-anchor" href="#understand-where-you%E2%80%99ve-spent-time">Understand where you’ve spent time</a></h2><p>You can also learn to save time when you understand where you did spend it. Replies.io has reports and counters that help you understand the problems users have. The consequence could be, that you modify your app and avoid questions in the first place. You also learn about the features the users desire the most and invite them to become qualified beta testers for these new features before they go public.</p><p class="img-wrapper"><img src="/assets/blll4j137gbrci.png" alt="Report" width="992" height="560" loading="lazy"></p><p><em>Conclusion:</em> <a href="https://replies.io?ref=holtwick" rel="noopener" target="_blank" class="external">Replies.io</a> helped me to get support under control and save a lot of time making use of the described features. I will not miss it anymore.</p></div><p><em>Published on January 16, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Cosy NSObject]]></title>
            <link>https://holtwick.de/blog/seaobject</link>
            <guid isPermaLink="false">https://holtwick.de/blog/seaobject</guid>
            <pubDate>Tue, 08 May 2018 06:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>Often I’m finding myself in a situation, where a lot of nonsense repeating code needs to be written, like for example when implementing <code>NSCoder</code> for an object or when the syntax does not appeal me like keyed subscription for a dictionary like object.</p><p>Since I’m still coding in Objective-C, I tried to find an easy solution for these requirements:</p><ul><li>An object with <strong>simple properties</strong> like <code>NSString</code> and <code>NSNumber</code></li><li>Allowing <strong>deeper levels</strong> by adding <code>NSArray</code> or <code>NSDictionary</code> properties</li><li>Should <strong>serialize and deserialize to JSON or MessagePack</strong> easily</li><li>Should have <strong>type check and autocompletion</strong> in the editor i.e. instead of <code>obj[@&quot;name&quot;]</code> I would like to write <code>obj.name</code></li><li>It shouldn’t be to strict about everything :)</li></ul><p>I know there is a lot of prior work doing similar magic and also CoreData comes with similar features, but hey, sometimes it is fun to just do it yourself.</p><p>So I started with a base class <code>SeaObject</code> derived from <code>NSObject</code>. This is what the header looks like:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@interface</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> SeaObject</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> : </span><span style="color:#6f42c1;--shiki-dark:#B392F0">NSObject</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> &lt;</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSCopying</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">nonatomic</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#d73a49;--shiki-dark:#F97583">assign</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#d73a49;--shiki-dark:#F97583">BOOL</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> needsSave;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">nonatomic</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#d73a49;--shiki-dark:#F97583">readonly</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSUInteger</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> count;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">nonatomic</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#d73a49;--shiki-dark:#F97583">readonly</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSEnumerator</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">keyEnumerator;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">nonatomic</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#d73a49;--shiki-dark:#F97583">readonly</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSArray</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;NSString *&gt; </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">allKeys;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">nonatomic</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#d73a49;--shiki-dark:#F97583">copy</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSDictionary</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">jsonDictionary;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">instancetype</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">initWithDictionary:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSDictionary</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)dict;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">configure</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">objectForKey:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)aKey;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">setObject:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)anObject </span><span style="color:#6f42c1;--shiki-dark:#B392F0">forKey:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSCopying</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;)aKey;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">removeObjectForKey:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)key;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">setObject:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)obj </span><span style="color:#6f42c1;--shiki-dark:#B392F0">forKeyedSubscript:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)key;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">objectForKeyedSubscript:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)key;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">BOOL</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">writeAsJSON:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)path;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">- (</span><span style="color:#d73a49;--shiki-dark:#F97583">BOOL</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">readAsJSON:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)path;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@end</span></span></code></pre><p>If e.g. I want to build a todo list I can do like this</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@interface</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> TodoItem</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> : </span><span style="color:#6f42c1;--shiki-dark:#B392F0">SeaObject</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">title;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSNumber</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">done;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@end</span></span></code></pre><p>The implementations requires some <code>@dynamic</code> declarations in order to get through to my fallback algorithms I’ll describe later:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@implementation</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> SeaObject</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@dynamic</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> title, done;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@end</span></span></code></pre><p>Now I can nicely set properties like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">item.title </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> @&quot;Clean kitchen&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">item.done </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> @</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NO</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span></code></pre><p>On the implementation side of <code>SeaObject</code> all data is stored in <code>NSMutableDictionary *_properties</code>. The glue code for the keyed subscription part is trivial.</p><p>The magic is in the code that handle the access to the properties. Since we used <code>@dynamic</code> before, there is no counterpart on the implementation side for those properties. This is why we can override some fallbacks and voila everything ends up in <code>setObject:forKey</code> and <code>objectForKey:</code></p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSMethodSignature</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)methodSignatureForSelector:(</span><span style="color:#d73a49;--shiki-dark:#F97583">SEL</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)selector {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">sel </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSStringFromSelector</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(selector);</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> ([sel </span><span style="color:#005cc5;--shiki-dark:#79B8FF">rangeOfString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;set&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">].location </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSMethodSignature</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> signatureWithObjCTypes:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;v@:@&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    } </span><span style="color:#d73a49;--shiki-dark:#F97583">else</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSMethodSignature</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> signatureWithObjCTypes:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;@@:&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)forwardInvocation:(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSInvocation</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)invocation {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">sel </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSStringFromSelector</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(invocation.selector);</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> ([sel </span><span style="color:#005cc5;--shiki-dark:#79B8FF">rangeOfString:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;set&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">].location </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        sel </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSString</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> stringWithFormat:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;</span><span style="color:#005cc5;--shiki-dark:#79B8FF">%@%@</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">               [sel </span><span style="color:#005cc5;--shiki-dark:#79B8FF">substringWithRange:NSMakeRange</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">3</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#005cc5;--shiki-dark:#79B8FF">1</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)].lowercaseString,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">               [sel </span><span style="color:#005cc5;--shiki-dark:#79B8FF">substringWithRange:NSMakeRange</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">4</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, sel.length</span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#005cc5;--shiki-dark:#79B8FF">5</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)]];</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        id</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> __unsafe_unretained obj;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        [invocation </span><span style="color:#005cc5;--shiki-dark:#79B8FF">getArgument:</span><span style="color:#d73a49;--shiki-dark:#F97583">&amp;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">obj </span><span style="color:#005cc5;--shiki-dark:#79B8FF">atIndex:2</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> setObject:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">obj </span><span style="color:#005cc5;--shiki-dark:#79B8FF">forKey:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">sel];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    } </span><span style="color:#d73a49;--shiki-dark:#F97583">else</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        id</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> obj </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [_properties </span><span style="color:#005cc5;--shiki-dark:#79B8FF">objectForKey:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">sel];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        [invocation </span><span style="color:#005cc5;--shiki-dark:#79B8FF">setReturnValue:</span><span style="color:#d73a49;--shiki-dark:#F97583">&amp;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">obj];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Some more magic reading and writing dictionaries and this nice little helper is doing its job. Also adding categories works nicely.</p><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Source Code</p><p>You get the <a href="https://gist.github.com/holtwick/53ac035dd3eff102c68a4470cd195ea3" rel="noopener" target="_blank" class="external">full source code at GitHub</a>.</p></div><p><em>Please leave your comments below. I’m looking forward to your feedback.</em></p></div><p><em>Published on May 8, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Serialization on macOS and iOS - Speed and Size]]></title>
            <link>https://holtwick.de/blog/serialization</link>
            <guid isPermaLink="false">https://holtwick.de/blog/serialization</guid>
            <pubDate>Sat, 03 Feb 2018 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>I am currently working on a new storage for my Objective-C apps and I wondered if there is significant difference in <strong>speed</strong> and <strong>size</strong> of various serialization methods popular on macOS and iOS.</p><h2 id="the-contestants" tabindex="-1"><a class="header-anchor" href="#the-contestants">The Contestants</a></h2><p>On board with Foundation we already got some great ones:</p><ul><li><code>NSJSONSerialization</code></li><li><code>NSKeyedArchiver</code></li><li><code>NSPropertyListSerialization</code> in the two flavors <strong>XML</strong> and <strong>Binary</strong></li></ul><p>As the newcomer I chose:</p><ul><li><a href="https://github.com/gabriel/MPMessagePack" rel="noopener" target="_blank" class="external"><code>MPMessagePackWriter</code></a> implementing <a href="https://msgpack.org" rel="noopener" target="_blank" class="external">MessagePack</a></li></ul><p>I know, there are others like <strong>BSON</strong>, <strong>Thrift</strong> or <strong>Avro</strong>, but I want to keep it flexible on my side and not define any schema beforehand. I also have in mind, to use the format cross platform, which should not be a problem with <strong>JSON</strong> and <strong>MessagePack</strong>. I left the other ones in the test anyway out of curiosity, but they are not the winners anyway as we’ll see later ;)</p><h2 id="the-setting" tabindex="-1"><a class="header-anchor" href="#the-setting">The Setting</a></h2><p>I wrote a little Unit Test to perform my non representative tests. I then implemented <strong>performance testing</strong> parts and a <strong>size comparison</strong> one. You can take a look at this <a href="https://gist.github.com/holtwick/e0a15c66d6ad06b9bc6b1d1bcc4c8e67" rel="noopener" target="_blank" class="external">gist</a> to see what I did.</p><p>I also chose a very <strong>small</strong> test object and a very <strong>large</strong> one. As I said, this is really not very representative, but it might give an idea.</p><p>Finally, I also added <strong>GZIP</strong> into the mix, just to see if I was over optimizing for my problem.</p><h2 id="the-size-results" tabindex="-1"><a class="header-anchor" href="#the-size-results">The Size Results</a></h2><p class="img-wrapper"><img src="/assets/mxor37jqldbdmx.png" alt="small-size" width="487" height="247" loading="lazy"></p><p>For the small test <strong>MessagePack</strong> is the winner. JSON is also doing very well. GZIP is not playing a big role for small size, it is making things even worse, but that was to be expected. Even though XML was expected to be large, I wondered that KeyedArchiver is too.</p><p class="img-wrapper"><img src="/assets/la7amt9r1bk94sx.png" alt="large-size" width="484" height="246" loading="lazy"></p><p>For the large sizes GZIP really makes a difference. Of course there are a lot of repetitions for the property names which should make a nice target for compression.</p><p>But then again <strong>MessagePack</strong> is the winner and needs almost half as much space as the looser in this race does. But the distance to JSON again is not that big.</p><p>A very strange observation is, that the Binary variant of Plist is even worse than XML.</p><h2 id="the-speed-results" tabindex="-1"><a class="header-anchor" href="#the-speed-results">The Speed Results</a></h2><p>Green stands for tests on the small data and blue for large one:</p><p class="img-wrapper"><img src="/assets/dtugiji8eqpitn.png" alt="speed" width="487" height="313" loading="lazy"></p><p>For smaller data JSON seems to be the fastest one, followed by Message pack. For larger ones Plist is faster. Keyed Archiver is the slowest one in the field.</p><h2 id="the-final" tabindex="-1"><a class="header-anchor" href="#the-final">The Final</a></h2><p>Overall for my personal purposes <strong>JSON</strong> and <strong>MessagePack</strong> seem to be the most appropriate ones. I was very positively surprised of the JSON results. MessagePack as the clear winner in the size comparison is probably the best choice for the projects I’m working on.</p><p>I was very disappointed of KeyedArchiver, which I previously expected to be in the top field. If not required for Apple OS specific purposes it really does not make sense to use any of those proprietary formats anymore.</p></div><p><em>Published on February 3, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Smart Reordering for UITableView]]></title>
            <link>https://holtwick.de/blog/smart-table-reordering</link>
            <guid isPermaLink="false">https://holtwick.de/blog/smart-table-reordering</guid>
            <pubDate>Tue, 27 Feb 2018 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>For my current project <a href="https://zutun.io" rel="noopener" target="_blank" class="external">zutun.io</a> I’m implementing the reordering of entries as described in the <a href="https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/TableView_iPhone/ManageReorderRow/ManageReorderRow.html" rel="noopener" target="_blank" class="external">Apple Documentation</a>.</p><h2 id="algorithm-idea" tabindex="-1"><a class="header-anchor" href="#algorithm-idea">Algorithm Idea</a></h2><p>The goal is to perform as few operations on the database as possible, therefore I use a <strong>floating point number as the sort property</strong>. The basic idea is simple:</p><blockquote><p>To put an entry between ordering numbers A and B choose a number that is greater than A and smaller than B.</p></blockquote><p>The trick is to choose <strong>a random number</strong> to avoid conflicts when synching the data later on.</p><a href="https://youtu.be/A3CvEfWeVc4" target="_blank">https://youtu.be/A3CvEfWeVc4</a><h2 id="implementation" tabindex="-1"><a class="header-anchor" href="#implementation">Implementation</a></h2><p>First of all lets create a little helper that will create random floating numbers in a range:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">#import</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &lt;math.h&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">static</span><span style="color:#d73a49;--shiki-dark:#F97583"> inline</span><span style="color:#d73a49;--shiki-dark:#F97583"> double</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> hxRandomDouble</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#d73a49;--shiki-dark:#F97583">double</span><span style="color:#e36209;--shiki-dark:#FFAB70"> min</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#d73a49;--shiki-dark:#F97583">double</span><span style="color:#e36209;--shiki-dark:#FFAB70"> max</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    double</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _min </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> MIN</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(min, max);</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    double</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _max </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> MAX</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(min, max);</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    double</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _rnd </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> ((</span><span style="color:#d73a49;--shiki-dark:#F97583">double</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#6f42c1;--shiki-dark:#B392F0">arc4random</span><span style="color:#24292e;--shiki-dark:#E1E4E8">() </span><span style="color:#d73a49;--shiki-dark:#F97583">/</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">double</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)(UINT32_MAX</span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#005cc5;--shiki-dark:#79B8FF">1</span><span style="color:#24292e;--shiki-dark:#E1E4E8">));</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    return</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _min </span><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> ((_max </span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _min) </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> _rnd);</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>Lets say our entries are defined like this:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@interface</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> TodoRecord</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> : </span><span style="color:#6f42c1;--shiki-dark:#B392F0">NSObject</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSString</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">title;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@property</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> NSNumber</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">order;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">@end</span></span></code></pre><p>In our view controller we’ll store the entries in the property <code>objects</code>. Adding a new entry is then as easy as:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">TodoRecord </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">rec </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [[TodoRecord </span><span style="color:#005cc5;--shiki-dark:#79B8FF">alloc</span><span style="color:#24292e;--shiki-dark:#E1E4E8">] </span><span style="color:#005cc5;--shiki-dark:#79B8FF">init</span><span style="color:#24292e;--shiki-dark:#E1E4E8">];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">rec.title </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> title;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">NSNumber</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">max </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects </span><span style="color:#005cc5;--shiki-dark:#79B8FF">valueForKeyPath:</span><span style="color:#032f62;--shiki-dark:#9ECBFF">@&quot;@max.order&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">] </span><span style="color:#d73a49;--shiki-dark:#F97583">?:</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> @</span><span style="color:#005cc5;--shiki-dark:#79B8FF">1</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">rec.order </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> @(max.doubleValue </span><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> hxRandomDouble</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">1.</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#005cc5;--shiki-dark:#79B8FF">2.</span><span style="color:#24292e;--shiki-dark:#E1E4E8">));</span></span></code></pre><p>This will create a new entry with a distance of at least <code>1</code> to the previous one.</p><p>Now comes the tricky part. We will first allow reordering in general, which only makes sense if we have at least 2 entries. I consider the rest of the editing code as trivial, like calling <code>setEditing:</code> etc.:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">BOOL</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)tableView:(UITableView </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)tableView canEditRowAtIndexPath:(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSIndexPath</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)indexPath {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    return</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects.count </span><span style="color:#d73a49;--shiki-dark:#F97583">&gt;</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>The following code is the heart of the code that applies the movement. It is basically the implementation of the described idea. It will just do one database operation and sync nicely.</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-objc"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">void</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)tableView:(UITableView </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)tableView moveRowAtIndexPath:(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSIndexPath</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)sourceIndexPath toIndexPath:(</span><span style="color:#005cc5;--shiki-dark:#79B8FF">NSIndexPath</span><span style="color:#d73a49;--shiki-dark:#F97583"> *</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)destinationIndexPath {</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    NSInteger</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> src </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> sourceIndexPath.row;</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    NSInteger</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> dst </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> destinationIndexPath.row;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">    // Nothing to do</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (src </span><span style="color:#d73a49;--shiki-dark:#F97583">==</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> dst) </span><span style="color:#d73a49;--shiki-dark:#F97583">return</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">    // Move the entry in the representing NSMutableArray, see Apple Docs</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    TodoRecord </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">rec </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)[</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects </span><span style="color:#005cc5;--shiki-dark:#79B8FF">objectAtIndex:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">src];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects </span><span style="color:#005cc5;--shiki-dark:#79B8FF">removeObjectAtIndex:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">src];</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    [</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects </span><span style="color:#005cc5;--shiki-dark:#79B8FF">insertObject:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">rec </span><span style="color:#005cc5;--shiki-dark:#79B8FF">atIndex:</span><span style="color:#24292e;--shiki-dark:#E1E4E8">dst];</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">    // Find the new neighbours of our entry</span></span>
<span class="line"><span style="color:#005cc5;--shiki-dark:#79B8FF">    NSInteger</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> len </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects.count;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    TodoRecord </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">beforeRec </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> dst </span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 1</span><span style="color:#d73a49;--shiki-dark:#F97583"> &gt;=</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 0</span><span style="color:#d73a49;--shiki-dark:#F97583"> ?</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects[dst </span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292e;--shiki-dark:#E1E4E8">] </span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> nil</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    TodoRecord </span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#24292e;--shiki-dark:#E1E4E8">afterRec </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> dst </span><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 1</span><span style="color:#d73a49;--shiki-dark:#F97583"> &lt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> len </span><span style="color:#d73a49;--shiki-dark:#F97583">?</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span><span style="color:#005cc5;--shiki-dark:#79B8FF">self</span><span style="color:#24292e;--shiki-dark:#E1E4E8">.objects[dst </span><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292e;--shiki-dark:#E1E4E8">] </span><span style="color:#d73a49;--shiki-dark:#F97583">:</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> nil</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">    // Find the range for the new ordering number</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    double</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> before </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> beforeRec.order.doubleValue;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    double</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> after </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> afterRec.order.doubleValue;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    double</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> current </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> rec.order.doubleValue;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">    // At the ends add some margin</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">!</span><span style="color:#24292e;--shiki-dark:#E1E4E8">beforeRec) before </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> after </span><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 2.5</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (</span><span style="color:#d73a49;--shiki-dark:#F97583">!</span><span style="color:#24292e;--shiki-dark:#E1E4E8">afterRec)  after </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> before </span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 2.5</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">    // If not alread inside of the range...</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    if</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (before </span><span style="color:#d73a49;--shiki-dark:#F97583">&lt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> current </span><span style="color:#d73a49;--shiki-dark:#F97583">||</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> after </span><span style="color:#d73a49;--shiki-dark:#F97583">&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> current) {</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">        // ... put somewhere middleish</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">        double</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> dist </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> (before </span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> after) </span><span style="color:#d73a49;--shiki-dark:#F97583">/</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> 3.</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">        rec.order </span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> @(</span><span style="color:#6f42c1;--shiki-dark:#B392F0">hxRandomDouble</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(after </span><span style="color:#d73a49;--shiki-dark:#F97583">+</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> dist,</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">                                     before </span><span style="color:#d73a49;--shiki-dark:#F97583">-</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> dist));</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><h2 id="conclusion" tabindex="-1"><a class="header-anchor" href="#conclusion">Conclusion</a></h2><p>There may be theoretical limits for this implementation, but the complications are minimal compared to the benefit of the algorithm.</p><p>You would like to comment on it? Send me a message on <a href="https://twitter.com/holtwick" rel="noopener" target="_blank" class="external">@holtwick</a></p><h2 id="update-2018-02-27" tabindex="-1"><a class="header-anchor" href="#update-2018-02-27">Update 2018-02-27</a></h2><p>I made some tests to see when the first collisions will occur. For the example above this will usually be around 50 steps. That means you can move items 50 times between a specific pair of entries before the order number will converge to identical values.</p><p>I then tried the same with <code>int32_t</code> and it turned out it reached about 30 steps without collision, which makes sense for a 32bit number divided by 2 in each step ;)</p><p>So sadly <em>infinity</em> in reality isn’t to far away. But I still think that the edge cases will not often be reached and then it is still enough time to do apply the classic approaches again and have room for another 30-50 rounds.</p></div><p><em>Published on February 27, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Static websites the jQuery way]]></title>
            <link>https://holtwick.de/blog/static-jquery</link>
            <guid isPermaLink="false">https://holtwick.de/blog/static-jquery</guid>
            <pubDate>Sat, 30 Dec 2017 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p><strong>This blog and website consists of static pages created using a practical technique that I would like to introduce in this article.</strong></p><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Update 2018-02-15</p><p></p><p>The project described here can be <a href="https://github.com/holtwick/seasite" rel="noopener" target="_blank" class="external">downloaded from GitHub</a> now.</p></div><p>The special thing about this is that a large part of this website generator consists of programming patterns, which are also used in dynamic websites via jQuery. A simple example says more than a thousand words:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-js"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> site</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> SeaSite</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">  &apos;public&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#6a737d;--shiki-dark:#6A737D">// Source folder</span></span>
<span class="line"><span style="color:#032f62;--shiki-dark:#9ECBFF">  &apos;dist&apos;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#6a737d;--shiki-dark:#6A737D">// Destination folder</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">site.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">handle</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;index.html&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, (</span><span style="color:#e36209;--shiki-dark:#FFAB70">$</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#d73a49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  $</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;title&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">text</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;New Title&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">})</span></span></code></pre><p>This example creates the <code>site</code> object with the source directory <code>public</code> and the target directory <code>dist</code>. In the first step, the content of the source directory is cloned into the target directory. The next step is to edit the file <code>index.html</code>. The help function gets the variable <code>$</code> known from jQuery and sets the content of the <code>title</code> element to <code>New Title</code>. The modified content is saved automatically by the framework.</p><h2 id="file-patterns-and-templating" tabindex="-1"><a class="header-anchor" href="#file-patterns-and-templating">File patterns and templating</a></h2><p>From here it is easy to build more complex websites with a few lines of code:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-js"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">site.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">handle</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/</span><span style="color:#005cc5;--shiki-dark:#79B8FF">.</span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#22863a;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032f62;--shiki-dark:#DBEDFF">md</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, (</span><span style="color:#e36209;--shiki-dark:#FFAB70">content</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#e36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#d73a49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> $</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> site.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">readDOM</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;template.html&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> htmlPath</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> path.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">replace</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/</span><span style="color:#22863a;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032f62;--shiki-dark:#DBEDFF">md</span><span style="color:#d73a49;--shiki-dark:#F97583">$</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;.html&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> md</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> parseMarkdown</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(content)</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">  const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> title</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> md.props.title</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  $</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;title&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">text</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">`${</span><span style="color:#24292e;--shiki-dark:#E1E4E8">title</span><span style="color:#032f62;--shiki-dark:#9ECBFF">} - My Website`</span><span style="color:#24292e;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  $</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;#title&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">text</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(title)</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  $</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;#content&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">html</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(md.html)</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  site.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">write</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(htmlPath, $.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">html</span><span style="color:#24292e;--shiki-dark:#E1E4E8">())</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">})</span></span></code></pre><p><strong>template.html</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-html"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;!</span><span style="color:#22863a;--shiki-dark:#85E89D">DOCTYPE</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> html</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">head</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">title</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Template&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">title</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">head</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">body</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">h1</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;title&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Title&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">h1</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> id</span><span style="color:#24292e;--shiki-dark:#E1E4E8">=</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&quot;content&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;Content&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">div</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">body</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span></code></pre><p><strong>hello-world.md</strong>:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-markdown"><span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"><span style="color:#22863a;--shiki-dark:#85E89D">title</span><span style="color:#24292e;--shiki-dark:#E1E4E8">: </span><span style="color:#032f62;--shiki-dark:#9ECBFF">Hello World</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">Lorem </span><span style="color:#24292e;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold">**ipsum**</span></span></code></pre><p>This example uses a <strong>file pattern</strong> to find all Markdown files in the site’s source folder. As you may notice, this time we get a plain string instead of the DOM object from the previous example. This is because DOM objects are only generated from <code>html</code> and <code>xml</code> files, otherwise a <strong>string</strong> is returned.</p><p>We then directly create a new DOM object from the <code>template.html</code> file. There we set the content of the <code>title</code> element as well as for the DOM element with the ID <code>#title</code>. The title is extracted from the Markdown file, where we could put even more properties like e.g. language, description, keywords.</p><p>The Markdown parser “<a href="https://github.com/chjj/marked" rel="noopener" target="_blank" class="external">marked</a>” we use, converts the contents to an HTML string we can pass to the <code>#content</code> element in out template.</p><p>The last step is to write the file with a <code>.html</code> suffix. We don’t need the Markdown files anymore and could clean up by calling <code>site.remove(/.*\.md/)</code>.</p><p>This little script will be applied to all Markdown files in the site’s source folder, so you can quickly build up a site with easy to create content. The CSS selectors are super powerful and changing other aspects of the page is super simple and intuitive.</p><h3 id="static-jsx" tabindex="-1"><a class="header-anchor" href="#static-jsx">Static JSX</a></h3><p>But it doesn’t stop here, let’s push it a bit further! Lets use JSX to generate portions of HTML that need to be even more flexible. Let’s imagine we want to create an index of all Markdown files we converted in the previous example:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-jsx"><span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">const</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> pages</span><span style="color:#d73a49;--shiki-dark:#F97583"> =</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> []</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">site.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">handle</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/</span><span style="color:#005cc5;--shiki-dark:#79B8FF">.</span><span style="color:#d73a49;--shiki-dark:#F97583">*</span><span style="color:#22863a;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032f62;--shiki-dark:#DBEDFF">md</span><span style="color:#032f62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, (</span><span style="color:#e36209;--shiki-dark:#FFAB70">content</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, </span><span style="color:#e36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#d73a49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">  // ...</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  pages.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">push</span><span style="color:#24292e;--shiki-dark:#E1E4E8">({ htmlPath, title })</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">site.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">handle</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;index.html&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">, (</span><span style="color:#e36209;--shiki-dark:#FFAB70">$</span><span style="color:#24292e;--shiki-dark:#E1E4E8">) </span><span style="color:#d73a49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">  $</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#032f62;--shiki-dark:#9ECBFF">&apos;#content&apos;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">).</span><span style="color:#6f42c1;--shiki-dark:#B392F0">html</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">ul</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">      {pages.</span><span style="color:#6f42c1;--shiki-dark:#B392F0">map</span><span style="color:#24292e;--shiki-dark:#E1E4E8">(</span><span style="color:#e36209;--shiki-dark:#FFAB70">page</span><span style="color:#d73a49;--shiki-dark:#F97583"> =&gt;</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> &lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">li</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;&lt;</span><span style="color:#22863a;--shiki-dark:#85E89D">a</span><span style="color:#6f42c1;--shiki-dark:#B392F0"> href</span><span style="color:#d73a49;--shiki-dark:#F97583">=</span><span style="color:#24292e;--shiki-dark:#E1E4E8">{page.htmlPath}&gt;{title}&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">a</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">li</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;)}</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">    &lt;/</span><span style="color:#22863a;--shiki-dark:#85E89D">ul</span><span style="color:#24292e;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">  )</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">})</span></span></code></pre><p>We enhanced the previous example by collecting page info in the <code>pages</code> variable. After all Markdown pages are processed the links should be added to the <code>index.html</code> file. We use JSX to create a simple list with links. This is the same code you would use in a React JS project, but of course this is a custom JSX generator, which creates an HTML string form the JSX code.</p><p>This way you don’t need any complex templating language in the HTML file itself to get things done.</p><h2 id="the-technology" tabindex="-1"><a class="header-anchor" href="#the-technology">The technology</a></h2><p>All this is made possible by the awesome <a href>cheerio</a> project, which is driving the DOM and jQuery like part. The API is covering everything you’ll need to manipulate the HTML and XML files.</p></div><p><em>Published on December 30, 2017</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Typora, perfect Markdown]]></title>
            <link>https://holtwick.de/blog/typora</link>
            <guid isPermaLink="false">https://holtwick.de/blog/typora</guid>
            <pubDate>Wed, 21 Mar 2018 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>I love <a href="https://typora.io" rel="noopener" target="_blank" class="external">Typora</a> for editing Markdown. It really unites everything I ever expected from an editor of that type. Of course, I’m writing this article with Typora as well and process it with my static website builder <a href="https://github.com/holtwick/seasite" rel="noopener" target="_blank" class="external">SeaSite</a>.</p><h2 id="adding-a-custom-theme" tabindex="-1"><a class="header-anchor" href="#adding-a-custom-theme">Adding a Custom Theme</a></h2><p>Although it already comes with great <strong>themes</strong> for different editor requirements, I’d still love to get a preview of the content that is as close as possible to what the later published text will look like.</p><p>And as to be expected Typora lets you customize the look and some feel by simply dropping in a CSS file. That is what I did, I got to the theme folder via the apps preferences and added a symbolic link to my custom CSS:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-sh"><span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">ln</span><span style="color:#005cc5;--shiki-dark:#79B8FF"> -s</span><span style="color:#d73a49;--shiki-dark:#F97583"> &lt;</span><span style="color:#032f62;--shiki-dark:#9ECBFF">path_to_my_cs</span><span style="color:#24292e;--shiki-dark:#E1E4E8">s</span><span style="color:#d73a49;--shiki-dark:#F97583">&gt;</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> holtwick.css</span></span></code></pre><p>After a restart it will appear in the menu like this:</p><p class="img-wrapper"><img src="/assets/hqzo616czyl1o8.png" alt="mage-20180321174319" width="192" height="193" loading="lazy"></p><p>Perfect! Now to reload the style after any update to the CSS I just select the theme again from the menu and it will reload.</p><h2 id="reuse-existing-css" tabindex="-1"><a class="header-anchor" href="#reuse-existing-css">Reuse existing CSS</a></h2><p>Of course, I wanted to reuse as much of the CSS I already have. To achieve that I’ve split up the CSS into some LESS files. In <code>text.less</code> I put the basic text styles like <code>h1</code> , <code>blockquote</code> etc.</p><p>I then use two more LESS files, one for the website and the other to be linked with Typora:</p><pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code class="language-less"><span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">// Website</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">.blog</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    @import</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;text&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6a737d;--shiki-dark:#6A737D">// Typora</span></span>
<span class="line"><span style="color:#6f42c1;--shiki-dark:#B392F0">#write</span><span style="color:#24292e;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#d73a49;--shiki-dark:#F97583">    @import</span><span style="color:#032f62;--shiki-dark:#9ECBFF"> &quot;text&quot;</span><span style="color:#24292e;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292e;--shiki-dark:#E1E4E8">}</span></span></code></pre><p>As you might notice, the Typora stuff needs to be wrapped into <code>#write</code> to not unintentionally affect other areas of the window like the navigation.</p><p>You can the add some fine tuning to it, like adding styles for <code>.md-meta-block</code> which would be the YAML property area.</p><p>And here is the final design, attention, it might be a deja vu ;)</p><p class="img-wrapper"><img src="/assets/ouza5ijt1cstpk4.png" alt="mage-20180321175628" width="810" height="1058" loading="lazy"></p><p>I hope you enjoy Typora as much as I do and I send a big compliment to its author from my side.</p></div><p><em>Published on March 21, 2018</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[UnplugTrump]]></title>
            <link>https://holtwick.de/blog/unplug-trump</link>
            <guid isPermaLink="false">https://holtwick.de/blog/unplug-trump</guid>
            <pubDate>Tue, 11 Mar 2025 07:00:00 GMT</pubDate>
            <description><![CDATA[UnplugTrump: How can you make yourself independent of US services, especially in the age of Donald Trump? The key points are self-hosting, decentralization and open source.]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>What crazy times we live in, and now everything is becoming even more absurd with President Trump. Regardless of the big-picture political situation, the goal should now be to become independent of the US and strengthen the EU. There is also a lot that can be done in the private sphere, and I would like to gradually present alternatives for US services and products in a small series.</p><p>Early 2020, I had tried to sketch out in my article <a href="it-strategy-for-europe">“An IT Strategy for Europe”</a> what I thought such a step towards digital sovereignty might look like in broad strokes. Not much is happening in this regard.</p><p>I would like to offer the following criteria for achieving independence and reliability:</p><ul><li><strong>Self-hosting</strong> → If you operate your services yourself, no one can shut them down and the data belongs to you.</li><li><strong>Decentralized</strong> → A distributed system is less susceptible to outages and censorship.</li><li><strong>Open source</strong> → The software cannot disappear and can be verified.</li></ul><p><img src="/assets/l0xwi0281x0e78m.jpg" alt="Image" width="775" height="434" loading="lazy"><em>Image by <a href="https://chat.mistral.ai/chat/93944700-3a0c-4d90-a1a6-c42c5462064e" rel="noopener" target="_blank" class="external">mistral.ai</a></em></p><h2 id="selfhosting" tabindex="-1"><a class="header-anchor" href="#selfhosting">Selfhosting</a></h2><p>It is so nice and convenient to use the services of large providers like Google, Microsoft or Apple, but more often than not you pay with your personal data. Even services like Apple can quickly change their privacy-friendly stance; there are already signs of this and the history of comparable services makes trust shrink.</p><h3 id="home-network" tabindex="-1"><a class="header-anchor" href="#home-network">Home network</a></h3><p>Unfortunately, operating your own server is not for everyone, but it is less complicated than you might think. You can start at home with a <a href="https://en.wikipedia.org/wiki/Network-attached_storage" rel="noopener" target="_blank" class="external"><strong>NAS</strong></a>. I own a <a href="https://en.wikipedia.org/wiki/Synology" rel="noopener" target="_blank" class="external">Synology</a> (Taiwan). It comes with many services of good quality that cover the essentials. But actually it can be any computer, the main thing is that <strong><a href="https://en.wikipedia.org/wiki/Docker_(software)" rel="noopener" target="_blank" class="external">Docker</a></strong> can be operated on it.</p><p><strong>Docker</strong> is the key to really easy and secure operation of “self-hosted services”. There is an almost <a href="https://github.com/awesome-selfhosted/awesome-selfhosted" rel="noopener" target="_blank" class="external">infinite number of solutions</a> for every problem.</p><p>For example, I run <a href="https://www.home-assistant.io/" rel="noopener" target="_blank" class="external"><strong>Home Assistant</strong></a> (open source) on my Synology, which is the dashboard for all devices and sensors in my own home that are somehow connected to the network. For photos, I use <a href="https://immich.app/" rel="noopener" target="_blank" class="external"><strong>Immich</strong></a> (open source), which also has features that can keep up. A <a href="https://en.wikipedia.org/wiki/Virtual_private_network" rel="noopener" target="_blank" class="external">VPN connection</a> - in my case WireGuard via a fritz.box - allows me to use all services while on the road.</p><h3 id="internet" tabindex="-1"><a class="header-anchor" href="#internet">Internet</a></h3><p>But even running a Docker service on the internet is not rocket science. At <a href="https://hetzner.cloud/?ref=thK9VpOJK5Sg" rel="noopener" target="_blank" class="external">Hetzner</a> (promo link), for example, you can set up a virtual server for less than 5 EUR and get it ready to go with Docker. After a few preparations (<a href="https://github.com/holtwick/selfhosted?tab=readme-ov-file#selfhosted" rel="noopener" target="_blank" class="external">https://github.com/holtwick/selfhosted?tab=readme-ov-file#selfhosted</a>) to connect your own domain to the IP, you’re ready to go.</p><p>I run many services there, but for private use I mainly use <a href="https://nextcloud.com/" rel="noopener" target="_blank" class="external"><strong>nextCloud</strong></a>. The look is a bit outdated in some places, but it serves its purpose for file sharing and other things you know from iCloud, for example.</p><h2 id="decentralized" tabindex="-1"><a class="header-anchor" href="#decentralized">Decentralized</a></h2><p>Everyone uses the internet’s most famous decentralized service: email. Actually, the whole magic of the internet is its decentralized structure. If something fails somewhere, the information finds another way, just like an ant trail.</p><p>Large providers are usually centralized. Of course, they try to distribute the load in the background, but these services are still technically vulnerable to disruptions, data loss and security breaches.</p><p>All the better that there are now more “federated” services again. Lately, <a href="https://joinmastodon.org" rel="noopener" target="_blank" class="external"><strong>Mastodon</strong></a> has become particularly well known as a Twitter /X alternative. But in this so-called <a href="https://de.wikipedia.org/wiki/Fediverse" rel="noopener" target="_blank" class="external">Fediverse</a>, other solutions are also emerging that can then also connect with each other. A good example of this is the YouTube replacement <a href="https://joinpeertube.org/de/" rel="noopener" target="_blank" class="external"><strong>PeerTube</strong></a>.</p><p>But there are other forms of decentralized data processing. One movement that I particularly like is called <a href="https://www.localfirstconf.com/" rel="noopener" target="_blank" class="external"><strong>Local First</strong></a>, which again emphasizes the importance of data being available locally. Storing the same data in different physical locations is also a form of decentralized structure. The prime example of this is versioning systems like <a href="https://git-scm.com/" rel="noopener" target="_blank" class="external"><strong>git</strong></a>.</p><h2 id="open-source" tabindex="-1"><a class="header-anchor" href="#open-source">Open Source</a></h2><p>Free and open software is the cornerstone of deeper trust. First, open software is not so quickly “out of the world”; it exists a thousand times over in copies. Secondly, with a little expertise, you can see exactly what the software does. And thirdly, it can be adapted, which in turn contributes to the project. It’s a digital form of volunteer work, without having to join an association ;)</p><h2 id="further-links" tabindex="-1"><a class="header-anchor" href="#further-links">Further links</a></h2><ul><li>Kuketz blog on the same topic with interesting links and tips (German):<br><a href="https://www.kuketz-blog.de/unplugtrump-mach-dich-digital-unabhaengig-von-trump-und-big-tech/" rel="noopener" target="_blank" class="external">https://www.kuketz-blog.de/unplugtrump-mach-dich-digital-unabhaengig-von-trump-und-big-tech/</a></li><li>Directory of European alternatives:<br><a href="https://european-alternatives.eu" rel="noopener" target="_blank" class="external">https://european-alternatives.eu</a></li><li>General search for alternatives:<br><a href="https://alternativeto.net" rel="noopener" target="_blank" class="external">https://alternativeto.net</a></li><li>Lots of pre-installed self-hosted projects, good for testing:<br><a href="https://adminforge.de" rel="noopener" target="_blank" class="external">https://adminforge.de</a></li></ul></div><p><em>Published on March 11, 2025</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[Website SSG]]></title>
            <link>https://holtwick.de/blog/website-ssg</link>
            <guid isPermaLink="false">https://holtwick.de/blog/website-ssg</guid>
            <pubDate>Wed, 06 Dec 2023 07:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p>After a few years, it was time to update the website. For the last version, I had even developed my own static website generator called <a href="birth-of-hostic">Hostic</a> and everything went wonderfully. But even though I love reinventing the wheel, I only did it halfway for this new attempt.</p><p>Basically, this website consists of <a href="https://vuejs.org/" rel="noopener" target="_blank" class="external">Vue</a> and <a href="https://en.wikipedia.org/wiki/Markdown" rel="noopener" target="_blank" class="external">Markdown</a>. A static version of this is created so that the corresponding page with content is available for each URL. This allows the website to be easily indexed by search engines and the loading time is very short. The process is called <a href="https://vitejs.dev/guide/ssr" rel="noopener" target="_blank" class="external">SSG</a> (Server-Side Rendering).</p><p>The highlight, however, is that this approach allows dynamic elements to be integrated into the website and even individual posts. A first example is the interactive registration for my e-mail newsletter, because I simply insert <code>&lt;AppNewsletter/&gt;</code> at this point:</p><div><a href="https://newsletter.holtwick.de/subscription/form" target="_blank">https://newsletter.holtwick.de/subscription/form</a></div><p>Another thing that makes things easier is the use of <a href="https://obsidian.md" rel="noopener" target="_blank" class="external">Obsidian</a> as a Markdown editor for the content. I use this practical tool every day anyway and can thus easily maintain the content.</p><div class="markdown-alert markdown-alert-info"><p class="markdown-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Obsidian Callouts</p><p>Obsidian’s <a href="https://help.obsidian.md/Editing+and+formatting/Callouts" rel="noopener" target="_blank" class="external">Callouts</a> are also supported. Also known as <a href="https://github.com/orgs/community/discussions/16925" rel="noopener" target="_blank" class="external">Github Alerts</a>. This is made possible by the practical <a href="https://github.com/antfu/markdown-it-github-alerts" rel="noopener" target="_blank" class="external">Markdown Plugin</a>.</p></div><p>I derived my own approach from the great project <a href="https://github.com/antfu/vitesse" rel="noopener" target="_blank" class="external">Vitesse</a>. The technique used is almost identical, but I had to make a few adjustments for my purposes.</p></div><p><em>Published on December 6, 2023</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
        <item>
            <title><![CDATA[XML2Invoice]]></title>
            <link>https://holtwick.de/blog/xml2invoice</link>
            <guid isPermaLink="false">https://holtwick.de/blog/xml2invoice</guid>
            <pubDate>Wed, 30 Oct 2024 07:00:00 GMT</pubDate>
            <description><![CDATA[E-invoices:Easily convert and pay. Basic PDF becomes machine readable, XML invoice becomes human-readable]]></description>
            <content:encoded><![CDATA[<div class="post-body"><div class="markdown-body"><p><strong>Worried about e-invoices? With XML2invoice you can start the new year relaxed…</strong></p><p class="img-wrapper"><img src="/assets/ld44m5h1twywwl.jpeg" alt="Image" width="2736" height="1824" loading="lazy"></p><p><a href="https://e-invoice.space" rel="noopener" target="_blank" class="external"><strong>XML2Invoice</strong></a> is a web-based app that converts your “normal” PDF invoice into XML format: Either into a pure <strong>XML file</strong> or into a <strong>ZUGFeRD</strong> PDF file that looks exactly like your invoice and in which the prescribed XML data is embedded.</p><p>And it also works the other way around: <a href="https://e-invoice.space" rel="noopener" target="_blank" class="external"><strong>XML2Invoice</strong></a> generates an electronically readable invoice in <strong>ZUGFeRD format</strong> from XML files that can no longer be read by humans, which looks like a standard PDF invoice on the outside, but also contains the XML data. Readable for humans and machines. If desired, you can import the PDF with the embedded XML data <strong>into Receipts</strong> with a click of the mouse.</p><p class="action"><a href="https://e-invoice.space" class="button oui-button external" target="_blank" rel="noopener noreferrer">Start now for free on xml2invoice.com !</a></p><p><strong>Update 2025-01-29:</strong> XML2Invoice is now called <a href="https://e-invoice.space" rel="noopener" target="_blank" class="external">E-Invoice Space</a></p></div><p><em>Published on October 30, 2024</em></p></div>]]></content:encoded>
            <author>support@holtwick.de (Dirk Holtwick)</author>
        </item>
    </channel>
</rss>