Введение
Длинные статьи на сайте неудобно читать без навигации. Оглавление решает это сразу: показывает структуру, дает быстрые переходы, подсвечивает текущий раздел при прокрутке.

Ниже готовое решение для uCoz, которое автоматически строит оглавление по заголовкам статьи и назначает короткие якоря вида toc_1, toc_2, toc_3 вместо длинных ссылок из текста.

Что делает скрипт
Ищет заголовки в тексте статьи.
Поддерживает h2, h3, h4, h5.
Если у заголовка нет id, назначает короткий цифровой якорь toc_1, toc_2 и так далее.
Строит оглавление в указанном контейнере.
Делает плавную прокрутку по клику.
Подсвечивает активный пункт при прокрутке.
Позволяет свернуть и развернуть оглавление.
Аккуратно выглядит на мобильных.


Важно про eMessage и где именно скрипт ищет заголовки
Да. Скрипт ищет заголовки не по всей странице, а внутри блока с текстом материала. На uCoz чаще всего текст материала находится в контейнере .eMessage, поэтому он используется первым.

Если у вас в шаблоне текст выводится в другом контейнере, нужно указать его селектор в настройках скрипта. Это одна строка в конфиге, покажу ниже.


Скрипт использует список селекторов и берет первый найденный:


  • .eMessage это самый частый вариант на uCoz.
  • .entry-content, .post-content, article, main и другие добавлены как запасные.


Установка на uCoz

Шаг 1. Добавьте контейнер для оглавления
В шаблоне страницы материала нужного модуля вставьте контейнер там, где должно появиться оглавление. Обычно его ставят перед выводом текста статьи.


Код
<div id="tocMount"></div>


Шаг 2. Добавьте стили
Вставьте стили в шаблон. Можно в общий CSS, можно перед </body>.


Код
<style>
  :root{
  --toc-bg: rgba(0,0,0,.04);
  --toc-border: rgba(0,0,0,.12);
  --toc-text: inherit;
  --toc-muted: rgba(0,0,0,.65);
  --toc-accent: #0d6efd;
  --toc-radius: 14px;
  }

  @media (prefers-color-scheme: dark){
  :root{
  --toc-bg: rgba(255,255,255,.06);
  --toc-border: rgba(255,255,255,.14);
  --toc-muted: rgba(255,255,255,.7);
  }
  }

  .u-toc{
  margin: 16px 0 18px 0;
  padding: 14px 14px 12px 14px;
  border: 1px solid var(--toc-border);
  border-radius: var(--toc-radius);
  background: var(--toc-bg);
  color: var(--toc-text);
  }

  .u-toc__top{
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: space-between;
  }

  .u-toc__title{
  font-weight: 700;
  font-size: 16px;
  line-height: 1.2;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  }

  .u-toc__badge{
  font-weight: 600;
  font-size: 12px;
  line-height: 1;
  padding: 4px 8px;
  border-radius: 999px;
  border: 1px solid var(--toc-border);
  color: var(--toc-muted);
  white-space: nowrap;
  flex: 0 0 auto;
  }

  .u-toc__btn{
  appearance: none;
  border: 1px solid var(--toc-border);
  background: transparent;
  color: inherit;
  border-radius: 10px;
  padding: 8px 10px;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  user-select: none;
  transition: transform .05s ease, background-color .15s ease;
  flex: 0 0 auto;
  }

  .u-toc__btn:active{
  transform: translateY(1px);
  }

  .u-toc__btn:hover{
  background: rgba(0,0,0,.05);
  }

  @media (prefers-color-scheme: dark){
  .u-toc__btn:hover{
  background: rgba(255,255,255,.08);
  }
  }

  .u-toc__list{
  margin: 12px 0 0 0;
  padding: 0;
  list-style: none;
  }

  .u-toc__item{
  margin: 6px 0;
  padding: 0;
  }

  .u-toc__link{
  display: inline-flex;
  gap: 8px;
  align-items: baseline;
  text-decoration: none;
  color: inherit;
  padding: 6px 8px;
  border-radius: 10px;
  transition: background-color .15s ease, color .15s ease;
  }

  .u-toc__link:hover{
  background: rgba(0,0,0,.05);
  }

  @media (prefers-color-scheme: dark){
  .u-toc__link:hover{
  background: rgba(255,255,255,.08);
  }
  }

  .u-toc__dot{
  width: 8px;
  height: 8px;
  border-radius: 999px;
  border: 2px solid var(--toc-border);
  flex: 0 0 auto;
  position: relative;
  top: 1px;
  }

  .u-toc__link.is-active{
  color: var(--toc-accent);
  background: rgba(13,110,253,.08);
  }

  .u-toc__link.is-active .u-toc__dot{
  border-color: var(--toc-accent);
  background: var(--toc-accent);
  }

  .u-toc__sub{
  margin-left: 18px;
  }

  .u-toc.is-collapsed .u-toc__list{
  display: none;
  }

  .u-toc__hint{
  margin: 10px 0 0 0;
  font-size: 12px;
  color: var(--toc-muted);
  }

  .u-toc__hint code{
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  font-size: 12px;
  }

  @media (max-width: 575.98px){
  .u-toc__top{
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px;
  }

  .u-toc__title{
  width: 100%;
  flex-wrap: wrap;
  gap: 8px;
  }

  .u-toc__btn{
  width: 100%;
  display: block;
  text-align: center;
  }
  }
