<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Heorhii Lavrynenko</title>
    <description>The latest articles on DEV Community by Heorhii Lavrynenko (@mapalocal).</description>
    <link>https://dev.to/mapalocal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4205307%2Fb7f8d239-c4f7-47b3-b11c-069e0718f5a4.png</url>
      <title>DEV Community: Heorhii Lavrynenko</title>
      <link>https://dev.to/mapalocal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mapalocal"/>
    <language>en</language>
    <item>
      <title>Inspeccionar enlaces externos con JavaScript: URLs, atributos rel y límites del DOM</title>
      <dc:creator>Heorhii Lavrynenko</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:17:08 +0000</pubDate>
      <link>https://dev.to/mapalocal/inspeccionar-enlaces-externos-con-javascript-urls-atributos-rel-y-limites-del-dom-4f30</link>
      <guid>https://dev.to/mapalocal/inspeccionar-enlaces-externos-con-javascript-urls-atributos-rel-y-limites-del-dom-4f30</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Transparencia: este artículo se ha preparado con ayuda de IA para la redacción y los ejemplos. El código de clasificación se ha comprobado con casos sintéticos; no es una auditoría de sitios de terceros.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Al revisar enlaces conviene separar dos preguntas: «¿a dónde enlaza este documento?» y «¿qué documentos de Internet enlazan a mi dominio?». JavaScript en la página puede ayudar con la primera. La segunda necesita un índice de enlaces o un rastreo con otro alcance.&lt;/p&gt;

&lt;p&gt;Este ejemplo es deliberadamente pequeño: recoge enlaces HTTP(S) del documento principal y conserva evidencias, sin asignarles una puntuación SEO.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Definir qué significa externo
&lt;/h2&gt;

&lt;p&gt;Aquí usaremos &lt;strong&gt;hostname diferente al de la página abierta&lt;/strong&gt;. Es una decisión explícita, no una definición universal de sitio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;blog.example.com&lt;/code&gt; es externo respecto a &lt;code&gt;www.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Un puerto diferente con el mismo hostname no es externo para esta regla.&lt;/li&gt;
&lt;li&gt;No eliminamos &lt;code&gt;www&lt;/code&gt; ni calculamos el dominio registrable. Agrupar subdominios necesitaría otra política y, para hacerlo correctamente, una lista de sufijos públicos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No basta con buscar si un &lt;code&gt;href&lt;/code&gt; contiene el nombre del dominio: &lt;code&gt;example.com.evil.test&lt;/code&gt; y &lt;code&gt;evil.test/?next=example.com&lt;/code&gt; darían falsos resultados.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Resolver la URL y conservar los atributos
&lt;/h2&gt;

&lt;p&gt;La función recibe la base de resolución y el hostname de comparación por separado. Un &lt;code&gt;&amp;lt;base href&amp;gt;&lt;/code&gt; puede cambiar el destino de un enlace relativo, pero no debería cambiar qué página estamos inspeccionando.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;externalLinkRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawHref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseURI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageHostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rawHref&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rawHref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawHref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseURI&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;pageHostname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\t\n\f\r&lt;/span&gt;&lt;span class="sr"&gt; &lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nofollow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nofollow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;sponsored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sponsored&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;ugc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ugc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;new URL()&lt;/code&gt; resuelve referencias relativas y rechaza entradas no válidas. Filtrar el protocolo evita mezclar enlaces web con &lt;code&gt;mailto:&lt;/code&gt;, &lt;code&gt;tel:&lt;/code&gt; o &lt;code&gt;javascript:&lt;/code&gt;. Los atributos se guardan como señales independientes: un enlace puede tener &lt;code&gt;sponsored&lt;/code&gt; y &lt;code&gt;nofollow&lt;/code&gt; simultáneamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Obtener una instantánea del documento
&lt;/h2&gt;

&lt;p&gt;Este segundo bloque utiliza la función anterior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;externalLinkRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseURI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;occurrences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;uniqueURLs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;
&lt;span class="p"&gt;})));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí &lt;code&gt;anchor&lt;/code&gt; significa texto del nodo, no nombre accesible. Un enlace que solo contiene una imagen puede tener &lt;code&gt;anchor&lt;/code&gt; vacío y seguir siendo perfectamente utilizable. Tampoco filtramos por visibilidad: &lt;code&gt;textContent&lt;/code&gt; puede incluir texto oculto.&lt;/p&gt;

