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

Добавление блоков кода в статьи (обновлено под 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> останется всего лишь строкой, а не исполняемым скриптом.