Making-of : un site ni statique, ni dynamique, bien au contraire

Le meilleur des deux mondes avec Nuxt, Netlify et Apify 🔥

Cette couverture manque cruellement d’originalité, promis le reste vaut le coup ! (Alejandro Escamilla on Unsplash)

C’était un vendredi soir et il était temps de repenser mon site perso après 3 ans de vie, soit une éternité sur l’Internet. L’idée était d’avoir quelque chose qui se met à jour en fonction de mon activité sur les différents réseaux pour éviter l’effet “site mort”, tout en gardant un fonctionnement simple, tant en développement qu’en maintenance.

Pas question donc d’utiliser Wordpress avec des plugins pour chaque réseau, et à l’inverse impossible de se contenter d’un bon vieux HTML si c’est pour passer 1h de mise à jour pour chaque nouvel article sur Medium.

C’était donc l’occasion idéale de tester Nuxt. Il s’agit d’un petit framework bâti sur Vue (à l’instar de Next qui l’est sur React), permettant d’une part de faciliter le démarrage d’un projet (architecture, plugins basiques, helpers en tout genre), mais surtout de générer un site statique ; et c’est ce qui va nous intéresser le plus ici.

Sommaire :

  1. Récupérer des données dynamiquement avec Apify
  2. Générer un site statique avec Nuxt
  3. Déployer facilement chez Netlify

1️⃣ Récupérer des données dynamiquement avec Apify

Apify — scrap all the things!

Le problème dans un premier temps, c’est de pouvoir récupérer les données. Certains réseaux ne le proposent pas du tout (Medium), de manière un peu complexe (Twitter), ou encore dans un format peu pratique (flux RSS chez Github). Le plus simple serait donc de scanner la page de notre profil sur chacun des sites !

Apify est un service en ligne (gratuit dans des limites que nous serons loin d’atteindre ici) qui permet d’automatiser le scrapping : c’est à dire récupérer le contenu d’un site. Comme les bons vieux aspirateurs de site, oui ! Mais en mieux.

Scrap it

La première étape consiste à créer un crawler. Celui-ci a besoin à minima d’une URL de départ, et d’une fonction en Javascript qui scanne la page et retourne un tableau. Dans notre cas nous n’aurons pas besoin de plus compliqué, mais sachez qu’il est aussi possible de suivre des liens internes par exemple, et ainsi récupérer les données de manière récurrente sur toute une arborescence ; si jamais vous vous lancez dans un clone d’Amazon, ça peut servir. Il y a des tonnes d’options qu’on ne passera pas en revue ici.

À noter que chez Medium il est impossible d’avoir une page qui répertorie nos propres articles uniquement, sans commentaires (un comble). Le plus simple est donc de créer une publication dans laquelle on ajoute manuellement nos articles, comme ici.

Après avoir renseigné l’URL, il faudra jouer avec l’inspecteur de votre navigateur préféré pour trouver les bons sélecteurs CSS. Voilà un exemple d’à quoi ressemble ma fonction en question (jQuery est proposé par défaut, mais ES6 n’est pas disponible pour l’instant) :

