Введение
Длинные статьи редко читают за один раз. Пользователь открыл материал, пролистал, отвлёкся, закрыл вкладку. Через час или через несколько дней вернулся и начинается классическая ситуация: где я остановился и что уже читал.
В uCoz такого поведения из коробки нет. Поэтому ниже разберём аккуратное клиентское решение, которое запоминает место чтения и предлагает продолжить ровно с того же места. Без регистрации, без куки-баннеров, без внешних сервисов и без нагрузки на сервер.
Что делает это решение и зачем оно нужно
Скрипт отслеживает прокрутку страницы и сохраняет прогресс чтения в localStorage браузера пользователя. При повторном открытии статьи он показывает компактную плашку с предложением продолжить чтение.
Возможности решения:
- запоминает позицию чтения в конкретной статье;
- корректно работает и со стандартными URL uCoz, и с красивыми;
- предлагает продолжить чтение только если пользователь реально пролистал страницу;
- аккуратно прокручивает к сохранённому месту;
- подсвечивает ближайший блок, чтобы пользователь сразу понял, где он остановился.
Это особенно полезно для:
- блогов с длинными статьями;
- инструкций и мануалов;
- баз знаний;
- технических разборов и гайдов.
Как это работает
Логика максимально простая и надёжная.
- Для каждой статьи формируется уникальный ключ на основе canonical URL.
- Во время прокрутки сохраняется позиция чтения в пикселях и в процентах.
- При следующем заходе проверяется, есть ли сохранённые данные.
- Если данные есть и пользователь находится в начале страницы, появляется плашка «Продолжить чтение».
- По нажатию выполняется плавный скролл к сохранённому месту и подсветка ближайшего блока.
Все данные хранятся локально в браузере пользователя и автоматически устаревают.
Как выглядит плашка сейчас
После доработок плашка:
- не растягивается на всю ширину экрана;
- располагается по центру снизу;
- занимает ровно столько места, сколько нужно;
- выглядит как компактный toast, а не системное уведомление;
- не перекрывает контент и не раздражает.
Установка и подключение
Куда вставлять CSS
- Панель управления сайта
- Управление дизайном
- Таблица стилей CSS
Вставьте стили целиком.
.uc-resume-bar {
position: fixed;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
z-index: 9999;
display: none;
align-items: center;
gap: 8px;
max-width: 420px;
width: max-content;
padding: 8px 12px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.85);
color: #ffffff;
backdrop-filter: blur(6px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.28);
}
.uc-resume-bar.is-show {
display: flex;
}
.uc-resume-bar__text {
font-size: 13px;
line-height: 1.2;
white-space: nowrap;
opacity: 0.95;
}
.uc-resume-bar__text b {
font-weight: 600;
}
.uc-resume-bar__btn,
.uc-resume-bar__close {
border: 0;
border-radius: 8px;
padding: 5px 8px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
transition: background 0.15s ease;
}
.uc-resume-bar__btn {
background: rgba(255, 255, 255, 0.20);
color: #ffffff;
}
.uc-resume-bar__btn:hover {
background: rgba(255, 255, 255, 0.28);
}
.uc-resume-bar__close {
background: rgba(255, 255, 255, 0.10);
color: #dddddd;
}
.uc-resume-bar__close:hover {
background: rgba(255, 255, 255, 0.18);
}
.uc-resume-mark {
position: relative;
outline: 2px solid rgba(255, 193, 7, 0.55);
outline-offset: 5px;
border-radius: 8px;
scroll-margin-top: 90px;
}
@media (max-width: 480px) {
.uc-resume-bar {
max-width: calc(100% - 24px);
padding: 8px 10px;
}
.uc-resume-bar__text {
font-size: 12px;
}
.uc-resume-bar__btn,
.uc-resume-bar__close {
font-size: 11px;
padding: 5px 7px;
}
}
Куда вставлять JavaScript
- Панель управления сайта
- Управление дизайном
- Модуль Новости / Блог или каталоги
- Страница материала и комментариев
- Перед закрывающим </body>
<script>
(function () {
'use strict';
var SAVE_EVERY_MS = 1200;
var SHOW_IF_MORE_THAN_PX = 600;
var EXPIRE_DAYS = 45;
var SMOOTH_SCROLL = true;
function nowMs() { return Date.now(); }
function normUrl(u) {
try {
var x = new URL(u, location.href);
return x.origin + x.pathname.replace(/\/+$/, '');
} catch (e) {
return (u || '').split('#')[0].split('?')[0].replace(/\/+$/, '');
}
}
function pageKey() {
var c = document.querySelector('link[rel="canonical"]');
var base = c && c.href ? c.href : location.href;
return normUrl(base);
}
function isEntryPage() {
if (document.querySelector('.eMessage')) return true;
if (document.querySelector('article, .entry, .post, .article-post')) return true;
var og = document.querySelector('meta[property="og:type"][content="article"]');
return !!og;
}
function getEntryNode() {
return document.querySelector('article.article-post') ||
document.querySelector('.article-post') ||
document.querySelector('.post') ||
document.querySelector('.entry') ||
document.querySelector('.eMessage') ||
document.querySelector('article');
}
if (!isEntryPage()) return;
var entryNode = getEntryNode();
if (!entryNode) return;
var KEY = 'uc_resume:' + pageKey();
function safeParse(s) {
try { return JSON.parse(s); } catch (e) { return null; }
}
function docHeight() {
var d = document.documentElement;
return Math.max(d.scrollHeight, d.offsetHeight, d.clientHeight);
}
function winHeight() {
return window.innerHeight || document.documentElement.clientHeight || 0;
}
function scrollTop() {
return window.pageYOffset || document.documentElement.scrollTop || 0;
}
function setScrollTop(y) {
if (SMOOTH_SCROLL) {
window.scrollTo({ top: y, behavior: 'smooth' });
} else {
window.scrollTo(0, y);
}
}
function save() {
var y = scrollTop();
var max = Math.max(1, docHeight() - winHeight());
var pct = Math.round((y / max) * 1000) / 10;
try {
localStorage.setItem(KEY, JSON.stringify({ px: y, pct: pct, ts: nowMs() }));
} catch (e) {}
}
function load() {
var raw = localStorage.getItem(KEY);
var data = raw ? safeParse(raw) : null;
if (!data || !data.ts) return null;
if ((nowMs() - data.ts) > EXPIRE_DAYS * 86400000) {
localStorage.removeItem(KEY);
return null;
}
return data;
}
function buildBar(data) {
var el = document.createElement('div');
el.className = 'uc-resume-bar';
el.innerHTML =
'<div class="uc-resume-bar__text">Продолжить чтение с <b>' + (data.pct || 0) + '%</b>.</div>' +
'<button class="uc-resume-bar__btn" type="button">Продолжить</button>' +
'<button class="uc-resume-bar__close" type="button">Скрыть</button>';
el.querySelector('.uc-resume-bar__btn').onclick = function () {
el.classList.remove('is-show');
setScrollTop(data.px || 0);
setTimeout(markNearestBlock, 500);
};
el.querySelector('.uc-resume-bar__close').onclick = function () {
el.classList.remove('is-show');
};
return el;
}
function markNearestBlock() {
var nodes = entryNode.querySelectorAll('p, h2, h3, h4, li, pre, blockquote');
if (!nodes.length) return;
var y = scrollTop();
var best = null;
var bestDist = Infinity;
nodes.forEach(function (n) {
var top = n.getBoundingClientRect().top + scrollTop();
var dist = Math.abs(top - y);
if (dist < bestDist) {
bestDist = dist;
best = n;
}
});
if (best) best.classList.add('uc-resume-mark');
}
var data = load();
if (data && data.px > SHOW_IF_MORE_THAN_PX && scrollTop() < 200) {
var bar = buildBar(data);
document.body.appendChild(bar);
requestAnimationFrame(function () {
bar.classList.add('is-show');
});
}
var lastSave = 0;
window.addEventListener('scroll', function () {
var t = nowMs();
if (t - lastSave > SAVE_EVERY_MS) {
lastSave = t;
save();
}
}, { passive: true });
window.addEventListener('beforeunload', save);
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') save();
});
})();
</script>
Как адаптировать под свой шаблон
Самое важное место — функция getEntryNode().
Если у вас другой контейнер статьи, просто добавьте его первым.
Пример:
function getEntryNode() {
return document.querySelector('.article-content') ||
document.querySelector('.article-post') ||
document.querySelector('.eMessage') ||
document.querySelector('article');
}
Больше ничего менять не требуется.
Заключение
Этот скрипт решает реальную проблему длинных статей. Он не навязывается, не мешает чтению и работает незаметно, пока не нужен.
Такие мелкие UX-фичи сильно повышают удобство сайта и делают его ощущаемо более продуманным, даже без сложной серверной логики.
Оцените полезность материала!
Лицензия: CC BY-SA 4.0
Автор: Юрий Герук
Комментарии