</style>


Шаг 3. Добавьте скрипт
Вставьте код перед </body> в шаблоне страницы материала.

В конфиге есть два места, которые обычно правят под шаблон:

headings если нужно добавить или убрать уровни заголовков.
contentSelectors если текст статьи у вас не в .eMessage.



Код
<script>
(function () {
  "use strict";

  var CFG = {
  mountId: "tocMount",

  // Какие заголовки собирать в оглавление.
  // Можно оставить "h2, h3" если нужны только два уровня.
  headings: "h2, h3, h4, h5",

  // Минимум заголовков, чтобы оглавление показывалось.
  minHeadings: 2,

  // Префикс для коротких якорей.
  idPrefix: "toc_",

  // Отступ при прокрутке к заголовку, если есть фиксированная шапка.
  activeOffsetPx: 110,

  // Плавная прокрутка.
  smoothScroll: true,

  // Где искать заголовки.
  // Скрипт берет первый найденный контейнер.
  contentSelectors: [
  ".eMessage",
  ".entry-content",
  ".post-content",
  ".u-entry__text",
  "article",
  "main",
  ".content",
  "body"
  ],

  // Запоминаем свернуто или нет.
  storageKey: "ucoz_toc_collapsed_v6"
  };

  function qs(sel, root) { return (root || document).querySelector(sel); }
  function qsa(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }

  function escapeHtml(s){
  return String(s).replace(/[&<>"']/g, function(ch){
  return ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[ch];
  });
  }

  function pickContentRoot(){
  for (var i = 0; i < CFG.contentSelectors.length; i++) {
  var el = qs(CFG.contentSelectors[i]);
  if (el) return el;
  }
  return document.body;
  }

  function ensureUniqueId(base){
  var id = base;
  var n = 2;
  while (document.getElementById(id)) {
  id = base + n;
  n++;
  }
  return id;
  }

  function build(){
  var mount = document.getElementById(CFG.mountId);
  if (!mount) return;

  var root = pickContentRoot();

  var hs = qsa(CFG.headings, root).filter(function(h){
  var txt = (h.textContent || "").trim();
  return txt.length > 0;
  });

  if (hs.length < CFG.minHeadings) return;

  var items = [];
  var seq = 1;

  hs.forEach(function(h){
  var text = (h.textContent || "").trim();
  var level = (h.tagName || "").toLowerCase();

  var id = h.getAttribute("id");
  if (!id) {
  id = ensureUniqueId(CFG.idPrefix + String(seq));
  h.setAttribute("id", id);
  seq++;
  }

  items.push({ id: id, level: level, text: text, el: h });
  });

  if (items.length < CFG.minHeadings) return;

  var isCollapsed = false;
  try { isCollapsed = localStorage.getItem(CFG.storageKey) === "1"; } catch(e) {}

  var html = "";
  html += '<nav class="u-toc' + (isCollapsed ? ' is-collapsed' : '') + '" aria-label="Оглавление">';
  html += '<div class="u-toc__top">';
  html += '<p class="u-toc__title">Оглавление <span class="u-toc__badge">' + items.length + '</span></p>';
  html += '<button type="button" class="u-toc__btn" data-toc-toggle>' + (isCollapsed ? 'Показать' : 'Свернуть') + '</button>';
  html += '</div>';
  html += '<ul class="u-toc__list">';

  items.forEach(function(it){
  var subClass = (it.level === "h3" || it.level === "h4" || it.level === "h5") ? " u-toc__sub" : "";
  html += '<li class="u-toc__item' + subClass + '">';
  html += '<a class="u-toc__link" href="#' + encodeURIComponent(it.id) + '" data-toc-link="' + escapeHtml(it.id) + '">';
  html += '<span class="u-toc__dot" aria-hidden="true"></span>';
  html += '<span class="u-toc__text">' + escapeHtml(it.text) + '</span>';
  html += '</a>';
  html += '</li>';
  });

  html += '</ul>';
  html += '<p class="u-toc__hint">Якоря короткие. Пример: <code>#' + escapeHtml(CFG.idPrefix) + '1</code>.</p>';
  html += '</nav>';

  mount.innerHTML = html;

  var toc = mount.querySelector(".u-toc");
  var btn = mount.querySelector("[data-toc-toggle]");
  var links = qsa("[data-toc-link]", mount);

  if (btn) {
  btn.addEventListener("click", function(){
  var collapsed = toc.classList.toggle("is-collapsed");
  btn.textContent = collapsed ? "Показать" : "Свернуть";
  try { localStorage.setItem(CFG.storageKey, collapsed ? "1" : "0"); } catch(e) {}
  }, { passive: true });
  }

  links.forEach(function(a){
  a.addEventListener("click", function(ev){
  var id = a.getAttribute("data-toc-link");
  var target = document.getElementById(id);
  if (!target) return;

  ev.preventDefault();

  var top = target.getBoundingClientRect().top + window.pageYOffset - CFG.activeOffsetPx;
  if (CFG.smoothScroll && "scrollBehavior" in document.documentElement.style) {
  window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
  } else {
  window.scrollTo(0, Math.max(0, top));
  }

  history.replaceState(null, "", "#" + encodeURIComponent(id));
  }, { passive: false });
  });

  var currentId = null;

  function setActive(id){
  if (currentId === id) return;
  currentId = id;
  links.forEach(function(a){
  a.classList.toggle("is-active", a.getAttribute("data-toc-link") === id);
  });
  }

  function onScroll(){
  var y = window.pageYOffset || document.documentElement.scrollTop || 0;
  var best = null;

  for (var i = 0; i < items.length; i++) {
  var el = items[i].el;
  var top = el.getBoundingClientRect().top + window.pageYOffset;
  if (top - CFG.activeOffsetPx <= y) best = items[i].id;
  }

  setActive(best || items[0].id);
  }

  onScroll();
  window.addEventListener("scroll", onScroll, { passive: true });
  window.addEventListener("resize", onScroll, { passive: true });
  }

  if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", build);
  } else {
  build();
  }
})();
</script>


