Введение
Длинные статьи на сайте неудобно читать без навигации. Оглавление решает это сразу: показывает структуру, дает быстрые переходы, подсвечивает текущий раздел при прокрутке.
Ниже готовое решение для 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, чтобы прокрутка не прятала заголовок под шапкой.
Заключение
Оглавление повышает удобство чтения длинных материалов, делает структуру статьи понятной и добавляет нормальную навигацию, как в документации. При этом якоря остаются короткими и стабильными, независимо от того, как меняется текст заголовков.
Оцените полезность материала!
Лицензия: CC BY-SA 4.0
Автор: Юрий Герук
Комментарии