Aller au contenu principal
divagations - Retour à l'accueil

Ajouter une section "articles"

Guillaume Barbier

Temps de lecture : ~ 16 minutes

Maintenant que mon journal est à peu près opérationnel, il est temps de créer la deuxième section du site, celle dédiée à nos articles.

Pour cette section, je reprends le modèle posé avec mon journal et ses entrées, et très rapidement je constate :

  • qu'il me reste des points à optimiser
  • qu'un peu de mutualisation va être nécessaire

Structure de la section "Articles"

J'applique (en l'adaptant) la même structure que pour nos entrées de journal évoquée dans Lancer mon journal de bord et mon premier constat est que la page /articles.html n'est pas générée tant que je n'ai pas au moins un article. Je règle ce point[1] en ajoutant une propriété dans mes deux pages de type "liste" (/journal.html et /articles.html) :

generatePageOnEmptyData: true

La récurrence de certains motifs (et le fait que certains d'entre eux sont à corriger) m'incite à commencer à créer des composants (que je créé au format .liquid pour les différencier de mes templates HTML). Mais mon dossier /_includes commence à devenir un peu lourd, je décide donc d'isolé mes templates dans un dossier dédié

Au final mes fichiers et dossiers "articles" sont structurés ainsi :

/_includes                  # Dossier contenant mes composants
  /table-of-content.liquid
  /…
/_layouts
  /article.html             # Template spécifique aux articles
/content
  /articles                 # Le dossier où écrire mes articles
    /articles.11tydata.js   # Les données globales pour mes articles (notamment pour la navigation et le format d'URL)
  /articles.liquid          # Ma liste d'articles (équivalent pour les articles de journal.html)

Optimisations transverses : Création de composants

Comme je l'évoquais plus tôt, j'ai eu pas mal de petites corrections à effectuer sur mes articles, mon journal et ses entrées. Beaucoup de ces modifications étaient identiques pour tous les types de contenus, à quelques variations près.

Pour m'éviter d'avoir à répliquer dans chaque template concernée chaque optimisation, j'ai décidé de créer des composants Liquid.

L'avantage est qu'ainsi je peux mutualiser la logique de génération des contenus (assigner/construire des variables, conditionner des affichages, etc.)

Pagination du journal et de la liste d'articles

Ces pages étant paginées, il est important que j'indique leur position dans cette pagination, pour plusieurs raisons :

  • SEO : Je ne suis pas expert sur ce sujet, mais de ce que j'ai compris de la doc de Google sur le sujet(S'ouvre dans un nouvelle fenêtre) les robots indexent toutes les paginations (pas juste la première page) et il est nécessaire de leur fournir des informations pour comprendre la relation entre toutes ces pages.
  • Utilisabilité : pour se repérer mes utilisateurs et utilisateurices auront besoin de savoir combien de pages existent et sur quelle page ils ou elles sont.
    • Pour les utilisateurices sans handicaps visuels, le module de navigation devrait suffire à fournir l'information
    • Pour les utilisateurices de lecteurs d'écrans, il faut fournir l'information dès le chargement de la page, pour leur éviter d'avoir à naviguer jusqu'au module de navigation. Pour cela, il faut que j'indique cette pagination dans le titre du document.

Ajustement des titres

Afin de pouvoir indiquer la pagination dans le titre, je la remplace par une propriété calculée, directement dans ces deux pages :

# In content/articles.liquid
---
title: Articles
pagination:eleventyComputed:
  title: "{{ title }} - Page {{ pagination.pageNumber | plus: 1}} sur {{ pagination.pages.length }}"
---

Avertissement

Remarque

Dans mon title calculé, je dois incrémenter la variable pagination.pageNumber car la pagination d'Eleventy démarre sa numérotation à 0 (Un classique des listes de valeurs en javascript).

La pagination ajoutée dans le titre apparaît maintenant dans mon h1[2] et mon menu… pas vraiment le résultat attendu. Pour corriger cela, j'ajoute des titres alternatifs sur les deux pages :

# In content/articles.liquid
---
pageTitle: Articles # custom data I use to populate the <h1> of my pages (defaults to title)
navigation:title: Articles
pagination:eleventyComputed:
  title: "{{ pageTitle }} - Page {{ pagination.pageNumber | plus: 1}} sur {{ pagination.pages.length }}"
---

Information

Titre par défaut

