Créer et maintenir des maquettes HTML avec NodeAtlas

NodeAtlas est une application multi-os écrite en Node.js qui va vous permettre de créer, modifier et maintenir tout un ensemble de maquette HTML. Ces maquettes HTML pourront ensuite être validées par le client, et ré-utilisées totalement ou en partie par les développeurs Back-end quelque soit leurs technologies. Ces maquettes HTML pourront également servir de site stand-alone fonctionnant sans serveur ou avec un simple serveur HTTP pour créer des documentations JSDoc ou autre comme pour GitHub par exemple. C'est ce que nous allons voir dans cet article.

Mais NodeAtlas permet également de faire tourner des sites web complet, performant et rapide tel que le site sur lequel vous être entrain de lire cet article.

NodeAtlas est designé pour :

Pourquoi ?

  1. Remplacer le travail initialement fait avec des wireframes par une création en HTML avec l'aide de CSS de wireframing ou de bootstraping. Plus besoin de créer un ama de documents de wireframe, et le travail est directement prêt pour l'étape de design. Vous pouvez ensuite soumettre vos « Wireframes Live » au client.
  2. Remplacer le travail initialement fait avec Photoshop directement à partir de l'étape de wireframing avec des CSS pour le design et diverses images. Plus besoin de transférer des amas de PSD ; tout est mutualisé et la moindre partie n'est pas dupliquée. Vous pouvez ensuite soumettre vos maquettes au client. Le design est intégrable pour l'étape suivante puisqu'il tourne déjà.
  3. Pouvoir fournir à des Intégrateurs, qu'ils travail avec PHP, Java, .NET, Ruby, Node.js etc. des assets HTML directement exploitables et intégrables. Ils pourront ré-utiliser des morceaux de code, voir la totalité des maquettes dans le site qui va être créé.
  4. Transformer ces maquettes HTML à destination des Back-end directement en site web qui tournera avec Node.js sous le module NodeAtlas qui, en plus de créer des maquettes, fait tourner des sites web.

Installer et prendre en main NodeAtlas

Vous pouvez vous familiariser avec les bases de la création de Vue et de Route grâce à l'article « Des sites web Node.js pour les débutants en JavaScript avec NodeAtlas » afin de mieux appréhender cet article.

Pour la suite de ce billet, nous allons travailler sur un exemple concret : le site de présentation de NodeAtlas ! Ce site est hébergé sur un espace mis en place par GitHub capable de faire tourner des pages HTML simples. Nous allons donc ici voir :

  • comment maintenir plusieurs pages HTML de documentation sans redondance de code et générer son site « serverless »,
  • comment créer ses pages avec comme source d'origine le README.md du projet,
  • comment créer une architecture HTML/CSS/JS Vanilla en s'appuyant sur des conventions de nommage établies,
  • comment compresser la totalité du contenu pour des performances optimales côté client.

Générer des maquettes à partir de pages lives

Le but de la manœuvre va être le suivant : nous allons créer un site web qui tourne en localhost le temps du développement. Une fois que celui-ci nous conviendra, nous utiliserons la commande --generate du CLI NodeAtlas afin de créer tout le panel de page HTML correspondant initialement au site que nous avons développé. Ses pages seront consultables sans serveur ou avec n'importe quel serveur HTTP simple (Pas de node.exe, pas de php.exe, etc.).

Structure initiale du site

Comme toujours, tout débute avec le fichier webconfig.json dans votre projet de travail que nous allons ici nommer nodeatlas-website.

Ce fichier va donc être ajouté avec d'autres fichiers de démarrage à l'architecture suivante :

dist/
nodeatlas-website/
├─ views/
│  ├─ content.htm
│  └─ partials/
│     ├─ content.htm
│     ├─ head.htm
│     ├─ header.htm
│     └─ foot.htm
├─ variations/
│  └─ common.json
└─ webconfig.json

Vous remarquerez également la présence du dossier dist pour « distribution » au même niveau. C'est là que nos maquettes seront générées.

Nous allons exposer ci-après le contenu du site avec la configuration suivante :

webconfig.json

{
    "languageCode": "fr",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "htmlGenerationBeforeResponse": true,
    "routes": {
        "/index.html": "content.htm"
    }
}

et les vues suivantes :

views/partials/head.htm

<!DOCTYPE html>
<!-- `languageCode` renvoi "fr" dans notre cas. -->
<html lang="<?= languageCode ?>">
    <head>
        <meta charset="utf-8" />
        <title>NodeAtlas</title>

        <!-- `urlBasePath` renvoi "http://localhost" dans notre cas quand un serveur tourne. -->
        <!-- `urlBasePath` renvoi `` dans une page générée à la racine, il renvoit `..` pour un fichier généré dans un sous, `../..` pour un sous-sous-dossier, etc. -->
        <base href="<?= urlBasePath ?>/" />

        <meta name="description" content="NodeAtlas est un Framework JavaScript MVC(2) côté serveur." />
        <meta name="geo.placename" content="Annecy, Haute-Savoie, France" />

        <meta name="robots" content="index, follow" />

        <!--[if IE]><meta name="viewport" content="width=device-width, user-scalable=no" /><![endif]-->
        <!--[if !IE]><!--><meta name="viewport" content="initial-scale=1.0, user-scalable=no" /><!--<![endif]-->
    </head>
    <body>
        <div class="layout">
            <noscript class="no-js cmpt">
                <div class="no-js--inner">
                    <p>Le JavaScript n'est pas activé.</p>
                </div>
            </noscript>

views/partials/header.htm

            <header class="header cmpt">
                <div class="header--inner">
                    <div class="header--title">
                        <h1>NodeAtlas</h1>
                    </div>
                </div>
            </header>

views/partials/content.htm

            <article class="content cmpt">
                <div class="content--inner">
                    <?- common.content ?>
                </div>
            </article>

views/partials/foot.htm

        </div>
    </body>
</html>

views/content.htm

<?- include("partials/head.htm") ?>
<?- include("partials/header.htm") ?>
<?- include("partials/content.htm") ?>
<?- include("partials/foot.htm") ?>

alimentés par le fichier de variation commun suivant :

variations/common.json

{
    "content": "Contenu pour la documentation."
}

En installant NodeAtlas en mode global avec la commande npm install -g node-atlas, nous pouvons l'utiliser en temps que CLI à la ligne de commande. C'est ce que nous allons faire dans cet article.

Lançons donc notre site qui pour le moment ne contient qu'une page à l'adresse : http://localhost/index.html. En ouvrant un invité de commande depuis le dossier nodeatlas-website (Ctrl + Shitf + Clique droit dans une zone vide de l'explorateur), tapez la commande suivante :

> node-atlas --browse index.html

Notre page affiche donc « Contenu pour la documentation. » à l'adresse http://localhost/index.html.

Génération de maquettes

