<?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: Nooralto1</title>
    <description>The latest articles on DEV Community by Nooralto1 (@nooralto1).</description>
    <link>https://dev.to/nooralto1</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%2F4142070%2Fcb914660-6c73-45a4-8446-f905969c8943.png</url>
      <title>DEV Community: Nooralto1</title>
      <link>https://dev.to/nooralto1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nooralto1"/>
    <language>en</language>
    <item>
      <title>Forms that lose leads silently</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:22:14 +0000</pubDate>
      <link>https://dev.to/nooralto1/forms-that-lose-leads-silently-4lok</link>
      <guid>https://dev.to/nooralto1/forms-that-lose-leads-silently-4lok</guid>
      <description>&lt;h1&gt;
  
  
  Forms that lose leads silently
&lt;/h1&gt;

&lt;p&gt;A contact form can look perfect and still throw away every lead that comes through it. The visitor&lt;br&gt;
fills the fields, clicks submit, sees a thank you message, and leaves satisfied. Nobody on either&lt;br&gt;
side knows the message never arrived. This is a different failure than the one everyone talks about,&lt;br&gt;
the confusing layout or the field that asks for too much. Here the form works, in the sense that it&lt;br&gt;
renders, accepts input, and reports success. It just doesn't deliver.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thank you page lied
&lt;/h2&gt;

&lt;p&gt;Most contact forms do two separate things: they confirm the submission on the client, and they try&lt;br&gt;
to send an email on the server. Those two steps are not the same operation, and a lot of&lt;br&gt;
implementations treat them as if they were. The browser gets a 200 response and shows the success&lt;br&gt;
state the moment the server accepts the request, not once the mail has actually gone out. If the&lt;br&gt;
mail step then fails, because an API key expired, a transactional email provider changed its&lt;br&gt;
authentication requirements, or a PHP &lt;code&gt;mail()&lt;/code&gt; call silently returns false on a server that never&lt;br&gt;
had a real MTA configured, nothing tells the visitor. Nothing tells the business either, unless&lt;br&gt;
someone is watching server logs that nobody watches. The form has told everyone it worked. It hasn't.&lt;/p&gt;

&lt;p&gt;This is worth testing directly rather than trusting the interface: submit the form, then go and&lt;br&gt;
check the inbox it's supposed to reach, not the confirmation screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivered doesn't mean read
&lt;/h2&gt;

&lt;p&gt;Assume the send actually happens. The message can still land somewhere nobody checks. A domain&lt;br&gt;
that sends form notifications without SPF and DKIM records configured gets treated by receiving&lt;br&gt;
mail providers as an unverified sender, and unverified senders get filtered, quietly, without a&lt;br&gt;
bounce. There is no error anywhere in this chain. The form succeeded, the email sent, and it sat in&lt;br&gt;
a spam folder that gets emptied once a month. A business can run for a long stretch believing its&lt;br&gt;
site simply isn't generating inquiries, when in fact it's generating them and losing them at the&lt;br&gt;
mail server. Checking these records takes minutes with any DNS lookup tool. Almost nobody does it&lt;br&gt;
until they've already lost the leads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The button that looks broken but isn't
&lt;/h2&gt;

&lt;p&gt;Client-side validation is supposed to help, catching a malformed email or a missing required field&lt;br&gt;
before the request goes out. It only helps when it's visible. A validation rule that blocks&lt;br&gt;
submission but fails to render any message, because a CSS class didn't load, because a JavaScript&lt;br&gt;
error upstream stopped the error-rendering function from running, or because the error text was&lt;br&gt;
styled the same color as the background, leaves the visitor clicking a button that appears to do&lt;br&gt;
nothing. Most people try twice. Then they assume the site is broken and leave, often without&lt;br&gt;
emailing or calling instead, because a form that seems broken doesn't inspire confidence in the&lt;br&gt;
rest of the site. This failure mode is worse than the mail delivery problem in one respect: it&lt;br&gt;
happens in front of the visitor, and it still generates zero signal on the business side.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the plugin stops working and nobody notices
&lt;/h2&gt;

&lt;p&gt;Sites built on a CMS often hand form handling to a third-party plugin or an embedded widget from&lt;br&gt;
an external service. These integrations update on their own schedule, sometimes automatically, and&lt;br&gt;
an update can change an API contract, deprecate an endpoint, or quietly disable a feature on a&lt;br&gt;
free tier. The widget keeps rendering. The fields keep accepting text. The submit button keeps&lt;br&gt;
being clickable. What stops is the delivery, somewhere behind the interface, with no error surfaced&lt;br&gt;
in the browser and often none in the plugin's own dashboard. Weeks can pass before someone thinks&lt;br&gt;
to test it, usually after noticing that inquiry volume has dropped for no obvious reason. A site&lt;br&gt;
built without dependence on a black-box widget doesn't remove every risk, but it removes this&lt;br&gt;
category of it, and it's one of the reasons a&lt;br&gt;
&lt;a href="https://www.nooralto.com/creation-site-internet/" rel="noopener noreferrer"&gt;custom-built contact flow&lt;/a&gt; is worth the extra&lt;br&gt;
setup over a bundled plugin nobody on the team can inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  A working form and a form that delivers are not the same thing
&lt;/h2&gt;

&lt;p&gt;That's really the point underneath all four failure modes. The interface tells you the form&lt;br&gt;
rendered. It tells you the button was clickable. None of that tells you whether a message reached&lt;br&gt;
an inbox someone actually reads. The only way to know is to submit the form the way a real visitor&lt;br&gt;
would, on a schedule, and check the destination, not the confirmation screen. Set a calendar&lt;br&gt;
reminder, or better, automate a monthly test submission with an alert if the expected email doesn't&lt;br&gt;
land within a few minutes. It costs almost nothing to check. It costs real inquiries not to.&lt;/p&gt;

&lt;p&gt;None of this shows up in a design review. A form can pass every visual and usability check and&lt;br&gt;
still fail at the one job it has. The fix isn't more polish on the front end, it's verifying the&lt;br&gt;
plumbing behind it, on a schedule, the same way anyone would monitor an API endpoint that mattered.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at Nooralto, a web and SEO studio working between Agadir and Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Observability for a small business website</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:16:32 +0000</pubDate>
      <link>https://dev.to/nooralto1/observability-for-a-small-business-website-70l</link>
      <guid>https://dev.to/nooralto1/observability-for-a-small-business-website-70l</guid>
      <description>&lt;h1&gt;
  
  
  Observability for a small business website
&lt;/h1&gt;

&lt;p&gt;A redesign ships, the team moves on to the next project, and the site enters a phase nobody plans&lt;br&gt;
for: the months where nothing about it is actively worked on, but plenty can still quietly break.&lt;br&gt;
Large engineering teams call the discipline that watches for this observability, and treat it as a&lt;br&gt;
first-class part of running a service. A small business site rarely gets the same treatment, not&lt;br&gt;
because the need is different, but because nobody thought to set it up before launch, when it was&lt;br&gt;
cheapest to add.&lt;/p&gt;
&lt;h2&gt;
  
  
  Uptime is the easy part, and the least useful on its own
&lt;/h2&gt;

