Bloques de código estilo GitHub: barras de título, botones de copiar, números de línea y líneas resaltadas

¿Te fue útil?

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.

Botón de copiar: corrección antes que ingenio

Un botón de copiar que de vez en cuando copia lo incorrecto es peor que no tenerlo. Crea confianza y luego la traiciona.

Qué copiar (y qué no)

Reglas que funcionan en entornos reales:

  • Nunca copies números de línea. Son chrome de UI. Manténlos fuera del nodo de texto que se copia.
  • Nunca copies prompts por defecto. Los prompts son útiles visualmente (“esto es un comando”), pero son veneno cuando se pegan en una shell no interactiva.
  • Normaliza finales de línea a \n al copiar. El contenido del portapapeles debe ser consistente entre OS; la terminal se adaptará.
  • Recorta exactamente un salto de línea final (opcional). GitHub suele copiar sin agregar espacios raros; iguala esa expectativa.
  • No reescribas tabs a espacios. La gente copia Makefiles, YAML y Python. No seas “servicial”.

Chiste #1: Los botones de copiar son como las copias de seguridad—todo el mundo asume que funcionan hasta el día en que no lo hacen.

Clipboard API y alternativas

Los navegadores modernos soportan navigator.clipboard.writeText(), pero necesitas planear para:

  • Restricciones de permisos (algunos contextos limitan el acceso al portapapeles).
  • HTTP vs HTTPS (la API de portapapeles normalmente quiere contextos seguros).
  • Content Security Policy (scripts inline y manejadores de eventos pueden estar bloqueados).

Guía de implementación:

  • Prefiere un elemento button con type="button".
  • Establece aria-label="Copy code".
  • Usa una región aria-live para el feedback “Copiado”, no popups de alerta.
  • Copiar desde una cadena almacenada (el copyTextOverride del modelo o el código crudo), no desde innerText del DOM mostrado, que puede incluir números de línea y spans ocultos.

Prompts: muéstralos, no los copies

A la gente le encantan los snippets con estilo de prompt porque comunican contexto rápidamente. Pero los prompts también rompen el copiar/pegar. El compromiso sensato es:

  • Renderiza los prompts visualmente (p. ej., con un span separado).
  • Almacena una carga útil de copia sin prompts.
  • Ofrece un toggle opcional “copiar con prompts” para docs de formación, si es necesario.

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:

  1. 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.
  2. 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
141

Significado 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
6220

Significado 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: 62

Significado 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 status

Significado 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-copy y 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, 2 o 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-height inconsistente.
  • 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

  1. 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.
  2. Define el modelo del bloque de código. language, title, code crudo, rangos de highlight, showLineNumbers, kind, payload de copia.
  3. Normaliza la entrada. Convierte CRLF a LF, conserva tabs, preserva espacios finales cuando importen y rechaza caracteres de formato en CI.
  4. Implementa la ruta de visualización. Renderiza barra de título + código; mantiene el texto del código en una estructura DOM estable.
  5. Implementa números de línea de forma segura. Contadores CSS o gutter separado; nunca inyectes números en el texto del código.
  6. Implementa líneas resaltadas de forma determinista. Parsea rangos “1,3-5”; valida; resalta solo líneas lógicas.
  7. Implementa copia usando la carga almacenada. No rasques el DOM; maneja fallos del portapapeles con un fallback (seleccionar + copia manual).
  8. Añade accesibilidad. Foco por teclado, labels aria, región aria-live para feedback, contraste suficiente para resaltados.
  9. Define límites de rendimiento. Topes en DOM por línea; modo “ver raw” para bloques enormes; limita paquetes de lenguaje.
  10. Añade telemetría. Éxito/fallo de copia, errores de parseo de highlight, marcas de rendimiento en páginas pesadas.
  11. Escribe tests. Tests snapshot para estructura HTML; unit tests para parseo de rangos; e2e tests para payload de copia.
  12. 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:

  1. Define el contrato del componente (campos del modelo, reglas de payload de copia, reglas de rangos de highlight).
  2. Mueve el resaltado fuera del cliente a menos que tengas contenido verdaderamente dinámico.
  3. Implementa copia desde la fuente, no desde el texto renderizado del DOM.
  4. Establece límites de rendimiento (líneas máximas para render por línea, idiomas máximos enviados).
  5. Lint del contenido de docs para peligros Unicode, rangos inválidos de highlight y uso de prompts.
  6. 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.

← Anterior
Falsos positivos en Rspamd: afina la puntuación antispam sin dejar pasar basura
Siguiente →
Proxmox «No se puede eliminar el nodo»: Eliminación segura de un nodo del clúster

Deja un comentario