Vous remarquerez dans votre console que le message « HTML : The file « \dist\index.html » was generated ! ». Si vous vous rendez dans le dossier dist, vous remarquerez qu'un fichier index.html vous y attend. Si vous l'ouvrez depuis votre gestionnaire de fichier, vous remarquerez que le fichier tourne sans serveur web (dans mon cas avec Chrome, et mon OS, l'adresse est file:///C:/nodejs/example/dist/index.html).

Cela est possible grâce aux paramètres de webconfig serverlessRelativePath avec ../dist/ pour informer NodeAtlas de l'endroit où les maquettes seront générées et htmlGenerationBeforeResponse pour que la génération se fasse après chaque consultation de page sur localhost.

Il est possible de ne pas générer la « distribution » page par page après chaque consultation mais en une seule fois avec une seule commande. Commencez par couper votre instance en coupant le site (Ctrl + C dans l'invité de commande) puis supprimez l'intégralité du contenu de dist.

Lancez alors la commande :

> node-atlas --generate

et vous pourrez de nouveau consulter une page index.html dans le dossier dist.

Alimenter dynamiquement le webconfig

En fait, NodeAtlas n'utilise pas les valeurs de route du webconfig.json tel quel mais construit une copie de celui-ci pour tourner. Cela signifie que l'on peut ajouter dynamiquement des routes depuis la partie contrôleur avant le lancement du serveur. Nous allons donc dans un premier temps, ajouter un contrôleur commun common.json à notre configuration et en profiter pour mettre en place « l'index des maquettes » ; très utile quand on créé des maquettes avec NodeAtlas,

{
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "htmlGenerationBeforeResponse": true,
    "routes": {
        "/index.html": {
            "view": "content.htm"
        }
    }
}

et ajouter ce fichier à notre arborescence :

...
nodeatlas-website/
┊┉
├─ controllers/
│  └─ common.js
┊┉

avec le contenu suivant :

// On se connecte a NodeAtlas au moment ou celui-ci configure les routes.
exports.setRoutes = function (next) {

    // On récupère l'instance de NodeAtlas en cours.
    var NA = this,

        // Et nous récupérons les routes en provenance du webconfig...
        route = NA.webconfig.routes;

    // ...pour ajouter la route "/content.html" à la liste de nos routes.
    route["/content.html"] = "content.htm";

    // On redonne la main à NodeAtlas pour la suite.
    next();
};

Il ne nous reste plus qu'à tester cela avec la commande suivante :

> node-atlas --browse

Vous arriverez sur la page « Index of Webconfig's Routes » mise en place. Vous pourrez accéder d'ici aux pages http://localhost/index.html ou http://localhost/content.html avec le contenu « Contenu pour la documentation. ». Vous constaterez également leurs présence dans le dossier dist, toujours grâce à htmlGenerationBeforeResponse.

Injecter le contenu d'un fichier externe

Nous allons à présent injecter le contenu du README.md de NodeAtlas que vous pouvez télécharger ici. Ce contenu est initialement rédigé en Markdown or nous le voulons en « Hyper-Text Markup Language ». Nous allons donc utiliser également un module npm additionel, le module marked pour transformer le contenu du README.md en HTML.

Nous obtenons donc le nouveau fichier dans l'arborescence :

...
node-atlas-website/
┊┉
├─ README.md
┊┉

Pour ajouter marked à notre environnement, utilisez un invité de commande et lancez la commande :

> npm install -g marked

cela nous permet de ne pas piéger le module dans le projet. Une autre solution serait de créer un package.json et de lister tous les modules utilisés par ce projet, mais nous verrons cela dans le prochain billet.

Modifions à présent le fichier controllers/common.js comme suit :

controllers/common.js

// On se place au niveau du chargement des modules NodeAtlas, et on ajoute le notre.
exports.setModules = function () {

    // On récupère l'instance de NodeAtlas en cours.
    var NA = this;

    // On ajoute le module `marked`. Il va être cherché
    // dans le dossier `node_modules` local (qui n'existe pas)
    // puis finalement cherché dans les modules installés de manière
    // globale (-g) et le trouver. Il va être chargé.
    NA.modules.marked = require("marked");
};

exports.setRoutes = function (next) {
    var NA = this,
        route = NA.webconfig.routes;

    route["/content.html"] = "content.htm";

    next();
};

// On se place au niveau de la création de la Response HTTP suite à la Request HTTP d'une route.
// Cette partie sera exécutée quelque soit la route demandée car elle est dans le fichier commun.
exports.changeVariations = function (next, locals) {

    // On récupère l'instance de NodeAtlas en cours.
    var NA = this,

        // On récupère le module interne `fs` qui va nous permettre de lire des fichiers.
        fs = NA.modules.fs,

        // On récupère le module externe `marked` que l'on va utiliser depuis le moteur NodeAtlas.
        marked = NA.modules.marked;

    // On récupère le contenu de `README.md` en `utf-8`.
    // Celui-ci sera dans la variable `content`.
    fs.readFile("README.md", "utf-8", function (err, content) {
        // En cas d'erreur...
        if (err) {
            // ...on passe tout de suite au chargement de la page,
            // sans le contenu du fichier.
            return next();
        }

        // On récupère les variations compilées spécifiquement pour une route,
        // demandée par une requête. C'est pour cela qu'elles proviennent du
        // paramètre `locals` et non de l'objet `NA`.
        locals

            // On surcharge la valeur de `content` en provenance du fichier `variations/common.json`
            // par celle de la variable `content` en retour de la lecture du fichier. On transforme
            // son contenu de `Markdown` à `HTML`.
            .common.content = marked(content);

        // On redonne la main à NodeAtlas pour la suite en passant les modifications.
        next();
    });
};

En coupant le site (Ctrl + C dans l'invité de commande) et en le lançant de nouveau avec :

> node-atlas --browse

Note : si le message Error: Cannot find module 'marked' apparaît, c'est que la variable NODE_PATH n'est pas configurée sur votre environnement. C'est elle qui explique où trouver les modules globaux. Dans ce cas, sur Windows, faites un clique droit sur « Ce PC » ou (« Ordinateur ») dans l'Explorateur de fichier et allez dans « Propriétés » puis dans « Paramètres système avancés » puis dans « Variables d'environnement... » puis dans « Variable utilisateur pour... » puis cliquez sur « Nouvelle... ». Ajoutez NODE_PATH dans « Nom de la variable » et %USERPROFILE%\AppData\Roaming\npm\node_modules dans « Valeur de la variable ». Cliquez sur « Ok » pour fermer chaque fenêtre. Fermez votre invité de commande et r'ouvrez s'en un. Tout devrait être en ordre.

Vous serez à même de lire le README.md à l'adresse http://localhost/index.html ou à l'adresse http://localhost/content.html (et dans le dossier dist).

setModules, setRoutes et changeVariations

Vous aurez remarqué que ces trois fonctions ne fonctionnent pas exactement de la même manière. La première est simple, la seconde nécéssite l'appel de la callback next et la dernière en plus, doit passer à cette callback la variable variations. Nous allons expliquer trois concepts associés à leurs utilisation :

  • setModules intervient au niveau du chargement des modules de NodeAtlas. Le module NodeAtlas est récupérable via this dans cette fonction. this est lié au moteur et toute modification de celui-ci dans un exports est répercuté dans le moteur. Cela signifie qu'en ajoutant des modules, ils se retrouvent utilisable aux autres endroit à travers tout le moteur. Puisque cette zone est dédiée uniquement à charger les require, et que les require se chargent de manière synchrone, on sait que le moteur ne reprend pas la main avant que tout soit chargé. Il n'y a pas de nécéssité de callback donc.

  • setRoutes intervient juste avant le chargement des routes, mais quand le moteur est complètement opérationnel. La nature de ce qu'on va faire ici fait que les actions peuvent être asynchrones. Il est donc important d'attendre le retour des fonctions asynchrones avant de rendre la main au moteur et démarrer le serveur. Pour être sur que, par exemple, toutes les routes ont bien finies d'être chargées avant la suite du processus, on glisse la fonction next dans le retour de la fonction asynchrone.

  • changeVariations intervient à chaque fois que le client fait une requête au serveur NodeAtlas, durant la création de la réponse juste avant la phase de compilation des vues. Comme le contenu de params (et donc params.variations) est unique à chaque réponse, il ne peut pas être placé dans le this (qui représente ici, toujours le moteur NodeAtlas global) sinon les valeurs s'écraseraient les unes les autres à chaque requête concurrante : c'est pour cela qu'il est passé en premier paramètre. Une fois les modifications faites, il est nécessaire de les ré-injecter dans le moteur en premier paramètre de la callback next.

Note : en réalité, le premier paramètre de la callback next est optionnel pour changeVariations car params.variations est un objet passé par référence mais obligatoire pour changeDom car params.dom est une string passé en tant que variable.

Créer des fragments de contenu à partir du README.md

Bien sur, il n'y a pas d'utilité à ajouter la page http://localhost/content.html car elle est actuellement identique à la http://localhost/index.html.

Ce que nous allons faire : c'est créer un ensemble de pages générées à partir du README.md directement dans des fichiers accessibles côté Front et côté Back. Nous allons décider que chaque <h2> et ce qui le suit sera le contenu d'un fichier. Et chaque fichier sera injecté dans la vue views/content.htm pour créer une page derrière une route dynamiquement. Voyons plutôt.

Les fragments HTML seront donc créer dans assets/content/.

...
nodeatlas-website/
┊┉
├─ assets/
│  └─ content/
┊┉

Dans un premier temps, nous allons lire le README.md avant que le serveur soit lancé pour créer les fichiers nécessaires puis les ajouter à la liste des routes.

controllers/common.js

exports.setModules = function () {
    var NA = this;
    NA.modules.marked = require("marked");
};

exports.setRoutes = function (next) {
    var NA = this,
        fs = NA.modules.fs,

        // On utilise de quoi manipuler le DOM côté serveur.
        jsdom =  NA.modules.jsdom,

        // On permet de gérer une callback synchrone à la fin de
        // plusieurs appels asynchrones (en parallèle).
        async = NA.modules.async,
        marked = NA.modules.marked,
        route = NA.webconfig.routes;

    fs.readFile("README.md", "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        // On récupère la version HTML du README.md.
        var html = marked(content),

            // On transforme la string Response en DOM.
            dom = new jsdom.JSDOM(html),

            // On prépare la liste des actions asynchrones dans un tableau.
            allRoutes = [];

        // Pour chaque H2 dans la sortie du README.md.
        Array.prototype.forEach.call(dom.window.document.getElementsByTagName('h2'), function (title) {

            // On ajoute au tableau les instructions...
            allRoutes.push(function (nextRoute) {

                // ...de créer un fichier, dont le nom est l'id généré par `marked`
                // à partir du H2 et dont le contenu
                // est les autres balises jusqu'au H2 suivant.
                fs.writeFile(
                    "assets/" + NA.webconfig._content + encodeURIComponent(title.getAttribute("id")) + ".htm",
                    title.outerHTML + content.map(function (element) { return element.outerHTML; }).join(''),
                function () {

                    // On ajoute la route qui correspondra au chargement du fichier généré.
                    route["/" + title.getAttribute("id") + ".html"] = "content.htm";

                    // Et on passe à la suite.
                    nextRoute();
                });
            });
        });

        // On utilise async pour créer les fichiers de manière
        // asynchrone, peu importe l'ordre de création. Mais quand
        // tout est créé...
        async.parallel(allRoutes, function() {

            // ...on rend la main à NodeAtlas.
            next();
        });
    });
};

exports.changeVariations = function (next, locals) {
    var NA = this,
        fs = NA.modules.fs;

    // Pour chaque route créée, on lit le fichier HTML généré correspondant...
    fs.readFile("assets/content/" + locals.route.replace(".html", ".htm"), "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        // ... et on met son contenu dans la variable
        // utilisée dans la vue.
        locals.common.content = content;

        next();
    });
};

En coupant le site et en le lançant de nouveau avec :

> node-atlas --browse

Vous pourrez voir la liste complète des pages sur l'index http://localhost/ et constater qu'au fur et à mesure que vous consultez les autres pages, celles-ci apparaissent dans dist. Vous pourrez également remarquer que nos fragments sont bien tous présent dans le dossier nodeatlas-website/assets/content/.

En coupant le site et en lançant la commande :

> node-atlas --generate

vous remarquerez que non seulement la totalité des pages se retrouve dans le dossier dist, mais que en plus le dossier content/ et tout son contenu a été copié également. Ce mécanisme permet d'automatiquement rapatrier les futurs fichiers publiques dans la « distribution » lors de la génération, en conservant la même arborescence de fichier.

Utilisation de contrôleurs spécifiques

Nous allons ensuite récupérer la table des matières pour la placer derrière le chemin http://localhost/index.html. Nous allons toujours travailler dans changeVariations, cependant le mécanisme de chargement sera différent en fonction qu'il soit injecté dans views/index.htm ou dans views/content.htm. C'est pourquoi nous allons créer deux contrôleurs spécifiques pour ses deux fichiers, et déplacer le contenu de changeVariations en provenance de controllers/common.js vers ces deux fichiers.

Notre nouvelles arborescences est donc :

dist/
nodeatlas-website/
├─ assets/
│  └─ content/
├─ controllers/
│  ├─ common.js
│  ├─ content.js
│  └─ index.js
├─ views/
│  ├─ content.htm
│  └─ partials/
│     ├─ content.htm
│     ├─ head.htm
│     ├─ header.htm
│     └─ foot.htm
├─ variations/
│  └─ common.json
├─ webconfig.json
└─ README.md

avec le webconfig suivant :

{
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    }
}

et avec les trois nouveaux fichiers suivant :

controllers/common.js

exports.setModules = function () {
    var NA = this;

    NA.modules.marked = require("marked");
};

exports.setRoutes = function (next) {
    var NA = this,
        fs = NA.modules.fs,
        jsdom =  NA.modules.jsdom,
        async = NA.modules.async,
        marked = NA.modules.marked,
        route = NA.webconfig.routes;

    // On va ré-aligner la manière dont les ids sont générés.
    // Cela va nous servir d'ancre/lien unique pour le nom des fichiers, des urls et des ids,
    // ainsi il n'y aura pas de lien mort.
    function toUrl(text) {
        return text.toLowerCase().replace(/'| |\(|\)|\/|\!|\?|,|\&|\;|\[|\]|\%/g, "-").replace(/-+/g, "-").replace(/^-/g, "").replace(/-$/g, "");
    }

    fs.readFile("README.md", "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        var html = marked(content),
            dom = new jsdom.JSDOM(html),
            allRoutes = [],

            // On déclare la futur fonction de création du menu.
            menu,

            // On choisit la partie du `README.md` que l'on va utiliser comme page d'index.
            key = "table-des-matières";

        // On coordonnonne la manière dont les titres sont transformé en id...
        Array.prototype.forEach.call(dom.window.document.querySelectorAll('h2, h3'), function (title) {
            title.setAttribute("id", toUrl(title.textContent));
        });

        // On récupère la table des matières,
        // et on l'enregistre à part dans un autre fichier.
        Array.prototype.forEach.call(dom.window.document.querySelectorAll('h3[id="' + key + '"]'), function (title) {
            var toc = title.nextElementSibling;

            // On transforme toutes les ancres H2 du menu...
            Array.prototype.forEach.call(toc.children, function (sublink) {
                var subtitle = sublink.children[0],

                    // On transforme tous les titres...
                    url = encodeURIComponent(toUrl(subtitle.textContent)) + ".html";

                // ...en liens correctes.
                subtitle.setAttribute("href", url);

                // Et on réoriente toutes les ancres H3 du menu...
                Array.prototype.forEach.call(sublink.querySelectorAll('li'), function (sublink) {
                    var subtitle = sublink.children[0];

                    // ...vers les bonnes adresses avec une bonne ancre.
                    subtitle.setAttribute("href", url + "#" + encodeURIComponent(toUrl(subtitle.textContent)));

                    // On efface l'entrée `table des matières` de la table des matières.
                    if (toUrl(subtitle.textContent) === key) {
                        sublink.parentNode.removeChild(sublink);
                    }
                });
            });

            // On créer une fonction pour créer le fichier à part qui servira pour le menu global.
            menu = function (next) {
                fs.writeFile("assets/content/index.htm", title.outerHTML + toc.outerHTML, function () {

                    // On retire le menu de la source pour l'enregistrement ultérieur.
                    toc.parentNode.removeChild(toc);
                    title.parentNode.removeChild(title);

                    // On passera à la suite.
                    next();
                });
            };

        });

        // On se créer une fonction utilitaire pour simuler le comportement de `nextUntil` de jQuery
        function nextUntil(title, selector) {
            var htmlElement = title,
                nextUntil = [],
                until = true;
            while (htmlElement = htmlElement.nextElementSibling) {
                (until && htmlElement && !htmlElement.matches(selector)) ? nextUntil.push(htmlElement) : until = false;
            }
            return nextUntil;
        }

        Array.prototype.forEach.call(dom.window.document.getElementsByTagName('h2'), function (title) {
            allRoutes.push(function (nextRoute) {
                fs.writeFile(
                    "assets/content/" + encodeURIComponent(title.getAttribute("id")) + ".htm",
                    title.outerHTML + content.map(function (element) { return element.outerHTML; }).join(''),
                function () {
                    // On ajoute le contrôleur pour les pages dynamiques.
                    route["/" + encodeURIComponent(title.getAttribute("id")) + ".html"] = {
                        "view": "content.htm",
                        "controller": "content.js"
                    };

                    nextRoute();
                });
            });
        });

        // On créer le fichier du menu...
        menu(function () {

            // ...puis tous les autres fichiers dans le désordre...
            async.parallel(allRoutes, function() {

                // ... puis on rend la main à NodeAtlas.
                next();
            });
        });
    });
};

