Умный якорь «Продолжить чтение» для uCoz

Юрий Герук 2025-12-25 50
Умный якорь «Продолжить чтение» для uCoz

Введение

Длинные статьи редко читают за один раз. Пользователь открыл материал, пролистал, отвлёкся, закрыл вкладку. Через час или через несколько дней вернулся и начинается классическая ситуация: где я остановился и что уже читал.

В uCoz такого поведения из коробки нет. Поэтому ниже разберём аккуратное клиентское решение, которое запоминает место чтения и предлагает продолжить ровно с того же места. Без регистрации, без куки-баннеров, без внешних сервисов и без нагрузки на сервер.

Что делает это решение и зачем оно нужно

Скрипт отслеживает прокрутку страницы и сохраняет прогресс чтения в localStorage браузера пользователя. При повторном открытии статьи он показывает компактную плашку с предложением продолжить чтение.

Возможности решения:

  • запоминает позицию чтения в конкретной статье;
  • корректно работает и со стандартными URL uCoz, и с красивыми;
  • предлагает продолжить чтение только если пользователь реально пролистал страницу;
  • аккуратно прокручивает к сохранённому месту;
  • подсвечивает ближайший блок, чтобы пользователь сразу понял, где он остановился.

Это особенно полезно для:

  • блогов с длинными статьями;
  • инструкций и мануалов;
  • баз знаний;
  • технических разборов и гайдов.

Как это работает

Логика максимально простая и надёжная.

  1. Для каждой статьи формируется уникальный ключ на основе canonical URL.
  2. Во время прокрутки сохраняется позиция чтения в пикселях и в процентах.
  3. При следующем заходе проверяется, есть ли сохранённые данные.
  4. Если данные есть и пользователь находится в начале страницы, появляется плашка «Продолжить чтение».
  5. По нажатию выполняется плавный скролл к сохранённому месту и подсветка ближайшего блока.

Все данные хранятся локально в браузере пользователя и автоматически устаревают.

Как выглядит плашка сейчас

После доработок плашка:

  • не растягивается на всю ширину экрана;
  • располагается по центру снизу;
  • занимает ровно столько места, сколько нужно;
  • выглядит как компактный 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

Автор: Юрий Герук

Комментарии