<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[At least it works.]]></title><description><![CDATA[I wonder how ...]]></description><link>https://blog.aaronlenoir.com/</link><image><url>https://blog.aaronlenoir.com/favicon.png</url><title>At least it works.</title><link>https://blog.aaronlenoir.com/</link></image><generator>Ghost 2.38</generator><lastBuildDate>Wed, 11 Feb 2026 08:15:19 GMT</lastBuildDate><atom:link href="https://blog.aaronlenoir.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Google Sheets driven client-side car stats app: Part 5]]></title><description><![CDATA[<p>I don't know if this warrants its own post, but I'd like to get the code that fetches the data out of my vue.js configuration.</p><p>This code:</p><!--kg-card-begin: code--><pre><code class="language-javascript">  created: function () {
    fetch(url)
    .then(
      function(response) {
          response.json().then(function(data) {
            app.data = data;
            app.dataAvailable = true;
          });
      }
    ).catch((error) =&gt; {
      console.</code></pre>]]></description><link>https://blog.aaronlenoir.com/2020/03/07/google-sheets-driven-client-side-car-stats-app-part-4-2/</link><guid isPermaLink="false">5e4f0d2514133b00015bda73</guid><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sat, 07 Mar 2020 07:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I don't know if this warrants its own post, but I'd like to get the code that fetches the data out of my vue.js configuration.</p><p>This code:</p><!--kg-card-begin: code--><pre><code class="language-javascript">  created: function () {
    fetch(url)
    .then(
      function(response) {
          response.json().then(function(data) {
            app.data = data;
            app.dataAvailable = true;
          });
      }
    ).catch((error) =&gt; {
      console.error('Error:', error);
    });
  },</code></pre><!--kg-card-end: code--><p>Instead, I'll write a "service", or at least something that looks like it. So I'll put this in a separate "services.js" file:</p><!--kg-card-begin: code--><pre><code class="language-javascript">let url = 'https://spreadsheets.google.com/feeds/cells/16hJ_67ox89L3DuVuc8KWAGMYFwb58rVP2fjCkc2AoGE/1/public/full?alt=json';﻿

let edriveDataService = (function () {
    let fetchData = function (callback) {
        fetch(url)
            .then(function (response) {
                return response.json();
            })
            .then(function (data) {
                callback(data);
            })
            .catch((error) =&gt; {
                console.error('Error:', error);
            });;
    };

    return {
        fetchData: fetchData
    };
}());</code></pre><!--kg-card-end: code--><p>Then I can use it, like so:</p><!--kg-card-begin: code--><pre><code class="language-javascript">  created: function () {
    edriveDataService.fetchData(function (data) {
      app.data = data;
      app.dataAvailable = true;
    });
  },</code></pre><!--kg-card-end: code--><p>That moves some plumbing (the .json() call, the chaining of the .then's) out of the main code.</p><h2 id="conclusion">Conclusion</h2><p>I moved the code to fetch data into it's own class.</p><p>Next up, I will finally start doing some analysis on the data!</p>]]></content:encoded></item><item><title><![CDATA[Google Sheets driven client-side car stats app: Part 4]]></title><description><![CDATA[I want to analyse some data I collect about my trips with my electric car, using html/css/javascript and Google Sheets]]></description><link>https://blog.aaronlenoir.com/2020/03/01/google-sheets-driven-client-side-car-stats-app-part-4/</link><guid isPermaLink="false">5e4723f314133b00015bd9ea</guid><category><![CDATA[edrive]]></category><category><![CDATA[web]]></category><category><![CDATA[prototyping]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sun, 01 Mar 2020 09:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Currently, all I have on the app is a table that shows the data I receive from Google Sheet.</p><p>I would like to use Vue's component abilities to make that table a component. The goal is then to extend the app with various components that make up the whole site.</p><h2 id="organizing-the-code">Organizing the code</h2><p>To keep it simple, for now, I will put my components straight into the app.js file.</p><p>In the future, I could consider putting them in a components.js file, putting each component in its own file or use something like webpack that adds a "compile" step to my process.</p><h2 id="creating-a-basic-component-in-vue-js">Creating a basic Component in Vue.js</h2><p>I add this to app.js:</p><!--kg-card-begin: code--><pre><code class="language-javascript">Vue.component('trip-overview', {
  data: function () {
    return {
      message: "Hello world!"
    }
  },
  template: '&lt;div&gt;{{ message }}&lt;/div&gt;'
});</code></pre><!--kg-card-end: code--><p>And in index.html:</p><!--kg-card-begin: code--><pre><code class="language-html">&lt;div id="app"&gt;
  &lt;trip-overview&gt;&lt;/trip-overview&gt;
  ...
&lt;/div&gt;</code></pre><!--kg-card-end: code--><p>This will show the div from the template of the component, with the appropriate message.</p><h2 id="bind-some-data-into-the-component">Bind some data into the Component</h2><p>I can change my component and add a "prop":</p><!--kg-card-begin: code--><pre><code class="language-javascript">Vue.component('trip-overview', {
  props: ['rows'],
  template: '&lt;div&gt;{{ rows }}&lt;/div&gt;'
});</code></pre><!--kg-card-end: code--><p>Here, I just expect as the value of <code>rows</code> all the rows, as returned by <code>getRows</code> </p><p>I can bind that via the html declaration:</p><!--kg-card-begin: code--><pre><code class="language-html">&lt;trip-overview v-if="dataAvailable" v-bind:rows="getRows()"&gt;&lt;/trip-overview&gt;</code></pre><!--kg-card-end: code--><p>Note that I use <code>v-if</code> to make sure the component is only rendered AFTER the data is loaded.</p><p>If I run <code>getRows</code> before that, at the moment the data is not available. That function will fail. This also shows that if you exclude a component with <code>v-if</code> is will not only hide it, but also not try to render it or run its lifecycle.</p><p>The above code will just dump the JSON data on the page, but it shows I have the data available in the component:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card"><img src="https://blog.aaronlenoir.com/content/images/2020/02/2020-02-15-00_10_13-index.html.png" class="kg-image"></figure><!--kg-card-end: image--><h2 id="copy-the-table-into-the-component">Copy the table into the component</h2><p>The component code now looks as follows:</p><!--kg-card-begin: code--><pre><code class="language-javascript">Vue.component('trip-overview', {
  props: ['rows'],
  template: `
  &lt;table class="pure-table"&gt;
    &lt;thead&gt;
      &lt;tr v-for="row in rows.slice(0,1)"&gt;
        &lt;th v-if="row.cells[0].row == 1" v-for="cell in row.cells"&gt;
          {{ cell.inputValue }}
        &lt;/th&gt;
        &lt;td v-if="row.cells[0].row &gt; 1" v-for="cell in row.cells"&gt;
          {{ cell.inputValue }}
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr v-for="row in rows.slice(1)"&gt;
        &lt;th v-if="row.cells[0].row == 1" v-for="cell in row.cells"&gt;
          {{ cell.inputValue }}
        &lt;/th&gt;
        &lt;td v-if="row.cells[0].row &gt; 1" v-for="cell in row.cells"&gt;
          {{ cell.inputValue }}
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  `
});</code></pre><!--kg-card-end: code--><p>Note the use of backticks to surround the code. This is mostly to include newlines in the variable.</p><p>Also notice I changed the <code>getRows()</code> call to <code>rows</code>. This also means I did an optimization, since now <code>getRows()</code> is called only once instead of twice.</p><p>Now I can replace the whole <code>&lt;table&gt;</code> element in my html with my component. Resulting in the following:</p><!--kg-card-begin: code--><pre><code class="language-html">  &lt;div id="app"&gt;
    Data available: {{ dataAvailable }}
    
    &lt;trip-overview v-if="dataAvailable" v-bind:rows="getRows()"&gt;&lt;/trip-overview&gt;
  &lt;/div&gt;</code></pre><!--kg-card-end: code--><p>Nothing has changed to the final result, but the index.html looks cleaner again and I have a re-usable component to show a table listing a number of trip entries.</p><p>If I would choose to show a second table, maybe with a list showing only trips of &gt; 50 km I could add a second <code>trip-overview</code> element. For example:</p><!--kg-card-begin: code--><pre><code class="language-html">  &lt;div id="app"&gt;
    Data available: {{ dataAvailable }}
    
    &lt;trip-overview v-if="dataAvailable" v-bind:rows="getRows()"&gt;&lt;/trip-overview&gt;
    &lt;trip-overview v-if="dataAvailable" v-bind:rows="getFilteredRows()"&gt;&lt;/trip-overview&gt;
  &lt;/div&gt;</code></pre><!--kg-card-end: code--><h2 id="conclusion">Conclusion</h2><p>Using components allows me to build the page up out of smaller re-usable building blocks. You can also use components inside other components and build up the complexity like that.</p><p>It automatically already gave me an optimization in efficiency. And I'm now set to add more components.</p><p>Next up, I will be having a look at organizing the data my components will be showing to the user. I don't want to have the logic of calculating stats, grouping things etc ... in my components. So I'll do that elsewhere.</p>]]></content:encoded></item><item><title><![CDATA[Google Sheets driven client-side car stats app: Part 3]]></title><description><![CDATA[I want to analyse some data I collect about my trips with my electric car, using html/css/javascript and Google Sheets]]></description><link>https://blog.aaronlenoir.com/2020/02/23/google-sheets-driven-client-side-car-stats-app-part-3/</link><guid isPermaLink="false">5e45d16014133b00015bd9ab</guid><category><![CDATA[edrive]]></category><category><![CDATA[web]]></category><category><![CDATA[prototyping]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sun, 23 Feb 2020 09:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Now that I'm listing the data. I want to include something that will help make the site a little more pretty. But I also don't want to do a lot of designing.</p><p>"<a href="https://purecss.io/">Pure CSS</a>" is, according to them, a "set of small, responsive CSS modules that you can use in every web project."</p><p>The responsive part means it should include the things necessary to adjust to different screen sizes. Also they are small but look nice!</p><h2 id="adding-pure-css">Adding Pure CSS</h2><p>That's one line in the html's &lt;head&gt;:</p><!--kg-card-begin: code--><pre><code class="language-html">&lt;link rel="stylesheet" href="https://unpkg.com/purecss@1.0.1/build/pure-min.css" integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47" crossorigin="anonymous"&gt;</code></pre><!--kg-card-end: code--><h2 id="styling-the-table">Styling the table</h2><p>Add the class "pure-table" to the table tag:</p><!--kg-card-begin: code--><pre><code>      &lt;table class="pure-table"&gt;
          ...
      &lt;/table&gt;</code></pre><!--kg-card-end: code--><p>The table now looks like this:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card"><img src="https://blog.aaronlenoir.com/content/images/2020/02/2020-02-13-23_55_10-Window.png" class="kg-image"></figure><!--kg-card-end: image--><p>To show the headers as actual headers, I must change the table rendering a little.</p><!--kg-card-begin: code--><pre><code class="language-html">      &lt;table class="pure-table"&gt;
        &lt;thead&gt;
          &lt;tr v-for="row in getRows().slice(0,1)"&gt;
            &lt;th v-if="row.cells[0].row == 1" v-for="cell in row.cells"&gt;
              {{ cell.inputValue }}
            &lt;/th&gt;
            &lt;td v-if="row.cells[0].row &gt; 1" v-for="cell in row.cells"&gt;
              {{ cell.inputValue }}
            &lt;/td&gt;
          &lt;/tr&gt;
        &lt;/thead&gt;
        &lt;tbody&gt;
          &lt;tr v-for="row in getRows().slice(1)"&gt;
            &lt;th v-if="row.cells[0].row == 1" v-for="cell in row.cells"&gt;
              {{ cell.inputValue }}
            &lt;/th&gt;
            &lt;td v-if="row.cells[0].row &gt; 1" v-for="cell in row.cells"&gt;
              {{ cell.inputValue }}
            &lt;/td&gt;
          &lt;/tr&gt;
        &lt;/tbody&gt;
      &lt;/table&gt;</code></pre><!--kg-card-end: code--><p>This is not idea, since I'm calling the <code>getRows</code> function twice. But I will change this later so for now it's OK.</p><p>The result looks like this:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card"><img src="https://blog.aaronlenoir.com/content/images/2020/02/2020-02-14-00_12_14-Window.png" class="kg-image"></figure><!--kg-card-end: image--><h2 id="conclusion">Conclusion</h2><p>The web app isn't amazing yet. But at least I have a UI framework in place, as well as some styling built-in.</p><p>Next up will be to better organize things, using vue components and a dedicated class that will take care of parsing the data received from Google Sheets.</p>]]></content:encoded></item><item><title><![CDATA[Google Sheets driven client-side car stats app: Part 2]]></title><description><![CDATA[I want to analyse some data I collect about my trips with my electric car, using html/css/javascript and Google Sheets]]></description><link>https://blog.aaronlenoir.com/2020/02/20/google-sheets-driven-client-side-car-stats-app-part-2/</link><guid isPermaLink="false">5e4480717aee5d0001e8630c</guid><category><![CDATA[edrive]]></category><category><![CDATA[web]]></category><category><![CDATA[prototyping]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Thu, 20 Feb 2020 22:49:00 GMT</pubDate><content:encoded><![CDATA[<p>Now I have my data, but to connect it to the UI, I'm going to use a framework.</p><p>I've had some good experiences with <a href="https://vuejs.org/">vue.js</a> in the past: it's easy to get started without much fuss, but broadly used and well supported.</p><p>As the most basic feature of my web app, I just want to show the data that I receive from the Sheet.</p><h2 id="load-vue-js">Load Vue.js</h2><p>But to do that, I already want to get my UI framework in place, so that I can continue extending the app from there.</p><h3 id="add-vue-js-as-a-reference">Add Vue.js as a reference</h3><p>Checkout the <a href="https://vuejs.org/v2/guide/">Getting Started</a> guide for vue.js if you're going to use it.</p><p>For now, I'll add this to my index.html head:</p><!--kg-card-begin: code--><pre><code class="language-html">&lt;script src="https://cdn.jsdelivr.net/npm/vue"&gt;&lt;/script&gt;</code></pre><!--kg-card-end: code--><h3 id="create-the-vue-app">Create the Vue App</h3><p>To make sure everything is loaded, I will follow the getting started guide to add a message, in index.html:</p><!--kg-card-begin: code--><pre><code class="language-html">&lt;body&gt;
  &lt;div id="app"&gt;
    {{ message }}
  &lt;/div&gt;