&lt;p&gt;El total cuenta &lt;strong&gt;apariciones&lt;/strong&gt;, mientras que &lt;code&gt;uniqueURLs&lt;/code&gt; cuenta URLs serializadas distintas. No eliminamos fragmentos ni parámetros: decidir que dos destinos son equivalentes requiere una regla adicional. Repetir el mismo destino en el menú y el pie de página no demuestra un problema.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Casos pequeños que conviene probar
&lt;/h2&gt;

&lt;p&gt;Con base &lt;code&gt;https://example.com/docs/&lt;/code&gt; y hostname &lt;code&gt;example.com&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entrada&lt;/th&gt;
&lt;th&gt;Resultado esperado&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/contacto/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Se excluye: mismo hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://example.com:8443/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Se excluye con esta regla&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//other.test/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Se incluye como HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://blog.example.com/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Se incluye: subdominio distinto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://example.com.evil.test/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Se incluye: otro hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mailto:hello@example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Se excluye por protocolo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http://[&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Se excluye: URL no válida&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Añade una prueba con &lt;code&gt;rel="UGC nofollow sponsored"&lt;/code&gt;: las tres señales deben resultar verdaderas. Y prueba una base externa: un enlace relativo resuelto contra ella puede ser externo aunque su texto original no contenga un dominio.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Lo que este resultado no demuestra
&lt;/h2&gt;

&lt;p&gt;La consulta devuelve una instantánea: los enlaces añadidos después por JavaScript requieren otra lectura. No entra en iframes ni atraviesa Shadow DOM. No extrae enlaces de botones, manejadores de eventos ni todo el historial de navegación de una aplicación.&lt;/p&gt;

&lt;p&gt;Tampoco realiza solicitudes HTTP, sigue redirecciones, comprueba indexación o descubre backlinks. Un enlace sin esos tres atributos &lt;code&gt;rel&lt;/code&gt; no garantiza transferencia de PageRank. Una señal aislada no sustituye la revisión del destino y del contexto.&lt;/p&gt;

&lt;p&gt;Si amplías el ejemplo con CSV, trata las celdas como datos no confiables: las hojas de cálculo pueden interpretar texto que empiece por ciertos caracteres como fórmulas. Entrecomillar valores no elimina por sí solo ese riesgo. Este ejemplo se queda en &lt;code&gt;console.table&lt;/code&gt; para mantener el alcance visible.&lt;/p&gt;

&lt;p&gt;La parte útil de una herramienta de inspección empieza en su contrato: qué documento lee, cómo decide qué es externo y qué conclusiones deja deliberadamente fuera.&lt;/p&gt;

&lt;p&gt;Referencias: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL" rel="noopener noreferrer"&gt;constructor URL en MDN&lt;/a&gt; y &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement/hostname" rel="noopener noreferrer"&gt;hostname de los enlaces en MDN&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Performance-first WordPress for a real business site: decisions that helped us reach 98 on mobile</title>
      <dc:creator>Heorhii Lavrynenko</dc:creator>
      <pubDate>Sat, 24 Oct 2026 18:48:13 +0000</pubDate>
      <link>https://dev.to/mapalocal/performance-first-wordpress-for-a-real-business-site-decisions-that-helped-us-reach-98-on-mobile-1bhp</link>
      <guid>https://dev.to/mapalocal/performance-first-wordpress-for-a-real-business-site-decisions-that-helped-us-reach-98-on-mobile-1bhp</guid>
      <description>&lt;p&gt;Editorial note: publish as a technical story. Keep the single homepage benchmark in scope, rather than implying site-wide 98 scores. If DEV requests a canonical for a close duplicate, set it to the case URL; this is a distinct English adaptation.&lt;/p&gt;

&lt;p&gt;I recently worked on the website for a small digital-marketing project in Spain. It needed the things most business websites need: service landing pages, an editorial section, contact forms, analytics, a consent interface, and room to grow.&lt;/p&gt;