controllers/content.js

exports.changeVariations = function (next, locals) {
    var NA = this,
        fs = NA.modules.fs;

    fs.readFile("assets/content/" + locals.route.replace(".html", ".htm"), "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        locals.common.content = content;
        next();
    });
};

controllers/index.js

exports.changeVariations = function (next, locals) {
    var NA = this,
        fs = NA.modules.fs;

    // Avec pour le moment la différence que le fichier choisit
    // est le fichier `index.htm`.
    fs.readFile("assets/content/index.htm", "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        locals.common.content = content;
        next();
    });
};

Vous constaterez alors après avoir complètement effacé le contenu de dist qu'avec l'utilisation, depuis nodeatlas-website, de :

> node-atlas --browse

Nous retrouverons à l'index la totalité de la table des matières. Nous pourrons constater que la page qui contenait la table des matières (actuellement http://localhost/avant-propos.html) en est maintenant dépourvu.

En ce qui concerne le dossier dist, vous aurez peut-être remarqué que nous avons retiré du webconfig le paramètre htmlGenerationBeforeResponse ? C'est pourquoi plus rien n'y est généré. Cependant, avec l'ajout des paramètres output et assetsCopy, vous pouvez toujours couper le serveur web, lancer la commande,

> node-atlas --generate

et constater que tous nos fichiers sont de retour !

Changer de page, sans rechargement

Vous vous êtes peut-être demandé pourquoi nous avons créé des fragments HTML dans des fichiers accessibles côté client (dossier content/) ? C'est pour pouvoir en injecter le contenu dynamiquement côté client avec des requêtes AJAX ! Grâce à la fonction history.pushState, nous allons changer l'url de la page courante et injecter son nouveau contenu sans rafraîchir la page. Cela signifie donc que :

  • Si un robot réclame une page, comme il ne navigue pas par l'interface du site et donc sans JavaScript, il la réclamera toujours avec une requête HTTP et recevra donc une réponse complète avec du HTML prêt à être indexé.
  • Si un visiteur réclame la page, le JavaScript prendra la main et les liens deviendront des actions permettant de rapatrier les fragments correspondant et de mettre à jour la page sans la recharger.

Bien sur qui dit demande AJAX dit qu'un simple serveur web devra héberger votre maquette (comme c'est le cas pour GitHub). Quand le site sera seulement consulté par le système de fichier, les pages se rechargeront simplement comme c'est le cas actuellement.

Pour réaliser cela nous allons ajouter un fichier JavaScript commun côté client :

nodeatlas-website/
┊┉
├─ assets/
│  ┊┉
│  ├─ javascripts/
│  │  └─ common.js
│  ┊┉
┊┉

puis l'ajouter dans le pied de page de nos vues :

views/partials/foot.htm

            <!-- Injection du fichier en provenance de `assets/javascripts/common.js` -->
            <script src="javascripts/common.js"></script>
        </div>
    </body>
</html>

Nous allons également créer un menu global dans lequel nous allons catégoriser les pages crées depuis le README.md.

views/partials/navigation.htm

            <nav class="navigation cmpt">
                <div class="navigation--inner">
                    <div class="navigation--home">
                        <a href="index.html" title="Accueil">Accueil</a>
                    </div>
                    <div class="navigation--menu">
                        <ul>
                            <li>
                                <a href="avant-propos.html">Présentation</a>
                                <ul>
                                    <li>
                                        <a href="installation.html">Installation</a>
                                    </li>
                                    <li>
                                        <a href="commencer-avec-nodeatlas.html">Démarrage rapide</a>
                                    </li>
                                </ul>
                            </li>
                            <li>
                                <a href="partie-view-et-template.html">View et Template</a>
                            </li>
                            <li>
                                <a href="partie-controller-et-model.html">Controller et Model</a>
                            </li>
                            <li>
                                <a href="pour-aller-plus-loin.html">Route et Plus</a>
                                <ul>
                                    <li>
                                        <a href="api-nodeatlas-comme-module-npm.html">API</a>
                                    </li>
                                    <li>
                                        <a href="cli-commandes-de-lancement.html">CLI</a>
                                    </li>
                                    <li>
                                        <a href="nodeatlas-comme-simple-serveur-web.html">Simple Serveur</a>
                                    </li>
                                    <li>
                                        <a href="environnement-de-d%C3%A9veloppement.html">Développement</a>
                                    </li>
                                    <li>
                                        <a href="environnement-de-production.html">Production</a>
                                    </li>
                                </ul>
                            </li>
                            <li>
                                <a href="plus-sur-nodeatlas.html">Et les autres ?</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>

views/content.htm

<?- include("partials/head.htm") ?>
<?- include("partials/header.htm") ?>

<!-- Ajout de la navigation globale. -->
<?- include("partials/navigation.htm") ?>
<?- include("partials/content.htm") ?>
<?- include("partials/foot.htm") ?>

Nous allons enfin ajouter le contenu du fichier JavaScript commun :

assets/javascripts/common.js

// On récupère tous les liens du menu.
var links = document.querySelectorAll(".navigation a"),

    // On se créer une fonction « type » pour faire des appels AJAX
    xhrRequest = function(url, next) {

        // Création d'un objet d'appel AJAX.
        var request = new XMLHttpRequest();

        // Pour un appel en GET à l'url <url>.
        request.open("GET", url, true);

        // Envoi de la requête
        request.send();

        // Si la requête atteind sa cible.
        request.addEventListener("load", function () {

            // Si la réponse fournit est invalide...
            if (request.status < 200 && request.status >= 400) {

                // ...on retourne une erreur.
                return next(new Error("We reached our target server, but it returned an error."));
            }

            // Sinon on renvoi la réponse à la callback.
            next(null, request.responseText);
        });

        // Si la requête n'obtient pas de réponse ou une mauvaise réponse.
        request.addEventListener("error", function () {
            return next(new Error("There was a connection error of some sort."));
        });
    };

// Au chargement de la page, tous les liens du menu...
[].forEach.call(links, function (link) {

    // ...deviennent cliquable...
    link.addEventListener("click", function (e) {

        // et basé sul l'adresse du lien, on récupère le nom du fichier.
        var urn = link.getAttribute("href").replace(".html", "");

        // On empèche la page de suivre le lien initial.
        e.preventDefault();

        // On réclame le fragment nécéssaire à l'affichage de notre nouvelle page.
        xhrRequest("content/" + encodeURIComponent(urn) + ".htm", function (err, response) {
            if (err) {
              return err;
            }

            // On ajoute une nouvelle page dans l'historique,
            // et on s'y déplace (paramètre trois)
            // en laissant l'urn comme information (premier paramètre)
            // utilisable pour le retour en arrière.
            history.pushState(urn, null, "/" + urn + ".html");

            // Et on injecte la response dans la page principale.
            document.getElementsByClassName("content--inner")[0].innerHTML = response;
        });
    });
});

// À chaque fois que l'on réclamera un retour en arrière (comme le bouton back du navigateur)
window.addEventListener("popstate", function (e) {

    // On consultera l'url passée comme référence pour ramener ce contenu...
    if (e.state) {

        // ... en AJAX...
        xhrRequest("content/" + encodeURIComponent(e.state) + ".htm", function (err, response) {
            if (err) {
              return err;
            }

            // et l'injecter dans la page principale.
            document.getElementsByClassName("content--inner")[0].innerHTML = response;
        });

    // Cependant s'il n'y a pas d'état,
    // c'est que nous somme arrivé à la page
    // d'origine et que l'on souhaite retourner sur le site
    // qui avait précédé notre arrivé sur le site.
    } else {
        history.back();
    }
});

Lancez le serveur :

> node-atlas --browse

Vous constaterez alors en vous rendant à l'adresse http://localhost/index.html que naviguer à travers les pages via votre menu ne recharge plus la page et que, après avoir navigué à travers plusieurs page, le bouton « retour » vous ramène également aux pages précédentes sans rechargement. Vous pourrez réellement vous en assurez dans le panneau « Network » (F12) de votre navigateur en voyant que ce sont bien des requêtes « xhr ».

Nous allons maintenant générer la maquette :

> node-atlas --generate

Et en ouvrant le fichier dist\index.html par votre système de fichier, vous vous rendrez compte que les pages ne changent plus. Dans votre console (F12) vous pourrez voir l'erreur suivante (sous Chrome) : « XMLHttpRequest cannot load . ». Cela est normal car il faut un serveur web pour répondre aux requêtes AJAX.

Nous allons donc modifier le script pour qu'il ne génère pas d'erreur et aille sur la page souhaitée normalement (en rechargeant la page) si l'AJAX n'est pas géré.

assets/javascripts/common.js

var links = document.querySelectorAll(".navigation a"),
    xhrRequest = function(url, next) {
        var request = new XMLHttpRequest();

        // Vérifier que l'objet request peut envoyer une requête AJAX.
        if (location.protocol !== "file:") {
            request.open("GET", url, true);
            request.send();
        } else {
            return next(new Error("Impossible to use AJAX in file system mode."));
        }

        request.addEventListener("load", function () {
            if (request.status < 200 && request.status >= 400) {
                return next(new Error("The server was reached, but with no correct response."));
            }
            next(null, request.responseText);
        });

        request.addEventListener("error", function () {
            return next(new Error("The server is unreachable."));
        });
    },

    // Fonction utilisée si les requêtes AJAX ne fonctionnent pas.
    xhrFallback = function (url) {
        // On atteint l'url normalement.
        location.href = encodeURIComponent(url) + ".html";
    };