&lt;/body&gt;</code></pre><!--kg-card-end: code--><p>And in js/app.js:</p><!--kg-card-begin: code--><pre><code class="language-javascript">var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
});</code></pre><!--kg-card-end: code--><blockquote>Note: for this to work I had to move the &lt;script&gt; tag to include my js/app.js to the bottom of the &lt;body&gt; tag</blockquote><p>In summary, index.html:</p><!--kg-card-begin: code--><pre><code>&lt;html&gt;
&lt;head&gt;
  &lt;script src="https://cdn.jsdelivr.net/npm/vue"&gt;&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div id="app"&gt;
    {{ message }}
  &lt;/div&gt;
  
  &lt;script src="js/app.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><!--kg-card-end: code--><p>And js/app.js:</p><!--kg-card-begin: code--><pre><code>let url = 'https://spreadsheets.google.com/feeds/cells/16hJ_67ox89L3DuVuc8KWAGMYFwb58rVP2fjCkc2AoGE/1/public/full?alt=json';

fetch(url)
  .then(
    function(response) {
        response.json().then(function(data) {
          console.log(data);
        });
    }
  );

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
});</code></pre><!--kg-card-end: code--><h2 id="load-data-when-vue-js-app-loads">Load data when Vue JS app loads</h2><p>Currently, I fetch the data and only write it to the console.</p><p>What I want is to tell the vue app to load the data, and once it's loaded make it available to views inside the vue app.</p><p>Remember I'm making this app as a prototype. I may not be using vue in the ideal manner. Just a way to get it going. I may change this later.</p><p>What I can do is:</p><ul><li>Add a flag to the app data indicating if the data is available or not</li><li>On the "created" hook of the app, actually load the data</li></ul><p>I can do this as follows in the app:</p><!--kg-card-begin: code--><pre><code class="language-javascript">// ...
var app = new Vue({
  el: '#app',
  data: {
    dataAvailable: false,
    data: {}
  },
  created: function () {
    fetch(url)
    .then(
      function(response) {
          response.json().then(function(data) {
            app.data = data;
            app.dataAvailable = true;
          });
      }
    ).catch((error) =&gt; {
      console.error('Error:', error);
    });
  }
});</code></pre><!--kg-card-end: code--><p>Note, I replace the <code>message</code> from the original with my new variables: <code>dataAvailable</code> and <code>data</code>.</p><p>I can change my html as follows:</p><!--kg-card-begin: code--><pre><code class="language-html">  &lt;div id="app"&gt;
    Data available: {{ dataAvailable }}
    &lt;div v-if="dataAvailable"&gt;
      HERE COMES THE LIST!
    &lt;/div&gt;
  &lt;/div&gt;</code></pre><!--kg-card-end: code--><p>Now I have a page that indicates "loading: false" as soon as it's loading, and "loading: true" as soon as the data is available.</p><p>The second div is only rendered when the data is available. The plan is to put some markup in there to actually list the values in a table.</p><h2 id="show-a-list-with-vue">Show a list with vue</h2><p>The data structure coming from the Sheet offers a property <code>.feed.entry</code> which is the array of cells with data.</p><p>It's a little annoying that the things are structured in a flat list of cells. So I'm adding a method to my app that will restructure the data as a list of rows:</p><!--kg-card-begin: code--><pre><code class="language-javascript">var app = new Vue({
// ...
  methods: {
    getRows: function () {
      // group by row
      let rows = app.data.feed.entry.reduce((acc, reducer) =&gt; {
        let row = acc[reducer.gs$cell.row - 1] || { row: reducer.gs$cell.row, cells: [] };
        row.cells.push(reducer.gs$cell);
        acc[reducer.gs$cell.row - 1] = row;
        return acc;
      }, []);
      
      return rows;
    }
  }
});</code></pre><!--kg-card-end: code--><p>The <code>reduce</code> call is used to group by row. It's not the most elegant implementation, but the result is an array of two rows, each with 14 cells.</p><p>I can now continue to render an HTML table based on the data:</p><!--kg-card-begin: code--><pre><code class="language-html">  &lt;div id="app"&gt;
    Data available: {{ dataAvailable }}
    &lt;div v-if="dataAvailable"&gt;
      &lt;table&gt;
        &lt;tr v-for="row in getRows()"&gt;
          &lt;td v-for="cell in row.cells"&gt;
            {{ cell.inputValue }}
          &lt;/td&gt;
        &lt;/tr&gt;
      &lt;/table&gt;
    &lt;/div&gt;
  &lt;/div&gt;</code></pre><!--kg-card-end: code--><p>It looks ugly, but that displays my data on screen:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card"><img src="https://blog.aaronlenoir.com/content/images/2020/02/2020-02-13-00_50_47-Mozilla-Firefox.png" class="kg-image"></figure><!--kg-card-end: image--><h2 id="conclusion">Conclusion</h2><p>Here I added vue js to my app. This will allow me to more easily drive my UI from the data in my app.</p><p>I now have a vue app that, when available, shows the loaded data from the Google Sheet as an html table on the page.</p><p>Next up is adding something to make the page look better, without doing too much design / css work.</p>]]></content:encoded></item><item><title><![CDATA[Google Sheets driven client-side car stats app: Part 1]]></title><description><![CDATA[I want to analyse some data I collect about my trips with my electric car, using html/css/javascript and Google Sheets]]></description><link>https://blog.aaronlenoir.com/2020/02/11/google-sheets-driven-client-side-car-stats-app-part-1/</link><guid isPermaLink="false">5e41e4dd3b6c3c00014a21dc</guid><category><![CDATA[edrive]]></category><category><![CDATA[web]]></category><category><![CDATA[prototyping]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Tue, 11 Feb 2020 00:34:41 GMT</pubDate><content:encoded><![CDATA[<p>Most of the world runs on Spreadsheets. But stuff looks nicer on the web.</p><p>I'm going to build a web application with the following characteristics:</p><ul><li>Driven by data from a Google Spreadsheet</li><li>Running with HTML, CSS and JavaScript</li><li>No server-side technology, except for a webserver serving static HTML, CSS and JavaScript</li></ul><p>Why do I want this? It avoids a lot of complications and boiler plating, but it's also lots of fun. Furthermore, the hosting I have is getting a little low on resources.</p><h2 id="the-motivation">The Motivation</h2><p>I like to have some insight in data, in the form of stats. For some reason, that is pleasant to me.</p><p>If all goes well, I will soon be driving an electric car and I'd like to see some details. What are the average trips, how fast does the battery discharge in various circumstances, etc etc ...</p><p>There's probably stuff for that around, but it's fun to be free in whatever you want to learn.</p><h2 id="the-plan">The Plan</h2><p>First of all I want a Google Spreadsheet with data of each trip. Date, time, outside temperature, total km, use of highway, use of airco, ...</p><p>Secondly, I want a webpage where I can see whatever analysis I feel like doing.</p><p>For that second part, I'd like to do everything in HTML/CSS/JavaScript that fetches and parses the data from Google directly.</p><p>As far as the back-end is concerned, all I need is a web server that can serve static HTML, CSS and JavaScript files.</p><p>Given my scenario, this seems reasonable. I don't require complex data storage (databases), user management (logins), content creation, ... all I need to do is show some data.</p><p>But I wonder if not having a back-end at all is an advantage or disadvantage. I made a similar app before, for our office foosball stats - but that one had a little bit of back-end code.</p><h2 id="why-google-sheets">Why Google Sheets?</h2><ul><li>I don't have to worry about setting up and running a Database</li><li>I don't have to create data entry forms</li><li>I don't have to worry about authentication / authorization (google does it for me!)</li></ul><p>In general, it's less stuff to think about.</p><h2 id="first-steps-setting-up-the-spreadsheet">First Steps: Setting up the Spreadsheet</h2><h3 id="create-the-sheet">Create the Sheet</h3><ul><li>Login to Google Sheets</li><li>Create a new Sheet</li><li>On the first row, put the column headers</li><li>Add some data</li></ul><p>In my case, I will be having the following columns for now:</p><ul><li>Date</li><li>Start time</li><li>Start km</li><li>Start charge</li><li>Start temp</li><li>End time</li><li>End km</li><li>End charge</li><li>POB (people on board)</li><li>Eco-mode?</li><li>B-mode?</li><li>Airco?</li><li>Highway?</li></ul><h3 id="publish-the-google-sheet-as-json">Publish the Google Sheet as JSON</h3><p>Google sheets allows you to publish the sheet and consume the data - without authentication - as JSON.</p><p>Here's a good post on how to set it up:</p><ul><li><a href="https://www.freecodecamp.org/news/cjn-google-sheets-as-json-endpoint/">How to use Google Sheets as a JSON endpoint</a> (<a href="https://www.freecodecamp.org/">freecodecamp.org</a>)</li></ul><p>I don't feel like reproducing that post, so please check it out. I made sure it's on <a href="https://web.archive.org/web/20200210235117/https://www.freecodecamp.org/news/cjn-google-sheets-as-json-endpoint/">archive.org</a> if the link dies.</p><p>Bottom line, here's my JSON endpoint:</p><ul><li><a href="https://spreadsheets.google.com/feeds/cells/16hJ_67ox89L3DuVuc8KWAGMYFwb58rVP2fjCkc2AoGE/1/public/full?alt=json">https://spreadsheets.google.com/feeds/cells/16hJ_67ox89L3DuVuc8KWAGMYFwb58rVP2fjCkc2AoGE/1/public/full?alt=json</a></li></ul><h2 id="first-steps-loading-the-json-in-a-webpage">First Steps: Loading the JSON in a webpage</h2><p>For the UI, I will have to use some third party tools. Something like bootstrap for the looks, and something like Vue.js for hooking up the data to the UI. </p><p>For now, all I care about is an index.html that's able to load the data. To load the data, I use the "<a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">fetch API</a>": <code>fetch</code></p><p>I have one HTML file:</p><!--kg-card-begin: code--><pre><code class="language-HTML">&lt;html&gt;