&lt;p&gt;We used WordPress, Kadence, and the Gutenberg editor. One of my goals was to avoid turning performance into a separate rescue project after the design was finished.&lt;/p&gt;

&lt;p&gt;The homepage eventually recorded a 98/100 mobile Performance score and 98/100 desktop score in PageSpeed Insights on October 22, 2026. GTmetrix rated it A in a separate test.&lt;/p&gt;

&lt;p&gt;Here are the choices I think mattered most.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A familiar theme, but controlled components
&lt;/h3&gt;

&lt;p&gt;We did not build a CMS from scratch. Kadence gave us the foundation, and Gutenberg made content editing practical.&lt;/p&gt;

&lt;p&gt;For shared interface elements such as the header and footer, we used small custom modules rather than introducing an all-purpose visual builder for every detail.&lt;/p&gt;

&lt;p&gt;We also reused page patterns for services, sectors, and locations without making every landing page identical. The same structure can support very different content, but each page should still answer its own question.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CSS before another JavaScript dependency
&lt;/h3&gt;

&lt;p&gt;Menus, forms, consent, and some components require JavaScript. That is fine.&lt;/p&gt;

&lt;p&gt;For animations and responsive layout, though, I preferred native CSS whenever it could do the job. I wanted to avoid adding a whole library for a small visual effect.&lt;/p&gt;

&lt;p&gt;The important question was not "Can I remove every script?" It was "What work does this script do, and does every page need it?"&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Images are a layout and loading problem, not just a format problem
&lt;/h3&gt;

&lt;p&gt;We prepared most content images in WebP and used different hero assets for mobile and desktop where needed.&lt;/p&gt;

&lt;p&gt;Above-the-fold imagery received a different loading priority from images lower down the page.&lt;/p&gt;

&lt;p&gt;The goal was not just a smaller file. It was to give the browser the right asset early enough for the first screen to make sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Self-hosted fonts
&lt;/h3&gt;

&lt;p&gt;We used locally hosted Inter WOFF2 files, with font-display: swap and a priority for the font needed early in the page.&lt;/p&gt;

&lt;p&gt;Fonts are easy to ignore in performance discussions, but their loading behavior affects how the first content appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Third-party tags need deliberate placement
&lt;/h3&gt;

&lt;p&gt;Business sites need measurement. Ours also needed Google Tag Manager and a consent flow.&lt;/p&gt;

&lt;p&gt;We separated early consent initialization from later analytics loading and controlled how third-party code entered the page.&lt;/p&gt;

&lt;p&gt;This comes with a trade-off: delaying tracking can affect the capture of short visits and early interactions. That is why I treat measurement integrity as part of the performance work, not something to sacrifice for the score.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results and context
&lt;/h3&gt;

&lt;p&gt;The homepage test on October 22 showed 98/100 in mobile Performance, 98/100 in desktop Performance, and 100/100 in the Lighthouse SEO category. GTmetrix gave the homepage an A grade in its October 20 report.&lt;/p&gt;

&lt;p&gt;Those are snapshots of a specific page, not a universal score for every URL or real-user metric.&lt;/p&gt;

&lt;p&gt;Separately, Google Search Console recorded 106 organic clicks and 14,785 impressions between September 17 and October 20. We also worked on the site's content and SEO architecture, so I would not attribute that search growth to speed alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd repeat
&lt;/h3&gt;

&lt;p&gt;Start with the actual page types and the user journey. Define the components you need, think about loading priorities early, and measure the resulting pages.&lt;/p&gt;

&lt;p&gt;It's much easier to stay disciplined with a WordPress site when performance is part of the design process rather than a late plugin-shopping exercise.&lt;/p&gt;

&lt;p&gt;The full case, including screenshots and the Spanish write-up, is here:&lt;br&gt;
&lt;a href="https://mapalocal.es/casos-de-exito/mapalocal-wordpress-pagespeed-98/" rel="noopener noreferrer"&gt;https://mapalocal.es/casos-de-exito/mapalocal-wordpress-pagespeed-98/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm curious how other WordPress developers balance analytics and consent requirements with fast initial rendering.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>performance</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
