<?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: Alba LM</title>
    <description>The latest articles on DEV Community by Alba LM (@alorenzom).</description>
    <link>https://dev.to/alorenzom</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%2F4192253%2Fb17f65d0-81b6-499e-8cd0-93ac4350f5dc.jpeg</url>
      <title>DEV Community: Alba LM</title>
      <link>https://dev.to/alorenzom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alorenzom"/>
    <language>en</language>
    <item>
      <title>I'm launching Fadely on Product Hunt today: an iOS vault where documents self-destruct</title>
      <dc:creator>Alba LM</dc:creator>
      <pubDate>Sun, 15 Nov 2026 08:47:39 +0000</pubDate>
      <link>https://dev.to/alorenzom/im-launching-fadely-on-product-hunt-today-an-ios-vault-where-documents-self-destruct-3b8a</link>
      <guid>https://dev.to/alorenzom/im-launching-fadely-on-product-hunt-today-an-ios-vault-where-documents-self-destruct-3b8a</guid>
      <description>&lt;p&gt;Today Fadely is live on Product Hunt 🚀 👉 &lt;a href="https://www.producthunt.com/posts/fadely" rel="noopener noreferrer"&gt;https://www.producthunt.com/posts/fadely&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;My brother-in-law flies constantly. One day he found his passport photo buried among hundreds of old downloads on his phone. First relief, then worry: what if his phone gets stolen?&lt;/p&gt;

&lt;p&gt;We all do this. We save a boarding pass, a ticket or an ID copy "just for a trip", and it stays on our phone for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Fadely does
&lt;/h2&gt;

&lt;p&gt;Fadely is a vault for documents you only need for a while:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✈️ Add boarding passes, tickets, a passport copy&lt;/li&gt;
&lt;li&gt;📅 Pick an expiry date&lt;/li&gt;
&lt;li&gt;🔔 Get a reminder 24 hours and 1 hour before, and extend it by 48 hours from the notification if your trip runs late&lt;/li&gt;
&lt;li&gt;🔒 When the date comes, the vault is gone for good&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Each vault is encrypted with &lt;strong&gt;AES-256&lt;/strong&gt; and its own key, stored in the &lt;strong&gt;iOS Keychain&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;When a vault expires, its key is deleted, so the files become unreadable noise&lt;/li&gt;
&lt;li&gt;If iOS skips the background task, the vault is always destroyed before it can be opened again&lt;/li&gt;
&lt;li&gt;The countdown widget uses a pre-built timeline, so it updates at the exact expiry without opening the app, and it only holds emoji and dates, never documents or keys&lt;/li&gt;
&lt;li&gt;No accounts, no cloud, no backups, no analytics. Everything stays on your iPhone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned in the first two weeks
&lt;/h2&gt;

&lt;p&gt;I launched on the App Store on November 1st and shared honest numbers on X: tiny, but the conversations with other indie devs were worth it. The biggest lesson: talking to developers is easy, reaching the people who actually need the app is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'd love your feedback
&lt;/h2&gt;

&lt;p&gt;If you have a minute, take a look on Product Hunt and tell me what you think, especially about the privacy model.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.producthunt.com/posts/fadely" rel="noopener noreferrer"&gt;https://www.producthunt.com/posts/fadely&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! 🙏&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built an iOS app where your documents delete themselves</title>
      <dc:creator>Alba LM</dc:creator>
      <pubDate>Fri, 13 Nov 2026 00:40:18 +0000</pubDate>
      <link>https://dev.to/alorenzom/i-built-an-ios-app-where-your-documents-delete-themselvespublished-true-4n5k</link>
      <guid>https://dev.to/alorenzom/i-built-an-ios-app-where-your-documents-delete-themselvespublished-true-4n5k</guid>
      <description>&lt;p&gt;We keep too many documents forever. Scans of contracts, tickets, IDs sent for a one-time procedure, a rental agreement from three years ago. Most of them only matter for a short time, and then they just sit in your phone, your cloud and your photo library, waiting to be leaked, lost or forgotten.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Fadely&lt;/strong&gt;: an iOS app that works as a vault for temporary documents. You add a document, choose a date, and when that date arrives the document destroys itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;My brother-in-law flies constantly. His passport was buried among hundreds of downloads on his phone, and he always worried: &lt;em&gt;what if it gets stolen?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So in Fadely, docs get an expiry date. Then they're gone for good. 🔒&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Most storage apps are built on one assumption: keep everything, forever. Fadely does the opposite. Every document has an expiry date from the moment you save it. You decide how long it should exist, and the app takes care of the rest.&lt;/p&gt;

