<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Danny van Kooten</title>
	<atom:link href="https://www.dannyvankooten.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.dannyvankooten.com</link>
	<description>WordPress developer. Founder of ibericode.</description>
	<lastBuildDate>Mon, 01 Jun 2026 12:22:18 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://www.dannyvankooten.com/wp-content/uploads/2026/05/favicon-512-150x150.png</url>
	<title>Danny van Kooten</title>
	<link>https://www.dannyvankooten.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Attending WordCamp Europe 2026 in Krakow</title>
		<link>https://www.dannyvankooten.com/blog/2026/attending-wceu-2026-in-krakow/</link>
					<comments>https://www.dannyvankooten.com/blog/2026/attending-wceu-2026-in-krakow/#respond</comments>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Sat, 30 May 2026 09:55:03 +0000</pubDate>
				<category><![CDATA[Personal]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/?p=562</guid>

					<description><![CDATA[Next week, I&#8217;ll be attending WordCamp Europe again for the first time since 2014. I&#8217;m looking forward to reconnecting with people from the WordPress community and hopefully meeting some new faces too. I&#8217;m not going to fill my calendar with back-to-back meetings. I&#8217;ve never really enjoyed that approach to conferences, and some of the best [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Next week, I&#8217;ll be attending <a href="https://europe.wordcamp.org/2026/">WordCamp Europe</a> again for the first time <a href="https://www.dannyvankooten.com/blog/2014/2014-year-in-review/" data-type="post" data-id="84">since 2014</a>. I&#8217;m looking forward to reconnecting with people from the WordPress community and hopefully meeting some new faces too.</p>



<p class="wp-block-paragraph">I&#8217;m not going to fill my calendar with back-to-back meetings. I&#8217;ve never really enjoyed that approach to conferences, and some of the best conversations happen spontaneously.</p>



<p class="wp-block-paragraph">That said, if you&#8217;ll be in Krakow and would like to make sure we get a chance to chat, feel free to <a href="https://www.dannyvankooten.com/contact/" data-type="page" data-id="23">get in touch</a>.</p>



<h2 class="wp-block-heading">Hitting the trails on Sunday</h2>



<p class="wp-block-paragraph">After the event wraps up, I&#8217;ll be renting mountain bikes on Sunday to explore the trails west of Krakow.</p>



<p class="wp-block-paragraph">If you enjoy riding bikes, being outdoors, and occasionally dodging trees, you&#8217;re welcome to join. Nothing competitive. Just a fun ride and a good way to unwind after a few days of conference talks and conversations.</p>



<p class="wp-block-paragraph">If that sounds like your kind of thing, let me know via email, Mastodon, or Twitter.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dannyvankooten.com/blog/2026/attending-wceu-2026-in-krakow/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Back on WordPress</title>
		<link>https://www.dannyvankooten.com/blog/2026/back-on-wordpress/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Wed, 13 May 2026 19:42:40 +0000</pubDate>
				<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/?p=462</guid>

					<description><![CDATA[As of today, after a twelve-year journey across various static site generators, this site is once again powered by WordPress. In 2014 I started experimenting with content management systems beyond WordPress. First Ghost, then Jekyll, then Hugo, then 11ty, then Gozer (my own), then Astro. All of them were mostly great, but all had their [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">As of today, after a twelve-year journey across various static site generators, this site is once again powered by <a href="https://www.wordpress.org/">WordPress</a>.</p>



<p class="wp-block-paragraph">In 2014 I started experimenting with content management systems beyond WordPress. First Ghost, then Jekyll, then Hugo, then 11ty, then Gozer (my own), then Astro. All of them were mostly great, but all had their downsides as well. </p>



<p class="wp-block-paragraph">The reason I&#8217;m back on WordPress now is not technical. A static site comes with many benefits if you&#8217;re a developer: you get to edit posts in your favorite IDE, everything is in version control and hosting is free, simple and secure.</p>



<p class="wp-block-paragraph">Those benefits eventually turned into a downside for me. </p>



<h2 class="wp-block-heading">Writing versus coding</h2>



<p class="wp-block-paragraph">My IDE is for coding, not for writing. It is the same reason people say your bedroom should only be for sleeping: context matters. Every time I opened the site to write, my brain switched into engineering mode. Too often I would open up my site to write a new post, drift and suddenly find myself focusing on some technicality instead. Another redesign. Getting the build working. Moving stuff around so I could upgrade to the latest version.</p>



<h2 class="wp-block-heading">Static is really static</h2>



<p class="wp-block-paragraph">You can host a static site pretty much anywhere, often for free. It is just a collection of files, after all. There is no database, no runtime, and no dynamic processing. That is part of the appeal.</p>



<p class="wp-block-paragraph">It also has limits.</p>



<p class="wp-block-paragraph">Want to show off a list of your GitHub repositories? You can generate it at build time, but it will not update until the next time you build and deploy the site.</p>



<p class="wp-block-paragraph">Static sites are excellent when the content can be fully known at build time. Increasingly, I found myself wanting small dynamic pieces. Maybe a contact form, a comment section or some content generated from various API calls.<br><br>Of course there are ways around this. But every workaround would move the site a little further away from being a simple publishing tool, often requires a dependency on a third-party service and moves the site a little closer to being another software project.</p>



<h2 class="wp-block-heading">It&#8217;s the content that matters</h2>



<p class="wp-block-paragraph">While moving this site back to WordPress it occurred to me that I was finally looking at and updating blog posts written well over a decade ago. I had spent years rebuilding the site around them, but not nearly enough time tending to the content itself.<br><br>WordPress gives me a place that is clearly for managing content. Not building, not configuring, not upgrading a toolchain. Writing. That matters more to me now than whether the site is statically generated, server-rendered, or deployed from Git.</p>



<p class="wp-block-paragraph">I want this site to be boring again. WordPress is familiar, imperfect, flexible, occasionally annoying, and very good at being a place where words can live for a long time. Right now, that is exactly what I want.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Using phpactor as a language server for WordPress development</title>
		<link>https://www.dannyvankooten.com/blog/2026/using-phpactor-with-wordpress/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2026/using-phpactor-with-wordpress/</guid>

					<description><![CDATA[Set up phpactor for WordPress development by indexing WordPress core files or using WordPress stubs while keeping diagnostics focused on your own code.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">This post shows how to use <a href="https://phpactor.readthedocs.io/en/master/">phpactor</a> as a language server for WordPress development. Phpactor is a strong tool for PHP development, and it works with editors and IDEs that support the Language Server Protocol (LSP).</p>



<p class="wp-block-paragraph">Phpactor works well with modern PHP code when the project either follows <a href="https://www.php-fig.org/psr/psr-4/">PSR-4</a> or uses <a href="https://getcomposer.org/">Composer</a> for autoloading. WordPress does not use either approach, so phpactor can have trouble discovering symbols from WordPress core by default.</p>



<h2 class="wp-block-heading">Configuring phpactor to include WordPress core files</h2>



<p class="wp-block-paragraph">You can solve this by setting the <a href="https://phpactor.readthedocs.io/en/master/reference/configuration.html#indexer-include-patterns">indexer.include_patterns</a> configuration option to include WordPress core files. Add the following to your <code>.phpactor.json</code> configuration file:</p>



<pre class="wp-block-code"><code class="language-json">{
    "indexer.include_patterns": [
        "wp-includes/**/*.php",
        "wp-admin/**/*.php",
        "wp-content/plugins/**/*.php",
        "wp-content/themes/**/*.php"
    ]
}</code></pre>



<p class="wp-block-paragraph">I also exclude WordPress core and third-party plugins from phpactor&#8217;s diagnostics through the <a href="https://phpactor.readthedocs.io/en/master/reference/configuration.html#language-server-diagnostic-exclude-paths">language_server.diagnostic_exclude_paths</a> configuration setting.</p>



<pre class="wp-block-code"><code class="language-json">{
  "language_server.diagnostic_exclude_paths": [
    "wp-includes/**/*",
    "wp-admin/**/*",
    "wp-content/plugins/woocommerce/**/*"
  ]
}</code></pre>



<p class="wp-block-paragraph">With these two configuration options in place, phpactor can discover symbols from WordPress core and provide accurate diagnostics for your own code while ignoring issues in WordPress core or third-party plugins.</p>



<h2 class="wp-block-heading">Using WordPress stubs</h2>



<p class="wp-block-paragraph">Alternatively, you can use <a href="https://phpactor.readthedocs.io/en/master/reference/stubs.html">stubs</a> to give phpactor the information it needs about WordPress core functions and classes.</p>



<pre class="wp-block-code"><code class="language-sh">composer require --dev php-stubs/wordpress-stubs</code></pre>



<p class="wp-block-paragraph">This installs the <a href="https://github.com/php-stubs/wordpress-stubs">php-stubs/wordpress-stubs</a> package. You can then configure phpactor to use these stubs by adding the following to your <code>.phpactor.json</code> configuration file:</p>



<pre class="wp-block-code"><code class="language-json">{
    "worse_reflection.additive_stubs": [
        "vendor/php-stubs/wordpress-stubs/wordpress-stubs.php"
    ]
}</code></pre>



<p class="wp-block-paragraph">Both approaches make phpactor more useful in WordPress projects. Indexing core files gives phpactor more project context, while stubs provide a lightweight way to expose WordPress functions and classes to the language server.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Pensioenbeleggen: optimaal jaarruimte benutten?</title>
		<link>https://www.dannyvankooten.com/blog/2025/pensioenbeleggen-hoeveel-jaarruimte-benutten/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Tue, 07 Oct 2025 00:00:00 +0000</pubDate>
				<category><![CDATA[Personal]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2025/pensioenbeleggen-hoeveel-jaarruimte-benutten/</guid>

					<description><![CDATA[Hoeveel jaarruimte benutten voor pensioenbeleggen als zelfstandige? Een methode om tot een redelijk bedrag te komen.]]></description>
										<content:encoded><![CDATA[
<style>
    h3 { font-size: 1.1rem; }
    h2 { font-size: 1.2rem; }
</style>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">This post is in Dutch because it is specific to the Dutch retirement system.</p>
</blockquote>



<div style="background: #2a2f34; padding: 1rem;">
<h3 style="margin-top: 0;">Inhoudsopgave</h3>
<a href="#wat-is-jaarruimte">Wat is jaarruimte?</a><br/>
<a href="#hoe-benut-je-jaarruimte-optimaal">Hoe benut je je jaarruimte optimaal?</a><br/>
<a href="#math">Hoeveel moet je dan inleggen?</a><br/>
<a href="#calculator">Calculator: bepalen periodieke inleg</a>
</div>



<p class="wp-block-paragraph">Als zelfstandige kan het lastig zijn om te bepalen hoeveel je opzij moet zetten om later voldoende pensioen op te bouwen. Een veelgehoorde vuistregel is <strong>5 tot 15% van je bruto inkomen</strong>. Maar als je inkomen jaarlijks sterk varieert, of als je pas later begint met pensioenbeleggen, zegt die vuistregel weinig.</p>



<p class="wp-block-paragraph">Dan blijft de vraag hoeveel jaarruimte je jaarlijks wilt benutten. Dit artikel is geen financieel advies, maar een praktische rekenmethode om tot een redelijk bedrag te komen.</p>



<h2 class="wp-block-heading" id="wat-is-jaarruimte">Wat is jaarruimte?</h2>



<p class="wp-block-paragraph">Jaarruimte is een fiscaal vriendelijke manier om vermogen op te bouwen dat je na je pensioendatum periodiek kunt laten uitkeren. In essentie gebruik je het om inkomen van nu naar na je pensioendatum te verplaatsen. Dat heeft zowel voor- als nadelen:</p>



<h3 class="wp-block-heading">Voordelen van jaarruimte</h3>



<ul class="wp-block-list"><li>Het geld dat je op een geblokkeerde pensioenrekening stort, kun je aftrekken van je belastbaar inkomen in box 1. Na je pensioendatum betaal je hier alsnog belasting over, maar waarschijnlijk tegen een lager tarief (momenteel 17,92% vs. 35,82% t/m €38.441).</li><li>Je bespaart mogelijk jarenlang op box 3-belasting.</li></ul>



<h3 class="wp-block-heading">Nadelen van jaarruimte</h3>



<ul class="wp-block-list"><li>Het geld staat vast tot ongeveer je AOW-datum, tenzij je bereid bent revisierente te betalen.</li><li>Je moet met het geld dat vrijkomt een periodieke uitkering aankopen.</li><li>Belastingregels kunnen in de tussentijd veranderen.</li></ul>



<h2 class="wp-block-heading" id="hoe-benut-je-jaarruimte-optimaal">Hoe benut je je jaarruimte optimaal?</h2>



<p class="wp-block-paragraph">Daar is geen eenduidig antwoord op te geven. Veel hangt af van je persoonlijke omstandigheden.</p>



<p class="wp-block-paragraph">De grootste voordelen van jaarruimte benutten zijn het verplaatsen van inkomen naar een hopelijk lager belastingtarief en het besparen op box 3-belasting wanneer je boven het heffingsvrij vermogen zit.</p>



<p class="wp-block-paragraph">Met de huidige belastingregels betaal je na je AOW-datum 17,92% box 1-belasting in de eerste schijf. Daarom is het meestal verstandig om in elk geval pensioeninkomen op te bouwen tot maximaal €38.441. Dat bedrag is inclusief AOW en eventuele andere inkomsten.</p>



<p class="wp-block-paragraph">Bij een pensioeninkomen boven €38.441 zijn de belastingvoordelen beperkter. Dan spelen je persoonlijke omstandigheden een grotere rol, zoals in welke box 1-schijf je huidige inkomen valt, of je boven het heffingsvrij vermogen in box 3 zit en hoeveel waarde je hecht aan flexibiliteit.</p>



<h2 class="wp-block-heading" id="math">Hoeveel moet je dan inleggen?</h2>



<p class="wp-block-paragraph">Concreet wil je weten hoeveel pensioeninkomen je ongeveer nodig hebt, welk eindbedrag daarbij hoort en hoeveel je tot je pensioendatum jaarlijks moet inleggen om op dat bedrag uit te komen.</p>



<p class="wp-block-paragraph">We kunnen een conservatieve schatting van het benodigde eindkapitaal <var>FV</var> berekenen door de looptijd te vermenigvuldigen met het deel van het inkomen waar we zelf voor moeten zorgen:</p>



<math display="block">
<mrow>
<mi>FV</mi>
<mo>=</mo>
<mi>looptijd</mi>
<mo>⋅</mo>
<mo>(</mo>
<mi>gewenst inkomen</mi>
<mo>&#8211;</mo>
<mi>AOW</mi>
<mo>)</mo>
</mrow>
</math>



<p class="wp-block-paragraph">Vervolgens berekenen we de benodigde maandelijkse inleg als een annuïteit. Daarbij is de looptijd <var>n</var> gelijk aan het aantal jaren tot je pensioendatum, gebruiken we de huidige waarde <var>PV</var> van je pensioenpot als startpunt en nemen we het verwachte jaarlijkse rendement op je beleggingen <var>r</var> als rente.</p>



<math display="block" xmlns="http://www.w3.org/1998/Math/MathML">
<mrow>
<mi>PMT</mi>
<mo>=</mo>
<mo>&#8211;</mo>
<mfrac>
<mrow>
<mi>r</mi>
<mo>×</mo>
<mfenced>
<mrow>
<mi>PV</mi>
<mo>⁢</mo>
<msup>
<mrow>
<mo>(</mo>
<mrow>
<mn>1</mn>
<mo>+</mo>
<mi>r</mi>
</mrow>
<mo>)</mo>
</mrow>
<mi>n</mi>
</msup>
<mo>+</mo>
<mi>FV</mi>
</mrow>
</mfenced>
</mrow>
<mrow>
<msup>
<mrow>
<mo>(</mo>
<mrow>
<mn>1</mn>
<mo>+</mo>
<mi>r</mi>
</mrow>
<mo>)</mo>
</mrow>
<mi>n</mi>
</msup>
<mo>&#8211;</mo>
<mn>1</mn>
</mrow>
</mfrac>
</mrow>
</math>



<p class="wp-block-paragraph">Je kunt dit uitrekenen met een rekenmachine of spreadsheet en de <code>PMT</code>-functie gebruiken, maar de tool hieronder maakt het eenvoudiger.</p>



<p class="wp-block-paragraph">Zo zie je snel of je op schema zit, extra moet inleggen of mogelijk al meer spaart dan nodig is.</p>



<h2 class="wp-block-heading">Berekenen maandelijkse inleg pensioenrekening</h2>



<style>
#calculator {
    padding: 1rem;
    background: #2a2f34;
}
#calculator label { line-height:38px; }
#calculator .row {
    margin-bottom: 1.5rem;
}
@media (min-width: 720px) {
    #calculator .row {
        display: flex;
        flex-flow: row wrap;
        margin-bottom: 1rem;
    }
    #calculator .col {
        width: 66.6666%;
    }
    #calculator .col-1 {
        width: 33.333%;
    }
}
#calculator input {
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    display: inline-block;
    width: 240px;
    padding: .375rem .75rem;
    font-size: 1rem;
    font-weight: 400;
    line-height: 1.5;
    color: #dee2e6;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-color: #212529;
    background-clip: padding-box;
    border: 1px solid #495057;
    border-radius: 0.375rem;
    transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
    margin-right: 0.5rem;
}
#calculator .help {
    font-size: 14px;
    color: #AAA;
    margin-top: 0.5rem;
}
</style>



<form id="calculator">
<div class="row">
<div class="col col-1"><label for="date-aow">AOW datum</label></div>
<div class="col">
<input id="date-aow" name="date-aow" type="date" value="2060-01-01"/>
<div class="help"><a href="https://www.rijksoverheid.nl/onderwerpen/algemene-ouderdomswet-aow/vraag-en-antwoord/wanneer-gaat-mijn-aow-in">Wanneer gaat mijn AOW in? (rijksoverheid.nl)</a></div>
</div>
</div>
<div class="row">
<div class="col col-1"><label for="retirement-income">Gewenst bruto inkomen <sup>1</sup></label></div>
<div class="col">
<input id="retirement-income" min="0" name="retirement-income" step="100" type="number" value="38441"/> €
            <div class="help">
                Zie ook <a href="https://www.belastingdienst.nl/wps/wcm/connect/bldcontentnl/belastingdienst/prive/inkomstenbelasting/heffingskortingen_boxen_tarieven/boxen_en_tarieven/box_1/box_1">Box 1: belastbaar inkomen uit werk en woning (belastingdienst.nl)</a>
</div>
</div>
</div>
<div class="row">
<div class="col col-1"><label for="aow-income">Verwacht inkomen uit AOW</label></div>
<div class="col">
<input id="aow-income" min="0" name="aow-income" step="100" type="number" value="14100"/> €
            <div class="help">
                Hoeveel inkomen verwacht je jaarlijks vanuit AOW en andere pensioenvoorzieningen? Zie <a href="https://www.mijnpensioenoverzicht.nl/nl">mijnpensioenoverzicht.nl</a>
</div>
</div>
</div>
<div class="row">
<div class="col col-1"><label for="present-value">Huidige waarde</label></div>
<div class="col">
<input id="present-value" min="0" name="present-value" type="number" value="50000"/> €
            <div class="help">
                Wat is de huidige waarde van je pensioenbeleggen rekening(en)?
            </div>
</div>
</div>
<div class="row">
<div class="col col-1"><label for="duration">Gewenste looptijd</label></div>
<div class="col">
<input id="duration" max="30" min="5" name="duration" type="number" value="20"/> jaar
            <div class="help">
                Voor hoeveel jaar wil je een inkomen kopen?<br/>
                Bij de meeste aanbieders van lijfrente producten geldt hier een minimum van 5 en een maximum van 20 jaar.
            </div>
</div>
</div>
<div class="row">
<div class="col col-1"><label for="expected-real-return">Verwacht rendement</label></div>
<div class="col">
<input id="expected-real-return" min="0.0" name="expected-real-return" step="0.5" type="number" value="4.0"/>%
            <div class="help">
                Je verwachte rendement op jaarbasis na inflatie. 4% is een degelijk lange termijn gemiddelde voor een wereldwijde mix van 60% aandelen en 40% obligaties.
            </div>
</div>
</div>
<div class="row" style="margin-bottom:0;">
<div class="col col-1"></div>
<div class="col">
            € <span id="result" style="font-weight:bold; font-size: 2rem;"></span> inleg per maand
        </div>
</div>
</form>



<div class="text-muted">
<p><sup>1</sup> Het belastingvoordeel voor het benutten van jaarruimte boven een pensioeninkomen van €38.441 (de eerste schijf in box 1) is beperkt. Het kan voordeliger en bovendien flexibeler zijn om daarboven in box 3 of een spaar-BV te beleggen voor je pensioen.</p>
</div>



<p class="text-muted wp-block-paragraph">De informatie op deze site is mijn persoonlijke mening, geen financieel advies. Je blijft zelf verantwoordelijk voor je eigen keuzes.</p>



<script>
function pmt(ir, np, pv, fv, type) {
    /*
     * ir   - interest rate per month
     * np   - number of periods (months)
     * pv   - present value
     * fv   - future value
     * type - when the payments are due:
     *        0: end of the period, e.g. end of month (default)
     *        1: beginning of period
     */
    var pmt, pvif;

    fv || (fv = 0);
    type || (type = 0);

    if (ir === 0) {
        return -(pv + fv)/np;
    }

    pvif = Math.pow(1 + ir, np);
    pmt = - ir * (pv * pvif + fv) / (pvif - 1);

    if (type === 1) {
        pmt /= (1 + ir);
    }

    return pmt;
}

function getMonthDifference(startDate, endDate) {
  return (
    endDate.getMonth() -
    startDate.getMonth() +
    12 * (endDate.getFullYear() - startDate.getFullYear())
  );
}

function runCalculator() {
    let presentValue = parseInt(form.elements.namedItem('present-value').value);
    let expectedRealReturn = parseFloat(form.elements.namedItem('expected-real-return').value) / 100.0;
    let duration = parseInt(form.elements.namedItem('duration').value);
    let targetDate = new Date(form.elements.namedItem('date-aow').value);
    let retirementIncome = parseInt(form.elements.namedItem('retirement-income').value);
    let aowIncome = parseInt(form.elements.namedItem('aow-income').value);
    let monthsRemaining = getMonthDifference(new Date(), targetDate);
    let futureValue = (retirementIncome - aowIncome) * duration;
    let monthlyContribution = -pmt(expectedRealReturn/12.0, monthsRemaining, -presentValue, futureValue);
    document.querySelector('#result').textContent = Math.ceil(monthlyContribution / 10) * 10;
}

const form = document.querySelector('#calculator');
form.addEventListener('input', runCalculator);
form.addEventListener('change', runCalculator);
runCalculator();
</script>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Using Composer with a specific PHP version on Alpine Linux</title>
		<link>https://www.dannyvankooten.com/blog/2025/composer-php-version-alpine-linux/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Wed, 10 Sep 2025 00:00:00 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2025/composer-php-version-alpine-linux/</guid>

					<description><![CDATA[Fixing Composer using the wrong PHP version on Alpine Linux due to a hardcoded shebang in the composer package.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">While updating a few PHP applications to PHP 8.4, I ran into a Composer issue on Alpine Linux: Composer was using a different PHP binary than the one resolved by <code>/usr/bin/env php</code>.</p>



<p class="wp-block-paragraph">If your <code>composer.json</code> requires a specific PHP version, this can result in an error like this even when the required PHP version is installed:</p>



<pre class="wp-block-code"><code class="language-txt">- Root composer.json requires php &gt;=8.4 but your php version (8.3.24) does not satisfy that requirement.</code></pre>



<p class="wp-block-paragraph">The issue is that the <a href="https://pkgs.alpinelinux.org/package/edge/community/x86_64/composer">composer</a> package in the Alpine Linux Package Repository depends on a hard-coded PHP version. The <a href="https://gitlab.alpinelinux.org/alpine/aports/-/blob/f2f1500af08c9eec5be81ba7856671120b05a655/community/composer/APKBUILD#L53">APKBUILD file</a> creates <code>/usr/bin/composer</code> as a shell wrapper that calls that specific PHP binary instead of the one from <code>/usr/bin/env php</code>.</p>



<pre class="wp-block-code"><code class="language-sh">$ head /usr/bin/composer
#!/bin/sh

/usr/bin/php83 /usr/bin/composer.phar "$@"</code></pre>



<p class="wp-block-paragraph">You can confirm which PHP binary Composer is using by running <code>composer --version</code>. It prints both the Composer version and the PHP version behind it:</p>



<pre class="wp-block-code"><code class="language-sh">$ composer --version
Composer version 2.8.10 2025-07-10 19:08:33
PHP version 8.3.24 (/usr/bin/php83)</code></pre>



<h2 class="wp-block-heading">How to fix Composer to your desired PHP version</h2>



<p class="wp-block-paragraph">Once you know Composer is using the wrong PHP binary, the fix is straightforward. You can either adjust the <code>/usr/bin/composer</code> wrapper or skip the Alpine package and install Composer manually.</p>



<h3 class="wp-block-heading">Point the wrapper at your desired PHP version</h3>



<p class="wp-block-paragraph">One option is to use <a href="https://linux.die.net/man/1/sed">sed</a> to change the PHP binary used by <code>/usr/bin/composer</code>, so it runs whatever <code>/usr/bin/env php</code> resolves to.</p>



<pre class="wp-block-code"><code class="language-sh">$ sudo sed -i 's/php83/env php/g' /usr/bin/composer
$ composer --version
Composer version 2.8.10 2025-07-10 19:08:33
PHP version 8.4.11 (/usr/bin/php)</code></pre>



<h3 class="wp-block-heading">Install Composer manually</h3>



<p class="wp-block-paragraph">Another option is to skip the package from the Alpine Package Repository and install Composer yourself using the <a href="https://getcomposer.org/download/">official installation instructions</a>.</p>



<pre class="wp-block-code"><code>php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6') { echo 'Installer verified'.PHP_EOL; } else { echo 'Installer corrupt'.PHP_EOL; unlink('composer-setup.php'); exit(1); }"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/bin/composer</code></pre>



<p class="wp-block-paragraph">After that, <code>composer --version</code> should show Composer running with the expected PHP version:</p>



<pre class="wp-block-code"><code class="language-sh">$ composer --version
Composer version 2.8.11 2025-08-21 11:29:39
PHP version 8.4.11 (/usr/bin/php)</code></pre>



<p class="wp-block-paragraph">With Composer using the correct PHP binary, Alpine Linux builds can use the intended PHP version without failing on a platform requirement mismatch.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Benchmarking listing files in a local directory in PHP</title>
		<link>https://www.dannyvankooten.com/blog/2025/benchmarking-scandir-glob-directoryiterator-readdir-php/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2025/benchmarking-scandir-glob-directoryiterator-readdir-php/</guid>

					<description><![CDATA[Benchmarking scandir, readdir, glob and DirectoryIterator for listing files in a directory in PHP.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I benchmarked the common ways to list files in a local directory in PHP.</p>



<p class="wp-block-paragraph">The methods I tested were <code>scandir</code>, <code>readdir</code>, <code>glob</code>, and <code>DirectoryIterator</code>.</p>



<p class="wp-block-paragraph">As expected, runtime grows linearly with the number of directory entries.</p>



<p class="wp-block-paragraph">Sorting accounts for a noticeable share of that runtime. <code>scandir("", SCANDIR_SORT_NONE)</code> and <code>glob("*", GLOB_NOSORT")</code> are both at least 10-30% faster than their sorted versions, which are the default.</p>



<p class="wp-block-paragraph"><code>scandir("", SCANDIR_SORT_NONE)</code> is consistently the fastest option, with <code>readdir</code> usually coming in second.</p>



<p class="wp-block-paragraph"><code>DirectoryIterator</code> is the slowest option for directories with fewer than 2000 files. Its relative performance improves as the directory grows, until it is about as fast as <code>readdir</code> and <code>scandir</code> for directories with 10000 files.</p>



<p class="wp-block-paragraph">Below are the raw benchmark results and the code I used. Times are in <code>ms</code> and calculated as the average across 100 iterations.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.dannyvankooten.com/wp-content/uploads/2025/php-directory-listing-benchmark.svg" alt=""/></figure>



<h3 class="wp-block-heading">Benchmark results</h3>



<figure class="wp-block-table"><table><thead><tr><th>files</th><th>DirectoryIterator</th><th>readdir</th><th>scandir</th><th>scandir unsorted</th><th>glob</th><th>glob unsorted</th></tr></thead><tbody><tr><td>1</td><td>0.008</td><td>0.006</td><td>0.006</td><td>0.006</td><td>0.006</td><td>0.005</td></tr><tr><td>10</td><td>0.013</td><td>0.009</td><td>0.009</td><td>0.008</td><td>0.010</td><td>0.009</td></tr><tr><td>100</td><td>0.071</td><td>0.041</td><td>0.046</td><td>0.039</td><td>0.056</td><td>0.049</td></tr><tr><td>1000</td><td>0.683</td><td>0.376</td><td>0.477</td><td>0.357</td><td>0.568</td><td>0.449</td></tr><tr><td>10000</td><td>3.994</td><td>3.872</td><td>5.248</td><td>3.612</td><td>6.277</td><td>4.472</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Benchmark code</h3>



<pre class="wp-block-code"><code class="language-php">&lt;?php
namespace D;
class DirectoryReader
{
    public function __construct(
        protected string $directory,
    ) {
    }
    public function iterator($flags = 0)
    {
        $results = array();
        foreach (new \DirectoryIterator($this-&gt;directory) as $fileInfo) {
            $results[] = $fileInfo;
        }
        return $results;
    }
    public function readdir($flags = 0)
    {
        $results = array();
        if ($handle = opendir($this-&gt;directory)) {
            while (false !== ($filename = readdir($handle))) {
                $results[] = $filename;
            }
            closedir($handle);
        }
        return $results;
    }
    public function scandir($flags = 0)
    {
        return scandir($this-&gt;directory, $flags);
    }
    public function glob($flags = 0)
    {
        return glob($this-&gt;directory . '/*', $flags);
    }
}
if (empty($argv[1])) {
    echo "Usage: php " . basename(__FILE__) . " &lt;directory&gt;\n";
    exit(1);
}
$directory = $argv[1];
if (is_dir($directory)) {
    echo "Directory exists already\n";
    exit(1);
}
mkdir($directory, 0755);
$reader = new DirectoryReader($directory);
$n_iterations = 100;
$step_size = 10;
$tests = [
    ['iterator', 0],
    ['readdir', 0],
    ['scandir', 0],
    ['scandir', SCANDIR_SORT_NONE],
    ['glob', 0],
    ['glob', GLOB_NOSORT],
];
echo "files\t" . join("\t", array_map(function ($t) {
    $name = $t[0];
    if ($t[1]) {
        $name .= ' unsorted';
    }
    return $name;
}, $tests)) . "\n";
for ($n = 1; $n &lt;= 10000; $n *= 10) {
    for ($f = (int) ($n / 10); $f &lt;= $n; $f++) {
        touch("{$directory}/{$f}.txt");
    }
    $line = "{$n}\t";
    foreach ($tests as [$method, $flags]) {
        $start = microtime(true);
        for ($i = 0; $i &lt; $n_iterations; $i++) {
            $reader-&gt;{$method}($flags);
        }
        $end = microtime(true);
        $time_per_it = sprintf('%.3f', ($end - $start) / $n_iterations * 1000);
        $line .= "$time_per_it\t";
    }
    $line .= "\n";
    echo $line;
}</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Building self-hosted and privacy-friendly website analytics. Again.</title>
		<link>https://www.dannyvankooten.com/blog/2025/building-privacy-friendly-website-analytics/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Thu, 16 Jan 2025 00:00:00 +0000</pubDate>
				<category><![CDATA[Web Performance]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2025/building-privacy-friendly-website-analytics/</guid>

					<description><![CDATA[Building a standalone version of Koko Analytics for non-WordPress sites, using PHP with MySQL, PostgreSQL or SQLite.]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">This post is about a standalone version of Koko Analytics. It is currently functional and we are using it internally, but we chose not to open-source it for now.</p>
</blockquote>



<p class="wp-block-paragraph">The year is 2025 and I once again find myself building an open-source, self-hostable, privacy-friendly website analytics product.</p>



<p class="wp-block-paragraph">By now I am starting to worry if I will ever be able to leave this topic alone. I mean, it was quite the rage <a href="/blog/2018/reviving-ana-as-fathom/">back in 2018</a> but surely there are more than enough good solutions available now, well over 6 years later?</p>



<p class="wp-block-paragraph">The one thing I have going for me is that I am not actually starting from scratch. Instead, I am porting over <a href="https://www.kokoanalytics.com/">Koko Analytics</a> to a standalone version in modern PHP.</p>



<p class="wp-block-paragraph">The WordPress plugin version is currently in active use on more than 50.000 sites according to <a href="https://wordpress.org/plugins/koko-analytics/advanced/">the statistics on WordPress.org</a>. Over the years, I have received several requests for a standalone version for being able to track non-WordPress sites. Having been there when I was initially involved with Fathom, I never really wanted to go there again.</p>



<p class="wp-block-paragraph">However, a couple of weeks ago I found myself itching to get it done, rolled up my sleeves and started the work.</p>



<h2 class="wp-block-heading">Introducing Koko Analytics Standalone</h2>



<p class="wp-block-paragraph">I haven&#8217;t really decided whether I want to recycle the Koko Analytics brand name or name it differently, but let&#8217;s just roll with what we got for now.</p>



<p class="wp-block-paragraph">You can follow the project on GitHub here: <a href="https://github.com/koko-analytics/koko-analytics">github.com/koko-analytics/koko-analytics</a>.</p>



<h3 class="wp-block-heading">Project goals</h3>



<p class="wp-block-paragraph">Goals for the project are to have a PHP based solution that uses a minimal amount of resources and can run on a wide range of hosting options.</p>



<p class="wp-block-paragraph">You can use either MySQL, PostgreSQL or SQLite as the database backend. You may wonder how this will possibly scale or why we&#8217;re not choosing Clickhouse or DuckDB for the database. The answer is that Koko Analytics doesn&#8217;t want to force you on certain hardware or spinning up a whole new database just for some metrics. By making some choices, we can make a traditional OLTP database perform really well:</p>



<ul class="wp-block-list">
<li>Daily granularity.</li>



<li>Writing data to an temporary buffer file, then periodically aggregating this and only then persisting to the database (in bulk).</li>



<li>Limiting the amount of metrics that Koko Analytics tracks: we think visitors, pageviews, referral traffic and custom events are most important for our target audience.</li>
</ul>



<h3 class="wp-block-heading">Open-source licensing</h3>



<p class="wp-block-paragraph">The application itself is licensed under the AGPL license.</p>



<p class="wp-block-paragraph">To avoid issues with AGPL virality, the tracking snippet (which you include on the sites you would like to track and allows the project to track pages served from cache) is MIT licensed.</p>



<p class="wp-block-paragraph">This mirrors the licensing used by established alternatives like <a href="https://matomo.org/licences/">Matomo</a> and <a href="https://plausible.io/blog/open-source-licenses">Plausible</a>.</p>



<h3 class="wp-block-heading">Where we at?</h3>



<p class="wp-block-paragraph">Koko Analytics Standalone is functional already, but I still expect some things to change over the next few months.</p>



<p class="wp-block-paragraph">We are about to start dogfooding it and hope to release a stable version some time during the second or third quarter of 2025.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating a Doctrine database from SQLite to MySQL</title>
		<link>https://www.dannyvankooten.com/blog/2024/migrating-doctrine-database-sqlite-to-mysql/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Sat, 22 Jun 2024 00:00:00 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2024/migrating-doctrine-database-sqlite-to-mysql/</guid>

					<description><![CDATA[A step-by-step guide to migrating a Doctrine database from SQLite to MySQL in a Symfony application.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Recently I had to migrate a production database from SQLite[^1] to MySQL for a Symfony application using Doctrine. I assumed this would be as easy as running a single command, but it turned out to be slightly more involved. Not hard by any means, but I did have to figure out a few things and piece together a few pieces of the puzzle.</p>



<p class="wp-block-paragraph">The first few searches led me to SQLite&#8217;s <code>.dump</code> command<a href="https://sqlite.org/cli.html#converting_an_entire_database_to_a_text_file">^2</a>, which dumps the entire database (including table structure and indices) to a text file. This approach requires you to manually edit out the differences between SQLite and MySQL in SQL syntax, but this seemed a bit tedious to me. Surely there must be a better way, especially when using Doctrine?</p>



<p class="wp-block-paragraph">In the end I came up with a reasonably short bash script which successfully creates the MySQL database structure and copies over all data from the SQLite database with minimal downtime. In this post I&#8217;ll walk you through the script, so that you can hopefully modify and use it for your own needs.</p>



<h3 class="wp-block-heading">Setting up the bash script</h3>



<p class="wp-block-paragraph">First we&#8217;ll create a new file, make sure it&#8217;s executed using bash and set a few flags.</p>



<pre class="wp-block-code"><code>#!/usr/bin env bash

set -e
set -o pipefail</code></pre>



<ul class="wp-block-list"><li><code>set -e</code>: The set -e option instructs bash to immediately exit if any command has a non-zero exit status.</li><li><code>set -o pipefail</code>: This setting prevents errors in a pipeline from being masked by later successful commands. If any command in a pipeline fails, that return code will be used as the return code of the whole pipeline.</li></ul>



<h3 class="wp-block-heading">Connecting to the MySQL server</h3>



<p class="wp-block-paragraph">We want our application to keep using the SQLite database until we run this script. So we start out by writing our updated database connection string to the <code>.env.local</code> file.</p>



<pre class="wp-block-code"><code>echo 'DATABASE_URL="mysql://user:password@host/database_name"' &gt;&gt; .env.local</code></pre>



<p class="wp-block-paragraph">Note that your <code>DATABASE_URL</code> may require a <code>serverVersion</code> and <code>charset</code> argument<a href="https://symfony.com/doc/6.4/doctrine.html#configuring-the-database">^4</a>, but I left that out for simplicity reasons.</p>



<h3 class="wp-block-heading">Creating the new database</h3>



<p class="wp-block-paragraph">We can use the commands from the Doctrine Symfony bundle to create our (empty) database. This also ensures our database connection string is working before we proceed.</p>



<pre class="wp-block-code"><code class="language-sh">echo "-- Setting up new MySQL database"
bin/console doctrine:database:create</code></pre>



<h3 class="wp-block-heading">Creating database tables and indices</h3>



<p class="wp-block-paragraph">Now, instead of converting our SQLite database structure to MySQL we can simply have Doctrine inspect our current PHP models (&#8220;entities&#8221;) and create the schema from that.</p>



<pre class="wp-block-code"><code class="language-sh">echo "-- Creating table structure"
bin/console doctrine:schema:update --force</code></pre>



<p class="wp-block-paragraph">We should now have our database in MySQL with a bunch of empty tables in it. Let&#8217;s make sure by running <code>doctrine:schema:validate</code>.</p>



<pre class="wp-block-code"><code class="language-sh">echo "-- Validating schema"
bin/console doctrine:schema:validate</code></pre>



<p class="wp-block-paragraph">A nice side effect of creating the database structure from the current PHP models is that we can remove any previously used Doctrine migrations after we&#8217;re done migrating from SQLite to MySQL, effectively starting from a clean slate.</p>



<h3 class="wp-block-heading">Migrating the data</h3>



<p class="wp-block-paragraph">OK, now the important part. Copying over all data from SQLite to our new MySQL database.</p>



<p class="wp-block-paragraph">SQLite has an option for changing output formats<a href="https://sqlite.org/cli.html#changing_output_formats">^3</a>, with one of them being <code>insert</code> mode. This output mode will output the textual result of any <code>SELECT</code> queries you do as <code>INSERT</code> queries. Insert mode can be used to generate text that can later be used to input data into a different database.</p>



<p class="wp-block-paragraph">One caveat is that we have to manually specify our columns in the right order in our <code>SELECT</code> queries, as the order of columns in SQLite can differ from the order of attributes in our PHP models.</p>



<pre class="wp-block-code"><code class="language-sh">echo "-- Dumping current data"
rm -f var/data-dump.sql || true # Remove any leftovers from previous (attempted) runs
sqlite3 db.sqlite -cmd ".mode insert users" 'SELECT id, email, password, name FROM users;' &gt;&gt; var/data-dump.sql
sqlite3 db.sqlite -cmd ".mode insert payments" 'SELECT id, amount FROM payments;' &gt;&gt; var/data-dump.sql
# Repeat for all your database tables</code></pre>



<p class="wp-block-paragraph">We should now have a text file called <code>data-dump.sql</code> which contains our production data as `INSERT queries. We can simply execute this on our MySQL database and be done with it!</p>



<pre class="wp-block-code"><code class="language-sh">echo "-- Importing data"
mysql database_name &lt; var/data-dump.sql</code></pre>



<p class="wp-block-paragraph">At this point there is just one thing left that needs to be done, and that is removing the old <code>DATABASE_URL</code> line from <code>.env.local</code> and confirming that everything went well.</p>



<p class="wp-block-paragraph">Of course your process may look slightly different, but I hope this gives you some ideas on how to tackle this problem in a mostly automated way.</p>



<p class="wp-block-paragraph">[^1]: Yes, I have no qualms running SQLite in production for some of my projects. <a href="https://avi.im/blag/2024/sqlite-bad-rep/">Why does SQLite in production have such a bad rep?</a></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>C++ development setup in 2024</title>
		<link>https://www.dannyvankooten.com/blog/2024/cpp-development-setup/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Sat, 06 Apr 2024 00:00:00 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2024/cpp-development-setup/</guid>

					<description><![CDATA[My C++ development setup using Sublime Text 4 with Clangd as the language server.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I&#8217;ve been doing a lot of C and C++ programming lately. After trying a range of editors and related tooling, I have settled on a setup that is fast, reliable, and powerful.</p>



<p class="wp-block-paragraph">This post walks through that C++ development setup: Sublime Text 4 with clangd, compiler flags, clang-tidy, clang-format, and perf.</p>



<h2 class="wp-block-heading">Editor: Sublime Text</h2>



<p class="wp-block-paragraph">The two most popular options right now are probably VSCode and CLion, yet I found neither of them performant or reliable enough for my taste. Instead, I use <a href="https://www.sublimetext.com/">Sublime Text 4</a> in combination with <a href="https://lsp.sublimetext.io/">its Language Server Protocol implementation</a> and <a href="https://github.com/sublimelsp/LSP-clangd">Clangd</a>.</p>



<p class="wp-block-paragraph"><img decoding="async" alt="Sublime Text Editor in dark mode with Clangd as language server" src="https://www.dannyvankooten.com/wp-content/uploads/2024/04/sublime-text-clangd.png"></p>



<p class="wp-block-paragraph">Why Sublime?</p>



<ul class="wp-block-list">
<li>It&#8217;s an order of magnitude faster than both CLion and VSCode.</li>



<li>It doesn&#8217;t suffer from random crashes on large projects.</li>



<li>It&#8217;s easy to configure by modifying JSON config files.</li>



<li>It works without hours of configuration. Install <strong>LSP</strong> and <strong>LSP-Clangd</strong> through Package Control and you have a powerful editor ready to go.</li>



<li>It&#8217;s much more resource efficient than the alternatives. I can have a large C++ project open while it consumes less than 600 MB of RAM and the CPU sits idle.</li>
</ul>



<p class="wp-block-paragraph">The main drawback is that it&#8217;s not open source. Still, I like how a small team from Australia can build a small but profitable business around a code editor.</p>



<p class="wp-block-paragraph">The <a href="https://packagecontrol.io/">community for plugins</a> is not as vibrant or active as it once was, but everything I need is there.</p>



<h2 class="wp-block-heading">Configuring Clangd</h2>



<p class="wp-block-paragraph">Clangd works with Sublime Text through LSP-clangd out of the box, but a few settings are worth configuring.</p>



<p class="wp-block-paragraph">First, enable background indexing explicitly.</p>



<ol class="wp-block-list">
<li>Go to <strong>Preferences &gt; Package Settings &gt; LSP &gt; Servers &gt; clangd</strong>.</li>



<li>Ensure <code>initializationOptions["clangd"]["background-index"]</code> is set to <code>true</code>:</li>
</ol>



<pre class="wp-block-code"><code>{
   "initializationOptions":{
      "clangd.background-index":true
   }
}</code></pre>



<p class="wp-block-paragraph">To let Clangd know how to build your project, you can use several methods:</p>



<ul class="wp-block-list">
<li>A <code>.clangd</code> YAML file in your project root containing a <code>CompileFlags</code> key.
    <code>CompileFlags:
      Add: [ -std=c++20, -Wall, -Wextra, -Wconversion ]</code></li>



<li>A <code>compile_flags.txt</code> file containing one flag per line.
    <code>-std=c++20
    -Wall
    -Wextra
    -Wconversion</code></li>



<li>A <code>compile_commands.json</code> file. You can have this file generated by CMake by setting the <code>CMAKE_EXPORT_COMPILE_COMMANDS</code> environment variable.</li>
</ul>



<h2 class="wp-block-heading">Compiler flags</h2>



<p class="wp-block-paragraph">My workstation runs <a href="https://www.debian.org/">Debian</a>, so I tend to compile the C and C++ projects I work on using whatever version of <a href="https://gcc.gnu.org/">GCC</a> and <a href="https://clang.llvm.org/">Clang</a> is in the official package repositories.</p>



<p class="wp-block-paragraph">Currently, this means GCC 12 and Clang 14, both of which have near-complete support for <code>-std=c++20</code> <a href="https://clang.llvm.org/cxx_status.html">^1</a>.</p>



<p class="wp-block-paragraph">When a newer compiler is needed, there&#8217;s always <a href="https://gcc.gnu.org/wiki/InstallingGCC">building GCC from source</a> or <a href="https://apt.llvm.org/">LLVM&#8217;s APT repositories</a>.</p>



<p class="wp-block-paragraph">Both GCC and Clang are conservative with warnings, so it is worth enabling some of them explicitly. The group of warnings from <code>-Wall</code> and <code>-Wextra</code> is what I always enable as a baseline <a href="https://clang.llvm.org/docs/DiagnosticsReference.html#w-warnings">^3</a>.</p>



<p class="wp-block-paragraph">For compilation profiles, I tend to use three:</p>



<h3 class="wp-block-heading">Development</h3>



<p class="wp-block-paragraph">Development mode mostly cares about fast compilation times. The <code>LDFLAGS</code> environment value instructs the compiler to use <a href="https://github.com/rui314/mold">mold</a> for the linking stage, which is usually an order of magnitude faster than ld.</p>



<pre class="wp-block-code"><code>CFLAGS="-std=c11 -Wall -Wextra -Wvla -Wformat -Wformat=2 -Wconversion -Wdouble-promotion -g"
CXXFLAGS="-std=c++20 -Wall -Wextra -Wconversion -g"
LDFLAGS="-fuse-ld=mold"</code></pre>



<h3 class="wp-block-heading">Debug</h3>



<p class="wp-block-paragraph">In debug mode, we want debug symbols, stack traces, and runtime checks from both Address Sanitizer and Undefined Behavior Sanitizer.</p>



<pre class="wp-block-code"><code>CFLAGS="-g -fsanitize=address,undefined -fno-omit-frame-pointer"
CXXFLAGS="-g -fsanitize=address,undefined -D_GLIBCXX_ASSERTIONS -fno-omit-frame-pointer"
ASAN_OPTIONS="strict_string_checks=1:strict_memcmp=1:quarantine_size_mb=512:detect_stack_use_after_return=1:check_initialization_order=1"
UBSAN_OPTIONS="print_stacktrace=1"</code></pre>



<p class="wp-block-paragraph"><code>-D_GLIBCXX_ASSERTIONS</code> adds runtime bound checks for C++ containers from the STL. There is also <code>-D_FORTIFY_SOURCE=2</code>, which adds runtime buffer overflow detection for a collection of functions from glibc, but it requires an optimization level of 1 or higher (<code>-O1</code>).</p>



<p class="wp-block-paragraph"><code>ASAN_OPTIONS</code> is used to <a href="https://github.com/google/sanitizers/wiki/AddressSanitizerFlags">configure Address Sanitizer</a> so it can use more memory for detecting use-after-free errors and perform some stricter checks that are disabled by default.</p>



<p class="wp-block-paragraph"><code>UBSAN_OPTIONS</code> is used alongside <code>-fno-omit-frame-pointer</code> to <a href="https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html">configure Undefined Behavior Sanitizer</a> to print a symbolized stack trace for each error report.</p>



<h3 class="wp-block-heading">Release</h3>



<p class="wp-block-paragraph">In release mode, we want the compiler to produce the fastest code possible at the cost of longer compilation times.</p>



<pre class="wp-block-code"><code>CFLAGS="-O3"
CXXFLAGS="-O3"</code></pre>



<p class="wp-block-paragraph">If you don&#8217;t care about portability and want to target your specific CPU, you could add <code>-march=native</code> and <code>-mtune=native</code>.</p>



<h2 class="wp-block-heading">Diagnostics: clang-tidy</h2>



<p class="wp-block-paragraph">Since we use <a href="https://clangd.llvm.org/">Clangd</a> as our language server, we can instruct it to emit diagnostics beyond compiler warnings through <a href="https://clang.llvm.org/extra/clang-tidy/">clang-tidy</a>.</p>



<p class="wp-block-paragraph"><img decoding="async" alt="clang-tidy diagnostics in Sublime Text" src="https://www.dannyvankooten.com/wp-content/uploads/2024/04/sublime-text-clangd-diagnostics.png"></p>



<p class="wp-block-paragraph">clang-tidy is disabled by default, but you can enable it by modifying the settings for the LSP-clangd plugin.</p>



<ol class="wp-block-list">
<li>Go to <strong>Preferences &gt; Package Settings &gt; LSP &gt; Servers &gt; Clangd</strong>.</li>



<li>Ensure <code>initializationOptions.clangd["clang-tidy"]</code> is set to <code>true</code></li>
</ol>



<pre class="wp-block-code"><code>{
   "initializationOptions":{
      "clangd.clang-tidy":true,
      "clangd.background-index":true,
      "clangd.header-insertion":"iwyu",
      "clangd.completion-style":"detailed"
   }
}</code></pre>



<ol start="3" class="wp-block-list">
<li>You can configure clang-tidy by creating a <code>.clangd</code> YAML file in your project root. In it, you can add or remove the <a href="https://clang.llvm.org/extra/clang-tidy/checks/list.html">clang-tidy checks</a> you want. I mostly stick to the ones from <code>performance-*</code> and <code>cppcoreguidelines-*.</code></li>
</ol>



<pre class="wp-block-code"><code>Diagnostics: 
    ClangTidy: 
        Add: 
        - performance-* 
        - cppcoreguidelines-* 
        Remove: 
        - cppcoreguidelines-avoid-magic-numbers</code></pre>



<p class="wp-block-paragraph">The LLVM suite also contains a <code>run-clang-tidy</code> command, which you can use to run a single check against your source directory:</p>



<pre class="wp-block-code"><code>run-clang-tidy -checks='-*,performance-unnecessary-value-param' src/</code></pre>



<h2 class="wp-block-heading">Formatter: clang-format</h2>



<p class="wp-block-paragraph">We can use <a href="https://clang.llvm.org/docs/ClangFormat.html">clang-format</a> through Sublime&#8217;s LSP-clangd plugin.</p>



<p class="wp-block-paragraph">Go to <strong>Preferences &gt; Package Settings &gt; LSP &gt; Settings</strong> and ensure <code>lsp_format_on_save</code> is set to <code>true</code>.</p>



<p class="wp-block-paragraph">Your code style can be configured through a <code>.clang-format</code> YAML file in your project root.</p>



<h2 class="wp-block-heading">Profiling: perf</h2>



<p class="wp-block-paragraph">To find performance bottlenecks, I have not come across anything that beats <a href="https://perf.wiki.kernel.org/index.php/Main_Page">perf</a> together with <a href="https://github.com/brendangregg/FlameGraph">flamegraph.pl</a>.</p>



<p class="wp-block-paragraph">The author of flamegraph.pl has a useful post with many language-specific tips on how to <a href="https://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html">create flamegraphs from a perf report</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Setting up a VPS for static site hosting</title>
		<link>https://www.dannyvankooten.com/blog/2024/static-site-hosting-vps/</link>
		
		<dc:creator><![CDATA[Danny van Kooten]]></dc:creator>
		<pubDate>Tue, 20 Feb 2024 00:00:00 +0000</pubDate>
				<category><![CDATA[Web Performance]]></category>
		<guid isPermaLink="false">https://www.dannyvankooten.com/blog/2024/static-site-hosting-vps/</guid>

					<description><![CDATA[Setting up a cheap VPS with nginx for hosting static sites, as documentation for future reference.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Remember me moving this site over to sourcehut pages last week? It did not last long. That was not really because of sourcehut pages itself.</p>



<p class="wp-block-paragraph">I had spent the previous few weeks moving some friends and family back to shared hosting, which turned out to be amazing value for money. For less than $10 per month, you get Apache, MySQL LTS, PHP 8.2, 64 MB of Varnish, SSH access, and hourly backups.</p>



<p class="wp-block-paragraph">With me going back to full-time employment in a few weeks, I liked the idea of having fewer systems to maintain. So I migrated all my remaining PHP applications to that same shared hosting. With this site on sourcehut pages, I could then power down all of the virtual servers I was renting.</p>



<p class="wp-block-paragraph">This worked well, but I missed one capability: an easy way to run code connected to the internet. I am especially interested in this because I am playing with the idea of allowing comments on this blog by letting people email a specific address.</p>



<p class="wp-block-paragraph">I decided to spin up a cheap VPS again and use it to host my various static sites. <code>hut publish</code> is great, but so is <code>rsync -rav</code>. I&#8217;ll use this post as documentation for future me, and hopefully it is useful to others in a similar boat too.</p>



<h3 class="wp-block-heading">Server details</h3>



<p class="wp-block-paragraph">For this server, we do not need much. A single-core vCPU with 1 GB of RAM, IPv4 and IPv6 networking enabled, some storage, and Debian installed is plenty.</p>



<p class="wp-block-paragraph">If your cloud provider lets you configure a firewall from their UI, allow inbound traffic only on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS).</p>



<h3 class="wp-block-heading">Software</h3>



<p class="wp-block-paragraph">Once logged in, update the base packages and install what we need.</p>



<pre class="wp-block-code"><code>apt update
apt upgrade
apt install vim nginx certbot python3-certbot-nginx</code></pre>



<p class="wp-block-paragraph">We could get somewhat newer versions by adding the nginx APT repository and using snap to install Certbot, but I am sticking to the Debian packaged versions here.</p>



<h3 class="wp-block-heading">Configuring nginx</h3>



<p class="wp-block-paragraph">We&#8217;ll store our websites and configuration files in <code>/var/www/</code>.</p>



<p class="wp-block-paragraph">Open <code>/etc/nginx/nginx.conf</code> and add the following line inside the <code>http { }</code> block:</p>



<pre class="wp-block-code"><code>include /var/www/nginx/*</code></pre>



<p class="wp-block-paragraph">This tells nginx to include all files in the <code>/var/www/nginx</code> directory, allowing us to leave the rest of this file alone.</p>



<p class="wp-block-paragraph">Create that directory and, inside it, create a file called <code>nginx.conf</code> for global configuration across all sites.</p>



<pre class="wp-block-code"><code>mkdir /var/www/nginx
touch /var/www/nginx/nginx.conf</code></pre>



<p class="wp-block-paragraph">The first thing we want to do is disable the <code>server_tokens</code> directive so nginx stops including its version in HTTP headers.</p>



<pre class="wp-block-code"><code>/var/www/nginx/nginx.conf</code></pre>



<pre class="wp-block-code"><code>server_tokens off;</code></pre>



<p class="wp-block-paragraph">Next, enable <a href="http://nginx.org/en/docs/http/ngx_http_gzip_module.html">gzip compression</a> and configure it properly.</p>



<pre class="wp-block-code"><code>/var/www/nginx/nginx.conf</code></pre>



<pre class="wp-block-code"><code>gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 32 4k;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;</code></pre>



<p class="wp-block-paragraph">This enables gzip compression for HTML, CSS, SVG, and JS responses at a level that strikes a good balance between compute cost and compression ratio.</p>



<p class="wp-block-paragraph">Responses with a <code>Content-Length</code> header below 1024 bytes are not compressed, since they would barely benefit from it.</p>



<p class="wp-block-paragraph">To determine a good setting for <code>gzip_buffers</code>, use <a href="https://man.openbsd.org/getconf.1">getconf</a> to get the size of a memory page on your system.</p>



<pre class="wp-block-code"><code>getconf PAGESIZE</code></pre>



<p class="wp-block-paragraph">If this returns a value other than 4096, modify the <code>gzip_buffers</code> setting accordingly.</p>



<h3 class="wp-block-heading">Serving your site</h3>



<p class="wp-block-paragraph">Next, create another file containing the server configuration for your static site.</p>



<pre class="wp-block-code"><code>/var/www/nginx/www.dannyvankooten.com</code></pre>



<pre class="wp-block-code"><code>server {
    listen 80;
    listen &#91;::]:80;
    index index.html;
    server_name www.dannyvankooten.com dannyvankooten.com;
    root /var/www/www.dannyvankooten.com;

    # Cache static assets for 1 year
    location ~* .(?:css|js|ico|txt|svg|jpg|jpeg|webp|png|csv)$ {
        expires 1y;
        add_header "Cache-Control" "public";
    }

    location / {
        try_files $uri $uri/ =404;
    }
}</code></pre>



<p class="wp-block-paragraph">Test your configuration with <code>nginx -t</code>. If that succeeds, reload nginx with <code>nginx -s reload</code>.</p>



<h3 class="wp-block-heading">Uploading your site</h3>



<p class="wp-block-paragraph">This site uses <a href="https://github.com/dannyvankooten/gozer">gozer</a> to turn Markdown into HTML files and generate an RSS feed. Uploading the site to our server is a simple matter of using rsync:</p>



<pre class="wp-block-code"><code>rsync -rav build/. remote-user@remote-host:/var/www/www.dannyvankooten.com</code></pre>



<p class="wp-block-paragraph">The nice thing about this is that on subsequent calls, only modified files are transferred. We could use <code>-z</code> to enable compression, but the gains are fairly minimal because files are sent in isolation.</p>



<h3 class="wp-block-heading">Update your DNS records</h3>



<p class="wp-block-paragraph">Our site is ready to go live. You can preview it by adding a temporary entry to your <code>/etc/hosts</code> file.</p>



<pre class="wp-block-code"><code>123.456.789.123 www.dannyvankooten.com dannyvankooten.com</code></pre>



<p class="wp-block-paragraph">If everything looks good, update the DNS records of your domain so it has an <code>A</code> and <code>AAAA</code> record pointing to your server.</p>



<p class="wp-block-paragraph">You can verify the DNS change using <code>dig</code>:</p>



<pre class="wp-block-code"><code>dig dannyvankooten.com +noall +answer -t A
dig dannyvankooten.com +noall +answer -t AAAA</code></pre>



<h3 class="wp-block-heading">Enable HTTPS</h3>



<p class="wp-block-paragraph">With your domain pointing to your server, it is time for the final step: enabling HTTPS on your site. We already have Certbot installed, so creating a new SSL certificate that automatically renews every 3 months is as easy as:</p>



<pre class="wp-block-code"><code>certbot --nginx -d dannyvankooten.com,www.dannyvankooten.com</code></pre>



<p class="wp-block-paragraph">That&#8217;s it. You can repeat the steps above for multiple sites. Unless your blog gets millions of pageviews per day, you can easily host a dozen static sites like this without making the server work hard.</p>



<h3 class="wp-block-heading">Tweaks</h3>



<p class="wp-block-paragraph">What follows are some final tweaks. They are not strictly necessary, but they are useful.</p>



<h4 class="wp-block-heading">Creating a non-root user</h4>



<p class="wp-block-paragraph">It is a good idea to create a non-privileged user account that requires <code>sudo</code> for actions that need elevated permissions.</p>



<pre class="wp-block-code"><code>adduser danny
adduser danny sudo
adduser danny www-data</code></pre>



<h4 class="wp-block-heading">Only allow SSH access using public key authentication</h4>



<p class="wp-block-paragraph">First, add your public key to <code>$HOME/&lt;user&gt;/.ssh/authorized_keys</code>.</p>



<p class="wp-block-paragraph">Then open <code>/etc/ssh/sshd_config</code> and disable password authentication.</p>



<pre class="wp-block-code"><code>/etc/ssh/sshd_config</code></pre>



<pre class="wp-block-code"><code>PasswordAuthentication no</code></pre>



<h4 class="wp-block-heading">Increasing the soft limit for open file descriptors</h4>



<p class="wp-block-paragraph">nginx defaults to 768 worker connections. We <a href="https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#insufficient-fds">should increase our soft limit for open file descriptors</a> to at least twice that value.</p>



<p class="wp-block-paragraph">Open <code>/etc/security/limits.conf</code> and add the following line just before the end of the file:</p>



<pre class="wp-block-code"><code>/etc/security/limits.conf</code></pre>



<pre class="wp-block-code"><code>www-data soft nofile 1536</code></pre>



<h4 class="wp-block-heading">Disabling or buffering access logging</h4>



<p class="wp-block-paragraph">Logging every request consumes both CPU and I/O cycles. You can disable it entirely by including the following directive in your configuration file.</p>



<pre class="wp-block-code"><code>/var/www/nginx/nginx.conf</code></pre>



<pre class="wp-block-code"><code>access_log off;</code></pre>



<p class="wp-block-paragraph">Another way to reduce the impact is to enable access log buffering.</p>



<pre class="wp-block-code"><code>/var/www/nginx/nginx.conf</code></pre>



<pre class="wp-block-code"><code>access_log /var/log/nginx/access.log combined buffer=4096 flush=1m;</code></pre>



<p class="wp-block-paragraph">This writes to the log only once the 4 kB buffer is full or a minute has passed since the last write.</p>



<h4 class="wp-block-heading">Limit request body size</h4>



<p class="wp-block-paragraph">Since we are hosting a static website that does not accept any request data, we can change the default value for <code>client_max_body_size</code> to something much closer to zero.</p>



<pre class="wp-block-code"><code>/var/www/nginx/nginx.conf</code></pre>



<pre class="wp-block-code"><code>client_max_body_size 4k;</code></pre>



<p class="wp-block-paragraph">[^1]: My two reservations are that they seem to have been somewhat less reliable in terms of uptime since <a href="https://sourcehut.org/blog/2024-01-19-outage-post-mortem/">they suffered a huge DDoS</a> a while ago, and that I am <a href="https://www.thegreenwebfoundation.org/green-web-check/?url=pages.sr.ht">unsure whether their new servers are powered by renewable energy</a>.</p>



<p class="wp-block-paragraph">[^2]: You can of course pick other distributions. A lot of tutorials online use Ubuntu, but I really enjoy Debian (which Ubuntu is based on) and its stability.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