&lt;head&gt;
  &lt;script src="js/app.js"&gt;&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;

&lt;/body&gt;
&lt;/html&gt;
</code></pre><!--kg-card-end: code--><p>And one js/app.js file:</p><!--kg-card-begin: code--><pre><code class="language-JavaScript">let url = 'https://spreadsheets.google.com/feeds/cells/16hJ_67ox89L3DuVuc8KWAGMYFwb58rVP2fjCkc2AoGE/1/public/full?alt=json';

fetch(url)
  .then(
    function(response) {
        response.json().then(function(data) {
          console.log(data);
        });
    }
  );
 
document.write('Hello world!');</code></pre><!--kg-card-end: code--><p>All it does is load the data and log it to the console. I also write "Hello world!" to the screen. If I see that then I know the JavaScript hasn't failed.</p><h2 id="first-steps-storing-the-code">First Steps: Storing the code</h2><p>To make it easy to load the code, and update it, I will put it all in a GitHub project.</p><p>On the webserver I can then just do a <code>git pull</code> to update the app.</p><p>I add the two files, but use the following folder structure:</p><ul><li>html/index.html</li><li>html/js/app.js</li></ul><p>Commit and push:</p><ul><li><a href="https://github.com/AaronLenoir/edrive">https://github.com/AaronLenoir/edrive</a></li></ul><h2 id="first-steps-hosting">First Steps: Hosting</h2><p>I already have hosting set up for this blog, my second blog and some additional apps. Each website is running in a docker container and gets its own hostname under aaronlenoir.com.</p><p>To host this, it's enough to set up an nginx server and point it to the html folder of the repo. Setting that up happens in the docker-compose configuration.</p><p>Not going into the full details, this is what I have to add:</p><!--kg-card-begin: code--><pre><code>  edrive:
    image: nginx:alpine
    restart: always
    depends_on:
      - "nginx-proxy"
    ports:
      - 127.0.0.1:8140:80
    volumes:
      - ./edrive/html:/usr/share/nginx/html:ro
    environment:
      - url=https://edrive.aaronlenoir.com
      - VIRTUAL_HOST=edrive.aaronlenoir.com
      - LETSENCRYPT_HOST=edrive.aaronlenoir.com
      - LETSENCRYPT_EMAIL=info@aaronlenoir.com</code></pre><!--kg-card-end: code--><p>This ensures that all requests for the domain "edrive.aaronlenoir.com" are passed to my new nginx instance.</p><p>The server is serving all files in the folder <code>./edrive/html</code> and its sub-folders.</p><p>Also notice the two LETSENCRYPT environment variables. These are used by the <a href="https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion">nginx-proxy-companion</a> docker container to fetch an SSL certificate from Let's Encrypt.</p><p>I also had to add an A-record to my DNS, which I do via Namecheap:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.aaronlenoir.com/content/images/2020/02/2020-02-11-01_19_42-Advanced-DNS---Brave.png" class="kg-image"><figcaption>A-Record in DNS configuration for "edrive.aaronlenoir.com"</figcaption></figure><!--kg-card-end: image--><h2 id="conclusion">Conclusion</h2><p>I've set up the base to start working, iteratively, on my simple stats apps for my car data:</p><ul><li><a href="https://edrive.aaronlenoir.com">https://edrive.aaronlenoir.com</a></li></ul><p>Using Google Sheets is a way of getting some data to your web application, without having to worry about: data entry, authentication, storage, back-end hosting, ... </p><p>Hosting requirements are limited to hosting static files. I can even run the app directly from my local disk.</p><p>I can push a change to github, and pull from there to update the app, wherever it's running.</p><p>Now to actually parse some data ...</p>]]></content:encoded></item><item><title><![CDATA[Embedding Line Chart with HighCharts]]></title><description><![CDATA[<p>I made a thing for my <a href="https://flightschool.aaronlenoir.com">other blog</a> to show the GPS track of my flights. Part of this is showing the altitude and speed profiles:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card"><img src="https://blog.aaronlenoir.com/content/images/2019/10/2019-10-21-23_25_34-Window.png" class="kg-image"></figure><!--kg-card-end: image--><p>I thought I'd write down how I did it.</p><h2 id="getting-the-data">Getting the Data</h2><p>Have a look at my <a href="https://blog.aaronlenoir.com/2019/09/25/draw-gps-track-on-openstreetmap/">previous blog post</a> to find out how</p>]]></description><link>https://blog.aaronlenoir.com/2019/10/21/show-line-chart/</link><guid isPermaLink="false">5dae21f80aa01f000170bb46</guid><category><![CDATA[tools]]></category><category><![CDATA[web]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Mon, 21 Oct 2019 21:56:06 GMT</pubDate><content:encoded><![CDATA[<p>I made a thing for my <a href="https://flightschool.aaronlenoir.com">other blog</a> to show the GPS track of my flights. Part of this is showing the altitude and speed profiles:</p><!--kg-card-begin: image--><figure class="kg-card kg-image-card"><img src="https://blog.aaronlenoir.com/content/images/2019/10/2019-10-21-23_25_34-Window.png" class="kg-image"></figure><!--kg-card-end: image--><p>I thought I'd write down how I did it.</p><h2 id="getting-the-data">Getting the Data</h2><p>Have a look at my <a href="https://blog.aaronlenoir.com/2019/09/25/draw-gps-track-on-openstreetmap/">previous blog post</a> to find out how I record the GPX track and get the data into JavaScript.</p><p>For this post, it's enough to know I end up with a parsed GPX structure:</p><!--kg-card-begin: markdown--><pre><code class="language-javascript">  let gpx = new gpxParser();
  gpx.parse(gpxData);
  drawAltitudeChart(gpx.tracks[0]);
</code></pre>
<!--kg-card-end: markdown--><p>The "magic" happens in the <code>drawAltitudeChart</code> function. Although the real magic actually happens in the HighCharts component.</p><h2 id="embedding-highcharts">Embedding HighCharts</h2><p>To show the graphs, I use a third party javascript component <a href="https://www.highcharts.com/blog/products/highcharts/">HighCharts</a>, which is free for non-profit and personal use.</p><p>Before we look at the details of <code>drawAltitudeChart</code>, which prepares the gpx data for the altitude profile, here's how to load up HighCharts:</p><p>Load the appropriate JavaScript:</p><!--kg-card-begin: code--><pre><code class="language-html">&lt;script src="https://code.highcharts.com/highcharts.js"&gt;&lt;/script&gt;
&lt;script src="https://code.highcharts.com/modules/series-label.js"&gt;&lt;/script&gt;
&lt;script src="https://code.highcharts.com/modules/exporting.js"&gt;&lt;/script&gt;</code></pre><!--kg-card-end: code--><p>Add a div as a place-holder for where the chart must appear:</p><!--kg-card-begin: markdown--><pre><code>&lt;div class=&quot;chart&quot; id=&quot;altitude&quot;&gt;&lt;/div&gt;
</code></pre>
<!--kg-card-end: markdown--><h2 id="preparing-the-data">Preparing the Data</h2><p>The first thing <code>drawAltitudeChart</code> is take the data from the gpx track and restructure it in a way that makes sense for the altitude chart.</p><p>What I need for that chart is a series of timepoints and the altitude at that time:</p><!--kg-card-begin: markdown--><pre><code>let dataPoints = track.points.filter(p =&gt; p.time !== null).map(p =&gt; [p.time, p.ele * 3.28084]);
</code></pre>
<!--kg-card-end: markdown--><p>I use some fancy JavaScript stuff like <code>filter</code> and <code>map</code>. <code>filter</code> is a function that can be executed on an array and will return a new array with only the items for which the given condition is true.</p><p>In my case I'm looking for points that have a time filled in. Because I noticed this happened sometimes.</p><p>With <code>map</code> I transform each element in the array of points into a new element - in my case a second array of a time and an elevation.</p><p>I multiply with 3.28084 because I want the altitude in ft (feet) and the gpx track has the altitude in meter. 1 meter is 3.28084 ft.</p><h2 id="showing-the-chart">Showing the Chart</h2><p>With the array of time-altitude pairs, the chart can be loaded. HighCharts offers many options, so it was quite the setup in the end.</p><!--kg-card-begin: markdown--><pre><code class="language-javascript">Highcharts.chart('altitude', {
    chart: {
        type: 'area'
    },
    title: {
        text: 'Altitude (ft)'
    },
    subtitle: {
        text: 'Altitude en route'
    },
    xAxis: {
        type: 'datetime',
        dateTimeLabelFormats: {
            month: '%e. %b',
            year: '%b'
        },
        title: {
            text: 'Date'
        }
    },
    yAxis: {
        title: {
            text: 'Altitude (ft)'
        }
    },
    tooltip: {
        headerFormat: '&lt;b&gt;{series.name}&lt;/b&gt;&lt;br&gt;',
        pointFormat: '{point.x:%e. %b %H:%M}: {point.y:.2f} ft'
    },

    plotOptions: {
        spline: {
            marker: {
                enabled: true
            }
        }
    },

    colors: ['#6CF', '#39F', '#06C', '#036', '#000'],

    series: [{
        name: &quot;Altitude (ft)&quot;,
        data: dataPoints
    }],

    time: { useUTC: false }
});
</code></pre>
<!--kg-card-end: markdown--><p>There are several types of charts, I like the look of the <em>area </em>chart.</p><!--kg-card-begin: code--><pre><code>chart: {
    type: 'area'
},</code></pre><!--kg-card-end: code--><p>The x-axis in my chart represents time, the object in javascript is of type <code>Date</code>, which HighCharts can understand if the x-axis type is <code>datetime</code></p><!--kg-card-begin: code--><pre><code>xAxis: {
    type: 'datetime',
    dateTimeLabelFormats: {
        month: '%e. %b',
        year: '%b'
    },
    title: {
        text: 'Date'
    }
},</code></pre><!--kg-card-end: code--><p>With the plot options it's possible to specify the line chart should show a spline, which smooths the segments between data points.</p><!--kg-card-begin: code--><pre><code>plotOptions: {
    spline: {
        marker: {
            enabled: true
        }
    }
},</code></pre><!--kg-card-end: code--><p>The colours for the chart I kept fairly standard.</p><!--kg-card-begin: markdown--><pre><code>colors: ['#6CF', '#39F', '#06C', '#036', '#000'],
</code></pre>
<!--kg-card-end: markdown--><p>Here's where I actually load in my data points. I also give the chart a name here. It's possible to load more than one series.</p><!--kg-card-begin: code--><pre><code>series: [{
    name: "Altitude (ft)",
    data: dataPoints
}],</code></pre><!--kg-card-end: code--><p>This time setting was important because by default HighCharts shows the time in UTC. Which is not as interesting in my case.</p><!--kg-card-begin: markdown--><pre><code>time: { useUTC: false }
</code></pre>
<!--kg-card-end: markdown--><h2 id="conclusion">Conclusion</h2><p>If you have a series of time-value pairs, you can use HighCharts to plot these in a chart, with a small amount of configuration.</p><p>HighCharts is free to use for non-profit and personal use.</p>]]></content:encoded></item><item><title><![CDATA[Switching Ghost 1 to Ghost 2]]></title><description><![CDATA[<p>As of writing, I have two blogs up and running. A new blog with hostname <a href="https://blog-test.aaronlenoir.com/">blog-test.aaronlenoir.com</a> running on Ghost 2 and <a href="https://blog-test.aaronlenoir.com/">blog.aaronlenoir.com</a> running on Ghost 1.</p><p>To switch hostnames, it should be fairly easy. Here's the two entries that need changing in <code>docker-compose.yml</code></p><!--kg-card-begin: code--><pre><code>  ...
  ghost:
    image:</code></pre>]]></description><link>https://blog.aaronlenoir.com/2019/09/29/switching-ghost-1-to-ghost-2/</link><guid isPermaLink="false">5d912fd64cd92f0001765c13</guid><category><![CDATA[ghost-upgrade]]></category><category><![CDATA[ghost]]></category><category><![CDATA[docker]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sun, 29 Sep 2019 22:35:28 GMT</pubDate><content:encoded><![CDATA[<p>As of writing, I have two blogs up and running. A new blog with hostname <a href="https://blog-test.aaronlenoir.com/">blog-test.aaronlenoir.com</a> running on Ghost 2 and <a href="https://blog-test.aaronlenoir.com/">blog.aaronlenoir.com</a> running on Ghost 1.</p><p>To switch hostnames, it should be fairly easy. Here's the two entries that need changing in <code>docker-compose.yml</code></p><!--kg-card-begin: code--><pre><code>  ...
  ghost:
    image: ghost:1-alpine
    restart: always
    depends_on:
      - "nginx-proxy"
    ports:
      - 127.0.0.1:8080:2368
    volumes:
      - ./blog/data/ghost:/var/lib/ghost/content
    environment:
      - url=https://blog.aaronlenoir.com
      - VIRTUAL_HOST=blog.aaronlenoir.com
      - LETSENCRYPT_HOST=blog.aaronlenoir.com
      - LETSENCRYPT_EMAIL=info@aaronlenoir.com
 ...
   ghost2:
    image: ghost:2-alpine
    restart: always
    depends_on:
      - "nginx-proxy"
    ports:
      - 127.0.0.1:8120:2368
    volumes:
      - ./blog2/data/ghost:/var/lib/ghost/content
    environment:
      - url=https://blog-test.aaronlenoir.com
      - VIRTUAL_HOST=blog-test.aaronlenoir.com
      - LETSENCRYPT_HOST=blog-test.aaronlenoir.com
      - LETSENCRYPT_EMAIL=info@aaronlenoir.com</code></pre><!--kg-card-end: code--><p>For reference, I will keep the old blog around for a bit on the blog-test URL.</p><p>Here's the new configuration:</p><!--kg-card-begin: code--><pre><code>...
  ghost:
    image: ghost:1-alpine
    restart: always
    depends_on:
      - "nginx-proxy"
    ports:
      - 127.0.0.1:8080:2368
    volumes:
      - ./blog/data/ghost:/var/lib/ghost/content
    environment:
      - url=https://blog-test.aaronlenoir.com
      - VIRTUAL_HOST=blog-test.aaronlenoir.com
      - LETSENCRYPT_HOST=blog-test.aaronlenoir.com
      - LETSENCRYPT_EMAIL=info@aaronlenoir.com
 ...
   ghost2:
    image: ghost:2-alpine
    restart: always
    depends_on:
      - "nginx-proxy"
    ports:
      - 127.0.0.1:8120:2368
    volumes:
      - ./blog2/data/ghost:/var/lib/ghost/content
    environment:
      - url=https://blog.aaronlenoir.com
      - VIRTUAL_HOST=blog.aaronlenoir.com
      - LETSENCRYPT_HOST=blog.aaronlenoir.com
      - LETSENCRYPT_EMAIL=info@aaronlenoir.com</code></pre><!--kg-card-end: code--><h2 id="conclusion">Conclusion</h2><p>If you are reading this post, everything worked out! Phew!</p>]]></content:encoded></item><item><title><![CDATA[Moving Custom Theme from Ghost 1 to 2]]></title><description><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>For my blog I use a slightly customized version of the default <em>Casper</em> theme.</p>
