Tus documentos están bien hasta que alguien intenta pegar un comando y se incluyen silenciosamente un número de línea, un símbolo de prompt y un espacio final. Entonces llega el ticket: “sus instrucciones rompieron producción”.
Los bloques de código de GitHub parecen sencillos: barra de título con nombre de archivo, botón de copiar que realmente copia lo correcto, números de línea que no contaminan el portapapeles y líneas resaltadas que señalan la única línea que importa. Reproducir eso en tu sitio es totalmente posible—si dejas de tratarlo como “solo CSS” y empiezas a tratarlo como un componente con requisitos de fiabilidad.
Cómo debe verse “bien” en la documentación de producción
Los bloques de código al estilo GitHub no se tratan de verse bonitos. Se tratan de reducir el riesgo operativo. Cuando alguien sigue un runbook a las 03:00, el bloque de código es la interfaz. Si miente, confunde o copia los bytes equivocados, tu “sitio de docs” acaba de contribuir a un incidente.
Así que aquí está el umbral que uso:
- Copiar es exacto. Copia solo el código. No prompts, no números de línea, ni confeti Unicode invisible.
- Los números de línea son puramente presentacionales. Ayudan a referir “línea 17” sin contaminar el contenido del portapapeles.
- Las líneas resaltadas están impulsadas por datos. El autor puede especificar qué líneas importan (contexto de diff, “cambia esto”, “no ejecutar”).
- La barra de título lleva metadatos útiles. Nombre de archivo, lenguaje, quizás “shell”, quizás “k8s”, quizás “output”. No decoración sin sentido.
- Accesible por defecto. El botón de copiar funciona con teclado, anuncia el estado y no roba el foco como un niño con un nuevo tambor.
- Rápido. Renderizar 30 bloques de código no debería convertir una página de docs en un calentador de espacio.
- Funciona sin conexión y bajo CSP. Los sistemas de producción suelen tener políticas. Tus docs deberían sobrevivirlas.
Idea parafraseada (de John Ousterhout): la complejidad es lo que hace que los sistemas sean difíciles de cambiar y razonar; si puedes eliminarla, hazlo.
Y sí, voy a tratar un “widget de bloque de código” como un mini sistema de producción. Porque lo es. También es un sistema distribuido: autor, renderizador, navegador, API del portapapeles y la paciencia del usuario—ninguno de los cuales controlas por completo.
Hechos y breve historia (por qué GitHub ganó)
Un poco de contexto te ayuda a tomar mejores decisiones. Estos son hechos pequeños, pero explican por qué el ecosistema se ve como se ve.
- Los primeros ejemplos de código web eran <pre> simples. El “resaltado de sintaxis” comenzó como hacks con regex en servidor en los años 90, mucho antes de que los navegadores tuvieran buenas tipografías o motores de maquetación.
- Pygments (mediados de 2000) hizo el resaltado mainstream. Popularizó el modelo de “tokenizar y aplicar spans con estilo” que la mayoría de los resaltadores aún usan.
- GitHub popularizó los bloques de código fenced en Markdown. La convención de triple backtick se volvió el modelo mental por defecto para código en docs.
- Las APIs del portapapeles evolucionaron tarde. Durante años, “copiar” significaba seleccionar texto y esperar que el DOM no incluyera basura; la Clipboard API moderna hizo posibles botones de copiar fiables.
- Los números de línea siempre han sido controvertidos. Los IDE los necesitan; la documentación a menudo no. El debate existe porque los números de línea son útiles pero fáciles de implementar incorrectamente.
- El resaltado del lado cliente fue reacción al hosting estático. Cuando todos empezaron a desplegar docs en CDNs, enviar resaltadores JS sintió más simple que renderizar en servidor—hasta que llegaron las facturas de rendimiento.
- Los patrones de UI de GitHub se volvieron estándar de facto. La barra de título + botón de copiar es familiar, así que los usuarios confían y la usan sin pensar.
- Los resaltadores ahora compiten en “corrección semántica”. Tree-sitter y parsers similares subieron el listón; los tokenizadores por regex son más rápidos de construir, pero menos precisos para lenguajes complejos.
Dos conclusiones: primero, la mayoría de las características de “bloque de código” están atornilladas sobre primitivas antiguas. Segundo, lo que parece una UI simple suele ser tres sistemas separados pegados con cinta: renderizado, interacción y autoría.
Arquitectura: un componente, tres rutas de datos
Un componente de bloque de código al estilo GitHub tiene tres rutas que deben coincidir:
1) Ruta de visualización: lo que el usuario ve (resaltado, números de línea, barra de título).
2) Ruta de portapapeles: lo que se copia (debe ser código crudo, normalizado de manera sensata).
3) Ruta de referencia: a qué se refieren autores y lectores (números de línea, líneas resaltadas, anclas).
Si estas divergen, obtienes modos de fallo que parecen error de usuario pero no lo son:
- El botón de copiar copia prompts o números → el comando pegado falla → el usuario desconfía de la docs.
- Las líneas resaltadas no coinciden con el código real debido a líneas envueltas o spans ocultos → el usuario cambia la línea equivocada.
- Los números de línea se desplazan entre SSR y la hidratación → la gente comenta “línea 14” y se refiere a contenido distinto.
Elige tu estrategia de renderizado: SSR, en tiempo de compilación o cliente
Tienes tres opciones realistas:
| Estrategia | Pros | Contras | Cuándo elegir |
|---|---|---|---|
| Resaltado en tiempo de build (p. ej., Shiki) | Páginas rápidas, sin JS de resaltado en runtime, salida consistente | Builds más lentos; cambios de tema requieren rebuild | Sitios de docs, blogs, runbooks, cualquier cosa estática |
| Renderizado en servidor | Consistente, puede hacer theming por petición, sin JS cliente pesado | Más infraestructura; el cacheo importa | Docs de apps integradas al producto, docs autenticadas |
| Resaltado del lado cliente (Prism/Highlight.js) | Integración fácil; contenido dinámico | Peso de JS, picos de CPU, rarezas en la hidratación | Editores interactivos, contenido generado por usuarios, último recurso |
Soy opinativo aquí: para la mayoría de documentación y runbooks operativos, haz el resaltado en tiempo de build o SSR. El resaltado del lado cliente es un impuesto que pagas para siempre.
Define un modelo explícito de bloque de código
Deja de permitir que el parser de Markdown “decida” qué es tu bloque de código. Modelalo. Como mínimo:
- language (bash, yaml, json, …)
- title (nombre de archivo, o etiqueta como “nginx.conf”)
- code (contenido crudo, finales de línea normalizados)
- highlight (rangos de líneas: 3,5-8)
- showLineNumbers (bool)
- copyTextOverride (opcional; p. ej., eliminar prompts)
- kind (source, terminal, output, diff)
Una vez tengas ese modelo, tu renderizador puede ser determinista, comprobable y aburrido. Aburrido es bueno. Lo aburrido se envía.
Números de línea: golosina UX con aristas
Los números de línea mejoran la colaboración: “cambia la línea 42” es una instrucción clara. Pero vienen con trampas.
Cómo fallan los números de línea
- Se copian. Si los implementas insertando nodos de texto reales al inicio de cada línea, se filtrarán en la selección y el portapapeles.
- Se desincronizan. Si el wrapping cambia lo que los usuarios perciben como “una línea”, se referirán a lo incorrecto.
- Rompen la búsqueda. Algunas implementaciones alteran el DOM de modo que el buscar-en-página del navegador deja de coincidir con segmentos de código esperados.
- Ralenticen el renderizado. Dividir en miles de elementos por línea puede convertirse en una explosión del DOM.
Patrones de implementación que soportan carga
Dos patrones suelen funcionar:
- Contadores CSS para los números de línea, sin insertar números en el texto. Es rápido y la selección puede mantenerse limpia.
- Columna de gutter separada con números de línea como elementos propios, mientras el texto del código permanece en un bloque seleccionable separado.
Si resaltas líneas envolviendo cada línea en un elemento, ya estás dividiendo líneas. Eso está bien para bloques pequeños, pero necesitas un umbral. Pasado cierto tamaño, cambia a “sin DOM por línea”.
Regla operativa: si un bloque de código excede unas pocas miles de líneas, no renderices spans por línea en el navegador. Renderiza un <pre> simple u ofrece una descarga.
Líneas resaltadas: la forma más rápida de reducir errores
Resaltar líneas no es decoración. Es una baranda de seguridad. Usado correctamente, reduce la carga cognitiva de “qué parte debo cambiar”.
Buenos usos
- Señalar ediciones en archivos de configuración: muestra el archivo completo, resalta solo las líneas que difieren.
- Apuntar a comandos peligrosos: resalta la línea destructiva en un snippet con varios pasos.
- Enseñanza estilo diff: resalta líneas que corresponden a una petición de cambio de una revisión.
Malos usos
- Resaltar la mitad del bloque. Eso no es énfasis; es un berrinche de resaltador.
- Usar color de resaltado con bajo contraste en modo oscuro. La gente lo pasará por alto.
- Resaltar basado en “líneas visuales envueltas”. Es una pesadilla. Usa solo líneas lógicas.
Formato de autoría: mantenlo aburrido
No inventes un mini-lenguaje nuevo para rangos de líneas. Usa el formato establecido “1,3-5,8”. Parséalo de forma determinista y falla ruidosamente.
Si el autor solicita líneas resaltadas fuera de la longitud del bloque, tienes dos opciones sensatas:
- Fallar el build (mi preferencia para runbooks), o
- Advertir e ignorar (aceptable para blogs).
Barras de título: nombres de archivos, etiquetas de lenguaje y metadatos
Una barra de título es útil cuando proporciona orientación. “Aquí está /etc/nginx/nginx.conf” es accionable. “Código” no lo es.
Qué incluir
- Nombre de archivo o etiqueta (p. ej.,
values.yaml,docker-compose.yml). - Lenguaje (una pequeña etiqueta ayuda: bash, yaml, json).
- Botón de copiar con una clara affordance.
- Opcional “ver raw” para bloques muy grandes (servir como archivo, no un DOM de 10k líneas).
No lo sobrecargues
Las barras de título no son dashboards. Si llenas de hashes de commit, timestamps y nombres de entorno, has construido un acordeón de distracciones. Mantenlo mínimo, consistente y estable en todo el sitio.
Rendimiento y preocupaciones operativas (sí, en serio)
Los bloques de código se convierten en un problema de rendimiento en tres escenarios previsibles:
- Muchos bloques en una página (los runbooks suelen ser densos).
- Bloques enormes (configs generados, logs, manifiestos de Kubernetes).
- Resaltado del lado cliente (picos de CPU, tareas largas, jank).
Qué presupuestar
Piénsalo en presupuestos como harías para una API:
- CPU: evita tokenizar contenido grande en el cliente.
- Nodos del DOM: evita wrappers por línea por encima de un umbral.
- Bytes de JS: no envíes 40 lenguajes si necesitas 6.
- Fuentes: una fuente monoespaciada de fallback está bien; no bloquees el renderizado por fuentes fancy.
El cache importa (incluso para el resaltado)
Si haces resaltado en servidor o en build-time, cachea la salida por una clave estable: hash(code + language + theme + highlighter-version). Si no, volverás a resaltar los mismos snippets en cada build o petición, y tu CI comenzará a sentirse como si minara criptomonedas.
Seguridad: trata los bloques de código como texto no confiable
Si tu sistema renderiza código generado por usuarios, asume que el contenido es hostil. El resaltado de sintaxis a menudo inyecta spans HTML; si no sanitizas correctamente, puedes crear XSS a través del “código”.
El enfoque más seguro es renderizar tokens a HTML con escaping hecho de forma centralizada, y nunca permitir passthrough de HTML crudo dentro de los bloques de código.
Instrumentación y monitorización para bloques de código
Si lanzas un botón de copiar y nunca lo mides, aprenderás sobre fallos vía humanos enfadados. Instruméntalo.
Qué medir
- Tasa de éxito de copia (promise resuelta vs rechazada).
- Time to interactive en páginas con muchos bloques de código.
- Tareas largas después de la carga de página (el resaltado cliente es reincidente).
- Recuento de nodos DOM en páginas grandes (proxy de “envolvimos cada línea”).
- Duración del resaltado en build-time (si se dispara, cambiaste algo).
Logging sin ser invasivo
No registres el contenido del código en eventos de copia. Terminarás con secretos, tokens y claves API en analítica. Registra solo metadatos: id de página, lenguaje, longitud del bloque, si había prompts, éxito/fallo.
Chiste #2: Lo único más sensible que los secretos de producción es la reacción del equipo legal cuando los registras.
Guía de diagnóstico rápido
Cuando los usuarios se quejan de que “los bloques de código son lentos” o “copiar no funciona”, no debatas estéticas. Triagéalo como un incidente.
Primero: confirma el modo de fallo en 60 segundos
- Corrección de copia: haz clic en copiar, pega en un editor de texto plano, inspecciona si hay números de línea/prompts/espacios extraños.
- Consola del navegador: busca errores de permiso de portapapeles o violaciones de CSP.
- Rendimiento de la página: abre devtools performance, recarga, busca tareas largas alrededor de resaltado/hidratación.
Segundo: localiza el cuello de botella
- Bound por CPU: muchos ms en ejecución JS → probablemente resaltado cliente, DOM por línea, o selectores costosos.
- Bound por DOM: layout/recalc style domina → demasiados nodos, CSS pesado, lógica de wrapping de líneas.
- Bound por red: bundles JS grandes o archivos de fuentes → highlighter o packs de lenguaje enviados innecesariamente.
Tercero: aplica una solución quirúrgica, no una reescritura
- Mueve el resaltado a build/SSR.
- Reduce los lenguajes enviados.
- Deja de envolver líneas por encima de un umbral.
- Copiar desde la cadena origen, no desde el DOM.
- Añade prompts visuales vía pseudo-elementos CSS o spans separados excluidos de la carga de copia.
Heurística: si la página lenta tiene 10+ bloques de código y el pico de CPU se correlaciona con funciones de “highlight”, la solución es arquitectónica, no micro-optimizaciones.
Tareas prácticas con comandos, salidas y decisiones
Estos son los tipos de comprobaciones que ejecutas cuando los bloques de código se comportan mal. Cada tarea incluye un comando, lo que significa la salida y qué decisión tomar. Los comandos asumen que trabajas en un host Linux donde corre el sitio de docs o el build.
Task 1: Verify Node and package manager versions (reproducibility)
cr0x@server:~$ node --version
v20.11.1
Significado de la salida: estás en Node 20; los polyfills de clipboard y toolchains de build se comportan distinto entre versiones mayores.
Decisión: fija Node en CI (y localmente vía herramientas) si ves salidas de resaltado inconsistentes entre entornos.
Task 2: Measure build-time highlighting cost (is it the bottleneck?)
cr0x@server:~$ /usr/bin/time -v npm run build
...
User time (seconds): 58.23
System time (seconds): 6.12
Percent of CPU this job got: 342%
Elapsed (wall clock) time: 0:18.74
Maximum resident set size (kbytes): 912344
Significado de la salida: mucho CPU, ~900MB RSS. Resaltadores como Shiki pueden consumir memoria con muchas páginas/lenguajes.
Decisión: cachea salida resaltada y limita los lenguajes soportados; si RSS amenaza contenedores CI, divide builds o precomputalo.
Task 3: Find the heaviest pages by code block count (risk hotspot)
cr0x@server:~$ rg -n "```" -S docs/ | cut -d: -f1 | sort | uniq -c | sort -nr | head
84 docs/runbooks/storage/zfs-replace-disk.md
62 docs/runbooks/kubernetes/etcd-restore.md
51 docs/platform/nginx/hardening.md
Significado de la salida: estos archivos tienen más bloques fenced.
Decisión: prueba de carga estas páginas primero; optimiza los peores antes de perseguir mejoras marginales en otros sitios.
Task 4: Confirm your rendered HTML isn’t copying line numbers (quick sanity check)
cr0x@server:~$ rg -n "data-line-number|class=\"line-number\"" -S dist/ | head
dist/runbooks/storage/zfs-replace-disk/index.html:412: 1
dist/runbooks/storage/zfs-replace-disk/index.html:413: 2
Significado de la salida: los números de línea son nodos de texto/spans reales, que pueden terminar en la selección/portapapeles.
Decisión: muévete a contadores CSS o gutter separado excluido de la selección/copia, o asegura que la ruta de copia use el código crudo, no el texto del DOM.
Task 5: Detect suspicious Unicode in snippets (clipboard correctness)
cr0x@server:~$ python3 -c 'import sys,unicodedata; s=open("docs/runbooks/kubernetes/etcd-restore.md","r",encoding="utf-8").read(); bad=[c for c in s if unicodedata.category(c) in ("Cf",)]; print(len(bad), sorted(set(hex(ord(c)) for c in bad))[:10])'
3 ['0x200b', '0x2060']
Significado de la salida: hay caracteres de formato (zero-width space, word joiner). Pueden romper comandos pegados.
Decisión: añade un hook pre-commit o lint en CI que rechace estos caracteres en docs, o al menos los marque.
Task 6: Confirm the JS bundle isn’t shipping 40 languages (weight control)
cr0x@server:~$ ls -lh dist/assets | sort -k5 -h | tail
-rw-r--r-- 1 cr0x cr0x 84K app.css
-rw-r--r-- 1 cr0x cr0x 312K app.js
-rw-r--r-- 1 cr0x cr0x 1.8M highlight.bundle.js
Significado de la salida: el bundle del resaltador domina tu payload JS.
Decisión: cambia a resaltado en build-time o tree-shake lenguajes; no aceptes 1.8MB de impuesto por colores bonitos.
Task 7: Check for CSP violations affecting clipboard
cr0x@server:~$ rg -n "Content-Security-Policy" -S nginx/conf.d/docs.conf
12:add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';" always;
Significado de la salida: los scripts inline están bloqueados; si tu botón de copiar depende de handlers inline, fallará silenciosamente.
Decisión: mueve la lógica de copia a JS externo, evita atributos de evento inline, o añade una política con nonce si es necesario.
Task 8: Validate that build output has stable anchors for line highlights
cr0x@server:~$ rg -n "data-highlight|data-line" -S dist/runbooks/storage/zfs-replace-disk/index.html | head 615:Significado de la salida: metadatos de highlight están presentes en el HTML; tu cliente puede estilarlos sin re-tokenizar.
Decisión: mantén los rangos de highlight como atributos data; evita recomputar mapas de líneas en el navegador.
Task 9: Identify oversized code blocks that should be “view raw”
cr0x@server:~$ awk 'BEGIN{in=0; n=0} /^```/{in=!in; if(!in){print n; n=0}} {if(in) n++}' docs/runbooks/storage/zfs-replace-disk.md | sort -nr | head 412 188 141Significado de la salida: hay un snippet de 412 líneas; no es enorme, pero es candidato a problemas de rendimiento si envuelves cada línea.
Decisión: establece un umbral (p. ej., 200–500 líneas) donde el DOM por línea se deshabilita o se cambia a un modo ligero.
Task 10: Confirm gzip/brotli is enabled (network bottlenecks)
cr0x@server:~$ nginx -T 2>/dev/null | rg -n "gzip|brotli" | head gzip on; gzip_types text/plain text/css application/javascript application/json image/svg+xml;Significado de la salida: gzip está activado; si tu JS sigue siendo pesado, la compresión ayuda pero no arregla el coste de CPU.
Decisión: mantiene la compresión, pero enfócate en reducir JS y DOM en lugar de celebrar transferencias más pequeñas.
Task 11: Spot-check for per-line DOM explosions (DOM node proxy)
cr0x@server:~$ rg -n "class=\"line\"" -S dist/runbooks/kubernetes/etcd-restore/index.html | wc -l 6220Significado de la salida: miles de elementos por línea fueron emitidos.
Decisión: deja de emitir wrappers por línea para bloques grandes; usa contadores CSS o un bloque tokenizado único.
Task 12: Check Lighthouse CLI for page regressions (automation-friendly)
cr0x@server:~$ lighthouse dist/runbooks/kubernetes/etcd-restore/index.html --quiet --chrome-flags="--headless" --only-categories=performance Performance: 62Significado de la salida: la puntuación de rendimiento es mediocre; los bloques de código son culpables comunes por JS pesado o DOM complejo.
Decisión: perfila la página; si las tareas largas se correlacionan con resaltado, mueve el trabajo a build-time y reduce la complejidad del DOM.
Task 13: Verify prompts aren’t being baked into copy payloads
cr0x@server:~$ rg -n "cr0x@server:~\\$" -S dist/ | head dist/runbooks/storage/zfs-replace-disk/index.html:618: cr0x@server:~$ zpool statusSignificado de la salida: cadenas de prompt aparecen en el HTML renderizado. Eso está bien visualmente, riesgoso si tu lógica de copia raspa texto del DOM.
Decisión: almacena una cadena de comando cruda separada para copiar, o marca spans de prompt con
data-no-copyy haz cumplir eso en la lógica de copia.Task 14: Confirm no secrets are embedded in code blocks (yes, people do this)
cr0x@server:~$ rg -n "AKIA|BEGIN PRIVATE KEY|password\s*=" -S docs/ | head docs/runbooks/app/deploy.md:203: password = "changeme"Significado de la salida: hay patrones sospechosos; a veces son ejemplos, a veces secretos reales.
Decisión: aplica reglas de redacción; en entornos reales, usa placeholders y un flujo de gestión de secretos, no credenciales inline.
Tres micro-historias corporativas desde el frente
Mini-historia 1: El incidente causado por una suposición equivocada
La compañía tenía un “Manual de Ingeniería” interno que todos usaban. Parecía moderno: tipografía limpia, bloques de código vistosos y un botón de copiar. Un equipo publicó una guía de migración para rotar credenciales de base de datos, con una docena de comandos shell.
Alguien asumió que los prompts eran inofensivos. El autor escribió ejemplos como dbadmin@bastion:~$ psql ... y el renderizador almacenó exactamente eso en el nodo de texto del bloque de código. El botón de copiar copió lo que vio.
Funcionó para quienes pegaron en una shell interactiva y quitaron el prompt manualmente. Falló para la automatización. Algunos ingenieros, haciendo la rotación bajo presión, pegaron todo en un runner no interactivo que trata tokens desconocidos como comandos. El primer token fue dbadmin@bastion:~$. El runner falló rápido, pero el flujo no. Interpretó el fallo como “intenta el siguiente paso”.
El resultado no fue catastrófico, pero sí ruidoso: cambios parciales, logs confusos y un usuario de DB bloqueado antes de tiempo. El análisis post-incidente fue embarazoso porque la causa raíz no fue PostgreSQL ni IAM. Fue un widget de UI de docs que copió los bytes equivocados.
Arreglarlo fue sencillo: los prompts pasaron a spans solo visuales, la copia usó una carga sin prompts y el build de docs empezó a lintear patrones de prompt en bloques “copiables”. La parte interesante fue cultural: después de eso, el equipo de docs empezó a ser invitado a revisiones de incidentes. Se lo ganaron.
Mini-historia 2: La optimización que salió mal
Otra organización decidió que su sitio de docs debía soportar “cambio de tema en vivo” entre claro y oscuro sin recargar. Cambiaron del resaltado en build-time al resaltado del lado cliente para que el navegador pudiera recolorear tokens dinámicamente.
En teoría, sonaba bien: enviar código crudo, ejecutar Prism en el navegador, aplicar temas CSS. En la práctica, el sitio de docs tenía runbooks largos con muchos bloques de código, algunos grandes (manifiestos de Kubernetes, cronologías de incidentes, extractos de logs). Cada carga de página hacía trabajo de tokenización en el hilo principal.
Notaron el impacto en rendimiento e intentaron optimizar. La “optimización” fue envolver cada línea en un span para que el resaltado de línea y los números de línea se pudieran hacer con CSS simple. Eso aumentó masivamente los nodos del DOM. El navegador pasó más tiempo en recalculado de estilos y layout que en el propio resaltado.
Luego vino lo peor: en laptops de gama baja y algunas configuraciones VDI, el desplazamiento se volvió entrecortado. La gente empezó a copiar menos porque la UI se sentía poco fiable. El proyecto consiguió el cambio de tema y perdió confianza—mala compensación.
El rollback fue pragmático. Mantuvieron el cambio de tema para el chrome de la página, pero los bloques de código volvieron a estar resaltados en build-time con dos temas precomputados. El switch de tema intercambiaba una clase y variables CSS; los bloques usaban spans tokenizados pre-renderizados. No fue “puro”. Fue rápido y estable. Eso fue lo que importó.
Mini-historia 3: La práctica aburrida y correcta que salvó el día
Una empresa de servicios financieros mantenía runbooks internos estrictos. Los docs eran estáticos, construidos en CI y publicados tras autenticación. Nada llamativo. Lo que sí tenían era una tubería de lint brutal.
Cada pull request corría una comprobación de docs que validaba fences de código: los lenguajes debían ser reconocidos, los rangos de líneas resaltadas válidos y caracteres prohibidos (zero-width spaces, espacios no separables en comandos) bloqueados. También verificaba que los bloques terminales usaran un formato de prompt consistente y proporcionaran una carga de copia sin prompt.
Una semana, un proveedor les envió un “script de arreglo” incrustado en un PDF. Un ingeniero lo copió en un runbook. El script contenía un espacio no separable entre una bandera y su argumento—visualmente indistinguible en el editor que usaban. El linter lo atrapó inmediatamente, falló el build e imprimió el punto de código Unicode.
El ingeniero refunfuñó, reemplazó el carácter y siguió. Dos días después, el script se ejecutó durante un incidente en vivo. Funcionó. Nadie volvió a pensar en el linter, que es el mayor elogio para la corrección aburrida.
Esa tubería no fue glamorosa. No ganó premios de diseño. Previno una clase de fallos que solo aparecen bajo estrés. En operaciones, eso es una victoria.
Errores comunes: síntomas → causa raíz → solución
Esta es la sección que reconocerás en retrospectiva. Ahórrate la retrospección.
1) “Copiar” incluye números de línea
- Síntomas: el código pegado empieza con
1,2o tiene números al inicio de cada línea; los comandos fallan. - Causa raíz: los números de línea se insertan como nodos de texto/spans reales dentro de la región seleccionable; la lógica de copia raspa
innerText. - Solución: copiar desde la cadena cruda del modelo; renderizar números de línea vía contadores CSS o gutter separado excluido de selección.
2) El botón de copiar no hace nada en producción pero funciona localmente
- Síntomas: no se muestra error; los usuarios reportan “botón de copiar muerto”.
- Causa raíz: CSP bloquea scripts inline o handlers; la Clipboard API requiere contexto seguro; los permisos difieren.
- Solución: mueve la lógica a JS externo; asegúrate de HTTPS; añade telemetría para fallos de copia y expón una alternativa “seleccionar código”.
3) Las líneas resaltadas están desfasadas por uno
- Síntomas: el autor resalta la línea 5, pero la UI resalta la 4 o la 6.
- Causa raíz: desajuste en cómo se cuentan las líneas (newline inicial, trimming, CRLF vs LF) o el parser cuenta desde 0 mientras la UI cuenta desde 1.
- Solución: normaliza finales de línea en ingestión; define numeración de líneas como 1-based; añade tests para casos límite (newline inicial, newline final).
4) Lag al desplazar y escribir en páginas con bloques grandes
- Síntomas: jank, scroll lento, CPU alta, ventiladores en marcha.
- Causa raíz: resaltado cliente y/o wrappers por línea creando miles de nodos; selectores CSS pesados.
- Solución: haz el resaltado en build/SSR; limita el DOM por línea; simplifica CSS; usa virtualización solo si realmente debes renderizar bloques enormes.
5) Usuarios copian comandos pero obtienen comillas tipográficas o guiones rotos
- Síntomas: flags parecen correctos pero la shell da errores; el texto pegado contiene puntuación rara.
- Causa raíz: transformaciones tipográficas o editores WYSIWYG introdujeron reemplazos (en-dash vs guion, comillas curvas).
- Solución: asegúrate de que los bloques de código sean texto plano; controla los editores; lint para Unicode sospechoso en fences de código.
6) Buscar-en-página no coincide con el código
- Síntomas: la búsqueda del navegador no encuentra una cadena visible en el bloque de código.
- Causa raíz: la tokenización inserta spans que dividen el texto; algunas implementaciones de búsqueda fallan, o el contenido se renderiza vía canvas/DOM virtual raro.
- Solución: mantiene el código como nodos de texto reales en el DOM; no renderices código vía canvas; evita reestructurar agresivamente el DOM.
7) Los números de línea rompen el wrapping y overflow
- Síntomas: el código se superpone al gutter; el scroll horizontal está roto; los números se desalinean.
- Causa raíz: no se reserva ancho para el gutter; métricas de fuente difieren entre gutter y código;
line-heightinconsistente. - Solución: usa un layout de dos columnas con ancho fijo de gutter; aplica la misma fuente y line-height; prueba en múltiples plataformas.
Listas de verificación / plan paso a paso
Paso a paso: construir un componente de bloque de código estilo GitHub que no te traicione
- Elige una estrategia de renderizado. Prefiere resaltado en build-time o SSR para docs; evita tokenización cliente salvo que el contenido sea verdaderamente dinámico.
- Define el modelo del bloque de código. language, title, code crudo, rangos de highlight, showLineNumbers, kind, payload de copia.
- Normaliza la entrada. Convierte CRLF a LF, conserva tabs, preserva espacios finales cuando importen y rechaza caracteres de formato en CI.
- Implementa la ruta de visualización. Renderiza barra de título + código; mantiene el texto del código en una estructura DOM estable.
- Implementa números de línea de forma segura. Contadores CSS o gutter separado; nunca inyectes números en el texto del código.
- Implementa líneas resaltadas de forma determinista. Parsea rangos “1,3-5”; valida; resalta solo líneas lógicas.
- Implementa copia usando la carga almacenada. No rasques el DOM; maneja fallos del portapapeles con un fallback (seleccionar + copia manual).
- Añade accesibilidad. Foco por teclado, labels aria, región aria-live para feedback, contraste suficiente para resaltados.
- Define límites de rendimiento. Topes en DOM por línea; modo “ver raw” para bloques enormes; limita paquetes de lenguaje.
- Añade telemetría. Éxito/fallo de copia, errores de parseo de highlight, marcas de rendimiento en páginas pesadas.
- Escribe tests. Tests snapshot para estructura HTML; unit tests para parseo de rangos; e2e tests para payload de copia.
- Documenta reglas de autoría. Cómo especificar títulos, prompts y highlights; qué se copia; qué no.
Lista previa al merge para autores de docs (la capa humana)
- ¿Marcaste los prompts del terminal como solo visuales?
- ¿Hay “puntuación inteligente” dentro de los fences de código?
- ¿Las líneas resaltadas están dentro de la longitud del bloque?
- ¿Estás enviando secretos, tokens o hostnames reales que deberían ser placeholders?
- ¿El bloque de código es demasiado grande para una página? ¿Debería ser una descarga de archivo?
- ¿Probaste el botón de copiar y pegaste en un editor de texto plano?
Checklist de Ops: cuando despliegas cambios en el renderizador de bloques de código
- ¿Puedes revertir el renderizador independientemente del contenido?
- ¿Las caches se indexan por versión del highlighter y tema?
- ¿Tienes una página canaria con bloques peores para probar rendimiento?
- ¿La CSP está aplicada en staging exactamente como en producción?
- ¿Alertas en errores JS que afecten interacciones de copia?
Preguntas frecuentes
1) ¿Debería siempre añadir números de línea?
No. Añádelos cuando el snippet se referencia por línea en el texto circundante, o cuando sea lo bastante largo para beneficiarse. Para comandos de 5 líneas, los números de línea son ruido.
2) ¿Cómo evito que los números de línea se copien?
No los renderices como parte del texto del código. Usa contadores CSS o un gutter separado. Y copia desde una cadena cruda almacenada, no desde innerText.
3) ¿Deben incluirse los prompts en los fences de código?
Visualmente, sí—los prompts comunican “esto es un comando”. En la carga de copia, normalmente no. Si debes soportar ambos, ofrece dos modos de copia.
4) ¿Por qué no usar Prism del lado cliente en todas partes?
Porque traslada costes de CPU y JS a cada lector, en cada visita. Para docs, ese impuesto a largo plazo no es necesario si puedes pre-renderizar.
5) ¿Cuál es la forma más limpia de soportar una barra de título en Markdown?
Usa una sintaxis de metadatos convencional que tu parser pueda leer (como una extensión del info string) y mapea eso a tu modelo de bloque de código. No parsees barras de título desde comentarios dentro del código.
6) ¿Cómo interactúan las líneas resaltadas con líneas envueltas?
No deberían. Resalta solo líneas lógicas. El wrapping es un detalle de presentación y varía por viewport, fuente y ajustes del usuario.
7) ¿Cómo manejar bloques enormes (logs, archivos generados)?
No los renderices como un DOM tokenizado por línea completo. Ofrece una vista previa truncada y un “ver raw” para descargar. Mantén la página rápida.
8) ¿Qué hay de la accesibilidad—los bloques de código necesitan ARIA?
El bloque de código debe permanecer como HTML estándar (<pre><code>). El botón de copiar necesita etiquetado adecuado, foco por teclado y feedback no intrusivo vía una región aria-live.
9) ¿Por qué mis líneas resaltadas se desplazan entre entornos?
Usualmente normalización de finales de línea (CRLF vs LF) o diferencias de trimming. Normaliza en ingestión y prueba con fixtures que incluyan finales de línea de Windows.
10) ¿Puedo instrumentar eventos de copia de forma segura?
Sí—logea solo metadatos. Nunca registres el contenido copiado. Asume que los snippets pueden contener secretos incluso cuando “no deberían”.
Siguientes pasos que realmente se envían
Si quieres bloques de código al estilo GitHub sin convertir tu plataforma de docs en un proyecto de ciencia, haz esto en orden:
- Define el contrato del componente (campos del modelo, reglas de payload de copia, reglas de rangos de highlight).
- Mueve el resaltado fuera del cliente a menos que tengas contenido verdaderamente dinámico.
- Implementa copia desde la fuente, no desde el texto renderizado del DOM.
- Establece límites de rendimiento (líneas máximas para render por línea, idiomas máximos enviados).
- Lint del contenido de docs para peligros Unicode, rangos inválidos de highlight y uso de prompts.
- Instrumenta fallos de copia y rendimiento de página en tus runbooks más críticos.
Luego envíalo. Observa las métricas. Si no ves menos pings de “la docs rompió mi comando”, la ruta de copia todavía le está mintiendo a alguien.