Cómo agregar dinámicamente etiquetas de anclaje a HTML con JavaScript

Cómo generar dinámicamente e inyectar enlaces ancla en HTML para mejorar la UX (experiencia del usuario) de su blog o aplicación basada en contenido.

Una gran parte del SEO es mejorar la indexabilidad de su sitio y garantizar que su contenido satisfaga las necesidades de la consulta de un usuario. Un poco de UX (experiencia de usuario) que puede agregar, especialmente si está creando contenido de formato largo como un blog, es proporcionar enlaces de anclaje para diferentes secciones de su contenido.

Hacer esto a mano es una tarea ardua, por lo que en este tutorial, aprenderemos cómo atravesar automáticamente algunos HTML, encontrar todas sus etiquetas h1-h6 y actualizarlas automáticamente para incluir un enlace de anclaje (completa con una versión slugified de su texto).

Primeros pasos

Para comenzar, vamos a confiar en CheatCode Next.js Boilerplate para darnos un buen punto de partida. Primero, clone una copia del modelo:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate

Luego, instale las dependencias repetitivas:

Terminal

cd nextjs-boilerplate && npm install

Después de instalar esas dependencias, instale las siguientes dependencias que usaremos más adelante en el tutorial:

Terminal

npm i cheerio commonmark speakingurl

Una vez que estén instalados, continúe y ponga en marcha el modelo estándar:

Terminal

npm run dev

Escribiendo el enlazador ancla

Antes de que realmente "veamos" algo en la pantalla, nos centraremos en la función central que necesitamos para ayudarnos a agregar automáticamente enlaces de anclaje a nuestro contenido. Para comenzar, configuremos una función en /lib/anchorLinker.js donde vivirá nuestro código:

/lib/anchorLinker.js

const anchorLinker = (content = "") => {
  // Our automatic anchor linking will go here.
};

export default anchorLinker;

Simple. Aquí, solo estamos creando un esqueleto para nuestra función, agregando un solo content argumento que esperamos que sea una cadena. El content = "" la sintaxis aquí dice "si no se pasa ningún valor para content , asígnele un valor predeterminado de una cadena vacía".

/lib/anchorLinker.js

import isClient from "./isClient";

const anchorLinker = (content = "") => {
  if (isClient) {
    // Client-side linking will go here.
  }

  // Server-side linking will go here.
};

export default anchorLinker;

A continuación, presentamos un if declaración, comprobando si isClient es verdadero (isClient se agrega como una importación arriba y es una función que se incluye automáticamente en el modelo en /lib/isClient.js ). Hemos agregado esto aquí porque, a pesar de que estamos trabajando con un modelo estándar solo para el front-end, Next.js, el marco sobre el que se construye el modelo estándar, tiene una función de representación del lado del servidor para generar HTML para los motores de búsqueda.

Lo hace a través de una función llamada getServerSideProps() . Esta función se ejecuta cuando llega una solicitud inicial a una aplicación basada en Next.js. Antes de que esa solicitud reciba una respuesta en forma de HTML en el navegador, primero, Next.js llama a getServerSideProps() para ayudar en la obtención de datos y otras tareas del lado del servidor antes devolviendo HTML a la solicitud.

Debido a que esta función se ejecuta en el contexto de un servidor, ciertas API a nivel de navegador (por ejemplo, métodos de manipulación DOM) no están disponibles. Entonces, cuando este código se ejecuta en eso contexto, arroja un error. Para evitar esto, vamos a escribir dos conjuntos de código aquí:una implementación del lado del cliente de nuestro enlazador ancla y una implementación del lado del servidor de nuestro enlazador ancla.

Adición de enlaces de anclaje del lado del cliente

Para el cliente, tenemos acceso total a las API de manipulación de DOM del navegador, por lo que no necesitamos incorporar ninguna dependencia o código especial:

/lib/anchorLinker.js

import isClient from "./isClient";
import parseMarkdown from "./parseMarkdown";

const anchorLinker = (content = "") => {
  if (isClient) {
    const html = document.createElement("div");
    html.innerHTML = parseMarkdown(content);
  }

  // Server-side linking will go here.
};

export default anchorLinker;

Primero, para aislar el HTML generado a partir de nuestro content cadena, usamos el document.createElement() método para crear un <div></div> elemento (en memoria, no representado en pantalla). A continuación, completamos ese <div></div> con el resultado de llamar a parseMarkdown() , pasando nuestro contenido.

Muy rápido, agreguemos esa función para que podamos completar la importación arriba:

/lib/parseMarkdown.js

import { Parser, HtmlRenderer } from "commonmark";

const parseMarkdown = (markdown = "", options) => {
  if (markdown) {
    const reader = new Parser();
    const writer = options ? new HtmlRenderer(options) : new HtmlRenderer();
    const parsed = reader.parse(markdown);
    return writer.render(parsed);
  }

  return "";
};