function pageFunction(context) {
var $ = context.jQuery
return $('div[data-post-id]').map(function() {
const thumb = $(this).find('.u-backgroundSizeCover')
return {
link: $(this).find('a[data-post-id]').attr('href'),
thumb: thumb.length && thumb.css('background-image').slice(4, -1).replace(/"/g, "") || false,
title: $(this).find('h3').text(),
excerpt: $(this).find('h3').next().text(),
date: $(this).find('[datetime]').attr('datetime')
}
}).get()
}

Ce qui nous donne un résultat ressemblant à ceci après avoir fait tourner un test du crawler :

Résultat d’un run du crawler Apify

Schedule it

Ne nous reste plus qu’à créer un scheduler et définir la fréquence à laquelle le script sera exécuté. Libre à vous de le définir en fonction de votre activité. Pour l’instant j’ai conservé la valeur @daily par défaut, mais ma fibre écolo me dit que je pourrais la réduire à une semaine… À voir à la longue.

Ne pas oublier de programmer l’exécution des crawlers

Vous pouvez aussi créer un scheduler qui lance plusieurs crawlers, ou encore refactoriser un peu tout ça avec le système des Actors (beta), des fonctions en tout genre qu’on peut lancer de la même manière, pas uniquement des crawlers ; celui-ci par exemple. Pas la peine de compliquer la chose pour l’instant en tout cas ! On y reviendra en fin d’article.

Consume it

Ne reste plus qu’à utiliser ces données ! Pour chaque crawler, dans son onglet API, plusieurs URL sont proposées en guise de webhooks : lire, modifier, supprimer… Mais surtout récupérer le résultat de la dernière exécution :

Le résultat est sous la forme d’un JSON tout ce qu’il y a de plus simple :

Cette URL contient un token qui est nécessaire de masquer du public, puisqu’on pourrait par exemple appeler la fonction de suppression grâce à celui-ci. Le problème ne se pose pas dans notre cas comme on va le voir dans la 2ème partie, puisque les contenus sont récupérés puis générés côté serveur uniquement. On verra enfin dans la 3ème partie comment cacher ces URLs dans Git.


2️⃣ Générer un site statique avec Nuxt

Si vous avez déjà un peu d’expérience avec Vue (et ses compagnons Vue-router et Vuex, optionnels), vous ne serez pas dépaysés avec Nuxt. Je ne détaillerai pas la procédure d’installation (enfantine) ni les avantages de cette solution (nombreux), mais m’attarderai plutôt sur les fonctionnalités qui nous intéressent particulièrement dans notre cas : récupérer du contenu dynamique puis en générer un site statique.

Server-side rendering et asyncData

Nuxt propose un concept de pages différentes des composants. Ou plutôt, les pages sont des composants comme les autres (dans le dossier pages plutôt que components) mais uniquement de premier niveau (dans l’arborescence des composants) et qui bénéficient d’une méthode particulière : asyncData .

Dans d’autres systèmes, on parle aussi de smart components, qui récupèrent les données, et de dumb components, qui ne font qu’utiliser les données qu’on leur transmet ou inclure d’autres dumb components.

Comme l’explique le guide (d’une grande qualité et entièrement traduit en Français d’ailleurs), la méthode asyncData est appelée avant l’instantiation du composant, et doit retourner un objet qui sera fusionné avec le reste de la propriété data. Attention, il est donc impossible d’avoir accès au composant lui-même avec this (puisqu’il n’existe pas encore) !

En utilisant Axios pour gérer les requêtes AJAX, on pourra simplement avoir une fonction de ce genre :

import axios from 'axios'
export default {
...,
async asyncData({params, error}) {
const medium = await cachios.get('https://api.apify.com/v1/KJmGFZ2mADwTHyKpp/crawlers/4jqjYdusZaPwu9bW7/lastExec?token=XXX')
    return {
medium: medium.data[0].pageFunctionResult
}
})
}

À ce propos, vous pouvez utilisez le package Cachios qui permet d’abstraire Axios avec une gestion de cache simplissime, afin d’éviter que chaque rechargement de page ne soit trop long à cause de ces appels répétitifs (à vous d’indiquer le paramètre ttl, par exemple 86400 pour une journée de cache).

Vous pouvez ensuite passer ces données comme d’habitude avec Vue dans vos composants enfants de cette manière :

<medium :articles="medium"/>

Générer une version statique

Nuxt propose deux scripts NPM pour déployer votre site : build pour compiler une version classique d’une SPA, et generate pour d’abord créer des fichiers HTML respectant votre arborescence (par défaut uniquement les routes statiques, et non pas celles qui se basent sur un ID par exemple /user/:id, à moins que vous les spécifiez).

Cette méthode s’appuie sur le SSR (Server Side Rendering), c’est à dire que Node génère vos pages tel que le ferait un navigateur (Client Side Rendering). Que ce soit en terme de performance ou de référencement, cette technique est très utile. La contrepartie, c’est d’avoir un code qui fonctionne aussi bien d’un côté que de l’autre. Attention donc à ne pas utiliser la variable window par exemple, et à choisir des packages compatibles.

La commande generate crée un dossier dist dans lequel vous trouverez comme d’habitude l’ensemble des fichiers statiques compilés, mais aussi des fichiers HTML. On pourra alors tout simplement diriger le traffic web vers ce dossier.


3️⃣ Déployer facilement chez Netlify

Déploiement continu

J’ai déjà loué les mérites de Netlify précédemment, mais je recommence volontiers ici tant je suis impressionné par la simplicité et l’efficacité de ce service. Il s’agit d’une plateforme de déploiement et d’hébergement de sites statiques. La première étape consiste à lier un dépôt Git à votre projet :

Une fois votre dépôt sélectionné, vous pouvez spécifier la commande generate à utiliser, et le dossier dist vers lequel rediriger :

Netlify commence le travail tout de suite et propose des URL de test par défaut, puis chaque push sur la branche que vous avez spécifiée déclenchera un nouveau déploiement. La liste des fonctionnalités ne s’arrête pas là et on s’attardera sur les trois dernières :

  • ajout de snippets (Google Analytics)
  • optimisation des assets (Nuxt le fait déjà) → performances
  • prerendering (idem) → SEO
  • une URL par branche (tester de nouveaux développements)
  • notifications de déploiement (Slack)
  • webhooks de build (permet de déclencher un build à l’appel de l’URL donnée)
  • variables d’environnements (pour cacher certaines données sensibles)
  • gestion des formulaires (contact)

Automatiser la mise à jour du site

