Iconos en Nuxt
glyphfox/vue se renderiza en el servidor, así que las páginas de Nuxt llegan con SVG real en el HTML. Esta guía muestra la configuración, un plugin para iconos compartidos, un componente envoltorio autoimportado y cómo se comportan el registro y los morphs entre el servidor y el navegador.
Instalación
npm install glyphfox
No hay ningún módulo de Nuxt que añadir. Los componentes de Vue están en glyphfox/vue, que se distribuye como módulos ES estándar con una versión CommonJS para require, así que Nuxt lo importa en el servidor y en el navegador como cualquier otra dependencia. Todo lo que explica la guía de Vue sigue siendo válido; esta página trata de lo que cambia cuando los mismos componentes se renderizan primero en el servidor.
Usa iconos en páginas y componentes
<script setup lang="ts"> import { SearchIcon, ArrowRightIcon } from 'glyphfox/vue'; </script> <template> <form class="search" action="/search"> <SearchIcon :size="18" /> <input name="q" type="search" placeholder="Search the docs" aria-label="Search the docs"> <button type="submit"> Search <ArrowRightIcon :size="16" mirror-rtl /> </button> </form> </template>
El svg forma parte del HTML del servidor, así que los iconos se ven antes de que se ejecute cualquier JavaScript y nunca desplazan el diseño cuando la página se hidrata. Después de la hidratación, los mismos componentes gestionan los disparadores, los morphs y los cambios de estado. mirror-rtl invierte los iconos direccionales, como las flechas, cuando la página o un elemento padre tiene dir="rtl".
Importar los componentes en cada archivo es la configuración más explícita y la que mantiene pequeño el chunk de cada ruta: una página solo lleva los iconos que usa.
Un plugin para iconos compartidos
Un plugin es el lugar para dos cosas: los componentes que todas las plantillas deben ver y los datos de los nombres que solo aparecen como cadenas, en Icon, Fusion, MorphIcon o morph-to.
import { HomeIcon, SettingsIcon, Icon, registerIcons } from 'glyphfox/vue'; import menu from 'glyphfox/icons/menu'; import x from 'glyphfox/icons/x'; import moon from 'glyphfox/icons/moon'; import cloud from 'glyphfox/icons/cloud'; import lock from 'glyphfox/icons/lock'; // Runs once per process on the server and once in the browser. registerIcons([menu, x, moon, cloud, lock]); export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.component('HomeIcon', HomeIcon); nuxtApp.vueApp.component('SettingsIcon', SettingsIcon); nuxtApp.vueApp.component('GfIcon', Icon); });
Mantén este plugin universal, sin el sufijo .client ni .server: el servidor necesita los datos para escribir el HTML y el navegador necesita esos mismos datos para hidratarlo. En el servidor, el registro es estado del módulo que comparten todas las solicitudes, lo cual es seguro porque los datos de los iconos nunca cambian.
Un componente envoltorio autoimportado
Nuxt importa automáticamente los componentes de components/. Un pequeño envoltorio le da a la aplicación un único componente de icono con tus valores por defecto y una lista fija de nombres, así que una errata hace fallar la comprobación de tipos en lugar de renderizar un svg vacío.
<script setup lang="ts"> import { Icon } from 'glyphfox/vue'; import home from 'glyphfox/icons/home'; import search from 'glyphfox/icons/search'; import settings from 'glyphfox/icons/settings'; import bell from 'glyphfox/icons/bell'; const icons = { home, search, settings, bell }; const props = withDefaults(defineProps<{ name: keyof typeof icons; label?: string; size?: number }>(), { size: 20 }); </script> <template> <Icon :icon="icons[props.name]" :size="props.size" :weight="1.75" :title="props.label" /> </template>
<template> <button type="button" aria-label="Notifications"> <AppIcon name="bell" /> </button> </template>
Pasar los datos del icono mediante la prop icon evita por completo el registro de nombres, así que el envoltorio renderiza el mismo marcado en el servidor y en el navegador, independientemente de qué se haya registrado y dónde. Sin label, el icono queda como decorativo, que es lo correcto dentro de un botón con etiqueta.
Morphs, animaciones e hidratación
El servidor renderiza un fotograma fijo. Las animaciones de los estados vivos son CSS dentro del svg, así que un spinner con animate="spin" ya gira en el HTML del servidor, salvo que el visitante prefiera movimiento reducido. Los morphs y los disparadores de hover y clic necesitan que el componente se hidrate primero.
morph-to con el disparador por defecto (mount) renderiza el icono inicial en el servidor y reproduce el morph justo después de la hidratación, lo que se ve como un parpadeo en cada carga de página. Si la página debe abrirse en el estado de destino, usa trigger="state" con :active enlazado a un estado compartido: el servidor renderiza el destino y el morph solo se reproduce cuando cambia el estado.
<script setup lang="ts"> import { SunIcon } from 'glyphfox/vue'; const dark = useState('dark', () => false); </script> <template> <button type="button" aria-label="Dark theme" :aria-pressed="dark" @click="dark = !dark"> <SunIcon morph-to="moon" trigger="state" :active="dark" /> </button> </template>
useState se serializa en el payload de la página, así que el servidor y el navegador parten del mismo valor y el icono se hidrata sin desajustes. El destino del morph, moon, está registrado en el plugin anterior.
Bundle y payload
En una aplicación renderizada en el servidor, un icono cuesta dos veces: el marcado svg en el HTML y el código del componente que lo hidrata. El marcado es pequeño, unos pocos trazados por icono. Medido en una compilación de producción con Vite 8, el runtime compartido de glyphfox para Vue ocupa unos 20 KB con gzip y cada componente de icono añade aproximadamente 0.1 KB con gzip, así que el runtime es lo que más pesa y los iconos adicionales salen baratos.
No guardes datos de iconos en useState ni en useAsyncData. Todo lo que se almacena ahí se serializa en el HTML de cada página que lo usa. Importa en su lugar los módulos de iconos; se almacenan en caché junto con el resto de tu JavaScript.
Para los iconos que nunca cambian, como los de un pie de página o una sección de marketing estática, un componente de servidor renderiza el marcado sin enviar código de iconos. Activa las islas de componentes y nombra el componente con el sufijo .server.vue:
export default defineNuxtConfig({ experimental: { componentIslands: true }, });
<script setup lang="ts"> import { GitBranchIcon, RssIcon, MailIcon } from 'glyphfox/vue'; </script> <template> <ul class="footer-links"> <li><a href="https://github.com/kwy404/glyphfox"><GitBranchIcon :size="18" /> Source</a></li> <li><a href="/feed.xml"><RssIcon :size="18" /> Feed</a></li> <li><a href="mailto:hello@example.com"><MailIcon :size="18" /> Contact</a></li> </ul> </template>
Nombres sin registrar
Un nombre que nadie registró se obtiene mediante una importación dinámica dentro del paquete. Nuxt compila con Vite, y Vite omite las importaciones dinámicas con variables cuando están en node_modules, así que en una compilación de producción esos nombres nunca se cargan. Si de verdad necesitas cualquier nombre en tiempo de ejecución, por ejemplo en un selector de iconos, deja que Vite empaquete el cargador. Así genera un pequeño chunk por icono, que solo se descarga cuando se usa ese nombre:
export default defineNuxtConfig({ vite: { build: { dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] }, }, }, });
Esto solo sirve después de la hidratación. El renderizado en el servidor no espera a una importación dinámica, así que un icono que debe estar en el HTML sigue necesitando registrarse o importarse como componente.
Props
Los componentes son los de glyphfox/vue, con las mismas props. En las plantillas puedes usar los nombres de atributo en kebab-case.
| Prop | En plantillas | Tipo | Por defecto | Descripción |
|---|---|---|---|---|
size | size | number, string | 24 | Ancho y alto; los números son píxeles. |
color | color | string | currentColor | Color principal. |
secondaryColor | secondary-color | string | color | Color de la capa duotone y del relleno sólido. |
secondaryOpacity | secondary-opacity | number | 0.3 / 1 | Opacidad de la capa duotone o del relleno sólido. |
weight | weight | number | 2 | Trazo de 1 a 3 en la cuadrícula de 24, con compensación óptica por debajo de 24 px. |
absoluteWeight | absolute-weight | boolean | false | Mantiene el trazo en weight píxeles a cualquier tamaño. |
corners | corners | round, sharp | round | Uniones y extremos redondeados o rectos. |
variant | variant | outline, duotone, solid | outline | Eje de estilo; solid se obtiene de las formas cerradas. |
rotate | rotate | number | 0 | Rotación en grados. |
flipX | flip-x | boolean | false | Refleja en horizontal. |
flipY | flip-y | boolean | false | Refleja en vertical. |
mirrorRtl | mirror-rtl | boolean | false | Refleja en layouts de derecha a izquierda. |
animate | animate | draw, pop, wiggle, spin, pulse, bounce, shake | ninguno | Preset de estado vivo. |
trigger | trigger | mount, hover, click, state | mount | Cuándo se reproduce la animación o el morph. |
active | active | boolean | false | El estado para el disparador state. |
duration | duration | number (ms) | según el preset | Duración de la animación o del morph. |
delay | delay | number (ms) | 0 | Retraso de la animación o del morph. |
loop | loop | boolean, number | false | Repite sin fin o un número de veces (spin y pulse se repiten por defecto). |
modifier | modifier | plus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dot | ninguno | Insignia con hueco de recorte automático. |
modifierCorner | modifier-corner | top-left, top-right, bottom-left, bottom-right | bottom-right | Esquina de la insignia. |
morphTo | morph-to | name, data | ninguno | Destino del morph; el disparador decide cuándo se muestra. |
title | title | string | ninguno | Nombre accesible: title, role img y aria-label. |
decorative | decorative | boolean | true sin title | Oculta el icono a las tecnologías de apoyo. |
Errores comunes
- Funciona en desarrollo, vacío en producción
- El servidor de desarrollo pre-empaqueta la dependencia completa, así que todos los nombres se resuelven localmente. La compilación de producción solo conserva tus importaciones y deja fuera el cargador bajo demanda, así que los nombres que solo se usan en cadenas renderizan un svg vacío. Regístralos en el plugin universal.
- Un icono se renderiza en el servidor y luego desaparece
- Cuando el servidor carga glyphfox sin tree shaking, importar
glyphfox/vueahí registra todos los iconos, mientras que el bundle del navegador solo registra lo que importas. Así, un nombre que solo se usa como cadena puede aparecer en el HTML del servidor y quedarse vacío tras la hidratación, con un aviso de hidratación en la consola. Registrar explícitamente cada uno de esos nombres elimina la diferencia. - Registrar en un plugin solo de cliente
- Un
plugins/glyphfox.client.tsse ejecuta cuando el servidor ya ha renderizado la página, así que el HTML contiene iconos vacíos que aparecen de golpe más tarde. Usa un plugin sin sufijo. - Un morph que se reproduce en cada carga de página
morph-tocon el disparador mount por defecto se anima justo después de la hidratación. Usatrigger="state"con estado deuseState, como en el interruptor de tema anterior.- Nombres distintos en el servidor y en el navegador
- Elegir un icono a partir de
window,localStorageo la hora actual hace que el primer renderizado del navegador sea distinto del del servidor, y Vue señala un desajuste de hidratación. Lee esos valores enonMountedo guárdalos en una cookie que el servidor pueda leer.
Preguntas frecuentes
¿Necesito un módulo de Nuxt para glyphfox?
No. Importa los componentes de glyphfox/vue donde los uses o registra un conjunto compartido en un plugin. Un módulo solo añadiría el registro global, algo que un plugin de diez líneas ya te da sin ocultar qué iconos terminan en el bundle.
¿Funciona con nuxt generate y con rutas prerenderizadas?
Sí. El prerenderizado ejecuta el mismo renderizado en el servidor en tiempo de compilación, así que los archivos HTML generados contienen el marcado svg. La regla del registro es la misma: un nombre que la compilación no puede resolver se escribe como un svg vacío.