Кодові блоки в стилі GitHub: панелі заголовка, кнопки копіювання, нумерація та виділені рядки

Було корисно?

Ваші документи виглядають добре, поки хтось не вставить команду й у неї непомітно потраплять номера рядків, промпт або зайвий пробіл. Тоді приходить тікет: «ваші інструкції зламали продакшен».

Кодові блоки GitHub здаються простими: панель з іменем файлу, кнопка копіювання, яка копіює правильно, нумерація, що не псує буфер, і виділені рядки, що вказують на потрібне місце. Відтворити це на своєму сайті можна — якщо припинити думати «тільки CSS» і почати думати про це як про компонент із вимогами надійності.

Як «добре» має виглядати в продакшен‑доках

Кодові блоки в стилі GitHub — це не про естетику. Це про зниження операційного ризику. Коли хтось виконує рунакбук о 03:00, кодовий блок — це інтерфейс. Якщо він бреше, заплутує або копіює невірні байти, ваш «сайт документації» стає фактором простою.

Ось критерії, якими я користуюся:

  • Копіювання точно. Копіюється лише код. Ніяких промптів, номерів рядків чи невидимого Unicode‑конфетті.
  • Номери рядків — лише презентаційні. Вони допомагають посилатися на «рядок 17», не забруднюючи вміст буфера.
  • Виділені рядки — керовані даними. Автор може вказати, які рядки важливі (контекст diff, «змініть це», «не виконувати»).
  • Панель заголовка несе корисні метадані. Ім’я файлу, мова, можливо «shell», «k8s» або «output». Не порожня прикраса.
  • Доступність за замовчуванням. Кнопка копіювання працює з клавіатури, озвучує статус і не краде фокус, як дитина з новим барабаном.
  • Швидко. Відтворення 30 блоків коду не повинно перетворювати сторінку в обігрівач.
  • Працює офлайн і під CSP. Продакшен‑системи часто мають суворі політики. Ваша документація має виживати в них.

Парафраз думки (John Ousterhout): складність робить системи важкими для змін і розуміння; якщо можна її прибрати — зробіть це.

І так, я ставитимуся до «віджета кодового блоку» як до міні‑продуктової системи. Бо це саме так. Це також розподілена система: автор, рендерер, браузер, API буфера обміну та терпіння користувача — жодним з цих елементів ви не керуєте повністю.

Факти та коротка історія (чому переміг GitHub)

Трохи контексту допоможе робити кращі рішення. Це невеликі факти, але вони пояснюють, чому екосистема вийшла такою.

  1. Ранні приклади коду в мережі були простими <pre>. «Підсвічування синтаксису» починалося як серверні regex‑хакі в 1990‑х, задовго до того, як браузери мали хороші шрифти чи рушії виведення.
  2. Pygments (середина 2000‑х) зробив підсвічування мейнстрімом. Воно популяризувало модель «токенізувати і стилізувати span‑и», яку використовують більшість підсвічувачів досі.
  3. GitHub популяризував fenced code blocks у Markdown. Конвенція з потрійними бектіками стала типовою ментальною моделлю для коду в документації.
  4. API буфера обміну еволюціонували пізно. Довгі роки «копіювання» означало виділення тексту й надію, що DOM не містить сміття; сучасний Clipboard API зробив надійні кнопки можливими.
  5. Нумерація рядків завжди була спірною. IDE її потребують; у документації часто ні. Дебати існують, бо нумерація корисна, але легко реалізується неправильно.
  6. Клієнтське підсвічування виникло як реакція на статичний хостинг. Коли всі почали деплоїти документацію на CDN, відправляти JS‑підсвічувачі здавалося простіше, ніж серверний рендер — допоки не з’явився «рахунок» за продуктивність.
  7. Власні UI‑патерни GitHub стали де‑факто стандартом. Панель заголовка + кнопка копіювання знайомі користувачу, тож вони довіряють їм і користуються інтуїтивно.
  8. Підсвічувачі змагаються за «семантичну» коректність. Tree‑sitter і подібні парсери підняли планку; regex‑токенізатори швидші у створенні, але менш точні для складних мов.

Два висновки: по‑перше, більшість «фіч кодових блоків» прироблені до старих примітивів. По‑друге, те, що виглядає як простий UI, зазвичай — три системи, склеєні докупи: рендеринг, взаємодія та авторинг.

Архітектура: один компонент, три шляхи даних

Кодовий блок у стилі GitHub має три шляхи, які повинні збігатися:

1) Шлях відображення: те, що бачить користувач (підсвічування, номери рядків, панель заголовка).

2) Шлях буфера обміну: що копіюється (повинно бути сирим кодом, нормалізованим адекватно).

