Cloud & DevOps

Gérer les liens internes dans vos contenus CMS headless : la démarche avec Strapi

by Lukka Blois 13 octobre 2025

La démocratisation des CMS headless comme Strapi permet une approche plus modulaire sur la construction de nos projets.
À la différence des CMS monolithiques (WordPress, Drupal), ils proposent une plus grande flexibilité et liberté dans l’architecture et les solutions employées sur un projet (SPA, SSR, ISR, application mobile etc…) tout en conservant une centralisation du contenu. Un problème majeur subsiste cependant : comment créer des liens internes efficaces entre les différents contenus ? Si tous les CMS headless gèrent des relations entre contenus, ils ne peuvent pas créer automatiquement des liens au sein des textes riches. Dans cet article, nous allons voir comment résoudre ce problème pour Strapi, mais la logique reste applicable à d’autres CMS headless.

Concevoir des liens internes dans Strapi et les CMS headless

  1. Augmenter les capacités du CMS pour supporter des liens personnalisés (custom tag).
  2. Exposer les ressources nécessaires pour qu’ils soient consommés par le client.
  3. Gérer les liens personnalisés côté client pour y intégrer la logique de rendu, le tout grâce au point précédent.

Augmenter le CMS

L’éditeur de texte riche

Dans le cas de Strapi, l’éditeur rich-text par défaut est un éditeur markdown qui n’est pas prévu pour être étendu. La première étape sera donc de le remplacer par un éditeur basé sur CKEditor qui sera plus facilement extensible.
De par son approche modulaire, il n’y a qu’à installer ce plugin pour Strapi.
L’idée générale est d’ajouter la possibilité de créer des custom tags via CKEditor, pour qu’ils puissent être exploités par le client qui rend notre contenu.

Étendre l’éditeur CKEditor

Maintenant que CKEditor est installé, il est possible de l’étendre pour rajouter les fonctionnalités dont nous avons besoin. Pour ce faire, nous allons créer notre propre plugin Strapi. Bouton de toolbar
La première étape est de définir un bouton qui servira d’interface aux éditeurs pour créer les liens internes :

export function EntryLink(editor: any) {
  const toolbarButton = new window.CKEditor5.ui.ButtonView()
  toolbarButton.set({ label: "Strapi entry link", icon: STRAPI_SVG })

  editor.ui.componentFactory.add(pluginId, () => toolbarButton)
}

STRAPI\_SVG est une chaîne de caractère qui correspond au SVG qui sera rendu dans la toolbar de l’éditeur.

Commande CKEditor

Il faut ensuite enregistrer une commande dans l’éditeur (elle sera exécutée par la logique côté Strapi) et sert à appliquer notre custom tag à l’édition. Elles permettent d’ajouter un comportement à CKEditor et de l’appliquer via différents vecteurs (bouton, state de l’éditeur…).

editor.commands.add(pluginId, {
  execute: (attributes: any) => {
    const { selection } = editor.model.document

    editor.model.change((writer: any) => {
      if (selection.hasAttribute(pluginId)) {
        const ranges = selection.getRanges()

        for (const range of ranges) {
          writer.removeAttribute(pluginId, range)
        }
      } else {
        const range = selection.getFirstRange()
        writer.setAttribute(pluginId, attributes, range)
      }
    })
  },
  destroy: () => {}
})

Les attributes sont les arguments qui seront passés à la commande, dans notre cas un content type et un identifiant de contenu choisi par l’utilisateur via Strapi.
La première partie : lignes 6 à 11, permet de retirer notre custom tag s’il est déjà appliqué sur la sélection de l’utilisateur.
La seconde partie : lignes 13 et 14, permet d’appliquer notre custom tag à la sélection de l’utilisateur, en appliquant les attributes reçu en paramètre comme data-attribute HTML.

Pour l’appliquer, il suffit d’exposer notre plugin CKEditor dans la partie bootstrap de notre plugin Strapi pour être consommé par la librairie de rich-text.

window.CKEditorStrapiEntryLinkPlugin = EntryLink

Strapi et liens internes attributes

Comportement et réaction

Maintenant que le bouton est présent dans la toolbar, il faut y associer l’action. Comme CKEditor est un rich-text indépendant, il faut créer un lien entre ce dernier et notre Strapi : nous utilisons un CustomEvent javascript émit globalement, qui pourra être écouter par un composant côté CMS, tout en partageant l’état interne de notre éditeur.

Strapi et liens internes comportement et réaction

Pour appliquer ce comportement, un listener est ajouté sur le bouton de la toolbar pour émettre ce CustomEvent :

editor.on("ready", () => {
  if (!toolbarButton.element) return

  toolbarButton.element.addEventListener("click", () => {
    const { selection } = editor.model.document

    if (selection.hasAttribute(pluginId)) {
      return editor.model.change((writer: any) => {
        const ranges = selection.getRanges()

        for (const range of ranges) {
          writer.removeAttribute(pluginId, range)
        }
      })
    }

    const event = new CustomEvent(pluginId, { detail: { editor } })
    document.dispatchEvent(event)
  })
})

Le code entier du plugin CKEditor peut être trouvé sur ce gist.
À noter qu’il comprend la logique de CKEditor pour identifier les liens customs.

