Добавление блоков кода в статьи
Добавление блоков кода в статьи (обновлено под highlight.js 11.11.1)
Подсветка синтаксиса делает технические статьи читабельнее и понятнее. Ниже — обновлённый, практичный гайд по подключению highlight.js 11.11.1: от быстрого старта до кнопки «Скопировать», нумерации строк, тем и производительности.
Быстрый старт (CDN + рекомендованная инициализация v11)
Вставьте стили и скрипты в <head> (или перед </body> с defer) и вызовите hljs.highlightAll() после загрузки DOM.
<!-- Тема (light по умолчанию) --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css"> <!-- Библиотека (полная сборка) --> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" defer></script> <!-- Инициализация для v11 --> <script> document.addEventListener('DOMContentLoaded', function () { hljs.highlightAll(); }); </script> Минимальная разметка блока кода
<pre><code class="language-php"> <?php echo "Hello, world!\n"; </code></pre> Рекомендации:
Указывайте язык явно:
class="language-php"(илиlanguage-js,language-bashи т. д.) — так вы избегаете ошибок автодетекта и ускоряете подсветку.Для HTML-кода экранируйте
<и>.
Markdown и «чистый» HTML
Markdown: тройные бэктики + язык:
```php echo "Hello, world!\n"; ``` HTML: экранирование обязательно, если показываете HTML-код:
<pre><code class="language-html"><div class="card">…</div></code></pre> Динамически добавляемый код (SPA/редактор/ AJAX)
В v11 используйте hljs.highlightElement(el) для новых узлов:
<script> function highlightNewCode(root=document) { root.querySelectorAll('pre code').forEach(function (block) { hljs.highlightElement(block); }); } // Пример: // const frag=document.createRange().createContextualFragment('<pre><code class="language-js">const x=42;</code></pre>'); // document.body.appendChild(frag); // highlightNewCode(frag); </script> Темы, светлая/тёмная схема
Подключите две темы и переключайте их через prefers-color-scheme:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css" media="(prefers-color-scheme: light)"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/monokai-sublime.min.css" media="(prefers-color-scheme: dark)"> Популярные темы: github, atom-one-dark, monokai-sublime, solarized-light, vs2015.
Кнопка «Скопировать» (UX-улучшение)
<style> pre { position: relative; } .copy-btn { position: absolute; top: .5rem; right: .5rem; padding: .25rem .5rem; border: 0; border-radius: .375rem; background: rgba(0,0,0,.6); color: #fff; font: 12px/1 system-ui, sans-serif; cursor: pointer; opacity: .8; } .copy-btn:focus, .copy-btn:hover { opacity: 1; } pre code { display: block; padding: 1rem; overflow: auto; } </style> <script> document.addEventListener('DOMContentLoaded', function () { document.querySelectorAll('pre').forEach(function (pre) { if (pre.querySelector('.copy-btn')) return; const btn=document.createElement('button'); btn.className='copy-btn'; btn.type='button'; btn.setAttribute('aria-label', 'Скопировать код'); btn.textContent='Copy'; btn.addEventListener('click', async function () { const text=pre.querySelector('code')?.innerText || ''; try { await navigator.clipboard.writeText(text); btn.textContent='Copied!'; } catch { const ta=document.createElement('textarea'); ta.value=text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); btn.textContent='Copied!'; } setTimeout(()=> btn.textContent='Copy', 1200); }); pre.appendChild(btn); }); }); </script> Нумерация строк (опционально)
Плагин highlightjs-line-numbers.js совместим с v11. После подсветки вызовите инициализацию:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/styles/line-numbers.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js" defer></script> <script> document.addEventListener('DOMContentLoaded', function () { hljs.highlightAll(); // подсветка if (hljs.initLineNumbersOnLoad) { hljs.initLineNumbersOnLoad(); // нумерация } }); </script> <!-- Добавьте класс line-numbers к <pre> --> <pre class="line-numbers"><code class="language-js">const x=42;</code></pre> Если вы подсвечиваете динамически, вызывайте:
hljs.highlightElement(block); if (window.hljs?.lineNumbersBlock) hljs.lineNumbersBlock(block);
Производительность
Только нужные языки. Вместо «полной» сборки подключите ядро + конкретные языки:
<!-- Минимальное ядро --> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/core.min.js" defer></script> <!-- Нужные языки --> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/php.min.js" defer></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js" defer></script> <script> document.addEventListener('DOMContentLoaded', ()=> hljs.highlightAll()); </script>Отключите автодетект. Ставьте
language-xxx.Отложенная загрузка. Используйте
defer, подгружайте подсветку только там, где есть код.Длинные фрагменты. Оставляйте горизонтальный скролл (
overflow: auto;) вместо агрессивного переноса строк.
Доступность (a11y) и удобства
Оборачивайте блоки в
<figure>с подписью:<figure> <figcaption>Пример запроса к API (PHP)</figcaption> <pre><code class="language-php">...</code></pre> </figure>Дайте кнопке «Copy» видимый фокус.
При необходимости задайте
tab-size: 2;для ровных отступов:pre code { tab-size: 2; }
Безопасность при вставке кода
Если код приходит от пользователей (комменты, CMS-плагины):
Санитизируйте входной HTML на сервере/клиенте.
Вставляйте текст как
textContent, а неinnerHTML:const code=document.createElement('code'); code.className='language-js'; code.textContent='alert("hi")'; // безопасно
Частые ошибки при переходе на v11
Старая инициализация.
hljs.initHighlightingOnLoad()иhljs.highlightBlock(...)— это подход для v10. В v11 используйтеhljs.highlightAll()иhljs.highlightElement(...).Нет явного языка. Автодетект может подсветить неверно и медленнее.
Неэкранированный HTML-код. Ломает верстку и опасно.
Переподключение тем/скриптов. Дубли — причина «поехавших» цветов и стилей.
Подключили «полный» бандл на каждой странице. Лишние килобайты и более медленный LCP.
Альтернативы
Prism.js — лёгкая библиотека с богатой экосистемой плагинов (удобно для блогов/документации).
Shiki — «VS Code-классика» с серверной/статической подсветкой (идеально для SSG и «пиксель-перфект» отрисовки).
Готовый шаблон под hljs 11.11.1
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <title>Демо подсветки кода (hljs 11.11.1)</title> <!-- Светлая/тёмная темы --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css" media="(prefers-color-scheme: light)"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/monokai-sublime.min.css" media="(prefers-color-scheme: dark)"> <style> body { margin: 2rem; font: 16px/1.6 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; } pre { position: relative; margin: 1rem 0; border-radius: .5rem; } pre code { display: block; padding: 1rem; overflow: auto; } .copy-btn { position: absolute; top: .5rem; right: .5rem; padding: .25rem .5rem; border: 0; border-radius: .375rem; background: rgba(0,0,0,.6); color: #fff; font-size: 12px; cursor: pointer; opacity: .85; } .copy-btn:focus, .copy-btn:hover { opacity: 1; outline: 2px solid rgba(0,0,0,.2); } </style> <!-- Библиотека: ядро + нужные языки --> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/core.min.js" defer></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/php.min.js" defer></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js" defer></script> <script> document.addEventListener('DOMContentLoaded', function () { hljs.highlightAll(); // Кнопки «Copy» document.querySelectorAll('pre').forEach(function (pre) { const btn=document.createElement('button'); btn.className='copy-btn'; btn.type='button'; btn.textContent='Copy'; btn.setAttribute('aria-label', 'Скопировать код'); btn.addEventListener('click', async function () { const text=pre.querySelector('code')?.innerText || ''; try { await navigator.clipboard.writeText(text); btn.textContent='Copied!'; } catch {} setTimeout(()=> btn.textContent='Copy', 1200); }); pre.appendChild(btn); }); }); </script> </head> <body> <h1>Пример подсветки</h1> <figure> <figcaption>PHP: базовый вывод</figcaption> <pre><code class="language-php"><?php echo "Hello, world!\n"; ?></code></pre> </figure> <figure> <figcaption>JavaScript: функция суммы</figcaption> <pre><code class="language-javascript">const sum=(a, b)=> a + b;</code></pre> </figure> </body> </html> Как сделать так, чтобы добавляемый блок кода не запускался в браузере?
Ключевая идея: показывать код как текст, а не как HTML-разметку/скрипты. Ниже — быстрые и надёжные способы для статического и динамического контента.
Статическая разметка (Markdown/HTML)
Markdown-ограждения (тройные бэктики) автоматически экранируют угловые скобки — браузер не выполняет код:
```html <script>alert(1)</script> ```В «чистом» HTML экранируйте
<>&:<pre><code class="language-html"><script>alert(1)</script></code></pre>
Динамическая вставка (SPA, AJAX, редакторы)
Никогда не вставляйте код через innerHTML. Используйте только textContent / createTextNode — так браузер не интерпретирует теги:
<script> function insertCode(lang, rawText, root=document.body) { const pre=document.createElement('pre'); const code=document.createElement('code'); code.className='language-' + lang; // пример: language-html code.textContent=rawText; // ← безопасно: теги не парсятся pre.appendChild(code); root.appendChild(pre); hljs.highlightElement(code); // hljs 11.x } // Пример: // insertCode('html', '<script>alert(1)</script>'); </script> Санитизация пользовательского ввода
Если код приходит от пользователей (комментарии, CMS и т. п.), дополнительно санитизируйте HTML на сервере/клиенте (вырезайте <script>, on*-обработчики, javascript:-URI, <style>, <iframe> и пр.).
Даже при санитизации сохраняйте правило из п. 13.2: отображение — только через textContent.
CSP — защитная «сетка» на уровне заголовков
Добавьте строгую Content-Security-Policy, чтобы даже случайно вставленный HTML не выполнил скрипты:
# Пример для Nginx (адаптируйте под свой стек) add_header Content-Security-Policy " default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com; style-src 'self' https://cdnjs.cloudflare.com 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; " always; Примечание: мы разрешили
https://cdnjs.cloudflare.com(для highlight.js и тем).
Избегайтеscript-src 'unsafe-inline'. Если у вас есть инлайн-скрипты, лучше вынести их во внешние файлы или использовать nonce/hash.
Шаблонизаторы и движки
Blade (Laravel): если нужно показать фрагменты, содержащие
@if,{{ }}и др., оборачивайте в@verbatim … @endverbatim, чтобы Blade не интерпретировал код.Twig/Нужно буквально вывести теги: используйте блоки
verbatim/экранирование.Vue/React в тексте: при демонстрации
{{ mustache }}или JSX не давайте им попадать в реальный шаблон — выводите как текст (см. 13.2).
Песочница (для предпросмотра чужого HTML)
Если вам нужно отобразить готовый HTML как изолированный предпросмотр (не просто код), используйте <iframe sandbox>безallow-scripts:
<iframe sandbox srcdoc="<h1>Это только предпросмотр</h1><script>alert(1)</script>"> </iframe> Скрипты внутри такого iframe не выполнятся. Для подсветки именно «кода» лучше придерживаться 13.1–13.2.
Итого
Для блоков кода всегда рендерим текст (Markdown-ограждения, textContent), не используем innerHTML, внедряем санитизацию и включаем строгий CSP. Тогда <script>…</script> останется всего лишь строкой, а не исполняемым скриптом.