&lt;p&gt;A basic uptime check, pinging the homepage every few minutes and alerting if it stops responding,&lt;br&gt;
catches the most dramatic failure and almost nothing else. A site can return &lt;code&gt;200 OK&lt;/code&gt; on its&lt;br&gt;
homepage while the booking form on a different page silently stopped submitting three weeks ago,&lt;br&gt;
because a script update broke the endpoint it posts to. Uptime monitoring answers "is the site&lt;br&gt;
technically online," which is a real but narrow question next to "can a visitor actually do the&lt;br&gt;
thing this site exists for."&lt;/p&gt;
&lt;h2&gt;
  
  
  Synthetic checks that test the thing that matters
&lt;/h2&gt;

&lt;p&gt;A synthetic check that actually submits a form, on a schedule, against a test address, and confirms&lt;br&gt;
a confirmation email or a webhook fires within a reasonable window, catches the failure uptime&lt;br&gt;
monitoring misses entirely.&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="c1"&gt;// A minimal synthetic check, run every 15 minutes&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkContactForm&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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://example.com/api/contact&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Synthetic Check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;synthetic-check@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;automated check, ignore&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`contact form returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Running this against the real endpoint, not a staging copy, is what makes it trustworthy: a check&lt;br&gt;
against staging proves staging works, which was never the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Certificate and domain expiry, on a calendar nobody remembers
&lt;/h2&gt;

&lt;p&gt;A TLS certificate or a domain registration expiring is entirely predictable and entirely&lt;br&gt;
preventable, and it is also one of the most common causes of a site going dark with no code change&lt;br&gt;
behind it at all. Auto-renewal handles most of these silently until the one year it does not,&lt;br&gt;
because a payment method expired or a renewal setting changed. A monitor that checks the expiry&lt;br&gt;
date on both, on a weekly schedule, and alerts thirty days out rather than the day of, turns a&lt;br&gt;
guaranteed future incident into a calendar reminder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real user metrics, not just lab scores
&lt;/h2&gt;

&lt;p&gt;A performance audit run once, in a lab environment, on a fast connection, describes a version of&lt;br&gt;
the site nobody's actual visitor experiences. Real user monitoring, collecting Core Web Vitals from&lt;br&gt;
actual visits, catches what a lab test cannot: a specific mobile carrier with slow DNS in one&lt;br&gt;
region, a third-party script that only loads slowly for visitors outside the country the site was&lt;br&gt;
tested from, a page that degrades specifically for the segment of traffic converting best. The gap&lt;br&gt;
between a lab score and what real visitors experience is often where a redesign's promised&lt;br&gt;
improvement quietly evaporates in the weeks after launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually deserves an alert
&lt;/h2&gt;

&lt;p&gt;Monitoring everything sounds safer than monitoring a few things, and it produces the opposite&lt;br&gt;
result: an alert channel nobody reads because most of it is noise, and the one alert that mattered&lt;br&gt;
gets lost in it. A short list, watched consistently, beats a long list ignored after the second&lt;br&gt;
week: the site responds, the primary conversion action (a form, a booking, a checkout) completes&lt;br&gt;
end to end, the certificate has more than thirty days left, and Core Web Vitals for real visitors&lt;br&gt;
have not regressed past a set threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building this in before launch, not after an incident
&lt;/h2&gt;

&lt;p&gt;Observability set up after the first bad month is observability that arrives too late to prevent it,&lt;br&gt;
only to explain it after the fact. &lt;a href="https://www.nooralto.com/refonte-site-internet/" rel="noopener noreferrer"&gt;The redesign work at Nooralto&lt;/a&gt;&lt;br&gt;
treats this monitoring layer as part of the launch checklist rather than a separate project to&lt;br&gt;
revisit someday, because the cost of wiring a synthetic check and a certificate monitor at launch&lt;br&gt;
is a fraction of what a week of silent downtime costs a business that only finds out when a client&lt;br&gt;
mentions the site looked broken.&lt;/p&gt;

&lt;p&gt;A site that nobody is actively watching is not stable. It is untested since the day it shipped, and&lt;br&gt;
the first person to notice a failure is usually a visitor who does not bother reporting it, just&lt;br&gt;
leaves.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nooralto builds websites and runs search optimisation for businesses, from Agadir and Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Migrating from PrestaShop to Shopify without losing search rankings</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:10:50 +0000</pubDate>
      <link>https://dev.to/nooralto1/migrating-from-prestashop-to-shopify-without-losing-search-rankings-55jp</link>
      <guid>https://dev.to/nooralto1/migrating-from-prestashop-to-shopify-without-losing-search-rankings-55jp</guid>
      <description>&lt;h1&gt;
  
  
  Migrating from PrestaShop to Shopify without losing search rankings
&lt;/h1&gt;

&lt;p&gt;A PrestaShop-to-Shopify migration gets pitched, correctly, as an upgrade: less server maintenance,&lt;br&gt;
a checkout that does not need a security patch every quarter, a theme ecosystem that does not&lt;br&gt;
require a developer for every small change. What the pitch usually skips is that the two platforms&lt;br&gt;
structure URLs, categories and product data differently enough that a lift-and-shift migration,&lt;br&gt;
done without mapping the difference deliberately, drops search rankings for weeks or permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The URL structure does not translate on its own
&lt;/h2&gt;

&lt;p&gt;PrestaShop, out of the box or with a common SEO module enabled, tends to produce URLs like&lt;br&gt;
&lt;code&gt;/17-category-name/42-product-name.html&lt;/code&gt;, with numeric identifiers baked in and an &lt;code&gt;.html&lt;/code&gt;&lt;br&gt;
extension. Shopify generates &lt;code&gt;/collections/category-handle&lt;/code&gt; and &lt;code&gt;/products/product-handle&lt;/code&gt;, no&lt;br&gt;
extension, no numeric prefix. There is no automatic way to preserve the old pattern; the platform&lt;br&gt;
dictates the shape.&lt;/p&gt;

&lt;p&gt;That means every single indexed PrestaShop URL needs an explicit redirect to its Shopify&lt;br&gt;
equivalent, not a pattern-based rewrite rule. A generic rule stripping &lt;code&gt;.html&lt;/code&gt; and renumbering&lt;br&gt;
would work for some URLs and silently mismatch others, particularly where French accented&lt;br&gt;
characters in a product name got encoded differently by each platform's slug generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Categories and collections are not the same structure
&lt;/h2&gt;

&lt;p&gt;PrestaShop categories can nest arbitrarily deep, and a product can live in exactly one category by&lt;br&gt;
default. Shopify collections are flatter, and a product can belong to several collections&lt;br&gt;
simultaneously through tags or manual assignment, with no native parent-child nesting shown in the&lt;br&gt;
URL. A PrestaShop store with &lt;code&gt;Home &amp;gt; Kitchen &amp;gt; Cookware &amp;gt; Cast Iron&lt;/code&gt; as a category path has no&lt;br&gt;
direct Shopify equivalent; the migration has to decide whether that becomes one collection, a&lt;br&gt;
collection with a tag filter, or gets flattened into &lt;code&gt;Cookware&lt;/code&gt; with &lt;code&gt;Cast Iron&lt;/code&gt; demoted to a&lt;br&gt;
product tag. This decision changes which URL should receive the redirect, so it has to happen&lt;br&gt;
before the redirect map gets built, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exporting the map, and importing it Shopify's way
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;Old&lt;/span&gt; &lt;span class="k"&gt;PrestaShop&lt;/span&gt; &lt;span class="k"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;New&lt;/span&gt; &lt;span class="k"&gt;Shopify&lt;/span&gt; &lt;span class="k"&gt;URL&lt;/span&gt;
&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;17&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;cookware&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;42&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;iron&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;pan&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="k"&gt;products&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;iron&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;pan&lt;/span&gt;
&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;12&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;kitchen&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="k"&gt;collections&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="k"&gt;kitchen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shopify's admin accepts bulk redirects as a CSV import under URL Redirects, which is the&lt;br&gt;
mechanism to use rather than reproducing an Apache or nginx rewrite layer that Shopify does not&lt;br&gt;
give direct access to. The two-column format above is the whole contract; the discipline is making&lt;br&gt;
sure every row maps to a URL that actually resolves, not to a category that got merged or a product&lt;br&gt;
that got discontinued along the way, in which case that row should point to the closest surviving&lt;br&gt;
collection instead of guessing at a homepage fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured data written by a different hand
&lt;/h2&gt;

