Ваші документи виглядають добре, поки хтось не вставить команду й у неї непомітно потраплять номера рядків, промпт або зайвий пробіл. Тоді приходить тікет: «ваші інструкції зламали продакшен».
Кодові блоки GitHub здаються простими: панель з іменем файлу, кнопка копіювання, яка копіює правильно, нумерація, що не псує буфер, і виділені рядки, що вказують на потрібне місце. Відтворити це на своєму сайті можна — якщо припинити думати «тільки CSS» і почати думати про це як про компонент із вимогами надійності.
Як «добре» має виглядати в продакшен‑доках
Кодові блоки в стилі GitHub — це не про естетику. Це про зниження операційного ризику. Коли хтось виконує рунакбук о 03:00, кодовий блок — це інтерфейс. Якщо він бреше, заплутує або копіює невірні байти, ваш «сайт документації» стає фактором простою.
Ось критерії, якими я користуюся:
- Копіювання точно. Копіюється лише код. Ніяких промптів, номерів рядків чи невидимого Unicode‑конфетті.
- Номери рядків — лише презентаційні. Вони допомагають посилатися на «рядок 17», не забруднюючи вміст буфера.
- Виділені рядки — керовані даними. Автор може вказати, які рядки важливі (контекст diff, «змініть це», «не виконувати»).
- Панель заголовка несе корисні метадані. Ім’я файлу, мова, можливо «shell», «k8s» або «output». Не порожня прикраса.
- Доступність за замовчуванням. Кнопка копіювання працює з клавіатури, озвучує статус і не краде фокус, як дитина з новим барабаном.
- Швидко. Відтворення 30 блоків коду не повинно перетворювати сторінку в обігрівач.
- Працює офлайн і під CSP. Продакшен‑системи часто мають суворі політики. Ваша документація має виживати в них.
Парафраз думки (John Ousterhout): складність робить системи важкими для змін і розуміння; якщо можна її прибрати — зробіть це.
І так, я ставитимуся до «віджета кодового блоку» як до міні‑продуктової системи. Бо це саме так. Це також розподілена система: автор, рендерер, браузер, API буфера обміну та терпіння користувача — жодним з цих елементів ви не керуєте повністю.
Факти та коротка історія (чому переміг GitHub)
Трохи контексту допоможе робити кращі рішення. Це невеликі факти, але вони пояснюють, чому екосистема вийшла такою.
- Ранні приклади коду в мережі були простими <pre>. «Підсвічування синтаксису» починалося як серверні regex‑хакі в 1990‑х, задовго до того, як браузери мали хороші шрифти чи рушії виведення.
- Pygments (середина 2000‑х) зробив підсвічування мейнстрімом. Воно популяризувало модель «токенізувати і стилізувати span‑и», яку використовують більшість підсвічувачів досі.
- GitHub популяризував fenced code blocks у Markdown. Конвенція з потрійними бектіками стала типовою ментальною моделлю для коду в документації.
- API буфера обміну еволюціонували пізно. Довгі роки «копіювання» означало виділення тексту й надію, що DOM не містить сміття; сучасний Clipboard API зробив надійні кнопки можливими.
- Нумерація рядків завжди була спірною. IDE її потребують; у документації часто ні. Дебати існують, бо нумерація корисна, але легко реалізується неправильно.
- Клієнтське підсвічування виникло як реакція на статичний хостинг. Коли всі почали деплоїти документацію на CDN, відправляти JS‑підсвічувачі здавалося простіше, ніж серверний рендер — допоки не з’явився «рахунок» за продуктивність.
- Власні UI‑патерни GitHub стали де‑факто стандартом. Панель заголовка + кнопка копіювання знайомі користувачу, тож вони довіряють їм і користуються інтуїтивно.
- Підсвічувачі змагаються за «семантичну» коректність. 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)
Коли у вас є така модель, рендерер може бути детерміністичним, тестованим і нудним. Нудність — це добре. Нудність відправляє в продакшен.
Нумерація рядків: цукерка UX з гострими кутами
Номери рядків покращують співпрацю: «змінити рядок 42» — це чітка інструкція. Але вони мають підводні камені.
Як нумерація рядків ламається
- Вони копіюються. Якщо реалізувати їх вставкою реальних текстових вузлів на початку рядків, вони потраплять у виділення й буфер.
- Вони дрейфують. Якщо переноси змінюють сприйняття «рядка», люди посилатимуться не на те, що треба.
- Вони ламають пошук. Деякі реалізації змінюють DOM так, що пошук сторінки перестає коректно знаходити очікувані фрагменти коду.
- Вони сповільнюють рендеринг. Розбивка на тисячі елементів рядків може спричинити DOM‑вибух.
Патерни реалізації, що витримують навантаження
Два патерни працюють стабільно:
- CSS‑лічильники для номерів рядків, без вставки чисел у текст. Це швидко, і виділення можна залишити чистим.
- Окрема колонка‑гуттер з номерами як власними елементами, тоді текст коду лишається окремим вибірним блоком.
Якщо ви виділяєте рядки, загортаючи кожен рядок в окремий елемент, ви вже розбиваєте рядки. Це нормально для невеликих блоків, але потрібен поріг. Понад певний розмір перемикайтеся на «без 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, який не зрадить
- Виберіть стратегію рендерингу. Віддавайте перевагу підсвічуванню під час збірки або SSR для документації; уникайте клієнтської токенізації, якщо контент не справді динамічний.
- Визначте модель кодового блоку. language, title, raw code, highlight ranges, showLineNumbers, kind, copy payload.
- Нормалізуйте вхідні дані. Перетворіть CRLF у LF, збережіть табуляцію, зберігайте кінцеві пробіли там, де це важливо, і відхиляйте форматні символи в CI.
- Реалізуйте шлях відображення. Рендерьте панель заголовка + код; тримайте текст коду у стабільній структурі DOM.
- Реалізуйте номери рядків безпечно. CSS‑лічильники або окрема колонка; ніколи не інжектіть числа в текст коду.
- Реалізуйте виділені рядки детерміністично. Розбирайте «1,3-5»; валідуйте; виділяйте лише логічні рядки.
- Реалізуйте копіювання з використанням збереженого payload. Не скребіть DOM; обробляйте відмови clipboard з fallback (select + manual copy).
- Додайте доступність. Фокус клавіатурою, aria‑мітки, live‑регія зворотного зв’язку, достатній контраст для виділень.
- Встановіть обмеження продуктивності. Жорсткі ліміти per‑line DOM; режим «view raw» для величезних блоків; обмежити набори мов.
- Додайте телеметрію. Успіх/помилка копіювання, помилки парсингу виділень, таймінги продуктивності на важких сторінках.
- Пишіть тести. Снапшот‑тести HTML‑структури; unit‑тести для парсингу діапазонів; e2e‑тести для payload копіювання.
- Документуйте правила авторингу. Як вказувати заголовки, промпти і виділення; що копіюється, а що ні.
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, не перетворюючи платформу документації на науковий проєкт, зробіть це в такому порядку:
- Визначте контракт компонента (поля моделі, правила payload для копіювання, правила діапазонів виділення).
- Перенесіть підсвічування з клієнта, якщо у вас немає справді динамічного контенту.
- Реалізуйте копіювання зі source, а не з відрендереного DOM‑тексту.
- Встановіть обмеження продуктивності (макс‑рядків для per‑line рендеру, макс‑мови у бандлі).
- Лінтуйте контент документації на предмет Unicode‑небезпек, неправильних діапазонів та використання промптів.
- Інструментуйте відмови копіювання і продуктивність сторінок для ваших найгірших рунакбуків.
Потім відправляйте в продакшен. Слідкуйте за метриками. Якщо ви не бачите менше звернень «документація зламала мою команду», шлях копіювання й досі когось обманює.