export default parseMarkdown;

Markdown es un lenguaje abreviado para generar HTML a partir de archivos de texto utilizando una sintaxis especial. Así que podemos evitar tener que escribir un montón de etiquetas HTML para nuestra prueba, usaremos Markdown para generar automáticamente el HTML para nosotros. Aquí, parseMarkdown() es una función que envuelve el commonmark biblioteca. Commonmark es un analizador de Markdown que toma una cadena y la convierte en HTML, según la especificación de Markdown.

Los detalles aquí son limitados ya que solo sigue las instrucciones en el commonmark documentación sobre cómo utilizar el analizador. Para usarlo, creamos una instancia del Parser seguido de la creación de una instancia de HtmlRenderer . Aquí llamamos condicionalmente a new HtmlRenderer en función de si se pasó o no un valor al segundo options argumento de nuestro parseMarkdown función (estas son las opciones para la marca común, si es necesario).

Con nuestro HtmlRenderer configurado y almacenado en el writer variable, a continuación, analizamos nuestro markdown cadena a un DOM virtual (modelo de objeto de documento) y luego use writer.render() para convertir ese DOM en una cadena HTML.

/lib/anchorLinker.js

import cheerio from "cheerio";
import isClient from "./isClient";
import parseMarkdown from "./parseMarkdown";
import getSlug from "./getSlug";

const anchorLinker = (content = "") => {
  if (isClient) {
    const html = document.createElement("div");
    html.innerHTML = parseMarkdown(content);

    const hTags = html.querySelectorAll("h1, h2, h3, h4, h5, h6");

    hTags.forEach((hTag) => {
      const tagContent = hTag.innerHTML;
      const tagSlug = getSlug(tagContent);

      hTag.innerHTML = `<a class="anchor-link" href="#${tagSlug}"><i class="fas fa-link"></i></a> ${tagContent}`;
      hTag.setAttribute("id", tagSlug);
    });

    return html.innerHTML;
  }
};

export default anchorLinker;

Con nuestro Markdown analizado en HTML, ahora podemos entrar en el meollo de este tutorial. De vuelta en nuestro /lib/anchorLinker.js archivo, hemos expandido el if (isClient) bloque de nuestro anchorLinker() función para iniciar el proceso de enlace de anclaje.

Para vincular automáticamente todas las etiquetas h1-h6 en nuestro contenido, necesitamos recuperar esos elementos del <div></div> creamos anteriormente y luego lo llenamos con el resultado de analizar nuestro Markdown a HTML en parseMarkdown() .

Usando html.querySelectorAll("h1, h2, h3, h4, h5, h6") , decimos "ve y consíguenos todas las etiquetas h1-h6 dentro de este HTML". Esto nos devuelve una lista de nodos DOM de JavaScript que contiene todas nuestras etiquetas h1-h6. Con esto, a continuación, llamamos a hTags.forEach() ejecutando un ciclo sobre cada una de las etiquetas h1-h6 descubiertas.

En la devolución de llamada para nuestro forEach() hacemos el trabajo necesario para "autoenlazar" nuestras etiquetas. Para hacerlo, primero, tomamos el contenido no modificado de la etiqueta (este es el texto de la etiqueta, por ejemplo, "Este es un ancla h1" en <h1>This is an h1 anchor</h1> ) a través de hTag.innerHTML donde hTag es la etiqueta actual en el hTags matriz sobre la que estamos recorriendo.

Con ese contenido, a continuación, presentamos una nueva función getSlug() para ayudarnos a crear la versión slugified y segura para URL del contenido de nuestra etiqueta como this-is-an-h1-anchor . Veamos esa función rápidamente y analicemos cómo funciona:

/lib/getSlug.js

import speakingUrl from "speakingurl";

const getSlug = (string = "") => {
  return speakingUrl(string, {
    separator: "-",
    custom: { "'": "" },
  });
};

export default getSlug;

En este archivo, todo lo que estamos haciendo es crear una función contenedora alrededor del speakingurl dependencia que instalamos al comienzo del tutorial. Aquí, speakingUrl() es una función que toma un string y lo convierte a a-hyphenated-slug-like-this . ¡Eso es!

/lib/anchorLinker.js

import cheerio from "cheerio";
import isClient from "./isClient";
import parseMarkdown from "./parseMarkdown";
import getSlug from "./getSlug";

