Comment placer un slug à la place de l'id dans une URL Gatsby ?

Maintenant que j'ai installé react-bootstrap et un peu designé mon blog, je m'attaque à une nouvelle problématique. J'ai suivi le tutoriel officiel de Gatsby, et ce dernier nous permet d'afficher les articles et catégories par $id dans l'url. Ce n'est pas très esthétique, ni très user friendly.

J'ai donc cherché comment rendre mes URL plus joli en intégrant des slugs dans ma base strapi. J'ai longuement bataillé et je vous livre le résultat de mes fouilles webologiques.

1. Coté Strapi, on slugify !

La première chose à faire est d'installer slugify dans notre API Strapi :

npm install slugify

Ensuite, il faut se connecte à notre interface Strapi, et dans le Content Types Builder, ajouter un champ à Articles que l'on appelle slug (format text simple).

On va ensuite créer un model de slug dans le fichier suivant : Path — ./api/article/models/Article.js

module.exports = {
  lifecycles: {
    async beforeCreate(data) {
      strapi.log.debug("beforeCreate")
      data.slug = slugify(data.title, {lower: true});
    },
    async beforeUpdate (params,data) {
      strapi.log.debug("beforeUpdate")
      data.slug = slugify(data.title, {lower: true});
    },
  },
};

Enfin, il faut cliquer sur ajouter un article, puis sur configurer la vue et enfin sur slug. En placeholder, indiquer "Generated automatically based on the title" et décocher l'option modifiable. Enregistrer les modifications. Pour s'assurer que cela a bien fonctionné, mettre à jour vos article en modifiant un espace et les sauvegardant. Le slug devrait alors apparaître dans le champ correspondant.

J'ai fait la même manipulation pour les tags et les catégories. A la différence près que ces derniers ont un champ name et non title. Les fichiers models sont donc les suivants :

Path — ./api/article/models/Category.js Path — ./api/article/models/Tag.js

 lifecycles: {
    async beforeCreate(data) {
      strapi.log.debug("beforeCreate")
      data.slug = slugify(data.name, {lower: true});
    },
    async beforeUpdate (params,data) {
      strapi.log.debug("beforeUpdate")
      data.slug = slugify(data.name, {lower: true});
    },
  },
};

Voilà côté backend, on est good :) Passons au frontend.

Côté Gatsby, on fetch par URL !

Alors là, ça a été pour moi très fastidieux! Il faut dire que je suis une petite nouvelle dans le monde GraphQL. Je ne me rappelle plus l'ordre dans lequel j'ai effectué les modifications mais commençons par le fichier gatsby-node.js :

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const result = await graphql(
    `
      {
        articles: allStrapiArticle(sort: { fields: updated_at, order: DESC }) {
          edges {
            node {
              strapiId
              slug
            }
          }
        }
        categories: allStrapiCategory {
          edges {
            node {
              strapiId
              slug
            }
          }
        }
        tags: allStrapiTag {
          edges {
            node {
              strapiId
              slug
            }
          }
        }
      }
    `
  )

  if (result.errors) {
    throw result.errors
  }

  // Create blog articles pages.
  const articles = result.data.articles.edges
  const categories = result.data.categories.edges
  const tags = result.data.tags.edges

  articles.forEach((article, index) => {
    createPage({
      path: `/article/${article.node.slug}`,
      component: require.resolve("./src/templates/article.js"),
      context: {
        slug: article.node.slug,
        title: article.node.title,
      },
    })
  })

  categories.forEach((category, index) => {
    createPage({
      path: `/category/${category.node.slug}`,
      component: require.resolve("./src/templates/category.js"),
      context: {
        slug: category.node.slug,
      },
    })
  })

  tags.forEach((tag, index) => {
    createPage({
      path: `/tag/${tag.node.slug}`,
      component: require.resolve("./src/templates/tag.js"),
      context: {
        slug: tag.node.slug,
      },
    })
  })
}

On a ainsi créé des pages par slugs et placés ces derniers dans le context.