On a donc maintenant un site utilisant des données dynamiques et qui est mis à jour automatiquement à chaque changement dans notre code. Mais il reste à le mettre à jour en fonction de nos contenus. Netlify propose de créer des webhooks (sous la forme “https://api.netlify.com/build_hooks/XXX”) que l’on pourra appeler pour déclencher un build sans modification de code :

Différents niveaux de complexité s’offre à vous à ce moment-là. Sachant que les données sont rechargées quotidiennement chez Apify, j’ai d’abord choisi le plus simple en déclenchant ce webhook via un cron en webservice avec EasyCron (gratuit).

J’ai ensuite utilisé la fonctionnalité Actors d’Apify pour appeler le webhook une fois les crawlers terminés en créant un nouvel Actor à partir d’un modèle public :

Dupliquer un actor public : petr_cermak/crawl-manager

Il vous suffira alors de le configurer en spécifiant les IDs des différents crawlers à exécuter, ainsi que l’URL du webhook de Netlify à appeler en fin de tâche (attention à bien préciser "parallel": 1 car l’offre gratuite ne permet pas plus) :

Configurer l’actor en fonction de vos IDs et URL

Variables d’environnement

Les différentes URLs que vous utilisez pour récupérer les données d’Apify contiennent des tokens qui doivent être masqué du public. En générant le site avec Nuxt, on les cache déjà du navigateur, mais il faut aussi l’ignorer dans votre dépôt Git.

Dans un premier temps, ajoutez le package dotenv pour pouvoir utiliser un fichier .env qui sera chargé dans l’environnement de Node puis transmis à Nuxt dans la variable process.env. Indiquer sur chaque ligne les URL de cette manière :

medium=https://api.apify.com/v1/KJmGFZ2mADwTHyKpp/crawlers/4jqjYdusZaPwu9bW7/lastExec/results?token=XXX

Ignorez ce fichier dans votre .gitignore et créez un clone .env.dist que vous ajouterez cette fois bien à l’index, mais qui contient uniquement les clés sans les valeurs, c’est à dire :

medium=

Cela permettra à d’autres développeurs ou simplement votre vous-même du futur de se rappeler les différentes valeurs nécessaires au bon fonctionnement du site ;) Enfin, vos appels dans votre méthode asyncData deviendront donc :

const medium = await axios.get(process.env.medium)

De retour chez Netlify, vous pouvez finalement charger ces valeurs dans les options de la manière suivante :

Formulaires de contact

Voilà une autre fonctionnalité bienvenue : Netlify prend en compte automatiquement les formulaires trouvés dans votre site. J’avais prévu d’utiliser le service de formulaire FormSpree mais j’ai donc pu m’en passer. La documentation est très simple et vous pouvez simplement créer un formulaire classique en prenant soin d’ajouter les éléments en gras :

<form name="contact" method="POST" netlify>
<fieldset>
<label for="name">Nom</label>
<input type="text" id="name" name="name" required>
</fieldset>
<fieldset>
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</fieldset>
<fieldset>
<label for="message">Message</label>
<textarea name="message" id="message" rows="5" required></textarea>
</fieldset>
<fieldset>
<input type="hidden" name="form-name" value="contact" />
<button type="submit">Envoyer</button>
</fieldset>
</form>

Les messages sont stockés dans l’interface de Netlify et il est bien sûr possible de les transmettre à une adresse mail. Il est aussi possible de gérer un message de confirmation personnalisée, ou encore d’envoyer le formulaire en AJAX, mais je n’ai pas jugé nécessaire de modifier la page par défaut.

Domaines et HTTPS

On se rapproche de la fin et il est temps de mettre en ligne tout ça. Encore une fois Netlify est là pour nous faciliter la tâche. Une fois votre nom de domaine acheté, le plus simple est de modifier les entrées DNS pour transférer leur gestion à Netlify. Par exemple, chez Gandi :

Après un peu d’attente (temps de propagation des DNS), vous devriez arriver à ce genre de résultat :

Vous pourrez enfin activer un certificat SSL gratuit de chez Let’s Encrypt, et forcer la redirection vers la version HTTPS de votre site :

Quand on se rappelle le prix, le temps et la complexité nécessaire pour sécuriser son site il y a encore quelques années, on ne peut que remercier Let’s Encrypt et Netlify pour ce genre de services ! Je ne l’ai pas précisé, mais tout ça est gratuit. Amazing !


Conclusion

Nous voilà donc avec un site qui allie les bénéfices du dynamique et du statique, avec en prime les avantages de l’écosystème de Vue, le tout avec un minimum d’infrastructure et de maintenance. Cependant, parmi la liste de points à améliorer que j’ai en tête, on pourrait citer :

  • dans notre cas d’un site à page unique, ne pas charger tous les fichiers JS puisqu’on pourrait se contenter de la version prérendue par Nuxt
  • mieux gérer les mises à jour pour éviter de faire tourner des serveurs chez Apify et Netlify tous les jours, potentiellement sans changement
  • avoir un système d’alerte en cas de changement de la structure HTML des pages scannées par Apify

Le (très loin d’être parfait) code source de mon site est disponible sur Github. Des remarques, des idées d’améliorations ? N’hésitez pas 🤘