Настройка под ваш шаблон
Если заголовки находятся не в .eMessage, найдите контейнер, внутри которого выводится текст статьи, и добавьте его в начало списка contentSelectors.

Примеры:


  • Если текст в <div class="my-article">, добавьте .my-article.
  • Если текст в <div id="content">, добавьте #content.
  • Если хотите собирать только h2 и h3, измените строку:
  • Было: headings: "h2, h3, h4, h5"
  • Нужно: headings: "h2, h3"


Как писать статьи, чтобы оглавление работало правильно


  • Используйте настоящие заголовки h2, h3, h4, h5.
  • Не заменяйте заголовки жирным текстом и переносами строк.
  • Если у вас фиксированная шапка, увеличьте activeOffsetPx, чтобы прокрутка не прятала заголовок под шапкой.


Заключение
Оглавление — это инструмент, который делает чтение длинных материалов более удобным. Оно помогает понять структуру статьи и обеспечивает простую навигацию, как в документации. При этом ссылки (якоря) остаются короткими и стабильными, независимо от того, как меняются заголовки.

Комментарии

Минимальная длина комментария - 50 знаков. комментарии модерируются
HTMLSTART » Скрипты UCOZ » Каталога файлов » Липкое оглавление статьи для uCoz с короткими цифровыми якорями