[].forEach.call(links, function (link) {
    link.addEventListener("click", function (e) {
        var urn = link.getAttribute("href").replace(".html", "");
        e.preventDefault();
        xhrRequest("content/" + encodeURIComponent(urn) + ".htm", function (err, response) {
            if (err) {

                // On stop la fonction.
                return xhrFallback(urn);
            }

            history.pushState(urn, null, "/" + urn + ".html");

            document.getElementsByClassName("content--inner")[0].innerHTML = response;
        });
    });
});

window.addEventListener("popstate", function (e) {
    if (e.state) {
        xhrRequest("content/" + encodeURIComponent(e.state) + ".htm", function (err, response) {
            if (err) {

                // On stop la fonction.
                return xhrFallback(e.state);
            }

            document.getElementsByClassName("content--inner")[0].innerHTML = response;
        });
    } else {
        history.back();
    }
});

À présent, en relançant la génération de la maquette, vous pourrez, même par le système de fichier, naviguer entre vos pages qui cette fois se chargeront totalement.

Simple Serveur Web

Sachez que NodeAtlas aussi sait faire tourner un simple serveur web pour tester vos maquettes en local tel qu'elles seront lues une fois sur un serveur (dans notre exemple, sur GitHub). Il vous suffit de vous positionner dans le dossier dist/ et de lancer la commande :

> node-atlas --browse index.html

ou (si le port 80 est déjà utilisé)

> node-atlas --browse index.html --httpPort 3000

Génération multilingue

Nous allons à présent créer la version « internationale » de la documentation. Pour cela nous allons suivre les étapes suivantes :

  • Créer un webconfig spécifique à la version internationale.
  • Ramener le README.md international.
  • Mettre l'accès au README.md) dans une variable de webconfig.
  • Mettre les textes de l'applications dans le fichier de variation commun.

Il va être question d'obtenir la version française du site dans dist et la version internationale dans le sous-répertoire dist\english ce qui nécessite l'utilisation de la propriété de webconfig urlRelativeSubPath.

Notre nouvelle arborescence va donc être la suivante pour dist :

dist/
└─ english/

et pour nodeatlas-website

nodeatlas-website/
├─ assets/
│  ├─ content/
│  │  └─ english/
│  └─ javascripts/
│     └─ common.js
├─ controllers/
│  ├─ common.js
│  ├─ content.js
│  └─ index.js
├─ views/
│  ├─ content.htm
│  └─ partials/
│     ├─ content.htm
│     ├─ head.htm
│     ├─ navigation.htm
│     ├─ header.htm
│     └─ foot.htm
├─ variations/
│  ├─ common.json
│  └─ en
│     └─ common.json
├─ webconfig.json
├─ webconfig.en.json
├─ README.md
└─ README.en.md

Créez donc le nouveau dossier assets/content/english/ pour accueillir les fragments AJAX de la version internationale.

Avec notre nouveau nodeatlas-website\README.en.md récupérrable ici (changez le nom du fichier).

Nos deux webconfigs vont être les suivants,

pour la version française :

webconfig.json

{
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières"
}

et la version internationale :

webconfig.en.json

{
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents"
}

Nous allons nous servir de _readme, _toc, et de _content dans le code, dans les fichiers suivant :

controllers/common.js

exports.setModules = function () {
    var NA = this;

    NA.modules.marked = require("marked");
};

exports.setRoutes = function (next) {
    var NA = this,
        fs = NA.modules.fs,
        jsdom =  NA.modules.jsdom,
        async = NA.modules.async,
        marked = NA.modules.marked,
        route = NA.webconfig.routes;

    function toUrl(text) {
        return text.toLowerCase().replace(/'| |\(|\)|\/|\!|\?|,|\&|\;|\[|\]|\%/g, "-").replace(/-+/g, "-").replace(/^-/g, "").replace(/-$/g, "");
    }

    // On remplace le « README.md » par sa valeur dans le webconfig.
    fs.readFile(NA.webconfig._readme, "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        var html = marked(content),
            dom = new jsdom.JSDOM(html),
            allRoutes = [],
            menu,

            // On remplace « table des matières » par sa valeur dans le webconfig.
            key = NA.webconfig._toc;

        Array.prototype.forEach.call(dom.window.document.querySelectorAll('h2, h3'), function (title) {
            title.setAttribute("id", toUrl(title.textContent));
        });

        Array.prototype.forEach.call(dom.window.document.querySelectorAll('h3[id="' + key + '"]'), function (title) {
            var toc = title.nextElementSibling;

            Array.prototype.forEach.call(toc.children, function (sublink) {
                var subtitle = sublink.children[0],
                    url = encodeURIComponent(toUrl(subtitle.textContent)) + ".html";

                subtitle.setAttribute("href", url);

                Array.prototype.forEach.call(sublink.querySelectorAll('li'), function (sublink) {
                    var subtitle = sublink.children[0];

                    subtitle.setAttribute("href", url + "#" + encodeURIComponent(toUrl(subtitle.textContent)));

                    if (toUrl(subtitle.textContent) === key) {
                        sublink.parentNode.removeChild(sublink);
                    }
                });
            });

            menu = function (next) {

                // On remplace « assets/content/ » par sa valeur dans le webconfig.
                fs.writeFile("assets/" + NA.webconfig._content + "index.htm", title.outerHTML + toc.outerHTML, function () {
                    toc.parentNode.removeChild(toc);
                    title.parentNode.removeChild(title);

                    next();
                });
            };
        });

        function nextUntil(title, selector) {
            var htmlElement = title,
                nextUntil = [],
                until = true;
            while (htmlElement = htmlElement.nextElementSibling) {
                (until && htmlElement && !htmlElement.matches(selector)) ? nextUntil.push(htmlElement) : until = false;
            }
            return nextUntil;
        }

        Array.prototype.forEach.call(dom.window.document.getElementsByTagName('h2'), function (title) {
            allRoutes.push(function (nextRoute) {
                var content = nextUntil(title, "h2");

                // On remplace « assets/content/ » par sa valeur dans le webconfig.
                fs.writeFile("assets/" + NA.webconfig._content + encodeURIComponent(title.getAttribute("id")) + ".htm", title.outerHTML + content.map(function (element) { return element.outerHTML; }).join(''), function () {
                    route["/" + encodeURIComponent(title.getAttribute("id")) + ".html"] = {
                        "view": "content.htm",
                        "controller": "content.js"
                    };

                    nextRoute();
                });
            });
        });

        menu(function () {
            async.parallel(allRoutes, function() {
                next();
            });
        });
    });
};

controllers/content.js

exports.changeVariations = function (next, locals) {
    var NA = this,
        fs = NA.modules.fs;

    // On remplace « assets/content/ » par sa valeur dans le webconfig.
    fs.readFile("assets/" + NA.webconfig._content + locals.route.replace(".html", ".htm"), "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        locals.common.content = content;
        next();
    });
};

controllers/index.js

exports.changeVariations = function (next, locals) {
    var NA = this,
        fs = NA.modules.fs;

    // On remplace « assets/content/ » par sa valeur dans le webconfig.
    fs.readFile("assets/" + NA.webconfig._content + "index.htm", "utf-8", function (err, content) {
        if (err) {
            return next();
        }

        locals.common.content = content;
        next();
    });
};

puis nous allons augmenter le fichier de variation original :

variations/common.json

{
    "title": "NodeAtlas",
    "content": "Contenu pour la documentation.",
    "noJs": "Le JavaScript n'est pas activé.",
    "home": {
        "title": "Accueil",
        "url": "index.html"
    },
    "lang": {
        "title": "English",
        "url": "english/index.html"
    },
    "menu": [{
        "title": "Présentation",
        "url": "avant-propos.html",
        "menu": [{
            "title": "Installation",
            "url": "installation.html"
        }, {
            "title": "Démarrage rapide",
            "url": "commencer-avec-nodeatlas.html"
        }]
    }, {
        "title": "View et Template",
        "url": "partie-view-et-template.html"
    }, {
        "title": "Controller et Model",
        "url": "partie-controller-et-model.html"
    }, {
        "title": "Route et Plus",
        "url": "pour-aller-plus-loin.html",
        "menu": [{
            "title": "API",
            "url": "api-nodeatlas-comme-module-npm.html"
        }, {
            "title": "CLI",
            "url": "cli-commandes-de-lancement.html"
        }, {
            "title": "Simple Serveur",
            "url": "nodeatlas-comme-simple-serveur-web.html"
        }, {
            "title": "Développement",
            "url": "environnement-de-d%C3%A9veloppement.html"
        }, {
            "title": "Production",
            "url": "environnement-de-production.html"
        }]
    }, {
        "title": "Et les autres ?",
        "url": "plus-sur-nodeatlas.html"
    }]
}

et créer sa version internationale :

variations/en/common.json

{
    "title": "NodeAtlas",
    "content": "Content for documentation.",
    "noJs": "No JavaScript enabled.",
    "home": {
        "title": "Home",
        "url": "index.html"
    },
    "lang": {
        "title": "Français",
        "url": "../index.html"
    },
    "menu": [{
        "title": "Overview",
        "url": "overview.html",
        "menu": [{
            "title": "Installing",
            "url": "installing.html"
        }, {
            "title": "Get Started",
            "url": "start-with-nodeatlas.html"
        }]
    }, {
        "title": "View and Template",
        "url": "view-and-template-part.html"
    }, {
        "title": "Controller and Model",
        "url": "controller-and-model-part.html"
    }, {
        "title": "Route and More",
        "url": "more-features.html",
        "menu": [{
            "title": "API",
            "url": "api-nodeatlas-as-npm-module.html"
        }, {
            "title": "CLI",
            "url": "cli-running-commands.html"
        }, {
            "title": "Simple Server",
            "url": "nodeatlas-as-a-simple-web-server.html"
        }, {
            "title": "Development",
            "url": "development-environment.html"
        }, {
            "title": "Production",
            "url": "production-environment.html"
        }]
    }, {
        "title": "And others?",
        "url": "more-about-nodeatlas.html"
    }]
}

pour pouvoir changer les fichiers de vue partielle suivants :

views/partials/head.htm

<!DOCTYPE html>
<html lang="<?= languageCode ?>">
    <head>
        <meta charset="utf-8" />
        <title>NodeAtlas</title>

        <base href="<?= urlBasePath ?>/" />

        <meta name="description" content="NodeAtlas est un Framework JavaScript MVC(2) côté serveur." />
        <meta name="geo.placename" content="Annecy, Haute-Savoie, France" />

        <meta name="robots" content="index, follow" />

        <!--[if IE]><meta name="viewport" content="width=device-width, user-scalable=no" /><![endif]-->
        <!--[if !IE]><!--><meta name="viewport" content="initial-scale=1.0, user-scalable=no" /><!--<![endif]-->
    </head>

    <!-- On place dans `data-content` le chemin vers les fragments HTML. -->
    <!-- On place dans `data-subpath` le chemin vers le sous répertoire de la version internationale. -->
    <body data-content="<?= webconfig._content ?>" data-subpath="<?= webconfig.urlRelativeSubPath ?>">
        <div class="layout">
            <noscript class="no-js cmpt">
                <div class="no-js--inner">

                    <!-- Le message annonçant l'activation du JS est multilangue. -->
                    <p><?- common.noJs ?></p>
                </div>
            </noscript>

views/partials/header.htm

            <header class="header cmpt">
                <div class="header--inner">
                    <div class="header--title">

                        <!-- Le titre du site. -->
                        <h1><?- common.title ?></h1>
                    </div>
                </div>
            </header>

