Créer un carousel SEO friendly, sous Drupal, avec jQuery + ajax

Lors de la création du site d'un ostéopathe à Marseille, une idée graphique et ergonomique intéressante nous a fait nous creuser un peu la tête pour sa mise en oeuvre technique.

L'idée était de pouvoir passer d'une page à la suivante en cliquant sur un contrôle (les grosses flèches rouges).
Le clic devait provoquer une transition latérale (slide) pour chasser le contenu de la page courante avec celui de la nouvelle, le tout passant sous les blocs de navigation situés à gauche.

Ostéopathe à Marseille

Comment faire ?

Première idée : créer un vue avec jCarousel (jCarousel + views). Solution effectivement intéressante, très simple à mettre en oeuvre. En trois minutes on obtient un carousel dans lequel on a, par exemple, toutes les pages du site, avec possibilité de passer de l'une à l'autre avec les flèches de navigation.

Oui mais non

L'idée est intéressante mais pose plusieurs problèmes : on veut que chaque page du site doit être accessible par sa propre URL. Si l'on peut facilement créer un carousel sur la page d'accueil, la contrainte des URL distinctes pour chaque node imposerait alors de proposer ce même carousel derrière chaque URL. Or si la vue jCarousel est très pratique, elle a un inconvénient important ici : tous les contenus sont chargés dans la page, même si un seul est affiché à la fois.
Conséquences : on peut arriver rapidement à une taille de page importante (dans notre cas c'est un risque assez théorique, le contenu du site n'étant pas très important) mais, surtout, tout le contenu du site va être présent sur chaque page (vous vous rappelez de la contrainte précédente, une URL distincte pour chaque node ?). Or, si je laissais sur ma page d'accueil, voire sur chaque page, un carousel contenant l'ensemble de mes pages, j'obtiendrai ce que l'on nomme duplicate content, et ça, c'est mal !

Tu ne dupliqueras point

Bien sûr on pourrait ruser : avoir nos pages à carousel, et utiliser l'attribut canonical, jouer avec du nofollow, ou je ne sais quelle autre solution. Au final ce serait probablement pour se retrouver avec une seul URL indexée. Ca n'est pas l'idée.

Vite, une idée !

Résumons ce qu'on a : il faut donc un chargement dynamique, ajax, pour que le contenu ne soit pas présent dans la page au chargement, mais apparaisse dynamiquement au clic, pour pouvoir être animé (effet de slide).

Tout d'abord, il va nous falloir obtenir la liste des liens, la parcourir et déterminer où on se situe dans cette liste afin de savoir si on a une page après et une page avant.

jQuery mon amour

Le développeur un peu brut peut avoir tendance à faire ce qui nous a traversé l'esprit quelques secondes : requêter la base, ou à la rigueur utiliser une vue, simplement pour obtenir cette liste de liens qui va être la référence.

Mais y regardant de plus près on s'apperçoit que cette liste est sous notre nez : le menu de navigation, géré avec les liens primaires de Drupal.
Cette liste est là, il suffit de l'utiliser. Drupal nous fourni même une aide supplémentaire : l'entrée du menu correspondante à la page courante est signalée par une classe active-trail qui va servir de magnifique sélecteur.

Avec jQuery on récupère donc le lien précédent et le lien suivant notre page courante (si ils existent) :

  1. var lien = $("#block-menu-primary-links li.active-trail a")
  2. var conteneur = $('#pages_navigation');
  3. var page_courante = conteneur.children('.active');
  4. var url_precedente = lien.parent().prev().children('a').attr('href');
  5. var url_suivante = lien.parent().next().children('a').attr('href');
  6. var page_precedente = conteneur.children('.prev');
  7. var page_suivante = conteneur.children('.next');

Et voilà, moins de 10 lignes pour tout savoir.

On va maintenant pouvoir précharger les pages correspondant à ces deux URL.

Nous avons cependant besoin de résoudre un petit problème : lorsque j'appelle une URL de page j'ai son contenu, mais il n'est pas livré seul. J'ai en prime tout le layout de la page, les blocs de navigation par exemple. Ici, ça ne m'intéresse pas, et ça m'encombrerait même. Car ce que je veux c'est charger uniquement le contenu du node, ce qui apparaît dans le pavé central.

Ici nous avions deux solutions : obtenir un rendu des données correspondant à notre besoin, via un template, ou obtenir les données pour les manipuler, via du JSON par ex. Nous avons choisi la première option, la plus simple, mais la deuxième est très intéressante aussi. Dans les prochains jours nous mettrons en ligne un site qui utilise un chargement de nodes via un module maison appelé en Ajax et renvoyant les données en JSON. Un billet sur le sujet à venir.