3) Референсний шлях: до чого автори й читачі посилаються (номери рядків, виділені рядки, якірні посилання).

Якщо ці шляхи розходяться, виникають режими відмов, які виглядають як помилки користувача, але ними не є:

  • Кнопка копіювання копіює промпти або номери → вставлена команда не працює → користувач втрачає довіру до документації.
  • Виділені рядки не відповідають коду через перенос рядків або приховані span‑и → користувач змінює не те.
  • Номери рядків зміщуються між SSR і гідратацією → люди вказують «рядок 14», маючи на увазі різний вміст.

Виберіть стратегію рендерингу: SSR, під час збірки або на клієнті

Є три реалістичні опції:

Стратегія Переваги Недоліки Коли обирати
Підсвічування під час збірки (наприклад, Shiki) Швидкі сторінки, немає клієнтського JS для підсвічування, консистентний вивід Збірки повільніші; зміни теми вимагають повторної збірки Сайти документації, блоги, рунакбуки, усе статичне-ish
Server‑side rendering Консистентно, можна робити темінгово залежний рендер, без важкого клієнтського JS Потрібна інфраструктура; важливе кешування Доки інтегрована документація продукту, автентифіковані дори
Клієнтське підсвічування (Prism/Highlight.js) Проста інтеграція; динамічний контент Вага JS, стрибки по CPU, проблеми гідратації Інтерактивні редактори, контент від користувачів, крайній випадок

Я маю власну думку: для більшості документації і операційних рунакбуків робіть підсвічування під час збірки або на сервері. Клієнтське підсвічування — податок, який ви платитимете постійно.

Визначте явну модель кодового блоку

Припиніть дозволяти Markdown‑парсеру «вирішувати», що таке ваш блок коду. Задайте модель. Мінімум:

  • language (bash, yaml, json, …)
  • title (ім’я файлу або мітка, наприклад «nginx.conf»)
  • code (сирий вміст, нормалізовані кінці рядків)
  • highlight (діапазони рядків: 3,5-8)
  • showLineNumbers (булеве)
  • copyTextOverride (опціонально; наприклад, видалити промпти)
  • kind (source, terminal, output, diff)

Коли у вас є така модель, рендерер може бути детерміністичним, тестованим і нудним. Нудність — це добре. Нудність відправляє в продакшен.

Кнопка копіювання: коректність важливіша за хитрощі

Кнопка копіювання, яка іноді копіює невірно, гірша за відсутність кнопки. Вона створює довіру, а потім зраджує її.

Що копіювати (і чого не варто)

Правила, які працюють у реальних середовищах:

  • Ніколи не копіюйте номери рядків. Це UI‑хром. Тримайте їх поза текстовим вузлом для копіювання.
  • За замовчуванням не копіюйте промпти. Промпти корисні візуально («це команда»), але отруйні при вставленні в неінтерактивну оболонку.
  • Нормалізуйте кінці рядків до \n при копіюванні. Вміст буфера має бути послідовним між ОС; термінал із цим впорається.
  • Обрізайте рівно один кінцевий перенос рядка (опційно). GitHub зазвичай не додає дивних пробілів; дотримуйтесь подібного очікування.
  • Не перетворюйте табуляцію на пробіли. Люди копіюють Makefile‑и, YAML і Python. Не будьте «допоміжними».

Жарт №1: Кнопки копіювання як бекапи — всі вірять, що вони працюють, поки одного дня не перестануть.

Clipboard API і резервні варіанти

Сучасні браузери підтримують navigator.clipboard.writeText(), але потрібно планувати для:

  • Обмежень дозволів (деякі контексти обмежують доступ до буфера обміну).
  • HTTP vs HTTPS (Clipboard API зазвичай вимагає безпечного контексту).
  • Content Security Policy (inline‑скрипти та обробники подій можуть бути заблоковані).

Рекомендації з реалізації:

  • Віддавайте перевагу елементу button з type="button".
  • Встановіть aria-label="Copy code".
  • Використовуйте регіон aria-live для повідомлення «Скопійовано», а не спливаючі попапи.
  • Копіюйте збережений рядок (copyTextOverride або сирий код із моделі), а не з innerText відображеного DOM, який може містити номери рядків і приховані span‑и.

Промпти: відображати, але не копіювати

Людям подобається стиль із промптами, бо він передає контекст. Але промпти ламають вставляння. Розумний компроміс:

  • Рендерьте промпти візуально (наприклад, окремим span).
  • Зберігайте payload для копіювання без промптів.
  • За потреби запропонуйте опціональний перемикач «копіювати з промптами» для навчальних матеріалів.

Нумерація рядків: цукерка UX з гострими кутами

Номери рядків покращують співпрацю: «змінити рядок 42» — це чітка інструкція. Але вони мають підводні камені.

