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