&lt;p&gt;It is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documents you need for a limited period (a visa application, a rental process, a trip)&lt;/li&gt;
&lt;li&gt;Things you must share or keep temporarily and don't want to keep around&lt;/li&gt;
&lt;li&gt;Anyone who wants fewer sensitive files lying around on their devices&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it's a one-time purchase
&lt;/h2&gt;

&lt;p&gt;I wanted Fadely to be simple and honest: you pay once, no subscription. An app about &lt;em&gt;getting rid of things&lt;/em&gt; shouldn't charge you a monthly fee to keep storing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm sharing it here
&lt;/h2&gt;

&lt;p&gt;I'm a solo indie developer working in Swift and Flutter, and Fadely is my newest app. I'm building in public, so I would really like feedback from other developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How would you approach the expiry logic on iOS?&lt;/li&gt;
&lt;li&gt;What would make you trust an app like this with sensitive documents?&lt;/li&gt;
&lt;li&gt;Which feature would you expect that is not there yet?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fadely is available now on the App Store, with a launch price for a limited time:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apps.apple.com/app/fadely/id6820224574" rel="noopener noreferrer"&gt;https://apps.apple.com/app/fadely/id6820224574&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find all my apps here: &lt;a href="https://alorenzom.com" rel="noopener noreferrer"&gt;https://alorenzom.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading, and I'll answer every comment. 🙌&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your photos can tell strangers where you were. Here's how to check yours</title>
      <dc:creator>Alba LM</dc:creator>
      <pubDate>Thu, 12 Nov 2026 13:29:17 +0000</pubDate>
      <link>https://dev.to/alorenzom/your-photos-can-tell-strangers-where-you-were-heres-how-to-check-yours-3gpi</link>
      <guid>https://dev.to/alorenzom/your-photos-can-tell-strangers-where-you-were-heres-how-to-check-yours-3gpi</guid>
      <description>&lt;p&gt;&lt;strong&gt;The test&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I took a photo with my iPhone in a public place, emailed it to myself with Gmail, and opened the attachment on another device.&lt;br&gt;
It still carried the exact GPS location, the date and time down to the minute, my phone model, and even the direction I was facing (east, 70°). In total, 58 pieces of hidden information travelled silently with the picture.&lt;br&gt;
I'm a developer, and I still didn't expect that. Most people have no idea it happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a photo can carry&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time you take a photo, your phone can save extra information inside the file. It's called metadata, and you never see it when you look at the picture. It can include:&lt;br&gt;
• The exact location where you took it, often accurate to a few metres&lt;br&gt;
• The date and time, to the minute&lt;br&gt;
• Your phone's make and model&lt;br&gt;
• Camera and lens settings&lt;br&gt;
• The direction you were facing, altitude, and sometimes the app you used to edit it&lt;br&gt;
None of this is a bug. It's useful when you browse your own photos on a map. The problem starts when the photo leaves your phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many social apps remove this data when you upload a photo. WhatsApp and Instagram usually do. But photos sent by email, AirDrop, as a file or through a cloud link can keep everything.&lt;br&gt;
Think about the photos you send without a second thought: a picture of something you're selling, taken in your living room. A photo of your kids at the park near home. A "harmless" picture sent to someone you've only just met online.&lt;br&gt;
Anyone who receives the file can open its details and see where it was taken. You don't need to be a hacker to do it, and the person who sent it never knows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy is about choice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Privacy isn't about having something to hide. It's about deciding what you share, with whom, and when.&lt;br&gt;
We've got used to giving away information without noticing: location permissions, cookies, accounts for everything. Photo metadata is one more layer of that, and one of the least known. The good news is that it's also one of the easiest to fix, once you know it's there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check your own photos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built foto.alorenzom.com to make this visible to anyone, without technical knowledge.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the site and choose a photo.&lt;/li&gt;
&lt;li&gt;See everything it carries, with the location on a map.&lt;/li&gt;
&lt;li&gt;Download a clean copy without the hidden data.
The tool practises what it preaches: your photo never leaves your device. Everything happens in your browser. Nothing is uploaded, there are no accounts, no ads and no analytics. It's free.
One tip for iPhone: when you pick the photo, tap "Options" and turn on Location. Otherwise iOS hides it from the site, which is a good privacy default, but it means you won't see what the photo really carries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How to protect yourself&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On iPhone:&lt;br&gt;
• When sharing a photo, tap "Options" at the top and turn off "Location".&lt;br&gt;
• For a photo you already took: open it in Photos, tap (i), then Adjust, then No Location.&lt;br&gt;
• To stop saving it in future photos: Settings → Privacy &amp;amp; Security → Location Services → Camera → Never.&lt;br&gt;
On Android: open your camera settings and turn off location tags. The exact name changes from one phone to another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check one of your own photos at foto.alorenzom.com/en. It takes ten seconds, and you might be surprised.&lt;br&gt;
If it helped, share it with someone who sends photos by email. And if you find metadata it misses, I'd love to hear about it.&lt;br&gt;
I'm an indie iOS developer. I build small apps at alorenzom.com.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>security</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I checked what 31 big websites tell AI crawlers in robots.txt, and found a bug in my own tool</title>
      <dc:creator>Alba LM</dc:creator>
      <pubDate>Thu, 05 Nov 2026 22:59:19 +0000</pubDate>
      <link>https://dev.to/alorenzom/i-checked-what-31-big-websites-tell-ai-crawlers-in-robotstxt-and-found-a-bug-in-my-own-tool-20o6</link>
      <guid>https://dev.to/alorenzom/i-checked-what-31-big-websites-tell-ai-crawlers-in-robotstxt-and-found-a-bug-in-my-own-tool-20o6</guid>
      <description>&lt;p&gt;Can ChatGPT, Claude or Perplexity read your website? The first place to look is a small text file: &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wanted to see what big websites actually say in it, so I picked 40 well-known sites and audited their home pages for 12 AI crawlers. The results split much more cleanly by industry than I expected.&lt;/p&gt;