<p>Mostly because I include &quot;disqus&quot; comments at the bottom of the posts (comments are not supported by Ghost itself).</p>
<p>What I did to update my custom theme:</p>
<ul>
<li>Create and switch to a new branch:</li></ul>]]></description><link>https://blog.aaronlenoir.com/2019/09/29/moving-custom-theme-from-ghost-1-to-2/</link><guid isPermaLink="false">5d912fbe4cd92f0001765ad3</guid><category><![CDATA[ghost]]></category><category><![CDATA[ghost-upgrade]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sun, 29 Sep 2019 22:24:40 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>For my blog I use a slightly customized version of the default <em>Casper</em> theme.</p>
<p>Mostly because I include &quot;disqus&quot; comments at the bottom of the posts (comments are not supported by Ghost itself).</p>
<p>What I did to update my custom theme:</p>
<ul>
<li>Create and switch to a new branch: <code>ghost-2</code></li>
<li>Use <code>git fetch upstream</code> to get the latest branches from the original repo</li>
<li>Use <code>git merge upstream/master</code> to merge the original master into my branch</li>
</ul>
<p>This will make sure I have the latest updates from Ghost's default theme, as well as my changes.</p>
<p>I did have a small conflict on the line where I removed the &quot;feedly.com&quot; link in the RSS feed link.</p>
<p>After having done that, I cloned the repo into <code>blog2/data/ghost/themes/casper-custom</code>.</p>
<p>After doing that and restarting the blog, I could select the custom theme via the <em>Design</em> settings in Ghost.</p>
<p>Reloading everything, it turned out my comments were already working correctly on the new blog too!</p>
<p>Having done that, I think everything is ready to switch to the new version of the blog.</p>
<!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Moving posts from Ghost v1 to v2]]></title><description><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>In Ghost blog, I tried to export this current blog - as json - and import it to the new blog.</p>
<p>However, I received the error:</p>
<pre><code>Import failed
Request is larger than the maximum file size the server allows
</code></pre>
<p>Looking at the server's response it was:</p>
<pre><code>413 Request Entity Too</code></pre>]]></description><link>https://blog.aaronlenoir.com/2019/09/29/upgrade-to-ghost-2-import-existing/</link><guid isPermaLink="false">5d912fbe4cd92f0001765ad2</guid><category><![CDATA[ghost]]></category><category><![CDATA[docker]]></category><category><![CDATA[ghost-upgrade]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sun, 29 Sep 2019 21:38:07 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>In Ghost blog, I tried to export this current blog - as json - and import it to the new blog.</p>
<p>However, I received the error:</p>
<pre><code>Import failed
Request is larger than the maximum file size the server allows
</code></pre>
<p>Looking at the server's response it was:</p>
<pre><code>413 Request Entity Too Large
nginx/1.17.3
</code></pre>
<p>So my nginx proxy refuses to process a file this big in the upload.</p>
<h2 id="nginxconfigperdomain">nginx.config per domain</h2>
<p>With the nginx-proxy docker image, there's a way to create hostname specific configurations:</p>
<p>I have mapped the folder <code>./nginx/vhost.d</code> to <code>/etc/nginx/vhost.d</code> inside the container.</p>
<p>In that folder I can create a file named after the hostname. In this case:</p>
<pre><code>blog-test.aaronlenoir.com
</code></pre>
<h3 id="configureentitysize">Configure entity size</h3>
<p>According to <a href="https://www.cyberciti.biz/faq/linux-unix-bsd-nginx-413-request-entity-too-large/">this website</a> I must add a <code>client_max_body_size</code> setting to the ´http´, ´server´ or ´location´ context.</p>
<p>In my case, the config files are by default in the <code>location</code> context so I can add a line:</p>
<pre><code>client_max_body_size 2M;
</code></pre>
<p>The file is about 1.6M so this should be enough.</p>
<h3 id="restartnginx">Restart nginx</h3>
<p>To restart I take down all the containers using docker-compose:</p>
<pre><code>sudo docker-compose down
</code></pre>
<p>And I restart it back in &quot;background mode&quot; (<code>-d</code>):</p>
<pre><code>sudo docker-compose up -d
</code></pre>
<p>If there are no errors with my config, the nginx docker container will be up and running quickly, otherwise it'll probably bail out and I will have to look at the docker-compose output.</p>
<p>Luckily, everything started up without a hitch!</p>
<h2 id="importwarnings">Import Warnings</h2>
<p>After this change, I was able to import all posts.</p>
<p>There were a few warnings:</p>
<ul>
<li>User: Entry was imported, but we were not able to resolve the following user references: created_by, updated_by. The user does not exist, fallback to owner user.</li>
<li>User: Entry was not imported and ignored. Detected duplicated entry.</li>
<li>Settings: Theme not imported, please upload in Settings - Design</li>
<li>Settings: Permalink Setting was removed. Please configure permalinks in your routes.yaml.</li>
</ul>
<p>Only the last one, I think, will require some looking into.</p>
<h2 id="permalinks">Permalinks</h2>
<p>On my blog, I enable permalinks. This adds a date to the URL. Apparently, according to the warning, I should use <code>routes.yaml</code> for this.</p>
<p>In the &quot;Labs&quot; section of ghost I can upload a routes configuration file. But I'm not sure what I should put in the file.</p>
<p>The default, according to the Ghost 2 docs, looks like this:</p>
<pre><code>routes:

collections:
  /:
    permalink: /{slug}/
    template: index

taxonomies:
  tag: /tag/{slug}/
  author: /author/{slug}/
</code></pre>
<p>My current blog posts have a URL in the form of:</p>
<p><code>https://blog.aaronlenoir.com/&lt;YYYY&gt;/&lt;MM&gt;/&lt;DD&gt;/&lt;POST_TITLE&gt;</code></p>
<p>This post on <a href="https://andrewaadland.me/2018-11-05-preserving-publish-date-in-url-ghost-2-0/">Some Dude's Tech Blog</a> already details how to do this.</p>
<p>The <code>permalink</code> line needs updating to <code>/{year}/{month}/{day}/{slug}/</code>:</p>
<pre><code>routes:

collections:
  /:
    permalink: /{year}/{month}/{day}/{slug}/
    template: index

taxonomies:
  tag: /tag/{slug}/
  author: /author/{slug}/
