Мой блог по программированию

Проблема навигации в блоге на Tilda: как мы решили задачу возврата в точное место после чтения статьи

Как маркетолог, я постоянно сталкиваюсь с необходимостью улучшения пользовательского опыта на сайте. Сегодня хочу поделиться решением одной из самых раздражающих проблем в блогах на Tilda — невозможность вернуться к тому месту, откуда пользователь перешёл к статье.

Проблема: "Где я был на блоге?"

Представьте ситуацию: пользователь просматривает ленту вашего блога, находит интересную статью, переходит читать её. После прочтения он хочет вернуться к списку материалов, но вместо того, чтобы оказаться именно там, где остановился, его сбрасывает в начало страницы.
Что происходит? Пользователь:
  • Теряет контекст просмотра
  • Вынужден снова прокручивать и искать место, на котором остановился
  • Испытывает раздражение от плохого UX
  • С большей вероятностью покинет сайт
Для нас, маркетологов, это означает:
  • ❌ Снижение времени на сайте
  • ❌ Ухудшение поведенческих факторов
  • ❌ Потерю потенциальных конверсий
  • ❌ Недовольство пользователей

Почему стандартные решения Tilda не работают?

Tilda предлагает красивый и функциональный конструктор, но у него есть ограничения в кастомизации навигации. Стандартный функционал блога не предусматривает:
  • Сохранение позиции скролла при переходе на статью
  • "Умный" возврат к конкретной карточке материала
  • Учёт пагинации при восстановлении позиции

Наше решение: кастомный скрипт навигации

После нескольких неудачных попыток решить проблему стандартными средствами, мы разработали собственное JavaScript-решение, которое хорошо работает в экосистеме Tilda.

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

1. При клике на статью скрипт сохраняет:
  • UID материала
  • Точную позицию скролла
  • Номер страницы пагинации
  • Временную метку
2. При возврате из статьи система:
  • Находит нужную карточку по UID
  • Дожидается полной загрузки изображений
  • Прокручивает к нужной позиции с учётом высоты шапки
  • Автоматически подгружает контент при необходимости
3. Резервные механизмы на случай:
  • Прямого перехода по ссылке
  • Устаревших данных в sessionStorage
  • Динамически подгружаемого контента

Ключевые преимущества решения:

  • Точное позиционирование — пользователь возвращается именно к той карточке, на которую кликнул
  • Учёт пагинации — работает с постраничной навигацией и кнопкой "Показать ещё"
  • Адаптивность — учитывает высоту шапки на разных устройствах
  • Надёжность — несколько резервных механизмов на разные случаи
  • Производительность — не тормозит загрузку страницы

Результаты внедрения

После реализации этого решения мы получили:
  • На 13% увеличилось время пребывания в блоге
  • На 12% выросло количество просматриваемых статей за сессию
  • Снизился показатель отказов на страницах блога
  • Увеличилась конверсия в подписку на рассылку

Техническая реализация

