Заголовки страниц

Управление заголовками страниц (document.title) — важная часть клиентской маршрутизации. В одностраничных приложениях (SPA) изменение URL не приводит к перезагрузке страницы, поэтому обновление заголовка должно выполняться вручную при каждом переходе по маршруту.

Базовый подход к установке заголовка

В Navigo маршруты описываются через обработчики, внутри которых можно напрямую изменять заголовок:

import Navigo from 'navigo';

const router = new Navigo('/');

router
  .on({
    '/': () => {
      document.title = 'Главная';
    },
    '/about': () => {
      document.title = 'О нас';
    },
    '/contact': () => {
      document.title = 'Контакты';
    }
  })
  .resolve();

Каждый обработчик отвечает не только за отображение контента, но и за установку соответствующего заголовка.

Централизация логики заголовков

При увеличении количества маршрутов дублирование логики становится проблемой. Более масштабируемый подход — вынести заголовки в отдельную структуру:

const routes = {
  '/': {
    title: 'Главная',
    handler: () => {
      // логика отображения
    }
  },
  '/about': {
    title: 'О нас',
    handler: () => {}
  }
};

Инициализация маршрутов:

Object.keys(routes).forEach(path => {
  router.on(path, () => {
    document.title = routes[path].title;
    routes[path].handler();
  });
});

Преимущества:

  • централизованное хранение данных
  • упрощение поддержки
  • удобство масштабирования

Динамические заголовки

Часто требуется формировать заголовок на основе параметров маршрута:

router.on('/user/:id', ({ data }) => {
  const userId = data.id;
  document.title = `Пользователь #${userId}`;
});

В более сложных случаях заголовок зависит от данных, полученных с сервера:

router.on('/post/:id', async ({ data }) => {
  const response = await fetch(`/api/posts/${data.id}`);
  const post = await response.json();

  document.title = post.title;
});

Использование промежуточного обработчика (middleware)

Для унификации логики можно реализовать собственный слой обработки:

function withTitle(title, handler) {
  return (...args) => {
    document.title = title;
    handler(...args);
  };
}

router.on({
  '/': withTitle('Главная', () => {}),
  '/about': withTitle('О нас', () => {})
});

Такой подход:

  • уменьшает дублирование
  • отделяет логику заголовков от бизнес-логики

Глобальный обработчик маршрутов

Navigo позволяет использовать общий обработчик, который выполняется при каждом переходе:

router.hooks({
  after: (match) => {
    if (match.route && match.route.name) {
      document.title = match.route.name;
    }
  }
});

Маршруты с именами:

router.on({
  '/': { uses: () => {}, as: 'Главная' },
  '/about': { uses: () => {}, as: 'О нас' }
});

Здесь свойство as используется как источник заголовка.

Шаблоны заголовков

Для поддержания единообразия используется шаблон:

function setTitle(pageTitle) {
  const base = 'Моё приложение';
  document.title = pageTitle ? `${pageTitle} — ${base}` : base;
}

Использование:

router.on('/about', () => {
  setTitle('О нас');
});

Это обеспечивает:

  • единый стиль заголовков
  • улучшенную SEO-структуру
  • удобство изменения базового названия

Асинхронные переходы и состояние заголовка

Проблема: при асинхронной загрузке данных заголовок может устанавливаться с задержкой.

Решение — установка временного заголовка:

router.on('/post/:id', async ({ data }) => {
  document.title = 'Загрузка...';

  const response = await fetch(`/api/posts/${data.id}`);
  const post = await response.json();

  document.title = post.title;
});

Обработка ошибок

При ошибках маршрутизации важно корректно обновлять заголовок:

router.notFound(() => {
  document.title = 'Страница не найдена';
});

Или при ошибке загрузки данных:

router.on('/post/:id', async ({ data }) => {
  try {
    const response = await fetch(`/api/posts/${data.id}`);
    if (!response.ok) throw new Error();

    const post = await response.json();
    document.title = post.title;
  } catch {
    document.title = 'Ошибка загрузки';
  }
});

Интеграция с мета-тегами

Помимо document.title, часто требуется обновлять мета-теги:

function setMeta(name, content) {
  let tag = document.querySelector(`meta[name="${name}"]`);
  
  if (!tag) {
    tag = document.createElement('meta');
    tag.setAttribute('name', name);
    document.head.appendChild(tag);
  }

  tag.setAttribute('content', content);
}

Использование:

router.on('/about', () => {
  document.title = 'О нас';
  setMeta('description', 'Информация о компании');
});

Организация в крупных проектах

В больших приложениях применяется модульная структура:

// titleManager.js
export function updateTitle(route) {
  const titles = {
    home: 'Главная',
    about: 'О нас'
  };

  document.title = titles[route] || 'Приложение';
}
// router.js
import { updateTitle } from './titleManager';

router.on({
  '/': () => updateTitle('home'),
  '/about': () => updateTitle('about')
});

Практические рекомендации

  • Заголовок должен точно отражать содержимое страницы
  • Использование шаблонов повышает единообразие
  • Асинхронные операции требуют промежуточных состояний
  • Централизация логики упрощает поддержку
  • Ошибки и fallback-сценарии должны обрабатываться явно

Частые ошибки

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

Расширение через пользовательские обёртки

Создание универсального обработчика:

function createRoute({ title, action }) {
  return async (params) => {
    document.title = 'Загрузка...';

    const result = await action(params);

    document.title = title(result);
  };
}

Пример использования:

router.on('/post/:id', createRoute({
  title: (post) => post.title,
  action: async ({ data }) => {
    const res = await fetch(`/api/posts/${data.id}`);
    return res.json();
  }
}));

Такой подход:

  • объединяет асинхронную логику и управление заголовком
  • повышает читаемость
  • снижает вероятность ошибок