&lt;p&gt;PrestaShop's product schema, whether from the core or a module, and Shopify's theme-generated&lt;br&gt;
&lt;code&gt;Product&lt;/code&gt; schema rarely produce identical output. Price formatting, currency codes, and whether&lt;br&gt;
&lt;code&gt;availability&lt;/code&gt; reflects real-time stock or a static value at theme install time can all differ. A&lt;br&gt;
rich result that showed price and stock before the migration can vanish for a week simply because&lt;br&gt;
the new theme's schema block uses a different field name or omits &lt;code&gt;priceCurrency&lt;/code&gt; entirely,&lt;br&gt;
something worth checking directly on a handful of product pages before calling the migration done,&lt;br&gt;
rather than assuming the theme handled it correctly by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Language and multistore setups need their own mapping
&lt;/h2&gt;

&lt;p&gt;PrestaShop's multistore and multilingual features often produce URL patterns with a language or&lt;br&gt;
store prefix that Shopify's own multi-market or multi-language setup structures differently.&lt;br&gt;
Treating this as a detail to sort out after launch is how a bilingual store loses its French&lt;br&gt;
rankings while someone is still debugging why the English ones redirect correctly.&lt;/p&gt;

&lt;p&gt;A store selling into both France and Morocco through separate PrestaShop language trees needs each&lt;br&gt;
tree mapped to its own Shopify market or language variant before launch, with its own redirect rows.&lt;br&gt;
Merging them into a single default market to save setup time is how one language ends up redirecting&lt;br&gt;
correctly while the other quietly loses its indexed pages for weeks, because nobody checked the&lt;br&gt;
smaller of the two traffic sources until the monthly report flagged the drop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually gets checked after cutover
&lt;/h2&gt;

&lt;p&gt;Watching organic traffic in the days after launch answers little on its own, because traffic always&lt;br&gt;
dips during a platform change. What matters is the index coverage report: are old URLs showing as&lt;br&gt;
properly redirected, or as errors. &lt;a href="https://www.nooralto.com/blog/migrer-prestashop-shopify-sans-perdre-seo.html" rel="noopener noreferrer"&gt;The approach this kind of migration follows at Nooralto&lt;/a&gt; treats the&lt;br&gt;
redirect map, not the new theme, as the actual deliverable of the project, because a beautiful new&lt;br&gt;
storefront sitting behind broken redirects is invisible to the search traffic it was supposed to&lt;br&gt;
keep.&lt;/p&gt;

&lt;p&gt;A platform migration is rarely undone by the new platform being worse. It is undone by treating the&lt;br&gt;
gap between two URL structures as an afterthought instead of the center of the project.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nooralto builds websites and runs search optimisation for businesses, from Agadir and Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Structured data for a small hotel site: which schema actually earns its place</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 02:05:08 +0000</pubDate>
      <link>https://dev.to/nooralto1/structured-data-for-a-small-hotel-site-which-schema-actually-earns-its-place-5dg4</link>
      <guid>https://dev.to/nooralto1/structured-data-for-a-small-hotel-site-which-schema-actually-earns-its-place-5dg4</guid>
      <description>&lt;h1&gt;
  
  
  Structured data for a small hotel site: which schema actually earns its place
&lt;/h1&gt;

&lt;p&gt;A hotel site accumulates structured data the way it accumulates plugins: one gets added because a&lt;br&gt;
theme ships it, another because a booking widget injects its own block, a third because someone&lt;br&gt;
once read that schema helps rankings and pasted a generic template without adapting it. The result&lt;br&gt;
is rarely wrong in a way that throws an error. It is wrong in a way that describes a hotel that does&lt;br&gt;
not quite match the one a guest is looking at.&lt;/p&gt;
&lt;h2&gt;
  
  
  Hotel, not just LocalBusiness
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LocalBusiness&lt;/code&gt; validates and renders without complaint on a hotel page, which is exactly the&lt;br&gt;
problem: it is the wrong type, and nothing forces anyone to notice. &lt;code&gt;Hotel&lt;/code&gt; is a more specific type&lt;br&gt;
under the same schema.org tree, and it carries fields a generic business listing does not:&lt;br&gt;
&lt;code&gt;checkinTime&lt;/code&gt;, &lt;code&gt;checkoutTime&lt;/code&gt;, &lt;code&gt;amenityFeature&lt;/code&gt;, &lt;code&gt;starRating&lt;/code&gt; when the property actually holds a&lt;br&gt;
rating from a real authority, &lt;code&gt;petsAllowed&lt;/code&gt;. A validator that only checks for valid JSON will pass&lt;br&gt;
either type. Only a human deciding which type actually describes a hotel with rooms, check-in&lt;br&gt;
hours and amenities will catch the mismatch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hotel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Riad"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkinTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkoutTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amenityFeature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LocationFeatureSpecification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wifi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LocationFeatureSpecification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PostalAddress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"addressLocality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Marrakech"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"addressCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MA"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rooms are a separate entity, not a paragraph
&lt;/h2&gt;

&lt;p&gt;A room described only in prose inside the hotel's &lt;code&gt;description&lt;/code&gt; field gives a crawler nothing to&lt;br&gt;
extract. &lt;code&gt;Room&lt;/code&gt; (or &lt;code&gt;HotelRoom&lt;/code&gt;, depending on how detailed the site needs to be) is its own type,&lt;br&gt;
nested or linked under the hotel, and it is where occupancy, bed configuration and price range&lt;br&gt;
belong. A site with six room types deserves six room entities, not one paragraph that lists them&lt;br&gt;
comma-separated. The nesting also matters for maintenance: a room that gets renamed or retired&lt;br&gt;
should not require editing a block of hand-written prose buried in a JSON string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price ranges, not fabricated numbers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;priceRange&lt;/code&gt; is tempting to fill in with a plausible-looking string and forget. The trouble is that&lt;br&gt;
guests, and increasingly AI answer engines summarising a page, treat that field as current. A&lt;br&gt;
&lt;code&gt;priceRange&lt;/code&gt; left over from a rate that changed two seasons ago is worse than no &lt;code&gt;priceRange&lt;/code&gt; at&lt;br&gt;
all, because it sets an expectation the booking engine will contradict at checkout. If pricing&lt;br&gt;
changes with season or availability and nobody owns updating this field, it is more honest to leave&lt;br&gt;
it out than to ship a number the site itself does not stand behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The widget conflict nobody checks for
&lt;/h2&gt;