Як нумерація рядків ламається

  • Вони копіюються. Якщо реалізувати їх вставкою реальних текстових вузлів на початку рядків, вони потраплять у виділення й буфер.
  • Вони дрейфують. Якщо переноси змінюють сприйняття «рядка», люди посилатимуться не на те, що треба.
  • Вони ламають пошук. Деякі реалізації змінюють DOM так, що пошук сторінки перестає коректно знаходити очікувані фрагменти коду.
  • Вони сповільнюють рендеринг. Розбивка на тисячі елементів рядків може спричинити DOM‑вибух.

Патерни реалізації, що витримують навантаження

Два патерни працюють стабільно:

  1. CSS‑лічильники для номерів рядків, без вставки чисел у текст. Це швидко, і виділення можна залишити чистим.
  2. Окрема колонка‑гуттер з номерами як власними елементами, тоді текст коду лишається окремим вибірним блоком.

Якщо ви виділяєте рядки, загортаючи кожен рядок в окремий елемент, ви вже розбиваєте рядки. Це нормально для невеликих блоків, але потрібен поріг. Понад певний розмір перемикайтеся на «без DOM на рядок».

Операційне правило: якщо блок коду перевищує кілька тисяч рядків, не рендерьте per‑line spans у браузері. Рендерьте plain <pre> або запропонуйте завантаження файлу.

Виділені рядки: найшвидший спосіб зменшити помилки

Виділення рядків — це не прикраса. Це запобіжний засіб. Якщо використовувати правильно, воно знижує когнітивне навантаження «яку частину змінити?»

Добрі застосування

  • Вказати зміни у конфігураційних файлах: показати весь файл, виділити лише рядки, які відрізняються.
  • Підкреслити небезпечні команди: виділити руйнівний рядок у багатокроковому снипеті.
  • Навчання у стилі diff: виділити рядки, що відповідають зміні з code review.

Погані практики

  • Виділяти половину блоку. Це не акцент — це істерика маркера.
  • Використовувати колір виділення з низькою контрастністю в темній темі. Люди його пропустять.
  • Виділяти на основі «згорнутих візуальних рядків». Це кошмар. Використовуйте лише логічні рядки.

Формат авторингу: тримайте просто

Не вигадуйте нову міні‑мову для діапазонів рядків. Використовуйте встановлений формат «1,3-5,8». Розбирайте його детерміністично і видавайте помилки голосно.

Якщо автор вимагає виділити рядки за межами довжини блоку, у вас є два розумні варіанти:

  • Завалити збірку (мій пріоритет для рунакбуків), або
  • Попередити і ігнорувати (прийнятно для блогів).

Панелі заголовка: імена файлів, мітки мови та метадані

Панель заголовка корисна, коли вона дає орієнтир. «Ось /etc/nginx/nginx.conf» — це дієво. «Код» — ні.

Що додати

  • Ім’я файлу або мітка (наприклад, values.yaml, docker-compose.yml).
  • Мова (невелика мітка: bash, yaml, json).
  • Кнопка копіювання з очевидною affordance.
  • Опціонально «переглянути сире» для дуже великих блоків (подати як файл, а не 10к рядковий DOM).

Не перевантажуйте її

Панелі заголовка — не дашборди. Якщо втиснути туди хеші комітів, таймстемпи й імена середовищ, ви отримаєте акордеон відволікаючих деталей. Тримайте мінімум, послідовність і стабільність по сайту.

Продуктивність та операційні аспекти (так, серйозно)

Кодові блоки стають проблемою продуктивності у трьох передбачуваних сценаріях:

  • Багато блоків на одній сторінці (рунакбуки зазвичай густі).
  • Великі блоки (згенеровані конфіги, логи, маніфести Kubernetes).
  • Клієнтське підсвічування (стрибки CPU, довгі таски, підвисання).

Що бюджетувати

Думайте у вигляді бюджетів, як для API:

  • CPU: уникайте токенізації великого контенту на клієнті.
  • DOM‑вузли: уникайте пер‑рядкових врапперів понад поріг.
  • JS‑біти: не відправляйте 40 мов, якщо потрібно 6.
  • Шрифти: запасний монофонт підходить; не блокуйте рендер на кастомних шрифтах.

Кешування важливе (навіть для підсвічування)

Якщо ви робите серверне або під час збірки підсвічування, кешуйте результат за стабільним ключем: hash(code + language + theme + highlighter-version). Інакше ви будете знову підсвічувати ті самі снипети кожної збірки або запиту, і ваш CI почне відчувати себе як майнінг криптовалюти.

Безпека: тримайте код як недовірений текст