En plus de ces optimisation, j'en profite pour corriger mes titres par défaut. En effet, tel que j'ai initié le projet, toutes mes nouvelles pages ont le même nom (jusqu'à ce que je leur assigne un titre) : "Page sans titre"…

Ce n'est pas très intéressant et je décide donc de renommer dynamiquement toutes mes pages "non nommées" en fonction de leur nom de fichier :

// In _data/eleventyComputed.js
export default {
  title: (data) => data.title || data.page.fileSlug
}

Optimisation de la navigation

Optimisation SEO

Ma pagination actuelle n'a pas de liens vers la première et la dernière page. D'un point de vue utilisabilité, je ne pense pas que ce soit un problème majeur, mais si je me base sur la doc de Google citée plus haut, cela peut être un problème pour les robots d'indexation.

Afin d'éviter d'avoir à répliquer ces modifications (ainsi que toutes les futures évolutions, j'isole ma navigation dans un composant Liquid) et applique les conseils d'intégration de la doc d'Eleventy dans Pagination Navigation(Opens in a new window). Je fais juste bien attention à prévoir et documenter toutes les données nécessaires à la personnalisation de ma nav en fonction de son contexte.

J'aurais bien voulu pouvoir placer *toute la logique de pagination dans le composant, malheureusement les "composants" insérés avec Liquid sont des capsule isolées, incapable de renvoyer des informations aux strates supérieures. Je devrais donc accepter de répéter la logique de pagination sur chaque page.

Astuce

Piste d'évolution : Template "Liste paginée"

En y réfléchissant, je pourrais peut-être regrouper toute la logique de pagination au même endroit avec un template "Liste paginée" que j'appellerais sur ces deux pages. 🤔

Mais ce niveau de complexité n'est peut-être pas utile… Je garde l'idée dans un coin pour l'instant.

Optimisation accessibilité

Lors de mes recherches je suis tombé sur de nombreuses propositions d'implémentations, toutes différentes, allant d'intégration très simplistes (et clairement insuffisante) à d'autres très complexes avec un usage lourd des attributs ARIA (toujours un red flag 🚩).
J'ai heureusement fini par trouver cet article sur HTMHell : "Page by Page: How Pagination Makes the Web Accessible"(S'ouvre dans un nouvelle fenêtre)

Pleine de bon sens, son autrice nous présente une pagination simple et une plus complexe, tout en nous détaillant comment nous pouvons faire varier notre implémentation. En suivant son article, j'ai pu me faire une idée sur les autres motifs que j'ai trouvé sur le web et évaluer ce qui était pertinent ou non.[3].

Voici les adaptations que j'ai décidé de faire :

  • Passer en liste non-ordonnée (<ul>)
  • Je garde le lien vers la page courante actif : <a href="#" aria-current="page"
  • J'ai beaucoup hésité à re-simplifier mes liens inactifs (pour les précédents/suivants en bout de navigation) pour utiliser de simple textes mais j'ai fini par garder la complexe intégration à coup d'ancres sans href et d'aria-disabled… car j'avais la flemme de revenir en arrière et j'aimais bien le fait qu'ils soient explicitement déclarés comme désactivé. Il est tout à fait possible que je revienne sur cette décision.

Information

Méthode valides pour intégrer les liens en bout de navigation

En faisant mes recherches, j'ai identifié au total trois façons de gérer les liens de "bout de course"[4] de la navigation :

  1. Ne pas intégrer les liens inutiles sur les première et dernière pages.
  2. Remplacer les liens par un texte équivalent et une apparence "désactivée"
  3. Garder les liens mais supprimer leur attribut href et leur ajouter les attributs role="link" et aria-disabled="true"

La première approche est clairement la plus simple et la efficace, mais elle induit un décalage visuel dont je ne suis pas fan. J'étais initialement parti sur la seconde avant de me laisser embarquer dans la troisième.

La version finale (vraiment finale, finale ?) de ma navigation donne donc :

<nav class="pagination-nav" aria-label="Pagination" tabindex="-1" id="pagination-nav">
  <ul>
    <li>
      <a aria-disabled="true" role="link" aria-label="Première page">&lt;&lt;</a>
    </li>
    <li>
      <a aria-disabled="true" role="link" aria-label="Page précédente">&lt;</a>
    </li>
    <li>
      <a href="#" aria-current="page">Page 1</a>
    </li>
    <li>
      <a href="/journal/page-2/">Page 2</a>
    </li>
    <li>
      <a href="/journal/page-3/">Page 3</a>
    </li>
    <li>
      <a href="/journal/page-2/" aria-label="Page suivante">&gt;</a>
    </li>
    <li>
      <a href="/journal/page-3/" aria-label="Dernière page">&gt;&gt;</a>
    </li>
  </ul>
</nav>

Lien d'accès rapide vers la navigation

Pour améliorer le confort de navigation des utilisateurs et utilisateurices de lecteurs d'écrans, il faudrait que j'ajoute un lien d'accès rapide vers celle-ci au début de ma page. Mais pour cela, il va falloir que je dynamise cette partie de mon template de base :

Pour cela, je créé un composant quick-links.liquid et y insère la logique nécessaire pour convertir un objet quickLinks en :

  • soit en un unique lien (si l'objet ne contient qu'un seul élément)
  • soit en une liste de liens (si plusieurs éléments)

La difficulté ici a surtout été de trouver comment formatter la donnée en YAML de façon à ce qu'Eleventy fusionne[5] les différents liens rapides (celui ajouté au niveau du template de base et celui au niveau des pages journal et articles). Voici le format que j'ai trouvé :

# In /_layouts/base.html
---
quickLinks:
  - href: "#content"
    text: Aller au contenu principal
---

# In /content/journal.liquid
---
quickLinks:
  - href: "#pagination-nav-label"
    text: Aller à la pagination
---

Il ne me reste plus qu'à appeler mon composant dans /_layouts/base.html :

{% render "quick-links", quickLinks: quickLinks %}

Enrichir les liens vers les articles

Différenciation articles / entrées de journal

Mes liens vers les articles et les entrées de journal sont encore très simple. Il serait intéressant de fournir plus d'information à mes lecteurs à ce sujet.

Initialement je comptais mutualiser la gestion de ces liens "enrichis" mais, en en discutant avec myriam, nous en avons conclu qu'articles et entrées de journal avaient trop de différences pour que ça vaille le coup de les mutualiser :

  • Les articles doivent préciser leur auteur (tandis que les entrées de journal sont toute écrites par le même auteur, moi)
  • L'ordre chronologique est important pour les entrées de journal, moins pour les articles
  • Les articles pourront avoir des thèmes assez différents alors que les entrées du journal se concentreront sur la conception et le développement du blog

Cela étant dit, le design de ces éléments n'est pas du tout prêt, nous sommes encore en pleine réflexion. Je vais donc pour l'instant me contenter de créer des placeholders pour isoler la logique des items dans mes listes.

Générer les résumés des articles

Avec simplement un titre et une date, mes entrées sont assez vides… Un petit résumé du contenu de l'article serait bien pratique dans mes listes d'articles, mais aussi pour les affichages en dehors du site (dans les résultats de recherche ou lors de partages de liens sur les réseaux).

Astuce

Différencier le résumé pour les robots et celui pour l'affichage sur le site

C'est une problématique récurrente sur les CMS : le texte de description que l'on souhaite afficher sur le site et celui que l'on souhaite exposer aux robots ne sont pas toujours identiques. La principale raison est que l'on souhaite pouvoir mettre son texte affiché en forme, ce que ne supportent pas les robots.

Cette réflexion est aussi valable pour les réseaux, qui ne supportent en général pas la mise en forme (du moins pas dans les liens enrichis qu'ils génèrent).

Pour résoudre cela, j'ai prévu deux données pour gérer mes descriptions d'articles :

  • description : Sans mise en forme, ce texte peut être fourni aux robots et aux réseaux
  • summary : Acceptant la mise en forme, ce texte est destiné exclusivement à l'affichage sur le site

Avertissement

Remarque :

Eleventy propose (via Gray matter(Opens in a new window)) déjà une solution pour définir des extraits de contenus. Je me sers de cette fonction et de la propriété page.excerpt qu'elle génère, mais j'ai choisi de ne pas reprendre son nom dans mes données personnalisées :

  • Pour éviter les confusions entre la donnée fournie out of the box et ma propre surcharge
  • Très prosaïquement, je trouve excerpt difficile à saisir sans erreurs (j'ai tendance à m'emmêler les pinceaux sur son "x" et son "c")

Le résumé pour l'affichage sur le site

J'alimente donc summary avec page.excerpt (que j'ai un peu personnalisé via les options d'Eleventy(S'ouvre dans un nouvelle fenêtre)) :