&lt;p&gt;A booking engine embedded as a third-party widget frequently injects its own &lt;code&gt;Hotel&lt;/code&gt; or &lt;code&gt;Product&lt;/code&gt;&lt;br&gt;
schema into the page, separate from whatever the site's own template emits. Two blocks describing&lt;br&gt;
the same property, with different check-in times or a different address format, do not cancel each&lt;br&gt;
other out; a parser reads both and has no way to know which one to trust. This is worth an actual&lt;br&gt;
check, not an assumption: view the rendered page source after the widget loads, search for every&lt;br&gt;
&lt;code&gt;application/ld+json&lt;/code&gt; block on the page, and confirm there is exactly one describing the hotel&lt;br&gt;
itself. If there are two, one of them needs to be suppressed, usually by a configuration flag in the&lt;br&gt;
booking widget rather than by fighting the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually pays off
&lt;/h2&gt;

&lt;p&gt;None of this changes how the site looks to a visitor. It changes what shows up as a rich result in&lt;br&gt;
search, and increasingly what an AI system quotes when a traveler asks it to compare a few&lt;br&gt;
properties instead of scrolling a search results page. On &lt;a href="https://www.nooralto.com/site-hotel-reservation-directe/" rel="noopener noreferrer"&gt;a direct-booking hotel site built with this in mind&lt;/a&gt;, the schema is generated&lt;br&gt;
from the same room and rate data the booking engine already reads, rather than typed once by hand&lt;br&gt;
and left to drift the next time a room is renamed or a rate structure changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it like any other output
&lt;/h2&gt;

&lt;p&gt;The same discipline that applies to any structured data applies here: validate the shape before it&lt;br&gt;
ships, not after a rich result silently disappears. A short script that fetches the rendered hotel&lt;br&gt;
page, extracts every &lt;code&gt;ld+json&lt;/code&gt; block, and asserts there is exactly one &lt;code&gt;Hotel&lt;/code&gt; entity with a&lt;br&gt;
&lt;code&gt;checkinTime&lt;/code&gt;, an address and at least one &lt;code&gt;amenityFeature&lt;/code&gt;, catches a broken block on the next&lt;br&gt;
deploy instead of three months later, when someone finally notices the star rating vanished from&lt;br&gt;
search results and has no idea why.&lt;/p&gt;

&lt;p&gt;A small hotel site does not need every schema type schema.org offers. It needs the handful that&lt;br&gt;
actually describe what a guest is trying to confirm before booking: is there a room available for&lt;br&gt;
these dates, what time can they check in, and is the property what the photos suggest it is.&lt;br&gt;
Everything else is noise a crawler has to parse through to find the part that matters.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nooralto builds websites and runs search optimisation for businesses, from Agadir and Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>When a CMS becomes the bottleneck</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 01:59:26 +0000</pubDate>
      <link>https://dev.to/nooralto1/when-a-cms-becomes-the-bottleneck-fbj</link>
      <guid>https://dev.to/nooralto1/when-a-cms-becomes-the-bottleneck-fbj</guid>
      <description>&lt;h1&gt;
  
  
  When a CMS becomes the bottleneck
&lt;/h1&gt;

&lt;p&gt;Every CMS earns its place at some point. Someone picked it because it matched what the business&lt;br&gt;
published back then: a handful of pages, a blog, maybe a product catalog with a dozen fields. The&lt;br&gt;
choice was reasonable. The problem shows up years later, after the business has changed shape&lt;br&gt;
several times and the CMS has been patched to follow, never redesigned to lead.&lt;/p&gt;

&lt;p&gt;The signs are rarely dramatic. Nobody calls a meeting to announce that the content model has&lt;br&gt;
failed. Instead, a marketing coordinator mentions, almost in passing, that she updates the team&lt;br&gt;
page and the "who we are" section separately, because the CMS has no way to say that a person on&lt;br&gt;
the team page is the same person quoted in a case study. Two records, two places to remember, two&lt;br&gt;
chances to drift out of sync. That is not a training problem. That is a schema that was never built&lt;br&gt;
to relate one type of content to another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The friction that actually triggers a rebuild
&lt;/h2&gt;

&lt;p&gt;"The CMS is old" rarely moves a budget. What moves it is someone tracing a delay back to its&lt;br&gt;
source and finding the platform underneath it. A few patterns repeat across almost every migration&lt;br&gt;
project:&lt;/p&gt;

&lt;p&gt;A content type stops matching what the business sells. A services company that used to publish&lt;br&gt;
one kind of page now runs three different sales motions, each needing its own fields, its own&lt;br&gt;
relationships to case studies and pricing, its own approval flow. The CMS still has one generic&lt;br&gt;
"page" type with a rich text field, so editors invent conventions: a bolded line means this, a&lt;br&gt;
specific heading level means that. None of it is enforced. None of it is queryable. It works until&lt;br&gt;
the person who invented the convention leaves.&lt;/p&gt;

&lt;p&gt;A plugin graph turns into a liability. Each plugin solved a real problem when it was installed: a&lt;br&gt;
form builder, a gallery, an SEO panel, a caching layer that only plays well with specific versions&lt;br&gt;
of the others. Updating any one of them means checking whether it breaks the three that depend on&lt;br&gt;
its old behavior. At some point nobody updates anything, and the site runs on a stack that stopped&lt;br&gt;
receiving security patches two major versions ago, because the risk of updating now outweighs the&lt;br&gt;
risk of standing still.&lt;/p&gt;

&lt;p&gt;A simple content change needs a developer. This is the clearest tell. A client asks to change a&lt;br&gt;
phone number in a footer, or reorder three items in a list, and the answer is "let me check with&lt;br&gt;
the dev." Over years of patches, template logic and content ended up tangled in the same file:&lt;br&gt;
conditional blocks that only make sense if you know the history of the site, hardcoded values&lt;br&gt;
sitting next to fields that were supposed to replace them. The people who could safely touch that&lt;br&gt;
code moved on, and everyone left is afraid to.&lt;/p&gt;

&lt;p&gt;Page builders start working against the page. Drag-and-drop editors that felt liberating at launch&lt;br&gt;
accumulate nested rows, columns and widgets that never get cleaned up, because removing a block&lt;br&gt;
someone might still need feels riskier than leaving it. Every new page starts from a slightly&lt;br&gt;
heavier baseline than the last. Load times creep up. The editor who just wants to publish a press&lt;br&gt;
release has to wade through a builder interface that was designed for landing pages, not for a&lt;br&gt;
three-paragraph announcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a migration actually fixes
&lt;/h2&gt;

&lt;p&gt;None of this gets solved by a theme change or a plugin cleanup. It gets solved by deciding, on&lt;br&gt;
paper, what the business actually publishes now, not what it published when the CMS was chosen,&lt;br&gt;
and building a content model around that reality: explicit relationships between content types&lt;br&gt;
where the business needs them, fields that map to how editors already think about the content,&lt;br&gt;
and templates that pull from that structure instead of encoding logic that only one developer&lt;br&gt;
understands. Teams rebuilding around that principle often start with the same conversation used to&lt;br&gt;
scope a full &lt;a href="https://www.nooralto.com/creation-site-internet/" rel="noopener noreferrer"&gt;custom website build&lt;/a&gt;, because&lt;br&gt;
the content model has to be settled before a single template gets written, not patched in after&lt;br&gt;
launch.&lt;/p&gt;