Якщо ваша система рендерить код із контенту, створеного користувачами, вважайте його ворожим. Підсвічувачі часто інжектять HTML‑span‑и; якщо ви не санітуєте правильно, можна створити XSS через «код».

Найбезпечніший шлях — рендерити токени в HTML із централізованим екрануванням і ніколи не дозволяти сирому HTML‑проходженню всередині кодових блоків.

Інструментування та моніторинг для кодових блоків

Якщо ви запустите кнопку копіювання і ніколи її не поміряєте, ви дізнаєтеся про збої через розлючених людей. Інструментуйте її.

Що вимірювати

  • Коефіцієнт успішного копіювання (promise resolved vs rejected).
  • Time to interactive на сторінках з багатьма кодовими блоками.
  • Long tasks після завантаження сторінки (клієнтське підсвічування часто провокує такі).
  • Кількість DOM‑вузлів на великих сторінках (проксі для «ми обернули кожен рядок»).
  • Тривалість підсвічування під час збірки (якщо вона стрибнула — ви щось поміняли).

Логування без проникнення в приватність

Не логируйте вміст коду при подіях копіювання. Ви опинитеся з секретами, токенами й API‑ключами в аналітиці. Логируйте лише метадані: id сторінки, мова, довжина блоку, чи були промпти, успіх/помилка.

Жарт №2: Єдине чутливіше за production‑секрети — реакція юридичного відділу, коли ви їх залогували.

Плейбук швидкої діагностики

Коли користувачі скаржаться, що «кодові блоки повільні» або «копіювання не працює», не сперечайтеся про естетику. Тріажуйте як інцидент.

Спочатку: підтвердьте режим відмови за 60 секунд

  • Коректність копіювання: натисніть копіювати, вставте в простий текстовий редактор, перевірте на наявність номерів рядків/промптів/дивних пробілів.
  • Консоль браузера: шукайте помилки дозволів буфера обміну або порушення CSP.
  • Продуктивність сторінки: відкрийте devtools performance, перезавантажте, шукайте довгі таски навколо підсвічування/гідратації.

По‑друге: знайдіть вузьке місце

  • CPU bound: багато мс у виконанні JS → ймовірно клієнтське підсвічування, per‑line DOM або дорогі селектори.
  • DOM bound: домінує layout/recalc style → занадто багато вузлів, важкий CSS, логіка обгортання рядків.
  • Network bound: великі JS‑бандли або файли шрифтів → непотрібні пакети підсвічувача чи мов.

По‑третє: застосуйте хірургічний фікс, а не перепис

  • Перемістіть підсвічування в build/SSR.
  • Зменшіть набір мов, які відправляєте.
  • Перестаньте обгортати рядки понад поріг.
  • Копіюйте зі source string, а не з DOM.
  • Додайте візуальні промпти через CSS pseudo‑elements або окремі span‑и, виключені з payload для копіювання.

Геурістика: якщо на повільній сторінці є 10+ кодових блоків і стрибок CPU корелює з функціями «highlight», фікс — архітектурний, а не дрібні оптимізації.

Практичні завдання з командами та рішеннями

Це перевірки, які ви виконуєте, коли кодові блоки поводяться неправильно. Кожне завдання включає команду, що означає вивід і яке рішення прийняти. Команди розраховані на Linux‑хост, де працює сайт документації або збірка.

Завдання 1: Перевірте версії Node і менеджера пакетів (відтворюваність)

cr0x@server:~$ node --version
v20.11.1

Значення виводу: у вас Node 20; полифіли буфера обміну та інструменти збірки поводяться по‑різному між мажорними версіями.

Рішення: зафіксуйте Node у CI (і локально через тулзи), якщо бачите неконсистентний вивід підсвічування між середовищами.

Завдання 2: Виміряйте витрати підсвічування під час збірки (чи це вузьке місце?)

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

Значення виводу: багато CPU, ~900MB RSS. Підсвічувачі на кшталт Shiki можуть вимагати багато пам’яті при великій кількості сторінок/мов.

Рішення: кешуйте підсвічений результат і обмежуйте підтримувані мови; якщо RSS загрожує контейнерам CI — дробіть збірки або препроцесіть.

Завдання 3: Знайдіть найважчі сторінки за кількістю кодових блоків (гарячі точки ризику)

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

Значення виводу: ці файли мають найбільше fenced code blocks.

Рішення: навантажте тестами ці сторінки першими; оптимізуйте найгірші місця перед тим, як ганятися за маргінальними виграшами.

Завдання 4: Переконайтеся, що ваш HTML не копіює номери рядків (швидка перевірка)

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

Значення виводу: номери рядків — реальні текстові вузли/span‑и, які можуть потрапити у виділення/буфер.