const anchorLinker = (content = "") => {
  if (isClient) {
    const html = document.createElement("div");
    html.innerHTML = parseMarkdown(content);

    const hTags = html.querySelectorAll("h1, h2, h3, h4, h5, h6");

    hTags.forEach((hTag) => {
      const tagContent = hTag.innerHTML;
      const tagSlug = getSlug(tagContent);

      hTag.innerHTML = `<a class="anchor-link" href="#${tagSlug}"><i class="fas fa-link"></i></a> ${tagContent}`;
      hTag.setAttribute("id", tagSlug);
    });

    return html.innerHTML;
  }
};

export default anchorLinker;

Volviendo a nuestro /lib/anchorLinker.js archivo, ahora estamos preparados para crear nuestro enlace ancla. Aquí, tomamos el hTag actual estamos recorriendo y modificando su innerHTML (es decir, el contenido de el hTag , pero no el hTag mismo) para incluir un <a></a> etiqueta envuelta alrededor de un ícono de enlace (tomado de la biblioteca Font Awesome incluida en el modelo de Next.js que estamos usando).

Además de eso, si miramos de cerca, notaremos que para el <a></a> etiqueta que estamos agregando, establecemos el href atributo igual a #${tagSlug} . Esto es importante. Aquí, el # parte de eso es lo que le dice al navegador web que el siguiente texto representa el id de un elemento en la página. Cuando se escribe en la barra de URL, esto hará que el navegador busque un elemento con ese id en la página y desplaza al usuario hacia abajo. Por eso se llama enlace "anclaje":es anclaje la URL a ese punto específico en el contenido.

Para establecer el id , usamos hTag.setAttribute() para establecer el id en el hTag que actualmente estamos recorriendo. Establecemos esto aquí (a diferencia de en el <a></a> etiqueta) porque estamos tratando de anclar al usuario directamente al contenido, no al enlace en sí.

Después de esto, terminamos nuestro if (isClient) bloquear devolviendo html.innerHTML , o nuestro content convertido a HTML y actualizado para incluir nuestras etiquetas de anclaje (lo que mostraremos en la pantalla).

Agregar enlace de anclaje del lado del servidor

Antes de poner esto en práctica, recuerda que antes mencionamos la necesidad de también manejar esta vinculación para la representación del lado del servidor. El concepto aquí es el mismo, pero el método que usaremos para hacerlo es diferente (de nuevo, el entorno del lado del servidor no tener acceso a las API de manipulación de DOM como document.querySelectorAll() o hTag.setAttribute() ).

Para ayudarnos, vamos a confiar en el cheerio dependencia que instalamos al principio de este tutorial. Cheerio es una biblioteca de manipulación DOM compatible con Node.js del lado del servidor. Como ya entendemos la mecánica en juego aquí, agreguemos el código que necesitamos para hacer lo que acabamos de hacer usando cheerio y recorrerlo:

/lib/anchorLinker.js

import cheerio from "cheerio";
import isClient from "./isClient";
import parseMarkdown from "./parseMarkdown";
import getSlug from "./getSlug";

const anchorLinker = (content = "") => {
  if (isClient) {
    [...]

    return html.innerHTML;
  }

  const $ = cheerio.load("<div></div>");
  $("div").html(content);

  const hTags = $("body").find("h1, h2, h3, h4, h5, h6");

  hTags.each(function () {
    const tagContent = $(this).text();
    const tagSlug = getSlug(tagContent);

    $(this).html(
      `<a class="anchor-link" href="#${tagSlug}"><i class="fas fa-link"></i></a> ${tagContent}`
    );
    $(this).attr("id", tagSlug);
  });

  return $("body div").html();
};

export default anchorLinker;

Una vez más, la idea aquí es idéntica a lo que aprendimos anteriormente. La única diferencia real es el medio por el cual estamos implementando el código. Porque nosotros return dentro de nuestro isClient bloque, podemos omitir un else block y simplemente devuelva el código de enlace de anclaje del servidor directamente desde el cuerpo de nuestra función. Esto funciona porque if (isClient) es cierto, cuando JavaScript golpea el return declaración, dejará de evaluar cualquier código más allá de ese punto. Si es false , omitirá ese bloque y pasará a nuestro código del lado del servidor.

Centrándonos en ese código, comenzamos creando nuestro DOM en memoria usando cheerio.load("<div></div>") creando un <div></div> vacío tal como lo hicimos arriba. Almacenamos esto en un $ variable porque cheerio es técnicamente "jQuery para Node.js" (eso está entre comillas porque lo único "jQuery" sobre Cheerio es que su API fue influenciada por jQuery; no estamos usando ningún código jQuery aquí).

Similar al anterior, usamos el $("body") función para decir "busca el body etiqueta dentro del $ DOM que acabamos de generar y luego dentro de ese localice las etiquetas h1-h6". Esto debería resultarle familiar. Esto es idéntico a lo que hicimos con document.querySelectorAll() antes.

