diff --git a/src/assets/js/app.js b/src/assets/js/app.js
--- a/src/assets/js/app.js
+++ b/src/assets/js/app.js
@@ -1 +1,329 @@
-$(document).foundation();
+/*  -------------------------------------------------------------
+    Nasqueron infrastructure
+    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    Project:        Nasqueron
+    Author:         Sébastien Santoro aka Dereckson
+    Dependencies:   jQuery
+    Filename:       app.js
+    Licence:        CC-BY 4.0, MIT, BSD-2-Clause (multi-licensing)
+    -------------------------------------------------------------    */
+
+/*  -------------------------------------------------------------
+    Table of contents
+    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+ :: Servers log
+ :: Code to run when document is ready
+
+ */
+
+var ServersLog = function (url, container) {
+    var serversLog = {
+
+        ///
+        /// Private properties
+        ///
+
+        /**
+         * A JQuery selector expression to a DOM element to publish the log in.
+         *
+         * @var string
+         */
+        container: "",
+
+        /**
+         * The URL to fetch the log.
+         *
+         * @var string
+         */
+        url: "",
+
+        /**
+         * The log entries fetched.
+         *
+         * @var array
+         */
+        logEntries: [],
+
+        ///
+        /// Constructor
+        ///
+
+        /**
+         * Initializes an instance of this object.
+         *
+         * @param url The URL to the log
+         * @param container The DOM element JQuery selector where to write
+         */
+        load: function (url, container) {
+            this.url = url;
+            this.container = container;
+            this.refreshData();
+        },
+
+        ///
+        /// Main methods
+        ///
+
+        /**
+         * Fetches log entries. That will trigger an UI refresh once fetched.
+         */
+        refreshData: function () {
+            this.fetchLogEntries();
+        },
+
+        /**
+         * Refreshes the UI from the content in logEntries array.
+         */
+        refreshUI: function () {
+            $(this.container).html(this.formatEntries());
+        },
+
+        ///
+        /// Data helper methods
+        ///
+
+        /**
+         * Fetches the log entries at the log URL, fills logEntries array,
+         * refreshes the UI.
+         */
+        fetchLogEntries: function () {
+            $.getJSON(this.url, function(data) {
+                serversLog.logEntries = data.reverse(); // Log is chronological.
+                serversLog.refreshUI();
+            });
+        },
+
+        ///
+        /// UI helper methods
+        ///
+
+        formatEntries: function () {
+            var currentDate = "";
+            var currentMonth = "";
+
+            var entries = "";
+            for (var i = 0; i < this.logEntries.length; i++) {
+                var entry = this.logEntries[i];
+                var date = this.getDate(entry.date);
+                if (date != currentDate) {
+                    // Month heading
+                    var month = this.getMonth(entry.date);
+                    if (month != currentMonth) {
+                        entries += this.formatMonthHeadings(entry.date);
+                        currentMonth = month;
+                    }
+
+                    // Day heading
+                    entries += this.formatDateHeadings(date);
+                    currentDate = date;
+                }
+                entries += this.formatEntry(entry);
+            }
+            return entries;
+        },
+
+        formatEntry: function (entry) {
+            var format = `<p class="log-entry">
+                <span class="log-component secondary label">%%component%%</span>
+                <span class="log-time">%%date%%</span>
+                <span class="log-emitter">%%emitter%%</span>:
+                <span class="log-message">%%message%%</span>
+            </p>`;
+            return format
+                .replace("%%component%%", entry.component)
+                .replace("%%date%%", this.getTime(entry.date))
+                .replace("%%emitter%%", entry.emitter)
+                .replace("%%message%%", this.formatMessage(entry.entry))
+                ;
+        },
+
+        ///
+        /// Formats date headings
+        ///
+
+        /**
+         * The month names.
+         *
+         * @var array
+         */
+        monthNames: [
+            "January", "February", "March",
+            "April", "May", "June",
+            "July", "August", "September",
+            "October", "November", "December"
+        ],
+
+        /**
+         ** Gets a day headings element.
+         *
+         * @param date The date to print
+         * @returns {string} The day heading
+         */
+        formatDateHeadings: function (date) {
+            return '<h3>' + date + '</h3>';
+        },
+
+        /**
+         * Gets a month heading element.
+         *
+         * @param timestamp The date to parse
+         * @returns {string} The month heading
+         */
+        formatMonthHeadings: function (timestamp) {
+            var date = new Date(timestamp);
+            var month = this.monthNames[date.getMonth()];
+
+            return "<h2>" + month + "</h2>";
+        },
+
+        ///
+        /// Format messages helper functions
+        ///
+
+        /**
+         * @var array
+         */
+        messageDecorators: [
+            {
+                // SHA-1 Git commit hashes
+                re: /\b([0-9a-f]{7,40})\b/g,
+
+                /**
+                 * Callback method to linkify when needed a SHA-1 hash.
+                 *
+                 * @param match The expression matched by the regexp
+                 * @param p1 The SHA-1 hash candidate
+                 * @param offset The position p1 has been found
+                 * @param string The full string p1 has been found
+                 * @returns {string}
+                 */
+                replaceBy: function (match, p1, offset, string) {
+                    if (!serversLog.isHash(p1)) {
+                        return p1;
+                    }
+
+                    return '<a href="https://devcentral.nasqueron.org/search/?query=%1">%1</a>'
+                        .replace(/%1/g, p1);
+                },
+            },
+            {
+                // Tasks, reviews and pastes
+                re: /\b([TDP][0-9]{1,6}(\#[0-9]{1,10})?)\b/g,
+                replaceBy: '<a href="https://devcentral.nasqueron.org/$1">$1</a>',
+            },
+            {
+                // Repositories callsigns
+                re: /\br([A-Z]{3,32})\b/g,
+                replaceBy: '<a rel="repository" href="https://devcentral.nasqueron.org/diffusion/$1/">r$1</a>',
+            },
+            {
+                // Commits with callsigns
+                re: /\br([A-Z]{3,32}[0-9a-f]{7,40})\b/g,
+                replaceBy: '<a rel="commit" href="https://devcentral.nasqueron.org/r$1">r$1</a>',
+            },
+            {
+                // Code (or SQL query parameter)
+                re: /`(.*?)`/g,
+                replaceBy: '<code>$1</code>'
+            }
+        ],
+
+        /**
+         * Whitelist of known hexadecimal words.
+         *
+         * @var array
+         */
+        hexadecimalKnownWord: [
+            "added",
+            "ed25519",
+        ],
+
+        /**
+         * Determines if an expression matches a whitelisted hexadecimal word.
+         *
+         * @param word The word to check
+         * @returns {boolean}
+         */
+        isHexadecimalKnownWord: function (word) {
+            return this.hexadecimalKnownWord.indexOf(word) > -1;
+        },
+
+        /**
+         * Determines if the specified expression is probably an hash.
+         *
+         * An hash is anything hexadecimal with at least one digit < 10
+         * and one digit > 9 (A-F), not matching known vocabulary.
+         *
+         * @param hash
+         * @returns {boolean}
+         */
+        isHash: function (hash) {
+            if (this.isHexadecimalKnownWord(hash)) {
+                return false;
+            }
+
+            if (/^\d+$/.test(hash) || /^[a-z]+$/i.test(hash)) {
+                // Contains only letter or digits,
+                // so not a good hash candidate.
+                return false;
+            }
+
+            return true;
+        },
+
+        formatMessage: function (message) {
+            for (var i = 0; i < this.messageDecorators.length; i++) {
+                var decorator = this.messageDecorators[i];
+                message = message.replace(decorator.re, decorator.replaceBy);
+            }
+
+            return message;
+        },
+
+        ///
+        /// Date and time helper functions
+        ///
+
+        pad: function (number) {
+            if (number < 10) {
+                return '0' + number;
+            }
+            return number;
+        },
+
+        getDate: function (timestamp) {
+            var date = new Date(timestamp);
+            return date.getUTCFullYear()
+                + '-' + this.pad(date.getUTCMonth() + 1)
+                + '-' + this.pad(date.getUTCDate());
+        },
+
+        getTime: function (timestamp) {
+            var date = new Date(timestamp);
+            return this.pad(date.getUTCHours())
+                + ':' + this.pad(date.getUTCMinutes());
+        },
+
+        getMonth: function (timestamp) {
+            var date = new Date(timestamp);
+            return date.getUTCMonth();
+        }
+
+    }
+
+    typeof container === 'string' && serversLog.load(url, container);
+
+    return serversLog;
+}
+
+
+/*  -------------------------------------------------------------
+    Code to run when document is ready
+    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -    */
+
+$(document).ready(function() {
+    $(document).foundation();
+
+    new ServersLog("https://api.nasqueron.org/servers-log/all.json", "#log");
+});
diff --git a/src/pages/api.html b/src/pages/api.html
new file mode 100644
--- /dev/null
+++ b/src/pages/api.html
@@ -0,0 +1,21 @@
+<header class="row">
+    <div class="large-12 columns">
+        <h1><img src="https://assets.nasqueron.org/logos/logo-white-64px.png" /> Servers log</h1>
+    </div>
+</header>
+
+<section class="row">
+    <div class="large-12 columns">
+        {{> intro}}
+    </div>
+</section>
+
+<div class="row">
+    <div class="large-9 columns" id="log">
+    </div>
+    <div class="large-3 columns">
+        {{> log-help}}
+    </div>
+</div>
+
+{{> footer}}