Рішення: перейдіть на CSS‑лічильники або окрему колонку, виключену з виділення; або гарантуйте, що шлях копіювання використовує сирий код, а не DOM‑текст.

Завдання 5: Виявлення підозрілого Unicode у снипетах (коректність буфера)

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']

Значення виводу: символи форматування (zero‑width space, word joiner) присутні. Вони можуть зламати вставлені команди.

Рішення: додайте pre‑commit хук або CI‑лінт, що відхиляє такі символи в документації, або як мінімум помічає їх.

Завдання 6: Переконайтеся, що JS‑бандл не відправляє 40 мов (контроль ваги)

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

Значення виводу: бандл підсвічувача домінує у JS‑пейлоуді.

Рішення: переключіться на підсвічування під час збірки або tree‑shake мов; не погоджуйтеся на 1.8MB податок за гарні кольори.

Завдання 7: Перевірте CSP‑порушення, що впливають на буфер обміну

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;

Значення виводу: inline‑скрипти заблоковані; якщо кнопка копіювання покладається на inline‑обробники, вона мовчатиме.

Рішення: винесіть логіку копіювання в зовнішній JS, уникайте inline‑атрибутів подій або додайте політику з nonce, якщо потрібно.

Завдання 8: Перевірте, що вивід збірки має стабільні якорі для виділення рядків

cr0x@server:~$ rg -n "data-highlight|data-line" -S dist/runbooks/storage/zfs-replace-disk/index.html | head
615: 

Значення виводу: метадані виділення присутні в HTML; клієнт може стилізувати їх без повторної токенізації.

Рішення: зберігайте діапазони виділення як data‑атрибути; уникайте перерахунку мап рядків у браузері.

Завдання 9: Виявити надто великі блоки коду, які мають режим «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

Значення виводу: є 412‑рядковий снипет; не гігант, але кандидат на проблеми продуктивності, якщо ви обгортаєте кожен рядок.

Рішення: встановіть поріг (наприклад, 200–500 рядків), при якому per‑line DOM відключається або перемикається на легкий режим.

Завдання 10: Переконайтеся, що gzip/brotli увімкнено (мережеві вузькі місця)

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;

Значення виводу: gzip увімкнено; якщо ваш JS все ще важкий, стиснення допомагає, але не вирішує витрати CPU.

Рішення: тримайте стиснення, але зосередьтеся на зменшенні JS і DOM, а не на святкуванні менших трансферів.

Завдання 11: Перевірка на per‑line DOM‑вибухи (проксі кількості вузлів)

cr0x@server:~$ rg -n "class=\"line\"" -S dist/runbooks/kubernetes/etcd-restore/index.html | wc -l
6220

Значення виводу: тисячі per‑line елементів були згенеровані.

Рішення: перестаньте емінтувати per‑line враппери для великих блоків; використовуйте CSS‑лічильники або один токенізований блок.

Завдання 12: Перевірте Lighthouse CLI на регресії сторінки (підходить для автоматизації)

cr0x@server:~$ lighthouse dist/runbooks/kubernetes/etcd-restore/index.html --quiet --chrome-flags="--headless" --only-categories=performance
Performance: 62

Значення виводу: рейтинг продуктивності середній; кодові блоки часто винуватці через важкий JS або DOM.

Рішення: профілюйте сторінку; якщо довгі таски корелюють з підсвічуванням, перенесіть роботу в build‑time і зменшіть складність DOM.

Завдання 13: Переконайтеся, що промпти не потрапляють у payload для копіювання

cr0x@server:~$ rg -n "cr0x@server:~\\$" -S dist/ | head
dist/runbooks/storage/zfs-replace-disk/index.html:618: cr0x@server:~$ zpool status

Значення виводу: рядки з промптами з’являються в згенерованому HTML. Це нормально візуально, ризиковано, якщо логіка копіювання знімає текст із DOM.

Рішення: зберігайте окремий сирий рядок для копіювання або маркуйте span‑и промптів data-no-copy і обробляйте це в логіці копіювання.

Завдання 14: Перевірте відсутність секретів у кодових блоках (так, люди так роблять)

cr0x@server:~$ rg -n "AKIA|BEGIN PRIVATE KEY|password\s*=" -S docs/ | head
docs/runbooks/app/deploy.md:203: password = "changeme"

Значення виводу: є підозрілі патерни; інколи це приклади, інколи — реальні секрети.

Рішення: введіть правила редагування; в реальному середовищі використовуйте заповнювачі і workflow управління секретами, а не вбудовані облікові дані.

Три корпоративні міні‑історії з практики

Міні‑історія 1: Інцидент через хибне припущення