// In eleventy.config.js
// Activation de la fonction "page.excerpt" de front matter
export default function(eleventyConfig) {
  eleventyConfig.setFrontMatterParsingOptions({
		excerpt: true,
		// Start with "---", ends with :
		excerpt_separator: ""
	});
}

// In _data/eleventyComputed.js
export default {
  summary: (data) => {
    if (data.summary === false) return ""
    if (data.summary) return data.summary
    if (data.page.excerpt) return data.page.excerpt
    if (data.description) return data.description
  }
}

Avec cette approche, j'ai plusieurs options pour gérer l'affichage des résumés et ainsi m'adapter à différente situations.

Je peux simplement reprendre la description pour les robots :

---
title: Mon article
description: Description de mon article sans mise en forme.
---

Je peux aussi reprendre le début de mon article, jusqu'à un certain point, que je défini manuellement :

---
title: Mon article
description: Description de mon article sans mise en forme.
---
Descriptions basée sur le début de l'article, _avec_ **mise en forme**.
<!-- summary end -->

Si je souhaite un texte mise en forme mais n'apparaissant pas dans l'article, je peux à la place :

---
title: Mon article
description: Description de mon article sans mise en forme.
summary : Descriptions basée sur le début de l'article, _avec_ **mise en forme**.
---

Enfin, je peux désactiver l'affichage (même si une description est fournie) :

