El flujo de exportación limpio
Cómo montar el camino desde el fichero de diseño hasta el componente: convenciones en la herramienta, pasos automáticos, verificación, y el punto donde interviene una persona.
Optimizar un SVG a mano es una tarea de diez minutos que hay que repetir cada vez que el diseño cambia, y por eso no se hace. La única versión que sobrevive al contacto con un equipo es la automática, con un contrato claro sobre qué se espera del fichero de entrada y una verificación que falle el build cuando algo se sale. Este artículo es el montaje completo de ese camino.
- Definir el contrato del fichero de entrada que el pipeline puede procesar.
- Encadenar los pasos automáticos en el orden correcto.
- Verificar el resultado con comprobaciones que fallen el build.
- Situar el único punto donde hace falta una decisión humana.
El contrato de entrada
Antes de escribir código, el acuerdo con quien exporta. Seis puntos, y cada uno elimina un problema de raíz:
- Rejilla fija por familia. Todos los iconos de línea en 24; todos los de relleno en 24; las ilustraciones en la que sea, pero declarada. Mezclar rejillas no es un problema (los
symbollo resuelven), pero declararlo permite verificar. - Margen de seguridad. El dibujo no toca los bordes de la rejilla. Dos unidades de aire en una de 24. Evita el recorte del
symbolque se vio en el nivel 19. - Sin marcos con recorte. Elimina los
clipPathespurios. - Sin efectos ni filtros. Las sombras van en CSS.
- Trazos sin expandir. Conserva la posibilidad de cambiar el grosor.
- Paleta cerrada de tokens. Un conjunto acordado de colores, cada uno con su nombre. El pipeline los mapea a variables.
Ese documento es de una página y ahorra más que todo lo demás junto. Lo importante no es que se cumpla al cien por cien, sino que el pipeline pueda verificar cada punto y avisar.
Los pasos, en orden
El orden importa porque algunos pasos dependen del resultado de otros.
// pipeline-iconos.mjs
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { optimize } from 'svgo';
import config from './svgo.config.js';
const ENTRADA = 'diseno/iconos';
const SALIDA = 'src/iconos';
// 1. Normalizar colores a tokens
const PALETA = {
'#1e1e1e': 'currentColor',
'#0d6efd': 'var(--ico-acento, #0d6efd)',
'#6c757d': 'var(--ico-suave, #6c757d)',
};
function normalizarColores(svg) {
let out = svg;
for (const [hex, token] of Object.entries(PALETA)) {
out = out.replace(new RegExp(hex, 'gi'), token);
}
return out;
}
// 2. Quitar el fill none del elemento raiz, que bloquea la herencia
function limpiarRaiz(svg) {
return svg.replace(/(<svg\b[^>]*?)\sfill="none"/i, '$1');
}
// 3. Verificar el contrato
function verificarContrato(svg, nombre) {
const errores = [];
const vb = svg.match(/viewBox="([^"]+)"/)?.[1];
if (!vb) errores.push('sin viewBox');
else {
const [, , w, h] = vb.split(/[\s,]+/).map(Number);
if (w !== h) errores.push(`rejilla no cuadrada ${w}x${h}`);
}
if (/<filter\b/.test(svg)) errores.push('contiene un filter');
if (/<style\b/.test(svg)) errores.push('contiene un bloque style');
if (/#[0-9a-f]{3,8}\b/i.test(svg)) errores.push('color literal sin mapear');
if (errores.length) throw new Error(`${nombre}: ${errores.join('; ')}`);
}
await mkdir(SALIDA, { recursive: true });
const ficheros = (await readdir(ENTRADA)).filter(f => f.endsWith('.svg'));
for (const f of ficheros) {
const bruto = await readFile(join(ENTRADA, f), 'utf8');
const nombre = basename(f, '.svg');
// 4. Optimizar primero: elimina el ruido antes de verificar
const { data } = optimize(bruto, { ...config, path: f });
// 5. Normalizar despues: los colores ya estan en forma canonica
const listo = limpiarRaiz(normalizarColores(data));
// 6. Verificar al final, sobre lo que se va a publicar
verificarContrato(listo, nombre);
await writeFile(join(SALIDA, f), listo);
}
console.log(`${ficheros.length} iconos procesados`);
Por qué ese orden. Optimizar antes de normalizar porque SVGO canoniza los colores (convierte rgb(30,30,30) y #1E1E1E a la misma forma), lo que hace que el mapeo de la paleta funcione con una sola entrada por color. Verificar al final porque lo que importa es el fichero publicado, no el intermedio.
La verificación que falla el build
El bloque verificarContrato es el corazón del montaje y merece crecer. Cinco comprobaciones más que se pagan solas:
El viewBox existe y tiene la rejilla esperada. Ya está.
No hay identificadores sin prefijar. Si prefixIds está activo, todos los id deben empezar por el nombre del fichero.
No hay colores literales. Ya está, y es la que más avisos genera: cada aviso es un color que el diseño ha usado fuera de la paleta acordada.
El dibujo cabe en la rejilla con margen. Requiere calcular la caja envolvente, lo que exige un entorno con DOM. Con una biblioteca de parseo de rutas se puede aproximar leyendo las coordenadas de las d.
El peso está por debajo de un umbral. Un icono de línea que pesa 4 kB es un icono que alguien ha construido con trescientos puntos. El aviso obliga a mirarlo.
const LIMITE = 2048;
if (Buffer.byteLength(listo) > LIMITE) {
console.warn(`${nombre}: ${Buffer.byteLength(listo)} bytes, revisar`);
}
Umbral como aviso, no como error: hay iconos legítimamente complejos.
La comparación visual
La única prueba que detecta una optimización que cambia el dibujo. El montaje mínimo: rasterizar original y optimizado con un navegador sin interfaz, al tamaño real de uso, y comparar píxeles con una tolerancia.
Merece la pena decir lo que hay que evitar: comparar a un tamaño grande. Un icono comparado a 512 píxeles esconde los fallos que aparecen a 16, que son los que importan. Rasteriza a los tamaños que usas de verdad.
Y guardar las imágenes de referencia en el repositorio, para que un cambio de versión del optimizador se detecte como una diferencia en el diff.
Todo lo demás de este artículo es mecánico. Lo que ninguna herramienta puede hacer es la revisión que evita el problema real de las bibliotecas de iconos: el crecimiento sin control.
El patrón es siempre el mismo. Un diseñador necesita un icono de «documento» para una pantalla nueva. La biblioteca ya tiene «fichero», «página» y «hoja», pero ninguno le convence del todo, y añadir uno cuesta cinco minutos. A los dos años la biblioteca tiene cuatrocientos iconos, veinte de los cuales son variaciones imperceptibles del mismo concepto, y nadie sabe cuál usar. El sprite pesa lo que pesa, el peso ya no lo arregla ningún optimizador, y el problema de verdad no es el peso: es que la interfaz usa tres iconos distintos para la misma cosa en tres pantallas distintas.
Ese es un problema de diseño de sistema, y su única defensa es un punto de revisión humano en la incorporación: alguien que, ante cada icono nuevo, se pregunte si ya existe uno que sirva y si el concepto que representa es realmente nuevo. Es la misma disciplina que se aplica a añadir una dependencia.
La forma barata de implementarlo: que el pipeline detecte los ficheros nuevos y los liste en la salida. Un icono nuevo aparece en el registro del build, se ve en la revisión del código, y alguien tiene la oportunidad de preguntar. Sin ese aviso, los iconos nuevos entran en silencio dentro de un directorio que nadie revisa.
Y el corolario incómodo: la métrica que hay que vigilar no es el peso del sprite, es el número de iconos y su tasa de crecimiento. Un sprite que crece un icono por semana está diciendo algo sobre el sistema de diseño que ninguna optimización va a arreglar.
Monta el pipeline de este artículo para tu proyecto y hazlo fallar a propósito tres veces: con un icono que tenga un color fuera de la paleta, con uno que traiga un filtro, y con uno sin viewBox. Comprueba que los tres mensajes de error dicen exactamente qué hay que arreglar y en qué fichero. Un pipeline cuyo error no dice qué hacer se desactiva a la primera prisa.