&lt;p&gt;The tell that a migration is overdue is not a design that looks dated. It is a team that has&lt;br&gt;
learned to work around its own publishing tool: duplicate records, invented formatting&lt;br&gt;
conventions, a backlog of "quick changes" that all need a developer, pages that get heavier every&lt;br&gt;
quarter without anyone deciding they should. Those are architectural symptoms, not cosmetic ones,&lt;br&gt;
and no amount of editorial discipline fixes a content model that was never built for what the&lt;br&gt;
business does today.&lt;/p&gt;

&lt;p&gt;Migrating is disruptive enough that most teams put it off until the workaround culture is fully&lt;br&gt;
entrenched. By then the real cost is not the migration itself. It is every month spent maintaining&lt;br&gt;
two versions of the truth because the old system never learned to relate them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The team at Nooralto has walked several clients through exactly this kind of rebuild, from Agadir and from Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What a slow first byte costs you</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 01:59:05 +0000</pubDate>
      <link>https://dev.to/nooralto1/what-a-slow-first-byte-costs-you-4067</link>
      <guid>https://dev.to/nooralto1/what-a-slow-first-byte-costs-you-4067</guid>
      <description>&lt;h1&gt;
  
  
  What a slow first byte costs you
&lt;/h1&gt;

&lt;p&gt;Open the network tab on a page that "feels slow" and the first thing worth looking at is rarely the&lt;br&gt;
JavaScript bundle or the image sizes. It's the gap between the request going out and the first byte&lt;br&gt;
of the response coming back. On a healthy server that gap is a few dozen milliseconds. On a lot of&lt;br&gt;
sites it sits well past a full second, and everything downstream, rendering, hydration, paint,&lt;br&gt;
inherits that delay before a single pixel has been drawn. Time to first byte gets treated as a&lt;br&gt;
footnote in most performance audits. It shouldn't be. It's the one number that a crawler pays for&lt;br&gt;
directly, out of a budget that isn't unlimited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crawl budget is a time budget, not a page count
&lt;/h2&gt;

&lt;p&gt;Googlebot allocates a finite amount of time to a site per crawl session, not a finite number of&lt;br&gt;
URLs. That distinction matters more than most technical SEO checklists let on. A site with fast&lt;br&gt;
responses gets more pages crawled per session simply because each fetch finishes quickly and the&lt;br&gt;
crawler moves to the next URL. A site with a slow origin burns the same time budget on fewer pages,&lt;br&gt;
because every request sits there waiting on TTFB before the crawler has read a single tag of HTML.&lt;/p&gt;

&lt;p&gt;This shows up clearly in server logs once you cross-reference Googlebot's user agent against&lt;br&gt;
response times. Pages behind slow endpoints get crawled less often, and new pages behind a slow&lt;br&gt;
section of the site take longer to get picked up at all, not because they're unimportant, but&lt;br&gt;
because the crawler ran out of time budget before it reached them. A publishing site adding&lt;br&gt;
category or tag pages behind a database-driven archive view will often see exactly this pattern:&lt;br&gt;
the new URLs exist, they're linked internally, and they still take weeks to show up in the index,&lt;br&gt;
because every fetch into that section of the site costs the crawler more time than a fetch anywhere&lt;br&gt;
else.&lt;/p&gt;

&lt;h2&gt;
  
  
  TTFB is upstream of Largest Contentful Paint
&lt;/h2&gt;

&lt;p&gt;Largest Contentful Paint measures when the biggest visible element finishes rendering, and it feeds&lt;br&gt;
directly into Core Web Vitals, which sits inside the ranking systems Google has described as using&lt;br&gt;
page experience signals. TTFB is not LCP, but it's the floor LCP is built on: nothing the browser&lt;br&gt;
does to render content can start until the first byte of the document arrives. A page with a&lt;br&gt;
lightweight hero image and clean CSS can still post a bad LCP score if the server takes 1.8 seconds&lt;br&gt;
to respond, because that 1.8 seconds happens before the browser has anything to paint at all. Teams&lt;br&gt;
that spend weeks compressing images and deferring scripts while ignoring a slow origin are&lt;br&gt;
optimizing the smaller half of the number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the time actually goes
&lt;/h2&gt;

&lt;p&gt;Profiling a slow origin usually turns up one of three patterns, and none of them involve caching.&lt;/p&gt;

&lt;p&gt;An unindexed query is the most common. A page that joins a few tables to build a listing runs fine&lt;br&gt;
in development against a few hundred rows and turns into a multi-second query in production against&lt;br&gt;
a few hundred thousand. &lt;code&gt;EXPLAIN&lt;/code&gt; on the query usually shows a full table scan where an index would&lt;br&gt;
have turned it into a lookup. Nobody notices until the table grows.&lt;/p&gt;

&lt;p&gt;A synchronous third-party call is the second. A page fetching stock levels, shipping rates, or a&lt;br&gt;
loyalty balance from an external API on every request is only as fast as that API's slowest&lt;br&gt;
response, and that response happens inline, before the server can send anything back. When the&lt;br&gt;
third party has a bad day, so does every page that calls it.&lt;/p&gt;

&lt;p&gt;The third is a CMS or framework doing work on every request that belongs at build time. Rendering a&lt;br&gt;
page from a template and a set of content blocks on each hit, recomputing the same navigation tree&lt;br&gt;
or the same related-posts query for every visitor, is expensive in a way that never shows up in a&lt;br&gt;
local dev environment where traffic is one person clicking around. Under real load, that per-request&lt;br&gt;
computation is what an &lt;a href="https://www.nooralto.com/agence-seo/" rel="noopener noreferrer"&gt;SEO audit built around server response time&lt;/a&gt;&lt;br&gt;
tends to surface first, well before it gets anywhere near meta tags or heading structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like from a waterfall
&lt;/h2&gt;

&lt;p&gt;A slow TTFB rarely arrives with a label attached. It shows up as a long blank bar at the start of a&lt;br&gt;
waterfall, before any of the labeled phases begin. The instinct is to look at what comes after that&lt;br&gt;
bar, because that's where the named metrics live. The more useful instinct is to ask what the server&lt;br&gt;
was doing during it: which query ran, which API got called, which computation happened that&lt;br&gt;
shouldn't have needed to happen at all. That's a database and application question before it's a&lt;br&gt;
frontend one, and treating it as one is usually what gets the number down.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at Nooralto, a web and SEO studio based in Agadir and Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A build step that refuses to ship broken links</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:12:19 +0000</pubDate>
      <link>https://dev.to/nooralto1/a-build-step-that-refuses-to-ship-broken-links-c41</link>
      <guid>https://dev.to/nooralto1/a-build-step-that-refuses-to-ship-broken-links-c41</guid>
      <description>&lt;h1&gt;
  
  
  A build step that refuses to ship broken links
&lt;/h1&gt;

&lt;p&gt;Most teams check for broken links the way they check smoke detector batteries: once a quarter, with a tool that crawls the live site and spits out a spreadsheet nobody reads before the next release. By the time the audit runs, the broken link has been live for weeks and possibly already flagged by a user who did not bother reporting it.&lt;/p&gt;

&lt;p&gt;The fix is to move the check earlier and make it a gate instead of a report. Crawl the generated output before it deploys, fail the build on errors that are the site's fault, and warn on the ones that are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crawl the build output, not the live site
&lt;/h2&gt;

