Depuis que j'ai quitté Disqus pour un système de commentaires statiques, Staticman a muri avec des ajouts de fonctionnalités comme les fils de commentaires et les notifications par mail.
À l’aide des instructions fournies par Eduardo Bouças dans cette issue GitHub, je me suis lancé dans l’amélioration de l’expérience relative aux commentaires sur Made Mistakes. Voici comme j'ai procédé.
Passer à la version 2 de Staticman
Pour tirer parti de ces nouvelles fonctionnalités, il était nécessaire de migrer les paramètres de Staticman du fichier _config.yml
de Jekyll vers un nouveau fichier staticman.yml
1. Comme il n'y a eu aucun changement dans les paramètres, la transition vers la version 2 était grandement simplifiée.
comments:
allowedFields : ['name', 'email', 'url', 'message']
branch : "master"
commitMessage : "Nouveau commentaire."
filename : "commentaire-{@timestamp}"
format : "yaml"
moderation : true
path : "src/_data/comments/{options.slug}"
requiredFields : ['name', 'email', 'message']
transforms:
email : md5
generatedFields:
date:
type : "date"
options:
format : "iso8601"
Nouvelles options de configuration
Assurez-vous de jeter un œil au modèle de fichier de configuration et à la liste complète des paramètres pour vous faire une idée des possibilités de configuration.
Par exemple, vous pouvez configurer plusieurs propriétés (commentaires, avis et autres types de contenus générés par les utilisateurs), modifier le message de commit et le corps de texte de la pull request, activer les notifications par mail et bien plus à partir du fichier staticman.yml
.
Supprimer/Ajouter Staticman en tant que collaborateur
Je ne suis pas vraiment certain que l’opération suivante soit nécessaire. Je me suis heurté à des erreurs lors de mes tests de commentaires et cela a eu l’air de régler le problème. Il est possible que je me sois trompé quelque part ailleurs dans la configuration et que l’origine du problème était ailleurs…
Quoi qu’il en soit, vous pouvez toujours partager votre expérience de la mise à jour de la version 1 à la version 2 de Staticman dans les commentaires de ce billet.
- Révoquez les droits de collaboration de Staticman
v1
dans les paramètres de votre dépôt GitHub. - Ajoutez de nouveau Staticman en tant que collaborateur.
- Faites un appel sur ce endpoint de la version 2 de l’API
https://api.staticman.net/v2/connect/{votre nom d’utilisateur GitHub}/{nom de votre dépôt}
pour accepter l’invitation de collaboration.
Mettre à jour l’appel POST du formulaire de commentaires
Pour faire une requête POST
correcte à Staticman, l’attribut action
de mon formulaire de commentaire avait besoin d’une petite mise à jour. Remplacer v1
par v2
dans _includes/page__comments.html, puis suffixer avec /comments
2 et le tour était joué pour moi.
<form
id="comment-form"
class="page__form js-form form"
method="post"
action="https://api.staticman.net/v2/entry/{{ site.repository }}/{{ site.staticman.branch }}/comments"
></form>
Ajout du support des fils de commentaires
Réussir à faire marcher les commentaires imbriqués s'est révélé assez pénible. Plusieurs erreurs Liquid, plusieurs tentatives avant d’arriver à faire marcher des boucles for
à l’intérieur d’autres boucles for
, des filtres de tableau qui pétaient des trucs et tout un tas de galères font que j'ai mis un moment avant de m'en sortir.
Ajout d’un identifiant au parent
Pour imbriquer correctement les réponses, j'avais besoin de pouvoir déterminer leur hiérarchie. La v2
de Staticman possède un nouveau champ nommé options[parent]
3 qui peut être utilisé pour aider à établir cette relation. Avant d’aller plus loin, ajoutons déjà cet identifiant à mon formulaire dans un champ caché.
<input type="hidden" id="comment-parent" name="options[parent]" value="" />
Mise à jour des boucles Liquid
Afin d’éviter d’afficher des doublons, j'avais besoin d’exclure les réponses et de ne montrer que les commentaires parents dans la boucle principale. C'était le moment idéal pour utiliser le filtre where_exp
de Jekyll.
Si le champ caché options[parent]
que j'ai ajouté au formulaire fonctionne correctement, je devrais obtenir des fichiers de données de commentaires similaires à ceux-ci :
Exemple de commentaire parent
message: Ceci est le message du commentaire parent
name: Prénom Nom
email: md5g1bb3r15h
date: 2016-11-30T22:03:15.286Z
Exemple de commentaire enfant
_parent: 7
message: Ceci est un message de commentaire enfant
name: Prénom Nom
email: md5g1bb3r15h
date: 2016-11-02T05:08:43.280Z
Comme vous pouvez le voir ci-dessus, le commentaire "enfant" a une donnée _parent
renseignée à partir du champ caché options[parent]
du formulaire.
Sachant cela, j'ai tenté d’utiliser where_exp:"item","item._parent == nil"
pour créer un tableau ne contenant que les commentaires "parents".
Malheureusement, le code suivant n'a pas marché :
{% assign comments = site.data.comments[page.slug] | where_exp:"item","item._parent == nil" %}
{% for comment in comments %}
{% assign avatar = comment[1].avatar %}
{% assign email = comment[1].email %}
{% assign name = comment[1].name %}
{% assign url = comment[1].url %}
{% assign date = comment[1].date %}
{% assign message = comment[1].message %}
{% include comment.html index=forloop.index avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}
À la place, j'ai eu tout un tas de commentaires vides avec le balisage suivant :
<article id="comment-1" class="js-comment comment">
<div class="comment__avatar">
<img src="" alt="" />
</div>
<h3 class="comment__author-name"></h3>
<div class="comment__timestamp">
<a href="#comment-1" title="Permalien vers ce commentaire">
<time datetime=""></time>
</a>
</div>
<div class="comment__content"></div>
</article>
Hmm… j'imagine qu'il était temps d’ajouter des filtres inspect
à mes tableaux pour voir ce qui se passait.
{{ site.data.comments[page.slug] | inspect }}
Exemple de tableau avant filtrage avec where_exp
{
"comment-1471818805944" => {
"message" => "Ceci est un message de commentaire parent.",
"name" => "Prénom Nom",
"email" => "md5g1bb3r15h",
"url" => "",
"hidden" => "",
"date" => "2016-08-21T22:33:25.272Z"
},
"comment-1471904599908" => {
"message" => "Ceci est un autre message de commentaire parent.",
"name" => "Prénom Nom",
"email" => "md5g1bb3r15h",
"url" => "",
"hidden" => "",
"date" => "2016-08-22T21:42:48.075Z"
}
}
Exemple de tableau après filtrage avec where_exp
[
{
"message" => "Ceci est un message de commentaire parent.",
"name" => "Prénom Nom",
"email" => "md5g1bb3r15h",
"url" => "",
"hidden" => "",
"date" => "2016-08-21T22:33:25.272Z"
},
{
"message" => "Ceci est un autre message de commentaire parent.",
"name" => "Prénom Nom",
"email" => "md5g1bb3r15h",
"url" => "",
"hidden" => "",
"date" => "2016-08-22T21:42:48.075Z"
}
]
Apparemment l’utilisation du filtre where_exp
aplatit quelque peu les choses en supprimant les objets comment-xxxxxxxxxxxxx
. Cela fait que mes tags assign
retournent des valeurs nulles parce que comment[1]
n'existe plus.
{% assign avatar = comment[1].avatar %}
{% assign email = comment[1].email %}
{% assign name = comment[1].name %}
{% assign url = comment[1].url %}
{% assign date = comment[1].date %}
{% assign message = comment[1].message %}
Une fois cela découvert, la solution était simple --- supprimer [1]
pour chacun des noms des propriétés.
{% assign avatar = comment.avatar %}
{% assign email = comment.email %}
{% assign name = comment.name %}
{% assign url = comment.url %}
{% assign date = comment.date %}
{% assign message = comment.message %}
Afficher les commentaires imbriqués
Voici ce que je cherchais à accomplir… avant que le mal de tête ne commence 😧 🔫
- Déclarer une boucle et, à chaque itération, créer un nouveau tableau nommé
replies
ne contenant que les réponses aux commentaires. - Évaluer la valeur de
_parent
pour ces réponses. - Si
_parent
est égal à l’index de la boucle parente alors il doit être traité comme un commentaire "enfant". - Sinon, on passe à l’entrée suivante du tableau.
- Et ainsi de suite.
J'ai déterminé que la manière la plus simple d’assigner un identifiant unique à chaque commentaire parent était de le faire à l’aide d’une séquence.
Heureusement Liquid nous permet de faire cela à l’aide de forloop.index
.
{% assign index = forloop.index %}
Ensuite j'ai imbriqué une copie modifiée de la boucle parent précédente à l’intérieur d’elle-même --- pour faire fonction de boucle "enfant" ou replies
.
{% assign replies = site.data.comments[page.slug] | where_exp:"item","item._parent == include.index" %}
{% for reply in replies %}
{% assign parent = reply._parent %}
{% assign avatar = reply.avatar %}
{% assign email = reply.email %}
{% assign name = reply.name %}
{% assign url = reply.url %}
{% assign date = reply.date %}
{% assign message = reply.message %}
{% include comment.html parent=parent avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}
Malheureusement le filtre where_exp
s'est révélé problématique une fois de plus, obligeant Jekyll à générer l’erreur suivante :
Liquid Exception: Liquid error (line 47): Nesting too deep in /_layouts/page.html
.
Après avoir brièvement songé un moment au film Inception, j'ai appliqué un filtre inspect
pour m'aider à m'en sortir avec la boucle replies
. J'en ai conclu que la condition where_exp
échouait4 parce que je tentais de comparer un entier avec une chaîne de caractères 😳.
Pour résoudre cela, j'ai placé une balise capture
autour de la variable d’index pour la convertir en chaîne de caractères. Puis j'ai modifié la condition du filtre where_exp
afin de comparer _parent
avec cette nouvelle variable {{ i }}
--- pour corriger le problème et me permettre de passer à la suite.
{% capture i %}{{ include.index }}{% endcapture %}
{% assign replies = site.data.comments[page.slug] | where_exp:"item","item._parent == i" %}
_includes/page__comments.html
<section class="page__reactions">
{% if site.repository and site.staticman.branch %}
{% if site.data.comments[page.slug] %}
<!-- Start static comments -->
<div id="comments" class="js-comments">
<h2 class="page__section-label">
{% if site.data.comments[page.slug].size > 1 %}
{{ site.data.comments[page.slug] | size }}
{% endif %}
Comments
</h2>
{% assign comments = site.data.comments[page.slug] | sort | where_exp: 'comment', 'comment[1].replying_to == blank' %}
{% for comment in comments %}
{% assign index = forloop.index %}
{% assign replying_to = comment[1].replying_to | to_integer %}
{% assign avatar = comment[1].avatar %}
{% assign email = comment[1].email %}
{% assign name = comment[1].name %}
{% assign url = comment[1].url %}
{% assign date = comment[1].date %}
{% assign message = comment[1].message %}
{% include comment.html index=index replying_to=replying_to avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}
</div>
<!-- End static comments -->
{% endif %}
{% unless page.comments_locked == true %}
<!-- Start new comment form -->
<div id="respond">
<h2 class="page__section-label">Leave a Comment <small><a rel="nofollow" id="cancel-comment-reply-link" href="{{ page.url | absolute_url }}#respond" style="display:none;">Cancel reply</a></small></h2>
<form id="comment-form" class="page__form js-form form" method="post" action="https://api.staticman.net/v2/entry/{{ site.repository }}/{{ site.staticman.branch }}/comments">
<fieldset>
<label for="comment-form-message"><strong>Comment</strong> <small>(<a href="https://kramdown.gettalong.org/quickref.html">Markdown</a> is allowed)</small></label>
<textarea type="text" rows="6" id="comment-form-message" name="fields[message]" required spellcheck="true"></textarea>
</fieldset>
<fieldset>
<label for="comment-form-name"><strong>Name</strong></label>
<input type="text" id="comment-form-name" name="fields[name]" required spellcheck="false">
</fieldset>
<fieldset>
<label for="comment-form-email"><strong>Email</strong> <small>(used for <a href="http://gravatar.com">Gravatar</a> image and reply notifications)</small></label>
<input type="email" id="comment-form-email" name="fields[email]" required spellcheck="false">
</fieldset>
<fieldset>
<label for="comment-form-url"><strong>Website</strong> <small>(optional)</small></label>
<input type="url" id="comment-form-url" name="fields[url]"/>
</fieldset>
<fieldset class="hidden" style="display: none;">
<input type="hidden" name="options[origin]" value="{{ page.url | absolute_url }}">
<input type="hidden" name="options[parent]" value="{{ page.url | absolute_url }}">
<input type="hidden" id="comment-replying-to" name="fields[replying_to]" value="">
<input type="hidden" id="comment-post-id" name="options[slug]" value="{{ page.slug }}">
<label for="comment-form-location">Leave blank if you are a human</label>
<input type="text" id="comment-form-location" name="fields[hidden]" autocomplete="off">
</fieldset>
<!-- Start comment form alert messaging -->
<p class="hidden js-notice">
<span class="js-notice-text"></span>
</p>
<!-- End comment form alert messaging -->
<fieldset>
<label for="comment-form-reply">
<input type="checkbox" id="comment-form-reply" name="options[subscribe]" value="email">
Notify me of replies by email.
</label>
<button type="submit" id="comment-form-submit" class="btn btn--large">Submit Comment</button>
</fieldset>
</form>
</div>
<!-- End new comment form -->
{% else %}
<p><em>Comments are closed. If you have a question concerning the content of this page, please feel free to <a href="/contact/">contact me</a>.</em></p>
{% endunless %}
{% endif %}
</section>
_includes/comment.html
<article id="comment{% unless include.r %}{{ index | prepend: '-' }}{% else %}{{ include.index | prepend: '-' }}{% endunless %}" class="js-comment comment {% if include.name == site.author.name %}admin{% endif %} {% unless include.replying_to == 0 %}child{% endunless %}">
<div class="comment__avatar">
{% if include.avatar %}
<img src="{{ include.avatar }}" alt="{{ include.name | escape }}">
{% elsif include.email %}
<img src="https://www.gravatar.com/avatar/{{ include.email }}?d=mm&s=60" srcset="https://www.gravatar.com/avatar/{{ include.email }}?d=mm&s=120 2x" alt="{{ include.name | escape }}">
{% else %}
<img src="/assets/images/avatar-60.png" srcset="/assets/images/avatar-120.png 2x" alt="{{ include.name | escape }}">
{% endif %}
</div>
<h3 class="comment__author-name">
{% unless include.url == blank %}
<a rel="external nofollow" href="{{ include.url }}">
{% if include.name == site.author.name %}<svg class="icon" width="20px" height="20px"><use xlink:href="#icon-mistake"></use></svg> {% endif %}{{ include.name }}
</a>
{% else %}
{% if include.name == site.author.name %}<svg class="icon" width="20px" height="20px"><use xlink:href="#icon-mistake"></use></svg> {% endif %}{{ include.name }}
{% endunless %}
</h3>
<div class="comment__timestamp">
{% if include.date %}
{% if include.index %}<a href="#comment{% if r %}{{ index | prepend: '-' }}{% else %}{{ include.index | prepend: '-' }}{% endif %}" title="path to this comment">{% endif %}
<time datetime="{{ include.date | date_to_xmlschema }}">{{ include.date | date: '%B %d, %Y' }}</time>
{% if include.index %}</a>{% endif %}
{% endif %}
</div>
<div class="comment__content">
{{ include.message | markdownify }}
</div>
{% unless include.replying_to != 0 or page.comments_locked == true %}
<div class="comment__reply">
<a rel="nofollow" class="btn" href="#comment-{{ include.index }}" onclick="return addComment.moveForm('comment-{{ include.index }}', '{{ include.index }}', 'respond', '{{ page.slug }}')">Reply to {{ include.name }}</a>
</div>
{% endunless %}
</article>
{% capture i %}{{ include.index }}{% endcapture %}
{% assign replies = site.data.comments[page.slug] | sort | where_exp: 'comment', 'comment[1].replying_to == i' %}
{% for reply in replies %}
{% assign index = forloop.index | prepend: '-' | prepend: include.index %}
{% assign replying_to = reply[1].replying_to | to_integer %}
{% assign avatar = reply[1].avatar %}
{% assign email = reply[1].email %}
{% assign name = reply[1].name %}
{% assign url = reply[1].url %}
{% assign date = reply[1].date %}
{% assign message = reply[1].message %}
{% include comment.html index=index replying_to=replying_to avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}
HTML et JavaScript pour la réponse à un commentaire
L'étape suivante a consisté à ajouter quelques touches finales pour que le tout fonctionne.
Habitué à la manière dont WordPress gère les formulaires de réponse, j'y ai pioché de l’inspiration. En mettant le nez dans le code JavaScript qui se trouve dans wp-includes/js/comment-reply.js
j'ai trouvé tout ce dont j'avais besoin :
- une fonction
respond
pour déplacer le formulaire dans la vue, - une fonction
cancel
pour supprimer un formulaire de réponse et le repositionner à son état d’origine, - passer l’identifiant unique du parent à
options[parent]
lors de la soumission du formulaire.
J'ai commencé par utiliser une condition unless
pour n'afficher que les liens "répondre" sur les commentaires parents. J'avais seulement envisagé un seul niveau de profondeur pour les réponses, donc cela m'a semblé être un bon moyen pour m'en tenir à ça.
{% unless r %}
<div class="comment__reply">
<a rel="nofollow" class="btn" href="#comment-{{ include.index }}">
Reply to {{ include.name }}
</a>
</div>
{% endunless %}
Pour donner vie au lien répondre j'ai lui ai ajouté l’attribut onclick
suivant et du JavaScript.
onclick = "return addComment.moveForm('comment-{{ include.index }}', '{{ include.index }}', 'respond’, '{{ page.slug }}')";
J'ai juste eu à modifier quelques noms de variables dans le script comment-reply.js
de WordPress pour que tout marche bien avec le balisage de mon formulaire.
Ajout du support des notifications par mail
Comparées aux réponses de commentaires imbriqués, les notifications par mail furent très simples à mettre en place.
Mise à jour de la configuration staticman.yml
Pour s'assurer que les liens dans les mails de notifications sont sûrs et ne
proviennent que de domaines de confiance, définissez allowedOrigins
en
fonction.
Exemple :
allowedOrigins: ["mademistakes.com"]
Le(s) domaine(s) autorisé()s doi(ven)t correspondre à ceux passés par le champ options.origin
que nous allons ajouter à la prochaine étape. Seuls les domaines correspondants déclencheront les notifications à envoyer, faute de quoi l’opération échouera.
Mise à jour du formulaire de commentaire
Pour terminer, ajoutons deux champs au formulaire de commentaire.
Champ 1 : Un champ caché qui passe la valeur d’origin
5 défini dans le fichier staticman.yml
:
<input type="hidden" name="options[origin]" value="{{ page.url | absolute_url }}">
Champ 2 : Un input
de type case à cocher pour s'inscrire aux notifications par mail.
<label for="comment-form-reply">
<input type="checkbox" id="comment-form-reply" name="options[subscribe]" value="email">
Me prévenir par mail des nouveaux commentaires.
</label>
Rien de bien surprenant ici, name=options[subscribe]
and value="email"
sont ajoutés au champ pour associer les données d’abonnement avec l’adresse mail.
Si tout est correctement configuré, l’utilisateur devrait recevoir un mail dès qu'un nouveau commentaire est posté sur le billet ou la page auxquels il s'est abonné.
Voilà, vous avez mis en place un système de commentaires basé sur des fichiers statiques dans Jekyll et qui gère les commentaires imbriqués et les notifications de réponse. Maintenant j'aimerais gagner une minute de temps de génération pour pouvoir ajouter les nouveaux commentaires encore plus vite 😦.
Un des avantages du nouveau fichier de configuration c'est qu'on peut utiliser Staticman avec d’autres générateurs de site statique. La
v2
ne vous oblige plus à utiliser un fichier_config.yml
spécifique à Jekyll. ↩Les propriétés de site sont optionnelles. Se reporter à la documentation de Staticman pour plus de détails sur comment connecter vos formulaires. ↩
Staticman nomme ce champ
_parent
dans les entrées. ↩15
n'est pas la même chose que'15'
. Ces guillemets simples font toute la différence… ↩Cette URL sera ajoutée dans la notification par mail envoyée aux abonnés pour leur permettre d’ouvrir directement la page. ↩