views/partials/navigation.htm

            <nav class="navigation cmpt">
                <div class="navigation--inner">
                    <div class="navigation--lang">
                        <a href="<?= common.lang.url ?>" title="<?= common.lang.title ?>"><?- common.lang.title ?></a>
                    </div>
                    <div class="navigation--home">
                        <a href="<?= common.home.url ?>" title="<?= common.home.title ?>"><?- common.home.title ?></a>
                    </div>
                    <div class="navigation--menu">
                        <ul>
                            <? for (var i = 0; i < common.menu.length; i++) { ?>
                            <li>
                                <a href="<?= common.menu[i].url ?>" title="<?= common.menu[i].title ?>"><?- common.menu[i].title ?></a>
                                <? if (common.menu[i].menu) { ?>
                                <ul>
                                    <? for (var j = 0; j < common.menu[i].menu.length; j++) { ?>
                                    <li>
                                        <a href="<?= common.menu[i].menu[j].url ?>" title="<?= common.menu[i].menu[j].title ?>"><?- common.menu[i].menu[j].title ?></a>
                                    </li>
                                    <? } ?>
                                </ul>
                                <? } ?>
                            </li>
                            <? } ?>
                        </ul>
                    </div>
                </div>
            </nav>

Nous pouvons à présent grâce à data-content dans le <body> choisir les fragments français ou internationaux dans le JavaScript client grâce à la modification suivante :

assets/javascripts/common.js

// On retire le lien pour changer de version (.navigation--lang)
// En sélectionnant que la home et le menu.
var links = document.querySelectorAll(".navigation--home a, .navigation--menu a"),

    // On va chercher le chemin correcte jusqu'au fichier.
    fragmentPath = document.body.getAttribute("data-content"),

    // On récupère le sous répertoire si celui-ci existe (version anglaise).
    urlRelativeSubPath = document.body.getAttribute("data-subpath"),
    xhrRequest = function(url, next) {
        var request = new XMLHttpRequest();

        if (location.protocol !== "file:") {
            request.open("GET", url, true);
            request.send();
        } else {
            return next(new Error("Impossible d'utiliser AJAX par simple ouverture de fichier."));
        }

        request.addEventListener("load", function () {
            if (request.status < 200 && request.status >= 400) {
                return next(new Error("Le serveur a été atteind mais à renvoyé une erreur."));
            }
            next(null, request.responseText);
        });

        request.addEventListener("error", function () {
            return next(new Error("Le serveur n'a pas pu être atteind."));
        });
    },
    xhrFallback = function (url) {
        location.href = encodeURIComponent(url) + ".html";
    };


[].forEach.call(links, function (link) {
    link.addEventListener("click", function (e) {
        var urn = link.getAttribute("href").replace(".html", "");
        e.preventDefault();

        // Et on injecte le chemin pour les liens.
        xhrRequest(fragmentPath + encodeURIComponent(urn) + ".htm", function (err, response) {
            if (err) {
                return xhrFallback(urn);
            }

            // Et on injecte le sous répertoire pour les liens.
            history.pushState(urn, null, urlRelativeSubPath + "/" + urn + ".html");

            document.getElementsByClassName("content--inner")[0].innerHTML = response;
        });
    });
});

window.addEventListener("popstate", function (e) {
    if (e.state) {

        // Et on injecte le chemin pour les liens.
        xhrRequest(fragmentPath + encodeURIComponent(e.state) + ".htm", function (err, response) {
            if (err) {
                return xhrFallback(e.state);
            }

            document.getElementsByClassName("content--inner")[0].innerHTML = response;
        });
    } else {
        history.back();
    }
});

Nous pouvons à présent faire tourner la version française avec la commande :

> node-atlas --browse

ou la version internationale avec la commande :

> node-atlas --browse --webconfig webconfig.en.json

Vous constaterez que le lien pour changer de version pointe sur des pages inexistantes puisque chaque version ne fait tourner que sa langue. Cependant, en générant les fichiers français avec :

> node-atlas --generate

puis les fichiers anglais avec :

> node-atlas --generate --webconfig webconfig.en.json

vous pourrez, en vous rendant dans dist simuler un serveur web avec

> node-atlas --browse index.html

et constater que toutes les versions sont accessibles derrière tous les liens.

Après avoir mis en place de quoi maintenir et générer notre documentation à partir du README.md du projet, il est temps de nous attaquer à la partie visuelle de ce site. Mais arrêtons nous un instant pour automatiser le processus de génération des sites !

Génération avec l'API NodeAtlas

Nous venons de voir qu'il y a plusieurs étapes pour générer notre documentation. Il faut :

  • Générer la version française.
  • Générer la version internationale.
  • Lancer le site généré en localhost pour vérification.

Nous allons créer un fichier JavaScript dans le dossier nodeatlas-website qui va nous permettre de faire les trois en une seule fois. Le voici :

generate-website.js

// On récupère l'API NodeAtlas.
var nodeAtlas = require("node-atlas"),

    // On créé une instance pour générer la version française.
    versionFrench = new nodeAtlas(),

    // On créé une instance pour générer la version internationale.
    versionEnglish = new nodeAtlas(),

    // On créé une instance pour faire tourner le site générer.
    versionTest = new nodeAtlas();

// On paramètre la version française.
versionFrench.init({
    "generate": true

// On explique ce qu'il ce passe après la création de la version française.
}).generated(function() {

    // On paramètre la version internationale.
    versionEnglish.init({
        "generate": true,
        "webconfig": "webconfig.en.json"

    // On explique ce qu'il ce passe après la création de la version internationale.
    }).generated(function() {

        // On paramètre la version de test.
        versionTest.init({
            "browse": "index.html",
            "directory": "../dist/"

        // On lance le site de Test.
        }).start();

    // On lance la génération internationale.
    }).start();

// On lance la génération française.
}).start();

Et nous lançons la commande suivante depuis le dossier nodeatlas-website :

> node generate-website.js

Nous faisons tourner la version serverless de nos fichiers avec un simple serveur web équivalent de ce que donnera le site une fois sur GitHub.

Note : sous Windows, en renommant generate-website.js en generate-website.na et en expliquant que les fichiers .na s'ouvrent avec node.exe on peut se passer de la commande précédente et simplement double cliquer sur generate-website.na pour lancer la procédure !

Travailler avec des fichiers Less et Stylus

Nous allons à présent mettre en place tous les fichiers client qui vont permettre d'habiller notre documentation et de permettre à l'utilisateur d'intéragir avec. Nous allons suivre l'architecture et les normes indiqués ici pour cela. Nous parlons donc ici des fichiers CSS ainsi que des fichiers JavaScript.

Mise en place des CSS et des JS

Jusqu'à présent, nous avons travaillé avec le fichier javascripts/common.js. Nous allons ajouter à cela, un fichier JavaScript par composant effectif dans le HTML. Nous allons également créer un équivalent de chaque composant en CSS et un fichier commun stylesheets/common.css. Nous obtenons donc la nouvelle arborescence suivante :

nodeatlas-website/
┊┉
├─ assets/
│  ┊┉
│  ├─ javascripts/
│  │  ├─ common.js
│  │  ├─ cmpt.header.js
│  │  ├─ cmpt.navigation.js
│  │  └─ cmpt.content.js
│  └─ stylesheets/
│     ├─ common.css
│     ├─ cmpt.header.css
│     ├─ cmpt.navigation.css
│     └─ cmpt.content.css
┊┉

Nous allons inclure ces fichiers en pied de la balise head pour les CSS et en pied de body pour les JS.

views/partials/head.htm

<!DOCTYPE html>
<html lang="<?= languageCode ?>">
    <head>
        <meta charset="utf-8" />
        <title>NodeAtlas</title>

        <base href="<?= urlBasePath ?>/" />

        <meta name="description" content="NodeAtlas est un Framework JavaScript MVC(2) côté serveur." />
        <meta name="geo.placename" content="Annecy, Haute-Savoie, France" />

        <meta name="robots" content="index, follow" />

        <!--[if IE]><meta name="viewport" content="width=device-width, user-scalable=no" /><![endif]-->
        <!--[if !IE]><!--><meta name="viewport" content="initial-scale=1.0, user-scalable=no" /><!--<![endif]-->

        <!-- Inclusion des fichiers CSS -->
        <link rel="stylesheet" href="stylesheets/common.css">
        <link rel="stylesheet" href="stylesheets/cmpt.header.css">
        <link rel="stylesheet" href="stylesheets/cmpt.navigation.css">
        <link rel="stylesheet" href="stylesheets/cmpt.content.css">
    </head>
    <body data-content="<?= webconfig._content ?>" data-subpath="<?= webconfig.urlRelativeSubPath ?>">
        <div class="layout">
            <noscript class="no-js cmpt">
                <div class="no-js--inner">
                    <p><?- common.noJs ?></p>
                </div>
            </noscript>

views/partials/foot.htm

            <!-- Inclusion des fichiers JS -->
            <script src="javascripts/cmpt.header.js"></script>
            <script src="javascripts/cmpt.navigation.js"></script>
            <script src="javascripts/cmpt.content.js"></script>
            <script src="javascripts/common.js"></script>
        </div>
    </body>
</html>

Voici un contenu provisoire que nous allons attribuer à chacun des fichiers CSS :

assets/stylesheets/common.css

* {
    font-weight: bold;
}

assets/stylesheets/cmpt.header.css

.header {
    color: red;
}

assets/stylesheets/cmpt.navigation.css

.navigation,
.navigation a {
    color: green;
}

assets/stylesheets/cmpt.content.css

.content,
.content a {
    color: orange;
}

ainsi que le contenu des fichiers JavaScript suivant :

assets/javascripts/common.js

// On se créer (ou surcharge) les namespaces (existant ou non).
// Un namespace globale au site que l'on nomme `website`.
var website = window.website || {};

// Un namespace réservé aux composants que l'on nomme `website.component`.
website.component = website.component || {};

// On encapsule les mécanismes des fonctions globales
// pour éviter la collision de variable.
// `publics` est ici l'équivalent de `website`.
(function (publics) {

    // On fait de `xhrRequest` une fonction globale `website.xhrRequest`.
    publics.xhrRequest = function(url, next) {
        var request = new XMLHttpRequest();

        if (location.protocol !== "file:") {
            request.open("GET", url, true);
            request.send();
        } else {
            return next(new Error("Impossible to use AJAX in file system mode."));
        }

        request.addEventListener("load", function () {
            if (request.status < 200 && request.status >= 400) {
                return next(new Error("The server was reached, but with no correct response."));
            }
            next(null, request.responseText);
        });

        request.addEventListener("error", function () {
            return next(new Error("The server is unreachable."));
        });
    };

    // On fait de `xhrFallback` une fonction globale `website.xhrFallback`.
    publics.xhrFallback = function (url) {
        location.href = encodeURIComponent(url) + ".html";
    };

    // On créer une fonction d'initialisation pour tout le site.
    // Qui va charger tous les composants.
    publics.init = function () {
        var links = document.querySelectorAll(".navigation--home a, .navigation--menu a"),
            fragmentPath = document.body.getAttribute("data-content"),
            urlRelativeSubPath = document.body.getAttribute("data-subpath");

        // On charge chaque comportement de composant.
        (new website.component.Header()).init();
        (new website.component.Navigation()).init();
        (new website.component.Content()).init(links, fragmentPath, urlRelativeSubPath);
    };

// On passe l'objet website pour l'alimenter avec des fonctions globales.
}(website));