&lt;p&gt;Checking the deployed site catches problems after they ship. Checking the static output the build just produced catches them before, and it is faster too: no network latency, no CDN cache to fight, just files on disk.&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="c1"&gt;// scripts/crawl-links.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;existsSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;collectHtmlFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&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;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;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&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;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&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="nf"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isDirectory&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;collectHtmlFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;full&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.html&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="nx"&gt;files&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;full&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractLinksAndIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/href="&lt;/span&gt;&lt;span class="se"&gt;([^&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="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"/g&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;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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;ids&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;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&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;id="&lt;/span&gt;&lt;span class="se"&gt;([^&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="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"/g&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;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ids&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;A regex is enough here. The build output is markup this same pipeline generated, not arbitrary HTML from the web, so the shape of &lt;code&gt;href&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; attributes is known and consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internal links are the site's fault, external links are not
&lt;/h2&gt;

&lt;p&gt;The distinction that matters is who can fix the problem. An internal link pointing at a page that does not exist in the build is a bug in the site: someone renamed a route, deleted a page, or typo'd a path. That should fail the build. An external link returning a 404 might be a bug on someone else's server, or a site that restructured its URLs months ago without telling anyone. Worth knowing, but not a reason to block a deploy that has nothing to do with it.&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;classifyLink&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="nx"&gt;currentFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distRoot&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="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anchor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fragment&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="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&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;test&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;external&lt;/span&gt;&lt;span class="dl"&gt;'&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;href&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;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mailto:&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="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tel:&lt;/span&gt;&lt;span class="dl"&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;skip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pathPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;href&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentFile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;pathPart&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.html&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="nx"&gt;resolved&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&lt;/span&gt;&lt;span class="dl"&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;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fragment&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;Anchors get checked against the target page's own &lt;code&gt;id&lt;/code&gt; set, whether that page is the current one or a different internal page. A link to &lt;code&gt;/pricing#faq&lt;/code&gt; is only correct if &lt;code&gt;/pricing/index.html&lt;/code&gt; actually contains &lt;code&gt;id="faq"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  External checks: cache them, do not hammer someone else's server
&lt;/h2&gt;

&lt;p&gt;Re-fetching every external link on every build is slow and rude. A site with two hundred outbound links does not need two hundred fresh requests for a commit that only fixes a typo in the footer. Cache the result, keyed by URL, and only re-check entries older than a set window.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&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;CACHE_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.link-cache.json&lt;/span&gt;&lt;span class="dl"&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;CACHE_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// one week&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadCache&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="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_PATH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkExternal&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;cache&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;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;CACHE_TTL_MS&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="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;;&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// some servers reject HEAD, treat 405 as inconclusive-pass&lt;/span&gt;
    &lt;span class="nx"&gt;cache&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;checkedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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="nx"&gt;ok&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="nx"&gt;cache&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;checkedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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="kc"&gt;false&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;A &lt;code&gt;HEAD&lt;/code&gt; request is enough to know whether a link resolves, and it costs the remote server far less than a full &lt;code&gt;GET&lt;/code&gt;. Persisting the cache between runs, as a committed file or a CI cache artifact, means a flaky link gets flagged once, not on every commit for a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail on the right category
&lt;/h2&gt;

&lt;p&gt;The crawler walks every page, resolves every link, and separates the two buckets before deciding whether the build lives or dies.&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;let&lt;/span&gt; &lt;span class="nx"&gt;brokenInternal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;brokenExternal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... after walking all files and resolving every link:&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;brokenInternal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;brokenInternal&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; broken internal link(s). Failing build.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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="nx"&gt;brokenExternal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;brokenExternal&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; external link(s) did not resolve. Review before next release.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wired into CI, this runs after the static build and before the deploy step, so a broken internal link never reaches production in the first place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build site&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check links&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/crawl-links.mjs dist&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The team building this kind of pipeline gate for client sites, including the redirect and link checks that go with a redesign, writes about the broader approach at &lt;a href="https://www.nooralto.com/refonte-site-internet/" rel="noopener noreferrer"&gt;nooralto.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Treating link rot as a build-time concern rather than a quarterly chore changes who finds the problem. Instead of a user hitting a dead page, or an audit six weeks late, the commit that introduced the break never merges.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at Nooralto, a web and SEO studio working out of Agadir and Paris.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Nooralto — &lt;a href="https://www.nooralto.com/creation-site-internet/" rel="noopener noreferrer"&gt;Nooralto&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Structured data that survives a redesign</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:06:36 +0000</pubDate>
      <link>https://dev.to/nooralto1/structured-data-that-survives-a-redesign-3pkp</link>
      <guid>https://dev.to/nooralto1/structured-data-that-survives-a-redesign-3pkp</guid>
      <description>&lt;h1&gt;
  
  
  Structured data that survives a redesign
&lt;/h1&gt;

&lt;p&gt;Structured data breaks in a specific, boring way. Someone hand-writes a &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; block in the page template, ships it, and it works. Six months later the site gets a redesign: the breadcrumb component changes, a category gets renamed, the templating engine moves from server includes to a component framework. Nobody touches the JSON-LD block, because nobody remembers it exists. It keeps rendering, keeps validating as syntactically correct JSON, and keeps describing a page that no longer matches what a visitor sees.&lt;/p&gt;

&lt;p&gt;Search Console reports the drift weeks later, if at all, as a drop in a rich result rather than an error. By then it is a mystery to debug instead of a diff to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The block is duplicated data, not markup
&lt;/h2&gt;

&lt;p&gt;The underlying problem is that the JSON-LD is a second, hand-maintained copy of information the page already has. The breadcrumb trail exists once, to render the visible nav. It exists again, retyped, inside the schema block. Two copies drift the moment one of them changes and the other does not get the memo.&lt;/p&gt;

&lt;p&gt;The fix is to stop writing the block by hand and generate it from the same data the page uses to render. One function, one input, two outputs: the visible breadcrumb nav and the JSON-LD, both built from the same array.&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="c1"&gt;// schema/breadcrumb.js&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildBreadcrumbSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;breadcrumbs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@context&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://schema.org&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;@type&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;BreadcrumbList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;itemListElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;breadcrumbs&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;crumb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&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;ListItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;item&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildBreadcrumbSchema&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;breadcrumbs&lt;/code&gt; here is the exact array the nav component already receives as a prop. Rename a category and both outputs update from the same edit. There is no second place to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the shape before it ships
&lt;/h2&gt;

&lt;p&gt;Generating the block from real data removes typos, but it does not stop someone from passing an empty array, or a crumb with no label, or a path that resolves to a relative URL instead of an absolute one. &lt;code&gt;BreadcrumbList&lt;/code&gt; expects &lt;code&gt;itemListElement&lt;/code&gt; as a non-empty list of &lt;code&gt;ListItem&lt;/code&gt; entries, each with &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;item&lt;/code&gt;. A build step that checks that shape catches a broken page before it deploys instead of after Search Console notices.&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="c1"&gt;// schema/validate.js&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertValidBreadcrumbSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&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="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&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="s1"&gt;BreadcrumbList&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expected @type BreadcrumbList&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemListElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemListElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;itemListElement must be a non-empty array&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="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemListElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&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="s1"&gt;ListItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: wrong @type`&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: position must be a number`&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: missing name`&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^https&lt;/span&gt;&lt;span class="se"&gt;?&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: item must be an absolute URL`&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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;assertValidBreadcrumbSchema&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it right after &lt;code&gt;buildBreadcrumbSchema&lt;/code&gt;, in the same build script that writes the page out. A page with a broken schema should fail the build, not reach staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the rendered output, not the generator
&lt;/h2&gt;