&lt;p&gt;The study also found a bug in my own tool. More on that below, because it's a mistake you could easily make too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;On 5 November 2026 I ran &lt;a href="https://pagesignal.alorenzom.com/demo" rel="noopener noreferrer"&gt;PageSignal&lt;/a&gt;, the AI visibility API I built, against the home pages of 40 sites: news, e-commerce, SaaS and developer tools, and reference sites.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;31 sites&lt;/strong&gt; gave usable results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9 sites&lt;/strong&gt; could not be audited. Most answered with an anti-bot wall, two timed out, and one page was over my 2 MB limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I split the crawlers into two groups, because they do different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI search and assistant bots&lt;/strong&gt; (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User): they fetch pages to answer people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI training bots&lt;/strong&gt; (GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot, meta-externalagent): they collect content for training models.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Sites audited&lt;/th&gt;
&lt;th&gt;Block at least one AI search or assistant bot&lt;/th&gt;
&lt;th&gt;Publish llms.txt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;News&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-commerce&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS and dev tools&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;31&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across all 31 sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10&lt;/strong&gt; block at least one AI search or assistant bot on their home page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13&lt;/strong&gt; block at least one AI bot of any kind. Three of them (AliExpress, Figma and Notion) only block training bots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18&lt;/strong&gt; block none of the 12 crawlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 10 sites that block at least one search or assistant bot were The Guardian, BBC, Reuters, The Verge, TechCrunch, Wired, Amazon, eBay, Reddit and IMDb.&lt;/p&gt;

&lt;p&gt;Reuters, Reddit and IMDb are a special case. Their general &lt;code&gt;User-agent: *&lt;/code&gt; rules close the home page to any bot without its own rules, so this isn't always AI-specific. Reddit's file ends like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: *
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The pattern: publishers close, software companies open
&lt;/h2&gt;

&lt;p&gt;All 6 news sites block at least one bot that fetches pages to answer people. None of the 10 SaaS and developer tool companies do, and 9 of those 10 publish an &lt;code&gt;llms.txt&lt;/code&gt; file, while no news site does.&lt;/p&gt;

&lt;p&gt;My guess, and it's only a guess, is that this comes down to the business model. A publisher sells its content, so an AI answer can replace the visit. A software company sells a product, so being mentioned in an AI answer works like free marketing.&lt;/p&gt;

&lt;p&gt;Neither choice is automatically right. Blocking is often deliberate. The thing to avoid is blocking by accident, for example with a security rule or a template nobody remembers adding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug in my own tool
&lt;/h2&gt;

&lt;p&gt;My first run said Figma was blocking AI search bots. I double-checked before writing that here, and it was wrong.&lt;/p&gt;

&lt;p&gt;PageSignal was using Python's built-in &lt;code&gt;urllib.robotparser&lt;/code&gt;. It's convenient, but I found two behaviours that don't match the robots.txt standard, &lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;RFC 9309&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It doesn't understand &lt;code&gt;$&lt;/code&gt;, the end-of-URL marker.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.robotparser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RobotFileParser&lt;/span&gt;