website.init();

assets/javascripts/cmpt.header.js

// On se créer (ou surcharge) les namespaces (existant ou non).
var website = window.website || {};
website.component = website.component || {};

// On créer un constructeur Header pour les composants `.header`.
website.component.Header = function () {
    var publics = this;

    publics.name = "header";

    publics.init = function () {};
};

assets/javascripts/cmpt.navigation.js

// On se créer (ou surcharge) les namespaces (existant ou non).
var website = window.website || {};
website.component = website.component || {};

// On créer un constructeur Navigation pour les composants `.navigation`.
website.component.Navigation = function () {
    var publics = this;

    publics.name = "navigation";

    publics.init = function () {};
};

assets/javascripts/cmpt.content.js

// On se créer (ou surcharge) les namespaces (existant ou non).
var website = window.website || {};
website.component = website.component || {};

// On créer un constructeur Content pour les composants `.content`.
website.component.Content = function () {
    var publics = this;

    publics.name = "content";

    // On créer une fonction d'instance `updateContentByClick`
    // en passant en paramètre le nécessaire pour qu'elle fonctionne.
    publics.updateContentByClick = function (links, fragmentPath, urlRelativeSubPath) {
        [].forEach.call(links, function (link) {
            link.addEventListener("click", function (e) {
                var urn = link.getAttribute("href").replace(".html", "");
                e.preventDefault();

                website.xhrRequest(fragmentPath + encodeURIComponent(urn) + ".htm", function (err, response) {
                    if (err) {
                        return website.xhrFallback(urn);
                    }

                    history.pushState(urn, null, urlRelativeSubPath + "/" + urn + ".html");

                    // On se sert d'un nom de classe dynamique et publique pour le changer en cas de besoin.
                    document.getElementsByClassName(publics.name + "--inner")[0].innerHTML = response;
                });
            });
        });
    };

    // On créer une fonction d'instance `updateContentByHistoryBack`.
    publics.updateContentByHistoryBack = function () {
        window.addEventListener("popstate", function (e) {
            if (e.state) {
                website.xhrRequest("content/" + encodeURIComponent(e.state) + ".htm", function (err, response) {
                    if (err) {
                        return website.xhrFallback(e.state);
                    }

                    // On se sert d'un nom de classe dynamique et publique pour le changer en cas de besoin.
                    document.getElementsByClassName(publics.name + "--inner")[0].innerHTML = response;
                });
            } else {
                history.back();
            }
        });
    };

    // On créer une fonction d'initialisation pour la classe, un raccourci.
    // Et on l'alimente et execute toutes les fonctions utiles.
    publics.init = function (links, fragmentPath, urlRelativeSubPath) {
        publics.updateContentByClick(links, fragmentPath, urlRelativeSubPath);
        publics.updateContentByHistoryBack();
    };
};

Puis, en faisant tourner le projet français depuis nodeatlas-website :

> node-atlas --browse index.html

Nous constatons dans l'onglet « Network » (F12) que les fichiers en questions sont bien chargés (8 requêtes plus celle de la page en elle-même).

Tous les fichiers chargés

Preprocesseur CSS Stylus transparent

Nous allons à présent expliquer comment utiliser les préprocesseurs avec NodeAtlas. Le fonctionnement avec Less est exactement le même mais nous allons ici voir cela avec Stylus.

Il va être question ici d'activer Stylus dans nos webconfigs comme ceci :

webconfig.json

{
    "stylus": true,
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières"
}

et comme cela :

webconfig.en.json

{
    "stylus": true,
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents"
}

En fait avec NodeAtlas, l'utilisation de Less ou Stylus est totalement transparente pendant la phase de développement. Il suffit de renommer tous vos .css en .styl (ou .less pour Less) et le moteur se chargera, à chaque fois qu'un fichier .css sera demander, de compiler sa version .styl en CSS et d'en renvoyer son résultat .css. Cela signifie que du point de vu de vos fichiers HTML, c'est toujours des .css que vous réclamez.

Nous allons changer tous nos fichiers CSS en fichier Stylus (avec la syntaxe qui va bien). Nous allons également pour l'occasion créer un fichier de variables que nous allons pouvoir inclure dans chaque fichier pour profiter de fonctions communes. Cela nous donne maintenant :

nodeatlas-website/
┊┉
├─ assets/
│  ┊┉
│  └─ stylesheets/
│     ├─ variables.styl
│     ├─ common.styl
│     ├─ cmpt.header.styl
│     ├─ cmpt.navigation.styl
│     └─ cmpt.content.styl
┊┉

avec les contenus provisoires suivants :

assets/stylesheets/variables.styl

make-color(my-color)
    color my-color;
    border 1px solid my-color
    padding: 10px;
    margin: 10px
    -webkit-border-radius 4px
            border-radius 4px

assets/stylesheets/common.styl

@import "variables"

*
    font-weight bold

assets/stylesheets/cmpt.header.styl

@import "variables"

.header
    make-color red

assets/stylesheets/cmpt.navigation.styl

@import "variables"

.navigation
    make-color green

    a
        color green

assets/stylesheets/cmpt.content.styl

@import "variables"

.content
    make-color orange

    a
        color orange

Puis, en faisant tourner le projet français depuis nodeatlas-website :

> node-atlas --browse index.html

vous pourrez ainsi constater les CSS générés dans votre dossier assets/stylesheets/.

Preprocesseur CSS Stylus à la Génération

La transparence nécessite que le fichier .css soit réclamé en HTTP pour que le serveur le transforme. Cependant, pour mettre à jour vos fichiers Stylus lors de l'appel de --generate (sans donc faire démarrer le serveur), il va falloir référencer les fichiers que vous souhaitez compiler en CSS dans le webconfig.

Ainsi en changeant nos deux webconfigs comme suit, cela sera possible lors de l'appel de notre fichier generate-website.js par exemple.

webconfig.json

{
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières"
}

webconfig.en.json

{
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents"
}

Effacer vos fichiers .css de assets/stylesheets/ et lancez la commande :

> node generate-website.js

Vous verrez que les fichiers de travail dans nodeatlas-website contiennent bien les fichiers .css compilés à partir des .styl et que dans dist les fichiers CSS sont également bien présent. Un jeu d'enfant !

Et Less ?

Vous pouvez faire exactement la même chose avec Less. Il vous suffit de changer toutes les appelations .styl en .less et stylus en less. Bien entendu, la syntaxe des fichiers sera donc celle de Less.

Et en bonus, les deux peuvent fonctionner ensemble ! Le moteur cherche des .styl et des .less en même temps.

Minifier, Obfusquer et Optimiser les CSS, JS et Images

Pour des soucis de performance, bien que nous allions travailler dans la version originale de chaque fichier, la sortie produite devra être optimisée et par conséquent illisible. Tout ce processus habituellement fait à la main avec divers outils ou encore automatisé avec un coup de pousse, par exemple, de gulp est automatique dans NodeAtlas avec l'aide du webconfig.

Minifier les fichiers CSS

Dans un premier temps, nous allons minifier et placer dans un seul fichier le contenu CSS de notre site. Nous allons donc créer un Bundle spécifique aux Stylesheets en donnant à une clé un tableau d'élément. La clé représente le chemin de sortie et chaque élément représente un fichier d'entré :

webconfig.json

{
    "cssBundlingBeforeResponse": true,
    "bundles": {
        "stylesheets": {
            "stylesheets/common.min.css": [
                "stylesheets/common.css",
                "stylesheets/cmpt.header.css",
                "stylesheets/cmpt.navigation.css",
                "stylesheets/cmpt.content.css"
            ]
        }
    },
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières"
}

webconfig.en.json

{
    "cssBundlingBeforeResponse": true,
    "bundles": {
        "stylesheets": {
            "stylesheets/common.min.css": [
                "stylesheets/common.css",
                "stylesheets/cmpt.header.css",
                "stylesheets/cmpt.navigation.css",
                "stylesheets/cmpt.content.css"
            ]
        }
    },
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents"
}

Enfin, grâce à cssBundlingBeforeResponse, nous allons obtenir des fichiers CSS à jour et compilé après chaque chargement de page. Il ne nous reste donc plus qu'à demander la version minifiée des CSS plutôt que les fichiers d'origines :

views/partials/head.htm

<!DOCTYPE html>
<html lang="<?= languageCode ?>">
    <head>
        <meta charset="utf-8" />
        <title>NodeAtlas</title>

        <base href="<?= urlBasePath ?>/" />

        <meta name="description" content="NodeAtlas est un Framework JavaScript MVC(2) côté serveur." />
        <meta name="geo.placename" content="Annecy, Haute-Savoie, France" />

        <meta name="robots" content="index, follow" />

        <!--[if IE]><meta name="viewport" content="width=device-width, user-scalable=no" /><![endif]-->
        <!--[if !IE]><!--><meta name="viewport" content="initial-scale=1.0, user-scalable=no" /><!--<![endif]-->

        <!-- Inclusion du fichiers minifié de tous les CSS. -->
        <link rel="stylesheet" href="stylesheets/common.min.css">
    </head>
    <body data-content="<?= webconfig._content ?>" data-subpath="<?= webconfig.urlRelativeSubPath ?>">
        <div class="layout">
            <noscript class="no-js cmpt">
                <div class="no-js--inner">
                    <p><?- common.noJs ?></p>
                </div>
            </noscript>

Lançons donc depuis le dossier nodeatlas-website la commande :

> node-atlas --browse index.html

et constatons que tout est en order : compilation des Stylus vers les CSS, minification des .css vers le .min.css avant chaque affichage de page.

La génération avec

> node generate-website.js

fonctionne également.

Offusquer les JavaScript

Attardons nous à présent sur les JavaScript. Nous allons offusquer et placer dans un seul fichier le contenu JavaScript de notre site. Nous allons donc ajouter un Bundle spécifique aux JavaScript en donnant à une clé un tableau d'élément. La clé représente le chemin de sortie et chaque élément représente un fichier d'entré :

webconfig.json

{
    "cssBundlingBeforeResponse": true,
    "jsBundlingBeforeResponse": true,
    "bundles": {
        "stylesheets": {
            "stylesheets/common.min.css": [
                "stylesheets/common.css",
                "stylesheets/cmpt.header.css",
                "stylesheets/cmpt.navigation.css",
                "stylesheets/cmpt.content.css"
            ]
        },
        "javascripts": {
            "javascripts/common.min.js": [
                "javascripts/cmpt.header.js",
                "javascripts/cmpt.navigation.js",
                "javascripts/cmpt.content.js",
                "javascripts/common.js"
            ]
        }
    },
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières"
}

webconfig.en.json

{
    "cssBundlingBeforeResponse": true,
    "jsBundlingBeforeResponse": true,
    "bundles": {
        "stylesheets": {
            "stylesheets/common.min.css": [
                "stylesheets/common.css",
                "stylesheets/cmpt.header.css",
                "stylesheets/cmpt.navigation.css",
                "stylesheets/cmpt.content.css"
            ]
        },
        "javascripts": {
            "javascripts/common.min.js": [
                "javascripts/cmpt.header.js",
                "javascripts/cmpt.navigation.js",
                "javascripts/cmpt.content.js",
                "javascripts/common.js"
            ]
        }
    },
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents"
}