</code></pre>
<p>So I put this in a routes.yaml file and upload it ... this did the trick.</p>
<p>Checking on the server, the file is stored in: <code>data/ghost/settings</code>.</p>
<p>It may seem unimportant, but when I add a third party comment app to my blog, the comments from the old blog will still be there if the URL of the posts remains the same.</p>
<h2 id="images">Images</h2>
<p>The json that exports my posts <strong>does not</strong> include images.</p>
<p>For this, I must copy the &quot;images&quot; folder from my original blog to the new one. The images folder can be found in <code>data/ghost/images</code></p>
<p>Since both are on the same server, I could do this in a single command:</p>
<p><code>cp -a ~/aaronlenoir.com/blog/data/ghost/images ~/aaronlenoir.com/blog2/data/ghost</code></p>
<h2 id="conclusion">Conclusion</h2>
<p>In this post I wanted to import my existing posts in a blog running on Ghost 1.25.7 to a new blog running on Ghost 2.</p>
<p>I exported my posts using the Labs section in the Administration section.</p>
<p>That gave me a 1.6 MB JSON file which I could not upload because nginx limited the upload size. I changed the setting <code>client_max_body_size</code> to be <code>2M</code>, which allowed me to import the posts.</p>
<p>Secondly, I made sure the URL's to the posts remained the same by restoring the &quot;permalink&quot; setting that includes the date in my posts.</p>
<p>Finally, I copied over all images from my old posts, which were not included in the export.</p>
<p>I had some other warnings during the import. They were not so important: couldn't find an old author and a duplicated entry. Finally, it complained it could not find my custom theme. But that's for next time.</p>
<!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Adding a new website (Digital Ocean + Docker)]]></title><description><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>In this post, I documents the steps I take to add a new website to aaronlenoir.com using Digital Ocean and docker.</p>
<p>I think I'd like to upgrade my blogs to blog engine Ghost 2 (I'm using version 1 now).</p>
<p>It's quite the update, so first I want to run</p>]]></description><link>https://blog.aaronlenoir.com/2019/09/28/adding-a-new-website-digital-ocean-docker/</link><guid isPermaLink="false">5d912fbe4cd92f0001765ad1</guid><category><![CDATA[ghost]]></category><category><![CDATA[docker]]></category><category><![CDATA[digital ocean]]></category><category><![CDATA[ghost-upgrade]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sat, 28 Sep 2019 23:43:51 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>In this post, I documents the steps I take to add a new website to aaronlenoir.com using Digital Ocean and docker.</p>
<p>I think I'd like to upgrade my blogs to blog engine Ghost 2 (I'm using version 1 now).</p>
<p>It's quite the update, so first I want to run a new instance on the side to see how it works.</p>
<p>I will be adding, as a test, blog-test.aaronlenoir.com.</p>
<p>Update: I though this would be a simple ten minute task, but Ghost 2 is giving me problems</p>
<h2 id="currentsetup">Current Setup</h2>
<p>At the moment, I'm hosting four websites.</p>
<p>Two Ghost 1 blogs:</p>
<ul>
<li>blog.aaronlenoir.com</li>
<li>flightschool.aaronlenoir.com</li>
</ul>
<p>Two home-made .NET Core applications:</p>
<ul>
<li>tracks.aaronlenoir.com</li>
<li>kicker.aaronlenoir.com</li>
</ul>
<p>All of them run on a single Digital Ocean &quot;droplet&quot; (a Virtual Private Server or VPS).</p>
<p>In front of them is an nginx instance configured as a reverse proxy. Nginx is responsible for redirecting requests to the appropriate application, which are not exposed to the internet directly. Additionally nginx takes care of the SSL certificates.</p>
<p>All 4 applications and nginx run as a docker container. Additionally, I run a sixth docker contain responsible for renewing my SSL certificates for each of the hostnames.</p>
<p>To manage the configuration of each container, I use &quot;docker-compose&quot;. The docker-compose configuration can be found on GitHub: <a href="https://github.com/AaronLenoir/aaronlenoir.com/blob/a059df908ddbccc6147ea064a2fdf55ae2296669/docker-compose.yml">https://github.com/AaronLenoir/aaronlenoir.com/blob/a059df908ddbccc6147ea064a2fdf55ae2296669/docker-compose.yml</a></p>
<p>In summary I run 6 docker containers that do all the work. To add a new website I must add another container and associate it with a new hostname so that nginx can forward the requests correctly.</p>
<h2 id="step1runghost2viadockercompose">Step 1: Run Ghost 2 via docker-compose</h2>
<p>I log into my VPS using SSH. It's a UNIX system (I know this).</p>
<p>Ghost says they don't offer an &quot;official&quot; docker image to run the blog. But they do point to an &quot;unofficial&quot; one: <a href="https://hub.docker.com/_/ghost/">https://hub.docker.com/_/ghost/</a>.</p>
<p>Interestingly, the docker page says it's the <em>Official</em> image. So maybe the ghost FAQ is a little outdated and it IS official now.</p>
<p>According to the docs, to run a ghost image on the default port I must run the following:</p>
<pre><code>$ docker run -d --name some-ghost ghost
</code></pre>
<p>I COULD do that, but I wouldn't be able to test it. So I'm going to immediatly put it in my <code>docker-compose.yml</code> file:</p>
<pre><code>  ghost2:
    image: ghost:2-alpine
    restart: always
    depends_on:
      - &quot;nginx-proxy&quot;
    ports:
      - 127.0.0.1:8120:2368
    volumes:
      - ./blog2/data/ghost:/var/lib/ghost/content
    environment:
      - url=https://blog-test.aaronlenoir.com
      - VIRTUAL_HOST=blog-test.aaronlenoir.com
      - LETSENCRYPT_HOST=blog-test.aaronlenoir.com
      - LETSENCRYPT_EMAIL=info@aaronlenoir.com
</code></pre>
<p><code>ghost2</code> is the name of the container.</p>
<p><code>image</code> indicates I wish to use the latest 2.x version. I use alpine because of the lower footprint and it worked well with version 1.</p>
<p><code>restart</code> is set to <code>always</code> that way I'm sure if it's stopped correctly via docker that it gets restarted. I can still stop it using docker-compose.</p>
<p><code>depends-on</code> specifies the <code>nginx-proxy</code> must be running before this container can start up.</p>
<p><code>ports</code> means internally ghost runs on port 2368 (the default) but the container should listen on port 8120 externally.</p>
<p><code>volumes</code> points to the location on my server where all the files should be (theme, images hosted on the blog, logs, database, ...). The first part is the location on my server, the second part is the location inside the container to which it should map. I'm assuming ghost 2 still expects the data in <code>/var/lib/ghost/content</code></p>
<p><code>environment</code> sets a number of environment variables in the container:</p>
<ul>
<li><code>url</code> the URL to use for the blog - may not be necessary for ghost 2</li>
<li><code>VIRTUAL_HOST</code> is used for the nginx-proxy to know that requests for that hostname must go to this container</li>
<li><code>LETSENCRYPT_HOST</code> tells the let's encrypt container the hostname for which it must fetch a certificate</li>
<li><code>LETSENCRYPT_EMAIL</code> is passed to Let's Encrypt when I'm asking a new certificate, they use that to send me mails about the certificate expiration (but I renew automatically - normally)</li>
</ul>
<p>After adding this to my docker-compose.yml file I restart everything. Which results in the download of the image:</p>
<pre><code>Creating network &quot;aaronlenoircom_default&quot; with the default driver
Pulling ghost2 (ghost:2-alpine)...
2-alpine: Pulling from library/ghost
e7c96db7181b: Pull complete
50958466d97a: Pull complete
56174ae7ed1d: Pull complete
284842a36c0d: Pull complete
237455e2fb15: Pull complete
e9505cbbbd44: Pull complete
711e6ff570b7: Extracting [====================================&gt;              ]  4.522MB/6.145MBownload complete
 65.56MB/68.44MBwnload complete
    548B/548B