&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RobotFileParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;User-agent: OAI-SearchBot
Allow: /$
Disallow: /&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OAI-SearchBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# False, should be True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the standard, the most specific matching rule wins, so &lt;code&gt;Allow: /$&lt;/code&gt; opens the home page. That's how Figma's file is written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It can apply the wrong group when one bot name is contained in another.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RobotFileParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;User-agent: Applebot
Allow: /

User-agent: Applebot-Extended
Disallow: /&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Applebot-Extended&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# True, should be False
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule for &lt;code&gt;Applebot&lt;/code&gt; gets applied to &lt;code&gt;Applebot-Extended&lt;/code&gt;. This made my tool miss a real block on The Verge, Reuters and IMDb.&lt;/p&gt;

&lt;p&gt;In total, 4 of the 32 sites audited in that first run had wrong crawler results. I replaced the parser with a small RFC 9309 matcher and checked the numbers in this post against a separate implementation. They match.&lt;/p&gt;

&lt;p&gt;If you read robots.txt with &lt;code&gt;urllib.robotparser&lt;/code&gt; (tested on Python 3.12), it's worth testing these two cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this study doesn't prove
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a small, hand-picked sample.&lt;/strong&gt; It shows a pattern in these sites, not the whole web.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only looks at home pages.&lt;/strong&gt; This matters: Figma opens its home page to AI search bots but closes &lt;code&gt;/pricing&lt;/code&gt; to them. A page-level answer can differ from the rest of the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's one day's snapshot.&lt;/strong&gt; These files change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt is a request, not a lock.&lt;/strong&gt; It shows what a site asks for, not what each company's crawler really does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Being readable doesn't mean being cited.&lt;/strong&gt; Passing these checks gives an AI bot access. It can't guarantee a mention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A file doesn't show the whole relationship.&lt;/strong&gt; A publisher may also have a licensing deal that robots.txt doesn't reveal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check your own site
&lt;/h2&gt;

&lt;p&gt;Look at &lt;code&gt;yoursite.com/robots.txt&lt;/code&gt; and search for the bot names above, then look at the &lt;code&gt;User-agent: *&lt;/code&gt; block. Check the pages you care about, not only the home page.&lt;/p&gt;

&lt;p&gt;Also look at your page without JavaScript. Many AI crawlers don't run it, so a page built entirely in the browser can look almost empty to them.&lt;/p&gt;

&lt;p&gt;If you'd rather not do it by hand, there's a free &lt;a href="https://pagesignal.alorenzom.com/demo" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; with no signup. For automated checks, the API is on &lt;a href="https://rapidapi.com/AlbaLM/api/pagesignal-seo-audit-metadata-json-ld-validator" rel="noopener noreferrer"&gt;RapidAPI&lt;/a&gt; and &lt;a href="https://api.market/store/alorenzom/pagesignal-seo-ai-audit" rel="noopener noreferrer"&gt;API.market&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Did you decide on purpose what your &lt;code&gt;robots.txt&lt;/code&gt; says about AI crawlers, or is it just whatever your template shipped with? I'd love to hear what you found.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>How to audit a website's SEO and AI visibility with a single API call</title>
      <dc:creator>Alba LM</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:33:05 +0000</pubDate>
      <link>https://dev.to/alorenzom/how-to-audit-a-websites-seo-and-json-ld-with-a-single-api-call-4n4j</link>
      <guid>https://dev.to/alorenzom/how-to-audit-a-websites-seo-and-json-ld-with-a-single-api-call-4n4j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gn7j97573aqk0qh5mjt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gn7j97573aqk0qh5mjt.png" alt=" " width="800" height="801"&gt;&lt;/a&gt;Checking a page's SEO usually means opening five different tools: one for the title and meta description, another for Open Graph tags, another for canonicals and indexability, another for the sitemap, and yet another to validate JSON-LD.&lt;/p&gt;

&lt;p&gt;And now there's a new question: can AI assistants and AI search engines actually read your page? A site can look perfect in a browser and still be blocked in &lt;code&gt;robots.txt&lt;/code&gt;, or be almost empty for crawlers that don't run JavaScript.&lt;/p&gt;

