Krzysztof Kowalczyk bloghttps://blog.kowalczyk.info/atom.xml2001-08-29T00:00:00ZHow I implemented wc in the browser in 3 days2023-03-21T00:00:00Ztag:blog.kowalczyk.info,2023-03-21:/article/mzht/implementing-wc-in-a-browser.html<div class="notion-page" id="mzht">
<h1 class="hdr-with-anchor" id="building-wc-in-the-browser"><div>Building wc in the browser</div><a class="header-anchor" href="#building-wc-in-the-browser">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>From time to time I like to run <code>wc -l</code> on my source code to see how much code I wrote.</div>
<div>For those not in the know: <code>wc -l</code> shows number of lines in files.</div>
<div>Actually, what I have to do is more like <code>find -name "*.go" | xargs wc -l</code> because <code>wc</code> isn’t a particularly good at handling directories.</div>
<div>I just want to see number of lines in all my source files, man. I don’t want to google the syntax of <code>find</code> and <code>xargs</code> for a hundredth time.</div>
<div>After learning about <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API">File System API</a> I decided to write a tool that does just that as a web app. No need to install software.</div>
<div>I did just that and you can <a target="_blank" href="https://onlinetool.io/wc/">use it yourself</a>.</div>
<div>Here’s how it sees itself:</div>
<div><img class="blog-img" src="/img/wc-shot.png" alt="" /></div>
<div>The rest of this article describes how I would have done it if I did it.</div>
<h1 class="hdr-with-anchor" id="building-software-quickly"><div>Building software quickly</div><a class="header-anchor" href="#building-software-quickly">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>It only took me 3 days, which is a testament to how productive the web platform can be.</div>
<div>My weapons of choice are:</div>
<ul>
<li><a target="_blank" href="https://svelte.dev/">Svelte</a> for frontend</li>
<li><a target="_blank" href="https://tailwindcss.com/">Tailwind CSS</a> for CSS</li>
<li><a target="_blank" href="https://jsdoc.app/">JSDoc</a> for static typing of JavaScript</li>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API">File System API</a> to access files and directories on your computer</li>
<li><a target="_blank" href="https://vitejs.dev/">vite</a> for a bundler and dev server</li>
<li><a target="_blank" href="https://render.com">render</a> to deploy</li>
</ul>
<div>For a small project Svelte and Tailwind CSS are arguably an overkill. I used them because I standardized on that toolset. Standardization allows me to re-use prior experience and sometimes even code.</div>
<h2 class="hdr-with-anchor" id="why-those-technologies"><div>Why those technologies?</div><a class="header-anchor" href="#why-those-technologies">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Svelte is React without the bloat. Try it and you’ll love it.</div>
<div>Tailwind CSS is CSS but more productive. You have to try it to believe it.</div>
<div>JSDoc is happy medium between no types at all and TypeScript. I have great internal resistance to switching to TypeScript. Maybe 5 years from now.</div>
<div>And none of that would be possible without browser APIs that allow access to files on your computer. Which FireFox doesn’t implement because they are happy to loose market share to browser that implement useful features. Clearly <a target="_blank" href="https://news.ycombinator.com/item?id=30665913">$3 million a year</a> is not enough to buy yourself a CEO with understanding of the obvious.</div>
<h1 class="hdr-with-anchor" id="implementation-tidbits"><div>Implementation tidbits</div><a class="header-anchor" href="#implementation-tidbits">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<h2 class="hdr-with-anchor" id="getting-list-of-files"><div>Getting list of files</div><a class="header-anchor" href="#getting-list-of-files">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>To get a recursive listing of files in a directory use <code>showDirectoryPicker</code> to get a <code>FileSystemDirectoryHandle</code>. Call <code>dirHandle.values()</code> to get a list of directory entries. Recurse if an entry is a directory.</div>
<div>Not all browsers <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API#browser_compatibility">support</a> that API. To detect if it works:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @returns {boolean}
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">isIFrame</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">isIFrame</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// in iframe, those are different
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">isIFrame</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">self</span> <span class="o">!==</span> <span class="nb">window</span><span class="p">.</span><span class="nx">top</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// do nothing
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">isIFrame</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @returns {boolean}
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">supportsFileSystem</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s2">"showDirectoryPicker"</span> <span class="k">in</span> <span class="nb">window</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">isIFrame</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Because people on Hacker News always complain about slow, bloated software I took pains to make my code fast. One of those pains was using an array instead of an object to represent a file system entry.</div>
<div>Wait, now HN people will complain that I’m optimizing prematurely.</div>
<div>Listen buddy, Steve Wozniak wrote assembly in hex and he liked it. In comparison, optimizing memory layout of most frequently used object in JavaScript is like drinking champagne on Jeff Bezos’ yacht.</div>
<div>Here’s a JavaScript trick to optimizing memory layout of objects with fixed number of fields: derive your class from an Array.</div>
<h3 class="hdr-with-anchor" id="deriving-a-class-from-an-array"><div>Deriving a class from an Array</div><a class="header-anchor" href="#deriving-a-class-from-an-array">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Little known thing about JavaScript is that an Array is just an object and you can derive your class from it and add methods, getters and setters.</div>
<div>You get a compact layout of an array and convenience of accessors.</div>
<div>Here’s the sketch of how I implemented <code>FsEntry</code> object:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="c1">// a directory tree. each element is either a file:
</span></span></span><span class="line"><span class="cl"><span class="c1">// [file, dirHandle, name, path, size, null]
</span></span></span><span class="line"><span class="cl"><span class="c1">// or directory:
</span></span></span><span class="line"><span class="cl"><span class="c1">// [[entries], dirHandle, name, path, size, null]
</span></span></span><span class="line"><span class="cl"><span class="c1">// extra null value is for the caller to stick additional data
</span></span></span><span class="line"><span class="cl"><span class="c1">// without the need to re-allocate the array
</span></span></span><span class="line"><span class="cl"><span class="c1">// if you need more than 1, use an object
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// handle (file or dir), parentHandle (dir), size, path, dirEntries, meta
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">handleIdx</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">parentHandleIdx</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">sizeIdx</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">pathIdx</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">dirEntriesIdx</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">metaIdx</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">class</span> <span class="nx">FsEntry</span> <span class="kr">extends</span> <span class="nb">Array</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">get</span> <span class="nx">size</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="k">this</span><span class="p">[</span><span class="nx">sizeIdx</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// ... rest of the accessors
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre>
<div>We have 6 slots in the array and we can access them as e.g. <code>entry[sizeIdx]</code>. We can hide this implementation detail by writing a getter as <code>FsEntry.size()</code> shown above.</div>
<h2 class="hdr-with-anchor" id="reading-a-directory-recursively"><div>Reading a directory recursively</div><a class="header-anchor" href="#reading-a-directory-recursively">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Once you get <code>FileSystemDirectoryHandle</code> by using <code>window.showDirectoryPicker()</code> you can read the content of the directory.</div>
<div>Here’s one way to implement recursive read of directory:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {FileSystemDirectoryHandle} dirHandle
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {Function} skipEntryFn
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {string} dir
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @returns {Promise<FsEntry>}
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">readDirRecur</span><span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="nx">dirHandle</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">skipEntryFn</span> <span class="o">=</span> <span class="nx">dontSkip</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">dir</span> <span class="o">=</span> <span class="nx">dirHandle</span><span class="p">.</span><span class="nx">name</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="cm">/** @type {FsEntry[]} */</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">entries</span> <span class="o">=</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// @ts-ignore
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">for</span> <span class="kr">await</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">handle</span> <span class="k">of</span> <span class="nx">dirHandle</span><span class="p">.</span><span class="nx">values</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">skipEntryFn</span><span class="p">(</span><span class="nx">handle</span><span class="p">,</span> <span class="nx">dir</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">continue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">dir</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">?</span> <span class="nx">handle</span><span class="p">.</span><span class="nx">name</span> <span class="o">:</span> <span class="sb">`</span><span class="si">${</span><span class="nx">dir</span><span class="si">}</span><span class="sb">/</span><span class="si">${</span><span class="nx">handle</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">handle</span><span class="p">.</span><span class="nx">kind</span> <span class="o">===</span> <span class="s2">"file"</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">e</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">FsEntry</span><span class="p">.</span><span class="nx">fromHandle</span><span class="p">(</span><span class="nx">handle</span><span class="p">,</span> <span class="nx">dirHandle</span><span class="p">,</span> <span class="nx">path</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">entries</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">handle</span><span class="p">.</span><span class="nx">kind</span> <span class="o">===</span> <span class="s2">"directory"</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">e</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">readDirRecur</span><span class="p">(</span><span class="nx">handle</span><span class="p">,</span> <span class="nx">skipEntryFn</span><span class="p">,</span> <span class="nx">path</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">e</span><span class="p">.</span><span class="nx">path</span> <span class="o">=</span> <span class="nx">path</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">entries</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FsEntry</span><span class="p">(</span><span class="nx">dirHandle</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">res</span><span class="p">.</span><span class="nx">dirEntries</span> <span class="o">=</span> <span class="nx">entries</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">res</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Function <code>skipEntryFn</code> is called for every entry and allows the caller to decide to not include a given entry. You can, for example, skip a directory like <code>.git</code>.</div>
<div>It can also be used to show progress of reading the directory to the user, as it happens asynchronously.</div>
<h2 class="hdr-with-anchor" id="showing-the-files"><div>Showing the files</div><a class="header-anchor" href="#showing-the-files">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I use tables and I’m not ashamed.</div>
<div>It’s still the best technology to display, well, a table of values where cells are sized to content and columns are aligned.</div>
<div>Flexbox doesn’t remember anything across rows so it can’t align columns.</div>
<div>Grid can layout things properly but I haven’t found a way to easily highlight the whole row when mouse is over it. With CSS you can only target individual cells in a grid, not rows.</div>
<div>With table I just style <code><tr class="hover:bg-gray-100"></code>. That’s Tailwind speak for: on mouse hover set background color to light gray.</div>
<div>Folder can contain other folders so we need recursive components to implement it. Svelte supports that with <code><svelte:self></code>.</div>
<div>I implemented it as a tree view where you can expand folders to see their content.</div>
<div>It’s one big table for everything but I needed to indent each expanded folder to make it look like a tree.</div>
<div>It was a bit tricky. I went with <code>indent</code> property in my <code>Folder</code> component. Starts with <code>0</code> and goes <code>+1</code> for each level of nesting.</div>
<div>Then I style the first file name column as <code><td class="ind-{indent}">...</td></code> and use those CSS styles:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="p"><</span><span class="nt">style</span><span class="p">></span>
</span></span><span class="line"><span class="cl"> :global(.ind-1) {
</span></span><span class="line"><span class="cl"> padding-left: 0.5rem;
</span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl"> :global(.ind-2) {
</span></span><span class="line"><span class="cl"> padding-left: 1rem;
</span></span><span class="line"><span class="cl"> }
</span></span><span class="line"><span class="cl"> /* ... up to .ind-17 */
</span></span></code></pre>
<div>Except it goes to <code>.ind-17</code>. Yes, if you have deeper nesting, it won’t show correctly. I’ll wait for a bug report before increasing it further.</div>
<h2 class="hdr-with-anchor" id="calculating-line-count"><div>Calculating line count</div><a class="header-anchor" href="#calculating-line-count">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You can get the size of the file from <code>FileSystemFileEntry</code>.</div>
<div>For source code I want to see number of lines. It’s quite trivial to calculate:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {Blob} f
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @returns {Promise<number>}
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">lineCount</span><span class="p">(</span><span class="nx">f</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">f</span><span class="p">.</span><span class="nx">size</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// empty files have no lines
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">ab</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">f</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Uint8Array</span><span class="p">(</span><span class="nx">ab</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">nLines</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// if last character is not newline, we must add +1 to line count
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">let</span> <span class="nx">toAdd</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">b</span> <span class="k">of</span> <span class="nx">a</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// line endings are:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// CR (13) LF (10) : windows
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// LF (10) : unix
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// CR (13) : mac
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// mac is very rare so we just count 10 as they count
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// windows and unix lines
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="p">(</span><span class="nx">b</span> <span class="o">===</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">toAdd</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">nLines</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">toAdd</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">nLines</span> <span class="o">+</span> <span class="nx">toAdd</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>It doesn’t handle Mac files that use CR for newlines. It’s ok to write buggy code as long as you document it.</div>
<div>I also skip known binary files (<code>.png</code>, <code>.exe</code> etc.) and known “not mine” directories like <code>.git</code> and <code>node_modules</code>.</div>
<div>Small considerations like that matter.</div>
<h2 class="hdr-with-anchor" id="remembering-opened-directories"><div>Remembering opened directories</div><a class="header-anchor" href="#remembering-opened-directories">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I typically use it many times on the same directories and it’s a pain to pick the same directory over and over again.</div>
<div><code>FileSystemDirectoryHandle</code> can be stored in <code>IndexedDB</code> so I implemented a history of opened directories using a <a href="/article/persisted-svelte-store-indexeddb.html">persisted store using IndexedDB</a>.</div>
<h2 class="hdr-with-anchor" id="asking-for-permissions"><div>Asking for permissions</div><a class="header-anchor" href="#asking-for-permissions">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>When it comes to accessing files and directories on disk you can’t ask for forgiveness, you have to ask for permission.</div>
<div>User grants permissions in <code>window.showDirectoryPicker()</code> and browser remembers them for a while, but they expire quite quickly.</div>
<div>You need to re-check and re-ask for permission to <code>FileSystemFileHandle</code> and <code>FileSystemDirectoryHandle</code> before each access:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">verifyHandlePermission</span><span class="p">(</span><span class="nx">fileHandle</span><span class="p">,</span> <span class="nx">readWrite</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">readWrite</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">options</span><span class="p">.</span><span class="nx">mode</span> <span class="o">=</span> <span class="s2">"readwrite"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Check if permission was already granted. If so, return true.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="p">((</span><span class="kr">await</span> <span class="nx">fileHandle</span><span class="p">.</span><span class="nx">queryPermission</span><span class="p">(</span><span class="nx">options</span><span class="p">))</span> <span class="o">===</span> <span class="s2">"granted"</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Request permission. If the user grants permission, return true.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="p">((</span><span class="kr">await</span> <span class="nx">fileHandle</span><span class="p">.</span><span class="nx">requestPermission</span><span class="p">(</span><span class="nx">options</span><span class="p">))</span> <span class="o">===</span> <span class="s2">"granted"</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// The user didn't grant permission, so return false.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>If permissions are still valid from before, it’s a no-op. If not, the browser will show a dialog asking for permissions.</div>
<div>If you ask for write permissions, Chrome will show 2 confirmations dialogs vs. 1 for read-only access.</div>
<div>I start with read-only access and, if needed, ask again to get a write (or delete) permissions.</div>
<h2 class="hdr-with-anchor" id="deleting-files-and-directories"><div>Deleting files and directories</div><a class="header-anchor" href="#deleting-files-and-directories">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Deleting files has nothing to do with showing line counts but it was easy to implement, it was useful so I added it.</div>
<div>You need to remember <code>FileSystemDirectoryHandle</code> for the parent directory.</div>
<div>To delete a file: <code>parentDirHandle.removeEntry("foo.txt")</code></div>
<div>To delete a directory: <code>parentDirHandle.removeEntry("node_modules", {recursive: true})</code></div>
<h2 class="hdr-with-anchor" id="getting-bit-by-a-multi-threading-bug"><div>Getting bit by a multi-threading bug</div><a class="header-anchor" href="#getting-bit-by-a-multi-threading-bug">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>JavaScript doesn’t have multiple threads and you can’t have all those nasty bugs? Right? Right?</div>
<div>Yes and no.</div>
<div>Async is not multi-threading but it does create non-obvious execution flows.</div>
<div>I had a bug: I noticed that some <code>.txt</code> files were showing line count of 0 even though they clearly did have lines.</div>
<div>I went bug hunting.</div>
<div>I checked the <code>lineCount</code> function. Seems ok.</div>
<div>I added <code>console.log()</code>, I stepped through the code. Time went by and my frustration level was reaching DEFCON 1.</div>
<div>Thankfully before I reached <a target="_blank" href="https://en.wikipedia.org/wiki/DEFCON">cocked pistol</a> I had an epiphany.</div>
<div>You see, JavaScript has async where some code can interleave with some other code. The browser can splice those async “threads” with UI code.</div>
<div>No threads means there are no data races i.e. writing memory values that other thread is in the middle of reading.</div>
<div>But we do have non-obvious execution flows.</div>
<div>Here’s how my code worked:</div>
<ul>
<li>get a list of files (async)</li>
<li>show the files in UI</li>
<li>calculate line counts for all files (async)</li>
<li>update UI to show line counts after we get them all</li>
</ul>
<div>Async is great for users: calculating line counts could take a long time as we need to read all those files.</div>
<div>If this process wasn’t async it would block the UI.</div>
<div>Thanks to async there’s enough checkpoints for the browser to process UI events in between processing files.</div>
<div>The issue was that function to calculate line counts was using an array I got from reading a directory.</div>
<div>I passed the same array to <code>Folder</code> component to show the files. And I sorted the array to show files in human friendly order.</div>
<div>In JavaScript sorting mutates an array and that array was partially processed by line counting function.</div>
<div>As a result if series of events was unfortunate enough, I would skip some files in line counting. They would be resorted to a position that line counting thought it already counted.</div>
<div>Result: no lines for you!</div>
<div>A happy ending and an easy fix: <code>Folder</code> makes a copy of an array so sorting doesn’t affect line counting process.</div>
<h2 class="hdr-with-anchor" id="the-future"><div>The future</div><a class="header-anchor" href="#the-future">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>No software is ever finished but I arrived at a point where it does the majority of the job I wanted so I shipped it.</div>
<div>There is a feature I would find useful: statistics for each extensions.</div>
<div>How many lines in <code>.go</code> files vs. <code>.js</code> files etc.?</div>
<div>But I’m holding off implementing it until:</div>
<ul>
<li>I really, really want it</li>
<li>I get feature requests from people who really, really want it</li>
</ul>
<div>You can look at the <a target="_blank" href="https://github.com/kjk/onlinetool.io/tree/main/web/wc">source code</a>. It’s source visible but <a target="_blank" href="https://github.com/kjk/onlinetool.io/blob/main/LICENSE.md">not open source</a>.</div>
</div>
Ideas for replit bounties2023-03-13T00:00:00Ztag:blog.kowalczyk.info,2023-03-13:/article/1guh/ideas-for-replit-bounties.html<div class="notion-page" id="1guh">
<div>Apparently replit asks all Pro users about their thoughts.</div>
<div>As it happens, I have a lot of thoughts about how to improve Replit bounties.</div>
<h2 class="hdr-with-anchor" id="lower-transaction-costs"><div>Lower transaction costs</div><a class="header-anchor" href="#lower-transaction-costs">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Currently the process is:</div>
<ul>
<li>I post a bounty</li>
<li>one or more people apply</li>
<li>I select an applicant</li>
<li>they do the work</li>
<li>I accept or not</li>
</ul>
<div>The back-and-forth between bounty creator and applicant is a transaction cost.</div>
<div>The smaller the bounty amount, the higher the cost as percentage of the job.</div>
<div>Choosing applicants is also arbitrary: there’s just not enough info to decide that one person is better than the other but I have to pick one.</div>
<div>Experiment with (optional) mode that works more like 99designs: i.e.:</div>
<ul>
<li>I post a “first to complete wins” bounty</li>
<li>whoever completes the bounty first wins</li>
</ul>
<div>There is potential for abuse: someone does the work and I don’t pay. Penalize people who do that, potentially banning them from posting bounties.</div>
<div>You would need a way for devs to provide feedback on bounty creators (and vice versa) and a human who reviews this and takes action.</div>
<div>Messy, yes, but “do things that don’t scale” (Paul G.)</div>
<div>This incentivize meritocracy: bounty makers want fast work and good devs who work fast will make more money.</div>
<h2 class="hdr-with-anchor" id="educate-bounty-posters"><div>Educate bounty posters</div><a class="header-anchor" href="#educate-bounty-posters">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Most bounties are badly described: vague, not enough information, no clear acceptance criteria etc.</div>
<div>Write a concise “How to create a successful bounty” document (mostly based on what you see as good / bad practices in current bounties).</div>
<div>When creating a new bounty, provide a link to that article and in general promote it.</div>
<div>Topics to cover:</div>
<ul>
<li>set pricing expectations (i.e. no $5 for a week of work)</li>
<li>examples of good and bad requirements, acceptance criteria etc.
*</li>
</ul>
<h2 class="hdr-with-anchor" id="purge-obviously-bad-bounties"><div>Purge obviously bad bounties</div><a class="header-anchor" href="#purge-obviously-bad-bounties">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You want to establish a reputation:</div>
<ul>
<li>with devs as a place to make money</li>
<li>bounty creators as a place to get work done quickly</li>
</ul>
<div>It does you no good if there’s a $4 bounty to do weeks of work.</div>
<div>It’ll never be fulfilled, it creates a bad impression for both devs and bounty creators who understand market wages.</div>
<div>You should have a human who reviews new bounties and closes obviously bad ones.</div>
<div>He can use that to also educate i.e. link to the above “how to make a good bounty” article.</div>
<h2 class="hdr-with-anchor" id="don-t-list-cancelled-bounties-by-default"><div>Don’t list cancelled bounties by default</div><a class="header-anchor" href="#don-t-list-cancelled-bounties-by-default">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Default list is “all” which includes cancelled bounties. Those are just noise.</div>
<h2 class="hdr-with-anchor" id="educate-and-penalize-bad-bounty-creators"><div>Educate and penalize bad bounty creators</div><a class="header-anchor" href="#educate-and-penalize-bad-bounty-creators">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>An example of bad bounty creator: someone who has applicants but doesn’t assign the bounty in reasonable amount of time.</div>
<div>Educate: have a bot that checks for that and e.g. if an applicant isn’t chosen in e.g. 3 days, send a message.</div>
<div>Penalize: e.g. lower their ranking in the list of bounties.</div>
<h2 class="hdr-with-anchor" id="reverse-search-bounty-creator-looks-for-devs"><div>Reverse search: bounty creator looks for devs</div><a class="header-anchor" href="#reverse-search-bounty-creator-looks-for-devs">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>In addition to devs picking bounties, allow bounty makers to pick devs.</div>
<div>Create a directory of devs where they list the technologies they know, what bounties they’ve completed, feedback from bounty creators so far, their availability etc.</div>
<div>Allow bounty makers “ping” them i.e. suggest that they are a good candidate for a given bounty.</div>
<h2 class="hdr-with-anchor" id="change-how-entering-price-works"><div>Change how entering price works</div><a class="header-anchor" href="#change-how-entering-price-works">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Currently if I open a bounty and enter $200, I pay $200 and the dev gets $200 - replit cut.</div>
<div>As a result there are weirdly priced bounties, like $192.</div>
<div>It should be: if I enter $200 that’s what the dev makes and you charge me $200 + replit cut</div>
<h2 class="hdr-with-anchor" id="don-t-dismiss-create-a-bounty-with-outside-click"><div>Don’t dismiss “Create a Bounty” with outside click</div><a class="header-anchor" href="#don-t-dismiss-create-a-bounty-with-outside-click">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I clicked outside by accident while in the middle of filling out a bounty and thought I lost what I wrote.</div>
<div>I didn’t, because you persist the state, but it’s not at all obvious how to get back.</div>
<div>Only an explicit “Cancel” or (“Save as draft”) should dismiss the dialog.</div>
<div>Also, give it more horizontal space. The most important text box is tiny.</div>
<h2 class="hdr-with-anchor" id="redesign-discussions"><div>Redesign discussions</div><a class="header-anchor" href="#redesign-discussions">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>There was a bounty where I had expertise in subject matter and knew it couldn’t possibly be done.</div>
<div>Currently the system discourages any feedback or discussion unless between selected applicant.</div>
<div>I get it: you don’t want low quality discussions but I think it’s a bad focus. Moderation can mitigate low quality.</div>
<div>Discussions should be more like stack overflow or discourse: comments are below the bounty description (not as a separate tab) and are encouraged (vs. discouraged currently).</div>
<div>Bounty creators should be able to moderate (hide / delete comments).</div>
<div>Applications and Discussion should be merged: an application is just a comment with “I apply for this job” attribute (e.g. a checkbox).</div>
<h2 class="hdr-with-anchor" id="monitor-failed-bounties"><div>Monitor failed bounties</div><a class="header-anchor" href="#monitor-failed-bounties">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Look for cancelled / abandoned bounties.</div>
<div>Ask yourself: “why did this bounty failed” and “what could we do to increase possibility of success for this bounty”?</div>
<div>Your job is not to list bounties and collect the payments.</div>
<div>Your job is to make devs and bounty creators successful and that involves doing things that do no scale, like manually reviewing failed bounties and coming up with ideas on how to make them not fail.</div>
<h1 class="hdr-with-anchor" id="beyond-bounties"><div>Beyond bounties</div><a class="header-anchor" href="#beyond-bounties">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Other ideas</div>
<h2 class="hdr-with-anchor" id="bad-error-message-when-replit-has-issues"><div>Bad error message when replit has issues</div><a class="header-anchor" href="#bad-error-message-when-replit-has-issues">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I was using replit and I got an error message: “We’re either having technical difficulties or you’ve violated our tos. You can’t access your replit”.</div>
<div>As you can imagine “you’re a criminal” vs. “we’ve fucked up” is a very different message.</div>
<div>If you don’t know which scenario happened, you should fix it.</div>
<div>If you do, you should tell me exactly. “maybe we’ve taken away your access” is not confidence inspiring.</div>
<h2 class="hdr-with-anchor" id="go-improvement"><div>Go improvement</div><a class="header-anchor" href="#go-improvement">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>In Go, save should use <code>goimports</code> instead of plain <code>go fmt</code>. This automatically adds necessary imports.</div>
<div>A simple change that would be a significant improvement for Go developers.</div>
</div>
Advanced markdown processing in Go2023-03-11T00:00:00Ztag:blog.kowalczyk.info,2023-03-11:/article/cxn3/advanced-markdown-processing-in-go.html<div class="notion-page" id="cxn3">
<h1 class="hdr-with-anchor" id="using-gomarkdown-markdown-library"><div>Using gomarkdown/markdown library</div><a class="header-anchor" href="#using-gomarkdown-markdown-library">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>This article describes an advanced markdown processing in Go using <a target="_blank" href="https://github.com/gomarkdown/markdown">gomarkdown/markdown</a> library.</div>
<div>All the code examples are available at <a target="_blank" href="https://github.com/gomarkdown/markdown/tree/master/examples">https://github.com/gomarkdown/markdown/tree/master/examples</a></div>
<h1 class="hdr-with-anchor" id="basics-first"><div>Basics first</div><a class="header-anchor" href="#basics-first">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Here’s a good start for markdown => HTML conversion:</div>
<div style="position: relative"><pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">mdToHTML</span><span class="p">(</span><span class="nx">md</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">[]</span><span class="kt">byte</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// create markdown parser with extensions
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">extensions</span> <span class="o">:=</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">CommonExtensions</span> <span class="p">|</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">AutoHeadingIDs</span> <span class="p">|</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">NoEmptyLineBeforeBlock</span>
</span></span><span class="line"><span class="cl"> <span class="nx">p</span> <span class="o">:=</span> <span class="nx">parser</span><span class="p">.</span><span class="nf">NewWithExtensions</span><span class="p">(</span><span class="nx">extensions</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">doc</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="nx">md</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// create HTML renderer with extensions
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">htmlFlags</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nx">CommonFlags</span> <span class="p">|</span> <span class="nx">html</span><span class="p">.</span><span class="nx">HrefTargetBlank</span>
</span></span><span class="line"><span class="cl"> <span class="nx">opts</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nx">RendererOptions</span><span class="p">{</span><span class="nx">Flags</span><span class="p">:</span> <span class="nx">htmlFlags</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">renderer</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nf">NewRenderer</span><span class="p">(</span><span class="nx">opts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">markdown</span><span class="p">.</span><span class="nf">Render</span><span class="p">(</span><span class="nx">doc</span><span class="p">,</span> <span class="nx">renderer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><a target="_blank" href="https://onlinetool.io/goplayground/#txO7hJ-ibeU" class="tryonline">
Try it online
</a>
</div>
<div>Basic markdown syntax is very limited and there are many extensions that provide additional feature, like tables.</div>
<div>HTML render is customizable as well.</div>
<div>Here we create markdown parser and HTML renderer with common flags plus some extensions.</div>
<div>Here are all available options for parser and HTML renderer:</div>
<ul>
<li><a target="_blank" href="https://pkg.go.dev/github.com/gomarkdown/markdown/parser#Extensions">https://pkg.go.dev/github.com/gomarkdown/markdown/parser#Extensions</a> : they change how parser interprets markdown</li>
<li><a target="_blank" href="https://pkg.go.dev/github.com/gomarkdown/markdown/html#Flags">https://pkg.go.dev/github.com/gomarkdown/markdown/html#Flags</a> : they change generated HTML</li>
</ul>
<h1 class="hdr-with-anchor" id="advanced-processing"><div>Advanced processing</div><a class="header-anchor" href="#advanced-processing">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Markdown to HTML processing works like this:</div>
<ul>
<li><code>github.com/gomarkdown/markdown/parser</code> parses markdown and generates AST tree as defined in <code>github.com/gomarkdown/markdown/ast</code></li>
<li><code>github.com/gomarkdown/markdown/html</code> implements HTML renderer that takes AST tree and generates HTML</li>
</ul>
<div>There are options for even more control:</div>
<ul>
<li>customize HTML generator by providing <code>html.Renderer.RenderNodeHook</code>. You re-use most of the <code>html.Renderer</code> and change rendering of just some <code>ast.Node</code>.</li>
<li>fork <code>github.com/gomarkdown/markdown/html</code> and make changes you want to HTML renderer</li>
<li>modify ast tree after parsing but before rendering</li>
<li>customize the parser, define your own <code>ast.Node</code> types, add them to the tree while parsing and customize renderer to render those nodes as you want</li>
<li>pre-process markdown before sending to the parser</li>
</ul>
<h2 class="hdr-with-anchor" id="ast-node"><div><code>ast.Node</code></div><a class="header-anchor" href="#ast-node">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You need to understand AST tree. Start by skimming <a target="_blank" href="https://github.com/gomarkdown/markdown/blob/master/ast/node.go">https://github.com/gomarkdown/markdown/blob/master/ast/node.go</a>.</div>
<div><code>ast.Node</code> is an interface so you can create your own nodes as long as you implement the interface.</div>
<div>The are two types of nodes:</div>
<ul>
<li>container node has an array of children nodes e.g. a <code>List</code> contains <code>ListItem</code> nodes</li>
<li>a leaf node doesn’t have children, just content</li>
</ul>
<div>We have <code>ast.Leaf</code> and <code>ast.Container</code> to make it easy to implement custom nodes:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">MyCustomLeafNode</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">ast</span><span class="p">.</span><span class="nx">Leaf</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// .. additional data for this node
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">MyCustomContainerNode</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">ast</span><span class="p">.</span><span class="nx">Container</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// .. additional data for this node
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre>
<div>To render your custom node <code>ast.Node</code> to HTML you provide a render hook function that will be structured like this:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">myRenderHook</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">node</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">WalkStatus</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// you must render custom nodes because html.Renderer doesn't understand them
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="nx">leafNode</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">node</span><span class="p">.(</span><span class="o">*</span><span class="nx">MyCustomLeafNode</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nf">renderMyLeafNode</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">lefNode</span><span class="p">,</span> <span class="nx">entering</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">containerNode</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">node</span><span class="p">.(</span><span class="o">*</span><span class="nx">MyCustomContainerNode</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nf">renderMyContainerNode</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">containerNode</span><span class="p">,</span> <span class="nx">entering</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// you can also over-ride rendering of some specific nodes that html.Renderer would render
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="nx">image</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">node</span><span class="p">.(</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Image</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nf">renderImage</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">image</span><span class="p">,</span> <span class="nx">entering</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// return false to tell html.Renderer to use default render
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>You should always return <code>ast.GoToNext</code>.</div>
<div>You return <code>true</code> to indicate that you’ve rendered the node. Returning <code>false</code> tells <code>html.Renderer</code> to use default rendering.</div>
<div>For container nodes we typically need to render something before rendering children and after rendering children.</div>
<div>That’s why we need <code>entering</code> argument. The simplest rendering of <code>*ast.Paragraph</code> would be:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">myRenderParagraph</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">p</span> <span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Paragraph</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">entering</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">io</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">"<p>"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">io</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">"</p>"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div><code>html.Renderer</code> takes care of recursively rendering children.</div>
<div>For <code>ast.Leaf</code> nodes you only render on entering:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">myHr</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">p</span> <span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">HorizontalRule</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">entering</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">io</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">"<hr/>"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Skim <a target="_blank" href="https://github.com/gomarkdown/markdown/blob/master/html/renderer.go">render.go</a> to see how different nodes are rendered to HTML.</div>
<h2 class="hdr-with-anchor" id="customizing-html-renderer"><div>Customizing HTML renderer</div><a class="header-anchor" href="#customizing-html-renderer">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>To re-use most of <code>html.Renderer</code> but only over-ride rendering of a few nodes, you can provide a render hook.</div>
<div>Here’s an example of a simple hook that renders <code><div>{children}</div></code> instead of <code><p>{children}</p></code> for a <code>*ast.Paragraph</code> node.</div>
<div style="position: relative"><pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="c1">// an actual rendering of Paragraph is more complicated
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">renderParagraph</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">p</span> <span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Paragraph</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">entering</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">io</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">"<div>"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">io</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">"</div>"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">myRenderHook</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">node</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">WalkStatus</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">para</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">node</span><span class="p">.(</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Paragraph</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nf">renderParagraph</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">para</span><span class="p">,</span> <span class="nx">entering</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">newCustomizedRender</span><span class="p">()</span> <span class="o">*</span><span class="nx">html</span><span class="p">.</span><span class="nx">Renderer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">opts</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nx">RendererOptions</span><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">RenderNodeHook</span><span class="p">:</span> <span class="nx">myRenderHook</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">html</span><span class="p">.</span><span class="nf">NewRenderer</span><span class="p">(</span><span class="nx">opts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><a target="_blank" href="https://onlinetool.io/goplayground/#yFRIWRiu-KL" class="tryonline">
Try it online
</a>
</div>
<div>If a render hook needs access to more information than <code>io.Writer</code> and <code>ast.Node</code>, we can capture it in a closure:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/gomarkdown/markdown/html"</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">renderData</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... data needed for render hook function
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">makeRenderHook</span><span class="p">(</span><span class="nx">data</span> <span class="o">*</span><span class="nx">renderData</span><span class="p">)</span> <span class="nx">html</span><span class="p">.</span><span class="nx">RenderNodeFunc</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">myRenderHook</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">node</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">WalkStatus</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// has access to data
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">newCustomizedRender</span><span class="p">()</span> <span class="o">*</span><span class="nx">html</span><span class="p">.</span><span class="nx">Renderer</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">data</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">renderData</span><span class="p">{}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">opts</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nx">RendererOptions</span><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">RenderNodeHook</span><span class="p">:</span> <span class="nf">makeRenderHook</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">html</span><span class="p">.</span><span class="nf">NewRenderer</span><span class="p">(</span><span class="nx">opts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="modify-ast-tree"><div>Modify ast tree</div><a class="header-anchor" href="#modify-ast-tree">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The structure of the code would be:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">modifyAst</span><span class="p">(</span><span class="nx">root</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">)</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... tweak AST tree as needed
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nx">root</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">mds</span> <span class="p">=</span> <span class="s">`[link](http://example.com)`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">modifyAstExample</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">md</span> <span class="o">:=</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">mds</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">extensions</span> <span class="o">:=</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">CommonExtensions</span>
</span></span><span class="line"><span class="cl"> <span class="nx">p</span> <span class="o">:=</span> <span class="nx">parser</span><span class="p">.</span><span class="nf">NewWithExtensions</span><span class="p">(</span><span class="nx">extensions</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">doc</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="nx">md</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">doc</span> <span class="p">=</span> <span class="nf">modifyAst</span><span class="p">(</span><span class="nx">doc</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">htmlFlags</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nx">CommonFlags</span>
</span></span><span class="line"><span class="cl"> <span class="nx">opts</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nx">RendererOptions</span><span class="p">{</span><span class="nx">Flags</span><span class="p">:</span> <span class="nx">htmlFlags</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">renderer</span> <span class="o">:=</span> <span class="nx">html</span><span class="p">.</span><span class="nf">NewRenderer</span><span class="p">(</span><span class="nx">opts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">html</span> <span class="o">:=</span> <span class="nx">markdown</span><span class="p">.</span><span class="nf">Render</span><span class="p">(</span><span class="nx">doc</span><span class="p">,</span> <span class="nx">renderer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">"-- Markdown:\n%s\n\n--- HTML:\n%s\n"</span><span class="p">,</span> <span class="nx">md</span><span class="p">,</span> <span class="nx">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>You can re-arrange the tree: add, remove, rearrange nodes or add / remove information from nodes.</div>
<div>Working with trees is tricky so you’ll probably use <code>ast.Print(io.Writer, ast.Node)</code> to pretty-print and understand AST tree, both before and after making changes.</div>
<div>I won’t cover changing the structure of the tree but here’s an example that adds <code>target="_blank"</code> attribute to link (<code><a></code>) nodes that go outside of my website and adds custom <code>blog-img</code> class to all image (<code><img></code>) nodes.</div>
<div style="position: relative"><pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">modifyAst</span><span class="p">(</span><span class="nx">doc</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">)</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">ast</span><span class="p">.</span><span class="nf">WalkFunc</span><span class="p">(</span><span class="nx">doc</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">node</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">WalkStatus</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">img</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">node</span><span class="p">.(</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Image</span><span class="p">);</span> <span class="nx">ok</span> <span class="o">&&</span> <span class="nx">entering</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">attr</span> <span class="o">:=</span> <span class="nx">img</span><span class="p">.</span><span class="nx">Attribute</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">attr</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">attr</span> <span class="p">=</span> <span class="o">&</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Attribute</span><span class="p">{}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// TODO: might be duplicate
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">attr</span><span class="p">.</span><span class="nx">Classes</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">attr</span><span class="p">.</span><span class="nx">Classes</span><span class="p">,</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">"blog-img"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="nx">img</span><span class="p">.</span><span class="nx">Attribute</span> <span class="p">=</span> <span class="nx">attr</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">link</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">node</span><span class="p">.(</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Link</span><span class="p">);</span> <span class="nx">ok</span> <span class="o">&&</span> <span class="nx">entering</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">isExternalURI</span> <span class="o">:=</span> <span class="kd">func</span><span class="p">(</span><span class="nx">uri</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">(</span><span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="s">"https://"</span><span class="p">)</span> <span class="o">||</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="s">"http://"</span><span class="p">))</span> <span class="o">&&</span> <span class="p">!</span><span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="s">"blog.kowalczyk.info"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nf">isExternalURI</span><span class="p">(</span><span class="nb">string</span><span class="p">(</span><span class="nx">link</span><span class="p">.</span><span class="nx">Destination</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">link</span><span class="p">.</span><span class="nx">AdditionalAttributes</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">link</span><span class="p">.</span><span class="nx">AdditionalAttributes</span><span class="p">,</span> <span class="s">`target="_blank"`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span>
</span></span><span class="line"><span class="cl"> <span class="p">})</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">doc</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><a target="_blank" href="https://onlinetool.io/goplayground/#2yV5-HDKBUV" class="tryonline">
Try it online
</a>
</div>
<div>Important parts:</div>
<ul>
<li><code>ast.WalkFunc</code> is a helper function that recursively calls a callback function on every node in AST</li>
<li>we do modifications only once e.g. when <code>entering</code> is true</li>
<li>all <code>Container</code> nodes have (optional) <code>*Attribute</code> which contains HTML id attribute, class names and attributes, which we can modify</li>
<li>some nodes can have additional data we can modify e.g. <code>*ast.Link</code> has <code>AdditionalAttributes</code> which is array of attributes.</li>
</ul>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Attribute</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="p">[]</span><span class="kt">byte</span>
</span></span><span class="line"><span class="cl"> <span class="nx">Classes</span> <span class="p">[][]</span><span class="kt">byte</span>
</span></span><span class="line"><span class="cl"> <span class="nx">Attrs</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">][]</span><span class="kt">byte</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="custom-markdown-parser-custom-ast-node"><div>Custom markdown parser, custom <code>ast.Node</code></div><a class="header-anchor" href="#custom-markdown-parser-custom-ast-node">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You can also extend parser to recognize additional syntax, not present in markdown.</div>
<div>Here’s an example of a parser extension that recognizes the following:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">:gallery
</span></span><span class="line"><span class="cl">/img/image-1.png
</span></span><span class="line"><span class="cl">/img/image-2.png
</span></span><span class="line"><span class="cl">/img/image-3.png
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rest of document.
</span></span></code></pre>
<div>A gallery is a list of image urls. In generated HTML this will be a show as an image gallery.</div>
<div>First define a custom leaf node type:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Gallery</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">ast</span><span class="p">.</span><span class="nx">Leaf</span>
</span></span><span class="line"><span class="cl"> <span class="nx">ImageURLS</span> <span class="p">[]</span><span class="kt">string</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Then customize parser with a block parsing hook function which gets first dibs at parsing block text.</div>
<div>Block means: it only gets called at the beginning of each text block. Text blocks are separated by newlines.</div>
<div>It’s not possible to do custom inline text parsing.</div>
<div>If parser hook function recognizes its custom syntax, it returns <code>ast.Node</code> it generated (<code>Gallery</code> in this case), number of bytes consumed and inner content to be recursively parsed and inserted as children of <code>ast.Container</code> node.</div>
<div>Number of bytes consumed allows parser to skip the part parsed by your custom hook.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">parserHook</span><span class="p">(</span><span class="nx">data</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">node</span><span class="p">,</span> <span class="nx">d</span><span class="p">,</span> <span class="nx">n</span> <span class="o">:=</span> <span class="nf">parseGallery</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="nx">node</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">node</span><span class="p">,</span> <span class="nx">d</span><span class="p">,</span> <span class="nx">n</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">nil</span><span class="p">,</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">newMarkdownParser</span><span class="p">()</span> <span class="o">*</span><span class="nx">parser</span><span class="p">.</span><span class="nx">Parser</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">extensions</span> <span class="o">:=</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">CommonExtensions</span>
</span></span><span class="line"><span class="cl"> <span class="nx">p</span> <span class="o">:=</span> <span class="nx">parser</span><span class="p">.</span><span class="nf">NewWithExtensions</span><span class="p">(</span><span class="nx">extensions</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">Opts</span><span class="p">.</span><span class="nx">ParserHook</span> <span class="p">=</span> <span class="nx">parserHook</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">p</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Here’s the parser for <code>:gallery</code> syntax.</div>
<div>If current text starts with <code>:gallery\n</code> we expect a list of urls followed by an empty line.</div>
<div style="position: relative"><pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">gallery</span> <span class="p">=</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">":gallery\n"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">parseGallery</span><span class="p">(</span><span class="nx">data</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">bytes</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="nx">gallery</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">nil</span><span class="p">,</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">i</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="nx">gallery</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// find empty line that ends the block
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// TODO: should also consider end of document
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">end</span> <span class="o">:=</span> <span class="nx">bytes</span><span class="p">.</span><span class="nf">Index</span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span><span class="p">:],</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">"\n\n"</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">end</span> <span class="p"><</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">data</span><span class="p">,</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">end</span> <span class="p">=</span> <span class="nx">end</span> <span class="o">+</span> <span class="nx">i</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// split into lines, each line is an image URL
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">lines</span> <span class="o">:=</span> <span class="nb">string</span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="nx">i</span><span class="p">:</span><span class="nx">end</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"> <span class="nx">parts</span> <span class="o">:=</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Split</span><span class="p">(</span><span class="nx">lines</span><span class="p">,</span> <span class="s">"\n"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">res</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">Gallery</span><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">ImageURLS</span><span class="p">:</span> <span class="nx">parts</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">res</span><span class="p">,</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">end</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><a target="_blank" href="https://onlinetool.io/goplayground/#9fqKwRbuJ04" class="tryonline">
Try it online
</a>
</div>
<div>I will not cover how it gets rendered as HTML because it’s a very custom solution with lots of HTML obscuring the big picture.</div>
<h2 class="hdr-with-anchor" id="syntax-highlighting"><div>Syntax highlighting</div><a class="header-anchor" href="#syntax-highlighting">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Code blocks are much better with syntax highlighting.</div>
<div>We can use <a target="_blank" href="https://github.com/alecthomas/chroma">github.com/alecthomas/chroma</a> library to generate HTML with syntax highlighting for many languages.</div>
<div>We hook it up to HTML renderer with render hook function like described above.</div>
<div style="position: relative"><pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="s">"fmt"</span>
</span></span><span class="line"><span class="cl"> <span class="s">"io"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/gomarkdown/markdown"</span>
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/gomarkdown/markdown/ast"</span>
</span></span><span class="line"><span class="cl"> <span class="nx">mdhtml</span> <span class="s">"github.com/gomarkdown/markdown/html"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/alecthomas/chroma"</span>
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/alecthomas/chroma/formatters/html"</span>
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/alecthomas/chroma/lexers"</span>
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/alecthomas/chroma/styles"</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="nx">htmlFormatter</span> <span class="o">*</span><span class="nx">html</span><span class="p">.</span><span class="nx">Formatter</span>
</span></span><span class="line"><span class="cl"> <span class="nx">highlightStyle</span> <span class="o">*</span><span class="nx">chroma</span><span class="p">.</span><span class="nx">Style</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">htmlFormatter</span> <span class="p">=</span> <span class="nx">html</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">html</span><span class="p">.</span><span class="nf">WithClasses</span><span class="p">(</span><span class="kc">true</span><span class="p">),</span> <span class="nx">html</span><span class="p">.</span><span class="nf">TabWidth</span><span class="p">(</span><span class="mi">2</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">htmlFormatter</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="s">"couldn't create html formatter"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">styleName</span> <span class="o">:=</span> <span class="s">"monokailight"</span>
</span></span><span class="line"><span class="cl"> <span class="nx">highlightStyle</span> <span class="p">=</span> <span class="nx">styles</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">styleName</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">highlightStyle</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">"didn't find style '%s'"</span><span class="p">,</span> <span class="nx">styleName</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// based on https://github.com/alecthomas/chroma/blob/master/quick/quick.go
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">htmlHighlight</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">source</span><span class="p">,</span> <span class="nx">lang</span><span class="p">,</span> <span class="nx">defaultLang</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">lang</span> <span class="o">==</span> <span class="s">""</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lang</span> <span class="p">=</span> <span class="nx">defaultLang</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">l</span> <span class="o">:=</span> <span class="nx">lexers</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">lang</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">l</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">l</span> <span class="p">=</span> <span class="nx">lexers</span><span class="p">.</span><span class="nf">Analyse</span><span class="p">(</span><span class="nx">source</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">l</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">l</span> <span class="p">=</span> <span class="nx">lexers</span><span class="p">.</span><span class="nx">Fallback</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">l</span> <span class="p">=</span> <span class="nx">chroma</span><span class="p">.</span><span class="nf">Coalesce</span><span class="p">(</span><span class="nx">l</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">l</span><span class="p">.</span><span class="nf">Tokenise</span><span class="p">(</span><span class="kc">nil</span><span class="p">,</span> <span class="nx">source</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">htmlFormatter</span><span class="p">.</span><span class="nf">Format</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">highlightStyle</span><span class="p">,</span> <span class="nx">it</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// an actual rendering of Paragraph is more complicated
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">renderCode</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">codeBlock</span> <span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">CodeBlock</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">defaultLang</span> <span class="o">:=</span> <span class="s">""</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lang</span> <span class="o">:=</span> <span class="nb">string</span><span class="p">(</span><span class="nx">codeBlock</span><span class="p">.</span><span class="nx">Info</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nf">htmlHighlight</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">codeBlock</span><span class="p">.</span><span class="nx">Literal</span><span class="p">),</span> <span class="nx">lang</span><span class="p">,</span> <span class="nx">defaultLang</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">myRenderHook</span><span class="p">(</span><span class="nx">w</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Writer</span><span class="p">,</span> <span class="nx">node</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">entering</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">(</span><span class="nx">ast</span><span class="p">.</span><span class="nx">WalkStatus</span><span class="p">,</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">code</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">node</span><span class="p">.(</span><span class="o">*</span><span class="nx">ast</span><span class="p">.</span><span class="nx">CodeBlock</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nf">renderCode</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">code</span><span class="p">,</span> <span class="nx">entering</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ast</span><span class="p">.</span><span class="nx">GoToNext</span><span class="p">,</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><a target="_blank" href="https://onlinetool.io/goplayground/#Bk0zTvrzUDR" class="tryonline">
Try it online
</a>
</div>
<div>You’ll have to include chroma CSS as HTML generation marks up nodes with chroma CSS classes.</div>
<h2 class="hdr-with-anchor" id="pre-process-markdown-before-parsing"><div>Pre-process markdown before parsing</div><a class="header-anchor" href="#pre-process-markdown-before-parsing">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Imagine you want to add ability to include markdown files.</div>
<div>You can build a parser extension that e.g. recognizes this syntax:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">@include "foo/bar.md"
</span></span></code></pre>
<div>It’ll get complicated if you want to add more advanced functionality, like loops, variables etc.</div>
<div>But <a target="_blank" href="https://pkg.go.dev/text/template"><code>template/text</code></a> library already implement such features.</div>
<div>You can pre-process markdown with one of the many templating Go libraries before sending it to the parser.</div>
</div>
Persisted Svelte store using IndexedDB2023-03-09T00:00:00Ztag:blog.kowalczyk.info,2023-03-09:/article/persisted-svelte-store-indexeddb.html<div class="notion-page" id="5res">
<div>I’m working on <a target="_blank" href="https://onlinetool.io/notepad2/">notepad2 for web</a> and I need a history of opened files that persists across browser session.</div>
<div>Since I’m using <a target="_blank" href="https://svelte.dev/">Svelte</a>, having it available as a store makes sense.</div>
<div>This article describes how to implement a <a target="_blank" href="https://svelte.dev/docs#run-time-svelte-store">Svelte store</a> whose values are persisted in <code>IndexedDB</code>.</div>
<h2 class="hdr-with-anchor" id="what-is-svelte-store"><div>What is Svelte store?</div><a class="header-anchor" href="#what-is-svelte-store">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The simplest Svelte store is an object with <code>subscribe</code> function.</div>
<div>You call <code>subscribe</code> to provide a callback function that will be called when the value changes:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">makeSvelteStoreFoo</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">foo</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"New value of foo is:"</span><span class="p">,</span> <span class="nx">v</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre>
<div>Svelte provides a nicer, less verbose syntax to use stores:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">makeSvelteStoreFoo</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Value of foo is:"</span><span class="p">,</span> <span class="nx">$foo</span><span class="p">);</span>
</span></span></code></pre>
<div>Under the covers Svelte calls <code>subscribe</code> and makes it so <code>$foo</code> returns the latest value.</div>
<div>The <code>$foo</code> value is reactive so if you use it in a component like <code><div>{$foo}</div></code>, it’ll be automatically re-rendered when the value changes.</div>
<h2 class="hdr-with-anchor" id="creating-a-store"><div>Creating a store</div><a class="header-anchor" href="#creating-a-store">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Typically stores are created by a function returning a store object:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeSvelteStoreFoo</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">subscribe</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="p">...</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">unsubscribe</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">unsubscribe</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">{</span><span class="nx">subscriber</span><span class="o">:</span> <span class="nx">subscriber</span><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Function <code>subscribe</code> returns <code>unsubscribe</code> function to call when you no longer need to observe the changes to the value.</div>
<div>Svelte does that for you if you use <code>$foo</code> syntax.</div>
<h2 class="hdr-with-anchor" id="writable-store"><div>Writable store</div><a class="header-anchor" href="#writable-store">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The above store is read-only: it provides values but you can’t change it.</div>
<div>To make the store writable you also need to return <code>set(newValue)</code> function.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeSvelteStoreFoo</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">subscribe</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">set</span><span class="p">(</span><span class="nx">newValuse</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">{</span><span class="nx">subscriber</span><span class="o">:</span> <span class="nx">subscriber</span><span class="p">,</span> <span class="nx">set</span><span class="o">:</span> <span class="nx">set</span><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="global-store-vs-multiple-instances-of-store"><div>Global store vs. multiple instances of store</div><a class="header-anchor" href="#global-store-vs-multiple-instances-of-store">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Some stores can have multiple instances. In that case you’ll <code>export function makeSvelteStoreFoo()</code> and call it to get a new instance of the store.</div>
<div>Some stores are global and only should have one shared instance. In that case you’ll only export the single global instance:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeSvelteStoreFoo</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">let</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">makeSvelteStoreFoo</span><span class="p">();</span>
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="persisted-store-with-values-backed-by-indexeddb"><div>Persisted store with values backed by IndexedDB</div><a class="header-anchor" href="#persisted-store-with-values-backed-by-indexeddb">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>If you want your store to survive across browser sessions, you need to persist it somehow.</div>
<div>I like IndexedDB and lightweight <a target="_blank" href="https://www.npmjs.com/package/idb">idb</a> helper library.</div>
<div>IndexedDB supports more data types than <code>localStorage</code>, which only handles strings.</div>
<div>I create a single database for all my key-value needs and have each Svelte store persist values under unique database key.</div>
<div>Here’s a structure of such store:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeStore</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">set</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">subscribe</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">unsubscribe</span><span class="p">()</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">unsubscribe</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">{</span> <span class="nx">set</span><span class="p">,</span> <span class="nx">subscribe</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Let’s fill out the implementation.</div>
<div>First, we need a db for backing store:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">KV</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">"../dbutil"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">KV</span><span class="p">(</span><span class="s2">"np2store"</span><span class="p">,</span> <span class="s2">"keyval"</span><span class="p">);</span>
</span></span></code></pre>
<div>This is my global database for all key-value data, including my persisted Svelte store. See below for implementation of <code>KV</code>.</div>
<div>We use a unique database key for each store.</div>
<div>We have a variable that keeps the value in memory, which is more efficient that re-reading from database.</div>
<div>When creating a store we read the initial value from the database:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeStore</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">dbKey</span> <span class="o">=</span> <span class="s2">"browse-folders"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">curr</span> <span class="o">=</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">curr</span> <span class="o">=</span> <span class="nx">v</span> <span class="o">||</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl"> <span class="nx">broadcastValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>The initial value is <code>[]</code> because this store happens to store an array so we want the right “unset” value.</div>
<div><code>broadcastValue</code> informs all subscribers about the change in value of the store:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeStore</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">subscribers</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">broadcastValue</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscribers</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">cb</span><span class="p">)</span> <span class="p">=></span> <span class="nx">cb</span><span class="p">(</span><span class="nx">curr</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>We store subscriber callback functions in a <code>Set</code>. Here’s how <code>subscribe()</code> is implemented:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeStore</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">subscribers</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">subscribe</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscriber</span><span class="p">(</span><span class="nx">curr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscribers</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">unsubscribe</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscribers</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">unsubscribe</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>The contract for Svelte store is that upon subscription we need to synchronously call <code>susbscriber</code> callback to immediately provide the current value.</div>
<div>And finally the <code>set(newValue)</code> function:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeStore</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">set</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">curr</span> <span class="o">=</span> <span class="nx">v</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">broadcastValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">,</span> <span class="nx">v</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="changes-in-another-browser-tab"><div>Changes in another browser tab</div><a class="header-anchor" href="#changes-in-another-browser-tab">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>What if the value is changed in another browser tab?</div>
<div>If you open the same website twice in different tabs they both write to the same underlying database but they don’t know about each other changes and would over-write data changed by the other instance.</div>
<div>Turns out <code>localStorage</code> has a feature that allows us to fix that: we can monitor all changes to <code>localStorage</code> even if they are made by an instance in another tab.</div>
<div>I use unique <code>localStorage</code> value to notify all tabs about changes in our store.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeStore</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">dbKey</span> <span class="o">=</span> <span class="s2">"browse-folders"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">lsKey</span> <span class="o">=</span> <span class="s2">"store-notify:"</span> <span class="o">+</span> <span class="nx">dbKey</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">getCurrentValue</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">curr</span> <span class="o">=</span> <span class="nx">v</span> <span class="o">||</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl"> <span class="nx">broadcastValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">getCurrentValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {StorageEvent} event
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">storageChanged</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">storageArea</span> <span class="o">===</span> <span class="nx">localStorage</span> <span class="o">&&</span> <span class="nx">event</span><span class="p">.</span><span class="nx">key</span> <span class="o">===</span> <span class="nx">lsKey</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">getCurrentValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">"storage"</span><span class="p">,</span> <span class="nx">storageChanged</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">set</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">curr</span> <span class="o">=</span> <span class="nx">v</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">broadcastValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">,</span> <span class="nx">v</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// notify other browser tabs
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="o">+</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="nx">lsKey</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="nx">lsKey</span><span class="p">,</span> <span class="sb">`</span><span class="si">${</span><span class="nx">v</span> <span class="o">+</span> <span class="mi">1</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>Let’s dissect this tricky line:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="o">+</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="nx">lsKey</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">;</span>
</span></span></code></pre>
<div>To ensure the value changes, we implement a numeric counter. <code>localStorage</code> stores strings, so we need to convert string => number on reading and number => string on writing.</div>
<div><code>+foo</code> converts whatever <code>foo</code> is to a number. If <code>foo</code> is a string <code>"42"</code>, <code>+foo</code> returns number <code>42</code>.</div>
<div>For non-number strings it returns <code>NaN</code> (a special number value indicating Not A Number).</div>
<div>We do <code>NaN || 0</code> to convert <code>NaN</code> to <code>0</code>.</div>
<h2 class="hdr-with-anchor" id="factory-function-to-easily-create-stores"><div>Factory function to easily create stores</div><a class="header-anchor" href="#factory-function-to-easily-create-stores">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You could now piece together the whole implementation and create your own stores based on this template.</div>
<div>Then you would notice that they share most of the code. The differences are:</div>
<ul>
<li>database key used to persist the value</li>
<li>initial value</li>
</ul>
<div>We can abstract this into a factory function that creates the store for a given key and initial value.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeIndexedDBStore</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">,</span> <span class="nx">initialValue</span><span class="p">,</span> <span class="nx">crossTabSync</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">makeIndexedDBStore</span><span class="p">(</span><span class="s2">"foo"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="nx">bar</span> <span class="o">=</span> <span class="nx">makeIndexedDBStore</span><span class="p">(</span><span class="s2">"bar"</span><span class="p">,</span> <span class="p">[],</span> <span class="kc">true</span><span class="p">);</span>
</span></span></code></pre>
<div>Here’s our <code>makeIndexedDBStore()</code>:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * Create a generic Svelte store persisted in IndexedDB
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {string} dbKey unique IndexedDB key for storing this value
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {any} initialValue
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {boolean} crossTab if true, changes are visible in other browser tabs (windows)
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @returns {any}
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">makeIndexedDBStore</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">,</span> <span class="nx">initialValue</span><span class="p">,</span> <span class="nx">crossTab</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">makeStoreMaker</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">,</span> <span class="nx">initialValue</span><span class="p">,</span> <span class="nx">crossTab</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">lsKey</span> <span class="o">=</span> <span class="s2">"store-notify:"</span> <span class="o">+</span> <span class="nx">dbKey</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">curr</span> <span class="o">=</span> <span class="nx">initialValue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">subscribers</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">getCurrentValue</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">curr</span> <span class="o">=</span> <span class="nx">v</span> <span class="o">||</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscribers</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">cb</span><span class="p">)</span> <span class="p">=></span> <span class="nx">cb</span><span class="p">(</span><span class="nx">curr</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">getCurrentValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {StorageEvent} event
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">storageChanged</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">storageArea</span> <span class="o">===</span> <span class="nx">localStorage</span> <span class="o">&&</span> <span class="nx">event</span><span class="p">.</span><span class="nx">key</span> <span class="o">===</span> <span class="nx">lsKey</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">getCurrentValue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">crossTab</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">"storage"</span><span class="p">,</span> <span class="nx">storageChanged</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">set</span><span class="p">(</span><span class="nx">v</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">curr</span> <span class="o">=</span> <span class="nx">v</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscribers</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">cb</span><span class="p">)</span> <span class="p">=></span> <span class="nx">cb</span><span class="p">(</span><span class="nx">curr</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">,</span> <span class="nx">v</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">v</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">crossTab</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">n</span> <span class="o">=</span> <span class="o">+</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="nx">lsKey</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="nx">lsKey</span><span class="p">,</span> <span class="sb">`</span><span class="si">${</span><span class="nx">n</span> <span class="o">+</span> <span class="mi">1</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param {Function} subscriber
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">subscribe</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscriber</span><span class="p">(</span><span class="nx">curr</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscribers</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span> <span class="nx">unsubscribe</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">subscribers</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">subscriber</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">unsubscribe</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">{</span> <span class="nx">set</span><span class="p">,</span> <span class="nx">subscribe</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">makeStoreMaker</span><span class="p">(</span><span class="nx">dbKey</span><span class="p">,</span> <span class="nx">initialValue</span><span class="p">,</span> <span class="nx">crossTab</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="kv-store"><div>KV store</div><a class="header-anchor" href="#kv-store">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Here’s a helper class for key-value store using <a target="_blank" href="https://www.npmjs.com/package/idb">idb</a> library.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">openDB</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">"idb"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">class</span> <span class="nx">KV</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">dbName</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">storeName</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">dbPromise</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">constructor</span><span class="p">(</span><span class="nx">dbName</span><span class="p">,</span> <span class="nx">storeName</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">this</span><span class="p">.</span><span class="nx">dbName</span> <span class="o">=</span> <span class="nx">dbName</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">this</span><span class="p">.</span><span class="nx">storeName</span> <span class="o">=</span> <span class="nx">storeName</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">this</span><span class="p">.</span><span class="nx">dbPromise</span> <span class="o">=</span> <span class="nx">openDB</span><span class="p">(</span><span class="nx">dbName</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">upgrade</span><span class="p">(</span><span class="nx">db</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">.</span><span class="nx">createObjectStore</span><span class="p">(</span><span class="nx">storeName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kr">async</span> <span class="nx">get</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">(</span><span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">dbPromise</span><span class="p">).</span><span class="nx">get</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">storeName</span><span class="p">,</span> <span class="nx">key</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kr">async</span> <span class="nx">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">val</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">(</span><span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">dbPromise</span><span class="p">).</span><span class="nx">put</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">storeName</span><span class="p">,</span> <span class="nx">val</span><span class="p">,</span> <span class="nx">key</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kr">async</span> <span class="nx">del</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">(</span><span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">dbPromise</span><span class="p">).</span><span class="k">delete</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">storeName</span><span class="p">,</span> <span class="nx">key</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kr">async</span> <span class="nx">clear</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">(</span><span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">dbPromise</span><span class="p">).</span><span class="nx">clear</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">storeName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kr">async</span> <span class="nx">keys</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">(</span><span class="kr">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">dbPromise</span><span class="p">).</span><span class="nx">getAllKeys</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">storeName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
</div>
Find programming work by increasing luck surface area2022-06-29T00:00:00Ztag:blog.kowalczyk.info,2022-06-29:/article/4682085af50b41f88460593d21db6182/find-programming-work-by-increasing-luck-surface-area.html<div class="notion-page" id="4682085af50b41f88460593d21db6182">
<div>Antonio asked on HN: <a target="_blank" href="https://news.ycombinator.com/item?id=31908797">How do I earn a small amount of money to sustain myself as a developer?</a></div>
<div>I wrote a response centered around <a target="_blank" href="https://news.ycombinator.com/item?id=31911728">increasing luck surface area</a>.</div>
<div>This essay expands on it because I’ve seen this a few times now: good developers asking how to find work while botching the basics.</div>
<div>First a caveat: it only works if you are a good programmer.</div>
<div>I’ve looked at Antonio’s <a target="_blank" href="https://github.com/astoilkov">GitHub account</a> and his <a target="_blank" href="https://astoilkov.com/">website</a> and I’m convinced he is a good programmer.</div>
<div>He created 3 non-trivial Mac OS apps, written in web technologies / React and packaged with Electron.</div>
<div>He wrote non-trivial open-source React libraries.</div>
<div>His code looks good.</div>
<div>He made good looking personal website and websites for this apps.</div>
<div>A developer writing good code with proven ability to ship products should be in high demand.</div>
<div>So why isn’t there a line of companies waiting to hire him?</div>
<h2 class="hdr-with-anchor" id="it-s-the-basics"><div>It’s the basics</div><a class="header-anchor" href="#it-s-the-basics">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I believe that many capable people don’t succeed as much as they could not because they fail to do something brilliant but because they fail the basics.</div>
<div>The basics they are not aware of. The unknown (to them) unknowns.</div>
<h2 class="hdr-with-anchor" id="basics-for-programmers-looking-for-freelance-consulting-jobs"><div>Basics for programmers looking for freelance / consulting jobs</div><a class="header-anchor" href="#basics-for-programmers-looking-for-freelance-consulting-jobs">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>If you’re a programmer looking for a freelance or consulting job, what are the absolute basics?</div>
<div>A potential employer should be able to learn about what you offer, at what price and why you’re the right person for their job.</div>
<div>Therefore you should have a decent website with a page that describes what work you can do, how much you charge and a proof that you’re good at what you do.</div>
<div>I’m no longer looking for work, because I’m currently working on <a target="_blank" href="https://filerion.com/">Filerion</a>, a web-based file manager for online storage like Dropbox and S3.</div>
<div>When I was looking for a job, I was promoting <a href="https://blog.kowalczyk.info/goconsultantforhire.html">I’m a Go consultant for hire</a> page.</div>
<div>I’m not claiming it’s the best self-promoting page ever.</div>
<div>I’m claiming that you get 80% of value from having a decent page in the first place. The remaining 20% would be in refining your pitch.</div>
<div>The important basics of such page:</div>
<ul>
<li>it should be short and concise; this is a decent business proposal not a novella</li>
<li>a way to contact you i.e. your e-mail address</li>
<li>describe what you can do. In my case it was programming in Go</li>
<li>provide proof that you’re good at it. In my case I linked to my past writing about Go, my open source Go libraries and a Go contract I successfully completed</li>
<li>social proof (list of well-known companies I worked at)</li>
<li>my time zone because it’s important in remote work</li>
<li>and a photo for that human connection</li>
</ul>
<div>Antonio created more than enough stuff to show that he is a capable developer. He should create a similar web page pitching himself as a web or React or Mac developer.</div>
<div>Here are other people’s pages:</div>
<ul>
<li><a target="_blank" href="https://www.johndcook.com/blog/contact/">https://www.johndcook.com/blog/contact/</a></li>
<li><a target="_blank" href="https://simonhearne.com/consultancy">https://simonhearne.com/consultancy</a></li>
</ul>
<h2 class="hdr-with-anchor" id="increasing-luck-surface-area"><div>Increasing luck surface area</div><a class="header-anchor" href="#increasing-luck-surface-area">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You went to a React meetup, started a conversation with a random person. You talked about your React project, that random person needed a React developer and you got a contract gig out of that conversation.</div>
<div>That’s luck.</div>
<div>I wouldn’t recommend striking conversations with random people at meetups as the best way for finding programming jobs.</div>
<div>You can’t control luck but you can increase your <a target="_blank" href="https://alchemist.camp/metacast/luck-surface-area">luck surface area</a>.</div>
<div>Did you notice how few paragraphs above I mentioned <a target="_blank" href="https://filerion.com/">Filerion</a>, a web-based file manager for online storage like Dropbox and S3?</div>
<div>It’s an example of increasing my luck surface area of promoting <a target="_blank" href="https://filerion.com/">Filerion</a>.</div>
<div>Oops, I did it again. It’s getting recursive in here.</div>
<div>This post started as a comment on HN and I decided to turn it into a blog post. I believe it’s useful for people so I’ll promote it on HN when completed. It might or might not get upvoted but I’ll increase my luck surface area by posting it there.</div>
<div>Most of the things will not lead to job offers but all you need is one job to make this worthwhile.</div>
<div>Listen to Wayne Gretzky: you miss 100% of the shots you don’t take.</div>
<div>Here are few ideas for increasing luck surface area or programmers looking for a freelance job:</div>
<ul>
<li>make a link to your “hire me” page very visible on your website. Don’t be afraid to make it “in your face” big. It should be on every page of your website and it should not be missed</li>
<li>put your pitch in every place that allows it. Just a short “Hire me for X. <link to hire me web page>”.
Where to put it?
<ul>
<li>your Hacker News profile</li>
<li>your Twitter profile</li>
<li>your GitHub profile</li>
<li>your linkedin profile</li>
<li>your reddit profile</li>
<li>a footnote on every page on your website</li>
<li>readme of every open source project you publish</li>
</ul></li>
<li>if appropriate, in your social comments. Every week there’s a HN discussion about terrible hiring practices in software. I’m sure you could make non-spammy comments that includes “BTW: if you’re looking for a expert X programmer, I’m available <link to your hire me web page>”
<ul>
<li>all that is low probability but also low effort and it does increase your luck surface area</li>
</ul></li>
<li>research or freelancing / remote jobs platforms and if they fit your desired job, create an account and start applying for jobs:
<ul>
<li>upwork</li>
<li>fiverr</li>
<li>remoteok.com</li>
<li>codementor</li>
<li>every jobs site and forum</li>
<li>sub-reddits like <a target="_blank" href="https://www.reddit.com/r/javascriptjobs/">https://www.reddit.com/r/javascriptjobs/</a></li>
<li>craigslist has a jobs section</li>
<li>use google to find more freelancing / contracting websites</li>
</ul></li>
</ul>
<h2 class="hdr-with-anchor" id="have-a-process"><div>Have a process</div><a class="header-anchor" href="#have-a-process">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>So you did all the low effort stuff described above. What’s next?</div>
<div>One of the things that separates professionals form amateurs is having a process.</div>
<div>Marketing yourself as a contractor is no different that marketing anything else:</div>
<ul>
<li>create leads</li>
<li>convert leads into clients</li>
</ul>
<div>The “hire me” web page is how you convert leads into clients.</div>
<div>How do you get more leads?</div>
<div>Professional marketers have a process.</div>
<div>A set of known techniques for generating leads: buy tv ads, but newspaper ads, buy online ads, pay influencers to promote your stuff, throw a conference, publish a white paper, promote on social media.</div>
<div>Professional marketers are not paid because only they know how to buy ads on Google.</div>
<div>They are paid for knowing wide array of techniques, knowing which technique applies in a given context and simply spending hours doing the work and being better at it that someone who isn’t doing it full time.</div>
<div>Here’s how programmers looking for a programming jobs can generate leads.</div>
<h3 class="hdr-with-anchor" id="publish-technical-articles"><div>Publish technical articles</div><a class="header-anchor" href="#publish-technical-articles">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Write articles on technologies you’re an expert on and promote them on relevant social media.</div>
<div>The process is:</div>
<ul>
<li>write an article and publish on your website (or medium or <a target="_blank" href="http://dev.to">dev.to</a> or all the above)</li>
<li>the article must create value. Self promotion can only be a cherry on top, not the sundae</li>
<li>tweet about it</li>
<li>promote on relevant forums and websites</li>
</ul>
<div>I used to write articles about Go and <a target="_blank" href="https://www.reddit.com/user/kjk/?sort=top">promote them on r/golang</a>. Some of them would end up on Hacker News even without me submitting them.</div>
<h3 class="hdr-with-anchor" id="publish-open-source-packages"><div>Publish open source packages</div><a class="header-anchor" href="#publish-open-source-packages">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>More time consuming that writing an article but if you’re a full time programmer, especially in JavaScript / React ecosystem, you can often extract bits and pieces of functionality as open source packages.</div>
<div>In package readme link to your “hire me” web page.</div>
<div>Provide value, increase your luck surface area.</div>
<div>Don’t forget to promote the packages the same way you promote articles (tweet about it, post in relevant reddit groups etc.).</div>
<h3 class="hdr-with-anchor" id="build-online-tools"><div>Build online tools</div><a class="header-anchor" href="#build-online-tools">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Build a tool / program that provides value and link to your “hire me” web page.</div>
<div>This takes more effort than writing an article but if it provides value, it will be a constant source of new leads.</div>
<div>See this <a target="_blank" href="https://www.johndcook.com/interpolator.html">linear interpolator</a> for an example of a tool that wasn’t hard to build and has a good call to action.</div>
<h3 class="hdr-with-anchor" id="turn-experience-into-lead-generating-artifacts"><div>Turn experience into lead generating artifacts</div><a class="header-anchor" href="#turn-experience-into-lead-generating-artifacts">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>All this seems like a lot of work.</div>
<div>If you’re a full-time programmer you’re doing a lot of programming.</div>
<div>That work is experience.</div>
<div>You can sometimes cheaply turn that experience into artifacts.</div>
<div>Here are examples of how I turned experience into lead generating artifacts:</div>
<ul>
<li>I did a contract job porting a large Java library to Go. I spent 600 hours on it. After the contract was finished I spend a few hours more to write <a href="https://blog.kowalczyk.info/article/19f2fe97f06a47c3b1f118fd06851fad/lessons-learned-porting-50k-loc-from-java-to-go.html">Lessons learned porting 50k loc from Java to Go</a> <a target="_blank" href="http://article.It">article</a>. It was very popular on both Reddit and Hacker News. I spent 600 hours doing work and gaining valuable experience. The cost to writing article was, comparatively speaking, very low. Just a few hours to get thousands of people to read it. You can see that majority of my <a href="https://blog.kowalczyk.info/archives.html">recent articles</a> follows this pattern of turning experience into articles</li>
<li>I had an idea of using Notion as CMS for my blog. Notion at the time didn’t offer the API so I reverse engineered their protocol and wrote Go code to build a blog out of content in Notion. It took many, many hours. I spent a fraction of that time to generate lead generation artifacts from that experience
<ul>
<li>an <a href="https://blog.kowalczyk.info/article/88aee8f43620471aa9dbcad28368174c/how-i-reverse-engineered-notion-api.html">article about the process of reverse engineering the API</a></li>
<li>an <a target="_blank" href="https://github.com/kjk/notionapi">open-source library</a> for accessing Notion API. Again, I already spent many hours doing the work of reverse engineering and writing the code. It took relatively small amount of additional hours to extract that into an open source library</li>
</ul></li>
</ul>
<div>Look back at your own past programming work.</div>
<div>Can you write an article about something your did?</div>
<div>Can you extract part of the code as an open-source library?</div>
<div>Can you summarize your experience and provide insight?</div>
<h3 class="hdr-with-anchor" id="goto-1"><div>Goto 1</div><a class="header-anchor" href="#goto-1">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Notice that this is now a process: a set of actions you can repeat over and over again.</div>
<div>There’s an infinite number of useful articles, open source packages or online tools that you can create.</div>
<div>The only limit to how many leads you can generate using this process is the number of hours you’re willing to spend.</div>
<div>Don’t do it just once.</div>
<div>Set aside a certain number of hours per week dedicated to marketing yourself.</div>
<div>A decent goal would be to write and promote one article per week.</div>
<h3 class="hdr-with-anchor" id="stay-focused"><div>Stay focused</div><a class="header-anchor" href="#stay-focused">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>If you’re selling yourself as a Go programmer, all your articles, open source packages and tools should be related to Go.</div>
<div>Not Java, not Rust. Until you have more job offers than you can handle, everything you write should plausibly generate leads for your Go software contracting.</div>
<div>I’m sure many people will look at the above suggestions and say: that sounds like a lot of work, I don’t have that kind of time.</div>
<div>Many of those people browse Twitter for hours, write Hacker News comments that will be forgotten in a day etc.</div>
<div>Just recently a gentleman posted their super busy schedule of a working programmer and a father as a comment on Hacker News. He was trying to show how it would be impossible for him to do a take home coding assignment when looking for a job due to lack of time.</div>
<div>I’m not defending take home assignments but somehow his schedule didn’t include the time he wasted reading and commenting on Hacker News. And I’m pretty sure there’s a little bit of Twitter or Reddit or Facebook usage in there as well. Not to mention the 3 hours of tv watching of an average American adult.</div>
<div>A lot of people could wrangle a few hours a week by cutting down on social media or tv and use those hours for a focused self promotion activities.</div>
<h2 class="hdr-with-anchor" id="job-search-as-a-problem-to-be-solved"><div>Job search as a problem to be solved</div><a class="header-anchor" href="#job-search-as-a-problem-to-be-solved">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>We, the developers, are a problem solving machine.</div>
<div>We encounter problems daily and we solve them.</div>
<div>How do we solve them? Mostly by formulating the right question and entering it, verbatim, into a search engine.</div>
<div>For <a target="_blank" href="https://filerion.com/">Filerion</a>, my web-based file manager for online storage like Dropbox and S3 I needed to implement file upload via drag & drop.</div>
<div>I knew it’s possible because I’ve used many websites that are doing it but I had no clue how to do it.</div>
<div>How did I figure it out? I asked Google: <a target="_blank" href="https://www.google.com/search?q=javascript+drag+and+drop+file&oq=javascript+drag+and+drop+file&aqs=chrome..69i57j0i512l3j0i22i30l6.8318j0j7&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIHCAEQABiABDIHCAIQABiABDIHCAMQABiABDIICAQQABgWGB4yCAgFEAAYFhgeMggIBhAAGBYYHjIICAcQABgWGB4yCAgIEAAYFhgeMggICRAAGBYYHtIBCDgzMThqMGo3qAIAsAIA&sourceid=chrome&ie=UTF-8">“javascript drag and drop file”</a>.</div>
<div>What do you know: plenty of people have already solved that problem and wrote tutorials. All I had to do is to follow them and do the work.</div>
<h3 class="hdr-with-anchor" id="a-simple-matter-of-asking-the-right-question"><div>A simple matter of asking the right question</div><a class="header-anchor" href="#a-simple-matter-of-asking-the-right-question">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>This is my foolproof system of solving any problem:</div>
<ul>
<li>formulate a question</li>
<li>enter it into a search engine</li>
</ul>
<div>I’m only half joking. 80% of the work is asking the right question.</div>
<div>Antonio did the first step by formulating the question and asking on Hacker News.</div>
<div>He got a bunch of good suggestions, but he would get even more simply by entering similar questions into a search engine.</div>
<div>“How to get a freelance job”. “successful freelancer”. “successful contracting”. “increase contracting rates”. “successful upwork freelancing”.</div>
<div>This is just a sampling of possible questions related to getting a job as a freelancer / contractor / consultant.</div>
<h2 class="hdr-with-anchor" id="specialize"><div>Specialize</div><a class="header-anchor" href="#specialize">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>It’s counter-intuitive but you should specialize.</div>
<div>When you’re looking for a programming contract it’s better to narrow down your pitch.</div>
<div>“Backend developer” is better than “Developer” or “Full-stack developer”</div>
<div>“Go backend developer” is better than “Backend Developer” or “Go developer”.</div>
<div>“MySQL query optimization expert” is better than “Go backend developer”.</div>
<div>It’s counter-intuitive because why would you want to appeal to less potential employers?</div>
<div>If there is a million jobs for Java and a million jobs for JavaScript, isn’t it better to pitch yourself as a Java OR JavaScript developer?</div>
<div>No, it isn’t.</div>
<div>You only need one job. One employer.</div>
<div>Someone looking for a Java developer will likely pick someone who pitches themselves as expert in Java, not a Java / JavaScript expert.</div>
<div>Same for someone looking for a JavaScript developer.</div>
<div>Your pitch targets less potential employers but it has a much greater chance of appealing to that particular employer.</div>
<div>Furthermore, if you have 10 hours to invest, it’s better to invest 10 hours into showcasing you JavaScript skills than split into 5 hours of showing Java skills and 5 hours of showing JavaScript skills.</div>
<h2 class="hdr-with-anchor" id="level-up"><div>Level up</div><a class="header-anchor" href="#level-up">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>There’s an abundance of free or cheap advice on improving your freelancing game.</div>
<div>Blog posts, books, YouTube videos, Udemy courses, podcasts.</div>
<div>Just today on HN there’s a link to an <a target="_blank" href="https://sinews.siam.org/Details-Page/a-conversation-with-mathematical-consultant-john-d-cook">interview with successful consultant</a>.</div>
<div>You don’t have to wait for material like this to serendipitously show up on HN.</div>
<div>Formulate a question and enter it into a search engine or YouTube or Amazon website (for books) or podcast search engine.</div>
<h2 class="hdr-with-anchor" id="kaizen"><div>Kaizen</div><a class="header-anchor" href="#kaizen">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The Japanese have the best words. One of those words is kaizen. The idea of continuous improvement.</div>
<div>The above suggestions might be overwhelming and seem like too much work to do at once.</div>
<div>Start with the basics: a “hire me” web page linked from your Twitter / GitHub etc. profiles.</div>
<div>Then gradually create more lead generation artifacts.</div>
<div>Write one blog post. Then another one. And another one. Kaizen, my friend.</div>
<h2 class="hdr-with-anchor" id="i-will-coup-whoever-i-want"><div>I will coup whoever I want</div><a class="header-anchor" href="#i-will-coup-whoever-i-want">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>This is my website so I can shill whatever I want.</div>
<div>I’m working on <a target="_blank" href="https://filerion.com/">Filerion</a>, a web-based file manager for online storage services like Dropbox and S3.</div>
<div>I’m working in the open and <a href="https://blog.kowalczyk.info/article/30da6655040f47e693f8e66eacd308c1/diary-of-a-solo-dev-building-a-web-app.html">documenting the process of creating a software product from scratch</a> on a day to day basis.</div>
<div><a target="_blank" href="https://twitter.com/kjk">Follow along</a> if you want to know what does it take to build a software product. Technologies used, marketing activities and general musing related to programming and productivity.</div>
</div>
Extreme #include discipline for C++ code2022-04-12T00:00:00Ztag:blog.kowalczyk.info,2022-04-12:/article/96a4706ec8e44bc4b0bafda2d9ba502f/extreme-include-discipline-for-c-code.html<div class="notion-page" id="96a4706ec8e44bc4b0bafda2d9ba502f">
<div>C++ takes long to compile</div>
<div>There is more than one reason for it but one of the reasons is excessive re-parsing of the same <code>.h</code> header files.</div>
<div>In <a target="_blank" href="https://www.sumatrapdfreader.org/free-pdf-reader">SumatraPDF</a> I’m using an extreme <code>#include</code> discipline to keep compilation times in check.</div>
<div>The rule is simple: <strong>a <code>.h</code> file cannot <code>#include</code> other <code>.h</code> files</strong>.</div>
<div>I didn’t come up with this idea, I got it from Rob Pike: <a target="_blank" href="http://doc.cat-v.org/bell_labs/pikestyle">http://doc.cat-v.org/bell_labs/pikestyle</a></div>
<div>I’ve been following this rule for several years in <a target="_blank" href="https://github.com/sumatrapdfreader/sumatrapdf">SumatraPDF</a>, a medium sized C++ project of over 100k loc. It works.</div>
<div>“It works” is more important that it seems. Many ideas seem great on paper but fail in practice. Name an economically successful communist country.</div>
<div>Don’t get me wrong: the price of minimizing compilation times is eternal vigilance.</div>
<div>Writing C++ while following that rule is annoying.</div>
<div>In code, things depend on other things. If a struct in <code>foo.h</code> depends on struct in <code>bar.h</code> a quick fix is to <code>#include "bar.h"</code> in <code>foo.h</code>.</div>
<div>You do it once and it works</div>
<div>Done once and for all: in your <code>foo.c</code> you just include <code>foo.h</code> and it brings in <code>bar.h</code>.</div>
<div>That convenience comes with a hidden price. Imagine you have <code>foo2.h</code> that also depends on <code>bar.h</code> so you also <code>#include "bra.h"</code> in <code>foo2.h</code>.</div>
<div>You then <code>#include "foo2.h</code> in <code>foo.c</code> and bang! You just included and parsed <code>bar.h</code> twice.</div>
<div>In real C++ codebases the same headers are unnecessarily re-included and re-parsed hundreds of times.</div>
<div>It’s a known problem. We try to mitigate it with <code>#ifdef</code> guards, <code>#pragma once</code> etc. but in my experience those band-aids don’t solve the problem.</div>
<div>Following Rob Pike’s rule we must <code>#include "bar.h"</code> and <code>foo.h</code> and <code>foo2.h</code> in <code>foo.c</code> in correct order.</div>
<div>The “correct order” part is what makes it annoying.</div>
<div>Let’s face it: a month after writing <code>foo.h</code> I no longer remember that it depends on <code>bar.h</code>.</div>
<div>So the way it goes is:</div>
<ul>
<li>I <code>#include "foo.h"</code> in <code>brand_new.cpp</code> file</li>
<li>I get a compilation error <code>what is this Bar you're referring to?</code></li>
<li>I dig around and figure out that <code>Bar</code> is a struct defined in <code>bar.h</code> so I <code>#include "bar.h"</code> before <code>foo.h</code></li>
<li>I get another compilation error <code>what is that Bar2 you speak of?</code>. This could be unmet dependency from <code>foo.h</code> or newly included <code>bar..h</code></li>
<li>I keep adding 10 more <code>#include</code> to satisfy their cascading dependencies</li>
</ul>
<div>What used to be a simple <code>#include "foo.h"</code> can end up a lengthy game or <code>#include</code> whack-a-mole.</div>
<div>So beware: following this extreme rule will be occasionally painful.</div>
<div>I wasn’t following this rule from the beginning. A refactor of SumatraPDF code to follow it was painful.</div>
<div>I find this price is worth paying and not just because of shorter compilation times.</div>
<div>It also forces me to design better (simpler) dependencies.</div>
<div>Entropy is real. Complexity grows but our heads remain small.</div>
<div>In large programs you have hundreds of structs, classes, functions, enums and they form a complex web of dependencies.</div>
<div>It’s way too much to fully understand at once so we get sloppy, we take shortcuts just to get that damn thing to compile.</div>
<div>Over time the sloppiness accumulate and we might end up with inter-dependent, circular mess. You just want to <code>#include "Button.h"</code> and somehow it ends up bringing in <code>NuclearPowerPlant.h</code></div>
<div>I did that in my own code. Once things get tangled, it’s really hard to untangle them.</div>
<div>The chaos wins.</div>
<div>Don’t let chaos win. Be control.</div>
<div>I don’t think I’ve ever seen any C++ code bases that follows this rule.</div>
<div>This makes me either a madman or a genius.</div>
<div>An idea for reducing compilation times that has more awareness (but also not much adoption in actual code bases) is impl idiom.</div>
<div>I’m not using it because it requires writing more code. That is not a price I’m willing to pay.</div>
</div>
@levelsio and survivorship bias2021-10-20T00:00:00Ztag:blog.kowalczyk.info,2021-10-20:/article/de943f80c7924745abf9405f8c7a2c67/levelsio-and-survivorship-bias.html<div class="notion-page" id="de943f80c7924745abf9405f8c7a2c67">
<div>Pieter Levels is a prolific maker of software.</div>
<div>He’s also very successful maker of software: he’s close to making $1.5 million a year from his business, almost all of it profit.</div>
<div>Almost all of it is his profit since for most of the time he was a sole developer / marketer / copy writer, with a part-time sysadmin for ensuring the server stays alive. Recently he hired customer support and chat moderator.</div>
<div>This level of success <a target="_blank" href="https://news.ycombinator.com/item?id=28873956">attracts attention</a></div>
<div>It also attracts inevitable claims of survivorship bias. The gist of it is: if you do things he did, you won’t have the same level of success.</div>
<div>I think it’s a very defeatist attitude. Life is hard, there is no step-by-step guide to $1.5 million a year business.</div>
<div>You can, however, do things right or you can do them badly.</div>
<div>To create a successful business you need to do more things right than badly.</div>
<div>Let’s call it <strong>Do Things Right bias</strong>.</div>
<div>Let’s dissect Peter Level’s journey to see what he did right and how that differed from doing things badly.</div>
<div>I’ve been following Pieter for a long time and he created a large body of tweets and blog posts, a book, a few youtube talks, most of it related to his journey from making $0 a year to $1.5 million a year.</div>
<div>People say you can learn from other people’s mistakes. It’s even better to learn from other people’s successes.</div>
<div>What did Pieter do right?</div>
<h2 class="hdr-with-anchor" id="pieter-ships-products"><div>Pieter ships products</div><a class="header-anchor" href="#pieter-ships-products">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Here’s a <a target="_blank" href="https://levels.io/projects/">list of his projects</a> going back 11 years. Limiting the list to only software products:</div>
<ul>
<li>2010: Uber clone</li>
<li>2012 dating site for college campuses, YouTube network</li>
<li>2013: YouTube analytics</li>
<li>2014: Slack community for digital nomads, GIF Book, Nomad Jobs, Nomad List 1.0, Go Fucking Do It, Play My Inbox</li>
<li>2015: Startup Retreats, Taylor Bot (a Telegram bot), Nomad List 2.0, Remote OK</li>
<li>2016: Virtual Reality, Places to Work, learning 3D modelling</li>
<li>2017: Nomad List 3.0, Nomad Gear, Mute, Hoodmaps</li>
<li>2018: Nomad List FIRE Calculator, MAKE Book, Maker Rank, No More Google</li>
<li>2019: Bali Sea Cable, Nomad List 5, How Much Is My Side Project Worth</li>
<li>2020: QR Menu Creator, IdeasAI, Remote OK Workers, Airline List, Nomad List Climate Finder</li>
<li>2021: Rebase, Inflation Chart, MAKE Book NFT,</li>
</ul>
<div>You can see him <a target="_blank" href="https://levels.io/hoodmaps/">building a product from first line of code to frontpage of Reddit</a></div>
<div><strong>Doing it badly</strong></div>
<div>Not shipping products.</div>
<div>Commenting on Hacker News how you couldn’t possibly take away from your precious time commenting on Hacker News and write some code instead.</div>
<h2 class="hdr-with-anchor" id="pieter-is-persistent"><div>Pieter is persistent</div><a class="header-anchor" href="#pieter-is-persistent">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>He’s clearly persistent.</div>
<div>His overnight success was 11 years in the making.</div>
<div>A lot of his projects failed but he didn’t give up.</div>
<div>He kept making more.</div>
<div>He creates new things even when he doesn’t have to because Nomad List and RemoteOk bring more money that he needs.</div>
<div><strong>Doing it badly</strong></div>
<div>Abandoning all work after first failure and using survivorship bias as a convenient excuse.</div>
<h2 class="hdr-with-anchor" id="pieter-understands-compund-interest"><div>Pieter understands compund interest</div><a class="header-anchor" href="#pieter-understands-compund-interest">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>If you have a profitable business, there are 2 ways to make even more money:</div>
<ul>
<li>start a new business</li>
<li>sell more stuff to your existing customers</li>
</ul>
<div>The second option tends to be easier and more reliable way to increate revenue and profits.</div>
<div>The hardest thing in business is getting attention of potential customers.</div>
<div>It’s much easier to get attention of customers you already have.</div>
<div>While Pieter starts new projects, he’s smart enough to mostly do things related to hist most successful business: Nomad List.</div>
<div>He keeps improving Nomad List.</div>
<div>He built jobs website for remote workers and does other related projects like Nomad List Climate Finder.</div>
<div>This is similar to the magic of compounded interest in investing.</div>
<div><strong>Doing it badly</strong></div>
<div>Not doubling down on things that work. Chasing distractions.</div>
<h2 class="hdr-with-anchor" id="pieter-abandons-projects-that-are-not-working"><div>Pieter abandons projects that are not working</div><a class="header-anchor" href="#pieter-abandons-projects-that-are-not-working">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You need to be persistent, but in a smart way.</div>
<div>You’ll know when you have a product market fit.</div>
<div>Nomad List started as a Google Spreadsheets that Pieter created for himself and accidently left unprotected.</div>
<div>Within days people were adding more information to it. That was a clear sign of interest so he turned it into a website.</div>
<div>All projects start as weakling babies that need to be initially nurtured to health. But if they never manage to get up on their feet, you have to throw them in the river.</div>
<div>Ok, that was exceedingly bad analogy.</div>
<div><strong>Doing it badly</strong></div>
<div>Sticking with a product that doesn’t perform instead of creating a new project that just might.</div>
<h2 class="hdr-with-anchor" id="pieter-chargers-money"><div>Pieter chargers money</div><a class="header-anchor" href="#pieter-chargers-money">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Freemium is a popular tactic for getting users.</div>
<div>Many people find it psychologically hard to charge for software.</div>
<div>Those are potential reasons why people don’t charge for their products.</div>
<div>Not Pieter. He asks for money early and he asks for a lot. <a target="_blank" href="https://makebook.io/">His book</a> is $49 (and I bought it). Nomad List is $199.</div>
<div><a target="_blank" href="https://levels.io/idea-validation/">The only real validation is people paying for your product</a></div>
<div><strong>Doing it badly</strong></div>
<div>Not charging at all, not charging early, not charging enough.</div>
<h2 class="hdr-with-anchor" id="pieter-ships-quickly"><div>Pieter ships quickly</div><a class="header-anchor" href="#pieter-ships-quickly">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>NomadList started as a Google Docs Spreadsheet.</div>
<div>Pieter turned it into a simple website and made that simple website available immediately.</div>
<div>He then kept working on it and improving it.</div>
<div>This is true of all his projects: ship the smallest thing that provides value, promote it, double-down on things that are working.</div>
<div><strong>Doing it badly</strong></div>
<div>Spending a year writing that web app without showing it to anyone.</div>
<h2 class="hdr-with-anchor" id="pieter-does-a-lot-of-marketing-and-promotion"><div>Pieter does a lot of marketing and promotion</div><a class="header-anchor" href="#pieter-does-a-lot-of-marketing-and-promotion">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>From early days he was promoting his stuff on his blog, Hacker News, Product Hunt, Reddit, Twitter etc.</div>
<div>There’s a thin line between promotion and spamming. Don’t cross it.</div>
<div>Most people, especially people good at programming, neglect promotion and marketing.</div>
<div>Some say that you should spend as much time on marketing as you do on coding the product.</div>
<div>I don’t know what the right amount is but it’s not zero.</div>
<div><strong>Doing it badly</strong></div>
<div>Not spending any time on promotion and marketing.</div>
<div>Not writing blog posts related to your product.</div>
<div>Not tweeting, not trying to promote your writing on Hacker News or reddit or quora.</div>
<div>If a tree falls in the forest and there’s no one to see it, write a blog post about it and post it on Hacker News.</div>
<h2 class="hdr-with-anchor" id="pieter-is-good-at-promotion-and-marketing"><div>Pieter is good at promotion and marketing</div><a class="header-anchor" href="#pieter-is-good-at-promotion-and-marketing">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I’m not sure if this can be thought but Pieter seems to have a natural gift for story telling and promotion.</div>
<div>Consider those 2 examples:</div>
<ul>
<li>2014 <a target="_blank" href="https://levels.io/12-startups-12-months/">I’m launching 12 Startups in 12 Months</a>.
<ul>
<li>“12 startups in 12 months” is a much more interesting story than “I’m starting a startup”.</li>
<li>This is important because if you submit “I’m starting a startup” blog post to Hacker News, it’s unlikely to get traction. But “12 startups in 12 months” - it very well might.</li>
<li>People are attracted to new, never-been-done-before.</li>
<li>This trope is now over-used so you can’t just copy it. But this is a big world and you don’t have to be absolutely first and only. Pieter was inspired by <a target="_blank" href="https://jenniferdewalt.com/">180 websites in 180 days</a> project.</li>
</ul></li>
<li><a target="_blank" href="https://levels.io/100-to-0-things/">How I went from 100 to 0 things</a>.
<ul>
<li>He was robbed.</li>
<li>It happens to many people but most people don’t blog about it and promote that blog on Hacker News</li>
</ul></li>
</ul>
<div>It’s more than a catchy headline, though.</div>
<div>The content of those blog posts is good and goes beyond the obvious.</div>
<div>Sharing a lousy story will not do you any good.</div>
<div><strong>Doing it badly</strong></div>
<div>Not spending any time on promotion and marketing.</div>
<div>On the other hand, there are people who simply spam with low-effort, low-quality posts. That is not going to work either.</div>
<h2 class="hdr-with-anchor" id="pieter-is-productive"><div>Pieter is productive</div><a class="header-anchor" href="#pieter-is-productive">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The amount of projects he shipped is impressive.</div>
<div>The depth of some of his projects is impressive.</div>
<div>It’s not inhumanely great, though.</div>
<div>When you consider he did it over 10 years, it’s more than doable.</div>
<div>The difference between “doable” and “done” is measured in hours spent in front of the monitor, writing code, testing code, learning new things, applying those learnings.</div>
<div>Most people just don’t spend enough time and / or don’t spend the time productively.</div>
<div><strong>Doing it badly</strong></div>
<div>Writing a book is a time consuming activity.</div>
<div>Writing software is a time consuming activity.</div>
<div>There’s no around spending a lot of time on your business (occasional exception notwithstanding).</div>
<h2 class="hdr-with-anchor" id="pieter-learned-programming-by-himself"><div>Pieter learned programming by himself</div><a class="header-anchor" href="#pieter-learned-programming-by-himself">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>This is not about self-thought vs. going to college.</div>
<div>This is about doing what’s necessary.</div>
<div>Pieter had ideas for software but didn’t know how to program.</div>
<div>For most people that’s an insurmountable problem. It’s learned helplessness.</div>
<div>All you need to learn programming is out there, for free. On YouTube, on blogs.</div>
<div>You just need to start learning and keep going.</div>
<div>You can <a target="_blank" href="https://levels.io/diy/">read what Pieter says</a> about DYI ethos.</div>
<div><strong>Doing it badly</strong></div>
<div>Trying to outsource programming to contractors or someone you find on Upwork, fantasizing about rising VC and hiring programmers, trying to find technical cofounder that will do all the work for 10% of the company.</div>
<div>Some people succeed by hiring programmers or contractors.</div>
<div>Most people don’t because most people suck at managing programmers even more than Winklevoss brothers.</div>
<div>If you are a broke college student, you just don’t have the money to hire people.</div>
<h2 class="hdr-with-anchor" id="pieter-copies-best-practices"><div>Pieter copies best practices</div><a class="header-anchor" href="#pieter-copies-best-practices">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>We are not born being the best programmers, best marketers, best writers.</div>
<div>There is no step-by-step guide to becoming the person that creates great products and markets them well.</div>
<div>But there are many businesses that do things well.</div>
<div>You can deconstruct what they do well and copy it.</div>
<div>It requires an attentive, inquisitive mind.</div>
<div>Let’s take <a target="_blank" href="https://makebook.io/">https://makebook.io/</a> as an example. Do you see what Pieter did well there?</div>
<div>Go ahead, look some more. What did he do well on that website?</div>
<div>Answer: social proof.</div>
<div>At the top: “Product Hunt’s Book of the year”. At the bottom: twitter testimonials of people fawning over his book.</div>
<div>Does your business use social proof to make people more likely to buy?</div>
<div>This is just one of many things that a business can do well.</div>
<div>For another example: go to <a target="_blank" href="https://nomadlist.com/">https://nomadlist.com/</a> and try to sign up for Nomad List.</div>
<div>It’s a master class in how to convert a visitor into a customer.</div>
<div>Figuring it why it makes for a great conversion funnel is left as an exercise for the reader.</div>
<div><strong>Doing it badly</strong></div>
<div>Never asking yourself: how can I do X better? How can I make my business better?</div>
<div>Not noticing the best practices that other businesses use.</div>
<div>Not learning from blogs, books, tweet storms.</div>
<div>Not applying the things you learn to your business.</div>
<h2 class="hdr-with-anchor" id="pieter-is-not-a-technologist"><div>Pieter is not a technologist</div><a class="header-anchor" href="#pieter-is-not-a-technologist">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>It’s counter-intuitive that not being a technologist i.e. someone impressed by and interested in new technologies, can be an advantage when running a software business.</div>
<div>He build his $1.5 million/year empire on PHP, sqlite, and a single VPS server.</div>
<div>Those are not technologies that will impress anyone.</div>
<div>PHP is 27 years old. It might be older than you. It sure isn’t Rust or Swift.</div>
<div>SQLite is 21 years old and everyone knows that a serious web service needs to use a serious database, like PostgreSQL</div>
<div>But they get things done. More precisely: Pieter gets things done using them.</div>
<div><strong>Doing it badly</strong></div>
<div>I’m a technologies so trust me, I know the siren song of fancy new languages, Kubernetes clusters, latest web frameworks.</div>
<div>I’m doing it badly. I’m trying to do it less badly.</div>
<div>When it comes to being productive, using a mature tool that you know well beats chasing the latest thing.</div>
<div>For me it’s Go on the backend and Svelte on the front-end but as Pieter shows, you can be very prolific with a very old technology.</div>
<h2 class="hdr-with-anchor" id="pieter-surfs-the-high-waves"><div>Pieter surfs the high waves</div><a class="header-anchor" href="#pieter-surfs-the-high-waves">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>YouTube analytics in 2013.</div>
<div>Slack channel in 2014.</div>
<div>Virtual Reality in 2016.</div>
<div>Telegram bot in 2015.</div>
<div>Being a nomad in 2015.</div>
<div>QR Menu creator in 2020.</div>
<div>Bitcoin / NFT in 2021.</div>
<div>The guy is at the forefront of trends.</div>
<div>This doesn’t necessarily make or break his businesses but it’s easier to promote novel things.</div>
<div>This is not blind chasing of fads.</div>
<div>He doesn’t have a podcast, a TikTok, or hangs out on Clubhouse.</div>
<div>He did nomadic lifestyle years before it was a trend.</div>
<div>Don’t ignore trends but do apply your personal and busines filter.</div>
<div>It’s also different than being a technologist.</div>
<div>He didn’t rewrite his website on blockchain but he did notice that Bitcoin is a trendy thing and he did a project that is related to Bitcoin and provides value related to Bitcoin.</div>
<div><strong>Doing it badly</strong></div>
<div>Not paying attention to new trends.</div>
<div>Not trying to figure out what product can you build to take advantage of the new trend.</div>
<div>Not being selective about which trends to exploit.</div>
<h2 class="hdr-with-anchor" id="pieter-is-humble"><div>Pieter is humble</div><a class="header-anchor" href="#pieter-is-humble">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The first thing he said in <a target="_blank" href="https://news.ycombinator.com/item?id=28874985">this comment</a></div>
<blockquote>
<div>All of this is possible because I’ve been a HN reader since 2010 and was inspired by all of you and especially @patio11 on here to bootstrap my own things and do it VERY publicly.</div>
</blockquote>
<div>This might not impact his business but it does impact how people perceive him.</div>
<div><strong>Doing it badly</strong></div>
<div>Being an arrogant prick that people dislike and root against.</div>
<h2 class="hdr-with-anchor" id="doing-things-right-bias"><div>Doing Things Right bias</div><a class="header-anchor" href="#doing-things-right-bias">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Survivorship bias is for losers.</div>
<div>Doing Things Right bias is for winners.</div>
<div>So put that coffee down until you start <strong>Doing Things Right</strong>.</div>
<h2 class="hdr-with-anchor" id="learning-from-pieter"><div>Learning from Pieter</div><a class="header-anchor" href="#learning-from-pieter">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Do you want to create successful projects? You can learn a lot from Pieter:</div>
<ul>
<li>watch his <a target="_blank" href="https://www.youtube.com/watch?v=6reLWfFNer0">How to build a startup without funding</a> talk</li>
<li>read his <a target="_blank" href="https://levels.io/">blog posts</a></li>
<li>read <a target="_blank" href="https://makebook.io/">his book</a></li>
<li>watch <a target="_blank" href="https://levels.io/ama/">4 hour AMA</a></li>
<li>listen to <a target="_blank" href="https://levels.io/product-hunt-radio/">an interview he gave</a></li>
</ul>
<div>Don’t just read it passively.</div>
<div>You’re a business detective. He has a successful business. Your job is to figure out all the things he did right, not just the things he’s telling you.</div>
<div>You need to figure out that he’s using social proof even if he never talks about using social proof.</div>
</div>
Lessons learned from 15 years of SumatraPDF, an open source Windows app2021-07-25T00:00:00Ztag:blog.kowalczyk.info,2021-07-25:/article/2f72237a4230410a888acbfce3dc0864/lessons-learned-from-15-years-of-sumatrapdf-an-open-source-windows-app.html<div class="notion-page" id="2f72237a4230410a888acbfce3dc0864">
<div>I released first version of <a target="_blank" href="https://www.sumatrapdfreader.org/free-pdf-reader">SumatraPDF</a> in 2006. That’s 15 years ago which seems like a good time for a retrospective.</div>
<h2 class="hdr-with-anchor" id="the-app"><div>The app</div><a class="header-anchor" href="#the-app">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>SumatraPDF is a multi-format (PDF, ePub, Mobi, comic book, DjVu, XPS, CHM) viewer for Windows and currently looks like this:</div>
<div><img class="blog-img" src="/img/983eae4f6a4cbd89dd91ae7df84663081bbda06d.png" alt="" /></div>
<h2 class="hdr-with-anchor" id="the-code"><div>The code</div><a class="header-anchor" href="#the-code">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>SumatraPDF is an open-source document reader for Windows. It started as a PDF reader, hence the name. Over time I’ve added for e-book formats (epub, mobi), comic books (cbz, cbr), DjVu, XPS, image formats etc.</div>
<div>It’s about 127k lines of C++ (not counting libraries written by others).</div>
<div>It’s written against Win32 API, not using GUI abstraction libraries like Qt. This contributes to making it as small and fast as possible.</div>
<div>Almost all of it was written by 2 people, with occasional contributions from others.</div>
<div>The amount of code written is actually higher. It is the nature of long running code bases that the code gets written and re-written. We delete, add, change.</div>
<div>It’s a side project, done after hours, not a full time effort. How does a daily grind of working on an app looks like?</div>
<div>It looks like this:</div>
<div><img class="blog-img" src="/img/251a5dbb72a41441d87187a4379b4b6532ff5506.png" alt="" /></div>
<div>You can also take a peek at <a target="_blank" href="https://github.com/sumatrapdfreader/sumatrapdf/blob/master/src/docs/log.txt">my dev log</a>. I’ve only started it a year ago so only covers 1 year out of 15.</div>
<h2 class="hdr-with-anchor" id="why-i-created-sumatrapdf"><div>Why I created SumatraPDF</div><a class="header-anchor" href="#why-i-created-sumatrapdf">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>SumatraPDF is what I call an accidental success.</div>
<div>I never wanted to write a PDF reader for Windows.</div>
<div>In 2006 I was working at Palm and one of my job duties was writing a PDF reader for <a target="_blank" href="https://en.wikipedia.org/wiki/Palm_Foleo">Foleo</a>, an ARM and Linux powered mini laptop. You never heard of Foleo because it was cancelled weeks before launch for reasons I’m not privy to.</div>
<div>At the time I didn’t know that PDF is popular but Palm management did which is why they decided that PDF reader is a must have application. I ended up being the (sole) dev on the project.</div>
<div>Writing a PDF rendering library is a multi-year effort. We didn’t have years so I used <a target="_blank" href="https://poppler.freedesktop.org/">Poppler</a> open-source library.</div>
<div>My job was to write a basic PDF viewer that used Poppler to render PDF pages into a bitmap in memory and blit those bitmap on screen.</div>
<div>PDF is a complex format and rendering of some PDFs is slow. I wanted to improve the speed because Jeff Bezos told me that speed is something that customers will always care about.</div>
<h2 class="hdr-with-anchor" id="accidental-app"><div>Accidental app</div><a class="header-anchor" href="#accidental-app">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The way to improve speed is to profile the code and look at the result.</div>
<div>Unfortunately, the toolchain for unreleased ARM hardware wasn’t very good. Forget about a profiler, kid, be grateful you have a C++ compiler and don’t have to enter assembly by typing hex, like Steve Wozniak.</div>
<div>Windows had decent profilers, so I compiled Poppler for Windows.</div>
<div>Once I had the library working on Windows, I wrote simplest GUI app that would show the pages and allow navigating between pages.</div>
<div>What do you know: I had a simple PDF reader for Windows.</div>
<div>I released it on my website. It couldn’t do much so I tagged it as version 0.1.</div>
<blockquote>
<div>If you’re not embarrassed by your app then you’ve waited too long to release it</div>
</blockquote>
<div>I didn’t come up with this nugget of wisdom but I agree with it.</div>
<div>Getting early users, learning what features they want the most beats toiling for months or years and implementing lots of features before you know anyone even cares.</div>
<h2 class="hdr-with-anchor" id="profiling-performance-optimization-and-contributing-to-open-source"><div>Profiling, performance optimization and contributing to open source</div><a class="header-anchor" href="#profiling-performance-optimization-and-contributing-to-open-source">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Back to profiling: my plan worked.</div>
<div>I profiled the documents that took the longest to render and made a few surprisingly simple and surprisingly effective optimizations.</div>
<div>If memory servers, 2 optimizations had the biggest effect:</div>
<ul>
<li>optimizing string class to use what is know as “small string optimization” i.e. adding a small buffer inside string class to hold small strings inline (as opposed to always allocating memory for the string). Strings were used frequently and most of them were small</li>
<li>fixing byte-at-a-time i/o by converting it to bulk reads. The way the code was structured in some code-paths it would do a virtual C++ call and a call to C read() function for each byte. Those are extremely cheap but not when you do it 5 million times</li>
</ul>
<div>As a good boy I did submit my changes to Poppler.</div>
<div>As is my experience with contributing to open source projects, it was more of a miss than a hit.</div>
<div>Yes, I got 13 commits in but the project wasn’t very active and the maintainers weren’t eager to accept anything beyond small changes. Forget any major refactors.</div>
<div>I’m not one to voluntarily bash my head against the wall so I stopped trying.</div>
<div>(As you can see, I’m a fantastic team player).</div>
<h2 class="hdr-with-anchor" id="code-quality"><div>Code quality</div><a class="header-anchor" href="#code-quality">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I want it and you should want it to.</div>
<div>How to maintain high code quality while working mostly solo, with no-one doing code reviews, no dedicated QA team?</div>
<div>Here’s how:</div>
<ul>
<li>test the code yourself. Step through newly added code in the debugger, verify the newly added functionality works as expected and in general use the app a lot</li>
<li>automated crash reporting. Unfortunately it’s a pain to build but this is single most important thing you can do to improve quality of your software. Briefly: setup exception handlers to catch crashes in the app, in crash handler download symbols from the server to get readable callstack, create a crash report that includes callstacks of all thread, program and os information, log and submit that to a server. On the server, process those files and generate web pages for easy viewing of the crashes. Like I said: it’s a pain to build. Once you have crashes, look at them occasionally and try to figure out what went wrong and fix it</li>
<li><code>assert()</code>. asserts are well established practice in C++ code: an additional code only executed in debug builds that verifies some conditions are true. If they’re not, something went wrong and you should investigate. I wrote wrote my own <code>assert</code>-like function which I enable in non-debug pre-release builds so that I automatically get bug reports from people hitting those conditions. Trust me: there’s no amount of testing you can do yourself that would match all the different things that a thousand people will do just by using the app.</li>
<li>logging. When investigating issues it helps to know what sequence of events led to a crash. My tiny logging module logs to a block of memory. That gets sent along with crash report. I also have an option to log to a file and I’ve recently added logging to a separate logging app via named pipe. This is perfect because most of the time I don’t care about the logs but when I do, I don’t want to restart the app to enable logging. With separate logging app, SumatraPDF is logging all the time and when it detects that logging app is running, it’ll also log to it. Implementation was trivial: logging app creates a named pipe, logger opens the pipe (like a file) and if open succeeds, it means the logger app is running and it reads the logs we write to the pipe</li>
<li>static code analysis: max level of warnings in C++ compiler, make warnings into errors, Visual Studio’s `/analyze’ option, cppcheck, clang-tidy, GitHub’s CodeQL. Run those occasionally and fix the errors and warnings</li>
<li>ASAN (<a target="_blank" href="https://clang.llvm.org/docs/AddressSanitizer.html">Address Sanitizer</a>), is fantastic. Was added in some point release of Visual Studio 2019. At a very small performance cost it can detect if you over-write memory or try to read uninitialized memory. I have a configuration with ASAN enabled. It’s fast enough to be used as a regular build.</li>
<li>stress testing. Sumatra’s job is mostly to render complex document format. There often are crashes in specific files due to complexity of the formats. To ensure lack of crashes I wrote a stress test code that reads and renders all files in a directory. I typically run it before a release on a large collection of test files I amassed over the years</li>
<li>unit testing. I don’t have a lot of them, they’re mostly for testing edge cases for low-level functionality like string formatting. They occasionally find bugs.</li>
<li>memory leaks. It’s surprisingly hard to find an easy to use memory leak detection tool. I’m working on a very simple built-in leak detector. In the meantime I’m using <a target="_blank" href="https://drmemory.org/">Dr. Memory</a>. It works but it’s super slow.</li>
</ul>
<h2 class="hdr-with-anchor" id="frequent-releases"><div>Frequent releases</div><a class="header-anchor" href="#frequent-releases">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>When you don’t have many features, improving the app is fast and easy. It doesn’t take much effort to implement “Go to” dialog (implemented in v 0.2).</div>
<div>On one hand I don’t want to release too often but I also do want the users to get new features as quickly as possible.</div>
<div>My policy of new releases is: release when there’s at least one notable, user-visible improvement.</div>
<div>Web apps take it to the extreme (some companies deploy to production multiple times a day).</div>
<div>In desktop software it’s a bit more involved and I had to build functionality to make it easy i.e. add a check for new releases, write an installer that can update the program.</div>
<div>BTW: I mean “frequent in proportion to amount of new code written”. SumatraPDF releases are not frequent in absolute terms but frequent if you consider that it’s a part-time, after hours project.</div>
<h2 class="hdr-with-anchor" id="treat-open-source-projects-like-commercial-software"><div>Treat open source projects like commercial software</div><a class="header-anchor" href="#treat-open-source-projects-like-commercial-software">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Majority of open source projects probably don’t fall into this category, but if you want your open source to be as successful as possible, act as if it was a commercial product from a software company.</div>
<div>What does it mean in practice?</div>
<div>From day one I created a website for the app. It had screenshots, it had documentation, it was easy to download and install. Granted, a kind soul on Reddit called it “a website made by a 6-year old”. The lesson here is two-fold:</div>
<ul>
<li>ignore haters and assholes</li>
<li>a website built by a 6-year old is better than no website. It doesn’t have to be pretty, it has to be functional</li>
</ul>
<div>I did basic SEO. Nothing beyond Google’s “SEO 101” docs: just pay attention to URLs, put the right meta-data, use the right keywords.</div>
<div>I had a forum for users to ask questions, submit feature requests and occasionally support each other.</div>
<div>I made the installation process as easy as possible.</div>
<div>Everything that is a good idea for promoting commercial software is also a good idea for open source project.</div>
<h2 class="hdr-with-anchor" id="switching-the-engine-while-the-car-is-running"><div>Switching the engine while the car is running</div><a class="header-anchor" href="#switching-the-engine-while-the-car-is-running">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>At some point I decided to switch from Poppler to <a target="_blank" href="https://mupdf.com/">mupdf</a> because mupdf was better and actively maintained.</div>
<div>Changing the app to use completely different library is not something you can do in an afternoon.</div>
<div>It’s demoralizing to work long time on code that doesn’t even compile.</div>
<div>To keep things compiling while also working towards supporting alternative rendering engine I developed an abstraction for the rendering engine.</div>
<div>The engine would provide the functionality the UI needed: getting number of pages in the document, sizes of each page (to calculate layout), rendering a page as a bitmap etc.</div>
<div>I’m much less enthusiastic about abstractions than most programmers (at least those who like to opine on Hacker News) but in this case it served me well.</div>
<div>I was able to incrementally convert program form using Poppler API to using Poppler via engine abstraction to using mupdf via Engine abstraction.</div>
<div>For a while I supported both engines at the same time but eventually I switched to just mupdf, to keep the app small.</div>
<div>This opened the door for supporting other formats via the same abstraction.</div>
<h2 class="hdr-with-anchor" id="simplicity-vs-customizability"><div>Simplicity vs. customizability</div><a class="header-anchor" href="#simplicity-vs-customizability">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Simplicity sells.</div>
<div>I learned that from the history of Mozilla Firefox.</div>
<div>Before Firefox there was Netscape Navigator. It was a beast of an app, combining web browser with e-mail client.</div>
<div>Netscape couldn’t help themselves and was adding features upon features, leading to very complex UI.</div>
<div>A small group of renegades within Mozilla forked the code and focused on simple UI.</div>
<div>Simple Firefox was much more popular than the complex Navigator and eventually ate it completely.</div>
<div>From the beginning my goal was to keep the UI of SumatraPDF as simple as possible. An <sup>80</sup>⁄<sub>20</sub> app: 80% of functionality with 20% of the UI.</div>
<div>This requires resolve. I constantly get requests to add more icons to the toolbar and I constantly have to say “no” because adding 2 more icons to the toolbar to satisfy 10% of users makes the app slightly worse for 100% of the users.</div>
<div>Another trap is a siren song of additional settings. Sometimes people suggest that instead of doing X, the program should do Y. Not willing to remove X, they suggest adding a new UI setting “[ ] Do Y instead of X”.</div>
<div>Having settings dialog with 100 settings is not a good solution. It makes the app worse for everyone due to overwhelming them with choices and hiding important options in a sea of non-important options.</div>
<div>Not to mention that every conditional behavior requires more code, more potential bugs and more testing.</div>
<div>That being said, I also believe customizability is important. I believe that a big reason for Winamp being such a dominant music player (at the time) was its ability to skin the whole UI.</div>
<div>Some advanced features might only be used by 20% of users but those users are most likely power users that will evangelize the app more than the other 80% of the users.</div>
<div>My solution to UI simplicity vs. customizability: advanced settings file.</div>
<div>I designed a simple, human readable (and human writeable) textual format for <a target="_blank" href="https://www.sumatrapdfreader.org/settings/settings">advanced settings</a>. Think JSON, but better.</div>
<div>I didn’t bother to write UI for changing those advanced settings. I just launch notepad.exe with the file. When user changes the settings and saves the file, I reload it and apply the changes.</div>
<h2 class="hdr-with-anchor" id="be-water-my-friend"><div>Be water, my friend</div><a class="header-anchor" href="#be-water-my-friend">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Change is the only constant. We must adapt to the changes in the world.</div>
<div>I can’t believe how many popular projects still use craptastic Sourceforge for source repository or mailing list.</div>
<div>Actually, I can believe: changing things takes effort and the path of least resistance is to do nothing.</div>
<div>I started with Sourceforge, switched to code.google.com and then to github.com.</div>
<div>I switched forum software three times.</div>
<div>I’ve added a browser plugin and then removed it when browsers stopped supporting such plugins.</div>
<div>I changed the format for storing preferences from binary to human readable text.</div>
<div>Windows XP went from being the OS used by majority of users to no longer being supported (long after Microsoft stopped supporting it).</div>
<div>At first I only had 32-bit build and now I have both but emphasize 64-bit builds.</div>
<h2 class="hdr-with-anchor" id="think-outside-of-the-box"><div>Think outside of the box</div><a class="header-anchor" href="#think-outside-of-the-box">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Thinking outside of the box is hard because the box is invisible.</div>
<div>SumatraPDF wasn’t the first PDF reader application ever written.</div>
<div>But most PDF readers do not become multi-format readers.</div>
<div>In hindsight it’s an obvious idea to support as many document formats as possible but it took me 5 years to realize it.</div>
<div>Most readers are still single format and I do believe being multi-format helped SumatraPDF become popular.</div>
<div>I can’t say it’s totally unique idea. There were multi-format image viewers long before SumatraPDF and I probably was inspired by them.</div>
<h2 class="hdr-with-anchor" id="small-and-fast-pick-both"><div>Small and fast - pick both</div><a class="header-anchor" href="#small-and-fast-pick-both">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>By today’s standards SumatraPDF is tiny (installer smaller than 10 MB) and starts up instantly.</div>
<div>I believe being small and seemingly fast was a big reason for adoption.</div>
<div>This comes back to Jeff Bezos’ wisdom: there will never be a time when users want bloated and slow apps so being small and fast is a permanent advantage.</div>
<div>How do I keep SumatraPDF small?</div>
<div>I avoid unnecessary abstractions. Window’s system of controls is a giant pain in the ass to program against. I could use wrappers like Qt, WxWindows or Gtk. They are easier to use but cause instant, giant bloat.</div>
<div>I’m not afraid to write my own implementation of things. I have my own JSON, HTML / XML parsers that are a fraction of size of the popular libraries for those tasks.</div>
<div>I aggressively take advantage of rich functionality included in Windows.</div>
<div>Let’s say I need to do a network request. I could include a monster library like curl or I could write 300 lines of code using win32 APIs. I wrote 300 lines of code.</div>
<div>An absence of bloat is hard to notice because it isn’t there.</div>
<div>My pet peeve is over-using XML for storing data.</div>
<div>When I worked at Palm I was at a design meeting for auto-update system for a phone. Part of it was storing information about the current version in the image, downloading information about the latest version and comparing them.</div>
<div>The developer decided to use XML for storing that information. That seemed like a lot of bloat for storing simple information like a version number. An compliant XML parser alone is a lot of code. Surely a simple binary format would be easier to implement, I suggested and was ignored.</div>
<div>If you don’t have the power to fire someone, your ideas will be ignored.</div>
<div>(As you can see, I’m a great team player.)</div>
<div>For storing advanced settings I designed and implemented a file format that is smaller than XML, readable and writeable by humans and can be implemented in few hundred lines of code. It’s as powerful as JSON and even more readable.</div>
<div>It’s so simple that after implementing it I had the time to implement a serialization system for C++ objects and a Go code generator. To add more settings I don’t have to write more C++ code. I just add data definition to Go generator, re-run it and get data-driven C++ parsing auto-generated.</div>
<h2 class="hdr-with-anchor" id="it-s-my-project-and-i-act-like-it"><div>It’s my project and I act like it</div><a class="header-anchor" href="#it-s-my-project-and-i-act-like-it">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>When someone pays you to write code you have to do it the way they like it.</div>
<div>A big attraction of working on code you’re not paid for is that there is no one who can tell you what to do or how to do it.</div>
<div>My code would not pass a code review at Google and not because it’s bad but because it’s often unorthodox. Outside of accepted dogma.</div>
<div>(As you can see, I’m a great team player.)</div>
<div>I always used SumatraPDF as my playground for testing crazy ideas.</div>
<div>Minimize the code size by not using STL? That’s crazy but I did it. Granted, in 2006 STL wasn’t very good.</div>
<div>I learned about how Plan 9 C code had non-traditional scheme of #include files where they don’t put #ifdef wrappers in each .h file to allow multiple inclusion and .h files don’t include other .h files. As a result .c files have to include every .h file they need and in the right order. It’s a bit of a pain and no other modern C++ codebase I know of maintains such discipline.</div>
<div>But it’s my project so I did it and I keep doing it. It prevents circular dependencies between .h files and doesn’t inflate C++ build times because of careless including the same files over and over again.</div>
<div>I implemented a CSS inspired UI system. Not great, but mine. And I plan to replace with a different one.</div>
<div>Because I can.</div>
<div>Because no one can tell me not to.</div>
<h2 class="hdr-with-anchor" id="cross-platform-is-over-rated"><div>Cross-platform is over-rated</div><a class="header-anchor" href="#cross-platform-is-over-rated">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>SumatraPDF is unabashedly a <a target="_blank" href="https://www.sumatrapdfreader.org/docs/Why-only-Windows">Windows only</a> app.</div>
<div>Supporting other platforms (Linux, Mac, Android) is one of the most frequent requests. A request that I have to decline.</div>
<div>First, there is a pragmatic reason: I just don’t have the bandwidth to write code for 3 platforms.</div>
<div>Second, I believe an excellent app for one platform can become more popular than a mediocre app for 3 platforms.</div>
<div>Coming back to the first reason: I don’t have the bandwidth to write 3 excellent apps. Part of the reason SumatraPDF is small is my use of win32 APIs for the UI.</div>
<div>The only way for one person to even attempt cross-platform app is to use a UI abstraction layer like Qt, WxWidgets or Gtk.</div>
<div>The problem is that Gtk is ugly, Qt is extremely bloated and WxWidgets barely works.</div>
<h2 class="hdr-with-anchor" id="tests-are-not-necessary-neither-are-code-reviews"><div>Tests are not necessary, neither are code reviews</div><a class="header-anchor" href="#tests-are-not-necessary-neither-are-code-reviews">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I’m not saying tests are bad or that you shouldn’t write test or do code reviews.</div>
<div>I’m saying that they are not necessary.</div>
<div>Dogma is powerful. Sometimes in my corporate life I felt like writing tests was just going through motion. Maybe we should spend more time writing code instead, I though?</div>
<div>But try to make a nuanced point about more tests vs. more code to your fellow developers and you’ll be burned at stake and your smoldering carcass will be thrown to wild dogs. Village children will use your severed head to play soccer.</div>
<div>(As you can see, I’m a great team player.)</div>
<div>And yet I do know that you can write complex, relatively bug free code without tests, because I did it.</div>
<div>I do know that you can write complex, relatively bug free code without anyone looking over your code, because I did it.</div>
<div>If no one uses your app then who cares if it crashes.</div>
<div>If many people use your app and it crashes, they’ll tell you and then you’ll fix it.</div>
<h2 class="hdr-with-anchor" id="overnight-success-takes-a-decade"><div>Overnight success takes a decade</div><a class="header-anchor" href="#overnight-success-takes-a-decade">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>SumatraPDF is relatively popular. Not Facebook popular or DOOM popular, but more popular than most apps. A respectable level of popular.</div>
<div>It all started with v 0.1 and a trickle of downloads. It remained a trickle for many, many months.</div>
<div>I’m not sure there’s a lesson here.</div>
<div>Success often takes a long time.</div>
<div>Unfortunately, at that stage it’s undistinguishable from (eventual) failure so this wisdom doesn’t help you if you’re working on a not-yet-successful project and debating if you should continue or abandon</div>
<h2 class="hdr-with-anchor" id="the-money"><div>The money</div><a class="header-anchor" href="#the-money">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Open source is not a good business model.</div>
<div>If you want to make money do literally anything else: try to sell software, do consulting, build a SAAS and charge monthly for it, rob a bank.</div>
<div>I did experiment with making money and made some.</div>
<div>There was a time AdSense would pay decent CPM so I put AdSense ads on the website and it made some money. I no longer do because the rates did plummet and it isn’t worth annoying people. My soul has a price and AdSense can no longer afford it.</div>
<div>Now I’m experimenting with Patreon and Paypal donations. It makes more than $100 a month but not much more than that.</div>
<div>Like I said: don’t start open source project with intent to make money.</div>
<div>Rarely you can have both: freedom to do whatever you want and a good pay so pick what is more important to you. Open source gives you freedom but not money.</div>
<h2 class="hdr-with-anchor" id="on-to-the-future"><div>On to the future</div><a class="header-anchor" href="#on-to-the-future">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I need to continue being like water.</div>
<div>For years I resisted adding editing features. “It’s just a reader” I said. But why not add editing? If people want it, give it to them.</div>
<div>The future of all software is as a web app. Why not bring the spirit of SumatraPDF to the web?</div>
<div>Those are just a few ideas I have today.</div>
<div>Being like water means that in 5 years I’ll have other ideas, informed by what’s happening at that time.</div>
</div>
How I use Roam Research2021-06-27T00:00:00Ztag:blog.kowalczyk.info,2021-06-27:/article/2af6a10ab74d43a58053b709badb4ad0/how-i-use-roam-research.html<div class="notion-page" id="2af6a10ab74d43a58053b709badb4ad0">
<div>I take lots of notes and I’m always looking for that perfect note taking app.</div>
<div>Because I take a lot of notes, mobile-first note taking apps (e.g. Google’s Keep) are of no interest to me.</div>
<div>To me most important thing is:</div>
<ul>
<li>lowest possible friction in adding new notes</li>
<li>fast way of finding existing notes</li>
</ul>
<div>In theory I could use Google Docs as a note taking app but both <a target="_blank" href="https://notion.so">Notion</a> and <a target="_blank" href="https://roamresearch.com/">Roam Research</a> make it much faster to jot a new note or find a note I wrote in the past.</div>
<div>Over the years I’ve cycled through many note taking systems: a single big text file, collection of markdown files in a git repository, a hosted wiki, a self-hosted wiki, Evernote, my own note taking web app.</div>
<div>A combination of <a target="_blank" href="https://www.notion.so/">Notion</a> and <a target="_blank" href="https://roamresearch.com/">Roam Research</a> is, I hope, the end of the road and will be good enough for the rest of my life.</div>
<div>This article describes how I use <a target="_blank" href="https://roamresearch.com/">Roam Research.</a></div>
<h2 class="hdr-with-anchor" id="roam-use-cases"><div>Roam use cases</div><a class="header-anchor" href="#roam-use-cases">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Here are my main use cases of Roam:</div>
<ul>
<li>taking notes / writing</li>
<li>keeping a log of activity (what I did)</li>
<li>managing todo list</li>
<li>managing projects</li>
<li>bookmarking</li>
</ul>
<div>Roam can be hard to grasp at first. Things you need to know:</div>
<ul>
<li>Roam is a collection of named pages</li>
<li>you can trivially link between pages. Writing <code>[[foo]]</code> or <code>#foo</code> anywhere creates a link to a page named <code>foo</code>. A page also lists backlinks (i.e. pages linking to this page) at the end</li>
<li>there’s automatically a page for every day and the default view is a page for current day</li>
</ul>
<div>Knowing that, let’s go through my use cases.</div>
<h2 class="hdr-with-anchor" id="taking-notes-writing"><div>Taking notes / writing</div><a class="header-anchor" href="#taking-notes-writing">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Let’s say I have an idea to write an article about my use of Roam Research (i.e. this very article).</div>
<div>In most other note taking apps I would first have to think about where to put that note.</div>
<div>In Roam I just start writing in the page for current day and tag it with <code>#draft</code>.</div>
<div>Later on I can go to a <code>draft</code> page and see all the drafts I’m currently working on.</div>
<div>There is a greater point here: Roam is “write now, organize later” system.</div>
<div>When I have a though, I jot it down in today’s page and tag it with keywords that will help me find it later.</div>
<div>It’s the least amount of friction between having a thought and recording that thought for the future.</div>
<div>Taxonomy is personal. I use <code>#draft</code> for my draft articles, but you could just as well use <code>#article</code> or <code>#articles</code> (using singular vs. plural for tags is surprising hard to decide).</div>
<h2 class="hdr-with-anchor" id="logging-of-activity"><div>Logging of activity</div><a class="header-anchor" href="#logging-of-activity">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>In personal life logging of activity is just for curiosity.</div>
<div>I write down what I did in a page for current day and tag log entries with <code>#done</code>.</div>
<div>I visit <code>done</code> page to review what I did in the past.</div>
<div>What if I want to track what I did on a specific project, e.g. <a target="_blank" href="https://www.sumatrapdfreader.org/free-pdf-reader">SumatraPDF</a>?</div>
<div>I further tag the entry with a tag for the project e.g. <code>#sumatrapdf</code>. Roam has filtering capabilities that allows me to see entries tagged with <code>#done</code> AND <code>#sumatrapdf</code>.</div>
<div>In professional life logging your work could help you level up. If no-one knows what you did, did it happen? Will it lead to a promotion?</div>
<div>Roam is perfect for logging what you did so that you can review it later and send a summary to your manager or consulting client.</div>
<div>If I were a Google software engineer, I would tag my entries with <code>#done</code> and <code>#work</code>.</div>
<div>If I was doing a consulting job I would tag them as <code>#done</code>, <code>#consulting</code> and maybe with a tag specific to the project or the client.</div>
<div>Again, taxonomy is personal. You can use other tags if they make more sense to you.</div>
<h2 class="hdr-with-anchor" id="managing-projects"><div>Managing projects</div><a class="header-anchor" href="#managing-projects">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I work on several software projects at the same time.</div>
<div>I have a page per project that serves as a hub of information related to the project.</div>
<div>On that page I have:</div>
<ul>
<li>most important links (e.g. to github repository, Digital Ocean dashboard for deployed project etc.)</li>
<li>list of things to do next</li>
<li>list of ideas for future improvements</li>
<li>links to competing products</li>
<li>technical overviews. Memory is always fading so it pays to jot down things that might help you remember key information</li>
<li>documentation</li>
</ul>
<h2 class="hdr-with-anchor" id="todo-list"><div>Todo list</div><a class="header-anchor" href="#todo-list">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I don’t use dedicated todo list applications. I think that creating hundreds of todo items is overwhelming and counter-prodcutive.</div>
<div>I do believe in one of the tenets of Getting Things Done system: writing down your ideas and todo tasks to get them out of your head but be able to review them in the feature.</div>
<div>I maintain a “maybe do list” instead of “todo list”.</div>
<div>For example, if I have an idea for SumatraPDF, I jot it down in Roam and tag it with <code>#sumatrapdf</code>. I can then review those ideas in the future when I have time to work on SumatraPDF.</div>
<div>I do maintain a very short (few items) todo list for near future. Mostly today and tomorrow.</div>
<div>This is an anti-procrastination device.</div>
<div>I find that when I start working on a task, I manage to finish it.</div>
<div>Procrastination is what keeps me from even starting.</div>
<div>Having a list of things to do in the morning helps me start working (as opposed to starting to browse Twitter).</div>
<div>It’s worth mentioning that other people do use Roam as a sophisticated todo list. Roam is flexible enough. For example Roam understands dates so you can have pages with tags that record a completion date for a task and you can use Roam’s powerful query syntax to find tasks that are supposed to be done in the next week.</div>
<h2 class="hdr-with-anchor" id="bookmarking"><div>Bookmarking</div><a class="header-anchor" href="#bookmarking">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Bookmark managers in browsers are not good at managing large list of bookmarks. Hierarchies don’t scale.</div>
<div>Roam’s easy tagging makes it a replacement for services like <a target="_blank" href="https://pinboard.in/">pinboard.in</a> (and once-famous-now-defunct del.icio.us). Found a page about Docker you want to bookmark for later? Drop the link in Roam and tag it with <code>#docker</code>. Find it later in the <code>docker</code> page.</div>
<div>There is a use case for browser’s bookmarks: a fast way to get to most frequently used websites. I’m experimenting with using Roam for that as well.</div>
<div>I’ve created page <code>bookmarks</code> with most frequently used links. Opening a Roam page from scratch is relatively slow so I keep that page open at all times and as a pinned tab (a Chrome thing).</div>
<h2 class="hdr-with-anchor" id="one-more-thing"><div>One more thing…</div><a class="header-anchor" href="#one-more-thing">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Actually, lots of things.</div>
<div>I’m relatively light user of Roam. I’m still discovering new use cases for Roam and better ways of managing notes.</div>
<div>But even just the basics usage of jotting things down and tagging them for cross-referencing is very useful. Speed and flexibility are important and Roam has both.</div>
<div>Roam can offer much more if you’re willing to invest time. Powerful search queries, tags with values, storing files. There’s a reason people become obsessed with Roam.</div>
<div>I’m not obsessed but I do appreciate a great tool.</div>
<h2 class="hdr-with-anchor" id="notion-and-roam"><div>Notion and Roam?</div><a class="header-anchor" href="#notion-and-roam">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Why use both <a target="_blank" href="https://www.notion.so/">Notion</a> and <a target="_blank" href="https://roamresearch.com/">Roam</a>? Why not stick with just one of them?</div>
<div>Notion has some use cases that Roam Research doesn’t support well. For example sharing a subset of notes with other people.</div>
<div>They also have different philosophies of managing notes.</div>
<div>Notion is strongly hierarchical. Pages are nested within pages. It’s great when hierarchy is obvious but sometimes I spend brain cycles figuring out where in the hierarchy should a new page go.</div>
<div>Roam is a flat collection of interlinked pages. It’s more difficult to grasp but removes the thinking. Write things down, tag them now and maybe organize later.</div>
<div>Roam is more quirky. While Notion has more polished UI, Roam is often faster to use. Trying to change a link destination in Notion can be a struggle.</div>
<h2 class="hdr-with-anchor" id="final-thoughts"><div>Final thoughts</div><a class="header-anchor" href="#final-thoughts">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>We live in a golden era of note taking apps. I wish I had Notion or Roam Research 10 years ago.</div>
<div><a target="_blank" href="https://www.notion.so/">Notion</a> opened floodgates of high quality note taking apps. Before Notion there was just Evernote. After Notion we got lots of other options, including Roam.</div>
<div>I think they are both powerful enough to serve as a note taking system for life.</div>
</div>
The things we do to ship desktop software2019-05-01T00:00:00Ztag:blog.kowalczyk.info,2019-05-01:/article/39a15945117440d99a9ef0f7de1b618a/the-things-we-do-to-ship-desktop-software.html<div class="notion-page" id="39a15945117440d99a9ef0f7de1b618a">
<div>I wrote a <a href="https://blog.kowalczyk.info/software/fast-file-finder-for-windows/">small utility for Windows</a>. It indexes a hard-drive and allows to find a file by name in under a second.</div>
<div>It might surprise you that I spent more time on things that are not related to core functionality. Let’s call it a tax of shipping desktop software.</div>
<div><img class="blog-img" src="https://blog.kowalczyk.info/software/fast-file-finder-for-windows/" alt="" /></div>
<div>Here are some of the taxes you need to pay to ship a desktop application to users.</div>
<h2 class="hdr-with-anchor" id="logging"><div>Logging</div><a class="header-anchor" href="#logging">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>In the long term you want to be able to diagnose problems quickly and logging helps in that.</div>
<div>So I’ve implemented logging to a file.</div>
<div>I went beyond basics and implemented a way to easily see the logs in a window. Simple to implement as all you need is to use built-in read-only text box control.</div>
<div>You can toggle log window from File menu. As an additional touch, if it logs an error and it’s a dev build, it automatically opens a window.</div>
<h2 class="hdr-with-anchor" id="website"><div>Website</div><a class="header-anchor" href="#website">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You need to have <a href="https://blog.kowalczyk.info/software/fast-file-finder-for-windows/">a website</a>. It’s a simple website: few screenshots and a download button, but it does take a few hours to make.</div>
<div>It also has a <a href="https://blog.kowalczyk.info/software/fast-file-finder-for-windows/feedback.html">feedback page,</a> so that people can tell me how to improve the program.</div>
<h2 class="hdr-with-anchor" id="signing-certificate"><div>Signing certificate</div><a class="header-anchor" href="#signing-certificate">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>On Windows anti-virus software and Microsoft’s anti-phishing systems are lousy and often flag innocent software as malware.</div>
<div>You can decrease the probability of that by signing your installer and executables with software signing certificate.</div>
<div>Unfortunately, this certificate is both expensive and pain in the ass to buy. Entities selling them want a proof of your (or your company) identity but the rules are often bureaucratic idiocy.</div>
<h2 class="hdr-with-anchor" id="installer"><div>Installer</div><a class="header-anchor" href="#installer">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You need an installer because it makes the life of your users easier.</div>
<div><img class="blog-img" src="/img/29082c30f062f87de8996cd9a5e07eba3e2fd67e.png" alt="" /></div>
<div>What installer does is not complicated:</div>
<ul>
<li>copy the files in the right place</li>
<li>create necessary registry entries for un-installation</li>
<li>create a shortcut on the desktop and in Start Menu</li>
<li>implement un-installation logic to delete that which has been created during installation (files, shortcuts, registry entries)</li>
</ul>
<h2 class="hdr-with-anchor" id="auto-update-system"><div>Auto-update system</div><a class="header-anchor" href="#auto-update-system">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I want the users to get the latest version in the easiest possible way, One possible way to implement it:</div>
<div><img class="blog-img" src="/img/062fada349b758d345579a3730b6448d369e1f1c.png" alt="" /></div>
<h2 class="hdr-with-anchor" id="build-system"><div>Build system</div><a class="header-anchor" href="#build-system">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>To make release process smooth, you need automated build and release system.</div>
<div>In my case it’s a Go program that builds the software, signs it, uploads to Digital Ocean Spaces for storage, updates the info needed for auto-update system.</div>
<h2 class="hdr-with-anchor" id="the-taxes-add-up"><div>The taxes add up</div><a class="header-anchor" href="#the-taxes-add-up">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Those things are not hard to implement. Individually they don’t take a lot of time to implement.</div>
<div>But when you add it all up, for this particular project I estimate I spent more time building those system than on the core functionality.</div>
<div>The bright light in the tunnel is:</div>
<ul>
<li>I can re-use most of this code in other software</li>
<li>it only needs to be written once. The more time I spend on the app, the less expensive those things are as percentage of total development time</li>
</ul>
</div>
Lessons learned porting 50k loc from Java to Go2019-04-05T00:00:00Ztag:blog.kowalczyk.info,2019-04-05:/article/19f2fe97f06a47c3b1f118fd06851fad/lessons-learned-porting-50k-loc-from-java-to-go.html<div class="notion-page" id="19f2fe97f06a47c3b1f118fd06851fad">
<div>I was contracted to port a large Java code base to Go.</div>
<div>The code in question is a Java client for <a target="_blank" href="https://ravendb.net/">RavenDB</a>, a NoSQL JSON document database. Code with tests was around 50 thousand lines.</div>
<div>The result of the port is a <a target="_blank" href="https://github.com/ravendb/ravendb-go-client">Go client</a>.</div>
<div>This article describes what I’ve learn in the process.</div>
<h2 class="hdr-with-anchor" id="testing-code-coverage"><div>Testing, code coverage</div><a class="header-anchor" href="#testing-code-coverage">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Large projects benefit greatly from automated testing and tracking code coverage.</div>
<div>I used TravisCI and AppVeyor for testing. <a target="_blank" href="http://codecov.io">Codecov.io</a> for code coverage. There are many other services.</div>
<div>I used both AppVeyor and TravisCI because a year ago Travis didn’t have Windows support and AppVeyor didn’t have Linux support.</div>
<div>Today if I was settings this up from scratch, I would stick with just AppVeyor, as it can now do both Linux and Windows testing and the future of TravisCI is murky, after it was acquired by private equity firm and reportedly fired the original dev team.</div>
<div>Codecov is barely adequate. For Go, they count non-code lines (comments etc.) as not executed. It’s impossible to get 100% code coverage as reported by the tool. Coveralls seems to have the same problem.</div>
<div>It’s better than nothing but there’s an opportunity to do things better, especially for Go programs.</div>
<h2 class="hdr-with-anchor" id="go-s-race-detector-is-great"><div>Go’s race detector is great</div><a class="header-anchor" href="#go-s-race-detector-is-great">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Parts of the code use concurrency and it’s really easy to get concurrency wrong.</div>
<div>Go provides race detector that can be enabled with <code>-race</code> flag during compilation.</div>
<div>It slows down the program but additional checks can detect if you’re concurrently modifying the same memory location.</div>
<div>I always run tests with <code>-race</code> enabled and it alerted me to numerous races, which allowed me to fix them promptly.</div>
<h2 class="hdr-with-anchor" id="building-custom-tools-for-testing"><div>Building custom tools for testing</div><a class="header-anchor" href="#building-custom-tools-for-testing">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>In a project that big it’s impossible to verify correctness by inspection. Too much code to hold in your head at once.</div>
<div>When a test fails, it can be a challenge to figure out why just from the information in the test failure.</div>
<div>Database client driver talks to RavenDB database server over HTTP using JSON to encode commands and results.</div>
<div>When porting Java tests to Go, it was very useful to be able to capture the HTTP traffic between Java client and server and compare it with HTTP traffic generated by Go port.</div>
<div>I built custom tools to help me do that.</div>
<div>For capturing HTTP traffic in Java client, I built a <a target="_blank" href="https://github.com/kjk/httplogproxy">logging HTTP proxy</a> in Go and directed Java client to use that HTTP proxy.</div>
<div>For Go client, I built <a target="_blank" href="https://github.com/ravendb/ravendb-go-client/blob/20fade9ee6d22d60c7babf4a155c4de5bf4cfd3b/request_executor.go#L23">a hook in the library</a> that allows to intercept HTTP requests. I used it to log the traffic to a file.</div>
<div>I was then able to compare HTTP traffic generated by Java client to traffic generated by my Go port and spot the differences.</div>
<h2 class="hdr-with-anchor" id="porting-process"><div>Porting process</div><a class="header-anchor" href="#porting-process">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>You can’t just start porting 50 thousand lines of code in random order. Without testing and validating after every little step I’m sure I would be defeated by complexity.</div>
<div>I was new to RavenDB and Java code base. My first step was to get a high-level understanding how Java code works.</div>
<div>At the core the client talks to the server via HTTP protocol. I captured the traffic, looked at it and wrote the simplest Go code to talk the server.</div>
<div>When that was working it gave me confidence I’ll be able to replicate the functionality.</div>
<div>My first milestone was to port enough code to be able to port the simplest Java test.</div>
<div>I used a combination of bottom-up and top-down approach.</div>
<div>Bottom-up part is where I identified the code at the bottom of call chain responsible for sending commands to the server and parsing responses and ported those.</div>
<div>The top-down part is where I stepped through the test I was porting to identify which parts of the code need to be ported to implement that part.</div>
<div>After successfully porting the first step, the rest of the work was porting one test at a time, also porting all the necessary code needed to make the test work.</div>
<div>After the tests were ported and passing, I did improvements to make the code more Go-ish.</div>
<div>I believe that this step-by-step approach was crucial to completing the work.</div>
<div>Psychologically, when faced with a year-long project, it’s important to have smaller, intermediate milestones. Hitting those kept me motivated.</div>
<div>Keeping the code compiling, running and passing tests at all times is also good. Allowing bugs to accumulate can make it very hard to fix them when you finally get to it.</div>
<h2 class="hdr-with-anchor" id="challenges-of-porting-java-to-go"><div>Challenges of porting Java to Go</div><a class="header-anchor" href="#challenges-of-porting-java-to-go">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The objective of the port was to keep it as close as possible to Java code base, as it needs to be kept in sync with Java changes in the future.</div>
<div>I’m somewhat surprised how much code I ported in a line-by-line fashion. The most time consuming part of the port was reversing the order of variable declaration, from Java’s <code>type name</code> to Go’s <code>name type</code>. I wish there was a tool that would do that part for me.</div>
<h3 class="hdr-with-anchor" id="string-vs-string"><div>String vs. string</div><a class="header-anchor" href="#string-vs-string">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>In Java, <code>String</code> is an object that really is a reference (a pointer). As a result, a string can be <code>null</code>.</div>
<div>In Go <code>string</code> is a value type. It can’t be <code>nil</code>, only empty.</div>
<div>It wasn’t a big deal and most of the time I could mechanically replace <code>null</code> with <code>""</code>.</div>
<h3 class="hdr-with-anchor" id="errors-vs-exceptions"><div>Errors vs. exceptions</div><a class="header-anchor" href="#errors-vs-exceptions">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Java uses exceptions to communicate errors.</div>
<div>Go returns values of <code>error</code> interface.</div>
<div>Porting wasn’t difficult but it did require changing lots of function signatures to return error values and propagate them up the call stack.</div>
<h3 class="hdr-with-anchor" id="generics"><div>Generics</div><a class="header-anchor" href="#generics">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Go doesn’t have them (yet).</div>
<div>Porting generic APIs was the biggest challenge.</div>
<div>Here’s an example of a generic method in Java:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">public</span> <span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="n">T</span> <span class="nf">load</span><span class="o">(</span><span class="n">Class</span><span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="n">clazz</span><span class="o">,</span> <span class="n">String</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
</span></span></code></pre>
<div>And the caller:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="nx">Foo</span> <span class="nx">foo</span> <span class="p">=</span> <span class="nf">load</span><span class="p">(</span><span class="nx">Foo</span><span class="p">.</span><span class="nx">class</span><span class="p">,</span> <span class="s">"id"</span><span class="p">)</span>
</span></span></code></pre>
<div>In Go, I used two strategies.</div>
<div>One is to use <code>interface{}</code>, which combines value and its type, similar to <code>object</code> in Java. This is not preferred approach. While it works, operating on <code>interface{}</code> is clumsy for the user of the library.</div>
<div>In some cases I was able to use reflection and the above code was ported as:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">Load</span><span class="p">(</span><span class="nx">result</span> <span class="kd">interface</span><span class="p">{},</span> <span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span>
</span></span></code></pre>
<div>I could use reflection to query type of <code>result</code> and create values of that type from JSON document.</div>
<div>And the caller side:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">result</span> <span class="o">*</span><span class="nx">Foo</span>
</span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nf">Load</span><span class="p">(</span><span class="o">&</span><span class="nx">result</span><span class="p">,</span> <span class="s">"id"</span><span class="p">)</span>
</span></span></code></pre>
<h3 class="hdr-with-anchor" id="function-overloading"><div>Function overloading</div><a class="header-anchor" href="#function-overloading">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Go doesn’t have it (and most likely will never have it).</div>
<div>I can’t say I found a good solution to port those.</div>
<div>In some cases overloading was used to create shorter helpers:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">foo</span><span class="o">(</span><span class="kt">int</span> <span class="n">a</span><span class="o">,</span> <span class="n">String</span> <span class="n">b</span><span class="o">)</span> <span class="o">{}</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">foo</span><span class="o">(</span><span class="kt">int</span> <span class="n">a</span><span class="o">)</span> <span class="o">{</span> <span class="n">foo</span><span class="o">(</span><span class="n">a</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span> <span class="o">}</span>
</span></span></code></pre>
<div>Sometimes I would just drop the shorter helper.</div>
<div>Sometimes I would write 2 functions:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">foo</span><span class="p">(</span><span class="nx">a</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">fooWithB</span><span class="p">(</span><span class="nx">a</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">b</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{}</span>
</span></span></code></pre>
<div>When number of potential arguments was large I would sometimes do:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">FooArgs</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">A</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl"> <span class="nx">B</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">foo</span><span class="p">(</span><span class="nx">args</span> <span class="o">*</span><span class="nx">FooArgs</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
</span></span></code></pre>
<h3 class="hdr-with-anchor" id="inheritance"><div>Inheritance</div><a class="header-anchor" href="#inheritance">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Go is not especially object-oriented and doesn’t have inheritance.</div>
<div>Simple cases of inheritance can be ported with embedding.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">B</span> <span class="o">:</span> <span class="n">A</span> <span class="o">{</span> <span class="o">}</span>
</span></span></code></pre>
<div>Can sometimes be ported as:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">A</span> <span class="kd">struct</span> <span class="p">{</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">B</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">A</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>We’ve embedded <code>A</code> inside <code>B</code>, so <code>B</code> inherit all the methods and fields of <code>A</code>.</div>
<div>It doesn’t work for virtual functions.</div>
<div>There is no good way to directly port code that uses virtual functions.</div>
<div>One option to emulate virtual function is to use embedding of structs and function pointers. This essentially re-implements virtual table that Java gives you for free as part of <code>object</code> implementation.</div>
<div>Another option is to write a stand-alone function that dispatches the right function for a given type by using type switch.</div>
<h3 class="hdr-with-anchor" id="interfaces"><div>Interfaces</div><a class="header-anchor" href="#interfaces">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Both Java and Go have interfaces but they are different things, like apples and salami.</div>
<div>A few times I did create a Go interface type that replicated Java interface.</div>
<div>In more cases I dropped interfaces and instead exposed concrete structs in the API.</div>
<h3 class="hdr-with-anchor" id="circular-imports-between-packages"><div>Circular imports between packages</div><a class="header-anchor" href="#circular-imports-between-packages">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Java allows circular imports between packages.</div>
<div>Go does not.</div>
<div>As a result I was not able to replicate the package structure of Java code in my port.</div>
<div>For simplicity I went with a single package. Not ideal, because it ended up being very large package. So large, in fact, that Go 1.10 couldn’t handle so many source files in a single package on Windows. Luckily it was fixed in Go 1.11.</div>
<h3 class="hdr-with-anchor" id="private-public-protected"><div>Private, public, protected</div><a class="header-anchor" href="#private-public-protected">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Go’s designers are under-appreciated. Their ability to simplify concepts is unmatched and access control is one example of that.</div>
<div>Other languages gravitate to fine-grained access control: public, private, protected specified with the smallest possible granularity (per class field and method).</div>
<div>As a result a library implementing some functionality has the same access to other classes in the same library as external code using that library.</div>
<div>Go simplified that by only having public vs. private and scoping access to package level.</div>
<div>That makes more sense.</div>
<div>When I write a library to, say, parse markdown, I don’t want to expose internals of the implementation to users of the library. But hiding those internals from myself is counter-productive.</div>
<div>Java programmers noticed that issue and sometimes use an interface as a hack to fix over-exposed classes. By returning an interface instead of a a concrete class, you can hide some of the public APIs available to direct users of the class.</div>
<h3 class="hdr-with-anchor" id="concurrency"><div>Concurrency</div><a class="header-anchor" href="#concurrency">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Go’s concurrency is simply the best and a built-in race detector is of great help in repelling concurrency bugs.</div>
<div>That being said, in my first porting pass I went with emulating Java APIs. For example, I implemented a facsimile of Java’s <code>CompletableFuture</code> class.</div>
<div>Only after the code was working I would re-structure it to be more idiomatic Go.</div>
<h3 class="hdr-with-anchor" id="fluent-function-chaining"><div>Fluent function chaining</div><a class="header-anchor" href="#fluent-function-chaining">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>RavenDB has very sophisticated querying capabilities. Java client uses method chaining for building queries:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="n">List</span><span class="o"><</span><span class="n">ReduceResult</span><span class="o">></span> <span class="n">results</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="na">query</span><span class="o">(</span><span class="n">User</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"> <span class="o">.</span><span class="na">groupBy</span><span class="o">(</span><span class="s">"name"</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"> <span class="o">.</span><span class="na">selectKey</span><span class="o">()</span>
</span></span><span class="line"><span class="cl"> <span class="o">.</span><span class="na">selectCount</span><span class="o">()</span>
</span></span><span class="line"><span class="cl"> <span class="o">.</span><span class="na">orderByDescending</span><span class="o">(</span><span class="s">"count"</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"> <span class="o">.</span><span class="na">ofType</span><span class="o">(</span><span class="n">ReduceResult</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"> <span class="o">.</span><span class="na">toList</span><span class="o">();</span>
</span></span></code></pre>
<div>This only works in languages that communicate errors via exceptions. When a function additionally returns an error, it’s no longer possible to chain it like that.</div>
<div>To replicate chaining in Go I used a “stateful error” approach:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Query</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="kt">error</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">q</span> <span class="o">*</span><span class="nx">Query</span><span class="p">)</span> <span class="nf">WhereEquals</span><span class="p">(</span><span class="nx">field</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">val</span> <span class="kd">interface</span><span class="p">{})</span> <span class="o">*</span><span class="nx">Query</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">q</span><span class="p">.</span><span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">q</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// logic that might set q.err
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nx">q</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">q</span> <span class="o">*</span><span class="nx">Query</span><span class="p">)</span> <span class="nf">GroupBy</span><span class="p">(</span><span class="nx">field</span> <span class="kt">string</span><span class="p">)</span> <span class="o">*</span><span class="nx">Query</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">q</span><span class="p">.</span><span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">q</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// logic that might set q.err
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nx">q</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">q</span> <span class="o">*</span><span class="nx">Query</span><span class="p">)</span> <span class="nf">Execute</span><span class="p">(</span><span class="nx">result</span> <span class="nx">inteface</span><span class="p">{})</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">q</span><span class="p">.</span><span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">q</span><span class="p">.</span><span class="nx">err</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// do logic
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span></code></pre>
<div>This can be chained:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">result</span> <span class="o">*</span><span class="nx">Foo</span>
</span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nf">NewQuery</span><span class="p">().</span><span class="nf">WhereEquals</span><span class="p">(</span><span class="s">"Name"</span><span class="p">,</span> <span class="s">"Frank"</span><span class="p">).</span><span class="nf">GroupBy</span><span class="p">(</span><span class="s">"Age"</span><span class="p">).</span><span class="nf">Execute</span><span class="p">(</span><span class="o">&</span><span class="nx">result</span><span class="p">)</span>
</span></span></code></pre>
<h3 class="hdr-with-anchor" id="json-marshaling"><div>JSON marshaling</div><a class="header-anchor" href="#json-marshaling">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>Java doesn’t have a built-in marshaling and the client uses Jackson JSON library.</div>
<div>Go has JSON support in standard library but it doesn’t provide as many hooks for tweaking marshaling process.</div>
<div>I didn’t try to match all of Java’s functionality as what is provided by Go’s built-in JSON support seems to be flexible enough.</div>
<h3 class="hdr-with-anchor" id="go-code-is-shorter"><div>Go code is shorter</div><a class="header-anchor" href="#go-code-is-shorter">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h3>
<div>This is not so much a property of Java but the culture which dictates what is considered an idiomatic code.</div>
<div>In Java setter and getter methods are common. As a result, Java code:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">Foo</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">private</span> <span class="kt">int</span> <span class="n">bar</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setBar</span><span class="o">(</span><span class="kt">int</span> <span class="n">bar</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">this</span><span class="o">.</span><span class="na">bar</span> <span class="o">=</span> <span class="n">bar</span><span class="o">;</span>
</span></span><span class="line"><span class="cl"> <span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getBar</span><span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">bar</span><span class="o">;</span>
</span></span><span class="line"><span class="cl"> <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre>
<div>ends up in Go as:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Foo</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">Bar</span> <span class="kt">int</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>3 lines vs. 11 lines. It does add up when you have a lot of classes with lots of members.</div>
<div>Most other code ends up being of equivalent length.</div>
<h2 class="hdr-with-anchor" id="notion-for-organizing-the-work"><div>Notion for organizing the work</div><a class="header-anchor" href="#notion-for-organizing-the-work">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I’m a heavy user of <a target="_blank" href="https://www.notion.so">Notion.so</a>, a hierarchical note taking application.</div>
<div>Here’s how I used Notion to organize my work on Go port:</div>
<div><img class="blog-img" src="/img/842c3652a30dcf4c430b62f9594d05c605f12dc8.png" alt="" /></div>
<div>Here’s what’s there:</div>
<ul>
<li><div>not shown above, I have a page that is a calendar view where I take short notes about what I work on on a given day and how much time I spent. This is important information since it was a hourly contract. Thanks to those notes I know that I spent 601 hours over 11 months</div></li>
<li><div>clients like to know the progress. I had a page for each month were I summarized the work done like this:
<img class="blog-img" src="/img/93ab5039a61400a8966adda9b1775025125ed050.png" alt="" /></div>
<div>Those pages were shared with the client.</div></li>
<li><div>A short-term todo list helps when starting work each day:
<img class="blog-img" src="/img/791f7ae2d106b220e48a56d599119b64452ace6b.png" alt="" /></div></li>
<li><div>I even managed invoices as Notion pages and used “Export to PDF” function to generate PDF version of the invoice</div></li>
</ul>
<h2 class="hdr-with-anchor" id="additional-resources"><div>Additional resources</div><a class="header-anchor" href="#additional-resources">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I’ve provided some additional commentary in response to questions:</div>
<ul>
<li>in <a target="_blank" href="https://news.ycombinator.com/item?id=19589614">Hacker News discussion</a></li>
<li>in <a target="_blank" href="https://old.reddit.com/r/golang/comments/ba0lsm/lessons_learned_porting_50k_loc_from_java_to_go/">/r/golang discussion</a></li>
</ul>
<div>Other material:</div>
<ul>
<li>if you need a NoSQL, JSON document database, give <a target="_blank" href="https://ravendb.net/">RavenDB</a> a try. It’s chock full of advanced features</li>
<li>if you’re programming in Go, try a free <a target="_blank" href="https://www.programming-books.io/essential/go/">Essential Go</a> programming book</li>
<li>if you’re interested in Notion, I’m world’s most advanced user of Notion:
<ul>
<li>I <a href="/article/88aee8f43620471aa9dbcad28368174c/how-i-reverse-engineered-notion-api.html">reverse engineered</a> Notion API</li>
<li>I wrote an unofficial <a target="_blank" href="https://github.com/kjk/notionapi">Go library</a> for Notion API</li>
<li>all content on this website is written in Notion and published with <a href="/article/a8cf04d756ec4963905960822b004440/powering-a-blog-with-notion-and-netlify.html">my custom toolchain</a></li>
</ul></li>
</ul>
</div>
Trade offs in designing versatile log format2019-03-25T00:00:00Ztag:blog.kowalczyk.info,2019-03-25:/article/fc9203f7c72a4532b1ae51d018fef7b3/trade-offs-in-designing-versatile-log-format.html<div class="notion-page" id="fc9203f7c72a4532b1ae51d018fef7b3">
<div>This article shows that when designing software even seemingly simple things are complicated and trade offs abound.</div>
<div>I wanted to log events to a file. I had several requirements for my design:</div>
<ul>
<li>it should be simple and therefore easy to implement</li>
<li>it should be human-readable</li>
<li>it should allow various types of events</li>
</ul>
<div>It’s not a hard problem.</div>
<div>I could log it as stream of JSON objects. It would allow different types of events. It’s easy to implement (in the sense that there’s a library in every language for the hard part of encoding/decoding JSON).</div>
<div>One thing: it’s not particularly human friendly. I wouldn’t enjoy looking at <code>tail -f</code> of a JSON log.</div>
<div>How about something like Apache server logs:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
</span></span></code></pre>
<div>It’s readable but doesn’t allow for different types of events.</div>
<div>Implementing this format isn’t challenging but the format is ad-hoc. For each kind of data I would have to write a completely new formatter / parser.</div>
<div>How about something more structured:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">ip: 127.0.0.1
</span></span><span class="line"><span class="cl">request: GET /apache_pb.gif HTTP/1.0
</span></span><span class="line"><span class="cl">status code: 200
</span></span></code></pre>
<div>We can write a library that serializes key / value pairs and that gives us ability to write different kinds of events to the same file with generic code.</div>
<div>But hold on, how do we know where one event ends and another starts?</div>
<div>We must amend our format add add a record separator, like <code>---</code>:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">ip: 127.0.0.1
</span></span><span class="line"><span class="cl">request: GET /apache_pb.gif HTTP/1.0
</span></span><span class="line"><span class="cl">status code: 200
</span></span><span class="line"><span class="cl">---
</span></span></code></pre>
<div>We’re not there yet. We use newline to separate lines that encode a single key / value pair. What if the value has a newline in it?</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">body: hello fred
</span></span><span class="line"><span class="cl">I'm writing to you...
</span></span></code></pre>
<div>We could escape the value:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">body: hello fred\nI'm writing to you...
</span></span></code></pre>
<div>Escaping is not a good solution.</div>
<div>Without escaping, we can just write out the data as-is.</div>
<div>With escaping we have to scan the whole thing to determine if it needs escaping, and escape if it does.</div>
<div>Same goes for un-escaping.</div>
<div>This is slow and error prone.</div>
<div>Furthermore, what if the value is really large? It won’t be readable encoded on a single line.</div>
<div>Let’s amend our format to accommodate large values:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">body:+33
</span></span><span class="line"><span class="cl">hello fred
</span></span><span class="line"><span class="cl">I'm writing to you...
</span></span></code></pre>
<div><code>33</code> is the size of the value. Knowing the size we don’t need escaping. It’s faster, simpler to implement, more readable and supports binary data (like images).</div>
<div>To formalize, key / value pair can be encoded in 2 ways:</div>
<ul>
<li><code>${key}: ${value}\n</code></li>
<li><code>${key}:+${sizeOfValue}\n${value}</code></li>
</ul>
<div>Let’s revisit the idea of using a separator:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">ip: 10.0.0.1
</span></span><span class="line"><span class="cl">request: GET / HTTP/1.0
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">ip: 10.0.0.2
</span></span><span class="line"><span class="cl">request: GET /index.html HTTP/1.0
</span></span><span class="line"><span class="cl">---
</span></span></code></pre>
<div>For delimiting records we can use the same tricks we used for encoding large values:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">36
</span></span><span class="line"><span class="cl">ip: 10.0.0.1
</span></span><span class="line"><span class="cl">request: GET / HTTP/1.0
</span></span><span class="line"><span class="cl">43
</span></span><span class="line"><span class="cl">car: Toyota Corolla
</span></span><span class="line"><span class="cl">year: 2018
</span></span><span class="line"><span class="cl">price: 21000
</span></span></code></pre>
<div>First line is size of data as a string + newline. The data of this size follows.</div>
<div>This is very generic framing, agnostic to what is inside. It could be a picture, a JSON-encoded data or our key / value format.</div>
<div>We’ve arrived at a layered design:</div>
<ul>
<li>first layer is encoding arbitrary chunks of data by writing size and then data</li>
<li>second layer is key / value format inside the data</li>
</ul>
<div>It would be even simpler if the size was 8 byte, 64-bit integer. It wouldn’t be human-readable, though, so I picked a slightly more complicated, string-based encoding.</div>
<div>It’s not quite right yet.</div>
<div>Above we have logged an HTTP requests info and car info in the same file.</div>
<div>How do we know what type of record did we read?</div>
<div>Let’s amend our framing with optional name:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">36 httplog
</span></span><span class="line"><span class="cl">ip: 10.0.0.1
</span></span><span class="line"><span class="cl">request: GET / HTTP/1.0
</span></span><span class="line"><span class="cl">43 carinfo
</span></span><span class="line"><span class="cl">car: Toyota Corolla
</span></span><span class="line"><span class="cl">year: 2018
</span></span><span class="line"><span class="cl">price: 21000
</span></span></code></pre>
<div>Adding optional name (<code>httplog</code> and <code>carinfo</code>) allows us to know what kind of data is encoded in a given chunk of data.</div>
<div>Finally let’s add non-optional timestamp in <a target="_blank" href="https://en.wikipedia.org/wiki/Unix_time">Unix Epoch</a> format in milliseconds:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">36 1553564864010 httplog
</span></span><span class="line"><span class="cl">ip: 10.0.0.1
</span></span><span class="line"><span class="cl">request: GET / HTTP/1.0
</span></span><span class="line"><span class="cl">43 1553564864115 carinfo
</span></span><span class="line"><span class="cl">car: Toyota Corolla
</span></span><span class="line"><span class="cl">year: 2018
</span></span><span class="line"><span class="cl">price: 21000
</span></span></code></pre>
<div>Unlike name, which is optional, I decided timestamp is not optional. You can set it to 0 if you don’t need it but for most logging needs you want a timestamp.</div>
<div>Traditional Unix Epoch has precision of a second and that seems not enough. Millisecond-precision seems good enough. Nanosecond was also an option but seems like an over-kill.</div>
<div>What if you need more structure than key / value pairs?</div>
<div>Simplicity means that you can’t implement every possible feature. This format doesn’t preclude more structure: you can always use JSON as the value in key / value field. It’s just not going to be as easy to use.</div>
<h2 class="hdr-with-anchor" id="implementing-the-thing"><div>Implementing the thing</div><a class="header-anchor" href="#implementing-the-thing">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>This is not just theoretical exploration. I’ve implemented this format as a <a target="_blank" href="https://github.com/kjk/siser">Go package siser</a>.</div>
<div>It took me 2 days to implement. It’s just under 500 lines of code:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">$ wc -l reader.go record.go util.go writer.go
</span></span><span class="line"><span class="cl"> 167 reader.go
</span></span><span class="line"><span class="cl"> 196 record.go
</span></span><span class="line"><span class="cl"> 79 util.go
</span></span><span class="line"><span class="cl"> 57 writer.go
</span></span><span class="line"><span class="cl"> 499 total
</span></span></code></pre>
<div>Not counting tests but I do have them because I need this code to be rock solid. Storing data is serious business and has to be reliable.</div>
<div>How fast is it? I benchmarked it against JSON encoding:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">BenchmarkSiserMarshal-12 1000000 1136 ns/op
</span></span><span class="line"><span class="cl">BenchmarkJSONMarshal-12 1000000 1407 ns/op
</span></span><span class="line"><span class="cl">BenchmarkSiserUnmarshal-12 5000000 374 ns/op
</span></span><span class="line"><span class="cl">BenchmarkJSONUnmarshal-12 500000 3353 ns/op
</span></span></code></pre>
<div>The benchmark is mostly to make sure that I didn’t make a big performance mistake. It would be embarrassing to be slower than JSON encoding. As a side note: it’s impressive how fast JSON marshaling in Go standard library is.</div>
<div>I’m moving all my logging needs to this format.</div>
<div>This format is also good for very simple stores aka databases. The file can be seen as an append-only database log. To update a record I just write a new entry and it’ll over-write earlier entry.</div>
<h2 class="hdr-with-anchor" id="the-roads-not-taken"><div>The roads not taken</div><a class="header-anchor" href="#the-roads-not-taken">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>After reading this you might not be impressed. This design is clearly inspired by many others. All I did is put known things in a specific, but very familiar, way.</div>
<div>Simplicity is insidiously non-trivial. The design described here is a v2 of siser library. First design used <code>---</code> for record separator, didn’t have name and timestamp.</div>
<div>Only several months after first version I got enough insight to improve it.</div>
<div>In turn siser was an evolution of less robust ideas I implemented earlier. It took experiencing the limitations of those earlier designs in real use for better ideas to emerge.</div>
<div>This is even more apparent when you look at mistake of others.</div>
<div>MIME is a format used for encoding e-mail messages. While in some ways it’s very close to this format, they made a mistake of using a boundary string for separating multiple parts. Compared to framing data with size prefix it’s so much hard to implement. A MIME decoder is more than 500 lines of code and doesn’t offer more features.</div>
<div>Other example of massive mistakes of the past is choosing XML as a format for describing ant or Visual Studio build files. XML is super slow, unreadable for humans, hard to work with programmatically. A conforming implementation of XML parser requires thousands of lines of code.</div>
<div>Obvious things are often only obvious in hindsight.</div>
</div>
How I implemented Oembed Proxy for GitHub2018-10-13T00:00:00Ztag:blog.kowalczyk.info,2018-10-13:/article/7d25ad342c514a47a00f6e0c4ba84cbc/how-i-implemented-oembed-proxy-for-github.html<div class="notion-page" id="7d25ad342c514a47a00f6e0c4ba84cbc">
<h1 class="hdr-with-anchor" id="why-oembed-proxy-for-github"><div>Why Oembed Proxy for GitHub</div><a class="header-anchor" href="#why-oembed-proxy-for-github">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>I’m writing a programming book <a target="_blank" href="https://www.programming-books.io/essential/go/">Essential Go</a> in <a target="_blank" href="https://www.notion.so">Notion</a> and I need to include code snippets.</div>
<div>Notion has support for code blocks but it’s not good enough for my use case.</div>
<div>I want to make sure the code compiles so I write small programs and store them in GitHub repository. My custom book building script compiles and runs the programs to ensure the code is correct.</div>
<div>Notion supports embedding GitHub’s gists but not files from git repositories hosted on GitHub.</div>
<div>I researched things and turns out there’s a standard called <a target="_blank" href="https://oembed.com/">Oembed</a> that was created to enable embedding arbitrary content from one website in another.</div>
<div>Notion supports <a target="_blank" href="https://oembed.com/">Oembed</a>.</div>
<div>I didn’t find existing service that can provide Oembed support for GitHub repositories so I built one myself.</div>
<div>This article describes the high-level design of <a target="_blank" href="https://www.onlinetool.io/gitoembed/">Oembed Proxy for GitHub</a>.</div>
<h1 class="hdr-with-anchor" id="what-is-oembed"><div>What is Oembed?</div><a class="header-anchor" href="#what-is-oembed">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Let’s say you’re implementing a rich-text editor on the web and you want to allow embedding arbitrary content from other services: a tweet, a youtube video, a flickr photo.</div>
<div>You can add code to support each service you know about (and pray that they provide a stable way to get the necessary information) but it’s not scalable. There are way too many web services out there and more are created every day.</div>
<div>Some people noticed the problem and created Oembed protocol that provides a standard way to expose content for embedding. Now you need to only write code to support Oembed.</div>
<div>Here’s how it works, using Oembed Proxy for GitHub as an example.</div>
<div>In our example Notion is an Oembed client that wants to embed a file from GitHub repository <a target="_blank" href="https://github.com/essentialbooks/books/blob/master/README.md">https://github.com/essentialbooks/books/blob/master/README.md</a> in the body of a document.</div>
<div>Notion supports Oembed standard. If GitHub supported Oembed on their servers, you would add embed block and use <a target="_blank" href="https://github.com/essentialbooks/books/blob/master/README.md">https://github.com/essentialbooks/books/blob/master/README.md</a> link directly.</div>
<div>GitHub doesn’t support Oembed so instead, you can use URL via my Oembed Proxy: <a target="_blank" href="https://www.onlinetool.io/gitoembed/widget?url=https%3A%2F%2Fgithub.com%2Fessentialbooks%2Fbooks%2Fblob%2Fmaster%2FREADME.md">https://www.onlinetool.io/gitoembed/widget?url=https%3A%2F%2Fgithub.com%2Fessentialbooks%2Fbooks%2Fblob%2Fmaster%2FREADME.md</a></div>
<div>This is <a target="_blank" href="https://www.onlinetool.io/gitoembed/widget">https://www.onlinetool.io/gitoembed/widget</a> page with GitHub link provided as <code>url</code> argument.</div>
<div>If you view that page it’s the file from GitHub with source code highlighting.</div>
<div>Oembed supports auto-discovery. If you peek at HTML of that page, you’ll see this is <code><head></code> section:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">"alternate"</span> <span class="na">type</span><span class="o">=</span><span class="s">"application/json+oembed"</span> <span class="na">href</span><span class="o">=</span><span class="s">"https://www.onlinetool.io/gitoembed/oembed?format=json&url=https://github.com/essentialbooks/books/blob/master/README.md"</span> <span class="na">title</span><span class="o">=</span><span class="s">"README.md"</span> <span class="p">/></span>
</span></span><span class="line"><span class="cl"><span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">"alternate"</span> <span class="na">type</span><span class="o">=</span><span class="s">"text/xml+oembed"</span> <span class="na">href</span><span class="o">=</span><span class="s">"https://www.onlinetool.io/gitoembed/oembed?format=xml&url=https://github.com/essentialbooks/books/blob/master/README.md"</span> <span class="na">title</span><span class="o">=</span><span class="s">"README.md"</span> <span class="p">/></span>
</span></span></code></pre>
<div>Those are instructions telling Oembed client (Notion in this example) how to get embeddable HTML.</div>
<div>Oembed supports 2 formats for providing this information: JSON and XML. In my testing Notion worked with just <code>application/json+oembed</code> but I implemented both just in case other clients only understand XML.</div>
<div>Oembed client parses HTML to extract those links and, if present, gets Oembed information. In our example it’s in <a target="_blank" href="https://www.onlinetool.io/gitoembed/oembed?format=json&url=https://github.com/essentialbooks/books/blob/master/README.md">https://www.onlinetool.io/gitoembed/oembed?format=json&url=https://github.com/essentialbooks/books/blob/master/README.md</a> and looks like this:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"version"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"type"</span><span class="p">:</span> <span class="s2">"rich"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"provider_name"</span><span class="p">:</span> <span class="s2">"gitoembed"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"provider_url"</span><span class="p">:</span> <span class="s2">"https://www.onlinetool.io/gitoembed/"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"height"</span><span class="p">:</span> <span class="mi">320</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"width"</span><span class="p">:</span> <span class="mi">720</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"title"</span><span class="p">:</span> <span class="s2">"README.md"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"html"</span><span class="p">:</span> <span class="s2">"\u003ciframe width=\"100%\" height=320 src=\"https://www.onlinetool.io/gitoembed/widget?url=https://github.com/essentialbooks/books/blob/master/README.md\" frameborder=\"0\" onload=\"resizeFrame(this);\"\u003e\u003c/iframe\u003e"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>I hope the format is mostly self-explanatory.</div>
<div>The interesting bit is <code>html</code> field, which is:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="p"><</span><span class="nt">iframe</span>
</span></span><span class="line"><span class="cl"> <span class="na">width</span><span class="o">=</span><span class="s">"100%"</span>
</span></span><span class="line"><span class="cl"> <span class="na">height</span><span class="o">=</span><span class="s">320</span>
</span></span><span class="line"><span class="cl"> <span class="na">src</span><span class="o">=</span><span class="s">"https://www.onlinetool.io/gitoembed/widget?url=https://github.com/essentialbooks/books/blob/master/README.md"</span>
</span></span><span class="line"><span class="cl"> <span class="na">frameborder</span><span class="o">=</span><span class="s">"0"</span>
</span></span><span class="line"><span class="cl"> <span class="na">onload</span><span class="o">=</span><span class="s">"resizeFrame(this);"</span><span class="p">></span>
</span></span><span class="line"><span class="cl"><span class="p"></</span><span class="nt">iframe</span><span class="p">></span>
</span></span></code></pre>
<div>We could send the actual HTML content to insert but it’s more customary to send an iframe which loads the html.</div>
<div>In my implementation <code>src</code> of the iframe is the same page from which we extracted Oembed JSON link so it serves double-duty as both the content and an Oembed pointer to the content. Those could be different URLs.</div>
<h1 class="hdr-with-anchor" id="implementation-details-of-oembed-proxy-for-github"><div>Implementation details of Oembed Proxy for GitHub</div><a class="header-anchor" href="#implementation-details-of-oembed-proxy-for-github">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>First I needed a server. Usually, I use Digital Ocean but this time I went for biggest bang for the buck and used <a target="_blank" href="https://www.scaleway.com/pricing/">C2L server</a> from <a target="_blank" href="https://www.scaleway.com">Scaleway</a>.</div>
<div>For ~$30 I get 8 core server with 32 GB of RAM, 250 GB SSD drive and 600 MBits/s unmetered bandwidth. It’s a bare metal server, not a VPS, so it’s all mine, eliminating risk of noisy neighbors.</div>
<div>On Digital Ocean the closest server with such specs would be $160. I keep a list of <a target="_blank" href="https://www.notion.so/Comparing-prices-of-VPS-servers-dd5c0a813dfe4487a6cd432f82c0c2fc">cheap VPS servers</a> for comparison.</div>
<div>The downside is that the servers are in Europe (you can choose between Amsterdam or Paris) so the latency for users in US will be higher than if the server was hosted in US.</div>
<div>For the OS I went with Ubuntu 18.04. I know it best and it’s one of the most popular distros.</div>
<div>The server is written in Go. It’s my go-to language for writing backend code.</div>
<div>The service isn’t very complicated:</div>
<ul>
<li>it downloads the file via GitHub’s <a target="_blank" href="https://raw.githubusercontent.com/">https://raw.githubusercontent.com</a> url</li>
<li>to avoid over-loading GitHub servers (and exceeding their throttling limits) I cache downloaded files for a day. It’s not infinitely long cache because files on GitHub can change and I don’t want to cache outdated version forever</li>
<li>for code highlighting I use <a target="_blank" href="https://github.com/alecthomas/chroma">chroma</a> library</li>
</ul>
<div>There’s even less of front-end code. Explanation of the service and a way to test it implemented with a form and few lines of JavaScript.</div>
</div>
Powering a blog with Notion and Netlify2018-07-30T00:00:00Ztag:blog.kowalczyk.info,2018-07-30:/article/a8cf04d756ec4963905960822b004440/powering-a-blog-with-notion-and-netlify.html<div class="notion-page" id="a8cf04d756ec4963905960822b004440">
<div>The last iteration of this blog was a Go program running on <a target="_blank" href="https://www.digitalocean.com/">Digital Ocean</a>’s cheapest VM ($5/month).</div>
<div>Recently I’ve made 2 big changes:</div>
<ul>
<li>I converted it to a static site hosted on <a target="_blank" href="https://www.netlify.com/">Netlify</a></li>
<li>I used <a target="_blank" href="https://notion.so">Notion</a> for writing the posts instead of writing markdown files in a text editor</li>
</ul>
<h1 class="hdr-with-anchor" id="moving-to-netlify"><div>Moving to Netlify</div><a class="header-anchor" href="#moving-to-netlify">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>My blog was effectively a static website. It didn’t need a backend so writing a custom server and running it on a VPS was overkill.</div>
<div>Few months back Netlify reduced the price of their cheapest plan to $0 (from $10/month).</div>
<div>I’m always looking to simplify and cheapify my life so I bit the bullet and converted my custom server to generate static HTML files suitable for hosting on Netlify.</div>
<div>If you want to publish a static website and are starting from scratch, the best approach is to use one of the many static site generators (e.g. Hugo or Jekyll).</div>
<div>I already had a lot of content in .html and markdown files accumulated over the years and code to generate the website for serving from custom web server so I refactored to code to generate static HTML instead.</div>
<div>Refactoring process was time consuming but simple.</div>
<div>For dynamically generated html I changed the code to generate a .html file and setup appropriate url => file mapping using <code>_redirect</code> file.</div>
<div>For local testing I use <a target="_blank" href="https://caddyserver.com/">Caddy</a> and generate <code>Caddyfile</code> with appropriate redirects. There are minor differences when testing locally because there are semantic differences between redirect capabilities of Netlify and Caddy but it’s good enough.</div>
<div>Netlify also has an option to do a draft deploy under a unique URL. This is good for previewing the changes before publishing.</div>
<div>At the end of code refactoring I effectively ended up with a custom static site generator.</div>
<h2 class="hdr-with-anchor" id="the-verdict"><div>The verdict</div><a class="header-anchor" href="#the-verdict">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>I’m happy with the result.</div>
<div>Netlify has all the features I care about for a basic website.</div>
<div>Notably they provide free SSL with Let’s Encrypt, allow custom domains and have capable redirect capabilities. They use CDN so should be faster than hosting on a single host.</div>
<div>The only thing I miss is being able to see analytics for 404. With my own server I was logging all requests for pages that don’t exist on my server. Many of them were bots trying to hack me via known vulnerabilities in popular software like WordPress but sometimes it would be caused by my mistake or a request for a valid article incorrectly linked. Seeing those I was able to fix most of them by adding redirects.</div>
<div>I hope Netlify can sustain their generous free plan. Luckily there are plenty of options to host a static website so even if Netlify goes under, it’ll be easy to move somewhere else, like <a target="_blank" href="https://firebase.google.com/docs/hosting/">Firebase Hosting</a>, <a target="_blank" href="http://surge.sh/">surge.sh</a>, <a target="_blank" href="https://pages.github.com/">GitHub pages</a>, <a target="_blank" href="https://about.gitlab.com/features/pages/">GitLab pages</a> and <a target="_blank" href="https://www.notion.so/c674bebe8adf44d18c3a36cc18c131e2">many others</a>.</div>
<h1 class="hdr-with-anchor" id="using-notion-as-content-management-system"><div>Using Notion as Content Management System</div><a class="header-anchor" href="#using-notion-as-content-management-system">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>The easier it is to write, the more I write.</div>
<div>Using markdown files fails the “as easy as possible” part.</div>
<div>In absolute terms, creating a new markdown file doesn’t take much time.</div>
<div>In practice it’s enough friction to deter me from writing. In my worst year I only wrote 1 new blog post.</div>
<div>In a perfect world I would open an app and start writing. When I’m done writing I would publish with a click of a button.</div>
<h2 class="hdr-with-anchor" id="enter-notion"><div>Enter Notion</div><a class="header-anchor" href="#enter-notion">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><a target="_blank" href="https://www.notion.so/">Notion</a> is as close to a perfect writing tool as it gets: open a page and start writing.</div>
<div>The problem is: all that content is trapped in Notion. You can publish (publicly share) a page but I want more flexibility:</div>
<ul>
<li>I want to host on my own domain; my website is partly a tool for marketing myself</li>
<li>I want integration with Google Analytics</li>
<li>I want a custom design</li>
<li>I want to provide rss feed</li>
</ul>
<div>Thankfully I’m a programmer: if something can be done with software, I can do it.</div>
<div>I started a one-man Notion Liberation Front. My goal: liberate content trapped inside Notion.</div>
<div>I <a href="/article/88aee8f43620471aa9dbcad28368174c/how-i-reverse-engineered-notion-api.html">reverse-engineered their API</a>, wrote a <a target="_blank" href="https://github.com/kjk/notionapi">Go library</a> and after another round of code refactoring I had my blog powered by Notion thanks to this <a target="_blank" href="https://github.com/kjk/blog">Go program</a>.</div>
<div>I imported my old blog posts into Notion. They have a decent markdown importer although I did have to do some cleaning up.</div>
<div>I went even further and published most of my notes to my website as well. I still have many private notes in Notion but most of them can be just as well be publicly visible.</div>
<div>There’s no way I could do publish so much content without Notion.</div>
<div>I also automated publishing by using cron functionality in <a target="_blank" href="https://travis-ci.org/">Travis CI</a>. Every day a script downloads latest content from Notion, caches it in git repository and re-generates a website.</div>
<div>Everything now runs on auto-pilot. I can just write new articles in Notion and my website will be automatically updated every day.</div>
</div>
How I reverse engineered Notion API2018-07-23T00:00:00Ztag:blog.kowalczyk.info,2018-07-23:/article/88aee8f43620471aa9dbcad28368174c/how-i-reverse-engineered-notion-api.html<div class="notion-page" id="88aee8f43620471aa9dbcad28368174c">
<div><a href="/article/88aee8f43620471aa9dbcad28368174c/how-i-reverse-engineered-notion-api.html">Notion</a> is a great tool for writing but the content is trapped inside the web app.</div>
<div>The company is working on an official API but I’m impatient.</div>
<div>This article describes how I reverse engineered their API and created a Go library <a target="_blank" href="https://github.com/kjk/notionapi">notionapi</a>.</div>
<h1 class="hdr-with-anchor" id="it-all-began-with-a-failure"><div>It all began with a failure.</div><a class="header-anchor" href="#it-all-began-with-a-failure">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>My first attempt at extracting notion content was traditional web scraping.</div>
<div>I found a Python <a target="_blank" href="https://github.com/shariq/notion-on-firebase">script</a> that uses Selenium to recursively spider a Notion page and publish it to Firebase Hosting.</div>
<div>I ported it to Node to use <a href="/article/ea07db1b9bff415ab180b0525f3898f6/advanced-web-spidering-with-puppeteer.html">Puppeteer</a> (better technology than Selenium).</div>
<div>While it worked this approach is limited to getting a verbatim HTML of the pages as they are rendered by the Notion application.</div>
<div>I wanted to be able to change the look of the page, add elements like footers and headers and navigation bar.</div>
<div>I briefly considered trying to reconstruct the structure of the page from rendered HTML but at best that would be a lot of ugly guesswork.</div>
<h1 class="hdr-with-anchor" id="the-lightbulb-moment"><div>The lightbulb moment</div><a class="header-anchor" href="#the-lightbulb-moment">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Modern Single Page Applications (SPA) work by getting data from the server in structured format (most often JSON) and rendering HTML in the browser with JavaScript.</div>
<div>A trip to Chrome Dev Tools confirmed that Notion works like that.</div>
<div>When loading a Notion page I saw XHR requests like <code>/api/v3/getRecordValues</code> and <code>/api/v3/loadPageChunk</code>.</div>
<div><img class="blog-img" src="/img/1d0a837dbe1bd3454d177f08cee83255f2401591.png" alt="" /></div>
<div>Lucky for me the API is not obfuscated. It returns responses as JSON data. It isn’t hard to figure out the meaning of fields.</div>
<div>Working with the original JSON structure is much easier that trying to reconstruct it from rendered HTML.</div>
<h1 class="hdr-with-anchor" id="building-tools"><div>Building tools</div><a class="header-anchor" href="#building-tools">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>I could have looked at API requests between client and server in Chrome dev tools but it’s not the best workflow.</div>
<div>Instead I wrote node.js script that logs all XHR requests that web browser makes when rendering a given page.</div>
<div>That has several advantages over using dev tools:</div>
<ul>
<li>I could filter out requests to third-party services like amplitude, fullstory and intercom</li>
<li>I could filter out requests that are not interesting like <code>/api/v3/ping</code></li>
<li>I could pretty-print JSON</li>
<li>I could write captured traffic to a file for further analysis</li>
</ul>
<div>Here’s the script:</div>
<div><a target="_blank" href="https://gist.github.com/kjk/f33bc37d6ca8282b5c52b17391384693">https://gist.github.com/kjk/f33bc37d6ca8282b5c52b17391384693</a></div>
<h1 class="hdr-with-anchor" id="the-big-picture-analysis"><div>The big picture analysis</div><a class="header-anchor" href="#the-big-picture-analysis">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>After looking at captured data, the structure of Notion content is not complicated.</div>
<div>Everything, including a top-level page, is a block.</div>
<div>Blocks are identified by a unique id which looks like a standard UUID format.</div>
<div>Blocks are arranged into a tree i.e. some blocks have children.</div>
<div>Blocks have metadata, like creation time, last edit time, version etc.</div>
<div>There are different kinds of blocks: a page, text, todo item, list item etc.</div>
<div>Some blocks have properties specific to that block type. For example a page block has <code>title</code> property.</div>
<div>To get the content of a page we start with its UUID which we can find out because it’s last part of the URL of the page.</div>
<div>We can issue <code>/api/v3/getRecordValues</code> API to get list of blocks in the page and then <code>/api/v3/loadPageChunk</code> to get content of those blocks.</div>
<div>Majority of work was figuring out what kinds of blocks there are, how are they represented in JSON and writing code to to retrieve the data and present it in a format that is easier to work with than the raw data returned by the server.</div>
<h1 class="hdr-with-anchor" id="testing-different-kinds-of-blocks"><div>Testing different kinds of blocks</div><a class="header-anchor" href="#testing-different-kinds-of-blocks">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Notion page consist of different kinds of blocks and we need to know how each block is represented in JSON response.</div>
<div>To investigate it systematically, I’ve created a test page for each kind of block and used the request logging script to look at JSON returned by the server for that block.</div>
<h1 class="hdr-with-anchor" id="writing-go-library"><div>Writing Go library</div><a class="header-anchor" href="#writing-go-library">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Next step was writing a Go library.</div>
<div>I captured sample JSON responses from <code>getRecordValues</code> and <code>loadPageChunk</code> and used <a target="_blank" href="https://app.quicktype.io/">Quicktype</a> to generate Go structures.</div>
<div>I had to tweak them a bit to accommodate variations in JSON structure.</div>
<div>The rest of the effort was writing a helper function that abstracts the details of HTTP requests and returns an easy to use struct describing a notion page.</div>
<div>There result of that work is <a target="_blank" href="https://github.com/kjk/notionapi">notionapi</a> Go package.</div>
<h1 class="hdr-with-anchor" id="using-the-library-in-practice"><div>Using the library in practice</div><a class="header-anchor" href="#using-the-library-in-practice">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>This was not just an academic exercise.</div>
<div>This blog was powered by markdown files I stored in GitHub repository.</div>
<div>My goal was to move the content to Notion, so that I can edit it more easily, convert it to HTML and publish as my website/blog.</div>
<div>You can see the code <a target="_blank" href="https://github.com/kjk/blog">here</a>.</div>
<div>The high-level structure of the code:</div>
<ul>
<li>I use my Go <a target="_blank" href="https://github.com/kjk/notionapi">notionapi</a> library to download the content from Notion</li>
<li>I cache downloaded data and store them in git repository. This is to make sure I have a copy of data even if Notion disappears, to make it faster to tweak publishing code (no need to re-download) and to be nicer to Notion server (no re-downloads unless I absolutely have to)</li>
<li>I convert Notion data to HTML, wrap it in templates for my pages and write HTML files to disk</li>
<li>I deploy to <a target="_blank" href="https://www.netlify.com/">Netlify</a></li>
</ul>
</div>
Advanced web spidering with Puppeteer2018-07-18T00:00:00Ztag:blog.kowalczyk.info,2018-07-18:/article/ea07db1b9bff415ab180b0525f3898f6/advanced-web-spidering-with-puppeteer.html<div class="notion-page" id="ea07db1b9bff415ab180b0525f3898f6">
<div><a target="_blank" href="https://pptr.dev/">Puppeteer</a> is a node.js library that makes it easy to do advanced web scraping and spidering.</div>
<div>Older generation of web scraping and spidering tools would grab and analyze HTML pages as returned by a web server.</div>
<div>It doesn’t work well anymore because less and less website are static HTML pages. Today websites are often applications written in JavaScript that generate HTML on the client, not the server.</div>
<div>To get the final HTML output your scraper needs to run that JavaScript.</div>
<div>That used to be very difficult but Puppeteer makes it easy.</div>
<div>Puppeteer uses Chrome to run web application and uses CDP (<a target="_blank" href="https://chromedevtools.github.io/devtools-protocol/">Chrome DevTools Protocol</a>) to access the webpage.</div>
<div>This article describes some more advanced techniques but let’s start with basic example first.</div>
<h1 class="hdr-with-anchor" id="save-web-page-to-a-file"><div>Save web page to a file</div><a class="header-anchor" href="#save-web-page-to-a-file">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>First install the library:</div>
<ul>
<li><code>yarn add puppeteer</code> when using yarn</li>
<li><code>npm --save puppeteer</code> when using npm</li>
</ul>
<div>This is the simplest possible usage of Puppeteer:</div>
<ul>
<li>navigate to a page of interest</li>
<li>get content of the webpage as HTML and save it to a file</li>
</ul>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">puppeteer</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s2">"puppeteer"</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s2">"fs"</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">run</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">browser</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">puppeteer</span><span class="p">.</span><span class="nx">launch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s2">"https://www.google.com/"</span><span class="p">,</span> <span class="p">{</span> <span class="nx">waitUntil</span><span class="o">:</span> <span class="s2">"networkidle2"</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// hacky defensive move but I don't know a better way:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// wait a bit so that the browser finishes executing JavaScript
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitFor</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">html</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">content</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="nx">fs</span><span class="p">.</span><span class="nx">writeFileSync</span><span class="p">(</span><span class="s2">"index.html"</span><span class="p">,</span> <span class="nx">html</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">run</span><span class="p">();</span>
</span></span></code></pre>
<h1 class="hdr-with-anchor" id="handling-failures"><div>Handling failures</div><a class="header-anchor" href="#handling-failures">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>What if a url you tried to load didn’t exist?</div>
<div>The web server will return the ‘Not Found’ page with HTTP status code <code>404</code> in the response. The above script would treat such page as a perfectly valid response.</div>
<div>Most times you want to handle this as an error case.</div>
<div>For example, if you’re writing a bot that checks for broken links, you want to distinguish <code>404</code> NotFound response from <code>200</code> Ok response.</div>
<div>In HTTP protocol status codes <code>4xx</code> and <code>5xx</code> indicate errors. <code>2xx</code> indicate success and <code>3xx</code> indicate successful redirection.</div>
<div>Puppeteer provides <code>Page.setRequestInterception(true)</code> hook for intercepting HTTP requests before they happen as well as inspecting completed HTTP responses.</div>
<div>Here’s a program that prints information about all HTTP requests and responses:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">puppeteer</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s2">"puppeteer"</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">run</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">browser</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">puppeteer</span><span class="p">.</span><span class="nx">launch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">mainUrl</span> <span class="o">=</span> <span class="s2">"https://blog.kowalczyk.info/pas"</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">mainUrlStatus</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">setRequestInterception</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">page</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"request"</span><span class="p">,</span> <span class="nx">request</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"request url:"</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">request</span><span class="p">.</span><span class="k">continue</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="nx">page</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"requestfailed"</span><span class="p">,</span> <span class="nx">request</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"request failed url:"</span><span class="p">,</span> <span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="nx">page</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"response"</span><span class="p">,</span> <span class="nx">response</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">request</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">status</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"response url:"</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="s2">"status:"</span><span class="p">,</span> <span class="nx">status</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">url</span> <span class="o">===</span> <span class="nx">mainUrl</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">mainUrlStatus</span> <span class="o">=</span> <span class="nx">status</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="nx">mainUrl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"status for main url:"</span><span class="p">,</span> <span class="nx">mainUrlStatus</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">html</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">content</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">run</span><span class="p">();</span>
</span></span></code></pre>
<div>Here’s what it’ll print:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">$ node test.js
</span></span><span class="line"><span class="cl">request url: https://blog.kowalczyk.info/pas
</span></span><span class="line"><span class="cl">response url: https://blog.kowalczyk.info/pas status: 404
</span></span><span class="line"><span class="cl">request url: https://fonts.googleapis.com/css?family=Roboto:400,700&subset=latin,latin-ext
</span></span><span class="line"><span class="cl">response url: https://fonts.googleapis.com/css?family=Roboto:400,700&subset=latin,latin-ext status: 200
</span></span><span class="line"><span class="cl">request url: https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfChc9AMP6lQ.ttf
</span></span><span class="line"><span class="cl">request url: https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu7GxPKTU1Kg.ttf
</span></span><span class="line"><span class="cl">response url: https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu7GxPKTU1Kg.ttf status: 200
</span></span><span class="line"><span class="cl">response url: https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfChc9AMP6lQ.ttf status: 200
</span></span><span class="line"><span class="cl">status for main url: 404
</span></span></code></pre>
<div>Notice that fetching a page also fetches all resources used by that page, just like in a web browser. For that reason to find out status code for the url we requested, we have to remember it in a variable in <code>response</code> hook.</div>
<div><code>requestfailed</code> hook is for errors on network connection level e.g. DNS resolution failed, there’s not network at all, network connection got interrupted etc.</div>
<h1 class="hdr-with-anchor" id="see-console-log-from-inside-the-browser"><div>See <code>console.log</code> from inside the browser</div><a class="header-anchor" href="#see-console-log-from-inside-the-browser">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Your JavaScript code is executed in two different contexts:</div>
<ul>
<li>main script is executed in node.js. In that context <code>console.log("foo")</code> prints to shell</li>
<li>scripts provided to <code>Page.evaluate</code> method are serialized to text, sent to the browser via Chrome DevTools Protocol and executed inside the browser. In that context <code>console.log("foo")</code> prints to browser console, which you can’t see.</li>
</ul>
<div>To see what <code>console.log</code> prints in the browser, you can hook it and re-log to shell:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">browser</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">puppeteer</span><span class="p">.</span><span class="nx">launch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">"https://blog.kowalczyk.info/"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// this hooks `console.log()` in the browser
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">page</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">"console"</span><span class="p">,</span> <span class="nx">msg</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"The whole message:"</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">text</span><span class="p">());</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"\nEach argument:"</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">arg</span> <span class="k">of</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">args</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// arg is a Promise returning value of type JSHandle
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// https://pptr.dev/#?product=Puppeteer&show=api-class-jshandle
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">arg</span><span class="p">.</span><span class="nx">jsonValue</span><span class="p">().</span><span class="nx">then</span><span class="p">(</span><span class="nx">v</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">v</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(()</span> <span class="p">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// This is executed inside the browser so not visible in our script
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// unless we hook 'console' events
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Message from the browser"</span><span class="p">,</span> <span class="mi">5</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
</span></span></code></pre>
<h1 class="hdr-with-anchor" id="quickly-testing-evaluate-scripts"><div>Quickly testing <code>evaluate</code> scripts</div><a class="header-anchor" href="#quickly-testing-evaluate-scripts">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>It’s slow to test browser script executed via <code>Page.evaluate</code> because you have to start the browser, load the page etc.</div>
<div>To test scripts faster I test them directly in the browser, using excellent Chrome dev tools.</div>
<div>My process is:</div>
<ul>
<li>prepare the script, in IIFE form, in the editor</li>
<li>copy & paste in console window in Chrome dev tools</li>
</ul>
<div>What is IIFE form? To avoid conflicts with JavaScripts state from previous runs I wrap the code inside Immediately Invoked Function Expression:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// code here is isolated from things outside this function
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"My script"</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... my script
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"> <span class="c1">// when debugging I can trigger JavaScript debugger from inside the script
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// with debugger statement:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">debugger</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}()</span> <span class="c1">// immediately invoke the function
</span></span></span></code></pre>
<div>It’s faster to iterate on code this way. You can also use browser’s JavaScript debugger.</div>
<div>As shown in the snippet, I can also trigger the debugger for single-stepping through the code with <code>debugger;</code> statement.</div>
<h1 class="hdr-with-anchor" id="study-puppeteer-api"><div>Study Puppeteer API</div><a class="header-anchor" href="#study-puppeteer-api">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Now that you’ve seen a few advanced uses of Puppeteer, you should study its API a bit to learn what else is possible. CDP is very powerful:</div>
<ul>
<li><a target="_blank" href="https://pptr.dev/#?product=Puppeteer&show=api-class-page">Page class</a> allows hooking many events, reading and setting cookies, simulating interaction like mouse clicks etc.</li>
<li><a target="_blank" href="https://pptr.dev/#?product=Puppeteer&show=api-class-tracing">Tracing class</a> allows creating a trace file for future inspection in Chrome DevTools</li>
<li><a target="_blank" href="https://pptr.dev/#?product=Puppeteer&show=api-class-worker">Worker class</a> allows interacting with Web Workers</li>
<li><a target="_blank" href="https://pptr.dev/#?product=Puppeteer&show=api-class-coverage">Coverage class</a> allows measuring JavaScript and CSS coverage</li>
<li><a target="_blank" href="https://pptr.dev/#?product=Puppeteer&show=api-class-keyboard">Keyboard class</a> allows simulating keyboard events</li>
</ul>
<h1 class="hdr-with-anchor" id="other-cdp-tools-and-libraries"><div>Other CDP tools and libraries</div><a class="header-anchor" href="#other-cdp-tools-and-libraries">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h1>
<div>Puppeteer is not the only tool that takes advantage of Chrome DevTools protocol. A bunch of them is listed in <a target="_blank" href="https://github.com/ChromeDevTools/awesome-chrome-devtools">Awesome Chrome DevTools</a>.</div>
</div>
57 MicroConf videos for self-funded software businesses2017-12-24T00:00:00Ztag:blog.kowalczyk.info,2017-12-24:/article/l/57-microconf-videos-for-self-funded-software-businesses.html<div class="notion-page" id="l">
<div><a target="_blank" href="http://www.microconf.com/">MicroConf</a> is a conference for small/indie/self-funded software businesses. Many of their talks are <a target="_blank" href="https://vimeo.com/user12790628">available on Vimeo</a> but not well indexed. They have a <a target="_blank" href="http://www.microconf.com/past-videos/">better index</a> (and <a target="_blank" href="http://www.microconf.com/growth/past-videos/">another here</a>) on their website, but also not great.</div>
<div>This is a list of videos and a bit of info about each video. I hope this will help you find a video useful for you.</div>
<h2 class="hdr-with-anchor" id="1-lizards-thru-doorways-proven-ways-to-widen-your-funnel-using-just-your-ctas"><div>1. Lizards Thru Doorways: Proven ways to Widen Your Funnel Using Just Your CTAs</div><a class="header-anchor" href="#1-lizards-thru-doorways-proven-ways-to-widen-your-funnel-using-just-your-ctas">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/132932415">https://vimeo.com/132932415</a> by <a target="_blank" href="https://copyhackers.com/">Joanna Wiebe</a>, 39 min.</div>
<div>Joanna is a copywriter for hire i.e. she writes the text of emails, sales pages etc. for other businesses.</div>
<div>Her talk is about writing better copy. Tips and case studies. She also has a lot of free (and paid) tutorials on copywriting on her website <a target="_blank" href="https://copyhackers.com/">https://copyhackers.com/</a>. Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/joanna/">https://kaidavis.com/microconf-2015/joanna/</a></div>
<h2 class="hdr-with-anchor" id="2-an-inside-story-of-self-funded-saas-growth"><div>2. An Inside Story of Self-Funded SaaS Growth</div><a class="header-anchor" href="#2-an-inside-story-of-self-funded-saas-growth">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/132932414">https://vimeo.com/132932414</a> by <a target="_blank" href="https://www.softwarebyrob.com/">Rob Walling</a>, 51 min.</div>
<div>This is a history of Rob’s SaaS startup <a target="_blank" href="https://www.drip.com/">https://www.drip.com</a>(email marketing software). He also wrote about it online: <a target="_blank" href="https://wpcurve.com/bootstrapped-drip-into-a-7-figure-saas-business/">https://wpcurve.com/bootstrapped-drip-into-a-7-figure-saas-business/</a>.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/rob/">https://kaidavis.com/microconf-2015/rob/</a></div>
<h2 class="hdr-with-anchor" id="3-accelerating-growth-grow-faster-without-working-yourself-to-death"><div>3. Accelerating Growth: Grow Faster Without Working Yourself to Death</div><a class="header-anchor" href="#3-accelerating-growth-grow-faster-without-working-yourself-to-death">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/132139313">https://vimeo.com/132139313</a> by <a target="_blank" href="https://hitenism.com/">Hiten Shah</a>, 54 min</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/hiten/">https://kaidavis.com/microconf-2015/hiten/</a></div>
<h2 class="hdr-with-anchor" id="4-amplification-content-marketing-that-works"><div>4. Amplification - Content Marketing That Works</div><a class="header-anchor" href="#4-amplification-content-marketing-that-works">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/132139312">https://vimeo.com/132139312</a> by Justin Jackson, 11 min.</div>
<div>Content marketing is about writing blog posts, doing podcasts and videos to attract people to your website so that you can pitch them on your products.</div>
<h2 class="hdr-with-anchor" id="5-how-to-aggressively-acquire-customers-for-your-saas-with-an-efficient-outbound-sales-process"><div>5. How to Aggressively Acquire Customers for your SaaS with an Efficient Outbound Sales Process</div><a class="header-anchor" href="#5-how-to-aggressively-acquire-customers-for-your-saas-with-an-efficient-outbound-sales-process">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/132139308">https://vimeo.com/132139308</a> by <a target="_blank" href="https://twitter.com/JordanGal">Jordan Gal</a>, 12 min.</div>
<div>Outbound sales process is using email and phone calls (cold-calling) to find customers.</div>
<h2 class="hdr-with-anchor" id="6-how-i-designed-our-high-touch-sales-onboarding-to-run-without-me-2014"><div>6. How I Designed Our (High-Touch) Sales & Onboarding to Run Without Me (2014)</div><a class="header-anchor" href="#6-how-i-designed-our-high-touch-sales-onboarding-to-run-without-me-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/132139307">https://vimeo.com/132139307</a> by <a target="_blank" href="http://casjam.com/">Brian Casel</a>, 11 min.</div>
<div>Based on experience from his startup Restaurant Engine, talks about converting leads (from his inbound traffic) into customers and how to automate it.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/brian-casel-automated-content-marketing-machine-microconf-2014/">https://www.phraseexpander.com/microconf-2014/brian-casel-automated-content-marketing-machine-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="7-creating-an-explosive-email-course"><div>7. Creating an Explosive Email Course</div><a class="header-anchor" href="#7-creating-an-explosive-email-course">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/132139306">https://vimeo.com/132139306</a> by <a target="_blank" href="http://kadavy.net/">David Kadavy</a>, 13 min.</div>
<div>Talks about creating email course in order to get more leads (build email list).</div>
<h2 class="hdr-with-anchor" id="8-do-this-not-that-creating-an-exceptional-customer-support-experience-from-day-1-2015"><div>8. Do This, Not That: Creating an Exceptional Customer Support Experience from Day 1 (2015)</div><a class="header-anchor" href="#8-do-this-not-that-creating-an-exceptional-customer-support-experience-from-day-1-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/131466750">https://vimeo.com/131466750</a> by <a target="_blank" href="https://twitter.com/sh">Sarah Hattter</a>, 29 min.</div>
<div>Sarah runs <a target="_blank" href="http://cosupport.com/">CoSupport</a>, which teaches companies how to create good customer support.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/sarah/">https://kaidavis.com/microconf-2015/sarah/</a></div>
<h2 class="hdr-with-anchor" id="9-how-to-build-a-solo-saas-sales-machine-2015"><div>9. How to Build a Solo SaaS Sales Machine (2015)</div><a class="header-anchor" href="#9-how-to-build-a-solo-saas-sales-machine-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/131441010">https://vimeo.com/131441010</a> by <a target="_blank" href="https://twitter.com/Steli">Steli Efti</a>, 56 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/steli/">https://kaidavis.com/microconf-2015/steli/</a></div>
<h2 class="hdr-with-anchor" id="10-lessons-learned-building-a-wordpress-plugin-business-to-10k-month"><div>10. Lessons Learned Building a WordPress Plugin Business to $10k/month</div><a class="header-anchor" href="#10-lessons-learned-building-a-wordpress-plugin-business-to-10k-month">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130984501">https://vimeo.com/130984501</a> by <a target="_blank" href="https://philderksen.com/">Phil Derksen</a>, 15 min.</div>
<h2 class="hdr-with-anchor" id="11-how-to-systematically-fight-saas-churn-and-win-2015"><div>11. How To Systematically Fight SaaS Churn And Win (2015)</div><a class="header-anchor" href="#11-how-to-systematically-fight-saas-churn-and-win-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130984497">https://vimeo.com/130984497</a> by <a target="_blank" href="https://keepify.com/">Robert Graham</a>, 13 min.</div>
<div>Recap: <a target="_blank" href="https://bootstrapping.io/microconf-2015/robert/">https://bootstrapping.io/microconf-2015/robert/</a></div>
<h2 class="hdr-with-anchor" id="12-how-bookkeeping-tripled-my-revenue-in-two-years-and-other-unexpected-cash-flow-advice-2015"><div>12. How Bookkeeping Tripled My Revenue in Two Years (and Other Unexpected Cash Flow Advice) (2015)</div><a class="header-anchor" href="#12-how-bookkeeping-tripled-my-revenue-in-two-years-and-other-unexpected-cash-flow-advice-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130984492">https://vimeo.com/130984492</a> by <a target="_blank" href="https://twitter.com/jessemecham">Jesse Mecham</a>, 48 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/jesse/">https://kaidavis.com/microconf-2015/jesse/</a></div>
<h2 class="hdr-with-anchor" id="13-growing-your-userbase-with-better-onboarding-2015"><div>13. Growing Your Userbase with Better Onboarding (2015)</div><a class="header-anchor" href="#13-growing-your-userbase-with-better-onboarding-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130797721">https://vimeo.com/130797721</a> by <a target="_blank" href="http://www.samuelhulick.com/">Samuel Hulick</a>, 30 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/samuel/">https://kaidavis.com/microconf-2015/samuel/</a></div>
<h2 class="hdr-with-anchor" id="14-q-a-and-smart-bear-live-2015"><div>14. Q&A and Smart Bear Live (2015)</div><a class="header-anchor" href="#14-q-a-and-smart-bear-live-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130797720">https://vimeo.com/130797720</a> by <a target="_blank" href="https://twitter.com/asmartbear">Jason Cohen</a>, 1hr 7 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/jason/">https://kaidavis.com/microconf-2015/jason/</a></div>
<h2 class="hdr-with-anchor" id="15-micro-isv-to-micro-acquisition-selling-my-11-year-one-man-software-business-2015"><div>15. Micro-ISV to Micro-acquisition: Selling my 11-year one-man software business (2015)</div><a class="header-anchor" href="#15-micro-isv-to-micro-acquisition-selling-my-11-year-one-man-software-business-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130797718">https://vimeo.com/130797718</a> by <a target="_blank" href="https://twitter.com/jacobthurman">Jacob Thurman</a>. 11 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/jacob/">https://kaidavis.com/microconf-2015/jacob/</a></div>
<h2 class="hdr-with-anchor" id="16-how-to-start-a-saas-business-in-any-market-with-no-idea-or-connections-using-only-excel-email-phone-2015"><div>16. How to start a SaaS business in any market with no idea or connections, using only excel, email & phone (2015)</div><a class="header-anchor" href="#16-how-to-start-a-saas-business-in-any-market-with-no-idea-or-connections-using-only-excel-email-phone-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130797716">https://vimeo.com/130797716</a> by <a target="_blank" href="https://twitter.com/pawelwb">Pawel Brzeminski</a>, 12 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/pawel/">https://kaidavis.com/microconf-2015/pawel/</a></div>
<h2 class="hdr-with-anchor" id="17-how-i-grew-my-productized-consulting-offering-to-100k-yrr-in-12-months-2015"><div>17. How I Grew My Productized Consulting Offering To $100K YRR In 12 Months (2015)</div><a class="header-anchor" href="#17-how-i-grew-my-productized-consulting-offering-to-100k-yrr-in-12-months-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130797714">https://vimeo.com/130797714</a> by <a target="_blank" href="http://www.appaftercare.com/">Einer Vollset</a>, 9 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/einar/">https://kaidavis.com/microconf-2015/einar/</a>. A story of building <a target="_blank" href="http://www.appaftercare.com/">http://www.appaftercare.com/</a></div>
<h2 class="hdr-with-anchor" id="18-the-3-week-startup-2015"><div>18. The 3 Week Startup (2015)</div><a class="header-anchor" href="#18-the-3-week-startup-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/130499701">https://vimeo.com/130499701</a> by <a target="_blank" href="https://twitter.com/harisenbon79">Keith Perhac</a>, 10 min.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/keith/">https://kaidavis.com/microconf-2015/keith/</a></div>
<div>Tactical tips about how (and why) to build SaaS quickly (in 1 week).</div>
<h2 class="hdr-with-anchor" id="19-leveling-up-2015"><div>19. Leveling Up (2015)</div><a class="header-anchor" href="#19-leveling-up-2015">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/129913527">https://vimeo.com/129913527</a> by <a target="_blank" href="http://www.kalzumeus.com/">Patrick McKenzie</a>, 1 hr.</div>
<div>Recap: <a target="_blank" href="https://kaidavis.com/microconf-2015/patio11/">https://kaidavis.com/microconf-2015/patio11/</a></div>
<h2 class="hdr-with-anchor" id="20-how-to-validate-your-idea-and-launch-to-7k-in-recurring-revenue-2014"><div>20. How to Validate Your Idea and Launch to $7k in Recurring Revenue (2014)</div><a class="header-anchor" href="#20-how-to-validate-your-idea-and-launch-to-7k-in-recurring-revenue-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/96267945">https://vimeo.com/96267945</a> by <a target="_blank" href="https://www.softwarebyrob.com/">Rob Walling</a>, 58 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/rob-walling-validate-idea-launch-7k-recurring-revenue-microconf-europe-2014/">http://www.christophengelhardt.com/rob-walling-validate-idea-launch-7k-recurring-revenue-microconf-europe-2014/</a></div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/rob-walling-validate-idea-launch7k-microconf-2014/">https://www.phraseexpander.com/microconf-2014/rob-walling-validate-idea-launch7k-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="21-6-tricks-that-helped-me-triple-my-saas-growth-rate-2014"><div>21. 6 Tricks That Helped Me Triple My SaaS’ Growth Rate (2014)</div><a class="header-anchor" href="#21-6-tricks-that-helped-me-triple-my-saas-growth-rate-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/95680318">https://vimeo.com/95680318</a> by <a target="_blank" href="https://twitter.com/brennandunn">Brennan Dunn</a>, 41 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/brennan-dunn-6-tricks-helped-triple-saas-growth-rate-microconf-europe-2014/">http://www.christophengelhardt.com/brennan-dunn-6-tricks-helped-triple-saas-growth-rate-microconf-europe-2014/</a></div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/brennan-dunn-triple-saas-growth-rate-microconf-2014/">https://www.phraseexpander.com/microconf-2014/brennan-dunn-triple-saas-growth-rate-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="22-lifting-the-veil-the-data-behind-successful-product-launches-2014"><div>22. Lifting the Veil: The Data Behind Successful Product Launches (2014)</div><a class="header-anchor" href="#22-lifting-the-veil-the-data-behind-successful-product-launches-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/95680316">https://vimeo.com/95680316</a> by <a target="_blank" href="https://twitter.com/delk">Ryan Delk</a>, 13 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/ryan-delk-data-behind-successful-product-launches-microconf-2014/">https://www.phraseexpander.com/microconf-2014/ryan-delk-data-behind-successful-product-launches-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="23-3-habits-for-building-and-growing-a-product-empire-2014"><div>23. 3 Habits for Building (and Growing) a Product Empire (2014)</div><a class="header-anchor" href="#23-3-habits-for-building-and-growing-a-product-empire-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/95680313">https://vimeo.com/95680313</a> by <a target="_blank" href="http://nathanbarry.com/">Nathan Barry</a>, 38 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/nathan-barry-3-habits-grow-product-empire-microconf-2014/">https://www.phraseexpander.com/microconf-2014/nathan-barry-3-habits-grow-product-empire-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="24-from-zero-to-4m-year-without-quora-hacker-news-or-mixergy-2014"><div>24. From Zero to $4M/year Without Quora, Hacker News, or Mixergy (2014)</div><a class="header-anchor" href="#24-from-zero-to-4m-year-without-quora-hacker-news-or-mixergy-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/95653848">https://vimeo.com/95653848</a> by <a target="_blank" href="https://twitter.com/jessemecham">Jesse Mecham</a>, 50 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/jesse-mecham-from-zero-4m-microconf-2014/">https://www.phraseexpander.com/microconf-2014/jesse-mecham-from-zero-4m-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="25-10-business-questions-every-entrepreneur-needs-to-ask-their-analytics-and-where-to-find-the-answers-2014"><div>25. 10 Business Questions Every Entrepreneur Needs to Ask Their Analytics (And Where to Find the Answers) (2014)</div><a class="header-anchor" href="#25-10-business-questions-every-entrepreneur-needs-to-ask-their-analytics-and-where-to-find-the-answers-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/95653743">https://vimeo.com/95653743</a> by <a target="_blank" href="https://twitter.com/anniecushing">Annie Cushing</a>, 46 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/annie-cushing-10-business-questions-analytics-microconf-2014/">https://www.phraseexpander.com/microconf-2014/annie-cushing-10-business-questions-analytics-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="26-how-to-slay-the-customer-support-beast-2014"><div>26. How To Slay the Customer Support Beast (2014)</div><a class="header-anchor" href="#26-how-to-slay-the-customer-support-beast-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/95052087">https://vimeo.com/95052087</a> by <a target="_blank" href="https://twitter.com/ianlandsman">Ian Landsman</a>, 44 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/ian-landsman-slay-customer-support-beast-microconf-2014/">https://www.phraseexpander.com/microconf-2014/ian-landsman-slay-customer-support-beast-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="27-don-t-burn-up-in-the-launch-staying-emotionally-and-relationally-healthy-while-launching-your-startup-2013"><div>27. Don’t Burn-up in the Launch: Staying Emotionally and Relationally Healthy While Launching Your Startup (2013)</div><a class="header-anchor" href="#27-don-t-burn-up-in-the-launch-staying-emotionally-and-relationally-healthy-while-launching-your-startup-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/72211933">https://vimeo.com/72211933</a> by <a target="_blank" href="http://sherrywalling.com/">Sherry Walling</a>, 19 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/sherry-walling-dont-burn-up-in-the-launch-microconf-2013/">http://www.christophengelhardt.com/sherry-walling-dont-burn-up-in-the-launch-microconf-2013/</a></div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/sherry-walling-dont-burn-up-in-the-launch-staying-emotionally-and-relationally-healthy-while-launching-your-startup-microconf-europe-2013/">http://www.christophengelhardt.com/sherry-walling-dont-burn-up-in-the-launch-staying-emotionally-and-relationally-healthy-while-launching-your-startup-microconf-europe-2013/</a></div>
<h2 class="hdr-with-anchor" id="28-playing-the-long-game-making-entrepreneurship-a-sustainable-life-2014"><div>28. Playing the Long Game: Making Entrepreneurship a Sustainable Life (2014)</div><a class="header-anchor" href="#28-playing-the-long-game-making-entrepreneurship-a-sustainable-life-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/95052086">https://vimeo.com/95052086</a> by <a target="_blank" href="http://sherrywalling.com/">Sherry Walling</a>, 56 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/sherry-walling-entrepreneurship-sustainable-life-microconf-2014/">https://www.phraseexpander.com/microconf-2014/sherry-walling-entrepreneurship-sustainable-life-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="29-from-idea-to-5k-mo-in-5-months-2014"><div>29. From Idea to $5k/mo in 5 Months (2014)</div><a class="header-anchor" href="#29-from-idea-to-5k-mo-in-5-months-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/94623532">https://vimeo.com/94623532</a> by <a target="_blank" href="https://twitter.com/Shpigford">Josh Pigford</a>, 12 min.</div>
<div>History of building <a target="_blank" href="https://baremetrics.com/">https://baremetrics.com/</a></div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/josh-pigford-idea-5k-5months-microconf-2014/">https://www.phraseexpander.com/microconf-2014/josh-pigford-idea-5k-5months-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="30-ux-basics-that-convert-users-into-customers-2014"><div>30. UX Basics That Convert Users into Customers (2014)</div><a class="header-anchor" href="#30-ux-basics-that-convert-users-into-customers-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/94623531">https://vimeo.com/94623531</a> by <a target="_blank" href="http://www.samuelhulick.com/">Samuel Hulick</a>, 7 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/samuel-hulick-uxbasics-convert-user-into-customers-microconf-2014/">https://www.phraseexpander.com/microconf-2014/samuel-hulick-uxbasics-convert-user-into-customers-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="31-business-hacks-epic-wins-2014"><div>31. Business Hacks & Epic Wins (2014)</div><a class="header-anchor" href="#31-business-hacks-epic-wins-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/94623529">https://vimeo.com/94623529</a> by <a target="_blank" href="https://twitter.com/SingleFounder">Mike Taber</a>, 1 hr 4 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/mike-taber-business-hacks-epic-wins-microconf-2014/">https://www.phraseexpander.com/microconf-2014/mike-taber-business-hacks-epic-wins-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="32-how-to-grow-your-self-funded-business-faster-2014"><div>32. How to Grow Your Self-Funded Business Faster (2014)</div><a class="header-anchor" href="#32-how-to-grow-your-self-funded-business-faster-2014">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/94187473">https://vimeo.com/94187473</a> by <a target="_blank" href="https://hitenism.com/">Hiten Shah</a>, 43 min.</div>
<div>Recap: <a target="_blank" href="https://www.phraseexpander.com/microconf-2014/hiten-shah-grow-your-self-funded-business-faster-microconf-2014/">https://www.phraseexpander.com/microconf-2014/hiten-shah-grow-your-self-funded-business-faster-microconf-2014/</a></div>
<h2 class="hdr-with-anchor" id="33-designing-the-ideal-bootstrapped-business-2013"><div>33. Designing the Ideal Bootstrapped Business (2013)</div><a class="header-anchor" href="#33-designing-the-ideal-bootstrapped-business-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/74338272">https://vimeo.com/74338272</a> by <a target="_blank" href="https://twitter.com/asmartbear">Jason Cohen</a>, 1hr 5 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/jason-cohen-microconf-2013/">http://www.christophengelhardt.com/jason-cohen-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="34-dude-marketing-is-not-your-thing-2013"><div>34. Dude. Marketing is not your thing (2013)</div><a class="header-anchor" href="#34-dude-marketing-is-not-your-thing-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/72461554">https://vimeo.com/72461554</a> by Jody Burgess, 13 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/dude-marketing-is-not-your-thing-microconf-2013/">http://www.christophengelhardt.com/dude-marketing-is-not-your-thing-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="35-how-a-non-technical-founder-built-a-6-figure-saas-app-using-only-free-public-data-sources-2013"><div>35. How a Non-Technical Founder Built a 6 Figure Saas App Using Only Free Public Data Sources (2013)</div><a class="header-anchor" href="#35-how-a-non-technical-founder-built-a-6-figure-saas-app-using-only-free-public-data-sources-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/72461551">https://vimeo.com/72461551</a> by <a target="_blank" href="https://twitter.com/distressedpro">Brecht Palombo</a>, 15 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/brecht-palombo-how-a-non-technical-founder-built-a-6-figure-saas-app-using-only-free-public-data-sources-microconf-2013/">http://www.christophengelhardt.com/brecht-palombo-how-a-non-technical-founder-built-a-6-figure-saas-app-using-only-free-public-data-sources-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="36-finding-customers-who-are-100x-more-valuable-without-100x-the-effort-2013"><div>36. Finding Customers Who Are 100x More Valuable Without 100x the Effort (2013)</div><a class="header-anchor" href="#36-finding-customers-who-are-100x-more-valuable-without-100x-the-effort-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/72456666">https://vimeo.com/72456666</a> by <a target="_blank" href="https://twitter.com/ericabiz">Erica Douglass</a>, 48 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/erica-douglass-how-to-measurably-move-the-needle-with-your-software-company-microconf-2013/">http://www.christophengelhardt.com/erica-douglass-how-to-measurably-move-the-needle-with-your-software-company-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="37-bootstrapping-an-app-business-2013"><div>37. Bootstrapping an App Business (2013)</div><a class="header-anchor" href="#37-bootstrapping-an-app-business-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/72260021">https://vimeo.com/72260021</a> by <a target="_blank" href="https://twitter.com/pthompson">Patrick Thompson</a>, 16 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/patrick-thompson-bootstraping-an-app-business-microconf-2013/">http://www.christophengelhardt.com/patrick-thompson-bootstraping-an-app-business-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="38-building-things-to-help-sell-the-things-you-build-2013"><div>38. Building Things To Help Sell The Things You Build (2013)</div><a class="header-anchor" href="#38-building-things-to-help-sell-the-things-you-build-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/72140534">https://vimeo.com/72140534</a> by <a target="_blank" href="http://www.kalzumeus.com/">Patrick McKenzie</a>, 1 hr 13 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/patrick-mckenzie-building-things-to-help-sell-the-things-you-build-microconf-2013/">http://www.christophengelhardt.com/patrick-mckenzie-building-things-to-help-sell-the-things-you-build-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="39-killer-content-marketing-2013"><div>39. Killer Content Marketing (2013)</div><a class="header-anchor" href="#39-killer-content-marketing-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/72081980">https://vimeo.com/72081980</a> by <a target="_blank" href="https://hitenism.com/">Hiten Shah</a>, 44 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/hiten-shah-killer-content-marketing-microconf-2013/">http://www.christophengelhardt.com/hiten-shah-killer-content-marketing-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="40-seo-demystified-practical-techniques-that-produce-astonishing-results-2013"><div>40. SEO Demystified: Practical Techniques That Produce Astonishing Results (2013)</div><a class="header-anchor" href="#40-seo-demystified-practical-techniques-that-produce-astonishing-results-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/71547333">https://vimeo.com/71547333</a> by <a target="_blank" href="https://twitter.com/TheDaveCollins">Dave Collins</a>, 57 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/dave-collins-seo-demystified-practical-techniques-that-produce-astonishing-results-microconf-europe-2013/">http://www.christophengelhardt.com/dave-collins-seo-demystified-practical-techniques-that-produce-astonishing-results-microconf-europe-2013/</a></div>
<h2 class="hdr-with-anchor" id="41-shut-up-and-take-my-money-how-to-find-business-ideas-customers-want-2013"><div>41. Shut Up and Take My Money: How to Find Business Ideas Customers Want (2013)</div><a class="header-anchor" href="#41-shut-up-and-take-my-money-how-to-find-business-ideas-customers-want-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/71250239">https://vimeo.com/71250239</a> by <a target="_blank" href="https://twitter.com/joshkaufman">Josh Kaufman</a>, 42 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/josh-kaufman-shut-up-and-take-my-money-how-to-find-business-ideas-customers-want-microconf-2013/">http://www.christophengelhardt.com/josh-kaufman-shut-up-and-take-my-money-how-to-find-business-ideas-customers-want-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="42-copywriting-that-converts-how-to-sell-without-selling-your-soul-2013"><div>42. Copywriting that Converts: How to Sell Without Selling Your Soul (2013)</div><a class="header-anchor" href="#42-copywriting-that-converts-how-to-sell-without-selling-your-soul-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/71008640">https://vimeo.com/71008640</a> by <a target="_blank" href="https://copyhackers.com/">Joanna Wiebe</a>, 50 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/joanna-wiebe-copywriting-that-converts-microconf-2013/">http://www.christophengelhardt.com/joanna-wiebe-copywriting-that-converts-microconf-2013/</a></div>
<div>Another: <a target="_blank" href="http://www.workhappy.net/2013/05/how-you-can-drastically-improve-the-copy-on-your-site-even-if-you-only-have-5-minutes.html">http://www.workhappy.net/2013/05/how-you-can-drastically-improve-the-copy-on-your-site-even-if-you-only-have-5-minutes.html</a></div>
<h2 class="hdr-with-anchor" id="43-lean-analytics-how-to-focus-on-what-matters-2013"><div>43. Lean Analytics: How to Focus on What Matters (2013)</div><a class="header-anchor" href="#43-lean-analytics-how-to-focus-on-what-matters-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/70981922">https://vimeo.com/70981922</a> by <a target="_blank" href="https://twitter.com/byosko">Ben Yoskovitz</a>, 43 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/ben-yoskovitz-measure-what-matters-microconf-2013/">http://www.christophengelhardt.com/ben-yoskovitz-measure-what-matters-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="44-how-to-sell-anything-to-anyone-2013"><div>44. How to Sell Anything to Anyone (2013)</div><a class="header-anchor" href="#44-how-to-sell-anything-to-anyone-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/70901902">https://vimeo.com/70901902</a> by <a target="_blank" href="https://twitter.com/SingleFounder">Mike Taber</a>, 41 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/mike-taber-microconf-2013-a-k-a-the-liquor-fairy/">http://www.christophengelhardt.com/mike-taber-microconf-2013-a-k-a-the-liquor-fairy/</a></div>
<h2 class="hdr-with-anchor" id="45-how-to-10x-in-15-months-2013"><div>45. How to 10x in 15 months (2013)</div><a class="header-anchor" href="#45-how-to-10x-in-15-months-2013">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/70901901">https://vimeo.com/70901901</a> by <a target="_blank" href="https://www.softwarebyrob.com/">Rob Walling</a>, 51 min.</div>
<div>Recap: <a target="_blank" href="http://www.christophengelhardt.com/rob-walling-how-to-10x-in-15-months-microconf-2013/">http://www.christophengelhardt.com/rob-walling-how-to-10x-in-15-months-microconf-2013/</a></div>
<h2 class="hdr-with-anchor" id="46-cheap-and-easy-customer-support-2012"><div>46. Cheap and Easy Customer Support (2012)</div><a class="header-anchor" href="#46-cheap-and-easy-customer-support-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/51306662">https://vimeo.com/51306662</a> by <a target="_blank" href="https://twitter.com/sh">Sarah Hattter</a>, 40 min.</div>
<h2 class="hdr-with-anchor" id="47-google-adwords-stop-losing-start-exploiting-really-2012"><div>47. Google AdWords: Stop Losing & Start Exploiting (Really) (2012)</div><a class="header-anchor" href="#47-google-adwords-stop-losing-start-exploiting-really-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/51187193">https://vimeo.com/51187193</a> by <a target="_blank" href="https://twitter.com/TheDaveCollins">Dave Collins</a>, 58 min.</div>
<h2 class="hdr-with-anchor" id="48-how-i-bootstrapped-and-sold-my-software-company-by-maxing-out-my-credit-cards-2012"><div>48. How I Bootstrapped and Sold My Software Company By Maxing Out My Credit Cards (2012)</div><a class="header-anchor" href="#48-how-i-bootstrapped-and-sold-my-software-company-by-maxing-out-my-credit-cards-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/50372726">https://vimeo.com/50372726</a> by Bill Bither, 46 min.</div>
<h2 class="hdr-with-anchor" id="49-from-idea-to-7-figures-in-2-years-the-story-of-woothemes"><div>49. From Idea to 7 Figures in 2 years: The Story of Woothemes</div><a class="header-anchor" href="#49-from-idea-to-7-figures-in-2-years-the-story-of-woothemes">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/50209990">https://vimeo.com/50209990</a> by <a target="_blank" href="https://twitter.com/adii">Adii Pienaar</a>, 45 min.</div>
<h2 class="hdr-with-anchor" id="50-ask-me-anything-2012"><div>50. Ask Me Anything (2012)</div><a class="header-anchor" href="#50-ask-me-anything-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/49023444">https://vimeo.com/49023444</a> by <a target="_blank" href="https://twitter.com/peldi">Peldi Guilizzoni</a>, 48 min.</div>
<h2 class="hdr-with-anchor" id="51-if-you-don-t-like-drunk-frat-boys-don-t-open-an-irish-pub-2012"><div>51. If You Don’t Like Drunk Frat Boys, Don’t Open an Irish Pub… (2012)</div><a class="header-anchor" href="#51-if-you-don-t-like-drunk-frat-boys-don-t-open-an-irish-pub-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/48962410">https://vimeo.com/48962410</a> by <a target="_blank" href="https://stackingthebricks.com/">Amy Hoy</a>, 43 min.</div>
<div>Transcript: <a target="_blank" href="https://stackingthebricks.com/a-customer-is-your-mvp-a-video-talk-on-making-products-that-sell/">https://stackingthebricks.com/a-customer-is-your-mvp-a-video-talk-on-making-products-that-sell/</a></div>
<h2 class="hdr-with-anchor" id="52-growth-hacking-2012"><div>52. Growth Hacking (2012)</div><a class="header-anchor" href="#52-growth-hacking-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/48592609">https://vimeo.com/48592609</a> by Dan Martell, 39 min.</div>
<h2 class="hdr-with-anchor" id="53-losers-have-goals-winners-have-systems-2012"><div>53. Losers Have Goals, Winners Have Systems (2012)</div><a class="header-anchor" href="#53-losers-have-goals-winners-have-systems-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/48571431">https://vimeo.com/48571431</a> by <a target="_blank" href="https://twitter.com/SingleFounder">Mike Taber</a>, 27 min.</div>
<div>Related article: <a target="_blank" href="http://www.singlefounder.com/losers-have-goals-winners-have-systems/">http://www.singlefounder.com/losers-have-goals-winners-have-systems/</a></div>
<h2 class="hdr-with-anchor" id="54-naked-business-how-honesty-makes-you-more-money-2012"><div>54. Naked Business: How Honesty Makes You More Money (2012)</div><a class="header-anchor" href="#54-naked-business-how-honesty-makes-you-more-money-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/48549019">https://vimeo.com/48549019</a> by <a target="_blank" href="https://twitter.com/asmartbear">Jason Cohen</a>, 55 min.</div>
<h2 class="hdr-with-anchor" id="55-finding-your-flywheel-2012"><div>55. Finding Your Flywheel (2012)</div><a class="header-anchor" href="#55-finding-your-flywheel-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Video <a target="_blank" href="https://vimeo.com/47465229">https://vimeo.com/47465229</a> by <a target="_blank" href="https://www.softwarebyrob.com/">Rob Walling</a>, 1 hr 04 min.</div>
<h2 class="hdr-with-anchor" id="56-how-to-engineer-marketing-success-2012"><div>56. How to Engineer Marketing Success (2012)</div><a class="header-anchor" href="#56-how-to-engineer-marketing-success-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><a target="_blank" href="https://vimeo.com/47311461">https://vimeo.com/47311461</a> by <a target="_blank" href="http://www.kalzumeus.com/">Patrick McKenzie</a>, 51 min.</div>
<h2 class="hdr-with-anchor" id="57-more-lessons-i-ve-learned-as-a-serial-entrepreneur-2012"><div>57. More Lessons I’ve Learned as a Serial Entrepreneur (2012)</div><a class="header-anchor" href="#57-more-lessons-i-ve-learned-as-a-serial-entrepreneur-2012">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><a target="_blank" href="https://vimeo.com/46893380">https://vimeo.com/46893380</a> by <a target="_blank" href="https://hitenism.com/">Hiten Shah</a>, 48 min.</div>
<div>Related resources:</div>
<ul>
<li><a target="_blank" href="https://bootstrapping.io/microconf-2015/">https://bootstrapping.io/microconf-2015/</a></li>
<li><a target="_blank" href="https://www.phraseexpander.com/microconf-2014/">https://www.phraseexpander.com/microconf-2014/</a></li>
<li><a target="_blank" href="http://www.christophengelhardt.com/microconf-europe-2014-hub/">http://www.christophengelhardt.com/microconf-europe-2014-hub/</a></li>
<li><a target="_blank" href="http://www.christophengelhardt.com/microconf-europe-2013-hub-page/">http://www.christophengelhardt.com/microconf-europe-2013-hub-page/</a></li>
<li><a target="_blank" href="http://www.christophengelhardt.com/microconf-2013-hub-page/">http://www.christophengelhardt.com/microconf-2013-hub-page/</a></li>
<li><a target="_blank" href="https://accidentaltechnologist.com/entrepreneurship/microconf-2013-was-freakin-awesome/">https://accidentaltechnologist.com/entrepreneurship/microconf-2013-was-freakin-awesome/</a></li>
<li><a target="_blank" href="https://vimeo.com/theblnbusinessofsoftware">https://vimeo.com/theblnbusinessofsoftware</a> : talks from Business Of Software conference</li>
<li><a href="/article/wjRD/solo-founders-with-profitable-businesses-collected-stories.html">solo founders with profitable businesses</a></li>
</ul>
</div>
Guide to predefined macros in C++ compilers (gcc, clang, msvc etc.)2017-11-07T00:00:00Ztag:blog.kowalczyk.info,2017-11-07:/article/j/guide-to-predefined-macros-in-c-compilers-gcc-clang-msvc-etc..html<div class="notion-page" id="j">
<div>When writing portable C++ code you need to write conditional code that depends on compiler used or the OS for which the code is written.</div>
<div>Here’s a typical case:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="cp">#if defined (_MSC_VER)
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1">// code specific to Visual Studio compiler
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#endif
</span></span></span></code></pre>
<div>To perform those checks you need to check pre-processor macros that various compilers set.</div>
<div>It can either be binary is defined vs. is not defined check (e.g. <code>__APPLE__</code>) or checking a value of the macro (e.g. <code>_MSC_VER</code> defines version of Visual Studio compiler).</div>
<div>This document describes macros set by various compilers.</div>
<div>Other documentations:</div>
<ul>
<li>predefined macros in <a target="_blank" href="https://msdn.microsoft.com/en-us/library/b0084kay.aspx">Visual Studio</a></li>
<li><a target="_blank" href="https://sourceforge.net/p/predef/wiki/Compilers/">https://sourceforge.net/p/predef/wiki/Compilers/</a></li>
<li>to list clang’s pre-defined macros: <code>clang -x c /dev/null -dM -E</code></li>
<li>to list gcc’s pre-defined macros: <code>gcc -x c /dev/null -dM -E</code> (not that on mac gcc is actually clang that ships with XCode)</li>
<li>the <code>-x c /dev/null -dM -E</code> also works for mingw (which is based on gcc)</li>
<li>listing predefined macros <a target="_blank" href="http://nadeausoftware.com/articles/2011/12/c_c_tip_how_list_compiler_predefined_macros">for other compilers</a></li>
</ul>
<h2 class="hdr-with-anchor" id="checking-for-os-platform"><div>Checking for OS (platform)</div><a class="header-anchor" href="#checking-for-os-platform">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>To check for which OS the code is compiled:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">Linux and Linux-derived __linux__
</span></span><span class="line"><span class="cl">Android __ANDROID__ (implies __linux__)
</span></span><span class="line"><span class="cl">Linux (non-Android) __linux__ && !__ANDROID__
</span></span><span class="line"><span class="cl">Darwin (Mac OS X and iOS) __APPLE__
</span></span><span class="line"><span class="cl">Akaros (http://akaros.org) __ros__
</span></span><span class="line"><span class="cl">Windows _WIN32
</span></span><span class="line"><span class="cl">Windows 64 bit _WIN64 (implies _WIN32)
</span></span><span class="line"><span class="cl">NaCL __native_client__
</span></span><span class="line"><span class="cl">AsmJS __asmjs__
</span></span><span class="line"><span class="cl">Fuschia __Fuchsia__
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="checking-the-compiler"><div>Checking the compiler:</div><a class="header-anchor" href="#checking-the-compiler">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>To check which compiler is used:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">Visual Studio _MSC_VER
</span></span><span class="line"><span class="cl">gcc __GNUC__
</span></span><span class="line"><span class="cl">clang __clang__
</span></span><span class="line"><span class="cl">emscripten __EMSCRIPTEN__ (for asm.js and webassembly)
</span></span><span class="line"><span class="cl">MinGW 32 __MINGW32__
</span></span><span class="line"><span class="cl">MinGW-w64 32bit __MINGW32__
</span></span><span class="line"><span class="cl">MinGW-w64 64bit __MINGW64__
</span></span></code></pre>
<h2 class="hdr-with-anchor" id="checking-compiler-version"><div>Checking compiler version</div><a class="header-anchor" href="#checking-compiler-version">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<h2 class="hdr-with-anchor" id="gcc"><div>gcc</div><a class="header-anchor" href="#gcc">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><code>__GNUC__</code> (e.g. 5) and <code>__GNUC_MINOR__</code> (e.g. 1).</div>
<div>To check that this is gcc compiler version 5.1 or greater:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="cp">#if defined(__GNUC__) && (__GNUC___ > 5 || (__GNUC__ == 5 && __GNUC_MINOR__ >= 1))
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1">// this is gcc 5.1 or greater
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#endif
</span></span></span></code></pre>
<div>Notice the chack has to be: <code>major > 5 || (major == 5 && minor >= 1)</code>. If you only do <code>major == 5 && minor >= 1</code>, it won’t work for version 6.0.</div>
<h2 class="hdr-with-anchor" id="clang"><div>clang</div><a class="header-anchor" href="#clang">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><code>__clang_major__</code>, <code>__clang_minor__</code>, <code>__clang_patchlevel__</code></div>
<h2 class="hdr-with-anchor" id="visual-studio"><div>Visual Studio</div><a class="header-anchor" href="#visual-studio">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><code>_MSC_VER</code> and <code>_MSC_FULL_VER</code>:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">VS _MSC_VER _MSC_FULL_VER
</span></span><span class="line"><span class="cl">1 800
</span></span><span class="line"><span class="cl">3 900
</span></span><span class="line"><span class="cl">4 1000
</span></span><span class="line"><span class="cl">4 1020
</span></span><span class="line"><span class="cl">5 1100
</span></span><span class="line"><span class="cl">6 1200
</span></span><span class="line"><span class="cl">6 SP6 1200 12008804
</span></span><span class="line"><span class="cl">7 1300 13009466
</span></span><span class="line"><span class="cl">7.1 (2003) 1310 13103077
</span></span><span class="line"><span class="cl">8 (2005) 1400 140050727
</span></span><span class="line"><span class="cl">9 (2008) 1500 150021022
</span></span><span class="line"><span class="cl">9 SP1 1500 150030729
</span></span><span class="line"><span class="cl">10 (2010) 1600 160030319
</span></span><span class="line"><span class="cl">10 (2010) SP1 1600 160040219
</span></span><span class="line"><span class="cl">11 (2012) 1700 170050727
</span></span><span class="line"><span class="cl">12 (2013) 1800 180021005
</span></span><span class="line"><span class="cl">14 (2015) 1900 190023026
</span></span><span class="line"><span class="cl">14 (2015 Update 1) 1900 190023506
</span></span><span class="line"><span class="cl">14 (2015 Update 2) 1900 190023918
</span></span><span class="line"><span class="cl">14 (2015 Update 3) 1900 190024210
</span></span><span class="line"><span class="cl">15 (2017 Update 1 & 2) 1910 191025017
</span></span><span class="line"><span class="cl">15 (2017 Update 3 & 4) 1911
</span></span><span class="line"><span class="cl">15 (2017 Update 5) 1912
</span></span></code></pre>
<div>More information:</div>
<ul>
<li><a target="_blank" href="https://blogs.msdn.microsoft.com/vcblog/2016/10/05/visual-c-compiler-version/">https://blogs.msdn.microsoft.com/vcblog/2016/10/05/visual-c-compiler-version/</a></li>
<li><a target="_blank" href="https://blogs.msdn.microsoft.com/vcblog/2017/11/15/side-by-side-minor-version-msvc-toolsets-in-visual-studio-2017/#comment-467525">https://blogs.msdn.microsoft.com/vcblog/2017/11/15/side-by-side-minor-version-msvc-toolsets-in-visual-studio-2017/#comment-467525</a></li>
</ul>
<h2 class="hdr-with-anchor" id="mingw"><div>MinGW</div><a class="header-anchor" href="#mingw">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>MinGW (aka MinGW32) and MinGW-w64 32bit: <code>__MINGW32_MAJOR_VERSION</code> and <code>__MINGW32_MINOR_VERSION</code></div>
<div>MinGW-w64 64bit: <code>__MINGW64_VERSION_MAJOR</code> and <code>__MINGW64_VERSION_MINOR</code></div>
<h2 class="hdr-with-anchor" id="checking-processor-architecture"><div>Checking processor architecture</div><a class="header-anchor" href="#checking-processor-architecture">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<h2 class="hdr-with-anchor" id="gcc-1"><div>gcc</div><a class="header-anchor" href="#gcc-1">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>The meaning of those should be self-evident:</div>
<ul>
<li><code>__i386__</code></li>
<li><code>__x86_64__</code></li>
<li><code>__arm__</code>. If defined, you can further check:
<ul>
<li><code>__ARM_ARCH_5T__</code></li>
<li><code>__ARM_ARCH_7A__</code></li>
</ul></li>
<li><code>__powerpc64__</code></li>
<li><code>__aarch64__</code></li>
</ul>
</div>
Tutorial for github.com/kjk/flex Go package (implementation of CSS flexbox algorithm)2017-08-04T00:00:00Ztag:blog.kowalczyk.info,2017-08-04:/article/9/tutorial-for-github.comkjkflex-go-package-implementation-of-css-flexbox-algorithm.html<div class="notion-page" id="9">
<div>Package <a target="_blank" href="https://github.com/kjk/flex">github.com/kjk/flex</a> implements <a target="_blank" href="https://www.w3.org/TR/css-flexbox-1/">CSS flexbox</a> layout algorithm in Go.</div>
<div>It’s a pure Go <a href="/article/wN9R/experience-porting-4.5k-loc-of-c-to-go-facebooks-css-flexbox-implementation-yoga.html">port</a> of Facebook’s <a target="_blank" href="https://github.com/facebook/yoga">Yoga</a> C library.</div>
<h2 class="hdr-with-anchor" id="high-level-api-overview"><div>High-level API overview</div><a class="header-anchor" href="#high-level-api-overview">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Despite implementing CSS flexbox spec, it isn’t tied to CSS/HTML in any way. Yoga, for example, can be integrated with iOS app and used to layout UIView hierarchy.</div>
<div>The library works on abstract tree of nodes. In HTML a node would correspond to a block element like a <code>div</code>. When used in Cocoa app, a node could represent <code>UIView</code> or <code>NSView</code>.</div>
<div>When used on windows, it could represent a HWND-based control.</div>
<div>The high-level use is:</div>
<ul>
<li>create a tree of nodes that represents a layout you want to represent</li>
<li>set desired flexbox properties on each node using <code>node.StyleSet*()</code> <a target="_blank" href="https://github.com/kjk/flex/blob/master/yoga_props.go#L58">functions</a></li>
<li>call <code>flex.CalculateLayout(rootNode, parentWidth, parentHeight, direction)</code></li>
<li>each node is now measured and positioned so you can e.g. size and position widgets associated with each node. You can get the size on position of nodes with <code>node.LayoutGet*()</code> <a target="_blank" href="https://github.com/kjk/flex/blob/master/yoga_props.go#L531">functions</a></li>
<li>when layout hierachy changes (e.g. the node represents a label and you’ve changed its text, which changes it’s intrinsic size), call <code>node.MarkDirty()</code> and <code>CalculateLayout()</code> to re-calculate new size/position of the nodes</li>
</ul>
<h2 class="hdr-with-anchor" id="an-exmple"><div>An exmple</div><a class="header-anchor" href="#an-exmple">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Let’s assume that we want to re-create the following HTML layout:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"percentage_multiple_nested_with_padding_margin_and_percentage_values"</span> <span class="na">style</span><span class="o">=</span><span class="s">"width: 200px; height: 200px; flex-direction: column;"</span><span class="p">></span>
</span></span><span class="line"><span class="cl"> <span class="p"><</span><span class="nt">div</span> <span class="na">style</span><span class="o">=</span><span class="s">"flex-grow: 1; flex-basis: 10%; min-width: 60%; margin: 5px; padding: 3px;"</span><span class="p">></span>
</span></span><span class="line"><span class="cl"> <span class="p"><</span><span class="nt">div</span> <span class="na">style</span><span class="o">=</span><span class="s">"width: 50%; margin: 5px; padding: 3%;"</span><span class="p">></span>
</span></span><span class="line"><span class="cl"> <span class="p"><</span><span class="nt">div</span> <span class="na">style</span><span class="o">=</span><span class="s">"width: 45%; margin: 5%; padding: 3px;"</span><span class="p">></</span><span class="nt">div</span><span class="p">></span>
</span></span><span class="line"><span class="cl"> <span class="p"></</span><span class="nt">div</span><span class="p">></span>
</span></span><span class="line"><span class="cl"> <span class="p"></</span><span class="nt">div</span><span class="p">></span>
</span></span><span class="line"><span class="cl"> <span class="p"><</span><span class="nt">div</span> <span class="na">style</span><span class="o">=</span><span class="s">"flex-grow: 4; flex-basis: 15%; min-width: 20%;"</span><span class="p">></</span><span class="nt">div</span><span class="p">></span>
</span></span><span class="line"><span class="cl"><span class="p"></</span><span class="nt">div</span><span class="p">></span>
</span></span></code></pre>
<div>The equivalent Go code is:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="nx">config</span> <span class="o">:=</span> <span class="nx">flex</span><span class="p">.</span><span class="nf">NewConfig</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">root</span> <span class="o">:=</span> <span class="nx">flex</span><span class="p">.</span><span class="nf">NewNodeWithConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">root</span><span class="p">.</span><span class="nf">StyleSetWidth</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">root</span><span class="p">.</span><span class="nf">StyleSetHeight</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span> <span class="o">:=</span> <span class="nx">flex</span><span class="p">.</span><span class="nf">NewNodeWithConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetFlexGrow</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetFlexBasisPercent</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeLeft</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeTop</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeRight</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeBottom</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeLeft</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeTop</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeRight</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeBottom</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">StyleSetMinWidthPercent</span><span class="p">(</span><span class="mi">60</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">root</span><span class="p">.</span><span class="nf">InsertChild</span><span class="p">(</span><span class="nx">rootChild0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span> <span class="o">:=</span> <span class="nx">flex</span><span class="p">.</span><span class="nf">NewNodeWithConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeLeft</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeTop</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeRight</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetMargin</span><span class="p">(</span><span class="nx">EdgeBottom</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetPaddingPercent</span><span class="p">(</span><span class="nx">EdgeLeft</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetPaddingPercent</span><span class="p">(</span><span class="nx">EdgeTop</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetPaddingPercent</span><span class="p">(</span><span class="nx">EdgeRight</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetPaddingPercent</span><span class="p">(</span><span class="nx">EdgeBottom</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">StyleSetWidthPercent</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0</span><span class="p">.</span><span class="nf">InsertChild</span><span class="p">(</span><span class="nx">rootChild0Child0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span> <span class="o">:=</span> <span class="nx">flex</span><span class="p">.</span><span class="nf">NewNodeWithConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetMarginPercent</span><span class="p">(</span><span class="nx">EdgeLeft</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetMarginPercent</span><span class="p">(</span><span class="nx">EdgeTop</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetMarginPercent</span><span class="p">(</span><span class="nx">EdgeRight</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetMarginPercent</span><span class="p">(</span><span class="nx">EdgeBottom</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeLeft</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeTop</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeRight</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetPadding</span><span class="p">(</span><span class="nx">EdgeBottom</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0Child0</span><span class="p">.</span><span class="nf">StyleSetWidthPercent</span><span class="p">(</span><span class="mi">45</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild0Child0</span><span class="p">.</span><span class="nf">InsertChild</span><span class="p">(</span><span class="nx">rootChild0Child0Child0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">rootChild1</span> <span class="o">:=</span> <span class="nx">flex</span><span class="p">.</span><span class="nf">NewNodeWithConfig</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild1</span><span class="p">.</span><span class="nf">StyleSetFlexGrow</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild1</span><span class="p">.</span><span class="nf">StyleSetFlexBasisPercent</span><span class="p">(</span><span class="mi">15</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">rootChild1</span><span class="p">.</span><span class="nf">StyleSetMinWidthPercent</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">root</span><span class="p">.</span><span class="nf">InsertChild</span><span class="p">(</span><span class="nx">rootChild1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nx">flex</span><span class="p">.</span><span class="nf">CalculateLayout</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">flex</span><span class="p">.</span><span class="nx">Undefined</span><span class="p">,</span> <span class="nx">flex</span><span class="p">.</span><span class="nx">Undefined</span><span class="p">,</span> <span class="nx">DirectionLTR</span><span class="p">)</span>
</span></span></code></pre>
<div>After <code>CalculateLayout</code> we can see the position of each node e.g.:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">"root left: %f\n"</span><span class="p">,</span> <span class="nx">root</span><span class="p">.</span><span class="nf">LayoutGetLeft</span><span class="p">())</span> <span class="c1">// 0
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">fmt</span><span class="p">.</span><span class="nx">Printf</span><span class="s">"root top: %f\n"</span><span class="p">,</span> <span class="nx">root</span><span class="p">.</span><span class="nf">LayoutGetTop</span><span class="p">())</span> <span class="c1">// 0
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">"root width: %f\n"</span><span class="p">,</span> <span class="nx">root</span><span class="p">.</span><span class="nf">LayoutGetWidth</span><span class="p">())</span> <span class="c1">// 200
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">"root height: %f\n"</span><span class="p">,</span> <span class="nx">root</span><span class="p">.</span><span class="nf">LayoutGetHeight</span><span class="p">())</span> <span class="c1">// 200
</span></span></span></code></pre>
<div>To see example for every flexbox property, look into <a target="_blank" href="https://github.com/facebook/yoga/tree/master/gentest/fixtures">github.com/facebook/yoga/gentest/fixtures</a>. Their names hint at which properties are being used.</div>
<div>Each file there has corresponding <code>*_test.go</code> file in <a target="_blank" href="https://github.com/kjk/flex">github.com/kjk/flex</a> directory which shows how to express it in Go.</div>
<h2 class="hdr-with-anchor" id="size-of-root-s-parent"><div>Size of root’s parent</div><a class="header-anchor" href="#size-of-root-s-parent">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Notice that in this particular example we used <code>flex.Undefined</code> as both height and width of the parent container.</div>
<div>Imagine you’re using <code>flex</code> to implment layout for a dekstop application where each <code>flex.Node</code> represents a control inside the window.</div>
<div>Window is the parent of root node.</div>
<div>In response to user resizing the window, you want to pass width/height of the window to <code>flex.CalculateLayout()</code>.</div>
<div>When you create the window initially, you might do the reverse: pass <code>flex.Undefined</code> as width/height of parent container and then use the size of root node as the size of the window, to size it to its content.</div>
<h2 class="hdr-with-anchor" id="measure-function"><div>Measure function</div><a class="header-anchor" href="#measure-function">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Imagine that a node represents an OS button. The button has some intrisic size dictated by its text.</div>
<div>To represent that size <code>flex</code> allows setting a measuring function with <code>node.SetMeasureFunc(measureFunc MeasureFunc)</code>. It’s definition is:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">MeasureFunc</span> <span class="kd">func</span><span class="p">(</span><span class="nx">node</span> <span class="o">*</span><span class="nx">Node</span><span class="p">,</span> <span class="nx">width</span> <span class="kt">float32</span><span class="p">,</span> <span class="nx">widthMode</span> <span class="nx">MeasureMode</span><span class="p">,</span> <span class="nx">height</span> <span class="kt">float32</span><span class="p">,</span> <span class="nx">heightMode</span> <span class="nx">MeasureMode</span><span class="p">)</span> <span class="nx">Size</span>
</span></span></code></pre>
<div>The functions takes a hint width/height which is the size of parent container and returns intrinsic size of node.</div>
<div>This is usefule e.g. when a node represents a paragraph of text. When you know width of the parent container, you can break it into multi-line text.</div>
<div>If measuring function needs some state, you can use <code>node.Context</code> to store it.</div>
</div>
Solo founders with profitable businesses, collected stories2017-06-23T00:00:00Ztag:blog.kowalczyk.info,2017-06-23:/article/wjRD/solo-founders-with-profitable-businesses-collected-stories.html<div class="notion-page" id="wjRD">
<div>People sometimes wonder: can I have a successful business as a single founder?</div>
<div>The answer is: <strong>yes</strong>.</div>
<div>This is a collection of solo-preneur success stories (with occasional 2 people bands).</div>
<div>I only include businesses generating significant revenues. In this context it’s around $5k/mo or more, enough to replace full-time salary.</div>
<div>Maybe it’ll inspire you to start your own, solo business.</div>
<div>Before you get too excited, keep the following in mind.</div>
<div>This list is the pinnacle of survivorship bias. Solo-preneur software business is not different that any other business, and most businesses fail. You have 10-20% chance of success so pick your idea wisely, work hard and if you fail, do it again.</div>
<div>At the same time, this is only tip of the iceberg. Those are the stories that people shared, cribbed from a few online sources.</div>
<div>There are 100x more successful solo founders that don’t share their numbers publicly. A silent majority of successful solo businesses.</div>
<h2 class="hdr-with-anchor" id="1-anonymous-making-750k-year-with-a-desktop-app-sold-via-his-website"><div>1. Anonymous making $750k/year with a desktop app, sold via his website</div><a class="header-anchor" href="#1-anonymous-making-750k-year-with-a-desktop-app-sold-via-his-website">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13168965">https://news.ycombinator.com/item?id=13168965</a></div>
<div>Adivice:</div>
<ul>
<li>make a desktop app that you can sell for $50-$300.</li>
<li>attack a large market that has stagnated or has entranched players with lousy apps i.e. make a better mousetrap.</li>
<li>electron is a good technology to write such app.</li>
</ul>
<div>Important factors of success:</div>
<ul>
<li>SEO</li>
<li>good reputation</li>
<li>big market</li>
<li>staying alive long enough for word of mouth to kick in</li>
</ul>
<h2 class="hdr-with-anchor" id="2-jollyturns"><div>2. JollyTurns</div><a class="header-anchor" href="#2-jollyturns">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13170798">https://news.ycombinator.com/item?id=13170798</a></div>
<div>Web/iOS/Android app for ski resorts.</div>
<div>Income: unknown.</div>
<div>How it makes money: in-app purchase on iOS/Android ($1 per ski resort or $15 for all of them, see <a target="_blank" href="https://itunes.apple.com/us/app/jollyturns/id719208522">https://itunes.apple.com/us/app/jollyturns/id719208522</a>)</div>
<div>First version (iOS only) released in Dec 2013 after 2 <sup>1</sup>⁄<sub>2</sub> years of work (<a target="_blank" href="https://jollyturns.com/blog/first-public-release">https://jollyturns.com/blog/first-public-release</a>).</div>
<div>Insight: code is the easy part, marketing is hardest.</div>
<div>Tried to find a partner in SV but couldn’t. Wrote code himself, hired people to collect data about ski resorts.</div>
<h2 class="hdr-with-anchor" id="3-crm-plugin-that-finds-location-of-customer-s-offices-based-on-address-of-the-hotel-you-re-traveling-to"><div>3. CRM plugin that finds location of customer’s offices based on address of the hotel you’re traveling to</div><a class="header-anchor" href="#3-crm-plugin-that-finds-location-of-customer-s-offices-based-on-address-of-the-hotel-you-re-traveling-to">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13167930">https://news.ycombinator.com/item?id=13167930</a></div>
<div>Sold for $50. “made pretty good money”.</div>
<h2 class="hdr-with-anchor" id="4-niche-app-200k-year-after-6-years"><div>4. Niche app, $200k/year after 6 years</div><a class="header-anchor" href="#4-niche-app-200k-year-after-6-years">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13170346">https://news.ycombinator.com/item?id=13170346</a></div>
<div>Most likely USB driver that allows using USB devices remotely over a network (<a target="_blank" href="https://www.virtualhere.com">https://www.virtualhere.com,</a> <a target="_blank" href="https://forum.openwrt.org/search.php?action=show_user_posts&user_id=129043">https://forum.openwrt.org/search.php?action=show_user_posts&user_id=129043</a> is same user name as on HN post).</div>
<div>No marketing, gets sales via word of mouth and internet searches.</div>
<h2 class="hdr-with-anchor" id="5-website-templates-100k-year-profit"><div>5. Website templates, $100k/year profit</div><a class="header-anchor" href="#5-website-templates-100k-year-profit">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13935663">https://news.ycombinator.com/item?id=13935663</a></div>
<div>Sold on <a target="_blank" href="https://themeforest.net/">https://themeforest.net/</a>. Working about 4hrs a week.</div>
<h2 class="hdr-with-anchor" id="6-shopify-plugins"><div>6. Shopify plugins</div><a class="header-anchor" href="#6-shopify-plugins">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13167990">https://news.ycombinator.com/item?id=13167990</a></div>
<div>Makes enough money to hire full-time developer for onboarding of new clients, support and documentation. Does product development himself.</div>
<div>Insight: the trick for coming up with product ideas is to first do custom development. When enough clients are willing to spend a few thousand for a personal implementation of something, that’s when you know you have an opportunity to charge 40$ per month for a SaaS version.</div>
<h2 class="hdr-with-anchor" id="7-20k-mo-from-3-niche-saas-products"><div>7. $20k/mo from 3 niche SaaS products</div><a class="header-anchor" href="#7-20k-mo-from-3-niche-saas-products">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13170108">https://news.ycombinator.com/item?id=13170108</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=12066819">https://news.ycombinator.com/item?id=12066819</a></li>
</ul>
<div>Also in the past ran real estate SaaS, making $70k/mo at its height (it then crashed when real estate market crashed).</div>
<div>Advice:</div>
<ul>
<li>tip for a newbie would be to look for something niche that solves a pain point or allows your customer to make or save money. For example: if your customer can spend $10/mo on your software and make or save $30/mo from that, you will have no problem getting & keeping customers.</li>
<li>don’t focus on becoming a unicorn. You can make some serious money and build a very comfortable life running a $300k to $1m dollar business, and your chances of succeeding at that are much greater.</li>
<li>Look for things outside of tech. There are so many problems to solve in small businesses. Many will say there is no money to make with small businesses.</li>
<li>Learn everything you can about advertising. Get really good at it and be willing to spend money on advertising.</li>
<li>Be willing to kill something off quickly if it doesn’t make money. Test your market early to make sure people will pay for it. I have made the mistake of not doing this and I have lost a lot of money because of it. Now, I need to be able to see a positive ROI on my spend within 3 to 4 months. So if I spent $100 to acquire a customer, I want to be able to get that back + more within 3 to 4 months. I know this timeline is probably really short for a VC funded company, but I have always been bootstrapped so don’t have the luxury of risking a longer time-frame for return.</li>
<li>it is not easy! Prepare to put in long hours especially in the beginning. Prepare for it to take a mental toll at times. You will second guess yourself, feel insecure, be consumed oftentimes with your business.</li>
</ul>
<h2 class="hdr-with-anchor" id="8-ngrok"><div>8. Ngrok</div><a class="header-anchor" href="#8-ngrok">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13168185">https://news.ycombinator.com/item?id=13168185</a></div>
<div>Full time for past 4 years.</div>
<div>Insight: minimize support by improving UX, documentation, error messages.</div>
<h2 class="hdr-with-anchor" id="9-football-betting-analysis-predictions-website-75k-year"><div>9. Football betting analysis/predictions website, £75k / year</div><a class="header-anchor" href="#9-football-betting-analysis-predictions-website-75k-year">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=11216868">https://news.ycombinator.com/item?id=11216868</a></div>
<div><a target="_blank" href="http://betalyst.com/">http://betalyst.com/</a> makes £75,000 per year in advertising/sponsorship/affiliate revenue.</div>
<div>Gets !25k visitors a month.</div>
<div>Works 2-3 hrs a week.</div>
<div>Website traffic generation:</div>
<ul>
<li>Android app with 75k users</li>
<li>10% of traffic is from organic search</li>
<li>majority of traffic from email (20k subscribers)</li>
</ul>
<h2 class="hdr-with-anchor" id="10-watermarking-desktop-app-for-mac-windows"><div>10. Watermarking desktop app for Mac/Windows</div><a class="header-anchor" href="#10-watermarking-desktop-app-for-mac-windows">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13150864">https://news.ycombinator.com/item?id=13150864</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=11604736">https://news.ycombinator.com/item?id=11604736</a></li>
</ul>
<div>Makes $3k-$5k per month after 5 years.</div>
<div>Sells for $30/$60/$140.</div>
<h2 class="hdr-with-anchor" id="11-real-estate-startup-130k-year-of-profit"><div>11. Real estate startup, $130k/year of profit</div><a class="header-anchor" href="#11-real-estate-startup-130k-year-of-profit">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=10887978">https://news.ycombinator.com/item?id=10887978</a></div>
<div>Profit:</div>
<ul>
<li>130k in 2013</li>
<li>$100k in 2014</li>
<li>$140k in 2015</li>
</ul>
<div>Source of revenues:</div>
<ul>
<li>30% AdSense</li>
<li>20% users</li>
<li>50% affiliate marketing</li>
</ul>
<div>Sources of traffic:</div>
<ul>
<li>50% organic</li>
<li>37% referral</li>
<li>13% direct</li>
</ul>
<div>No marketing, no blog, no social presence.</div>
<h2 class="hdr-with-anchor" id="12-dan-grossman-improvely-and-w3counter-45k-mo"><div>12. Dan Grossman, improvely and w3counter, $45k/mo</div><a class="header-anchor" href="#12-dan-grossman-improvely-and-w3counter-45k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=10881301">https://news.ycombinator.com/item?id=10881301</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=9726951">https://news.ycombinator.com/item?id=9726951</a> : how he started</li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=10201718">https://news.ycombinator.com/item?id=10201718</a>, <a target="_blank" href="https://news.ycombinator.com/item?id=8396497">https://news.ycombinator.com/item?id=8396497</a> : business tips</li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=8406049">https://news.ycombinator.com/item?id=8406049</a></li>
</ul>
<div><a target="_blank" href="https://www.w3counter.com/">https://www.w3counter.com/</a> uses freemium model, people pay subscription for advanced features</div>
<div><a target="_blank" href="https://www.improvely.com/">https://www.improvely.com/</a> : SaaS priced $29/$79/$149/$299 / mo.</div>
<div>First customers for improvely came from $100-$200/month AdWords advertising for the first few months and $79/month banner ad on a web stats site bought via BuySellAds.com.</div>
<div>Used SnapEgage chat widget on the website to talk visitors to sign up.</div>
<div>Word of mouth and referrals started quickly after that.</div>
<div>Now referrals are biggest signup drivers.</div>
<h2 class="hdr-with-anchor" id="13-vnc-application-for-mac-and-ios"><div>13. VNC application for Mac and iOS</div><a class="header-anchor" href="#13-vnc-application-for-mac-and-ios">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Sells for $30 on Mac and $20 on iOS.</div>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://lucvandal.com/2017/01/19/ten-years/">https://lucvandal.com/2017/01/19/ten-years/</a></li>
<li><a target="_blank" href="https://lucvandal.com/2015/07/05/how-to-be-a-successful-indie-developer/">https://lucvandal.com/2015/07/05/how-to-be-a-successful-indie-developer/</a></li>
</ul>
<h2 class="hdr-with-anchor" id="14-nomadlist-400k-year-revenue"><div>14. NomadList, $400k/year revenue</div><a class="header-anchor" href="#14-nomadlist-400k-year-revenue">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13443124">https://news.ycombinator.com/item?id=13443124</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13443124">https://news.ycombinator.com/item?id=13443124</a></li>
</ul>
<div>Revenue source: membership fees for community of digital nomads and remote workers.</div>
<h2 class="hdr-with-anchor" id="15-b2b-windows-desktop-app-1-million-year-sales"><div>15. B2B Windows desktop app, $1 million/year sales</div><a class="header-anchor" href="#15-b2b-windows-desktop-app-1-million-year-sales">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=12066104">https://news.ycombinator.com/item?id=12066104</a></div>
<div>Wrote scrach-my-itch app, was side project for 10 years until it started making $120k/mo. Went full time after that. Is in a very crowded niche.</div>
<div>Insight: coding is easy, marketing is hard. Must be persistent.</div>
<h2 class="hdr-with-anchor" id="16-pinboard-bookmarking-web-service-200k-year"><div>16. Pinboard, bookmarking web service, $200k/year</div><a class="header-anchor" href="#16-pinboard-bookmarking-web-service-200k-year">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=14438221">https://news.ycombinator.com/item?id=14438221</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=14438296">https://news.ycombinator.com/item?id=14438296</a></li>
</ul>
<div>Revenue history: <a target="_blank" href="https://blog.pinboard.in/2016/07/pinboard_turns_seven/">https://blog.pinboard.in/2016/07/pinboard_turns_seven/</a></div>
<h2 class="hdr-with-anchor" id="17-s3stat-equivalent-of-a-nice-senior-developer-salary"><div>17. s3stat, “equivalent of a nice Senior Developer salary”</div><a class="header-anchor" href="#17-s3stat-equivalent-of-a-nice-senior-developer-salary">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="http://www.expatsoftware.com/Articles/guy-on-the-beach-with-a-laptop.html">http://www.expatsoftware.com/Articles/guy-on-the-beach-with-a-laptop.html</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=14438283">https://news.ycombinator.com/item?id=14438283</a></li>
</ul>
<h2 class="hdr-with-anchor" id="18-storeslider-700k-in-2016"><div>18. StoreSlider, $700k in 2016</div><a class="header-anchor" href="#18-storeslider-700k-in-2016">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=14438303">https://news.ycombinator.com/item?id=14438303</a></div>
<div>Makes money with affiliate revenue from eBay.</div>
<div>Built with Lumen on PHP 7.1, Nginx, running on Linode.</div>
<div>Source of traffic: word of mouth, social sharing, Google search.</div>
<div>Did a lot of A/B testing to maximize conversion.s.</div>
<h2 class="hdr-with-anchor" id="19-builtiwith-com-estimated-12-million-year"><div>19. BuiltiWith.com, estimated $12 million/year</div><a class="header-anchor" href="#19-builtiwith-com-estimated-12-million-year">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=14437983">https://news.ycombinator.com/item?id=14437983</a></li>
<li><a target="_blank" href="http://www.startupdaily.net/2015/09/builtwith-is-perhaps-one-of-australias-most-profitable-online-companies-and-has-zero-staff/">http://www.startupdaily.net/2015/09/builtwith-is-perhaps-one-of-australias-most-profitable-online-companies-and-has-zero-staff/</a></li>
</ul>
<h2 class="hdr-with-anchor" id="20-tarsnap-backup-service-better-than-google-salary"><div>20. tarsnap, backup service, “better than Google salary”</div><a class="header-anchor" href="#20-tarsnap-backup-service-better-than-google-salary">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=14442425">https://news.ycombinator.com/item?id=14442425</a></div>
<h2 class="hdr-with-anchor" id="21-sidekiq-1-million-year"><div>21. Sidekiq, $1 million/year</div><a class="header-anchor" href="#21-sidekiq-1-million-year">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://www.indiehackers.com/businesses/sidekiq">https://www.indiehackers.com/businesses/sidekiq</a></div>
<div>Open source library for Ruby, a background job framework.</div>
<div>Sells pro version for $950/year and enterprise version. Only needs 800 customer</div>
<h2 class="hdr-with-anchor" id="22-balsamiq-2-millions-in-revenue-after-18-months"><div>22. Balsamiq, $2 millions in revenue after 18 months</div><a class="header-anchor" href="#22-balsamiq-2-millions-in-revenue-after-18-months">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="http://conversionaid.com/podcast/peldi-guilizzoni-balsamiq/">http://conversionaid.com/podcast/peldi-guilizzoni-balsamiq/</a></li>
<li><a target="_blank" href="http://programmingzen.com/startup-interviews-balsamiq-studio-llc/">http://programmingzen.com/startup-interviews-balsamiq-studio-llc/</a></li>
</ul>
<div>It’s no longer a single person but it was created in 2008 by a single person and within 18 monts reached $2 million in revenue.</div>
<div>It’s a desktop app for creating mockups.</div>
<h2 class="hdr-with-anchor" id="23-john-gruber-32k-mo"><div>23. John Gruber, $32k/mo</div><a class="header-anchor" href="#23-john-gruber-32k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://daringfireball.net/feeds/sponsors/">https://daringfireball.net/feeds/sponsors/</a></div>
<div>Makes $8k per week for sponsorship (ads) on his very popular, Apple-oriented website.</div>
<h2 class="hdr-with-anchor" id="24-sales-tracking-crm-app-for-small-business"><div>24. Sales tracking & CRM app for small business.</div><a class="header-anchor" href="#24-sales-tracking-crm-app-for-small-business">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=14439284">https://news.ycombinator.com/item?id=14439284</a></div>
<div><a target="_blank" href="https://www.bottomlinehq.com/">https://www.bottomlinehq.com/</a>, 6-digit revenue.</div>
<div>Freemium model, $30/year. Web and iOS.</div>
<h2 class="hdr-with-anchor" id="25-https-officesnapshots-com-full-time-salary"><div>25. <a target="_blank" href="https://officesnapshots.com/">https://officesnapshots.com/</a>, full-time salary.</div><a class="header-anchor" href="#25-https-officesnapshots-com-full-time-salary">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=12065574">https://news.ycombinator.com/item?id=12065574</a></div>
<div>Started 9 years ago, full-time for last 4 years.</div>
<div>Revenue: AdSense and later selling his own advertising (ads are sold as $/month and sold in blocks of 1 to 12 months.</div>
<h2 class="hdr-with-anchor" id="26-pubexchange-com"><div>26. pubexchange.com</div><a class="header-anchor" href="#26-pubexchange-com">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13334001">https://news.ycombinator.com/item?id=13334001</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=12073418">https://news.ycombinator.com/item?id=12073418</a></li>
</ul>
<div>Stated in 2013, operated solo since then. Profitable since 2014.</div>
<div>In early days pitched his service on linkedin, now people come from referrals.</div>
<h2 class="hdr-with-anchor" id="27-park-io-125k-mo"><div>27. park.io, $125k/mo</div><a class="header-anchor" href="#27-park-io-125k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://www.indiehackers.com/businesses/park-io">https://www.indiehackers.com/businesses/park-io</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=9726435">https://news.ycombinator.com/item?id=9726435</a></li>
</ul>
<div>Started in 2014. In his spare time he wrote a script to auto-buy domain when it expires and turned that into paid service by adding registration, payments etc.</div>
<div>Most users find it from either parked domains or word of mouth.</div>
<div>Insight: automate all the things.</div>
<h2 class="hdr-with-anchor" id="28-cronitor-6k-mo-revenue-after-3-years"><div>28. cronitor, $6k/mo revenue after 3 years</div><a class="header-anchor" href="#28-cronitor-6k-mo-revenue-after-3-years">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://www.indiehackers.com/businesses/cronitor">https://www.indiehackers.com/businesses/cronitor</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13680922">https://news.ycombinator.com/item?id=13680922</a></li>
</ul>
<div>Started in 2014, written part-time by 2 people. They wrote it because it solved a real problem they had at a startup he worked at.</div>
<div>Marketing tactics:</div>
<ul>
<li>did ShowHN</li>
<li>answered questions on StackOverflow</li>
<li>added a link from a popular, open-source PHP library they had</li>
<li>created Stackshare page</li>
<li>submitted to startupli.st (site defunct)</li>
<li>submitted to “One Thing Well” website</li>
<li>wrote high-quality docs for SEO (topic-based articles on ‘how to use cronitor to do X’)</li>
</ul>
<div>Raised prices after 6 months from $7/$20/$50 => $10/$25/$50 and then $24/$70/$150.</div>
<h2 class="hdr-with-anchor" id="29-bugmuncher-4k-mo-revenue"><div>29. bugmuncher, $4k/mo revenue</div><a class="header-anchor" href="#29-bugmuncher-4k-mo-revenue">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13327868">https://news.ycombinator.com/item?id=13327868</a></li>
<li><a target="_blank" href="https://www.bugmuncher.com/blog/from-side-project-to-profitable-start-up-part-29/">https://www.bugmuncher.com/blog/from-side-project-to-profitable-start-up-part-29/</a></li>
</ul>
<div>Web-based bug tracking software.</div>
<div>Started as a side project in 2010, went full time in Nov 2015, reached living wage in Nov 2016.</div>
<h2 class="hdr-with-anchor" id="30-https-info-beamer-com-https-info-beamer-com-close-to-living-wage"><div>30. <a target="_blank" href="https://info-beamer.com">https://info-beamer.com</a>, close to living wage</div><a class="header-anchor" href="#30-https-info-beamer-com-https-info-beamer-com-close-to-living-wage">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13514865">https://news.ycombinator.com/item?id=13514865</a></div>
<div>Digital signage for Raspberry PI. Started as a side project, turned into profitable business.</div>
<h2 class="hdr-with-anchor" id="31-anonymous-app-5k-mo-profit-on-7k-mo-revenue"><div>31. Anonymous app, $5k/mo profit on $7k/mo revenue</div><a class="header-anchor" href="#31-anonymous-app-5k-mo-profit-on-7k-mo-revenue">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/threads?id=gaeappthrowaway">https://news.ycombinator.com/threads?id=gaeappthrowaway</a></div>
<div>App hosted on App Engine, ~50 users paying between $30/mo and $500/mo. Analytics API.</div>
<h2 class="hdr-with-anchor" id="32-wordpress-theme-5k-mo"><div>32. Wordpress theme, $5k/mo</div><a class="header-anchor" href="#32-wordpress-theme-5k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=8107836">https://news.ycombinator.com/item?id=8107836</a></div>
<div>Sold via ThemeForest.</div>
<h2 class="hdr-with-anchor" id="33-radio-silence-main-income"><div>33. Radio Silence, main income</div><a class="header-anchor" href="#33-radio-silence-main-income">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=12722772">https://news.ycombinator.com/item?id=12722772</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=12034558">https://news.ycombinator.com/item?id=12034558</a></li>
</ul>
<div>Mac app (<a target="_blank" href="https://radiosilenceapp.com/">https://radiosilenceapp.com/</a>) that evolved from side project to providing main income for the author. Sells for $9.</div>
<div>Author was able to quit his job.</div>
<div>Business tip: build related free app and host on the same domain.</div>
<h2 class="hdr-with-anchor" id="34-ryan-clark-10-games-3-million-over-10-years"><div>34. Ryan Clark, 10 Games, $3+ million over 10 years</div><a class="header-anchor" href="#34-ryan-clark-10-games-3-million-over-10-years">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="http://www.gamasutra.com/blogs/RyanClark/20150917/253842/What_Makes_an_Indie_Hit_How_to_Choose_the_Right_Design.php">http://www.gamasutra.com/blogs/RyanClark/20150917/253842/What_Makes_an_Indie_Hit_How_to_Choose_the_Right_Design.php</a></div>
<div>Working full time since 2004 on his games. Wrote 10 games in 11 years. 8 been profitable, 3 grossed more than $1M.</div>
<h2 class="hdr-with-anchor" id="35-https-phantomjscloud-com-https-phantomjscloud-com-ramen-profitable-for-seattle"><div>35. <a target="_blank" href="https://phantomjscloud.com">https://PhantomJsCloud.com</a>, ramen profitable for Seattle</div><a class="header-anchor" href="#35-https-phantomjscloud-com-https-phantomjscloud-com-ramen-profitable-for-seattle">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=13327835">https://news.ycombinator.com/item?id=13327835</a></div>
<h2 class="hdr-with-anchor" id="36-desktop-app-seating-planning-120k-year"><div>36. Desktop app, seating planning, $120k+/year</div><a class="header-anchor" href="#36-desktop-app-seating-planning-120k-year">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://successfulsoftware.net/2015/01/07/10-years-a-microisv/">https://successfulsoftware.net/2015/01/07/10-years-a-microisv/</a></li>
<li><a target="_blank" href="https://successfulsoftware.net/2013/11/06/lifestyle-programming/">https://successfulsoftware.net/2013/11/06/lifestyle-programming/</a></li>
</ul>
<div>2 desktop apps for Windows, written in C++.</div>
<div>Over 10 years, sold 40 thousand licenses of first desktop app, the cheapest is $30, which is at least $120k/year.</div>
<h2 class="hdr-with-anchor" id="37-desktop-app-in-construction-industry-making-a-living"><div>37. Desktop app in construction industry, making a living</div><a class="header-anchor" href="#37-desktop-app-in-construction-industry-making-a-living">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=11659140">https://news.ycombinator.com/item?id=11659140</a></div>
<div>21-year old app for Windows, written in Delphi 5.</div>
<h2 class="hdr-with-anchor" id="38-video-games-making-a-living"><div>38. Video games, making a living</div><a class="header-anchor" href="#38-video-games-making-a-living">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=11659283">https://news.ycombinator.com/item?id=11659283</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13838257">https://news.ycombinator.com/item?id=13838257</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13328138">https://news.ycombinator.com/item?id=13328138</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=12066509">https://news.ycombinator.com/item?id=12066509</a></li>
</ul>
<div>Multi-platform games written in C#, based on Unity game engine, released on Steam. Makes a game every X months.</div>
<h2 class="hdr-with-anchor" id="39-pinegrow-web-editor-comfortable-living"><div>39. Pinegrow Web Editor, comfortable living</div><a class="header-anchor" href="#39-pinegrow-web-editor-comfortable-living">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=11659561">https://news.ycombinator.com/item?id=11659561</a></li>
<li><a target="_blank" href="https://medium.com/@mattront/pinegrow-year-in-review-2014-from-0-to-100k-fed4e7a05689">https://medium.com/@mattront/pinegrow-year-in-review-2014-from-0-to-100k-fed4e7a05689</a></li>
</ul>
<div>Desktop web editor built with NWJS/Electron. Started by a single person, grew to 3 full-time people.</div>
<div>Launched in January 2014 after 2.5 years in development, sold $100k the first year. Sells for $49/$79.</div>
<div>Marketing: website and asking for e-mail address when starting the trial to build e-mail database to send promotions to.</div>
<div>Tried Carbon, Google and Reddit ads but was losing money on them.</div>
<h2 class="hdr-with-anchor" id="40-https-ipinfo-io-https-ipinfo-io-full-time-job"><div>40. <a target="_blank" href="https://ipinfo.io">https://ipinfo.io</a>, full-time job</div><a class="header-anchor" href="#40-https-ipinfo-io-https-ipinfo-io-full-time-job">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://blog.ipinfo.io/api-side-project-to-250-million-requests-with-0-marketing-budget-bb0de01c01f6">https://blog.ipinfo.io/api-side-project-to-250-million-requests-with-0-marketing-budget-bb0de01c01f6</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=14678473">https://news.ycombinator.com/item?id=14678473</a></li>
</ul>
<div>Wrote and launched in a couple of hours as a response to StackOverflow question about. Posted as a response, forgot about it, it became popular so he implemented paid plans and started charging for it.</div>
<h2 class="hdr-with-anchor" id="41-http-duetapp-com-http-duetapp-com-3-4k-mo"><div>41. <a target="_blank" href="http://duetapp.com">http://duetapp.com</a>, $3-4k/mo</div><a class="header-anchor" href="#41-http-duetapp-com-http-duetapp-com-3-4k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Self-hosted, web-based invoicing and project management app.</div>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=8630931">https://news.ycombinator.com/item?id=8630931</a></div>
<h2 class="hdr-with-anchor" id="42-https-betterexplained-com-https-betterexplained-com-6k-mo-after-10-years"><div>42. <a target="_blank" href="https://betterexplained.com">https://betterexplained.com</a>, $6k/mo after 10 years</div><a class="header-anchor" href="#42-https-betterexplained-com-https-betterexplained-com-6k-mo-after-10-years">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>A website with math tutorials. Started in 2006. Content is free. Makes money selling ebooks (on Amazon kindle and directly from the website), amazon affiliate links and newsletter sponsorships.</div>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://betterexplained.com/articles/life-lessons-10-years/">https://betterexplained.com/articles/life-lessons-10-years/</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=8631204">https://news.ycombinator.com/item?id=8631204</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=870015">https://news.ycombinator.com/item?id=870015</a></li>
</ul>
<h2 class="hdr-with-anchor" id="43-website-with-special-interest-news-0-15k-mo"><div>43. Website with special-interest news, $!0-15k/mo</div><a class="header-anchor" href="#43-website-with-special-interest-news-0-15k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Money from AdSense.</div>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=8630369">https://news.ycombinator.com/item?id=8630369</a></div>
<h2 class="hdr-with-anchor" id="44-5-5k-mo-from-udemy-course"><div>44. $5.5k/mo from Udemy course</div><a class="header-anchor" href="#44-5-5k-mo-from-udemy-course">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=8631035">https://news.ycombinator.com/item?id=8631035</a></div>
<h2 class="hdr-with-anchor" id="45-storemapper-21k-mo"><div>45. Storemapper, $21k/mo</div><a class="header-anchor" href="#45-storemapper-21k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://www.indiehackers.com/businesses/storemapper">https://www.indiehackers.com/businesses/storemapper</a></li>
<li><a target="_blank" href="http://tylertringas.com/storemapper-bootstrapped-to-50000year-in-2-years-with-live-metrics/">http://tylertringas.com/storemapper-bootstrapped-to-50000year-in-2-years-with-live-metrics/</a></li>
</ul>
<h2 class="hdr-with-anchor" id="46-brendan-dunn-451k-revenue-in-2014-from-several-products"><div>46. Brendan Dunn, $451k revenue in 2014 from several products</div><a class="header-anchor" href="#46-brendan-dunn-451k-revenue-in-2014-from-several-products">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://doubleyourfreelancing.com/annual-review-2014/">https://doubleyourfreelancing.com/annual-review-2014/</a></li>
<li><a target="_blank" href="https://doubleyourfreelancing.com/2016-year-review/">https://doubleyourfreelancing.com/2016-year-review/</a></li>
<li><a target="_blank" href="https://doubleyourfreelancing.com/2015-year-in-review/">https://doubleyourfreelancing.com/2015-year-in-review/</a></li>
</ul>
<div>His income:</div>
<ul>
<li><a target="_blank" href="https://doubleyourfreelancing.com/rate/">https://doubleyourfreelancing.com/rate/</a>, online course, $207k</li>
<li><a target="_blank" href="http://doubleyourfreelancing.com/leads/">http://doubleyourfreelancing.com/leads/</a>, online course, $40k</li>
<li><a target="_blank" href="https://planscope.io/">https://planscope.io/</a>, SaaS app, $71k</li>
<li><a target="_blank" href="http://buildaconsultancy.com/">http://buildaconsultancy.com/</a>, live classes, $44k from 3 live classes</li>
<li>consulting and coaching, $89k from 6 weeks of consulting</li>
</ul>
<h2 class="hdr-with-anchor" id="47-cooking-blog-5-6k-mo"><div>47. Cooking blog, $5-6k/mo</div><a class="header-anchor" href="#47-cooking-blog-5-6k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>He does the design/programming/marketing/monetization work behind <a target="_blank" href="http://www.theyummylife.com">http://www.theyummylife.com</a>, his mother does the writing. Revenue from Amazon affiliates, ads and ebook.</div>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=4467954">https://news.ycombinator.com/item?id=4467954</a></li>
<li><a target="_blank" href="https://www.lessannoyingcrm.com/blog/2012/04/259/How+I+monetized+a+blog+in+30+days%3A+what+worked%2C+and+what+didn%27t">https://www.lessannoyingcrm.com/blog/2012/04/259/How+I+monetized+a+blog+in+30+days%3A+what+worked%2C+and+what+didn%27t</a></li>
</ul>
<h2 class="hdr-with-anchor" id="48-few-dozens-entertainment-related-websites-122k-mo"><div>48. Few dozens entertainment-related websites, $122k/mo</div><a class="header-anchor" href="#48-few-dozens-entertainment-related-websites-122k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Revenue from AdSense, 8 million monthly uniques, after 6 years.</div>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=4468067">https://news.ycombinator.com/item?id=4468067</a></div>
<h2 class="hdr-with-anchor" id="49-https-www-tiki-toki-com-https-www-tiki-toki-com-5k-mo"><div>49. <a target="_blank" href="https://www.tiki-toki.com/">https://www.tiki-toki.com/</a>, $5k/mo</div><a class="header-anchor" href="#49-https-www-tiki-toki-com-https-www-tiki-toki-com-5k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Web and desktop app for creating pretty timelines. Makes money from premium accounts and selling desktop app.</div>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=4469672">https://news.ycombinator.com/item?id=4469672</a></div>
<h2 class="hdr-with-anchor" id="50-webapp-in-education-space-90k-m"><div>50. Webapp in education space, $90k/m</div><a class="header-anchor" href="#50-webapp-in-education-space-90k-m">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Revenue: AdSsense.</div>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=4468535">https://news.ycombinator.com/item?id=4468535</a></div>
<h2 class="hdr-with-anchor" id="51-large-web-community-90-110k-mo"><div>51. Large web community, $90-110k/mo</div><a class="header-anchor" href="#51-large-web-community-90-110k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Revenu from subscription, adsense, other ad revenue, license and royalty revenue</div>
<div>Source: <a target="_blank" href="https://news.ycombinator.com/item?id=2567487">https://news.ycombinator.com/item?id=2567487</a></div>
<h2 class="hdr-with-anchor" id="52-zencaster-12k-mo"><div>52. Zencaster, $12k/mo</div><a class="header-anchor" href="#52-zencaster-12k-mo">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Zencaster is a web-based tool that helps podcasters record their guests in studio quality.</div>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://www.indiehackers.com/businesses/zencastr">https://www.indiehackers.com/businesses/zencastr</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13533894">https://news.ycombinator.com/item?id=13533894</a></li>
<li><a target="_blank" href="https://news.ycombinator.com/item?id=13169704">https://news.ycombinator.com/item?id=13169704</a></li>
</ul>
<h2 class="hdr-with-anchor" id="53-workflowy-800k-year"><div>53. Workflowy, $800k/year</div><a class="header-anchor" href="#53-workflowy-800k-year">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Jesse Patel learn how to program building <a target="_blank" href="https://workflowy.com/">https://workflowy.com/</a>. After 9 months of working on it alone, he asked a friend he knew from college to join him. They got into YC to work on a different idea but pivoted back to Workflowy.</div>
<div>They started charging 2 years after they launched and got enough revenue to pay for living expenses.</div>
<div>They have 100k paying users and $800k/year revenue.</div>
<div>Source:</div>
<ul>
<li><a target="_blank" href="https://www.indiehackers.com/podcast/037-jesse-patel-of-workflowy">https://www.indiehackers.com/podcast/037-jesse-patel-of-workflowy</a></li>
</ul>
</div>
Analyzing browserify bundles to minimize JavaScript bundle size2017-01-04T00:00:00Ztag:blog.kowalczyk.info,2017-01-04:/article/3/analyzing-browserify-bundles-to-minimize-javascript-bundle-size.html<div class="notion-page" id="3">
<div>When building web apps, it’s important to keep the size of JavaScript code delivered to the browser as small as possible.</div>
<div>I write in ES6 or TypeScript then use browserify to combine all JavaScript code into a single bundle file. For production builds I use uglify to make the bundle smaller.</div>
<div>Unfortunately, by default we are blind to what ends up in the final bundle. A single <code>import</code> can introduce surprising, unneeded dependencies.</div>
<div>First step of fixing bloat is to see what code ends up in the final bundle.</div>
<h2 class="hdr-with-anchor" id="disc"><div>Disc</div><a class="header-anchor" href="#disc">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><a target="_blank" href="http://hughsk.io/disc/">Disc</a> is one tool that visualizes the content of JavaScript bundle.</div>
<div>To use it:</div>
<ul>
<li><code>npm install -g disc</code></li>
<li>add <code>fullPaths: true</code> option to <code>browserify</code> plugin (without it file paths are turned into opaque numbers)</li>
<li><code>discify dist/bundle.min.js >out.html</code> (or whatever <code>bundle.min.js</code> is called in your setup)</li>
<li><code>open out.html</code> (on mac, or open manually in the browser)</li>
</ul>
<div>The visualization is very pretty but not very good for understanding.</div>
<h2 class="hdr-with-anchor" id="source-map-explorer"><div>source-map-explorer</div><a class="header-anchor" href="#source-map-explorer">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div><a target="_blank" href="https://www.npmjs.com/package/source-map-explorer">source-map-explorer</a> shows the same information but in a more useful way.</div>
<div>To use it:</div>
<ul>
<li><code>npm install -g source-map-explorer</code></li>
<li>make sure that you generate JavaScript maps file</li>
<li><code>source-map-explorer dist/bundle.min.js dist/bundle.min.js.map</code></li>
</ul>
<div>This will open the browser for you with the treemap visualization.</div>
<h2 class="hdr-with-anchor" id="analyzing-dependency-tree"><div>Analyzing dependency tree</div><a class="header-anchor" href="#analyzing-dependency-tree">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>Disc and source-map-explorer can tell you what but not why.</div>
<div>When you see a JavaScript package that shouldn’t be there, you need to know why it’s there i.e. where it was imported from.</div>
<div>I haven’t found a tool that makes it easy, but it’s possible to create a primitive debug tool yourself.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">through</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'through2'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">b</span> <span class="o">=</span> <span class="nx">browserify</span><span class="p">(</span><span class="nx">browserifyOpts</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">showDeps</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// for debugging dump (flattened and inverted) dependency tree
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// b is browserify instance
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">b</span><span class="p">.</span><span class="nx">pipeline</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">'deps'</span><span class="p">).</span><span class="nx">push</span><span class="p">(</span><span class="nx">through</span><span class="p">.</span><span class="nx">obj</span><span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="kd">function</span><span class="p">(</span><span class="nx">row</span><span class="p">,</span> <span class="nx">enc</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// format of row is { id, file, source, entry, deps }
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// deps is {} where key is module name and value is file it comes from
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">row</span><span class="p">.</span><span class="nx">file</span> <span class="o">||</span> <span class="nx">row</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">k</span> <span class="k">in</span> <span class="nx">row</span><span class="p">.</span><span class="nx">deps</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="nx">row</span><span class="p">.</span><span class="nx">deps</span><span class="p">[</span><span class="nx">k</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">' '</span><span class="p">,</span> <span class="nx">k</span><span class="p">,</span> <span class="s1">':'</span><span class="p">,</span> <span class="nx">v</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">next</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">}));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>This displays dependencies in the format:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl">/quicknotes/node_modules/react-dom/lib/LinkedValueUtils.js
</span></span><span class="line"><span class="cl">./reactProdInvariant : /quicknotes/node_modules/react-dom/lib/reactProdInvariant.js
</span></span><span class="line"><span class="cl">./ReactPropTypesSecret : /quicknotes/node_modules/react-dom/lib/ReactPropTypesSecret.js
</span></span><span class="line"><span class="cl">react/lib/React : /quicknotes/node_modules/react/lib/React.js
</span></span><span class="line"><span class="cl">fbjs/lib/invariant : /quicknotes/node_modules/fbjs/lib/invariant.js
</span></span><span class="line"><span class="cl">fbjs/lib/warning : /quicknotes/node_modules/fbjs/lib/warning.js
</span></span></code></pre>
<div>It’s not an ideal presentation but you can figure out who ultimately imports a given JavaScript file by chasing chain of imports.</div>
<h2 class="hdr-with-anchor" id="things-i-learned"><div>Things I learned</div><a class="header-anchor" href="#things-i-learned">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>How does it help in practice? Here are 2 examples of how I reduced JavaScript bundle bloat by using those tools.</div>
<h2 class="hdr-with-anchor" id="bloated-highlight-js"><div>bloated highlight.js</div><a class="header-anchor" href="#bloated-highlight-js">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>In <a target="_blank" href="https://github.com/kjk/quicknotes">QuickNotes</a> I use <a target="_blank" href="https://highlightjs.org/">highlight.js</a> library to do syntax highlighting for code snippets.</div>
<div>Looking at output of source-map-explorer I noticed that highlight.js is 476 kB in size. That seemed excessive.</div>
<div>The problem was that while core of highlight.js is small, it supports 168 languages and doing <code>import 'highlight.js'</code> would bundle all of.</div>
<div>I only need to support small subset of most popular languages.</div>
<div>One way to fix it would be to use <a target="_blank" href="https://highlightjs.org/download/">https://highlightjs.org/download/</a> to generate a custom bundle. That would require repeating this manual step when I want to use the newer version.</div>
<div>I settled on a hacky but more automated solution.</div>
<div>Doing <code>import 'highlight.js'</code> loads <code>node_modules/highlight.js/index.js</code> which imports all languages.</div>
<div>I created a custom <code>index.js</code> that only imports the languages I want. Bbefore every compilation, I over-write <code>node_modules/highlight.js/index.js</code> with my custom version.</div>
<div>That way I can still use npm to manage the library and easily update to new version.</div>
<div>The result? Saved 416 kB.</div>
<h2 class="hdr-with-anchor" id="bloated-seedrandom-js"><div>bloated seedrandom.js</div><a class="header-anchor" href="#bloated-seedrandom-js">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>At <a target="_blank" href="https://wwww.folsomlabs.com/">work</a> we use tiny <a target="_blank" href="https://github.com/davidbau/seedrandom/blob/released/seedrandom.js">seendrandom.js</a> library.</div>
<div>When inspecting our JavaScript bundle I noticed suspicious libraries in it, like asn1 decoder.</div>
<div>I suspected our code doesn’t do asn1 decoding. Searching the codebase didn’t turn up any direct use.</div>
<div>I speculated that it’s imported indirectly by some other library.</div>
<div>I used my ad-hoc dependency tree dump to figure out that this code is imported from <code>seedrandom.js</code>.</div>
<div>This piece of code is a culprit:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="c1">// When in node.js, try using crypto package for autoseeding.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">nodecrypto</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'crypto'</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">ex</span><span class="p">)</span> <span class="p">{}</span>
</span></span></code></pre>
<div>Since node libraries are available during build step this line adds 294 kB of unneeded crypto code to our web app.</div>
<div>The fix was to fork the repo and remove those lines.</div>
<h2 class="hdr-with-anchor" id="automating-things"><div>Automating things</div><a class="header-anchor" href="#automating-things">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="header-anchor-widget-icon">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</a>
</h2>
<div>It’s handy to be able to re-run this analysis. Here’s a sample script <code>analyze_bundle.sh</code> I have in one of my projects:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -u -e -o pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># uses source-map-explorer (https://www.npmjs.com/package/source-map-explorer)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># to visualize what modules end up in final javascript bundle.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">install_sme<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="o">[</span> ! -f ./node_modules/.bin/source-map-explorer <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> npm install source-map-explorer
</span></span><span class="line"><span class="cl"> <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">analyze_prod<span class="o">()</span>
</span></span><span class="line"><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="cl"> rm -rf s/dist/*
</span></span><span class="line"><span class="cl"> install_sme
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> ./node_modules/.bin/gulp jsprod
</span></span><span class="line"><span class="cl"> ./node_modules/.bin/source-map-explorer s/dist/bundle.min.js s/dist/bundle.min.js.map
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">analyze_prod
</span></span></code></pre>
<div>The particulars will depend on your build system. The general idea is to run the build to generate <code>.js</code> and <code>.map.js</code> files and run <code>source-map-explorer</code> for analysis.</div>
</div>
Optimizing JavaScript by using arrays instead of objects2016-10-23T00:00:00Ztag:blog.kowalczyk.info,2016-10-23:/article/i/optimizing-javascript-by-using-arrays-instead-of-objects.html<div class="notion-page" id="i">
<div>Best optimizations are achievied by thinking about a problem holistically.</div>
<div>In this article I describe an optimization that uses arrays instead of classes while providing a class API for accessing data.</div>
<div>Imagine you’re building a web-based <a target="_blank" href="https://github.com/kjk/quicknotes">note taking application</a>.</div>
<div>It uses modern, single-page architecture. Front-end is written in React and backend provides JSON data to React.</div>
<div>The main view is a list of all notes of a given user. You need a backend api call that returns list of user’s notes. You survey how everyone else is implementing such API and you come up with the following: <code>/api/getnotes?user_id=<user_id></code> call which returns JSON response that looks like:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"notes"</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"id"</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"title"</span><span class="o">:</span> <span class="s2">"first note"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"createdAt"</span><span class="o">:</span> <span class="s2">"2016-08-14 15:34:32Z"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... more properties
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"id"</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"title"</span><span class="o">:</span> <span class="s2">"second note"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"createdAt"</span><span class="o">:</span> <span class="s2">"2016-08-14 16:03:12Z"</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... more properties
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... more notes
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>You notice there’s a lot of redundancy as we repeat property names in every note object.</div>
<div>In our case the structure of the note is fixed i.e. it always has the same properties. We can encode this data more efficiently:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="s2">"notes"</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="s2">"first note"</span><span class="p">,</span> <span class="s2">"2016-08-14 15:34:32Z"</span><span class="p">,</span> <span class="p">...</span> <span class="nx">more</span> <span class="nx">properties</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="s2">"second note"</span><span class="p">,</span> <span class="s2">"2016-08-14 16:03:12Z"</span><span class="p">,</span> <span class="p">...</span> <span class="nx">more</span> <span class="nx">properties</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... more notes
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>This is a holistic optimization that achieves several speedups at once:</div>
<ul>
<li>backend generates less text (JSON response)</li>
<li>backend compresses less data</li>
<li>browser downloads less data</li>
<li>browser decompresses less data</li>
<li>browser parsers less JSON text</li>
<li>JavaScript arrays are most likely more memory efficient that objects, so the data uses less memory</li>
</ul>
<div>If you know how gzip compression works you might protest that our effort to remove property names is mostly futile because gzip is very good at removing such redundancies.</div>
<div>I benchmarked <a target="_blank" href="https://github.com/kjk/quicknotes">QuickNotes</a> using my own notes and found that even after compression the size difference of two versions is ~50%. This might be a difference between a browser downloading 150 kB of data vs. 300 kB.</div>
<div>This representation comes at a cost of programmer’s convenience.</div>
<div>With objects we say <code>note.title</code>. With array representation it’s more work:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">noteIdIdx</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">noteTitleIdx</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1">// ... constants for more properties
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">note</span><span class="p">[</span><span class="nx">noteTitleIdx</span><span class="p">];</span>
</span></span></code></pre>
<div>This is not great. We can improve this by writing accessor functions:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">getTitle</span><span class="p">(</span><span class="nx">note</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">note</span><span class="p">[</span><span class="nx">noteTitleIdx</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
<div>JavaScript is a pliable language and we can get the best of both worlds: array representation with class API.</div>
<div>We create a class that derives from <code>Array</code> and extends it with accessor functions.</div>
<div>This example uses TypeScript, because static typing rocks, but will also work in pure JavaScript.</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kr">class</span> <span class="nx">Note</span> <span class="kr">extends</span> <span class="nb">Array</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">constructor</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">super</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">ID</span><span class="p">()</span><span class="o">:</span> <span class="kr">int</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="k">this</span><span class="p">[</span><span class="nx">noteIdIdx</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">Title</span><span class="p">()</span><span class="o">:</span> <span class="nx">string</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="k">this</span><span class="p">[</span><span class="nx">noteTitleIdx</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// .. more accessor functions
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// this "upgrades" rawArray object from being Array instance
</span></span></span><span class="line"><span class="cl"><span class="c1">// to Note instance by patching prototype chain.
</span></span></span><span class="line"><span class="cl"><span class="c1">// Beware: if rawAray is not Array instance, bad things will happen
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">toNote</span><span class="p">(</span><span class="nx">rawArray</span><span class="o">:</span> <span class="nx">any</span><span class="p">)</span><span class="o">:</span> <span class="nx">Note</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nb">Object</span><span class="p">.</span><span class="nx">setPrototypeOf</span><span class="p">(</span><span class="nx">rawArray</span><span class="p">,</span> <span class="nx">Note</span><span class="p">.</span><span class="nx">prototype</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">rawArray</span> <span class="nx">as</span> <span class="nx">Note</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// one way to convert raw array to object
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">rawNote</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="s2">"first note"</span><span class="p">,</span> <span class="p">...</span> <span class="nx">more</span> <span class="nx">properties</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">note</span> <span class="o">=</span> <span class="nx">toNote</span><span class="p">(</span><span class="nx">rawNote</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">note</span><span class="p">.</span><span class="nx">Title</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// a less efficient but also less hacky way is by constructing a new Note object
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">note</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Note</span><span class="p">(</span><span class="nx">rawNote</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">note</span><span class="p">.</span><span class="nx">Title</span><span class="p">();</span>
</span></span></code></pre>
<div>Class <code>Note</code> extends built-in JavaScript <code>Array</code> so it’s as efficient as an array and inherits all its functionality.</div>
<div>We add a couple of functions for getting/setting note data for a better API.</div>
<div>The magic happens in <code>toNote</code> function.</div>
<div><code>rawNote</code> is an instance of <code>Array</code> We could add our accessor functions directly to <code>Array.prototype</code> but that would make them available to all <code>Array</code> instances.</div>
<div>By defining a class <code>Note</code> that inherits from <code>Array</code>, <code>Note.prototype</code> inherits all of <code>Array.prototype</code> functions and gets our additional functions.</div>
<div>In order to convert a raw array to <code>Note</code> object we can either construct a new object from raw array or “upgrade” the object with <code>Object.setPrototypeOf(note, Note.prototype)</code>.</div>
<div>This is dangerous: if the object being upgraded is not an instance of <code>Array</code>, bad things will happen. It’s not a technique that should be overused.</div>
<div>Upgrading the object in place should be more efficient than creating a new object as it avoids an allocation.</div>
<div>On the other hand, <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf">according to MDN</a> changing prototype of an object makes for slowera access, so it can go either way.</div>
<div>To summarize:</div>
<ul>
<li>optimization is often achieved by looking at a problem as a whole</li>
<li>thanks to flexibility of JavaScript we can implement a micro-optimization where we represent objects as arrays but add convenient class-like API</li>
</ul>
</div>
Go package for better guid generation2015-02-12T00:00:00Ztag:blog.kowalczyk.info,2015-02-12:/article/e/go-package-for-better-guid-generation.html<div class="notion-page" id="e">
<div>The need to generate a globally unique identifier comes up often.</div>
<div>The way described in <a target="_blank" href="https://tools.ietf.org/html/rfc4122">RFC 4122</a> is popular but it can be done better.</div>
<div>I wrote <a target="_blank" href="https://github.com/kjk/betterguid">betterguid</a> Go package that does it better.</div>
<div>Unique id generated by this package:</div>
<ul>
<li>is a 20 character string, safe to include in urls (no need for escaping)</li>
<li>consist of 8 bytes of timestamp (millisecond precision) and 9 bytes of random data</li>
<li>sorts lexicographically</li>
<li>72-bits of random data ensures IDs won’t collide with IDs generated by other clients</li>
<li>are monotonically increasing even within the same timestamp</li>
</ul>
<div>You can read a <a target="_blank" href="https://www.firebase.com/blog/2015-02-11-firebase-unique-identifiers.html">longer description</a> of the algorithm.</div>
<div>My implementation is based on this <a target="_blank" href="https://gist.github.com/mikelehen/3596a30bd69384624c11">JavaScript code</a>.</div>
<div>Related: comparison of <a href="/article/JyRZ/generating-good-random-and-unique-ids-in-go.html">7 Go libraries</a> for unique id generation.</div>
</div>
Tip for per-test verbose logging in Go2014-12-03T00:00:00Ztag:blog.kowalczyk.info,2014-12-03:/article/c/tip-for-per-test-verbose-logging-in-go.html<div class="notion-page" id="c">
<div>One way to narrow down a problem when debugging a test is to add logging with e.g. <code>fmt.Printf()</code>.</div>
<div>The problem with this approach is lack of selectivity: imagine you have 100 tests and only 1 test fails. For debugging the issue you only need to see logs when executing that 1 test but you’ll drown in log output from all 100 tests.</div>
<div>My solution: control logging state with global variable <code>verboseLog</code> and allow toggling this flag per test.</div>
<div>Something like this:</div>
<pre tabindex="0" class="chroma"><code><span class="line"><span class="cl"><span class="kd">var</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="nx">verboseLog</span> <span class="p">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">myCodeWithBugs</span><span class="p">(</span><span class="nx">s</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">verboseLog</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">"s: %s\n"</span><span class="p">,</span> <span class="nx">s</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestMyCode</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">tests</span> <span class="p">=</span> <span class="p">[]</span><span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="o">...</span> <span class="nx">test</span> <span class="nx">fields</span>
</span></span><span class="line"><span class="cl"> <span class="nx">debug</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="cl"> <span class="p">}{</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="o">...</span><span class="p">,</span> <span class="kc">false</span> <span class="p">},</span> <span class="c1">// without verbose logging
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">{</span> <span class="o">...</span><span class="p">,</span> <span class="kc">true</span> <span class="p">},</span> <span class="c1">// with verbose logging
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">test</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">tests</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">verboseLog</span> <span class="p">=</span> <span class="nx">test</span><span class="p">.</span><span class="nx">debug</span>
</span></span><span class="line"><span class="cl"> <span class="o">...</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre>
</div>
SumatraPDF 3.0 released2014-10-29T00:00:00Ztag:blog.kowalczyk.info,2014-10-29:/article/b/sumatrapdf-3.0-released.html<div class="notion-page" id="b">
<div>We, <a target="_blank" href="http://www.ohloh.net/p/4623/contributors?sort=latest_commit&time_span=30+days">the SumatraPDF developers</a> have released a version 3.0 of Sumatra, a multi-format reader (PDF, epub and mobi ebooks, comic books, etc.) for Windows.</div>
<div>You can download it from <a target="_blank" href="https://www.sumatrapdfreader.org/free-pdf-reader.html">official SumatraPDF website</a></div>
<div>The biggest change in this version is addition of tabs, contributed by Stefan Stefanov.</div>
<div>If you don’t like tabs, you can go back to the old UI using Settings/Options… menu</div>
<div>We added support for table of contents and links in ebook UI.</div>
<div>We added support for PalmDoc ebooks.</div>
<div>Comic books now support CB7 and CBT format (in addition to CBZ and CBR).</div>
<div>We added support for LZMA and PPMd compression in CBZ comic books</div>
<div>You can now save Comic Book files as PDF.</div>
<div>We swapped keybindings:</div>
<ul>
<li>F11 : fullscreen mode (still also Ctrl+Shift+L)</li>
<li>F5 : presentation mode (also Shift+F11, still also Ctrl+L)</li>
</ul>
<div>We added a document measurement UI. Press ‘m’ to start. Keep pressing ‘m’ to change measurement units.</div>
<div>We added new <a target="_blank" href="https://www.sumatrapdfreader.org/settings.html">advanced settings</a>: FullPathInTitle, UseSysColors (no longer exposed through the Options dialog), UseTabs</div>
<div>We replaced non-free UnRAR with a free RAR extraction library. If some CBR files fail to open for you, download unrar.dll from <a target="_blank" href="http://www.rarlab.com/rar_add.htm">rar website</a> and place it alongside SumatraPDF.exe</div>
<div>We deprecated browser plugin. We don’t remove it if it was installed in previous version but both Chrome and FireFox are removing support for plugins so there’s no point in keeping it.</div>
<div>Finally, some of you really didn’t like the yellow background color. You’ve won: it’s now gray.</div>
</div>