Accrochez le cadre, pas l'image.
Une icône nécessite un cadre pour être affichée et une image à insérer dans ce cadre. Les paquets `@web-portfolio/icons` et `@web-portfolio/icons-sanity` existent afin qu'un développeur n'ait jamais besoin de faire autre chose que de placer le cadre ; l'image peut provenir de n'importe où, selon la décision qui lui est associée.

Cela a commencé comme une question, et non comme un plan : une icône doit-elle être du contenu ou du code ? La liste des compétences d'un portfolio change plus souvent que ses composants, et chaque nouvelle compétence implique une nouvelle icône. Il semblait donc évident de considérer les icônes comme quelque chose que Sanity pourrait gérer, modifiable sans nécessiter un nouveau déploiement. Mais cette évidence ne dura pas longtemps.
Où a réellement conduit l'approche "icône comme contenu" ?
La première version stockait une icône sous forme de code SVG brut dans un champ texte simple, car c'était la solution la plus évidente pour représenter "une icône comme contenu" : l'utilisateur colle ce qu'il a, et le frontend l'affiche. C'est là que les choses ont commencé à devenir moins évidentes.
Chaque icône importée provenait d'une source différente : l'une avec un tracé simple en deux couleurs, une autre avec un remplissage codé en dur et un élément <g> imbriqué contenant des styles intégrés qu'aucun éditeur ne pouvait voir ni supprimer. Certaines avaient une zone de visualisation (viewBox) de 24x24 pixels, d'autres de 512x512, et certaines n'en avaient pas du tout. Il n'y avait pas de forme unique autour de laquelle construire un composant.
Ainsi, le composant essayait d'imposer un paramètre spécifique – une taille, une couleur, une épaisseur de trait –, et c'est cette partie qui rendait l'uniformité moins bonne, pas meilleure. La propriété `currentColor` ne fonctionne que si le code source n'intègre pas déjà sa propre valeur de remplissage, et la modification de l'épaisseur du trait ne s'applique pas non plus une fois qu'une icône définit sa propre valeur. Chaque nouvelle icône collée nécessitait son propre cas spécifique, ce qui annulait l'intérêt d'un champ de contenu censé ne nécessiter aucune modification du code.
Un champ de contenu qui nécessite une modification du code à chaque fois qu'il est utilisé n'est pas vraiment un champ de contenu.
Sous toutes ces couches se trouvait la partie réellement dangereuse : ce champ était rendu avec `dangerouslySetInnerHTML`, injectant directement tout ce qui était collé dans le DOM, ce qui représente un réel risque de sécurité, et pas seulement un problème de formatage. Les tentatives de correction n'ajoutaient qu'encore du code sans résoudre le problème fondamental : la valeur du champ restait simplement ce qui y avait été copié-collé.
Les corrections qui ne concernent que le code ne résolvent pas non plus le problème.
Revenir à la façon dont tout le monde gère les icônes dans le code ne résout pas le problème du contenu. Les polices d'icônes combinent le cadre et l'image dans un nom de classe – `<i class="fa fa-docker">` –, et une simple faute de frappe rend l'élément vide. Les feuilles de sprites fonctionnent de la même manière, mais en utilisant un identifiant. Les composants SVG intégrés sont les plus difficiles à gérer : l'image est intégrée au graphe d'importation, il n'y a donc pas de registre ni de champ. Les bibliothèques tierces comme lucide-react améliorent l'expérience du développeur, mais `import { Docker }` reste une modification du code, même si elle est plus agréable.
Chacune de ces méthodes est une façon tout à fait valable d'écrire du code pour afficher une icône. Aucune d'entre elles ne permet de modifier l'image affichée sans modifier le code, ce qui était précisément le but recherché.
Fixez-le : un cadre qui n'attend qu'une photo.
@web-portfolio/icons résout ce problème. `<Icon>` est un cadre ; sa propriété "name" récupère une image à partir d'un registre de paquets que `core` crée en combinant devicon, Material Symbols et Simple Icons via SVGO. `<Icon>` ne peut pas distinguer une valeur littérale d'une variable, donc le même appel fonctionne dans les deux cas :
import { Icon } from '@web-portfolio/icons'
<Icon name="docker" size={32} /> // a literal picture
<Icon name={skill.icon} size={32} /> // a picture from a CMS fieldC'est ce dont @web-portfolio/icons-sanity avait besoin : une image n'est qu'une chaîne de caractères, elle peut donc provenir d'un éditeur qui la sélectionne plutôt que d'un développeur qui la tape. Son type de schéma iconRef transforme un champ en une grille consultable dans Sanity Studio — IconPickerInput ne renvoie jamais qu'une image trouvée dans le même registre que celui utilisé par <Icon>. Le rôle du développeur n'a jamais été de choisir l'image, mais simplement de définir la structure.
Il est également important de prendre en compte le coût : comme les deux paquets regroupent le même registre, la seule chose qui est transmise du serveur Sanity vers l'interface utilisateur est cette chaîne de caractères – environ 7 octets en moyenne, contre environ 955 octets pour le code source réel d'une icône typique (jusqu'à plusieurs Ko pour un logo complexe). Stocker directement du SVG dans un champ du CMS, ce que ce plugin vise à éviter, coûte 130 à 285 fois plus par icône, à chaque requête. Les polices d'icônes et les feuilles de sprites évitent également cela en faisant référence à un nom ; les composants SVG intégrés et les bibliothèques basées sur l'importation ne peuvent pas le faire, sauf si elles construisent d'abord le registre exact que ce paquet propose déjà.
Cette même modification résout un problème courant : l'envoi d'une icône avant que le service de conception ne valide une version. Utilisez simplement l'image la plus proche aujourd'hui — avec l'attribut "name="link"" — et modifiez cette valeur unique lorsque le logo officiel est disponible, sans réimportation ni nouveau composant. Pour une solution encore meilleure, intégrez-la dans un champ CMS, de sorte que ceux qui sont responsables du moment donné – le service de conception, le marketing, ou toute autre équipe en charge de la campagne de la semaine – puissent eux-mêmes remplacer l'image directement dans Studio, sans avoir besoin d'une modification du code.
L'image temporaire est intégrée au même commit que la fonctionnalité. Seule l'image est modifiée ultérieurement.
Où il réussit réellement.
Évalué selon les mêmes quatre critères, divisé en trois questions : comment est-ce que c'est d'écrire ce code, quel est son coût d'exécution, et que se passe-t-il lorsque l'icône doit être modifiée et que la personne qui la modifie n'est pas un développeur.
Writing it
icons + icons-sanity | @web-portfolio/icons | Third-party library | Inline SVG | SVG sprite | Icon font | |
|---|---|---|---|---|---|---|
| Adding an icon in code | Même appel — la chaîne de caractères peut provenir d'un champ. | <Icône nom="docker"/> — un seul attribut. | import { Docker} — l'éditeur propose des complétions automatiques. | Un nouveau fichier de composant par icône, copié-collé. | #docker - Le même risque de fautes de frappe qu'avec la saisie à l'aveugle. | fa-docker : aucune vérification que le glyphe existe. |
| Type safety / autocomplete | Picker n'écrit que de vraies clés, une garantie de Studio, et non un simple type. | Le nom est une simple chaîne de caractères ; seule une faute de frappe déclenche un avertissement. | Exportations nommées : une faute de frappe empêche la compilation. | Chaque icône est une importation distincte et nommée. | Une chaîne d'identifiant — même problème. | Une chaîne de caractères représentant le nom d'une classe ; rien ne vérifie son existence. |
| Styling — color, stroke, size | Mêmes propriétés, même composant. | propriétés taille/couleur/épaisseur pour l'élément <Icon>. | Propriétés de taille, de couleur et d'épaisseur de trait définies. | C'est aussi fiable que la personne qui l'a copié-collé. | La propriété fill: currentColor fonctionne de manière efficace. | La couleur est héritée, mais le poids est intégré à la glyphe. |
Running it
icons + icons-sanity | @web-portfolio/icons | Third-party library | Inline SVG | SVG sprite | Icon font | |
|---|---|---|---|---|---|---|
| Bundle impact | Même coût que les icônes seules. | Un registre unique contient les 633 icônes. | Arbre secoué par icône. | Seuls les icônes importées sont expédiées. | Livrés tous les icônes sur une seule feuille. | L'intégralité du fichier de police se charge quel que soit le cas. |
| Runtime selection | Identique, via le sélecteur. | Pas de câblage nécessaire. | Identique — créez la carte vous-même. | Nécessite une nomenclature manuelle. | Déjà identifié. | Déjà indexé par chaîne de caractères. |
Living with it
icons + icons-sanity | @web-portfolio/icons | Third-party library | Inline SVG | SVG sprite | Icon font | |
|---|---|---|---|---|---|---|
| Selecting from dynamic data | Contenu hébergé sur Lives in Sanity, avec mises à jour sans redéploiement. | Le nom peut venir de n'importe où, et rien ne vous aide vraiment à le choisir correctement. | Identique — chaque icône est un composant, et non une clé de recherche. | L'icône est une importation spécifique, et non une valeur interchangeable. | Identique — une chaîne d'identification sans sélecteur. | Une chaîne de caractères : rien ne déclenche l'affichage d'un sélecteur pour cela. |
| Non-developer control | L'élément IconPickerInput inclut déjà le sélecteur intégré. | Pas de zone d'édition disponible - un développeur modifie toujours une propriété. | C'est la même chose : c'est une importation au niveau du code. | Modifier l'icône signifie modifier le code. | Il semble que quelqu'un doive encore créer cet outil. | Nécessite un outil de sélection personnalisé qui connaisse les noms des glyphes. |
| Adding a brand-new icon | Même contrainte : le collage brut de code SVG par un administrateur constitue une solution de secours. | Initialisez la source, exécutez "generate-registry", puis republiez. | Vous recevez ce que la bibliothèque fournit, et rien d'autre. | Collez le code SVG, c'est terminé - il n'y a pas de fichier partagé à modifier. | Modifiez le fichier de sprite, puis redéployez. | Régénérer le fichier de police, reconstruire, redéployer. |
| Consistency across sources | Même registre : Studio signale également une valeur qui est devenue obsolète depuis. | Plusieurs ensembles d'icônes ont été regroupés dans un seul registre. | Cohérence interne assurée, mais les logos de marque ne sont généralement pas inclus. | Il n'y a pas de discipline commune sans que quelqu'un ne la fasse respecter. | La qualité ne peut être meilleure que celle des matériaux utilisés. | Une famille, un langage visuel, par construction. |
| License & provenance | Même attribution, héritée sans modification. | Attribuer une fois par source, dans le registre et le fichier README. | Une licence pour l'ensemble du paquet, généralement claire. | Non traçable : l'attribution reste auprès de la personne qui l'a insérée, le cas échéant. | Généralement, le suivi cesse une fois que les icônes sont copiées dans un seul fichier. | Quel que soit ce que dit la licence du paquet de polices, rarement par glyphe. |
Bundle impact, @web-portfolio/icons row: the ✕ is a client-side cost only. Render <Icon> in a server component and the registry never reaches the browser at all.
Le compromis, sans langue de bois.
Les deux inconvénients mentionnés ci-dessus sont bien réels : la taille du paquet et la sécurité des types. La taille du paquet est un problème car registry.generated.ts contient toutes les icônes dans un seul objet, au lieu de morceaux séparés qu'un bundler peut optimiser. Ainsi, <Icon> charge les 633 icônes même si une page n'en utilise qu'une seule. Une bibliothèque basée sur des importations simples évite ce problème en renonçant à la sélection au moment de l'exécution, ce qui annule le but de stocker les icônes comme du contenu. La solution, mentionnée dans la note ci-dessus : rendre <Icon> côté serveur, et le registre n'atteint jamais le navigateur, ce qui élimine l'écart de taille. C'est également la meilleure approche par défaut pour les icônes en général, surtout maintenant qu'elles peuvent être du contenu plutôt que du code : la valeur n'existe que lors de la requête, il n'y avait donc aucune raison de l'envoyer au navigateur. Rendu de cette manière, <Icon> est équivalent à un SVG intégré et à une importation optimisée, sans JavaScript supplémentaire, et surpasse les polices web (qui nécessitent leur propre fichier) et les feuilles de sprites (qui regroupent tout ou nécessitent leur propre requête).
La sécurité des types fonctionne de la même manière. Si vous tapez un nom à la main, ce ne sont que du texte : aucune faute de frappe n'est détectée. Le sélecteur d'icons-sanity permet uniquement à un éditeur de choisir un nom qui existe réellement, mais cette garantie est valable dans l'interface Studio, et non dans le schéma sous-jacent ; une faute de frappe peut toujours s'introduire depuis l'extérieur de Studio. L'application de cette règle au niveau du schéma est prévue pour la prochaine version mineure.
Quand il faut réellement utiliser ceci.
Cela convient pour un projet utilisant un CMS — site de portfolio, site d'agence, page marketing —, où le choix de l'icône est une question de contenu, et non une question technique. Si rien n'est jamais modifié en dehors du code source, utilisez simplement la bibliothèque optimisée : l'image ne change jamais, il n'y avait donc pas vraiment besoin d'un cadre pour la présenter.
Obtenez le cadre parfait une fois, et changer l'image cesse d'être une rénovation : retirez-en une, insérez-en une autre. Le cadre reste accroché au mur : pas de nouveaux trous, pas de risque de le faire basculer. C'est là que réside l'intérêt réel de la création du registre et du sélecteur : <Icon name>, le registre, et IconPickerInput ne changent plus. Seul le nom qui leur est transmis change.
Montez le cadre, remettez le tableau, reprenez vos activités d'expédition.