Comme précédemment, grâce à jsBundlingBeforeResponse, nous allons obtenir des fichiers JavaScript à jour et compilés après chaque chargement de page. Il ne nous reste donc plus qu'à demander la version minifiée des JavaScript plutôt que les fichiers d'origines :

views/partials/foot.htm

            <!-- Inclusion du fichiers minifié de tous les JavaScript. -->
            <script src="javascripts/common.min.js"></script>
        </div>
    </body>
</html>

Lançons donc depuis le dossier nodeatlas-website la commande :

> node-atlas --browse index.html

et constatons que l'offuscation des .js vers le .min.js avant chaque affichage de page.

La génération avec

> node generate-website.js

fonctionne tout autant.

Optimiser vos Images

Passons maintenant à l'optimisation d'image. NodeAtlas propose une optimisation sans perte pour obtenir les images les moins lourdes en conservant une qualité égale. Vous pourrez à souhait utiliser d'autre module npm pour faire bien plus.

Dans un premier temps, nous allons ajouter l'image suivante au projet et vous pourrez la télécharger ici.

Ce qui nous donne la nouvelle arborescence complète suivante :

dist/
└─ english/
nodeatlas-website/
├─ assets/
│  ├─ media/
│  │  └─ images/
│  │     └─ ex-logo-node-atlas.png
│  ├─ content/
│  │  └─ english/
│  ├─ javascripts/
│  │  ├─ common.js
│  │  ├─ cmpt.header.js
│  │  ├─ cmpt.navigation.js
│  │  └─ cmpt.content.js
│  └─ stylesheets/
│     ├─ common.css
│     ├─ cmpt.header.css
│     ├─ cmpt.navigation.css
│     └─ cmpt.content.css
├─ controllers/
│  ├─ common.js
│  ├─ content.js
│  └─ index.js
├─ views/
│  ├─ content.htm
│  └─ partials/
│     ├─ content.htm
│     ├─ head.htm
│     ├─ navigation.htm
│     ├─ header.htm
│     └─ foot.htm
├─ variations/
│  └─ common.json
│     └─ en
│        └─ common.json
├─ webconfig.json
├─ webconfig.en.json
├─ README.md
└─ README.en.md

et les nouveaux fichiers de variations et vues suivants :

variations/common.json

{
    "title": {
        "alt": "NodeAtlas",
        "src": "media/images/ex-logo-node-atlas.png"
    },
    "content": "Contenu pour la documentation.",
    "noJs": "Le JavaScript n'est pas activé.",
    "home": {
        "title": "Accueil",
        "url": "index.html"
    },
    "lang": {
        "title": "English",
        "url": "english/index.html"
    },
    "menu": [{
        "title": "Présentation",
        "url": "avant-propos.html",
        "menu": [{
            "title": "Installation",
            "url": "installation.html"
        }, {
            "title": "Démarrage rapide",
            "url": "commencer-avec-nodeatlas.html"
        }]
    }, {
        "title": "View et Template",
        "url": "partie-view-et-template.html"
    }, {
        "title": "Controller et Model",
        "url": "partie-controller-et-model.html"
    }, {
        "title": "Route et Plus",
        "url": "pour-aller-plus-loin.html",
        "menu": [{
            "title": "API",
            "url": "api-nodeatlas-comme-module-npm.html"
        }, {
            "title": "CLI",
            "url": "cli-commandes-de-lancement.html"
        }, {
            "title": "Simple Serveur",
            "url": "nodeatlas-comme-simple-serveur-web.html"
        }, {
            "title": "Développement",
            "url": "environnement-de-d%C3%A9veloppement.html"
        }, {
            "title": "Production",
            "url": "environnement-de-production.html"
        }]
    }, {
        "title": "Et les autres ?",
        "url": "plus-sur-nodeatlas.html"
    }]
}

variations/en/common.json

{
    "title": {
        "alt": "NodeAtlas",
        "src": "media/images/ex-logo-node-atlas.png"
    },
    "content": "Content for documentation.",
    "noJs": "No JavaScript enabled.",
    "home": {
        "title": "Home",
        "url": "index.html"
    },
    "lang": {
        "title": "Français",
        "url": "../index.html"
    },
    "menu": [{
        "title": "Overview",
        "url": "overview.html",
        "menu": [{
            "title": "Installing",
            "url": "installing.html"
        }, {
            "title": "Get Started",
            "url": "start-with-nodeatlas.html"
        }]
    }, {
        "title": "View and Template",
        "url": "view-and-template-part.html"
    }, {
        "title": "Controller and Model",
        "url": "controller-and-model-part.html"
    }, {
        "title": "Route and More",
        "url": "more-features.html",
        "menu": [{
            "title": "API",
            "url": "api-nodeatlas-as-npm-module.html"
        }, {
            "title": "CLI",
            "url": "cli-running-commands.html"
        }, {
            "title": "Simple Server",
            "url": "nodeatlas-as-a-simple-web-server.html"
        }, {
            "title": "Development",
            "url": "development-environment.html"
        }, {
            "title": "Production",
            "url": "production-environment.html"
        }]
    }, {
        "title": "And others?",
        "url": "more-about-nodeatlas.html"
    }]
}

views/partials/header.htm

            <header class="header cmpt">
                <div class="header--inner">
                    <div class="header--title">

                        <!-- Remplacement du texte par l'image. -->
                        <h1><img src="<?= common.title.src ?>" alt="<?= common.title.alt ?>"></h1>
                    </div>
                </div>
            </header>

Optimisons à présent notre image media/images/ex-logo-node-atlas.png :

webconfig.json

{
    "imgOptimizationsBeforeResponse": true,
    "cssBundlingBeforeResponse": true,
    "jsBundlingBeforeResponse": true,
    "optimizations": {
        "images": {
            "media/images/ex-logo-node-atlas.png": "media/images/min/"
        }
    },
    "bundles": {
        "stylesheets": {
            "stylesheets/common.min.css": [
                "stylesheets/common.css",
                "stylesheets/cmpt.header.css",
                "stylesheets/cmpt.navigation.css",
                "stylesheets/cmpt.content.css"
            ]
        },
        "javascripts": {
            "javascripts/common.min.js": [
                "javascripts/cmpt.header.js",
                "javascripts/cmpt.navigation.js",
                "javascripts/cmpt.content.js",
                "javascripts/common.js"
            ]
        }
    },
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières"
}

webconfig.en.json

{
    "imgOptimizationsBeforeResponse": true,
    "cssBundlingBeforeResponse": true,
    "jsBundlingBeforeResponse": true,
    "optimizations": {
        "images": {
            "media/images/ex-logo-node-atlas.png": "media/images/min/"
        }
    },
    "bundles": {
        "stylesheets": {
            "stylesheets/common.min.css": [
                "stylesheets/common.css",
                "stylesheets/cmpt.header.css",
                "stylesheets/cmpt.navigation.css",
                "stylesheets/cmpt.content.css"
            ]
        },
        "javascripts": {
            "javascripts/common.min.js": [
                "javascripts/cmpt.header.js",
                "javascripts/cmpt.navigation.js",
                "javascripts/cmpt.content.js",
                "javascripts/common.js"
            ]
        }
    },
    "stylus": {
        "files": [
            "stylesheets/common.styl",
            "stylesheets/cmpt.header.styl",
            "stylesheets/cmpt.navigation.styl",
            "stylesheets/cmpt.content.styl"
        ]
    },
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": {
        "/index.html": {
            "view": "content.htm",
            "controller": "index.js"
        }
    },
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents"
}

Comme précédemment, grâce à imgOptimizationsBeforeResponse, nous allons obtenir des images optimisées après chaque chargement de page. Il ne nous reste donc plus qu'à demander la version optimisées des images plutôt que les images d'origines :

{
    "title": {
        "alt": "NodeAtlas",
        "src": "media/images/min/ex-logo-node-atlas.png"
    },
    "content": "Contenu pour la documentation.",
    "noJs": "Le JavaScript n'est pas activé.",
    "home": {
        "title": "Accueil",
        "url": "index.html"
    },
    "lang": {
        "title": "English",
        "url": "english/index.html"
    },
    "menu": [{
        "title": "Présentation",
        "url": "avant-propos.html",
        "menu": [{
            "title": "Installation",
            "url": "installation.html"
        }, {
            "title": "Démarrage rapide",
            "url": "commencer-avec-nodeatlas.html"
        }]
    }, {
        "title": "View et Template",
        "url": "partie-view-et-template.html"
    }, {
        "title": "Controller et Model",
        "url": "partie-controller-et-model.html"
    }, {
        "title": "Route et Plus",
        "url": "pour-aller-plus-loin.html",
        "menu": [{
            "title": "API",
            "url": "api-nodeatlas-comme-module-npm.html"
        }, {
            "title": "CLI",
            "url": "cli-commandes-de-lancement.html"
        }, {
            "title": "Simple Serveur",
            "url": "nodeatlas-comme-simple-serveur-web.html"
        }, {
            "title": "Développement",
            "url": "environnement-de-d%C3%A9veloppement.html"
        }, {
            "title": "Production",
            "url": "environnement-de-production.html"
        }]
    }, {
        "title": "Et les autres ?",
        "url": "plus-sur-nodeatlas.html"
    }]
}

variations/en/common.json

{
    "title": {
        "alt": "NodeAtlas",
        "src": "media/images/min/ex-logo-node-atlas.png"
    },
    "content": "Content for documentation.",
    "noJs": "No JavaScript enabled.",
    "home": {
        "title": "Home",
        "url": "index.html"
    },
    "lang": {
        "title": "Français",
        "url": "../index.html"
    },
    "menu": [{
        "title": "Overview",
        "url": "overview.html",
        "menu": [{
            "title": "Installing",
            "url": "installing.html"
        }, {
            "title": "Get Started",
            "url": "start-with-nodeatlas.html"
        }]
    }, {
        "title": "View and Template",
        "url": "view-and-template-part.html"
    }, {
        "title": "Controller and Model",
        "url": "controller-and-model-part.html"
    }, {
        "title": "Route and More",
        "url": "more-features.html",
        "menu": [{
            "title": "API",
            "url": "api-nodeatlas-as-npm-module.html"
        }, {
            "title": "CLI",
            "url": "cli-running-commands.html"
        }, {
            "title": "Simple Server",
            "url": "nodeatlas-as-a-simple-web-server.html"
        }, {
            "title": "Development",
            "url": "development-environment.html"
        }, {
            "title": "Production",
            "url": "production-environment.html"
        }]
    }, {
        "title": "And others?",
        "url": "more-about-nodeatlas.html"
    }]
}

Lançons donc depuis le dossier nodeatlas-website la commande :

> node-atlas --browse index.html

et constatons que l'optimisation des media/images vers le media/images/min/ avant chaque affichage de page.

La génération avec

> node generate-website.js

fonctionne tout autant.

Webconfig avec fichiers partagés

Ce qui est génant avec nos précédents exemples, c'est que pour quelques valeurs différentes, nous devons maintenir deux webconfigs avec énormément d'options. Nous allons voir ici comment simplement déporter les configurations dans des fichiers différents afin de ne pas avoir de redondance de configuration. Ainsi nos deux webconfigs webconfig.json et webconfig.en.json deviennent :

webconfig.json

{
    "imgOptimizationsBeforeResponse": true,
    "cssBundlingBeforeResponse": true,
    "jsBundlingBeforeResponse": true,
    "optimizations": "optimizations.json",
    "bundles": "bundles.json",
    "stylus": "stylus.json",
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": "routes.json",
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières"
}

