Рынок найма в 2026 году работает примерно так: отправляешь резюме, получаешь автоответ от ATS, потом тишина. Зато у тебя теперь много свободного времени и самое время сделать себе сайт-портфолио. Пусть рекрутер хотя бы красивую 404 получит, когда перейдёт по ссылке.

Есть классический паттерн для лендингов и портфолио: одна страница, несколько секций, навигация в шапке. Кликаешь «About» и страница плавно скроллит вниз. Скроллишь сам, а активный пункт меню переключается. И в адресной строке всё время честный URL: /, /about, /contact.

Выглядит просто, но реализация с нуля неприятна. Нужно отслеживать видимость секций через IntersectionObserver, обновлять URL без перезагрузки, не допустить петли обратной связи когда смена URL снова триггерит скролл, который снова меняет URL… Это решаемо, но скучно.

Покажу как сделать это быстро с помощью хука useAnchorObserver из библиотеки react-use-observer-hooks.

react-use-observer-hooks-demo-800
react-use-observer-hooks-demo-800

Вся механика сводится к двум направлениям:

Скролл → URL. Когда пользователь скроллит страницу, нужно определить какая секция сейчас в зоне видимости и обновить URL.
URL → скролл. Когда URL меняется (клик по ссылке, кнопка «назад» в браузере), нужно проскроллить к нужной секции.
useAnchorObserver делает и то, и другое, достаточно передать ему список якорей и текущий URL.

Настройка проекта

Для демо используем Next.js 15 с Pages Router, React 19 и Tailwind CSS v4. Устанавливаем библиотеку npm install react-use-observer-hooks

Отключаем встроенное восстановление скролла

Next.js по умолчанию сам управляет позицией скролла при навигации, запоминает её и восстанавливает при переходе назад. Для обычных сайтов это удобно, но для нашего случая это конфликт: хук тоже управляет скроллом, и они будут мешать друг другу. Отключаем в next.config.ts:

const nextConfig = {
  experimental: {
    scrollRestoration: false,
  },
};

После этого скроллом при навигации занимается только useAnchorObserver.

Роутинг

Нам нужно, чтобы три разных URL: /, /about, /contact, открывали одну и ту же страницу. Для этого в Pages Router есть catch-all роут:

  [[...slug]].tsx   ← перехватывает / и /about и /contact

Код страницы

import { useAnchorObserver } from 'react-use-observer-hooks';
import { useRouter } from 'next/router';

const SECTIONS = [
  { href: '/',        label: 'Home',    Component: HomeSection },
  { href: '/about',   label: 'About',   Component: AboutSection },
  { href: '/contact', label: 'Join Us', Component: ContactSection },
];

export default function Page() {
  const router = useRouter();

  const { ref, focusedAnchor } = useAnchorObserver<HTMLDivElement>({
    anchors: SECTIONS.map(s => s.href),
    currentAnchor: router.asPath,
    onAnchorChange: (anchor) => {
      router.push(anchor, undefined, { scroll: false, shallow: true });
    },
  });

  return (
    <>
      <Navbar sections={SECTIONS} activeHref={focusedAnchor} />

      <div ref={ref}>
        {SECTIONS.map(({ href, Component }) => (
          <section key={href} style={{ minHeight: '100vh' }}>
            <Component />
          </section>
        ))}
      </div>
    </>
  );
}

Несколько важных моментов:
ref вешается на враппер, не на секции. Хук смотрит на прямых потомков враппера и сопоставляет их с массивом anchors по индексу: children[0]/, children[1]/about и т.д.
scroll: false в router.push обязателен. Без него Next.js сам проскроллит страницу наверх при смене URL, и возникнет конфликт с хуком.
shallow: true ускоряет навигацию и Next.js не будет заново запускать getServerSideProps / getStaticProps при смене URL.

Навигация в шапке

Ссылки в навбаре это обычный <Link>, но тоже с scroll={false}:

import Link from 'next/link';

export const Navbar = ({ sections, activeHref }) => (
  <nav>
    {sections.map(({ href, label }) => (
      <Link
        key={href}
        href={href}
        scroll={false}
        shallow
        className={activeHref === href ? 'active' : ''}
      >
        {label}
      </Link>
    ))}
  </nav>
);

focusedAnchor из хука всегда содержит якорь видимой в данный момент секции, передаём его в навбар как activeHref и получаем подсветку активного пункта бесплатно.

Что получается в итоге

Минимум кода и сайт ведёт себя «правильно»:

  • Прямая ссылка на /about откроет страницу сразу в нужной секции

  • Кнопка «назад» в браузере вернёт к предыдущей секции

  • Поисковики видят разные URL для разного контента

  • Активный пункт меню всегда соответствует тому, что видно на экране

Живое демо: react-use-observer-hooks-demo.vercel.app
Библиотека: github.com/d42f/react-use-observer-hooks
Если библиотека пригодилась то звёздочка на GitHub будет приятна ⭐