Tabla de contenidos Astro

Crear Tabla de Contenidos en Astro de forma simple


Como ya comentamos en nuestro último post, Astro se está convirtiendo en el favorito de muchos desarrolladores. Por ello queremos elaborar contenido que os ayuden a integraros y desarrollar vuestros nuevos proyectos en este framework. Una de las características que más nos gusta de Astro es que soporta de forma nativa Markdown y MDX (Markdown + JSX). Esto ayuda enormemente a tener una buena base de desarrollo a las personas que se centran en proyectos tipo blog o centrado mayormente en contenido estático. Así que en este post os mostraremos cómo crear una tabla de contenidos en Astro sin hacer uso de plugins como remark-toc.

Obtener información de los encabezados

En el ejemplo que vamos a usar para implementar la tabla de contenidos usamos las Astro Content Collections, que según Astro es “la mejor manera de administrar y crear contenido en cualquier proyecto de Astro”. Básicamente te ayuda a organizar el contenido markdown siguiendo una estructura de carpetas dentro de src/content.

src/pages/blog/[...slug].astro
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
import type { MarkdownHeading } from 'astro';

export async function getStaticPaths() {
	const posts = await getCollection('blog');

	const headings = await Promise.all(
		posts.map(async (post) => {
			const data = await post.render();
			return data.headings;
		})
	);

	return posts.map((post, index) => ({
		params: { slug: post.slug },
		props: {post, headings: headings[index]},
	}));
}

type Props = {
  post: CollectionEntry<'blog'>;
  headings: MarkdownHeading[];
};
const { post, headings } = Astro.props;
const { Content } = await post.render();
---

<BlogPost headings={headings} {...post.data} >
	<Content />
</BlogPost>

Este archivo es el [...slug].astro que muestra los post según las rutas dinámicas definidas en la función de Astro getStaticPaths. Hay mucho que explicar aquí, pero centrémonos en los encabezados. Dentro de la función extraemos los headings, una lista de objetos que representan los encabezados y nos aportan información útil como la profundidad (si es h1, h2, etc.), el slug (para crear los enlaces de la tabla de contenidos) y el texto (simplemente el texto literal del encabezado respetando el formato).

Como getStaticPaths se ejecuta, por así decirlo, previamente a la renderización del componente, devolvemos las props en las cuales incluimos los headings y podemos obtener está información en el mismo componente mediante Astro.props. Ya en la plantilla del componente pasamos estos headings al layout BlogPost que engloba al contenido.

Mostrar la tabla de contenidos

Ahora tenemos que renderizar la tabla de contenidos en el post, por lo que creamos el componente src/components/TableOfContents.astro.

src/components/TableOfContents.astro
---
import type { MarkdownHeading } from 'astro';

interface Props {
  headings: MarkdownHeading[];
}

const { headings } = Astro.props;

const filteredHeadings = headings.filter((heading) => heading.depth <= 2);
---

<nav>
  <ul>
    {
      filteredHeadings.map((heading) => (
        <li>
          <a href={`#${heading.slug}`}>{heading.text}</a>
        </li>
      ))
    }
  </ul>
</nav>

Este componente recibirá los encabezados desde el layout. Seguidamente, le realizamos un filtrado a estos encabezados para quedarnos solo con los de profundidad 2 o menor, es decir, tomar todos los h1 y h2. Para no complicarnos mucho creamos una lista html básica a partir de la lista de encabezados en la que el href toma el heading.slug para crear el hiperenlace interno en la misma página.

Por último, debemos renderizar TableOfContents.astro dentro del post en un lugar a nuestra elección. Obtenemos los headings como props y renderizamos la tabla de contenidos pasándoselos como props. Además, validamos para que solo se muestre la tabla de contenidos si existe algún encabezado.

src/layouts/BlogPost.astro
---
// Resto de js/ts
type Props = CollectionEntry<'blog'>['data'] & { headings: MarkdownHeading[] }
const { title, description, pubDate, updatedDate, heroImage, labels, headings } = Astro.props
---
<!-- Resto de la plantilla -->
{headings && <TableOfContents headings={headings} />}
<!-- Resto de la plantilla -->

Cómo se vería la tabla de contenidos

Finalmente, ya podemos ver el resultado y así quedaría nuestra tabla de contenidos (el ejemplo es de este mismo artículo):

Tabla de contenidos de CodeWebNow

astro

Comparte el post y ayúdanos

También te puede interesar