Implementación de TOC en Nextjs

힘센캥거루
2025년 10월 20일(수정됨)
4
29
Implementación de TOC en Nextjs-1

Hoy decidí crear un TOC que todos tienen.

Actualmente, estoy mostrando artículos escritos en Markdown compilados con next-mdx-remote.

Con rehype-slug, ya se han asignado ID a todas las etiquetas de título, así que la mitad del trabajo está hecho.

Primero, vamos a crear un TOC de forma sencilla con useEffect y querySelectorAll.

1. Crear un TOC de forma sencilla

Implementación de TOC en Nextjs-2

Cuando intentas algo por primera vez y no encuentras el camino, saber cómo hacer preguntas también es una habilidad.

Autodidactas como yo, sin nadie a quien pedir consejo, encontramos que recurrir a grandes mentores como Lütent, Claude o GPT es la forma más rápida.

Usé el código que obtuve de estos grandes mentores, con algunas modificaciones.

"use client";

import { useEffect, useState } from "react";

interface StringKeyValue {
  [key: string]: string;
}

export default function TOC() {
  const [headings, setHeadings] = useState<StringKeyValue[]>([]);
  const [toc, setToc] = useState(false);

  // Ejecuta una vez cuando el componente se carga.
  useEffect(() => {
    // Encuentra las etiquetas de nivel h2, h3 y h4 con querySelectorAll.
    const hTags = document.querySelectorAll<HTMLHeadingElement>("h2, h3, h4");
    // Convierte los elementos en una matriz.
    const headings = [...hTags];
    if (headings.length == 0) {
      return;
    }
    setToc(true);

    // Extrae solo el contenido necesario para crear el TOC.
    const allHeadings = headings.map((heading) => ({
      text: heading.innerText,
      id: heading.id || heading.innerText.replace(/\s+/g, "-").toLowerCase(),
      tagName: heading.tagName,
    }));

    setHeadings(allHeadings);
  }, []);

  return (
    toc && (
      // Calcula la posición del TOC para que esté justo al lado de la publicación.
      <div className="fixed hidden xl:block top-40 w-64 p-4 left-[calc(50%+390px)]">
        <div className="text-2xl mb-4">Contents</div>
        <div className="pl-2">
          <ul className="prose">
            {headings.map((heading) => (
              // Como rara vez llegaremos al nivel h4, solo se configuró la clase hasta h3.
              <li
                key={heading.id}
                className={`${
                  heading.tagName == "H3" && "pl-6"
                } border-l-2 border-gray-200`}
              >
                <a
                  href={`#${heading.id}`}
                  className={`block no-underline pl-2`}
                >
                  // Siempre se agrega un número como 1. 2. al título, así que se elimina con replace y
                  una expresión regular.
                  {heading.text.replace(/^\d+\.\s*/, "")}
                </a>
              </li>
            ))}
          </ul>
        </div>
      </div>
    )
  );
}

Y en globals.css, al agregar la opción de suavizado al scroll, se mejora el desplazamiento.

Como los desplazamientos bruscos no tienen mucho sentido, lo agregué globalmente como si fuera un tipo duro.

@layer base {
    html {
        scroll-behavior: smooth;
    }
}

Probé haciendo clic.

Funciona bien.

Implementación de TOC en Nextjs-3

Ahora vamos a destacar las etiquetas que entren en el viewport utilizando un intersection observer.

2. Intersection Observer

Los parámetros que entran en la función son los siguientes.

const options = {
  root: null, // Valor predeterminado es el viewport. Puede registrar un htmlelement como referencia.
  rootMargin: '0px', // Configuración del margen del viewport.
  threshold: 0.5 // Se llama al callback cuando el 50% entra en el viewport
};

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log(`${entry.target.id} está visible.`);
    } else {
      console.log('${entry.target.id} no está visible.');
    }
  });
}, options);

// Seleccionar elemento a observar
const target = document.querySelector('#target');
observer.observe(target);

UsaRef para crear una variable observador y asígnala al intersection observer.