On va donc faire une petite intervention rapide sur le template général de page, page.tpl.php, et lui ajouter un paramètre grâce auquel il saura ne renvoyer que cette partie de la page. La modification prend deux minutes. On commence donc le script par :

  1. <?php /* $Id: page.tpl.php,v 1.4 2008/07/14 01:41:22 add1sun Exp $ */ ?>
  2. <?php
  3. $current_alias = '/'.drupal_get_path_alias($_GET['q']);
  4. $ajax = false;
  5. if ($_GET['ajax'] == 1){
  6. $ajax = true;
  7. }
  8. if (!$ajax){
  9. ?>

Ensuite on a le reste du code "normal" du template. Juste à la fin on va intégrer le traitement de la version dédiée aux appels ajax :

  1. </html>
  2. <?php
  3. // Fin du if Ajax
  4. } else {
  5. if ($title) {
  6. ?>
  7. <h1 class="title"><?php print $title; ?></h1>
  8. <?php
  9. }
  10. $content = str_replace('?ajax=1','',$content);
  11. print $content;
  12. }
  13. ?>

Maintenant si j'appelle mon URL avec le paramètre qui va bien, j'ai donc uniquement le contenu du node, et plus son habillage, son template.

Dans la partie "normale" du page.tpl.php on va par ailleurs aménager la structure HTML de façon à avoir nos flèches de navigation et les conteneurs qui vont permettre de jouer, notamment de préparer nos chargements de pages :

  1. <?php if ($left): ?>
  2. <div id="left-sidebar" class="sidebar"><?php print $left; ?></div>
  3. <?php endif; ?>
  4. <div id='pages_conteneur'>
  5. <div id='pages_navigation'>
  6. <div class="volet_navigation prev"></div>
  7. <div class="volet_navigation active">
  8. <?php if ($breadcrumb): ?>
  9. <?php print $breadcrumb; ?>
  10. <?php endif; ?>
  11.  
  12. <?php if ($messages): ?>
  13. <?php print $messages; ?>
  14. <?php endif; ?>
  15.  
  16. <?php print $contenttop; ?>
  17.  
  18. <?php if ($is_front && $mission): ?>
  19. <div class="mission"><?php print $mission; ?></div>
  20. <?php endif; ?>
  21.  
  22. <?php if ($title): ?>
  23. <h1 class="title"><?php print $title; ?></h1>
  24. <?php endif; ?>
  25.  
  26. <?php if ($help): ?>
  27. <div class="help"><?php print $help; ?></div>
  28. <?php endif; ?>
  29.  
  30. <?php if ($tabs): ?>
  31. <?php print $tabs; ?>
  32. <?php endif; ?>
  33. <?php print $content; ?>
  34. </div>
  35. <div class="volet_navigation next"></div>
  36. </div>
  37. <div id='nav_precedent'></div>
  38. <div id='nav_suivant'></div>
  39. </div>
  40. <?php if ($right): ?>
  41. <div id="right-sidebar" class="sidebar"><?php print $right; ?></div>
  42. <?php endif; ?>

Il ne reste maintenant plus qu'à faire les appels ajax dans la page et placer le contenu des pages précédente et suivante dans les blocs, cachés, que l'on présentera avec une animation lors du clic sur la flèche correspondante.

Exemple pour la flèche de gauche, pour laquelle on va commencer par vérifier si il y a bien une url "précédente". Si oui on précharge le contenu de la page correspondante dans la zone dédiée et on rend la flèche cliquable :

  1. if (url_precedente != undefined) {
  2. $('#<span>nav_precedent</span>').css('display','block');
  3. $.ajax({
  4. url: url_precedente+'?ajax=1',
  5. context: page_precedente,
  6. success: function(data){
  7. page_precedente.html(data);
  8. page_precedente.children('.block').appendTo(page_precedente.children('.node'))
  9. page_precedente.css('background-color',page_precedente.find('.color').html())
  10. page_precedente.find('.node-page').height(480-page_precedente.find('h1').outerHeight(true))
  11. }
  12. });
  13. $('#nav_precedent').unbind('click').click(function(){
  14. page_suivante.css('left','0px')
  15. page_precedente.stop().animate({
  16. left: '940'
  17. },300,function(){
  18. $(this).removeClass('prev').addClass('active').find('.node-page').jScrollPane()
  19. });
  20. page_courante.stop().animate({
  21. left: '1880'
  22. },300,function(){
  23. page_suivante.removeClass('next').addClass('prev')
  24. $(this).removeClass('active').addClass('next')
  25. carousel();
  26. $("label").inFieldLabels();
  27. });
  28. url_courante = url_precedente;
  29. lien.parent().removeClass('active-trail').prev().addClass('active-trail');
  30. });
  31. } else {
  32. $('#nav_precedent').css('display','none');
  33. }

On fait la même chose de l'autre côté bien sûr :)

Voilà pour le fonctionnement. Pour être complet il faut mettre un peu d'huile dans les rouages, et placer le code jQuery dans une fonction qui sera appelée au chargement de la page.

La version complète du template page.tpl.php utilisé ici et du fichier javascript qui va avec sont disponibles dans cette archive zip. Enjoy :)