Для тех, кто столкнулся со схожей проблемой, вот готовое решение:
На странице блога с материалом размещаем в блок T123 код:
<script>
(function() {
  const KEY = 'u11_blog_scroll_state';

  function extractUid(url) {
    const match = url && url.match(/\/blog\/tpost\/([^\/?#]+)/i);
    return match ? match[1].split('-')[0] : null;
  }

  function saveState(state) {
    try { sessionStorage.setItem(KEY, JSON.stringify(state)); } catch(e) {}
  }
  function readState() {
    try { return JSON.parse(sessionStorage.getItem(KEY)); } catch(e){ return null; }
  }
  function clearState() {
    try { sessionStorage.removeItem(KEY); } catch(e){}
  }

  function findCardByUid(uid) {
    if (!uid) return null;
    const selector = `a[href*="/blog/tpost/${uid}"]`;
    const link = document.querySelector(selector);
    return link ? link.closest('.t-feed__grid-col, .t-col') : null;
  }

  function loadNextPage() {
    const moreButton = document.querySelector('.js-feed-btn-show-more, .t-feed__showmore-btn');
    if (moreButton) { moreButton.click(); return true; }
    return false;
  }

  document.addEventListener('click', function(e) {
    const link = e.target.closest('a[href*="/blog/tpost/"]');
    if (link && !link.hash && link.closest('.js-feed')) {
      const uid = extractUid(link.href);
      if (!uid) return;
      const scrollPosition = window.pageYOffset;
      const card = link.closest('.t-feed__grid-col, .t-col');
      let page = 1;
      if (card) {
        const feedContainer = card.closest('.js-feed-container');
        if (feedContainer) {
          const visibleCards = feedContainer.querySelectorAll('.t-feed__grid-col, .t-col');
          const cardIndex = Array.from(visibleCards).indexOf(card);
          page = Math.floor(cardIndex / 6) + 1;
        }
      }
      saveState({ uid, scroll: scrollPosition, page, timestamp: Date.now(), fromBlog: true });
    }
  });

  function restoreScrollPosition() {
    const state = readState();
    if (!state || !state.uid || !state.fromBlog) return;
    if (Date.now() - state.timestamp > 600000) { clearState(); return; }

    let attempts = 0;
    const maxAttempts = 30;
    let pageLoaded = state.page <= 1;

    const interval = setInterval(() => {
      attempts++;
      if (!pageLoaded) pageLoaded = loadNextPage();

      const card = findCardByUid(state.uid);
      if (card) {
        clearInterval(interval);
        setTimeout(() => {
          const offset = card.getBoundingClientRect().top + window.pageYOffset - 100;
          window.scrollTo({ top: offset, behavior: 'smooth' });
          clearState();
        }, 300);
      } else if (attempts >= maxAttempts) {
        clearInterval(interval);
        if (state.scroll) window.scrollTo({ top: state.scroll, behavior: 'smooth' });
        clearState();
      }
    }, 500);
  }

  if (document.location.pathname === '/blog') {
    setTimeout(restoreScrollPosition, 1000);
  }
})();
</script>
На самом материале блога размещаем в блок T123 код для кнопки "Назад в блог" с url = "#back"
<script>
document.addEventListener('DOMContentLoaded', function() {
  const BLOG_URL = '/blog';
  const KEY = 'u11_blog_scroll_state';

  const backButtons = document.querySelectorAll('a[href="#back"]');
  const state = JSON.parse(sessionStorage.getItem(KEY) || '{}');
  const cameFromBlog = state.fromBlog;

  const cleanUrl = window.location.href.split('#')[0];

  function replaceHistory() {
    if (window.history.replaceState) {
      window.history.replaceState(null, null, cleanUrl);
    }
  }

  replaceHistory();

  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
    anchor.addEventListener('click', function(e) {
      e.preventDefault();
      const targetId = this.getAttribute('href').substring(1);
      const targetEl = document.getElementById(targetId);
      if (targetEl) targetEl.scrollIntoView({ behavior: 'smooth' });
      if (window.history.replaceState) {
        window.history.replaceState(null, null, cleanUrl + '#' + targetId);
      }
    });
  });

  backButtons.forEach(button => {
    button.addEventListener('click', function(e) {
      e.preventDefault();
      if (cameFromBlog) {
        // Если история содержит блог
        if (document.referrer.includes(BLOG_URL) || window.history.length > 1) {
          window.history.back();
        } else {
          window.location.href = BLOG_URL;
        }
      } else {
        window.location.href = BLOG_URL;
      }
    });
  });
});
</script>

Выводы

Инвестиции в удобство навигации окупаются ростом вовлечённости и лояльности пользователей. Даже на платформах с ограниченными возможностями кастомизации, таких как Tilda, можно найти эффективные технические решения для улучшения пользовательского опыта.
Ключевой insight: Недостаточно просто публиковать качественный контент — нужно обеспечить комфортные условия для его потребления. Иногда небольшие технические доработки дают больший эффект, чем месяцы контент-работы.