Компанія мала внутрішній «Engineering Handbook», яким усі користувалися. Він виглядав сучасно: чиста типографія, гарні кодові блоки й кнопка копіювання. Команда випустила гайди з міграції для ротації облікових даних бази, із близько десятком shell‑команд.

Хтось припустив, що промпти безпечні. Автор написав приклади типу dbadmin@bastion:~$ psql ... і рендерер зберіг саме це у текстовому вузлі блокa. Кнопка копіювання копіювала те, що бачила.

Для людей, які вставляли в оболонку й вручну видаляли промпт, це працювало. Для автоматизації — ні. Декілька інженерів, що робили ротацію в умовах тиску, вставили все у non‑interactive shell runner, що трактує невідомі токени як команди. Першим токеном був dbadmin@bastion:~$. Runner швидко впав, але робочий процес ні. Він інтерпретував помилку як «спробуй наступний крок».

Результат не був катастрофічним, але був гучним: часткові зміни, заплутані логи і один користувач БД заблокований раніше, ніж треба. Пост‑інцидентний аналіз був незручним, бо корінь проблеми не PostgreSQL чи IAM. Це був віджет документації, який копіював невірні байти.

Виправлення було простим: промпти стали відображуваними span‑ами, копіювання використовувало payload без промптів, а збірка документації почала лінтити шаблони промптів у «копіювальних» блоках. Цікаво, що після цього команда документації була запрошена на розбори інцидентів. Вони заслужили це.

Міні‑історія 2: Оптимізація, що повернулася бумерангом

Інша організація вирішила, що сайт документації має підтримувати «живу зміну теми» між світлою і темною без перезавантаження. Вони перейшли з підсвічування під час збірки на клієнтське, щоб браузер міг динамічно перекрашувати токени.

На папері все виглядало чисто: відправляй сирий код, запускай Prism у браузері, застосовуй CSS‑теми. На практиці сторінки містили довгі рунакбуки з багатьма кодовими блоками, деякі з яких великі (маніфести Kubernetes, журнали інцидентів). На кожне завантаження сторінки виконувалась токенізація на основному потоці.

Вони помітили падіння продуктивності і почали оптимізувати. «Оптимізація» полягала в тому, щоб загорнути кожен рядок у span для простішого стилізування виділення й номерів. Це різко збільшило кількість DOM‑вузлів. Браузер витрачав більше часу на перерахунок стилів і layout, ніж на саме підсвічування.

Потім настала реальна прикрість: на слабких ноутбуках і VDI прокручування стало ривкоподібним. Люди менше копіювали код, бо UI здавався ненадійним. Проект домігся перемикання теми, але втратив довіру — погана угода.

Відкат був прагматичним. Вони залишили переключення тем для оболонки сторінки, але кодові блоки знову стали підсвічуватися під час збірки з двома препроцесованими темами. Перемикання тем міняло клас і CSS‑змінні; кодові блоки використовували попередньо згенеровані token span‑и. Це було не «чисто», натомість швидко й стабільно. Це було головне.

Міні‑історія 3: Нудна правильна практика, що врятувала день

Фінансова компанія підтримувала суворі внутрішні рунакбуки. Документація була статичною, збиралася в CI і публікувалася за автентифікацією. Нічого пафосного. Але в них був жорсткий лінт‑пайплайн.

Кожний PR запускав перевірку документації, яка валідувала code fence: мови мали бути розпізнані, діапазони виділення — коректні, заборонені символи (zero‑width space, non‑breaking space у командах) блокувалися. Також перевірялось, що термінальні блоки мають консистентний формат промпта і надають payload для копіювання без промпта.

Якось постачальник прислав «скрипт‑фікс» у PDF. Інженер скопіював його у рунакбук. Скрипт містив non‑breaking space між флагом і аргументом — в редакторі це було візуально непомітно. Лінтер помітив це відразу, збірка впала і вивела кодову точку Unicode.

Інженер нарікав, замінив символ і продовжив. Через два дні скрипт виконали під час живого інциденту. Він спрацював. Ніхто більше не згадував про лінтер — це найвища похвала за нудну правильність.

Той пайплайн не був гламурним. Він не вигравав дизайнерських премій. Але він попередив клас помилок, що проявляються тільки під стресом. В опсах це — перемога.

Поширені помилки: симптом → причина → виправлення

Цей розділ ви впізнаєте з огляду. Уникайте ретроспектив.

1) «Копіювання» включає номери рядків

  • Симптоми: вставлений код починається з 1, 2 або має числа на початку кожного рядка; команди не працюють.
  • Причина: номери рядків вставлені як реальні текстові вузли/span‑и всередині вибірної області; логіка копіювання скребе innerText.
  • Виправлення: копіюйте з сирого рядка моделі; рендерьте номери через CSS‑лічильники або в окремому gutter, не вставляйте числа в текст коду.