El código fue referenciado de el velog de ssoon-m.

export default function TOC() {

...
  const observer = useRef<IntersectionObserver>();
  const [activeToc, setActiveToc] = useState("");

  useEffect(() => {

    ...
    // Asignar intersection observer a observer.current
    observer.current = new IntersectionObserver(
      (entries) => {
        // Registrar el id del elemento en interacción como activeToc
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          setActiveToc(entry.target.id);
        });
      },
      {
        // Observar desde el 70% por debajo del punto más alto hasta el más bajo
        rootMargin: "0px 0px -70% 0px",
        threshold: 0.8,
      }
    );

    ...
    const allHeadings = headings.map((heading) => {
        // Observar el elemento con intersection observer.
      observer.current?.observe(heading);
    ...
    });

    ...
    // Llamar una función de limpieza al regresar una función anónima en return.
    return () => observer.current?.disconnect();
  }, []);

  return (
    toc && (
        ...
        // Asignar clase para resaltar el título actual
            {headings.map((heading) => (
              <li
                key={heading.id}
                className={`${
                  heading.tagName === "H3" && "pl-6"
                } ${activeToc == heading.id ? "border-l-4 border-gray-500":"border-l-2 border-gray-200"} `}
              >
                <a
                  href={`#${heading.id}`}
                  className={`block no-underline pl-2 ${
                    activeToc == heading.id ? "text-black font-black" : "text-gray-500"
                  }`}
                >
        ...
  );
}

Luego de hacer esto, lo probé deslizándome mientras verificaba el TOC.

Responde bien tanto a los clics como al desplazamiento.

Implementación de TOC en Nextjs-4

3. Conclusión

Quería crear un TOC con SSR, pero al ser reactivo, descubrí que es mejor generarlo en el lado del cliente para mi salud mental.

Ver TOCs en otros blogs me había dado ganas de implementarlo alguna vez, y ahora estoy feliz de haberlo logrado.

Lo siguiente será trabajar en introducir todos los posts en una base de datos.

관련 글

공문서 완전 자동화 해보기 - ChatGPT, Claude, kordoc + python-hwpx
공문서 완전 자동화 해보기 - ChatGPT, Claude, kordoc + python-hwpx
대한민국 공공기관은 여전히 .hwp 확장자를 가진 파일을 많이 사용한다.이 파일은 한글과컴퓨터에서 만든 독자 규격 문서다.문제는 이 형식이 다른 프로그램과 완전히 호환되지 않는다는 데 있다.문서를 열어보는 것은 가능하다.하지만 그 내용을 구조적으로 읽고, 데이터처럼 활...
Relato del desarrollo de un blog full‑stack con Next.js
Relato del desarrollo de un blog full‑stack con Next.js
Alrededor de un año después de empezar con el desarrollo web, empecé a pensar que quería tener mi propio blog.Así que estuve dedicado a esto durante u...
Despliegue sin interrupciones de Nextjs (servidor local) usando Caddy
Despliegue sin interrupciones de Nextjs (servidor local) usando Caddy
Cada vez que me entraban ganas de seguir añadiendo cosas a la página web, hacía un build, y en ese intervalo parece que había gente que se conectaba d...
Automatización de la indexación de búsqueda de Google - Web Search Indexing API
Automatización de la indexación de búsqueda de Google - Web Search Indexing API
Después de IndexNow, decidí probar también la automatización con Google.Buscando un poco, vi que Google ofrecía algo llamado Web Search Indexing a tra...
Automatizar la creación de índices de búsqueda - IndexNow
Automatizar la creación de índices de búsqueda - IndexNow
Mientras estaba registrando el sitio en Bing, me enteré de que Bing ofrece una función llamada IndexNow.Lo esencial es que, usando una clave de API, p...
Experiencia de que hackearan mi servidor Nextjs y React
Experiencia de que hackearan mi servidor Nextjs y React
La primera vez que me encontré con este problema de seguridad fue la madrugada del 5 de diciembre.Que en React era posible ejecutar código de forma re...

댓글을 불러오는 중...