Ensuite, dans le fichier index.js qui récupère l'intégralité des articles, on modifie la query en ajouter slug partout où l'on souhaite le récupérer (c'est à dire dans article, category et tag :

<StaticQuery
      query={graphql`
        query {
          allStrapiArticle(sort: { fields: updated_at, order: DESC }) {
            edges {
              node {
                strapiId
                title
                categories {
                  name
                  id
                  slug
                }
                tags {
                  id
                  name
                  slug
                }
                image {
                  publicURL
                }
                updated_at
                slug
              }
            }
          }
        }
      `}
      render={data => (
        <ArticlesComponent
          articles={data.allStrapiArticle.edges}
          key={`${data.allStrapiArticle.edges.length}_articles`}
        />
      )}
    />

On pense bien sûr à changer tous les liens dans l'affichage en remplaçant id ou strapiId par slug (par exemple ici dans components/articles.js :

 <Card
      seo={seo}
      className="h-100"
      key={`article_card_${article.node.strapiId}`}
    >
      <a href={`/article/${article.node.slug}`}>
        <Card.Img
          variant="top"
          src={article.node.image.publicURL}
          alt={article.node.image.publicURL}
        />
      </a>
      <Card.Body>
        <a href={`/article/${article.node.slug}`}>
          <Card.Title className="h3">{article.node.title}</Card.Title>
        </a>
        <Card.Text>
          <br />
          <a href={`/article/${article.node.slug}`}>
            <Button>LIRE L'ARTICLE</Button>
          </a>
        </Card.Text>
      </Card.Body>

      <Card.Footer>
        <div className="footer-card-meta">
          <div className="footer-category">
            <IoMdPricetag />{" "}
            {article.node.categories.map(category => (
              <a
                href={`/category/${category.slug}`}
                alt={category.name}
                key={`article_${article.node.strapiId}_cat_${category.id}`}
              >
                {category.name}{" "}
              </a>
            ))}
          </div>
          <div className="footer-category">
            <RiTimeLine />{" "}
            <Moment format="DD/MM/YYYY">{article.node.updated_at}</Moment>
          </div>
        </div>
      </Card.Footer>
    </Card>

Il faut également modifier le HeaderNav, qui contient la navigation vers les catégories :

 <StaticQuery
          query={graphql`
            query {
              allStrapiCategory(sort: { fields: name, order: ASC }) {
                edges {
                  node {
                    strapiId
                    name
                    slug
                  }
                }
              }
            }
          `}
          render={data =>
            data.allStrapiCategory.edges.map((category, i) => {
              return (
                <Nav.Link
                  key={category.node.strapiId}
                  href={`/category/${category.node.slug}`}
                >
                  {category.node.name}
                </Nav.Link>
              )
            })
          }
        />

Et enfin, les templates ! Voici les queries, mais pensez bien à modifier tous vos liens dans vos vos render ;)

article.js :
export const query = graphql`
  query ArticleQuery($slug: String!) {
    strapiArticle(slug: { eq: $slug }) {
      strapiId
      title
      content
      updated_at
      image {
        publicURL
      }
      categories {
        name
        id
        slug
      }
      tags {
        name
        id
        slug
      }
      slug
    }
  }
`
category.js :
export const query = graphql`
  query Categories($slug: String!) {
    articles: allStrapiArticle(
      filter: { categories: { elemMatch: { slug: { eq: $slug } } } }
      sort: { fields: updated_at, order: DESC }
    ) {
      edges {
        node {
          strapiId
          title
          categories {
            id
            name
            slug
          }
          image {
            publicURL
          }
          slug
        }
      }
    }
    categories: strapiCategory(slug: { eq: $slug }) {
      name
    }
  }
`
tag.js :
export const query = graphql`
  query Tags($slug: String!) {
    articles: allStrapiArticle(
      filter: { tags: { elemMatch: { slug: { eq: $slug } } } }
      sort: { fields: updated_at, order: DESC }
    ) {
      edges {
        node {
          strapiId
          title
          categories {
            id
            name
            slug
          }
          tags {
            id
            name
            slug
          }
          image {
            publicURL
          }
          slug
        }
      }
    }
    tags: strapiTag(slug: { eq: $slug }) {
      name
    }
  }
`

J'espère que cela vous auras aidé ;) Bon dev à vous !