&lt;p&gt;Unit-testing the generator function proves the function works. It does not prove the schema actually lands on the page, survives the templating layer, and matches what a crawler would parse out of the final HTML. That needs a test against the rendered output itself.&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="c1"&gt;// test/breadcrumb-schema.test.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:test&lt;/span&gt;&lt;span class="dl"&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;assert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderPage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category page ships a valid BreadcrumbList&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="o"&gt;=&amp;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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hiking-boots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;breadcrumbs&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;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Footwear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/footwear&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hiking boots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/footwear/hiking-boots&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;script type="application&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;ld&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;json"&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;([\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;script&amp;gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no JSON-LD script tag found in rendered HTML&lt;/span&gt;&lt;span class="dl"&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&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;BreadcrumbList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemListElement&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemListElement&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hiking boots&lt;/span&gt;&lt;span class="dl"&gt;'&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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemListElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/^https:&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="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;This is the test that actually catches a redesign breaking things. If a future template swap drops the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag entirely, or renames the prop the generator reads from, this fails on the next commit instead of on the next Search Console crawl.&lt;/p&gt;

&lt;p&gt;Run one of these per page type that carries a schema block: product, article, FAQ, organization. Each type has its own required fields, so each gets its own assertions rather than one generic check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it after deploy too
&lt;/h2&gt;

&lt;p&gt;A build-time test only proves the schema was correct at build time. A CDN caching an old bundle, a canary rollout serving two versions, or a third-party script mutating the DOM after load can still put stale or missing structured data in front of real crawlers. A lightweight periodic check, fetching a handful of key pages in production and re-running the same validator against whatever JSON-LD comes back, closes that gap without needing a full monitoring platform.&lt;/p&gt;

&lt;p&gt;We wire this kind of build-time validation into new builds at &lt;a href="https://www.nooralto.com/creation-site-internet/" rel="noopener noreferrer"&gt;Nooralto&lt;/a&gt;, so the schema block stops being a thing someone remembers to update by hand.&lt;/p&gt;

&lt;p&gt;Structured data earns its keep when it is generated, not authored. Treat it as a projection of the page's own data, test that projection like any other output, and a redesign stops being a threat to it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at Nooralto, a web and SEO studio working out of Agadir and Paris.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Nooralto — &lt;a href="https://www.nooralto.com/refonte-site-internet/" rel="noopener noreferrer"&gt;Nooralto&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why your pages are crawled but not indexed</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:01:18 +0000</pubDate>
      <link>https://dev.to/nooralto1/why-your-pages-are-crawled-but-not-indexed-4lfa</link>
      <guid>https://dev.to/nooralto1/why-your-pages-are-crawled-but-not-indexed-4lfa</guid>
      <description>&lt;h1&gt;
  
  
  Why your pages are crawled but not indexed
&lt;/h1&gt;

&lt;p&gt;"Crawled, currently not indexed" in Search Console gets treated as one problem with one fix,&lt;br&gt;
usually "publish better content." The status covers several distinct failure modes, and content&lt;br&gt;
quality is only one of them. The other three are almost always visible in the server response&lt;br&gt;
itself, if anyone actually diffs what Googlebot receives against what a browser receives.&lt;/p&gt;
&lt;h2&gt;
  
  
  What "crawled but not indexed" actually means
&lt;/h2&gt;

&lt;p&gt;Google fetched the URL, read the response, and made a deliberate decision not to add it to the&lt;br&gt;
index. That is different from "discovered, not crawled", which is a budget or robots issue, and&lt;br&gt;
different from a canonical replacing the URL with another one Google chose to index instead. The&lt;br&gt;
Search Console UI collapses these into similar-looking rows. The API is more precise, and it is the&lt;br&gt;
only practical way to check hundreds of URLs without clicking through the UI one at a time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Querying status through the API
&lt;/h2&gt;

&lt;p&gt;The URL Inspection API exposes the same data the UI shows for a single URL, built for automation.&lt;br&gt;
Each call needs the property's verified &lt;code&gt;siteUrl&lt;/code&gt; and the full &lt;code&gt;inspectionUrl&lt;/code&gt;, sent to&lt;br&gt;
&lt;code&gt;urlInspection.index.inspect&lt;/code&gt;, which maps to &lt;code&gt;POST /v1/urlInspection/index:inspect&lt;/code&gt; on the Search&lt;br&gt;
Console API.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;inspectUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;siteUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inspectionUrl&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;searchconsole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;searchconsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;searchconsole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlInspection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;siteUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inspectionUrl&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inspectionResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexStatusResult&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;inspectionUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// PASS, PARTIAL, FAIL, NEUTRAL&lt;/span&gt;
    &lt;span class="na"&gt;coverageState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverageState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// e.g. "Crawled - currently not indexed"&lt;/span&gt;
    &lt;span class="na"&gt;lastCrawlTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastCrawlTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;googleCanonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;googleCanonical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userCanonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userCanonical&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;The quota on this endpoint is small, so a full-site sweep needs to run as a slow batch rather than&lt;br&gt;
a loop firing as fast as the client allows. Space the calls, and cache the results, since a page's&lt;br&gt;
status does not move minute to minute.&lt;/p&gt;

&lt;p&gt;The two canonical fields are the first thing worth comparing. When &lt;code&gt;googleCanonical&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;userCanonical&lt;/code&gt; disagree, Google decided the site's own canonical tag was wrong and picked a&lt;br&gt;
different URL to index in its place. That alone explains a large share of "crawled, not indexed"&lt;br&gt;
rows, and it needs no further diagnosis beyond fixing the tag.&lt;/p&gt;
&lt;h2&gt;
  
  
  When the tag is correct but the server still lies
&lt;/h2&gt;