</code></pre>
<p>After that the following message indicates at least something started:</p>
<pre><code>Creating aaronlenoircom_ghost2_1               ... done
</code></pre>
<h2 id="step2testing">Step 2: Testing</h2>
<p>To test, I usually tell my local machine to resolve the test hostname to the IP address of my VPS.</p>
<p>In windows, that's done by editing the file C:\Windows\Systems32\drivers\etc\hosts</p>
<p>I add the following entry:</p>
<pre><code>82.196.2.207	blog-test.aaronlenoir.com
</code></pre>
<p>With that I entered the url in my browser. This immediatly gives me an error because I don't yet have an SSL certificate. This is normal because Let's Encrypt won't yet know of the hostname.</p>
<p>Of course I can tell Firefox to &quot;accept the risk and continue&quot;, because I know what this site is.</p>
<p>Sadly, I'm greeted by a server error:</p>
<pre><code>500 Internal Server Error
nginx/1.17.3
</code></pre>
<h2 id="step21troubleshooting">Step 2.1: Troubleshooting</h2>
<blockquote>
<p>TL;DR This troubleshooting session goes through a lot of useless stuff that eventually turned out not to be important. You could skip to Step 3 ...</p>
</blockquote>
<p>Since the error is coming from nginx, I'm assuming the nginx proxy server wasn't able to forward my request. I presume this is because the ghost container couldn't start correctly.</p>
<h3 id="dockercomposelogs">Docker-Compose logs</h3>
<p>To check this, I usually run docker-compose in interactive mode, so that I can see what happens in each of the containers:</p>
<pre><code>sudo docker-compose up
</code></pre>
<p>Strangely it looks like ghost 2 is running. This was the only logging I could find at start-up:</p>
<pre><code>ghost2_1                | [2019-09-28 22:12:11] INFO Ghost is running in production...
ghost2_1                | [2019-09-28 22:12:11] INFO Your site is now available on https://blog-test.aaronlenoir.com/
ghost2_1                | [2019-09-28 22:12:11] INFO Ctrl+C to shut down
ghost2_1                | [2019-09-28 22:12:11] INFO Ghost boot 25.093s
</code></pre>
<p>When I visit the site, the logging tells me, not as much as I was hoping for:</p>
<pre><code>nginx-proxy_1           | nginx.1    | blog-test.aaronlenoir.com 84.192.158.84 - - [28/Sep/2019:22:15:11 +0000] &quot;GET / HTTP/2.0&quot; 500 177 &quot;-&quot; &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0&quot;
</code></pre>
<h3 id="ghostlogs">Ghost Logs</h3>
<p>I did remember pointing to a data directory that doesn't yet exist: blog2/data/ghost</p>
<p>It was created and it did generate some logs in the folder: /blog2/data/ghost/logs</p>
<p>In the logs, there are no errors and I can see it created the database correctly:</p>
<pre><code>{&quot;name&quot;:&quot;Log&quot;,&quot;hostname&quot;:&quot;ec4432e19f48&quot;,&quot;pid&quot;:1,&quot;level&quot;:30,&quot;msg&quot;:&quot;Ghost is running in production...&quot;,&quot;time&quot;:&quot;2019-09-28T22:04:14.110Z&quot;,&quot;v&quot;:0}
{&quot;name&quot;:&quot;Log&quot;,&quot;hostname&quot;:&quot;ec4432e19f48&quot;,&quot;pid&quot;:1,&quot;level&quot;:30,&quot;msg&quot;:&quot;Your site is now available on https://blog-test.aaronlenoir.com/&quot;,&quot;time&quot;:&quot;2019-09-28T22:04:14.112Z&quot;,&quot;v&quot;:0}
{&quot;name&quot;:&quot;Log&quot;,&quot;hostname&quot;:&quot;ec4432e19f48&quot;,&quot;pid&quot;:1,&quot;level&quot;:30,&quot;msg&quot;:&quot;Ctrl+C to shut down&quot;,&quot;time&quot;:&quot;2019-09-28T22:04:14.114Z&quot;,&quot;v&quot;:0}
{&quot;name&quot;:&quot;Log&quot;,&quot;hostname&quot;:&quot;ec4432e19f48&quot;,&quot;pid&quot;:1,&quot;level&quot;:30,&quot;msg&quot;:&quot;Ghost boot 33.975s&quot;,&quot;time&quot;:&quot;2019-09-28T22:04:14.117Z&quot;,&quot;v&quot;:0}
</code></pre>
<p>So everything looks hunky-dory. Then why is nginx giving me a 500?!</p>
<h3 id="shellintonginx">Shell into nginx</h3>
<p>I can attach a console to the running nginx proxy server:</p>
<p>Use <code>docker ps</code> to find the container name:</p>
<pre><code>user@docker:~/aaronlenoir.com$ sudo docker ps
CONTAINER ID        IMAGE                                    COMMAND                                                                                      CREATED             STATUS              PORTS                                                                                                          NAMES
8c2683b52ce0        jrcs/letsencrypt-nginx-proxy-companion   &quot;/bin/bash /app/ent                                                                    r…&quot;   16 minutes ago      Up 2 minutes                                                                                                                       aaronlenoircom_nginx-proxy-companon_1
5c899f091de8        ghost:1-alpine                           &quot;docker-entrypoint.                                                                    s…&quot;   16 minutes ago      Up 2 minutes        127.0.0.1:8090-&gt;2368/tcp                                                                                       aaronlenoircom_flightschool_1
023f4d1bed3f        ghost:2-alpine                           &quot;docker-entrypoint.                                                                    s…&quot;   16 minutes ago      Up 2 minutes        127.0.0.1:8120-&gt;2368/tcp                                                                                       aaronlenoircom_ghost2_1
0448fff371fe        kicker                                   &quot;dotnet Kicker.Stat                                                                    s…&quot;   16 minutes ago      Up 2 minutes        127.0.0.1:8100-&gt;80/tcp                                                                                         aaronlenoircom_kicker_1
9200cd720030        tracks                                   &quot;dotnet Mapper.dll&quot;                                                                          16 minutes ago      Up 2 minutes        127.0.0.1:8110-&gt;80/tcp                                                                                         aaronlenoircom_tracks_1
eb8844ca3d79        ghost:1-alpine                           &quot;docker-entrypoint.                                                                    s…&quot;   16 minutes ago      Up 2 minutes        127.0.0.1:8080-&gt;2368/tcp                                                                                       aaronlenoircom_ghost_1
411b175ed115        jwilder/nginx-proxy                      &quot;/app/docker-entryp                                                                    o…&quot;   16 minutes ago      Up 2 minutes        0.0.0.0:80-&gt;80/tcp, 0.0.0.0:443-&gt;4                                                                    43/tcp   aaronlenoircom_nginx-proxy_1
</code></pre>
<p>It seems to be <code>aaronlenoircom_nginx-proxy_1</code>. Then I must execute bash to get a shell in the running container:</p>
<pre><code>user@docker:~/aaronlenoir.com$ sudo docker exec -it aaronlenoircom_nginx-proxy_1 /bin/bash
root@411b175ed115:/app#
</code></pre>
<p>Being in the shell I can see if nginx decided to log anything. But where?</p>
<p>In /etc/nginx/nginx.conf the location of the logs is mentioned:</p>
<pre><code>error_log  /var/log/nginx/error.log warn;
</code></pre>
<p>However, the file itself points to <code>/dev/stderr</code> so I think I would've seen something in the docker-compose test run. So no luck there!</p>
<h3 id="shellintoghost">Shell into ghost</h3>
<p>I can do the same with ghost though:</p>
<pre><code>user@docker:~/aaronlenoir.com$ sudo docker exec -it aaronlenoircom_ghost2_1 /bin/bash
bash-4.4#
</code></pre>
<p>Looking at netstat I can see it is in fact running and listening on port 2368.</p>
<pre><code>bash-4.4# netstat -an
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:2368            0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.11:43769        0.0.0.0:*               LISTEN
udp        0      0 127.0.0.11:44987        0.0.0.0:*
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node Path
</code></pre>
<p>Log files? No we already checked that!?</p>
<h3 id="wgetfromthehost">Wget from the host</h3>
<p>Normally, nginx sends a request to the port 8120. So I can do that too, from the VPS. And that yeilds some more information!</p>
<pre><code>user@docker:~/aaronlenoir.com$ wget localhost:8120
--2019-09-28 22:44:35--  http://localhost:8120/
Resolving localhost (localhost)... 127.0.0.1
Connecting to localhost (localhost)|127.0.0.1|:8120... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: https://localhost:8120/ [following]
--2019-09-28 22:44:35--  https://localhost:8120/
Connecting to localhost (localhost)|127.0.0.1|:8120... connected.
OpenSSL: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol
Unable to establish SSL connection.
</code></pre>
<p>What we see is that ghost sees my request, but tells me to come back via &quot;https&quot;. Since I did not configure https this - of course - won't work.</p>
<p>Since nginx takes care of this, the internal applications shouldn't be doing this. I'm a little surprised ghost has https enabled by default - BUT THAT'S GOOD, usually.</p>
<h2 id="step22letghost2allowhttp">Step 2.2: Let Ghost 2 allow HTTP</h2>
<p>I'll shell back into the ghost container:</p>
<pre><code>user@docker:~/aaronlenoir.com$ sudo docker exec -it aaronlenoircom_ghost2_1 /bin/bash
bash-4.4#
</code></pre>
<p>I wanted to use ghost cli to see the config but:</p>
<pre><code>bash-4.4# ghost config
You can't run commands as the 'root' user.
Switch to your regular user, or create a new user with regular account privileges and use this user to run 'ghost config'.
For more information, see https://docs.ghost.org/install/ubuntu/#create-a-new-user-.
</code></pre>
<p>Since I'm in docker, I don't really have a normal user.</p>
<p>Where is the config file? Oh it's here: /var/lib/ghost/config.production.json</p>
<pre><code>bash-4.4# cat config.production.json
{
  &quot;url&quot;: &quot;http://localhost:2368&quot;,
  &quot;server&quot;: {
    &quot;port&quot;: 2368,
    &quot;host&quot;: &quot;0.0.0.0&quot;
  },
  &quot;database&quot;: {
    &quot;client&quot;: &quot;sqlite3&quot;,
    &quot;connection&quot;: {
      &quot;filename&quot;: &quot;/var/lib/ghost/content/data/ghost.db&quot;
    }
  },
  &quot;mail&quot;: {
    &quot;transport&quot;: &quot;Direct&quot;
  },
  &quot;logging&quot;: {
    &quot;transports&quot;: [
      &quot;file&quot;,
      &quot;stdout&quot;
    ]
  },
  &quot;process&quot;: &quot;systemd&quot;,
  &quot;paths&quot;: {
    &quot;contentPath&quot;: &quot;/var/lib/ghost/content&quot;
  }
}
</code></pre>
<p>I don't immediatly see a setting forcing or not forcing https?</p>
<p>I did set the URL environment variable to <code>https://test-blog.aaronlenoir.com</code> so maybe that's why it does https. I'll change it to http in my <code>docker-compose.yml</code>.</p>
<p>That seemed to yield another result:</p>
<pre><code>user@docker:~/aaronlenoir.com$ wget localhost:8120
--2019-09-28 23:04:35--  http://localhost:8120/
Resolving localhost (localhost)... 127.0.0.1
Connecting to localhost (localhost)|127.0.0.1|:8120... connected.
HTTP request sent, awaiting response... 200 OK
Length: 21826 (21K) [text/html]
Saving to: ‘index.html’

index.html                           100%[======================================================================&gt;]  21.31K  --.-KB/s    in 0s

2019-09-28 23:04:36 (345 MB/s) - ‘index.html’ saved [21826/21826]
</code></pre>
<p>But <strong>still</strong> I receive an error 500 from nginx! But we now know the blog is running and NOT redirecting to https ...</p>
<h3 id="step23workingwithouthttps">Step 2.3: Working without HTTPS?!</h3>
<p>I took everything down and then ran</p>
<pre><code>sudo docker-compose up
</code></pre>
<p>And now everything is working! But over HTTP. Why am I redirected to HTTP?</p>
<p>I already have multiple apps running internally on http and externally on https and I've never seen this.</p>
<p>I will first try to register the hostname so that maybe I get my SSL certificate sorted in nginx!</p>
<h2 id="step3registerhostname">Step 3: Register hostname</h2>
<p>I own the domain aaronlenoir.com. I can add a subdomain in my administration panel with Namecheap:</p>
<p><img src="https://blog.aaronlenoir.com/content/images/2019/09/2019-09-29-01_31_19-Advanced-DNS---Brave.png" alt="2019-09-29-01_31_19-Advanced-DNS---Brave"></p>
<p>Good news! Doing that solved all the problems.</p>
<h3 id="step31moretroubleshooting">Step 3.1: More troubleshooting</h3>
<p>I now want to set the url in docker-compose.yml back to https to see if it still works ...</p>
<p>...</p>
<p>Success, everything still works!</p>
<p><img src="https://blog.aaronlenoir.com/content/images/2019/09/2019-09-29-01_39_20-Ghost.png" alt="2019-09-29-01_39_20-Ghost"></p>
<h2 id="conclusion">Conclusion</h2>
<p>I thought this was going to be a short post.</p>
<p>And - apart from the two hour debug session - that's how <em>eAsY</em> it was to add a new Ghost 2 instance to my blog.</p>
<p>😭</p>
<!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Draw GPS Track on OpenStreetMap]]></title><description><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>I made a thing for my <a href="https://flightschool.aaronlenoir.com">other blog</a> to show the GPS track of my flights.</p>
<p>I thought I'd write down how I did it.</p>
<h2 id="recordinggpstracks">Recording GPS Tracks</h2>
<p>I use an app for iOS called <a href="https://apps.apple.com/nl/app/mytracks-the-gps-logger/id358697908">&quot;myTracks&quot;</a>. In the free version you can start a recording and then stop</p>]]></description><link>https://blog.aaronlenoir.com/2019/09/25/draw-gps-track-on-openstreetmap/</link><guid isPermaLink="false">5d912fbe4cd92f0001765ad0</guid><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Wed, 25 Sep 2019 22:34:23 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>I made a thing for my <a href="https://flightschool.aaronlenoir.com">other blog</a> to show the GPS track of my flights.</p>
<p>I thought I'd write down how I did it.</p>
<h2 id="recordinggpstracks">Recording GPS Tracks</h2>
<p>I use an app for iOS called <a href="https://apps.apple.com/nl/app/mytracks-the-gps-logger/id358697908">&quot;myTracks&quot;</a>. In the free version you can start a recording and then stop the recording.</p>
<p>You will be able to view the track in the app itself, but you can also export it and e-mail yourself a file in the &quot;gpx&quot; format. Gpx stands for &quot;GPS Exchange Format&quot; and it's XML based.</p>
<h2 id="dowloadinggpxfiles">Dowloading GPX files</h2>
<p>I fetch the GPX file from a URL using the standard <a href="https://scotch.io/tutorials/how-to-use-the-javascript-fetch-api-to-get-data">&quot;Fetch API&quot;</a></p>
<pre><code>        fetch(trackPath)
            .then(function (response) {
                return response.text();
            }).then(function (gpxData) {
                // Parse gpxData here ...
            });
</code></pre>
<h2 id="parsinggpxfiles">Parsing GPX files</h2>
<p>I use a JavaScript library <a href="https://github.com/Luuka/gpx-parser">&quot;gpxParser&quot;</a> that I found on GitHub.</p>
<p>The parsing works like so:</p>
<pre><code>let gpx = new gpxParser();
gpx.parse(gpxData);
</code></pre>
<h2 id="embeddingamap">Embedding a Map</h2>
<p>I need a map to draw the gps track on. For this I use <a href="https://leafletjs.com/">leaflet</a> to embed an OpenStreetMap map.</p>
<p>With the following script:</p>
<pre><code>&lt;script src=&quot;https://unpkg.com/leaflet@1.4.0/dist/leaflet.js&quot;
            integrity=&quot;sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg==&quot;
            crossorigin=&quot;&quot;&gt;&lt;/script&gt;
</code></pre>
<p>This CSS</p>
<pre><code>&lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/leaflet@1.4.0/dist/leaflet.css&quot;
          integrity=&quot;sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA==&quot;
          crossorigin=&quot;&quot; /&gt;
