From 5bad4866d8d3a0ad767e57e31277cf8b0357a47e Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Thu, 23 Jul 2026 03:57:00 +0000
Subject: [PATCH] Add RSS and HTML Lang checks to AI audit
Adds new checks to the AI Readiness Audit for RSS/Atom feeds and the HTML \`lang\` attribute using Cloudflare \`HTMLRewriter\`. Increases points appropriately, adds related frontend details, and tests the updated logic.
Co-authored-by: SecH0us3 <5781038+SecH0us3@users.noreply.github.com>
---
ai-valid/src/index.js | 43 +++++++++++++++++++++++++++++++++++-
ai-valid/tests/index.test.js | 35 +++++++++++++++++++++++++++++
2 files changed, 77 insertions(+), 1 deletion(-)
diff --git a/ai-valid/src/index.js b/ai-valid/src/index.js
index ee89c61..6a01fc9 100644
--- a/ai-valid/src/index.js
+++ b/ai-valid/src/index.js
@@ -619,6 +619,8 @@ async function performAudit(baseUrl, requestOrigin, env, ctx) {
let hasWebMCP = false;
let hasARIA = false;
let hasMetaDesc = false;
+ let hasRssFeed = false;
+ let hasHtmlLang = false;
let currentScriptText = '';
try {
@@ -681,6 +683,22 @@ async function performAudit(baseUrl, requestOrigin, env, ctx) {
let lowerHtmlText = '';
const transformed = new HTMLRewriter()
+ .on('html', {
+ element(el) {
+ if (el.getAttribute('lang')) {
+ hasHtmlLang = true;
+ }
+ }
+ })
+ .on('link', {
+ element(el) {
+ const rel = (el.getAttribute('rel') || '').toLowerCase();
+ const type = (el.getAttribute('type') || '').toLowerCase();
+ if (rel === 'alternate' && (type === 'application/rss+xml' || type === 'application/atom+xml')) {
+ hasRssFeed = true;
+ }
+ }
+ })
.on('meta', {
element(el) {
const name = (el.getAttribute('name') || '').toLowerCase();
@@ -856,6 +874,8 @@ async function performAudit(baseUrl, requestOrigin, env, ctx) {
if (hasWebMCP) totalScore += 10;
if (hasARIA) totalScore += 5;
if (hasMetaDesc) totalScore += 5;
+ if (hasRssFeed) totalScore += 5;
+ if (hasHtmlLang) totalScore += 5;
// Process JSON-LD blocks extracted by HTMLRewriter
for (const block of jsonLdChunks) {
@@ -1151,6 +1171,8 @@ Example:
hasConditionalGET,
hasWebMCP,
hasStatistics,
+ hasRssFeed,
+ hasHtmlLang,
results: [
{
name: "Content Neg. (MD)",
@@ -1389,6 +1411,24 @@ Examples of specific types:
spec: "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name",
tooltip: `What it is: The <meta name="description"> or Open Graph (og:) tags in the HTML head.
Why it's critical for GEO: Answer Engines and AI crawlers often extract these tags to quickly summarize a page when detailed schema is unavailable.
Impact of missing it: AI models may generate sub-optimal or irrelevant summaries of your page content in search results.
Implementation Example: <meta name="description" content="A comprehensive guide to...">`,
code: hasMetaDesc ? 'Found' : 'Missing'
+ },
+ {
+ name: "RSS/Atom Feed",
+ prompt: `Add an RSS or Atom feed to my website and link to it in the
<link rel="alternate" type="application/rss+xml">.<link rel="alternate" type="application/rss+xml" href="/feed.xml">`,
+ code: hasRssFeed ? 'Found' : 'Missing'
+ },
+ {
+ name: "HTML Lang Attribute",
+ prompt: `Ensure my website's root tag includes a descriptive 'lang' attribute (e.g., ) to help AI agents understand the primary language of the content.`,
+ status: hasHtmlLang ? 'ok' : 'warn',
+ message: hasHtmlLang ? "HTML lang attribute found" : "Missing HTML lang attribute",
+ spec: "https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang",
+ tooltip: `What it is: The lang attribute explicitly defined on the root <html> element.<html lang="en">`,
+ code: hasHtmlLang ? 'Found' : 'Missing'
}
].sort((a, b) => {
const weights = {
@@ -1397,7 +1437,8 @@ Examples of specific types:
"Content-Use Parameter": 60, "NoAI Meta Tag": 55, "FAQ Schema": 50, "Authorship (E-E-A-T)": 45,
"Internal Architecture": 40, "Conditional Requests (304)": 35, "Freshness Headers": 30,
"Content Freshness": 25, "Viewport Meta Tag": 20, "External Citations": 15,
- "Quotation Addition": 10, "Statistics Addition": 5, "ARIA Accessibility": 10, "Meta Description": 5
+ "Quotation Addition": 10, "Statistics Addition": 5, "ARIA Accessibility": 10, "Meta Description": 5,
+ "RSS/Atom Feed": 45, "HTML Lang Attribute": 40
};
return (weights[b.name] || 0) - (weights[a.name] || 0);
})
diff --git a/ai-valid/tests/index.test.js b/ai-valid/tests/index.test.js
index 72a5f5e..23f43e3 100644
--- a/ai-valid/tests/index.test.js
+++ b/ai-valid/tests/index.test.js
@@ -1028,6 +1028,41 @@ describe('safeReadText helper', () => {
global.fetch = originalFetch;
}
});
+
+ it('should detect RSS/Atom feeds and HTML lang attribute properly', async () => {
+ const originalFetch = global.fetch;
+ global.fetch = async (url) => {
+ const urlStr = url.toString();
+ if (urlStr.includes('cloudflare-dns.com')) {
+ return new Response(JSON.stringify({ Answer: [{ type: 1, data: '93.184.216.34' }] }));
+ }
+ if (urlStr.includes('example.com') || urlStr.includes('93.184.216.34')) {
+ const html = `
+
+
+
+
+
+
+
+ `;
+ return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } });
+ }
+ return new Response('Not Found', { status: 404 });
+ };
+ try {
+ const req = new Request('https://localhost/api/audit?targetUrl=' + encodeURIComponent('https://example.com'), {
+ method: 'GET'
+ });
+ const res = await index.fetch(req, {}, {});
+ expect(res.status).toBe(200);
+ const data = await res.json();
+ expect(data.content.hasRssFeed).toBe(true);
+ expect(data.content.hasHtmlLang).toBe(true);
+ } finally {
+ global.fetch = originalFetch;
+ }
+ });
});