&lt;p&gt;If the canonical fields agree and the page still is not indexed, the next check is whether&lt;br&gt;
Googlebot and an ordinary visitor are looking at the same document. Some setups serve different&lt;br&gt;
HTML by user agent, deliberately or by accident, through a caching layer or a bot-protection rule&lt;br&gt;
that treats crawlers differently from everyone else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Compare what Googlebot fetches against what a browser fetches&lt;/span&gt;
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; googlebot.html &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"googlebot: %{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; browser.html &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"browser:   %{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;title&amp;gt;.*&amp;lt;/title&amp;gt;'&lt;/span&gt; googlebot.html&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;title&amp;gt;.*&amp;lt;/title&amp;gt;'&lt;/span&gt; browser.html&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A status code mismatch is easy to catch this way. A content mismatch hiding inside two identical&lt;br&gt;
&lt;code&gt;200&lt;/code&gt; responses is the one that survives for months, because nothing in the logs looks like an&lt;br&gt;
error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that produces no error at all
&lt;/h2&gt;

&lt;p&gt;JavaScript-rendered content is the quieter version of the same problem. Googlebot fetches the&lt;br&gt;
initial HTML, queues the page for rendering, executes the scripts, and only then sees the final&lt;br&gt;
DOM. If a script throws, if a third-party dependency times out, or if the content depends on a&lt;br&gt;
client-side fetch that the render sandbox blocks, the rendered page ends up thinner than the one a&lt;br&gt;
visitor sees, with an HTTP &lt;code&gt;200&lt;/code&gt; at every step along the way. There is no error to alert on. The&lt;br&gt;
only way to catch it is to render the page the way the crawler does and diff the result against the&lt;br&gt;
server-side output, the same comparison as above, run against the rendered DOM rather than the raw&lt;br&gt;
response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirect chains as a slow leak
&lt;/h2&gt;

&lt;p&gt;A URL that resolves through two or three redirects before reaching its final destination usually&lt;br&gt;
still gets indexed, but it competes for crawl budget against everything else on the domain, and&lt;br&gt;
each additional hop is another place for a canonical or a status code to quietly diverge from what&lt;br&gt;
was intended. Checking chain length is a short loop built on &lt;code&gt;curl -I --location-trusted&lt;/code&gt; with&lt;br&gt;
manual redirect handling, and it is worth running across the whole URL list on the same cadence as&lt;br&gt;
the &lt;a href="https://www.nooralto.com/creation-site-internet/" rel="noopener noreferrer"&gt;nooralto.com&lt;/a&gt; technical audits that catch&lt;br&gt;
this alongside the canonical and rendering checks above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the four checks together
&lt;/h2&gt;

&lt;p&gt;None of these checks needs the content rewritten first. Canonical agreement, user-agent parity,&lt;br&gt;
rendered-DOM parity and redirect chain length are structural facts about the response, and they&lt;br&gt;
answer whether Google is capable of indexing the page before the conversation moves to whether the&lt;br&gt;
page deserves to be. Running the content-quality argument before ruling these out wastes a sprint&lt;br&gt;
on the wrong hypothesis.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at Nooralto, a web and SEO studio working out of Agadir and Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Internal linking as a data structure</title>
      <dc:creator>Nooralto1</dc:creator>
      <pubDate>Fri, 25 Sep 2026 00:01:05 +0000</pubDate>
      <link>https://dev.to/nooralto1/internal-linking-as-a-data-structure-16ej</link>
      <guid>https://dev.to/nooralto1/internal-linking-as-a-data-structure-16ej</guid>
      <description>&lt;h1&gt;
  
  
  Internal linking as a data structure
&lt;/h1&gt;

&lt;p&gt;Most internal linking work happens by hand: an editor remembers a related page and drops a link&lt;br&gt;
at the bottom of a post. That works until the site passes a few hundred URLs, at which point&lt;br&gt;
nobody remembers what links to what, and pages start going dark without anyone noticing.&lt;/p&gt;

&lt;p&gt;The fix is to stop treating internal links as content and start treating them as a graph. Every&lt;br&gt;
URL is a node. Every &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; pointing at another URL on the same domain is a directed edge.&lt;br&gt;
Once the site is represented that way, orphan pages, link depth and missing connections become&lt;br&gt;
computable properties instead of things an editor has to remember.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the node list from the sitemap
&lt;/h2&gt;

&lt;p&gt;The sitemap is the fastest way to get every URL the site claims exists, but it does not tell you&lt;br&gt;
what links to what. Parse it first to get the full node set, then crawl the HTML to get the edges.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;XMLParser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fast-xml-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadSitemapUrls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitemapUrl&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;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sitemapUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;parser&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;XMLParser&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;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&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;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlset&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="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;list&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;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&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;If the sitemap is an index of sitemaps, recurse into &lt;code&gt;sitemapindex.sitemap&lt;/code&gt; the same way before&lt;br&gt;
returning the flat list.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the edge list from the HTML
&lt;/h2&gt;

&lt;p&gt;For each URL, fetch the rendered HTML (or the pre-render output, if the site is a SPA) and extract&lt;br&gt;
every internal anchor. This is where most homegrown link audits stop, at a flat list of "page X&lt;br&gt;
links to page Y". That list is already an adjacency table; the next steps just query it properly.&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="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cheerio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractLinks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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;targets&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="nf"&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;a[href]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;el&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;attr&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="k"&gt;try&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;u&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;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;targets&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="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&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;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* mailto:, tel:, javascript: ignored */&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="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&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;adjacency&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="nx"&gt;urls&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;u&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;u&lt;/span&gt;&lt;span class="p"&gt;,&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;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;urls&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;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractLinks&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;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;adjacency&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;url&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;adjacency&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="nx"&gt;l&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="nx"&gt;adjacency&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;The filter on the last line matters: keep only edges that land on a node already in the sitemap.&lt;br&gt;
Links to unlisted URLs are a separate problem, usually a stale sitemap or a page that was deleted&lt;br&gt;
without removing its incoming links.&lt;/p&gt;
&lt;h2&gt;
  
  
  Depth: a breadth-first search, nothing more
&lt;/h2&gt;

&lt;p&gt;"How many clicks from the home page" is the standard proxy for how much authority a page is&lt;br&gt;
likely to receive, and it is a textbook BFS.&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;computeDepth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;adjacency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;depth&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="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&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;next&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;adjacency&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;current&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="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;depth&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="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;depth&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;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&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;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;queue&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;next&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;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;depth&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;Any node from the sitemap that never appears in &lt;code&gt;depth&lt;/code&gt; after this runs is unreachable by internal&lt;br&gt;
links alone, no matter what the sitemap says about it. That is a stronger signal than a manual page&lt;br&gt;
review will ever give: the page exists, but nothing on the site walks a visitor, or a crawler, to&lt;br&gt;
it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Orphans and isolated clusters
&lt;/h2&gt;

&lt;p&gt;An orphan page is any node absent from &lt;code&gt;depth&lt;/code&gt;. A subtler failure is a connected component that&lt;br&gt;
never touches the root at all: a cluster of pages linking to each other whose only inbound link&lt;br&gt;
came from a page that is itself unreachable.&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;findOrphans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allUrls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&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="nx"&gt;allUrls&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="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;depth&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="nx"&gt;u&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;Category pages and old campaign landing pages tend to end up here after a redesign quietly drops&lt;br&gt;
the navigation block that used to reach them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating links instead of placing them
&lt;/h2&gt;

&lt;p&gt;Once the graph exists, the interesting move is to stop writing internal links by hand and generate&lt;br&gt;
the missing ones at build time, from data rather than editorial memory: match each page's tags or&lt;br&gt;
category against a candidate pool, cap how many new links a page can receive per build, and never&lt;br&gt;
touch a page that already has structural exits from its template. The &lt;a href="https://www.nooralto.com/agence-seo/" rel="noopener noreferrer"&gt;Nooralto&lt;/a&gt;&lt;br&gt;
approach on client sites runs this as a build step, so the link set is versioned alongside the&lt;br&gt;
content instead of living in an editor's head.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check on a schedule
&lt;/h2&gt;

&lt;p&gt;Depth and orphan detection are cheap enough to run on every deploy, not once a year. A page that&lt;br&gt;
used to sit three clicks from the home page and now sits at six, because a template change removed&lt;br&gt;
a related-posts block, is a regression like any other, and it should fail a build the same way a&lt;br&gt;
broken test does.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the team at Nooralto, a web and SEO studio working out of Agadir and Paris.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agencedigitale</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