A continuación, tomamos nuestras etiquetas y hacemos un bucle sobre ellas. Para cada etiqueta, nuevamente, extraemos el contenido de texto interno de la etiqueta, lo convertimos en un slug con getSlug() y luego inyectar el <a></a> "anclado" etiqueta de nuevo en el hTag y finalmente, configure el id atributo. Lo único que puede ser confuso aquí es el uso de this en lugar de hTag como vimos en nuestro .forEach() bucle en el cliente.

Aquí, this se refiere al contexto actual dentro del cual el hTags.each() loop se está ejecutando (es decir, el elemento actual sobre el que se está ejecutando el bucle). Aunque no podemos verlo, this está siendo ambientada por Cheerio detrás de escena.

Finalmente, inmediatamente después de nuestro .each() bucle, devolvemos el contenido HTML del <div></div> etiqueta que creamos con cheerio.load() .

¡Hecho! Ahora, estamos listos para poner esto en práctica y ver cómo se agregan algunos enlaces de anclaje a nuestro HTML.

Conectando el enlazador de anclaje a HTML

Para demostrar el uso de nuestro nuevo anchorLinker() función, vamos a conectar un componente simple con algo de texto de Markdown que incluye algunas etiquetas h1-h6 entre algunos párrafos de lorem ipsum:

/páginas/index.js

import React from "react";
import anchorLinker from "../lib/anchorLinker";

import StyledIndex from "./index.css";

const paragraphs = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.`;

const testContent = `This is some test content to verify our anchorLinker() is working.

# This is an h1 anchor
${paragraphs}

## This is an h2 anchor
${paragraphs}

### This is an h3 anchor
${paragraphs}

#### This is and h4 anchor
${paragraphs}

##### This is an h5 anchor
${paragraphs}

###### This is an h6 anchor
${paragraphs}
`;

const Index = ({ prop1, prop2 }) => (
  <StyledIndex
    dangerouslySetInnerHTML={{
      __html: anchorLinker(testContent),
    }}
  />
);

Index.propTypes = {};

export default Index;

Aquí, la parte a la que queremos prestar atención es el componente React cerca de la parte inferior del archivo que comienza con const Index = () => {} . Aquí, devolvemos un componente con estilo <StyledIndex /> eso nos ayuda a establecer algunos estilos básicos para nuestro contenido (esto se importa en la parte superior desde ./index.css ). No entraremos en los detalles de los estilos aquí, pero vamos a agregarlos ahora para evitar confusiones:

/páginas/index.css.js

import styled from "styled-components";

export default styled.div`
  .anchor-link {
    color: #aaa;
    font-size: 18px;

    &:hover {
      color: var(--primary);
    }

    .fa-link {
      margin-right: 5px;
    }
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    font-size: 20px;
    margin-bottom: 20px;
  }

  p {
    font-size: 16px;
    line-height: 26px;
    margin-bottom: 40px;
  }
`;

Nota :El .css.js El sufijo en el nombre del archivo aquí es intencional. Estamos creando nuestro CSS utilizando componentes con estilo que se realizan a través de JavaScript y lo nombramos de esta manera para implicar que el contenido del archivo es "CSS escrito en JavaScript".

/páginas/index.js

import React from "react";
import anchorLinker from "../lib/anchorLinker";

import StyledIndex from "./index.css";

const paragraphs = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.`;

const testContent = `This is some test content to verify our anchorLinker() is working.

# This is an h1 anchor
${paragraphs}

[...]
`;

const Index = ({ prop1, prop2 }) => (
  <StyledIndex
    dangerouslySetInnerHTML={{
      __html: anchorLinker(testContent),
    }}
  />
);

Index.propTypes = {};

export default Index;

De vuelta en nuestra prueba <Index /> componente, como accesorio en nuestro <StyledIndex /> componente, establecemos dangerouslySetInnerHTML igual a un objeto con un __html propiedad que contiene el resultado de llamar a nuestro anchorLinker() importado y pasando nuestro testContent cadena (nuestro Markdown sin compilar).

Recuerda, dentro de anchorLinker() , devolvemos una cadena de HTML de las versiones del enlazador tanto del lado del cliente como del lado del servidor. Entonces, cuando eso finalmente regrese, aquí, tomamos esa cadena HTML y la configuramos como el contenido del <StyledIndex /> representado. elemento en React.

¿En otras palabras? Esto generará la versión anclada de nuestro HTML en el navegador:

Terminando

En este tutorial, aprendimos cómo generar automáticamente etiquetas de anclaje para nuestro contenido HTML. Aprendimos cómo seleccionar y manipular elementos DOM en la memoria, generando una cadena HTML que contiene nuestros enlaces de anclaje y representándola en el navegador.

También aprendimos cómo utilizar Markdown para generar HTML sobre la marcha para nosotros a través de commonmark así como también cómo generar cadenas slugified con speakingurl .