&lt;p&gt;I got tired of checking all of this by hand, so I built &lt;strong&gt;PageSignal&lt;/strong&gt;: an API that audits any public URL in a single request and returns two explainable scores, one for SEO and one for AI visibility, so you know not just &lt;em&gt;what&lt;/em&gt; failed but &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SEO
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title and meta description&lt;/strong&gt;: length, and duplicate tags on the same page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Graph and Twitter Card tags&lt;/strong&gt;: and which recommended ones are missing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexability&lt;/strong&gt;: meta robots, googlebot and the &lt;code&gt;X-Robots-Tag&lt;/code&gt; header combined into one verdict&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical URL&lt;/strong&gt;: missing, empty, duplicated, self-referencing or pointing elsewhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt and XML sitemap&lt;/strong&gt; discovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON-LD&lt;/strong&gt;: every block parsed and validated, with its schema types&lt;/li&gt;
&lt;li&gt;Headings, image ALT coverage, internal and external links&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;0–100 SEO score&lt;/strong&gt; where every rule shows its points, severity and the reason&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI visibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt rules for 12 AI crawlers&lt;/strong&gt;: GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Google-Extended, Applebot-Extended, CCBot and meta-externalagent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search bots vs training bots&lt;/strong&gt;: blocking search and assistant bots can hide you from AI answers, while blocking training bots is a policy choice, so they are reported separately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content visible without JavaScript&lt;/strong&gt;: pages built in the browser can look almost empty to crawlers that don't run JS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON-LD&lt;/strong&gt; presence and &lt;strong&gt;llms.txt&lt;/strong&gt; detection&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;0–100 AI visibility score&lt;/strong&gt; with findings and plain-English fixes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it without signing up
&lt;/h2&gt;

&lt;p&gt;There's a live demo where you paste a URL and get both scores and the top issues to fix:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://pagesignal.alorenzom.com/demo" rel="noopener noreferrer"&gt;https://pagesignal.alorenzom.com/demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or straight from your terminal, no key needed (3 checks per day):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://pagesignal.alorenzom.com/demo/api/audit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url":"https://example.com/"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo returns a summary. The full API returns the complete report — &lt;a href="https://pagesignal.alorenzom.com/demo/sample.json" rel="noopener noreferrer"&gt;here's a real, unedited one for python.org&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Call the full API
&lt;/h2&gt;

&lt;p&gt;With a free RapidAPI key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--get&lt;/span&gt; &lt;span class="s1"&gt;'https://pagesignal-seo-audit-metadata-json-ld-validator.p.rapidapi.com/v1/audit'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'url=https://www.python.org/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-RapidAPI-Host: pagesignal-seo-audit-metadata-json-ld-validator.p.rapidapi.com'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RAPIDAPI_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only want the AI visibility report? Use the dedicated endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--get&lt;/span&gt; &lt;span class="s1"&gt;'https://pagesignal-seo-audit-metadata-json-ld-validator.p.rapidapi.com/v1/ai-visibility'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'url=https://www.python.org/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-RapidAPI-Host: pagesignal-seo-audit-metadata-json-ld-validator.p.rapidapi.com'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RAPIDAPI_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example output
&lt;/h2&gt;

&lt;p&gt;This is a trimmed, real response for &lt;code&gt;https://www.python.org/&lt;/code&gt;:&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;"success"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"network"&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;"final_url"&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://www.python.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;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"response_time_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;113&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"redirect_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;"quality_score"&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"grade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"errors_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"warnings_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"breakdown"&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;"rule_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title_check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"max_points"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"points_impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your title is shorter than recommended (30–60 characters). Consider adding primary keywords or brand context."&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;"rule_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xml_sitemap_check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"max_points"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"points_impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No XML sitemap was detected at common locations."&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"diagnostics"&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;"http_ok"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"likely_client_rendered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"notes"&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;"title"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&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="s2"&gt;"Welcome to Python.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;"length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&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;"indexability"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"is_indexable"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"raw_directives"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"index, follow"&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;"canonical"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"is_present"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"badge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing"&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;"structured_data"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"blocks_detected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"valid_blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invalid_blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"blocks"&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;"schema_types"&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="s2"&gt;"WebSite"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"errors"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ai_visibility"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"grade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"badge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Readable by AI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content_visible_without_js"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"robots_txt_checked"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"crawlers"&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;"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;"GPTBot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenAI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"training"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&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;"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;"OAI-SearchBot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenAI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&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;"blocked_search_crawlers"&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;"blocked_training_crawlers"&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;"llms_txt"&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;"checked"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"found"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every point lost comes with a &lt;code&gt;message&lt;/code&gt;, so you can show your users exactly what to fix instead of a bare number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail your CI build when SEO or AI visibility breaks
&lt;/h2&gt;