</code></pre>
<p>This HTML</p>
<pre><code>&lt;div id=&quot;map&quot;&gt;&lt;/div&gt;
</code></pre>
<p>And this piece of JavaScript to initialize the map:</p>
<pre><code>let mymap = L.map('map');

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&amp;copy; &lt;a href=&quot;https://www.openstreetmap.org/copyright&quot;&gt;OpenStreetMap&lt;/a&gt; contributors',
    maxZoom: 50
}).addTo(mymap);
</code></pre>
<h2 id="drawingthetrack">Drawing the Track</h2>
<p>After parsing the GPX data and embedding a map, it's possible to draw the track:</p>
<pre><code>function drawTrack(track) {
    let coordinates = track.points.map(p =&gt; [p.lat.toFixed(5), p.lon.toFixed(5)]);

    var polyline = L.polyline(coordinates, { weight: 6, color: 'darkred' }).addTo(mymap);

    // zoom the map to the polyline
    mymap.fitBounds(polyline.getBounds());
}
</code></pre>
<p>In the first line <code>track.points.map(p =&gt; [p.lat.toFixed(5), p.lon.toFixed(5)])</code> I &quot;map&quot; each point in the track to a new object that the Leaflet library understands: a two element array (latitude and lontitude).</p>
<p>To be clear, the &quot;track&quot; input is what the gpx library has parsed. From earlier:</p>
<pre><code>let gpx = new gpxParser();
gpx.parse(gpxData);
drawTrack(gpx.tracks[0]);
</code></pre>
<h2 id="example">Example</h2>
<p>Here's an example of what the embedded map looks like. Note it also contains two charts for altitude and groundspeed. These use the <a href="https://www.highcharts.com/">HighCharts</a> component - but I'll talk about that in some other post.</p>
<p>I also packages this into an app by itself, so it can be embedded by using a URL (in the example: <a href="https://tracks.aaronlenoir.com/?track=tracks/flights/flight-010-20190917.gpx">https://tracks.aaronlenoir.com/?track=tracks/flights/flight-010-20190917.gpx</a></p>
<p>You can see the URL of the gpx file is passed to the track variable.</p>
<iframe id="inlineFrameExample" title="Inline Frame Example" width="900" height="1600" src="https://tracks.aaronlenoir.com/?track=tracks/flights/flight-010-20190917.gpx">
</iframe><!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[25 Minutes of Rust (Part 7)]]></title><description><![CDATA[In this episode I finally finish the guessing game by adding a loop to allow multiple guesses.]]></description><link>https://blog.aaronlenoir.com/2019/09/25/25-minutes-of-rust-part-7/</link><guid isPermaLink="false">5d912fbe4cd92f0001765ace</guid><category><![CDATA[25 Minutes of Rust]]></category><category><![CDATA[english]]></category><category><![CDATA[Rust]]></category><category><![CDATA[Visual Studio Code]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Wed, 25 Sep 2019 22:09:00 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>In this episode I finally finish the guessing game from the book <a href="https://doc.rust-lang.org/book/second-edition/ch02-00-guessing-game-tutorial.html">&quot;The Rust Programming Language&quot;</a> by adding a loop to allow multiple guesses.</p>
<p>I'm doing a series of 25 minute sessions where I try to get familiar with the Rust programming language. The blogposts in these series are the notes I took of the lessons learned along the way.</p>
<p><strong>Warning to the reader:</strong> As always in this series, these are notes I take as I'm learning. They reflect my current understanding and may be incorrect!</p>
<h2 id="looping">Looping</h2>
<p>I was expecting a <code>while</code> loop, based on my experience with other languages. For example:</p>
<pre><code>while (x != 1) { ... }
</code></pre>
<p>This would execute the code inside the brackets (<code>{</code> and <code>}</code>) until x was no longer equal to 1.</p>
<p>But instead, the book suggests using a <code>loop</code> statement:</p>
<pre><code>loop {
   ...
}
</code></pre>
<p>This runs the code in the brackets until that code tells it to stop, using a <code>break</code> statement.</p>
<p>In the <code>match</code> where the result is evaluated we execute the <code>break</code> statement if the guess was correct:</p>
<pre><code>match guess.cmp(&amp;secret_number) {
    Ordering::Less =&gt; println!(&quot;Too small.&quot;),
    Ordering::Greater =&gt; println!(&quot;Too big.&quot;),
    Ordering::Equal =&gt; {
        println!(&quot;You win!&quot;);
        break;
    }
}
</code></pre>
<p>I presume <code>while</code> isn't used, because variables are typically not mutable. The variable I check the while condition on (like <code>x != 1</code>) requires <code>x</code> to be mutable. Maybe there is a <code>while</code> statement and I while learn about it later.</p>
<h2 id="read_linecanappend">read_line can append</h2>
<p>Originally, I made the mistake of leaving the following statement outside of the <code>loop</code>:</p>
<pre><code>let mut guess = String::new();
</code></pre>
<p>Then every time the player entered a guess the value was appended to the existing guess:</p>
<pre><code>Please input your guess.
5
You guessed: 5

Too small.
4
You guessed: 5
4

3
You guessed: 5
4
3
</code></pre>
<p>This teaches me that if you use <code>io::stdin().read_line</code> with a mutable <code>String</code> variable, whatever the user inputs is appended to the existing string.</p>
<p>Note that this was only possible because my <code>parse</code> call now also handles the <code>Err</code> result by executing continue.</p>
<pre><code>let guess: u32 = match guess.trim().parse() {
    Ok(num) =&gt; num,
    Err(_) =&gt; continue,
};
</code></pre>
<h2 id="matchexecutescode">Match executes code</h2>
<p>The match code for the <code>parse</code> call showed something interesting too.</p>
<p>At first, I thought a match would produce a value:</p>
<pre><code>Ok(num) =&gt; num,
</code></pre>
<p>This would read as: <em>If &quot;Ok&quot; then pass num as the result of the match.</em> Which is actually also what it does.</p>
<p>But considering this:</p>
<pre><code>Err(_) =&gt; continue,
</code></pre>
<p>I would read this as: <em>If &quot;Err&quot; then pass continue as the result of the match.</em> But that is not at all what happens.</p>
<p>It actually says: <em>If &quot;Err&quot; then execute the following statement and the result of that statement is the result of the match.</em></p>
<h3 id="pathstatement">Path statement?</h3>
<p>The above means, for the first statement <code>Ok(num) =&gt; num,</code> the result of the match statement would be <code>num</code>.</p>
<p>This implies that a line with the following statement would be a valid statement:</p>
<pre><code>`guess;`
</code></pre>
<p>Assuming <code>guess</code> is an existing variable.</p>
<p>And it is actually valid and it compiles. Although it does produce a rather cryptic warning: <code>path statement with no effect</code></p>
<p><img src="https://blog.aaronlenoir.com/content/images/2018/08/2018-08-31-00_04_45-Window.png" alt="2018-08-31-00_04_45-Window"></p>
<p>I can understand it warns about a useless statement, because it has no side-effects. But what I don't understand is what a <em>path statement</em> is. I've googled around for the term but haven't found anything. So if somebody can point me in a direction that would be super nice.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This was a quick session, but still I've learned that a match statement actually executes code.</p>
<p>I've also learned that read_line can append new text to an existing mutable String.</p>
<p>And finally, I've learned the important <code>loop</code> statement.</p>
<p>Next up in the book are no longer tutorials, so my way of working will have to change a little I think. We'll see!</p>
<!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Brainf*** interpreter optimization excercise (part 4)]]></title><description><![CDATA[In this post the brainf*** interpreter is further optimized by translating the source code to an internal instruction set.]]></description><link>https://blog.aaronlenoir.com/2019/09/25/brainf-interpreter-optimization-excercise-part-4/</link><guid isPermaLink="false">5d912fbe4cd92f0001765acc</guid><category><![CDATA[Bf Interpreter]]></category><category><![CDATA[english]]></category><category><![CDATA[c#]]></category><category><![CDATA[.net]]></category><category><![CDATA[interpreter]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Wed, 25 Sep 2019 22:09:00 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>In this post the brainf*** interpreter is further optimized by translating the source code to an internal instruction set.</p>
<p>In <a href="https://blog.aaronlenoir.com/2018/06/24/brainf-interpreter-optimization-excercise-part-3">Part 3</a> a second &quot;jump table&quot; was introduced to make the interpreter a little faster.</p>
<p>The result was an interpreter that could generate the mandelbrot example in 43 seconds.</p>
<p>While useful, it wasn't very creative compared to the earlier optimization. The following optimization is a lot more impactful.</p>
<h2 id="preprocessing">Preprocessing</h2>
<p>The idea is that a lot of time is spent reading through the code itself and that code is in memory as a string.</p>
<p>We have to go through that code a lot during loops, reading characters from the string. Additionally, we have to maintain those jump tables from the previous optimizations. This all adds up.</p>
<p>What if we run through the code once, record its meaning into instruction sets? Then we can loop through instructions that contain all the information we need.</p>
<p>If we do that we won't need to figure out every time how much to add or substract somewhere, we'll just know from looking at a single Instruction.</p>
<h2 id="instructionset">Instruction Set</h2>
<p>Instead of working with &quot;brainf***&quot; directly, we'll make our own instruction set and build up our instructions from the original code.</p>
<p>The plan is to have a list of <code>Instruction</code> objects that contain all the necessary information to perform a certain instruction. This Instruction will need a &quot;Type&quot; and an optional numerical parameter.</p>
<p>For example: <em>Add 5 to current memory location</em> is an instruction of the type &quot;Add&quot; with a &quot;5&quot; as parameter.</p>
<p>Each instruction will have a type, indicating what needs to be done:</p>
<pre><code>private enum InstructionType
{
    None = 0,
    Add, Subtract, ShiftLeft, ShiftRight, Print, Read, BeginLoop, EndLoop
}
</code></pre>
<p>Of these, <code>Add</code>, <code>Subtract</code>, <code>ShiftLeft</code> and <code>ShiftRight</code> have parameters. The others don't.</p>
<p>The <code>Instruction</code> is a C# class:</p>
<pre><code>private class Instruction
{
    public InstructionType Type { get; private set; }

    public int Parameter { get; set; }

    public Instruction(InstructionType type) : this(type, 0)
    { }

    public Instruction(InstructionType type, int parameter)
    {
        Type = type;
        Parameter = parameter;
    }
}
</code></pre>
<p>After looping <strong>once</strong> through the code we'll have a list of <code>Instruction</code> objects.</p>
<h2 id="runningthecode">Running the Code</h2>
<p>Running the code is now a three step process:</p>
<ul>
<li>Strip all irrelevant characters from the source</li>
<li>Build the Instruction set</li>
<li>Execute the Instructions</li>
</ul>
<pre><code>public void Run(FileStream source)
{
    var strippedSource = Strip(source);

    var instructions = BuildInstructions(strippedSource);

    Execute(instructions);
}
</code></pre>
<p>The step to strip all irrelevant characters is so that we don't have to worry about skipping those characters when reading through the code.</p>
<h2 id="buildinginstructions">Building Instructions</h2>
<p>Some bf instructions can be directly converted to an <code>Instruction</code>. For example <code>Print</code> and <code>Read</code>:</p>
<pre><code>case '.':
    instructions.Add(new Instruction(InstructionType.Print));
    break;
case ',':
    instructions.Add(new Instruction(InstructionType.Read));
    break;
</code></pre>
<p>For instruction <code>Add</code> (+), <code>Subtract</code> (-), <code>ShiftLeft</code> (&lt;) and <code>ShiftRight</code> (&gt;) we also need a parameter to know how much to add, substract or shift.</p>
<p>When we encounter such a character, we count how many times the same character follows, and we progress further in the code by that many steps.</p>
<p>For <code>Add</code> that looks like this:</p>
<pre><code>case '+':
    instructions.Add(new Instruction(InstructionType.Add, CountSeries(reader)));
    break;