---
title: Mon article
description: Description de mon article sans mise en forme.
summary: false
---

Avec cette approche, je peux garantir l'affichage d'un résumé avec un contrôle total de la sortie, tout en minimisant mes efforts (vu que je peux mutualiser cette saisie avec la description pour les robots ou avec le contenu de l'article lui-même).

Le résumé pour les robots

Toujours à l'écoute de ma fainéantise, j'ai tenté d'automatiser la génération de ma description, pour ne pas avoir à la définir à chaque article :

// In _data/eleventyComputed.js
export default {
  description: (data) => {
    if (data.description === false) return ""
    if (data.description) return data.description
    let safeContent = ""
    if(data.page.excerpt) {
      safeContent=  data.page.excerpt
    } else {
      safeContent = data.page.rawInput.replace(/(<([^>]+)>)/gi, "")
      safeContent = safeContent.substring(0, safeContent.lastIndexOf(" ", 200)) + '…'
    }
    return safeContent.replace(/"/g, '&quot;')
  }
}

Mais j'ai fini par abandonner et laisser la donnée description brute : si elle n'est pas saisie, aucune description n'est fournie.

J'ai plusieurs raisons à cet abandon :

  • Malgré tous mes efforts pour sécuriser les extraits (qu'ils viennent d'extraits définis manuellement ou d'une sélection automatique du contenu brut), je n'arrive pas à complètement sécuriser la description générée :
    • J'arrive à éviter de "casser" les balises <meta> en échappant les guillemets droits doubles (")
    • J'arrive aussi à supprimer la plupart du HTML saisi directement, mais il me reste encore du markdown à supprimer et des expressions Liquid à interpréter ou supprimer. L'effort restant à fournir pour avoir une description auto fiable est important.
  • Je peux baser ma description auto sur le contenu brut, un extrait d'article délimité avec page.excerpt, mais pas avec la donnée summary : cela déclencherait une boucle de dépendances. Ce petit écart de traitement qui nécessite de toujours définir manuellement la description quand un texte enrichi est fourni
  • Dans les recommandations de Google pour cette balise, ils précisent que cette balise doit contenir un résumé, une synthèse de la page, pas juste un extrait, ce qui ne correspond pas trop à ce que je fais avec mes essais d'automatisation.

J'en ai donc conclu que plutôt que de m'acharner sur un script d'automatisation dont le résultat serait quasi systématiquement inadéquat, il était préférable de traiter la description comme une donnée à saisir systématiquement et préférer ne pas définir les métadonnées associées si elle n'est pas fournir (laissant dans ce cas-là la main aux algorithmes des moteurs de recherches et des réseaux pour définir une description).

Je conserve au final simplement la fonction de remplacement des guillemets droits doubles, histoire de sécuriser le balisage tout en me laissant la possibilité de saisir des descriptions avec des guillemets sans me prendre la tête :

// In _data/eleventyComputed.js
export default {
  description: (data) => {
    if (data.description) return data.description.replace(/"/g, '&quot;')
    return ""
  }
}

  1. Ce n'est pas un problème très bloquant en dehors de la phase de développement : je ne compte pas laisser ces sections vides ! ↩︎

  2. a priori pas besoin d'indiquer la pagination dans le h1 de la page, le titre du document et la navigation s'en chargent déjà. ↩︎

  3. Et clairement, je ne suis pas partisan du conseil de "désactiver" le lien vers la page courante que j'ai pu trouver parfois, comme par exemple sur le Système de Design de l'État (français), Pagination(S'ouvre dans un nouvelle fenêtre) ↩︎

  4. Il s'agit des bien connus liens "Première page", "Page précédente", "Page suivante" et "Dernière page" ↩︎

  5. Eleventy sait fusionner et cumuler (ou deep merge(Opens in a new window)) les différentes valeurs données à une propriété à travers la cascade, mais seulement si à chaque étape de la cascade ce sont des listes de valeur (array) qui sont appliquée.

    La moindre chaîne de caractère ou nombre appliqué sur une propriété écrasera la liste de valeur créée par la fusion des strates précédentes. ↩︎