2) Кнопка копіювання не працює в продакшені, але локально працює

  • Симптоми: без помилок; користувачі кажуть «кнопка копіювання мертва».
  • Причина: CSP блокує inline‑скрипти або обробники подій; Clipboard API вимагає безпечного контексту; дозволи відрізняються.
  • Виправлення: винесіть логіку у зовнішній JS; забезпечте HTTPS; додайте телеметрію помилок копіювання таfallback «виділити код».

3) Виділені рядки зміщені на один

  • Симптоми: автор виділяє рядок 5, але UI підсвічує 4 або 6.
  • Причина: невідповідність того, як рахуються рядки (ведучий перенос, обрізка, CRLF vs LF) або парсер рахує з 0, а UI з 1.
  • Виправлення: нормалізуйте кінці рядків при інґестуванні; визначте нумерацію як 1‑базовану; додайте тести для крайових випадків (ведучий/кінцевий перенос).

4) Гальмування прокрутки та введення на сторінках з великими блоками

  • Симптоми: підвисання, повільна прокрутка, високий CPU, ввімкнення вентиляторів.
  • Причина: клієнтське підсвічування і/або per‑line DOM‑враппери, що створюють тисячі вузлів; важкий CSS.
  • Виправлення: робіть підсвічування під час збірки/SSR; обмежте per‑line DOM; спростіть CSS; використовуйте віртуалізацію лише якщо дійсно потрібно.

5) Користувачі копіюють команди, але отримують «розумні» лапки або зламані дефіси

  • Симптоми: прапори виглядають правильно, але shell повертає помилки; вставлений текст містить дивні знаки пунктуації.
  • Причина: типографські перетворення або WYSIWYG‑редактори замінили дефіси/лапки на інші символи.
  • Виправлення: гарантуйте, що кодові блоки — plain text; зафіксуйте редактори; лінтьте підозрілий Unicode у code fences.

6) Пошук на сторінці не знаходить код

  • Симптоми: браузерний пошук не знаходить рядок, видимий у кодовому блоці.
  • Причина: токенізація вставляє span‑и, що розділяють текст; деякі реалізації пошуку не справляються, або контент рендериться через canvas/віртуальний DOM.
  • Виправлення: зберігайте код як реальні текстові вузли в DOM; не рендерьте код через canvas; уникайте агресивної реструктуризації DOM.

7) Номери рядків ламають обтікання і переповнення

  • Симптоми: код перекриває gutter; горизонтальна прокрутка не працює; числа зміщені.
  • Причина: ширина gutter не зарезервована; метрика шрифту відрізняється між gutter і кодом; невідповідний line-height.
  • Виправлення: використовуйте двоколонний макет з фіксованою шириною gutter; застосуйте однаковий шрифт і line‑height; тестуйте на різних платформах.

Чеклісти / покроковий план

Покроково: побудуйте компонент кодового блоку в стилі GitHub, який не зрадить

  1. Виберіть стратегію рендерингу. Віддавайте перевагу підсвічуванню під час збірки або SSR для документації; уникайте клієнтської токенізації, якщо контент не справді динамічний.
  2. Визначте модель кодового блоку. language, title, raw code, highlight ranges, showLineNumbers, kind, copy payload.
  3. Нормалізуйте вхідні дані. Перетворіть CRLF у LF, збережіть табуляцію, зберігайте кінцеві пробіли там, де це важливо, і відхиляйте форматні символи в CI.
  4. Реалізуйте шлях відображення. Рендерьте панель заголовка + код; тримайте текст коду у стабільній структурі DOM.
  5. Реалізуйте номери рядків безпечно. CSS‑лічильники або окрема колонка; ніколи не інжектіть числа в текст коду.
  6. Реалізуйте виділені рядки детерміністично. Розбирайте «1,3-5»; валідуйте; виділяйте лише логічні рядки.
  7. Реалізуйте копіювання з використанням збереженого payload. Не скребіть DOM; обробляйте відмови clipboard з fallback (select + manual copy).
  8. Додайте доступність. Фокус клавіатурою, aria‑мітки, live‑регія зворотного зв’язку, достатній контраст для виділень.
  9. Встановіть обмеження продуктивності. Жорсткі ліміти per‑line DOM; режим «view raw» для величезних блоків; обмежити набори мов.
  10. Додайте телеметрію. Успіх/помилка копіювання, помилки парсингу виділень, таймінги продуктивності на важких сторінках.
  11. Пишіть тести. Снапшот‑тести HTML‑структури; unit‑тести для парсингу діапазонів; e2e‑тести для payload копіювання.
  12. Документуйте правила авторингу. Як вказувати заголовки, промпти і виділення; що копіюється, а що ні.