</code></pre>
<p>The <code>CountSeries</code> method progresses the reader until it encounters a character different from <code>+</code> and returns how many times it saw that character being repeated.</p>
<pre><code>private int CountSeries(CharReader cr)
{
    var count = 0;
    var currentChar = cr.GetChar();
    while (cr.HasCharacters() &amp;&amp; cr.GetChar() == currentChar)
    {
        count++;
        cr.Forward();
    }
    cr.Back();
    return count;
}
</code></pre>
<h3 id="readingtheloopinstructions">Reading the Loop Instructions</h3>
<p>For the loops (<code>[</code> and <code>]</code>) we're still using the jumptable from before. But only when building the instruction sets.</p>
<p>For the Instruction itself, the Parameter for <code>BeginLoop</code> indicates the location in the code to jump to when the loop can be skipped. For <code>EndLoop</code> it's the location where to jump to if we need to continue the loop.</p>
<p>The tricky part is the &quot;location to jump to&quot; refers to one of the instructions in the instruction set and no longer a location in the original code. Luckily we can use the Stack from Part 2 again:</p>
<pre><code>case '[':
    instructions.Add(new Instruction(InstructionType.BeginLoop));
    jumpTable.Push(instructions.Count - 1);
    break;
case ']':
    var beginPosition = jumpTable.Pop();
    var beginInstruction = instructions[beginPosition];
    instructions.Add(new Instruction(InstructionType.EndLoop, beginPosition));
    beginInstruction.Parameter = instructions.Count - 1;
    break;
</code></pre>
<h2 id="executingtheinstructions">Executing the Instructions</h2>
<p>With an array of <code>Instruction</code> objects, actualy executing the code becomes relatively straight-forward. This is good because some of the Instructions will be executed many times. Things that happen a lot are preferably really simple.</p>
<p>For example, the <code>Add</code> instruction is now:</p>
<pre><code> case InstructionType.Add:
    memory[pointer] += (byte)instruction.Parameter;
    break;
</code></pre>
<p>Even the most complicated part, the loops aren't too bad:</p>
<p>We either jump to the specified location or continue to the next instruction, depending on the condition.</p>
<pre><code> case InstructionType.BeginLoop:
    if (memory[pointer] == 0)
    {
        instructionPointer = instruction.Parameter;
    }
    break;
 case InstructionType.EndLoop:
    if (memory[pointer] != 0)
    {
        instructionPointer = instruction.Parameter;
    }
    break;
</code></pre>
<h2 id="results">Results</h2>
<p>The result is the mandelbrot can now be generated in 10 seconds. Quite something knowing our previous optimization left us at 43 seconds.</p>
<h2 id="drawbacks">Drawbacks</h2>
<p>There is a drawback to this approach: it requires looping through the whole code before we run, and keeping everything in memory as an <code>Instruction</code> object. So we put a little more strain on the memory. But in this case that's not too bad.</p>
<p>If the source code is really large, this could become a bigger issue.</p>
<p>This optimization required significant changes to the existing code.</p>
<h2 id="conclusion">Conclusion</h2>
<p>There's still more room for optimization, but going from 43 s to 10 s is quite something.</p>
<p>It shows that a good preparation can really make a lot of difference. In this case the preparation was building a set of useful instructions. This extra work payed off in the end.</p>
<p>Please find the full code here on Github: <a href="https://github.com/AaronLenoir/BfInterpreter/blob/master/src/BfInterpreter/Optimization05.cs">Optimization05.cs</a>etty sweet.</p>
<!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[SKYGGE: AI Assisted Pop Music]]></title><description><![CDATA[I found out about SKYGGE and its pop album "Hello World" with AI assisted compositions that actually sound good.]]></description><link>https://blog.aaronlenoir.com/2019/09/25/ai-assisted-pop-music/</link><guid isPermaLink="false">5d912fbe4cd92f0001765acf</guid><category><![CDATA[music]]></category><category><![CDATA[english]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Wed, 25 Sep 2019 22:08:00 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>I was looking around for some innovative music and apart from the usual atonal bleepedyblob stuff I was getting a little dissapointed.</p>
<p>Until I found out about <a href="https://www.helloworldalbum.net/skygge-biography-eng/">SKYGGE</a>. A project by composer Benoit Carré.</p>
<p>It's a collaboration between Benoit, several guest artists and AI tools called <a href="https://www.helloworldalbum.net/about-hello-world/"><em>Flow Machines</em></a>.</p>
<p>The result are good sounding pop songs, but composed and performed in part by Benoit's flow machine.</p>
<p>I'm surprised I haven't yet found more AI assisted music composition projects (as far as I can tell) because it sounds like an interesting fit.</p>
<p>I'd love to see and hear some listenable <strong>live</strong> AI - artist collaborations in the future. If only I had musical and AI abilities!</p>
<p>But for now, SKYGGE has some awesome songs considering the background. Especially this Hello Shadow, in collaboration with none other than <a href="https://www.youtube.com/watch?v=eOZLDQm9c2E">Stromae</a> and <a href="https://www.youtube.com/watch?v=Vnoz5uBEWOA">Kiesza</a>:</p>
<p>Check it out:</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/QgD9y9aBhSc" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<p>They released a full album: &quot;<a href="https://www.helloworldalbum.net/">Hello World</a>&quot;. Available on streaming services (I didn't find it on CD or LP anywhere).</p>
<p>On the website for the album, &quot;Hello World&quot;, there is detailed information for each track: <a href="https://www.helloworldalbum.net/track-by-track/">Track by Track (helloworldalbum.com)</a></p>
<!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Adding Disqus comments to Ghost]]></title><description><![CDATA[In this post I describe how I modified my Ghost Template to include Disqus comments in my blog posts.]]></description><link>https://blog.aaronlenoir.com/2018/09/02/adding-disqus-comments-to-ghost/</link><guid isPermaLink="false">5d912fbe4cd92f0001765acd</guid><category><![CDATA[blog]]></category><category><![CDATA[english]]></category><category><![CDATA[good to know]]></category><category><![CDATA[ghost]]></category><dc:creator><![CDATA[Aaron Lenoir]]></dc:creator><pubDate>Sun, 02 Sep 2018 13:00:00 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><!--kg-card-begin: markdown--><p>In this post I describe how I modified my Ghost Template to include Disqus comments in my blog posts.</p>
<p>The blog platform I use is a self-hosted version of <a href="https://ghost.org/">Ghost</a>.</p>
<p>One of the drawbacks is it doesn't have a built-in comment system. However, there exist some third party services that let you add comments to your site.</p>
<p>One such service is <a href="https://disqus.com/">Disqus</a>.</p>
<p>Sadly, adding this to my Ghost instance wasn't just a matter of checking a checkbox and filling in some parameters.</p>
<h2 id="settingupdisqus">Setting up Disqus</h2>
<p>I actually already had Disqus set up from older blogs. So I was able to use the existing Ghost project. In fact, the posts that got migrated from my previous blogs still have the comments from when I was running it on OpenShift (long time ago).</p>
<p>But to set it up, there's a guide here: <a href="https://disqus.com/admin/create/">Install Disqus on a Website</a> (the links requires you to be logged in to Disqus)</p>
<h2 id="updatingthetheme">Updating the theme</h2>
<p>To add Disqus I had to modify the Theme I use a little. I use a slightly modified version of the standard Ghost theme, called &quot;<a href="https://github.com/AaronLenoir/Casper">Casper</a>&quot;.</p>
<p>Disqus requires me to add a small block of HTML / JavaScript to the page where I want to embed comments. This block of code make the browser load the comment form from Disqus directly.</p>
<p>In my case, I want this under every blogpost.</p>
<p>To do that, in the template I must update the &quot;post.hbs&quot; file and add the block of code somewhere. The standard Ghost theme's &quot;post.hbs&quot; file contains a place to put this already, by default it looks like this:</p>
<pre><code>{{!--
&lt;section class=&quot;post-full-comments&quot;&gt;
    If you want to embed comments, this is a good place to do it!
&lt;/section&gt;
--}}
</code></pre>
<p>So I uncommented the section and replaced the text with the Disqus code block.</p>
<p>The disqus block looks like below:</p>
<pre><code>&lt;div id=&quot;disqus_thread&quot;&gt;&lt;/div&gt;
&lt;script&gt;
    var disqus_config = function () {
        this.page.url = '{{url absolute=&quot;true&quot;}}';  // Replace PAGE_URL with your page's canonical URL variable
        this.page.identifier = 'ghost-{{comment_id}}'; // Replace PAGE_IDENTIFIER with your page's unique identifier variable
    };
    (function() { // DON'T EDIT BELOW THIS LINE
        var d = document, s = d.createElement('script');
        s.src = 'https://blog-aaronlenoir.disqus.com/embed.js';
        s.setAttribute('data-timestamp', +new Date());
        (d.head || d.body).appendChild(s);
    })();
&lt;/script&gt;
&lt;noscript&gt;Please enable JavaScript to view the &lt;a href=&quot;https://disqus.com/?ref_noscript&quot;&gt;comments powered by Disqus.&lt;/a&gt;&lt;/noscript&gt;
</code></pre>
<p>When you have an account at Disqus, it will provide you with the appropriate code. The only thing Ghost specific is the URL and Identifier:</p>
<ul>
<li>URL: <code>'{{url absolute=&quot;true&quot;}}'</code></li>
<li>Identifier: <code>'ghost-{{comment_id}}'</code></li>
</ul>
<p>This is information I got from the Ghost documentation: <a href="https://help.ghost.org/article/15-disqus">Apps &amp; Integrations: Disqus</a></p>
<h2 id="fetchingthenewtheme">Fetching the new Theme</h2>
<p>On the server where I run my Ghost blog I have a script to stop and start the webserver(s). Each application runs in a docker container, fronted by an Nginx (OSS webserver) container.</p>
<p>Every time I start the blog, first the latest version of the theme is fetched from github:</p>
<pre><code>git -C blog/data/ghost/themes/casper-custom pull
</code></pre>
<h2 id="csp">CSP</h2>
<p>I have implement &quot;Content Security Policy&quot; on my website. This informs the browser from which domain my website is allowed to fetch data. By default loading the disqus platform wouldn't have been allowed, so I had to add the domain in my nginx CSP configuration:</p>
<pre><code>add_header Content-Security-Policy &quot;script-src 'self' 'unsafe-inline' https://code.jquery.com blog-aaronlenoir.disqus.com&quot;;
</code></pre>
<h2 id="possibleimprovements">Possible Improvements</h2>
<p>I would like to configure the comment section to only load on demand: <em>click here to load the comments</em></p>
<p>I had that in my previous version of the blog, so I will add that back in the future.</p>
<h2 id="code">Code</h2>
<p>Find my customized Casper template here: <a href="https://github.com/AaronLenoir/Casper">github.com/AaronLenoir/Casper</a></p>
<p>Find my nginx configuration here: <a href="https://github.com/AaronLenoir/aaronlenoir.com/blob/master/nginx/vhost.d/blog.aaronlenoir.com">github.com/AaronLenoir/aaronlenoir.com/blob/master/nginx/vhost.d</a></p>
<p>Find the complete set up for my containers here: <a href="https://github.com/AaronLenoir/aaronlenoir.com">github.com/AaronLenoir/aaronlenoir.com</a></p>
<h2 id="conclusion">Conclusion</h2>
<p>Adding Disqus comments to a Ghost blog requires modifications to the theme you are using.</p>
<p>I'm not sure what the SaaS solution of Ghost Pro offers. It's quite possible over there it can simply be checked on or off, but I haven't actually looked at that.</p>
<!--kg-card-end: markdown--><!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>