Добавление блоков кода в статьи

Добавление блоков кода в статьи (обновлено под 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">&lt;div class="card"&gt;…&lt;/div&gt;</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

  1. Старая инициализация.hljs.initHighlightingOnLoad() и hljs.highlightBlock(...) — это подход для v10. В v11 используйте hljs.highlightAll() и hljs.highlightElement(...).

  2. Нет явного языка. Автодетект может подсветить неверно и медленнее.

  3. Неэкранированный HTML-код. Ломает верстку и опасно.

  4. Переподключение тем/скриптов. Дубли — причина «поехавших» цветов и стилей.

  5. Подключили «полный» бандл на каждой странице. Лишние килобайты и более медленный 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 экранируйте&lt;&gt;&amp;:

    <pre><code class="language-html">&lt;script&gt;alert(1)&lt;/script&gt;</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="&lt;h1&gt;Это только предпросмотр&lt;/h1&gt;&lt;script&gt;alert(1)&lt;/script&gt;"> </iframe> 

Скрипты внутри такого iframe не выполнятся. Для подсветки именно «кода» лучше придерживаться 13.1–13.2.

Итого

Для блоков кода всегда рендерим текст (Markdown-ограждения, textContent), не используем innerHTML, внедряем санитизацию и включаем строгий CSP. Тогда <script>…</script> останется всего лишь строкой, а не исполняемым скриптом.