Pre‑merge чекліст для авторів документації (людський шар)

  • Ви позначили промпти в терміналі як display‑only?
  • Чи є в кодових блоках «розумна пунктуація»?
  • Чи знаходяться виділені рядки в межах довжини блоку?
  • Чи не відправляєте ви секрети, токени або реальні імена хостів замість заповнювачів?
  • Чи не надто великий блок для сторінки? Можливо краще завантаження файлу?
  • Чи перевіряли ви кнопку копіювання, вставивши в простий текстовий редактор?

Ops‑чекліст: коли ви розгортаєте зміни в рендерері кодових блоків

  • Чи можна відкотити рендерер незалежно від контенту?
  • Чи кеші ключуються за версією підсвічувача і темою?
  • Чи є у вас канарна сторінка з найгіршими кодовими блоками для тестування продуктивності?
  • Чи CSP у staging суворо такий самий, як у production?
  • Чи ви алертуєте про JS‑помилки, що впливають на взаємодію копіювання?

Питання та відповіді

1) Чи завжди додавати номери рядків?

Ні. Додавайте їх, коли фрагмент згадується по рядках у тексті або коли він достатньо довгий, щоб це було корисно. Для команд із 5 рядків нумерація — зайвий шум.

2) Як запобігти копіюванню номерів рядків?

Не рендерте їх як частину тексту коду. Використовуйте CSS‑лічильники або окрему колонку. А також копіюйте з збереженого raw‑рядка, а не з innerText.

3) Чи варто включати промпти у code fences?

Візуально — так, промпти дають контекст. У payload для копіювання — зазвичай ні. Якщо потрібно підтримати обидва варіанти, запропонуйте два режими копіювання.

4) Чому не просто використовувати клієнтський Prism всюди?

Тому що це перекладає витрати CPU і JS на кожного читача при кожному перегляді сторінки. Для документації це довгостроковий податок, який можна уникнути, якщо підсвічувати заздалегідь.

5) Який найчистіший спосіб підтримати панель заголовка в Markdown?

Використовуйте звичний синтаксис метаданих, який ваш парсер може прочитати (наприклад, розширення info string) і мапуйте його в модель кодового блоку. Не парсьте панелі заголовка з коментарів всередині коду.

6) Як виділені рядки взаємодіють з переносами рядків?

Вони не повинні. Виділяйте лише логічні рядки. Переноси — деталь подання і залежать від viewport, шрифту та налаштувань користувача.

7) Як опрацьовувати гігантські блоки коду (логи, згенеровані файли)?

Не рендерте їх повністю токенізованими з per‑line DOM. Надайте усічену превʼю і «view raw» для завантаження. Тримайте сторінку швидкою.

8) Що з доступністю — чи потрібні ARIA для кодових блоків?

Кодовий блок має залишатися стандартним HTML (<pre><code>). Кнопка копіювання потребує правильних міток, фокусу та ненав’язливого зворотного зв’язку через aria‑live.

9) Чому мої виділені рядки зсуваються між середовищами?

Зазвичай через нормалізацію кінців рядків (CRLF vs LF) або відмінності в обрізці. Нормалізуйте при інґесті і тестуйте на фікстурах з Windows‑кінцями рядків.

10) Чи можна інструментувати події копіювання безпечно?

Так — логируйте лише метадані. Ніколи не логируйте скопійований вміст. Припускайте, що снипети можуть містити секрети, навіть якщо «не повинні».

Наступні кроки, які реально відправляють у продакшен

Якщо ви хочете кодові блоки в стилі GitHub, не перетворюючи платформу документації на науковий проєкт, зробіть це в такому порядку:

  1. Визначте контракт компонента (поля моделі, правила payload для копіювання, правила діапазонів виділення).
  2. Перенесіть підсвічування з клієнта, якщо у вас немає справді динамічного контенту.
  3. Реалізуйте копіювання зі source, а не з відрендереного DOM‑тексту.
  4. Встановіть обмеження продуктивності (макс‑рядків для per‑line рендеру, макс‑мови у бандлі).
  5. Лінтуйте контент документації на предмет Unicode‑небезпек, неправильних діапазонів та використання промптів.
  6. Інструментуйте відмови копіювання і продуктивність сторінок для ваших найгірших рунакбуків.

Потім відправляйте в продакшен. Слідкуйте за метриками. Якщо ви не бачите менше звернень «документація зламала мою команду», шлях копіювання й досі когось обманює.

← Попередня
Rspamd Хибні спрацьовування: налаштуйте оцінювання спаму, не пропускаючи сміття
Наступна →
Proxmox «Не вдається видалити вузол»: безпечне видалення з кластера

Залишити коментар