Frameworks

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

terminal
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

pages/index.vue
<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.

plugins/glyphfox.ts
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.

components/AppIcon.vue
<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>
components/NotificationsButton.vue
<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.

components/ThemeToggle.vue
<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:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: { componentIslands: true },
});
components/FooterLinks.server.vue
<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:

nuxt.config.ts
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.

PropEn plantillasTipoPor defectoDescripción
sizesizenumber, string24Ancho y alto; los números son píxeles.
colorcolorstringcurrentColorColor principal.
secondaryColorsecondary-colorstringcolorColor de la capa duotone y del relleno sólido.
secondaryOpacitysecondary-opacitynumber0.3 / 1Opacidad de la capa duotone o del relleno sólido.
weightweightnumber2Trazo de 1 a 3 en la cuadrícula de 24, con compensación óptica por debajo de 24 px.
absoluteWeightabsolute-weightbooleanfalseMantiene el trazo en weight píxeles a cualquier tamaño.
cornerscornersround, sharproundUniones y extremos redondeados o rectos.
variantvariantoutline, duotone, solidoutlineEje de estilo; solid se obtiene de las formas cerradas.
rotaterotatenumber0Rotación en grados.
flipXflip-xbooleanfalseRefleja en horizontal.
flipYflip-ybooleanfalseRefleja en vertical.
mirrorRtlmirror-rtlbooleanfalseRefleja en layouts de derecha a izquierda.
animateanimatedraw, pop, wiggle, spin, pulse, bounce, shakeningunoPreset de estado vivo.
triggertriggermount, hover, click, statemountCuándo se reproduce la animación o el morph.
activeactivebooleanfalseEl estado para el disparador state.
durationdurationnumber (ms)según el presetDuración de la animación o del morph.
delaydelaynumber (ms)0Retraso de la animación o del morph.
looploopboolean, numberfalseRepite sin fin o un número de veces (spin y pulse se repiten por defecto).
modifiermodifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotningunoInsignia con hueco de recorte automático.
modifierCornermodifier-cornertop-left, top-right, bottom-left, bottom-rightbottom-rightEsquina de la insignia.
morphTomorph-toname, dataningunoDestino del morph; el disparador decide cuándo se muestra.
titletitlestringningunoNombre accesible: title, role img y aria-label.
decorativedecorativebooleantrue sin titleOculta 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/vue ahí 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.ts se 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-to con el disparador mount por defecto se anima justo después de la hidratación. Usa trigger="state" con estado de useState, como en el interruptor de tema anterior.
Nombres distintos en el servidor y en el navegador
Elegir un icono a partir de window, localStorage o 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 en onMounted o 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.

Iconos de esta guía