webconfig.en.json

{
    "imgOptimizationsBeforeResponse": true,
    "cssBundlingBeforeResponse": true,
    "jsBundlingBeforeResponse": true,
    "optimizations": "optimizations.json",
    "bundles": "bundles.json",
    "stylus": "stylus.json",
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": "routes.json",
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents"
}

Avec les nouveaux fichiers suivants :

optimizations.json

{
    "images": {
        "media/images/logo-node-atlas.png": "media/images/min/"
    }
}

bundles.json

{
    "stylesheets": {
        "stylesheets/common.min.css": [
            "stylesheets/common.css",
            "stylesheets/cmpt.header.css",
            "stylesheets/cmpt.navigation.css",
            "stylesheets/cmpt.content.css"
        ]
    },
    "javascripts": {
        "javascripts/common.min.js": [
            "javascripts/cmpt.header.js",
            "javascripts/cmpt.navigation.js",
            "javascripts/cmpt.content.js",
            "javascripts/common.js"
        ]
    }
}

stylus.json

{
    "files": [
        "stylesheets/common.styl",
        "stylesheets/cmpt.header.styl",
        "stylesheets/cmpt.navigation.styl",
        "stylesheets/cmpt.content.styl"
    ]
}

routes.json

{
    "/index.html": {
        "view": "content.htm",
        "controller": "index.js"
    }
}

donnant donc le nouvel ensemble de fichier suivant :

nodeatlas-website/
┊┉
├─ webconfig.json
├─ webconfig.en.json
├─ optimizations.json
├─ bundles.json
├─ stylus.json
├─ routes.json
├─ README.md
├─ README.en.md
┊┉

Lançons donc depuis le dossier nodeatlas-website la commande :

> node-atlas --browse index.html

et constatons que la génération avec

> node generate-website.js

fonctionne toujours.

Webconfig différent en développement et production

Vous aurez probablement remarqué que depuis les ajouts des mécanismes pour optimiser le code, les temps de réponse sont devenus plus lent. C'est parceque toutes l'optimisation se refait au chargement de chaque page. Nous allons voir ici comment :

  • Développer avec les fichiers originaux sans mécanisme d'optimisation.
  • Générer une version finale des fichiers optimisés.

Pour cela nous allons utiliser différents webconfigs.

Version Développement

Tout d'abord arrêtons d'utiliser l'optimisation après chaque chargement de page et n'utilisons pas d'instruction pour la minification et l'optimisation. Nous allons créer une variable pour indiquer que la version actuelle n'utilisera pas les fichiers minifiés.

Ensuite, nous allons également utiliser la propritété httpPort pour faire tourner nos deux versions côte à côte. Nous allons aussi utiliser une variable pour indiquer derrière quel port va tourner l'autre langue pour chaque webconfig.

webconfig.json

{
    "httpPort": 7777,
    "stylus": "stylus.json",
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "routes": "routes.json",
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières",
    "_otherPort": 7778,
    "_min": ""
}

webconfig.en.json

{
    "httpPort": 7778,
    "stylus": "stylus.json",
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "urlRelativeSubPath": "english",
    "routes": "routes.json",
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents",
    "_otherPort": 7777,
    "_min": ""
}

Nous allons également modifier conditionnellement les vues suivantes pour qu'elles affichent les fichiers de travail avec ses webconfigs et les fichiers de productions avec les webconfigs de génération que nous allons créer plus loin.

views/partials/head.htm

<!DOCTYPE html>
<html lang="<?= languageCode ?>">
    <head>
        <meta charset="utf-8" />
        <title>NodeAtlas</title>

        <base href="<?= urlBasePath ?>/" />

        <meta name="description" content="NodeAtlas est un Framework JavaScript MVC(2) côté serveur." />
        <meta name="geo.placename" content="Annecy, Haute-Savoie, France" />

        <meta name="robots" content="index, follow" />

        <!--[if IE]><meta name="viewport" content="width=device-width, user-scalable=no" /><![endif]-->
        <!--[if !IE]><!--><meta name="viewport" content="initial-scale=1.0, user-scalable=no" /><!--<![endif]-->

        <!-- Inclusion du fichiers minifié après génération. -->
        <? if (webconfig._min) { ?>
        <link rel="stylesheet" href="stylesheets/common.min.css">

        <!-- Inclusion du fichiers standar pendant le développement. -->
        <? } else { ?>
        <link rel="stylesheet" href="stylesheets/common.css">
        <link rel="stylesheet" href="stylesheets/cmpt.header.css">
        <link rel="stylesheet" href="stylesheets/cmpt.navigation.css">
        <link rel="stylesheet" href="stylesheets/cmpt.content.css">
        <? }  ?>
    </head>
    <body data-content="<?= webconfig._content ?>" data-subpath="<?= webconfig.urlRelativeSubPath ?>">
        <div class="layout">
            <noscript class="no-js cmpt">
                <div class="no-js--inner">
                    <p><?- common.noJs ?></p>
                </div>
            </noscript>

views/partials/foot.htm

            <!-- Inclusion du fichiers minifié après génération. -->
            <? if (webconfig._min) { ?>
            <script src="javascripts/common.min.js"></script>

            <!-- Inclusion du fichiers standar pendant le développement. -->
            <? } else { ?>
            <script src="javascripts/cmpt.header.js"></script>
            <script src="javascripts/cmpt.navigation.js"></script>
            <script src="javascripts/cmpt.content.js"></script>
            <script src="javascripts/common.js"></script>
            <? }  ?>
        </div>
    </body>
</html>

views/partials/header.htm

            <header class="header cmpt">
                <div class="header--inner">
                    <div class="header--title">

                        <!-- On utilise la version non minifiée en local. -->
                        <h1><img src="<?= (!webconfig._min) ? common.title.src.replace(webconfig._min, "") : common.title.src ?>" alt="<?= common.title.alt ?>"></h1>
                    </div>
                </div>
            </header>

views/partials/navigation.htm

            <nav class="navigation cmpt">
                <div class="navigation--inner">
                    <div class="navigation--lang">

                        <!-- On ajoute la base si le site est en développement, en attribuant le port de l'autre version. -->
                        <a href="<?= (!webconfig._min) ? urlBasePath.replace(webconfig.httpPort, webconfig._otherPort) : '' ?>/<?= common.lang.url ?>" title="<?= common.lang.title ?>"><?- common.lang.title ?></a>
                    </div>
                    <div class="navigation--home">
                        <a href="<?= common.home.url ?>" title="<?= common.home.title ?>"><?- common.home.title ?></a>
                    </div>
                    <div class="navigation--menu">
                        <ul>
                            <? for (var i = 0; i < common.menu.length; i++) { ?>
                            <li>
                                <a href="<?= common.menu[i].url ?>" title="<?= common.menu[i].title ?>"><?- common.menu[i].title ?></a>
                                <? if (common.menu[i].menu) { ?>
                                <ul>
                                    <? for (var j = 0; j < common.menu[i].menu.length; j++) { ?>
                                    <li>
                                        <a href="<?= common.menu[i].menu[j].url ?>" title="<?= common.menu[i].menu[j].title ?>"><?- common.menu[i].menu[j].title ?></a>
                                    </li>
                                    <? } ?>
                                </ul>
                                <? } ?>
                            </li>
                            <? } ?>
                        </ul>
                    </div>
                </div>
            </nav>

Et pour lancer les deux versions en même temps, nous allons créer un fichier develop-website.js :

develop-website.js

// On récupère l'API NodeAtlas.
var nodeAtlas = require("node-atlas"),

    // On créé une instance pour générer la version française.
    versionFrench = new nodeAtlas(),

    // On créé une instance pour générer la version internationale.
    versionEnglish = new nodeAtlas();

// On paramètre la version française et on la lance.
versionFrench.run({
    "browse": true
});

// On paramètre la version internationale et on la lance.
versionEnglish.run({
    "webconfig": "webconfig.en.json"
});

Et nous lançons la commande suivante depuis le dossier nodeatlas-website :

> node develop-website.js

Nous faisons tourner la version française et internationale de développement de nos fichiers aux adresses http://localhost:7777/ et http://localhost:7778/english/.

Note : sous Windows, en renommant develop-website.js en develop-website.na et en expliquant que les fichiers .na s'ouvrent avec node.exe on peut se passer de la commande précédente et simplement double cliquer sur develop-website.na pour lancer la procédure !

Version Production

Nous allons maintenant créer les webconfigs de génération qui eux vont bien avoir les Bundles et les Optimizations, qui utiliseront la variable _min du webconfig à true et qui seront derrière un unique site. Voyez plutôt :

webconfig.generate.json

{
    "optimizations": "optimizations.json",
    "bundles": "bundles.json",
    "stylus": "stylus.json",
    "index": true,
    "languageCode": "fr",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/",
    "assetsCopy": true,
    "output": true,
    "routes": "routes.json",
    "_readme": "README.md",
    "_content": "content/",
    "_toc": "table-des-matières",
    "_min": true
}

webconfig.generate.en.json

{
    "optimizations": "optimizations.json",
    "bundles": "bundles.json",
    "stylus": "stylus.json",
    "index": true,
    "languageCode": "en",
    "controller": "common.js",
    "variation": "common.json",
    "serverlessRelativePath": "../dist/english/",
    "assetsCopy": true,
    "output": true,
    "urlRelativeSubPath": "english",
    "routes": "routes.json",
    "_readme": "README.en.md",
    "_content": "content/english/",
    "_toc": "table-of-contents",
    "_min": true
}

Nous allons à présent modifier le fichier generate-website.js pour qu'ils utilisent ces nouveaux webconfigs :

generate-website.js

var nodeAtlas = require("node-atlas"),
    versionFrench = new nodeAtlas(),
    versionEnglish = new nodeAtlas(),
    versionTest = new nodeAtlas();

versionFrench.init({
    "generate": true,

    // Nouveau webconfig français pour la génération.
    "webconfig": "webconfig.generate.json"
}).generated(function() {
    versionEnglish.init({
        "generate": true,

        // Nouveau webconfig international pour la génération.
        "webconfig": "webconfig.generate.en.json"
    }).generated(function() {
        versionTest.init({
            "browse": "index.html",
            "directory": "../dist/"
        }).start();
    }).start();
}).start();

Finissons par lancer la commande suivante depuis le dossier nodeatlas-website :

> node generate-website.js

Et profitons de notre site complet, prêt à être placé sur Github depuis le dossier dist. Il suffit maintenant de faire du dossier dist un dépôt local de https://github.com/MachinisteWeb/NodeAtlas/tree/gh-pages de Github et le tour est joué !

Processus de mise en production

Pour résumer nous utilisons donc :

  • Pour développer (depuis nodeatlas-website),
> node develop-website.js
  • Pour tester la version de production en local (depuis nodeatlas-website),
> node generate-website.js
  • Pour monter le site sur GitHub (depuis dist/),

Ensemble de commande GIT.

Vous pouvez récupérer l'intégralité du code de cet article dans cette archive pour tester.

D'autres bonnes pratiques

Vous trouverez d'autres fonctionnalités de NodeAtlas sur le site officiel de NodeAtlas en attendant la rédaction du dernier article dédié :

  • aux développeur Back-end ou JavaScript en étendant le fonctionnement de NodeAtlas aux parties Contrôleur et Modèle avec une utilisations des Websockets pour les actions en temps réel ainsi qu'un tour des accès aux diverses bases de données.

Lire dans une autre langue