Implémenter la gestion des liens internes côté Strapi

Maintenant que notre événement est émit globalement, on peut gérer le reste de la procédure dans un contexte Strapi.
La subtilité de ce plugin vient du fait que l’on va :

Utiliser les zones d’injection de Strapi pour enregistrer une modale (dans le register de notre plugin)

app.injectContentManagerComponent("editView", "informations", { Component: ContentModal })

Définir la condition d’affichage de notre modal comme étant liée à notre CustomEvent émit par notre bouton côté CKEditor

const [editor, setEditor] = useState<any>()

const visible = useMemo(() => editor !== undefined, [editor])

useEffect(() => {
  document.addEventListener(pluginId, (event) => {
    // @ts-ignore
    setEditor(event.detail.editor)
  })
}, [])

Ainsi, dès que le contributeur cliquera sur le bouton de la toolbar, une modale apparaît pour qu’il puisse choisir le contenu à lier.
Le code entier de la modale peut être trouvé sur ce gist.

À partir de là, la liberté est totale pour présenter ça de la manière la plus ergonomique et pratique possible aux contributeurs. De notre côté, nous avons fait un simple formulaire en 2 étapes : l’utilisateur choisi un content-type, puis un contenu de ce même content-type.

Strapi et liens internes Content type

Une fois le choix fait, il ne nous reste plus qu’à appeler la commande CKEditor pour créer notre custom tag avec les données en provenances de Strapi :

const onApply = () => {
  editor.commands.get(pluginId).execute({
    "data-plugin": pluginId,
    "data-content-type": contentType,
    "data-content-id": contentId,
    style: "color: #006096"
  })

  setEditor(undefined)
}

À partir de ce point, la modal peut appliquer notre custom tag dans l’éditeur, ce qui permet de persister notre lien interne dans notre texte riche.

Exposer des endpoints pour gérer les liens internes dans Strapi

Côté serveur, il faut exposer autant d’endpoint que notre interface nécessite. Dans notre cas :

  • Un endpoint qui permet de lister les content type.
  • Un endpoint qui permet de chercher des contenus dans un content type donné.
export default ({ strapi }: { strapi: Strapi }) => ({
  getContentTypes: async (ctx) => {
    return strapi.plugin(pluginId).service("main").getContentTypesDefinition()
  },
  searchContent: async (ctx) => {
    const { contentTypeUid, search, locale } = ctx.request.query

    return strapi.plugin(pluginId).service("main").searchContent(contentTypeUid, search, locale)
  },
})

Le code du service utilisé est disponible sur ce gist.

Rendre les liens internes dans vos contenus CMS headless

Avec le remplacement de l’éditeur de texte de Strapi, nos champs rich-text sont maintenant du HTML (à la différence du markdown par défaut).
Comme nous utilisons Vue.js, nous n’avons pas besoin de librairie pour transformer le HTML de nos champs en composants pour qu’ils puissent être rendus, et ce grâce à la directive v-html.

Interpréter les custom tags

Avant de pouvoir rendre le contenu saisi dans Strapi, il faut en extraire les liens internes saisis et les remplacer par des liens valides HTML, plusieurs solutions sont possibles.
Comme la partie édition génère des <span> avec des attributes bien précis, nous pouvons utiliser différentes regex pour extraire les contenus liés facilement :

Cette première regex nous permet de sélectionner tous les <span> qui possède des data-attribute

/<span[^>]*>(?<text>[^<]+)<\/span>/

Après quoi, les deux regex suivantes permettent d’extraire le content-type et l’identifiant du contenu, et ce pour chacun des <span> identifiés par la regex précédente.

/<span[^>]*(?<attribute>data-content-type)="(?<value>\S+)"[^>]*>/
/<span[^>]*(?<attribute>data-content-id)="(?<value>\S+)"[^>]*>/

Suite à cette opération, les contenus associés sont identifiés et peuvent être récupérés dans Strapi.
Une fois les contenus récupérés, on répète les étapes précédentes, mais au lieux d’extraire les contenus, on génère les liens vers ces contenus avant de pouvoir rendre le contenu HTML à l’utilisateur.
L’ensemble de cette logique est portée par ce hook vuejs.

Les contraintes techniques à prendre en compte

Pour conclure, même si la fonctionnalité n’est pas native sur tous les CMS headless, ils offrent suffisamment d’extensibilité pour répondre à ce besoin.
Néanmoins, ce genre de solution vient avec son lot de points négatifs :

  • Plugin du CMS headless à écrire et maintenir
  • Interprétation client à écrire et maintenir
  • Charge réseau supplémentaire sur le CMS (pour la partie éditoriale **et **pour la partie client)
  • Résilience des données, si un contenu lié dans un texte riche est supprimé, le CMS ne peut pas impacter les références
Lukka Blois

Lukka Blois

Développeur full stack

Développeur full stack JS/TS passionné, j'adore traiter et comprendre les sujets de leur conception a leur implémentation.

Commentaires

Ajouter un commentaire

Votre commentaire sera modéré par nos administrateurs

Vous avez un projet ? Nos équipes répondent à vos questions

Contactez-nous