&lt;p&gt;Here's a small script that audits a list of URLs and exits with code &lt;code&gt;1&lt;/code&gt; if any page breaks a rule you care about. Save it as &lt;code&gt;seo-check.mjs&lt;/code&gt; (Node 18+, no dependencies):&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;// seo-check.mjs: audits a list of URLs and exits with code 1 if any of them&lt;/span&gt;
&lt;span class="c1"&gt;// breaks a rule you care about, so it can stop a CI pipeline.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pagesignal-seo-audit-metadata-json-ld-validator.p.rapidapi.com&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;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAPIDAPI_KEY&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;MIN_SCORE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&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;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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;2&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;KEY&lt;/span&gt; &lt;span class="o"&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;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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Usage: RAPIDAPI_KEY=your_key node seo-check.mjs &amp;lt;url&amp;gt; [more urls...]&lt;/span&gt;&lt;span class="dl"&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;2&lt;/span&gt;&lt;span class="p"&gt;);&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;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;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="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/audit?url=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&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="s2"&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;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;X-RapidAPI-Host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-RapidAPI-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KEY&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;report&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;res&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;url&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;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;`HTTP &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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;continue&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;quality_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;indexability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;structured_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ai_visibility&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;report&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;problems&lt;/span&gt; &lt;span class="o"&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;quality_score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_SCORE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;problems&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="s2"&gt;`score &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;quality_score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is below &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MIN_SCORE&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="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;indexability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_indexable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;problems&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page is not indexable&lt;/span&gt;&lt;span class="dl"&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;canonical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_present&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;problems&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canonical tag is missing&lt;/span&gt;&lt;span class="dl"&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;structured_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invalid_blocks&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;problems&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid JSON-LD found&lt;/span&gt;&lt;span class="dl"&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;ai_visibility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocked_search_crawlers&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;problems&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="s2"&gt;`AI search crawlers blocked: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ai_visibility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocked_search_crawlers&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="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="s2"&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;ai_visibility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content_visible_without_js&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;problems&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content is not visible without JavaScript&lt;/span&gt;&lt;span class="dl"&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;problems&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="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;log&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;url&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;problems&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="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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="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;log&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;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: SEO &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;quality_score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, AI visibility &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ai_visibility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&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;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="nx"&gt;failed&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="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;Run it like this:&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="nv"&gt;RAPIDAPI_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_key node seo-check.mjs https://example.com/ https://example.com/pricing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript is not executed, so single-page apps can return partial data. The response flags this in &lt;code&gt;diagnostics.likely_client_rendered&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Sites behind anti-bot walls can't be analyzed.&lt;/li&gt;
&lt;li&gt;Only public http/https URLs are supported.&lt;/li&gt;
&lt;li&gt;The AI checks show whether a page can be read. They can't guarantee that any AI product will cite it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://pagesignal.alorenzom.com/demo" rel="noopener noreferrer"&gt;https://pagesignal.alorenzom.com/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RapidAPI: &lt;a href="https://rapidapi.com/AlbaLM/api/pagesignal-seo-audit-metadata-json-ld-validator" rel="noopener noreferrer"&gt;https://rapidapi.com/AlbaLM/api/pagesignal-seo-audit-metadata-json-ld-validator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API.market: &lt;a href="https://api.market/store/alorenzom/pagesignal-seo-ai-audit" rel="noopener noreferrer"&gt;https://api.market/store/alorenzom/pagesignal-seo-ai-audit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What would you add to a check like this? I'd love to hear what you'd want it to catch.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>api</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hi DEV! I'm Alba, an indie dev building iOS/Flutter apps and PageSignal, an API that audits SEO, metadata &amp; JSON-LD of any URL in one call. Happy to hear what checks you'd want in a tool like this 👋</title>
      <dc:creator>Alba LM</dc:creator>
      <pubDate>Sun, 18 Oct 2026 14:35:24 +0000</pubDate>
      <link>https://dev.to/alorenzom/hi-dev-im-alba-an-indie-dev-building-iosflutter-apps-and-pagesignal-an-api-that-audits-seo-4b6e</link>
      <guid>https://dev.to/alorenzom/hi-dev-im-alba-an-indie-dev-building-iosflutter-apps-and-pagesignal-an-api-that-audits-seo-4b6e</guid>
      <description></description>
      <category>api</category>
      <category>seo</category>
      <category>showdev</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
