<?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: Calimero</title>
    <description>The latest articles on DEV Community by Calimero (@calimero_projects).</description>
    <link>https://dev.to/calimero_projects</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%2F4221783%2Fbdbb8773-d3a9-425b-afce-22b8fd12983c.png</url>
      <title>DEV Community: Calimero</title>
      <link>https://dev.to/calimero_projects</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/calimero_projects"/>
    <language>en</language>
    <item>
      <title>From sitemap.xml to a full audit report</title>
      <dc:creator>Calimero</dc:creator>
      <pubDate>Mon, 02 Nov 2026 19:51:19 +0000</pubDate>
      <link>https://dev.to/calimero_projects/from-sitemapxml-to-a-full-audit-report-46c7</link>
      <guid>https://dev.to/calimero_projects/from-sitemapxml-to-a-full-audit-report-46c7</guid>
      <description>&lt;p&gt;Most audits start with a crawl. That is usually the wrong first move: a crawl discovers what the crawler can reach, not what the site actually publishes. A sitemap is a better seed — the site's own list of canonical URLs, usually one HTTP request away.&lt;/p&gt;

&lt;p&gt;This tutorial runs a sitemap-to-report workflow with the &lt;a href="https://apify.com/calimero-projects/sitemap-audit" rel="noopener noreferrer"&gt;Sitemap Audit&lt;/a&gt; Actor: one audit item per URL, then a CSV export.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actor does
&lt;/h2&gt;

&lt;p&gt;The Sitemap Audit Actor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches the sitemap URL.&lt;/li&gt;
&lt;li&gt;Parses &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; entries.&lt;/li&gt;
&lt;li&gt;If the document is a sitemap index, follows its child sitemaps (up to 10 per index).&lt;/li&gt;
&lt;li&gt;Fetches each page as static HTML and runs the same audit engine as the &lt;a href="https://apify.com/calimero-projects/bulk-website-audit" rel="noopener noreferrer"&gt;Bulk Website Audit&lt;/a&gt; Actor.&lt;/li&gt;
&lt;li&gt;Pushes one dataset item per URL with a score, issue list, and page statistics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It does not render JavaScript, crawl links, or check &lt;code&gt;robots.txt&lt;/code&gt;. It audits exactly the URLs listed in the sitemap, up to &lt;code&gt;maxUrls&lt;/code&gt;. That makes it predictable: the input list is the scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Prepare the input
&lt;/h2&gt;

&lt;p&gt;The input is two fields:&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;"sitemapUrl"&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://example.com/sitemap.xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&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;ul&gt;
&lt;li&gt;
&lt;code&gt;sitemapUrl&lt;/code&gt; (required): the public URL of the sitemap or sitemap index.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxUrls&lt;/code&gt; (optional, default &lt;code&gt;100&lt;/code&gt;): the maximum number of page URLs to audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with a small &lt;code&gt;maxUrls&lt;/code&gt;. A sitemap that returns an HTML error page, a login wall, or a compressed file fails early, and you want to know that in 30 seconds, not 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Run it through the API
&lt;/h2&gt;

&lt;p&gt;You need an Apify API token in an environment variable — not in a notebook or a commit.&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_token_here"&lt;/span&gt;

curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/D49xJAuq6tledUTzL/run-sync-get-dataset-items?token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&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;"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;'{
    "sitemapUrl": "https://example.com/sitemap.xml",
    "maxUrls": 100
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; audit.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;run-sync-get-dataset-items&lt;/code&gt; endpoint starts the run, waits for it to finish, and returns the dataset items as JSON. It is the shortest path for small and medium sitemaps. The synchronous endpoint has a platform timeout (around five minutes), so keep &lt;code&gt;maxUrls&lt;/code&gt; small; for larger sitemaps, use the asynchronous pattern:&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;# Start the run&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/D49xJAuq6tledUTzL/runs?token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&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;"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;'{"sitemapUrl":"https://example.com/sitemap.xml","maxUrls":1000}'&lt;/span&gt;

&lt;span class="c"&gt;# Poll the run, then fetch its default dataset&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apify.com/v2/datasets/&lt;/span&gt;&lt;span class="nv"&gt;$DATASET_ID&lt;/span&gt;&lt;span class="s2"&gt;/items?format=json&amp;amp;token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; audit.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run response contains &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;defaultDatasetId&lt;/code&gt;. Poll &lt;code&gt;/v2/actor-runs/{id}&lt;/code&gt; until &lt;code&gt;SUCCEEDED&lt;/code&gt;, then fetch the dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Read the output
&lt;/h2&gt;

&lt;p&gt;Each dataset item describes one URL:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Final URL after redirects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number or null&lt;/td&gt;
&lt;td&gt;HTTP status; &lt;code&gt;null&lt;/code&gt; on a network error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;Heuristic 0–100 triage score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Page &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string or null&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;html lang&amp;gt;&lt;/code&gt; value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issues&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;Issue objects with &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;severity&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;examples&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;object&lt;/td&gt;
&lt;td&gt;Counts for words, images, links, form fields, headings, JSON-LD blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;stats&lt;/code&gt; object:&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="nl"&gt;"stats"&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;"words"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;812&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"images_without_alt"&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;"links"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"empty_links"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"form_fields"&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;"unlabeled_fields"&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;"headings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"json_ld_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="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;Do not trust &lt;code&gt;score&lt;/code&gt; without checking &lt;code&gt;status&lt;/code&gt;: a 403 challenge page can still parse as HTML and produce a misleading score. Filter to &lt;code&gt;status == 200&lt;/code&gt; and reject challenge titles such as "Just a moment..." or "Access Denied".&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Export CSV
&lt;/h2&gt;

&lt;p&gt;From the Apify console, open the run, go to the dataset, and use &lt;strong&gt;Export → CSV&lt;/strong&gt;. From the API:&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;-sS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apify.com/v2/datasets/&lt;/span&gt;&lt;span class="nv"&gt;$DATASET_ID&lt;/span&gt;&lt;span class="s2"&gt;/items?format=csv&amp;amp;token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; audit.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Top-level fields export cleanly. Nested &lt;code&gt;issues&lt;/code&gt; and &lt;code&gt;stats&lt;/code&gt; may arrive as JSON strings or flattened keys; for spreadsheet work, build the columns you need from JSON with &lt;code&gt;jq&lt;/code&gt;:&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;-sS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apify.com/v2/datasets/&lt;/span&gt;&lt;span class="nv"&gt;$DATASET_ID&lt;/span&gt;&lt;span class="s2"&gt;/items?format=json&amp;amp;token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  ["url","status","score","words","images","images_without_alt","form_fields","unlabeled_fields"],
  (.[] | [.url, .status, .score, .stats.words, .stats.images,
          .stats.images_without_alt, .stats.form_fields, .stats.unlabeled_fields])
  | @csv'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; audit.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a stable schema you can open in any spreadsheet or load into a database.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run with &lt;code&gt;maxUrls: 20&lt;/code&gt; to validate the sitemap and see whether the site blocks the Actor.&lt;/li&gt;
&lt;li&gt;Raise &lt;code&gt;maxUrls&lt;/code&gt; to the real size, or split the sitemap by section.&lt;/li&gt;
&lt;li&gt;Filter &lt;code&gt;status == 200&lt;/code&gt; and remove challenge pages.&lt;/li&gt;
&lt;li&gt;Group by issue &lt;code&gt;code&lt;/code&gt; to get prevalence, and sum &lt;code&gt;stats&lt;/code&gt; for element-level rates.&lt;/li&gt;
&lt;li&gt;Schedule the Actor weekly and store each dataset to track regressions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last step matters: scheduling turns a one-off report into a trend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Static HTML only.&lt;/strong&gt; No JavaScript execution. Client-rendered pages may return an empty shell or missing content; treat that as a rendering limitation, not an accessibility finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sitemap indexes are capped.&lt;/strong&gt; The Actor follows up to 10 child sitemaps per index and takes the first &lt;code&gt;maxUrls&lt;/code&gt; URLs it finds. Large sitemaps will be truncated; split them or run multiple inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No &lt;code&gt;lastmod&lt;/code&gt; filtering.&lt;/strong&gt; Every &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; is treated equally. To audit only recently changed pages, filter the sitemap first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No &lt;code&gt;robots.txt&lt;/code&gt; check.&lt;/strong&gt; The Actor fetches each URL directly. Keep &lt;code&gt;maxUrls&lt;/code&gt; sane, avoid hammering production, and schedule runs outside peak hours. Targets may still return 429 or 403; those items are not valid results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compressed sitemaps are not explicitly handled.&lt;/strong&gt; Plain XML is the safe input. If your sitemap is served as &lt;code&gt;.xml.gz&lt;/code&gt;, decompress it first or expose an uncompressed variant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The score is a triage signal.&lt;/strong&gt; It weights template-level issues such as missing titles, canonical links, labels, and &lt;code&gt;alt&lt;/code&gt; text. It is not a WCAG conformance score and cannot detect contrast, keyboard traps, focus order, or runtime ARIA problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;The sitemap is the scope; the Actor is the executor; the dataset is the report. Once the pipeline works, the hard part is deciding which recurring issue codes your team will actually fix.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Run the workflow with the &lt;a href="https://apify.com/calimero-projects/sitemap-audit" rel="noopener noreferrer"&gt;Sitemap Audit&lt;/a&gt; Actor. For arbitrary URL lists, use the &lt;a href="https://apify.com/calimero-projects/bulk-website-audit" rel="noopener noreferrer"&gt;Bulk Website Audit&lt;/a&gt; Actor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>tutorial</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I audited 100 public-sector websites in the EU and the US. The regional ranking is noise.</title>
      <dc:creator>Calimero</dc:creator>
      <pubDate>Mon, 02 Nov 2026 19:51:18 +0000</pubDate>
      <link>https://dev.to/calimero_projects/i-audited-100-public-sector-websites-in-the-eu-and-the-us-the-regional-ranking-is-noise-519p</link>
      <guid>https://dev.to/calimero_projects/i-audited-100-public-sector-websites-in-the-eu-and-the-us-the-regional-ranking-is-noise-519p</guid>
      <description>&lt;p&gt;&lt;em&gt;Fifty European and fifty US public-sector homepages, same static audit, same day. Seventy-nine were auditable. The average scores differ by 2.2 points. The failures are almost identical.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/calimero_projects/europe-vs-the-us-i-audited-40-public-sector-websites-both-fail-the-same-basics-f8k"&gt;My first EU-vs-US audit&lt;/a&gt; used 40 URLs and produced a 3.7-point gap. On a sample that small, that is sampling error with a chart. So I scaled to 100 URLs and looked for failures that survive a larger n. The ranking stayed noise; the same template-level defects stayed at the top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;I built two convenience samples of 50 homepages each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EU (50):&lt;/strong&gt; EU institutions (Parliament, Commission, ECB, EIB, agencies), national government portals, and major city portals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;US (50):&lt;/strong&gt; federal departments and agencies, state portals, and major city portals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each URL was audited once with the &lt;a href="https://apify.com/calimero-projects/bulk-website-audit" rel="noopener noreferrer"&gt;Bulk Website Audit&lt;/a&gt; Actor on Apify, in four batches of 25. The Actor fetches static HTML with a 20-second timeout, does not execute JavaScript, and returns a 0–100 heuristic score plus issue codes and page statistics. The score is a triage signal, not a WCAG conformance score. Data was collected on 2 November 2026. I did not retry failures: bot protection is part of what an automated audit actually sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample size and exclusions
&lt;/h2&gt;

&lt;p&gt;Of the 100 URLs, &lt;strong&gt;21 were excluded&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;EU&lt;/th&gt;
&lt;th&gt;US&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 403 (WAF / bot challenge)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 429 (rate limit / checkpoint)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 202 with no content&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Excluded total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The blocked pages carried titles such as "Just a moment...", "Access Denied", "Browser check", "Azure WAF", "Not allowed", and "Vercel Security Checkpoint". None were scored as content. No HTTP 200 page was removed for an empty title or a language selector.&lt;/p&gt;

&lt;p&gt;That left &lt;strong&gt;79 auditable homepages: 42 EU and 37 US&lt;/strong&gt; (79% of the original sample).&lt;/p&gt;

&lt;p&gt;Big federal domains — state.gov, defense.gov, hhs.gov, transportation.gov — were among the excluded. A pipeline that ignores status codes would score challenge pages and report nonsense. The US sample therefore skews toward agencies and states that allow automated fetching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;EU (n=42)&lt;/th&gt;
&lt;th&gt;US (n=37)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average score&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;85.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;87.7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median score&lt;/td&gt;
&lt;td&gt;88.0&lt;/td&gt;
&lt;td&gt;90.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score range&lt;/td&gt;
&lt;td&gt;61–97&lt;/td&gt;
&lt;td&gt;73–97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages scoring ≥90&lt;/td&gt;
&lt;td&gt;18 (42.9%)&lt;/td&gt;
&lt;td&gt;19 (51.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 2.2-point gap on 42 and 37 pages is not a ranking: the ranges overlap, and the EU sample contains both the lowest score (61) and nine pages at 95 or above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue prevalence (share of auditable pages with at least one occurrence)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;EU (n=42)&lt;/th&gt;
&lt;th&gt;US (n=37)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing canonical&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;42 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing JSON-LD structured data&lt;/td&gt;
&lt;td&gt;26 (61.9%)&lt;/td&gt;
&lt;td&gt;27 (73.0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing meta description&lt;/td&gt;
&lt;td&gt;14 (33.3%)&lt;/td&gt;
&lt;td&gt;8 (21.6%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links without accessible text&lt;/td&gt;
&lt;td&gt;20 (47.6%)&lt;/td&gt;
&lt;td&gt;11 (29.7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;8 (19.0%)&lt;/td&gt;
&lt;td&gt;10 (27.0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form fields without a label&lt;/td&gt;
&lt;td&gt;9 (21.4%)&lt;/td&gt;
&lt;td&gt;3 (8.1%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images without &lt;code&gt;alt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4 (9.5%)&lt;/td&gt;
&lt;td&gt;6 (16.2%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;4 (9.5%)&lt;/td&gt;
&lt;td&gt;4 (10.8%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heading level skip&lt;/td&gt;
&lt;td&gt;5 (11.9%)&lt;/td&gt;
&lt;td&gt;10 (27.0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Title outside 10–65 characters&lt;/td&gt;
&lt;td&gt;3 (7.1%)&lt;/td&gt;
&lt;td&gt;5 (13.5%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short meta description&lt;/td&gt;
&lt;td&gt;2 (4.8%)&lt;/td&gt;
&lt;td&gt;2 (5.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thin content (&amp;lt;200 words)&lt;/td&gt;
&lt;td&gt;3 (7.1%)&lt;/td&gt;
&lt;td&gt;2 (5.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;lang&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1 (2.4%)&lt;/td&gt;
&lt;td&gt;0 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Aggregate element counts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;EU&lt;/th&gt;
&lt;th&gt;US&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;852&lt;/td&gt;
&lt;td&gt;660&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images without &lt;code&gt;alt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;30 (3.5%)&lt;/td&gt;
&lt;td&gt;24 (3.6%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links&lt;/td&gt;
&lt;td&gt;7,038&lt;/td&gt;
&lt;td&gt;4,912&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links without accessible text&lt;/td&gt;
&lt;td&gt;136 (1.9%)&lt;/td&gt;
&lt;td&gt;29 (0.6%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form fields&lt;/td&gt;
&lt;td&gt;391&lt;/td&gt;
&lt;td&gt;292&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form fields without a label&lt;/td&gt;
&lt;td&gt;17 (4.4%)&lt;/td&gt;
&lt;td&gt;4 (1.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Canonical is not a regional problem. It is a template problem.&lt;/strong&gt; All 79 auditable pages — 100% in both regions — had no canonical link. On sites with aliases, translated paths, and legacy URLs, that is one template omission repeated across thousands of pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data is missing on most pages, and the US is slightly worse.&lt;/strong&gt; 61.9% of EU pages and 73.0% of US pages had no JSON-LD block. For public services, structured data tells search engines about opening hours, locations, services, and FAQs. It is the cheapest visible improvement here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta descriptions are missing on a third of EU pages and a fifth of US pages.&lt;/strong&gt; The gap (33.3% vs 21.6%) is larger than the score gap, but the absolute numbers matter: 22 homepages left search engines to generate the snippet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty links are widespread per page, rare per link.&lt;/strong&gt; 47.6% of EU pages and 29.7% of US pages contained at least one link with no accessible text. Those links are only 1.9% of all EU links and 0.6% of all US links. In practice this is a few icon links, logo wrappers, or cards repeated in a template. The bug is localised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form labels are the clearest EU weakness.&lt;/strong&gt; 21.4% of EU pages had at least one unlabeled field, versus 8.1% in the US. Aggregated, 17 of 391 EU fields (4.4%) lacked a label, against 4 of 292 US fields (1.4%).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image &lt;code&gt;alt&lt;/code&gt; is close to parity in aggregate.&lt;/strong&gt; 3.5% of EU images and 3.6% of US images had no &lt;code&gt;alt&lt;/code&gt; attribute. Page-level rates differ more (9.5% vs 16.2%) because a few pages concentrate many decorative images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heading structure is worse in the US sample.&lt;/strong&gt; 27.0% of US pages had multiple &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; tags and 27.0% skipped a heading level, against 19.0% and 11.9% in the EU. Missing &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; is almost identical (9.5% vs 10.8%).&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;This is a static audit of one homepage per domain, on one day, on a non-random sample. It cannot detect contrast, keyboard traps, focus order, runtime ARIA state, screen-reader behaviour, or JavaScript-injected issues. Several large sites returned a shell or a challenge page and were excluded.&lt;/p&gt;

&lt;p&gt;Scores are heuristic; that gap is sampling variation. The robust signal is binary: 79 of 79 pages missing canonical is not a sampling artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The sample produced no regional winner, only the same five failures on both sides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Canonical&lt;/strong&gt; — missing on every page in both regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured data&lt;/strong&gt; — absent on 62–73% of pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta descriptions&lt;/strong&gt; — absent on 22–33% of pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty links&lt;/strong&gt; — on 30–48% of pages, concentrated in templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form labels&lt;/strong&gt; — missing on 8–21% of pages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None require a redesign, just template ownership and release checks. The regional ranking is noise; the shared backlog is the finding.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Audit run with the &lt;a href="https://apify.com/calimero-projects/bulk-website-audit" rel="noopener noreferrer"&gt;Bulk Website Audit&lt;/a&gt; Actor on Apify. Sample: 50 EU + 50 US public-sector homepages; 79 auditable after excluding 21 non-200 or rate-limited URLs. Data collected 2 November 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>seo</category>
      <category>webdev</category>
      <category>data</category>
    </item>
    <item>
      <title>Europe vs the US: I audited 40 public-sector websites. Both fail the same basics.</title>
      <dc:creator>Calimero</dc:creator>
      <pubDate>Mon, 02 Nov 2026 19:23:46 +0000</pubDate>
      <link>https://dev.to/calimero_projects/europe-vs-the-us-i-audited-40-public-sector-websites-both-fail-the-same-basics-f8k</link>
      <guid>https://dev.to/calimero_projects/europe-vs-the-us-i-audited-40-public-sector-websites-both-fail-the-same-basics-f8k</guid>
      <description>&lt;p&gt;&lt;em&gt;I ran the same static accessibility and SEO audit on 20 European and 20 US public-sector websites. 25 were auditable. The ranking is noise; the shared failures are the story.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I wanted a clean comparison, so I used the same checks on both sides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20 EU URLs: EU institutions, national governments, a few central portals.&lt;/li&gt;
&lt;li&gt;20 US URLs: federal departments and agencies.&lt;/li&gt;
&lt;li&gt;Same static audit: title, meta description, &lt;code&gt;lang&lt;/code&gt;, headings, image &lt;code&gt;alt&lt;/code&gt;, form labels, viewport, canonical, &lt;code&gt;noindex&lt;/code&gt;, thin content, JSON-LD.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The audit is a triage, not a WCAG compliance test. It runs on static HTML and does not execute JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened before the scores
&lt;/h2&gt;

&lt;p&gt;15 of the 40 URLs could not be audited:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 EU sites&lt;/strong&gt; returned HTTP 403 or a "Just a moment..." challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8 US sites&lt;/strong&gt; returned 403, 202 or an "Access Denied" page from a WAF.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a finding in itself: a large share of public-sector sites now sit behind bot protection. Any audit pipeline that scores a 403 page as if it were content will produce garbage. The first rule of automated auditing is: &lt;strong&gt;check the status code and the title before you trust the score.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After removing challenge pages and language selectors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EU: 13 auditable pages&lt;/strong&gt;, average score &lt;strong&gt;80.8 / 100&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;US: 12 auditable pages&lt;/strong&gt;, average score &lt;strong&gt;84.5 / 100&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 3.7-point difference on a sample of 25 is not a ranking. It is noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared failures
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;EU (n=13)&lt;/th&gt;
&lt;th&gt;US (n=12)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing canonical&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing JSON-LD structured data&lt;/td&gt;
&lt;td&gt;69%&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing meta description&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links without accessible text&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form fields without a label&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images without &lt;code&gt;alt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;lang&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Aggregate counts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EU:&lt;/strong&gt; 338 images, 9 without &lt;code&gt;alt&lt;/code&gt; (2.7%); 100 form fields, 5 unlabeled (5%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;US:&lt;/strong&gt; 182 images, 0 without &lt;code&gt;alt&lt;/code&gt;; 87 form fields, 3 unlabeled (3.4%).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What stands out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Canonical is missing everywhere.&lt;/strong&gt; Not "most sites". Every single auditable page in both regions. For public-sector sites with multiple domains, aliases and old URLs, this is a template-level bug that multiplies across thousands of pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data is rare.&lt;/strong&gt; 69% of EU pages and 83% of US pages have no JSON-LD at all. For public services, structured data is how search engines understand opening hours, locations, events and FAQs. It is also the cheapest fix on this list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta descriptions are missing on roughly half the pages.&lt;/strong&gt; Search engines will write their own snippet, often from the first paragraph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility basics are better than expected, but inconsistent.&lt;/strong&gt; The US sample had zero images without &lt;code&gt;alt&lt;/code&gt;; the EU sample had 15% of sites with at least one. Form labels are unlabeled on 17-23% of sites in both regions. These are template fixes, not redesigns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The caveat
&lt;/h2&gt;

&lt;p&gt;This is a static audit on a small, non-random sample. It cannot detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contrast ratios,&lt;/li&gt;
&lt;li&gt;keyboard traps,&lt;/li&gt;
&lt;li&gt;ARIA state that lies at runtime,&lt;/li&gt;
&lt;li&gt;screen-reader behaviour,&lt;/li&gt;
&lt;li&gt;issues introduced by JavaScript.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 100% missing-canonical rate is a real signal because it is a binary check on a template. A 3.7-point average difference between regions is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;"Europe vs the US" makes a good headline and a bad conclusion. The interesting finding is that &lt;strong&gt;both regions fail the same basics&lt;/strong&gt;: canonical, structured data, meta descriptions, empty links and form labels. These are not exotic accessibility problems. They are template hygiene.&lt;/p&gt;

&lt;p&gt;If you run public-sector sites, fix these five things before you buy another overlay:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a canonical URL to every template.&lt;/li&gt;
&lt;li&gt;Add JSON-LD for the organisation and the main services.&lt;/li&gt;
&lt;li&gt;Add a meta description to the templates that lack one.&lt;/li&gt;
&lt;li&gt;Give every form field a real &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Audit the media library for &lt;code&gt;alt&lt;/code&gt;, not just the homepage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these require a redesign. They require someone to own the template.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The audit was run with the &lt;a href="https://apify.com/calimero-projects/bulk-website-audit" rel="noopener noreferrer"&gt;Bulk Website Audit&lt;/a&gt; Actor on Apify. Data collected on 2 November. Sample: 20 EU + 20 US public-sector URLs; 25 auditable after excluding 403/202 challenge pages and language selectors.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>seo</category>
      <category>webdev</category>
      <category>data</category>
    </item>
    <item>
      <title>How to Audit 1,000 Pages for Accessibility and SEO in One Run</title>
      <dc:creator>Calimero</dc:creator>
      <pubDate>Mon, 02 Nov 2026 18:46:29 +0000</pubDate>
      <link>https://dev.to/calimero_projects/how-to-audit-1000-pages-for-accessibility-and-seo-in-one-run-1nlh</link>
      <guid>https://dev.to/calimero_projects/how-to-audit-1000-pages-for-accessibility-and-seo-in-one-run-1nlh</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical workflow for agencies that need a full-site baseline before a manual review.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;A client sends you a website with 1,000 pages and asks: "Is it accessible? Is the SEO clean?"&lt;/p&gt;

&lt;p&gt;You open the first page. You check the title, the meta description, the images, the form labels, the heading structure. You do it again on page two. By page ten you already know you will not finish today.&lt;/p&gt;

&lt;p&gt;Manual page-by-page audits do not scale. What scales is a &lt;strong&gt;triage&lt;/strong&gt;: run the obvious checks on every URL, sort by severity, and spend your human time on the pages that actually need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;I use two Apify Actors for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website Accessibility &amp;amp; SEO Audit&lt;/strong&gt; — crawls one site and audits its pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk Website Audit&lt;/strong&gt; — takes a list of URLs and audits each one in a single run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both return one JSON item per page, with a score and a list of concrete issues.&lt;/p&gt;

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

&lt;p&gt;For each page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; presence and length&lt;/li&gt;
&lt;li&gt;meta description presence and length&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lang&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; presence, multiple H1s, heading-level skips&lt;/li&gt;
&lt;li&gt;images without &lt;code&gt;alt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;links without accessible text&lt;/li&gt;
&lt;li&gt;form fields without an associated label&lt;/li&gt;
&lt;li&gt;viewport meta&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noindex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;canonical link&lt;/li&gt;
&lt;li&gt;thin content (&amp;lt; 200 words)&lt;/li&gt;
&lt;li&gt;JSON-LD structured data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a static check. It does not replace a manual WCAG 2.2 audit, but it removes the boring part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Build the URL list
&lt;/h2&gt;

&lt;p&gt;You can start from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;sitemap.xml&lt;/code&gt; (use the Sitemap Audit Actor to extract and audit URLs directly),&lt;/li&gt;
&lt;li&gt;a CSV of landing pages,&lt;/li&gt;
&lt;li&gt;a list of client sites.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this example, a JSON array is enough:&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;"urls"&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="s2"&gt;"https://example.com"&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://example.com/about"&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://example.com/contact"&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;
  
  
  Step 2 — Run the Actor
&lt;/h2&gt;

&lt;p&gt;From the Apify Console, paste the JSON and click &lt;strong&gt;Start&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From the API:&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;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/calimero-projects~bulk-website-audit/runs?token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&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;"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;'{"urls":["https://example.com","https://example.com/about"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with the Python client:&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;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;YOUR_TOKEN&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calimero-projects/bulk-website-audit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&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;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/about&lt;/span&gt;&lt;span class="sh"&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="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&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;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 — Read the output
&lt;/h2&gt;

&lt;p&gt;One item per page:&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;"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://example.com/"&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="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;"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;65&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="s2"&gt;"Example Domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issues"&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;"code"&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_meta_description"&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;"medium"&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;"Missing meta description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;"examples"&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;"stats"&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;"words"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"images"&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;"images_without_alt"&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;"links"&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;"empty_links"&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;"form_fields"&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;"unlabeled_fields"&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;"headings"&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="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;Export the dataset as CSV or Excel and you have a client-ready baseline in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Triage, not fix
&lt;/h2&gt;

&lt;p&gt;Sort by score. Ignore the pages above 90 unless the client asked for perfection. Start from the pages with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing &lt;code&gt;alt&lt;/code&gt; on images,&lt;/li&gt;
&lt;li&gt;form fields without labels,&lt;/li&gt;
&lt;li&gt;missing or multiple &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noindex&lt;/code&gt; on pages that should rank.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are template-level issues: fix the template, fix hundreds of pages at once.&lt;/p&gt;

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

&lt;p&gt;The Bulk Website Audit uses pay-per-event pricing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0.07&lt;/strong&gt; per run (start event, 1 GB memory)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0.01&lt;/strong&gt; per audited page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 1,000-page audit costs about &lt;strong&gt;$10.07&lt;/strong&gt;. An agency can bill that as a few minutes of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It does not render JavaScript. For SPA-heavy sites, use a browser-based check.&lt;/li&gt;
&lt;li&gt;It does not test contrast, keyboard navigation or screen-reader behaviour.&lt;/li&gt;
&lt;li&gt;It does not decide whether a site is legally compliant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It tells you where to look first. That is usually the difference between a two-day audit and a two-hour triage.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/calimero-projects/bulk-website-audit" rel="noopener noreferrer"&gt;Bulk Website Audit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/calimero-projects/website-accessibility-seo-audit" rel="noopener noreferrer"&gt;Website Accessibility &amp;amp; SEO Audit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/calimero-projects/sitemap-audit" rel="noopener noreferrer"&gt;Sitemap Audit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run it on a client site, I would like to know which issues show up most often. That is the data I use to decide what the next version should check.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>seo</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>I audited 12 Italian public-sector websites for accessibility and SEO. Here's what I found.</title>
      <dc:creator>Calimero</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:50:23 +0000</pubDate>
      <link>https://dev.to/calimero_projects/i-audited-12-italian-public-sector-websites-for-accessibility-and-seo-heres-what-i-found-44gd</link>
      <guid>https://dev.to/calimero_projects/i-audited-12-italian-public-sector-websites-for-accessibility-and-seo-heres-what-i-found-44gd</guid>
      <description>&lt;p&gt;&lt;em&gt;Method: static HTML audit with an Apify Actor. 15 URLs sampled, 12 returned HTML and were analysed. This is a quick triage, not a full WCAG 2.2 audit.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Italian public-sector websites are used by millions of people, including older citizens and people with low vision. The European Accessibility Act has made accessibility a legal requirement for many digital services. Yet most public bodies still publish pages with missing alternative text, unlabeled form fields or broken heading structures.&lt;/p&gt;

&lt;p&gt;I ran a static audit on a small sample of Italian public administration websites to see what a quick, automated triage can catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample
&lt;/h2&gt;

&lt;p&gt;15 URLs: government, tax agency, social security, statistics, major cities and regions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;12 returned HTML and were analysed.&lt;/li&gt;
&lt;li&gt;3 could not be analysed: one returned HTTP 403 to the bot, two timed out.&lt;/li&gt;
&lt;li&gt;Average score: &lt;strong&gt;81.6 / 100&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Best: 97. Worst: 64.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the audit checks
&lt;/h2&gt;

&lt;p&gt;The Actor runs static checks on each page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and meta description&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lang&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; presence, multiple H1s, heading-level skips&lt;/li&gt;
&lt;li&gt;images without &lt;code&gt;alt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;links without accessible text&lt;/li&gt;
&lt;li&gt;form fields without an associated label&lt;/li&gt;
&lt;li&gt;viewport meta&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noindex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;canonical link&lt;/li&gt;
&lt;li&gt;thin content&lt;/li&gt;
&lt;li&gt;JSON-LD structured data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not replace a manual WCAG audit. It is a fast way to find the obvious issues before spending hours with a screen reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Sites affected&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing canonical&lt;/td&gt;
&lt;td&gt;12 / 12&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing structured data (JSON-LD)&lt;/td&gt;
&lt;td&gt;10 / 12&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing meta description&lt;/td&gt;
&lt;td&gt;6 / 12&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5 / 12&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heading-level skips&lt;/td&gt;
&lt;td&gt;5 / 12&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links without accessible text&lt;/td&gt;
&lt;td&gt;5 / 12&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images without &lt;code&gt;alt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3 / 12&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form fields without a label&lt;/td&gt;
&lt;td&gt;3 / 12&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Aggregate counts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;467 images&lt;/strong&gt; found; &lt;strong&gt;13 without &lt;code&gt;alt&lt;/code&gt;&lt;/strong&gt; (~2.8%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;121 form fields&lt;/strong&gt; found; &lt;strong&gt;16 without an associated label&lt;/strong&gt; (~13%).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What stands out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Canonical is missing everywhere.&lt;/strong&gt; 12 out of 12. For public-sector sites with multiple domains, aliases and old URLs, this is a real SEO problem: it splits ranking signals and makes duplication harder to control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data is rare.&lt;/strong&gt; 10 out of 12 have no JSON-LD at all. For public services, structured data is how search engines understand opening hours, locations, events and FAQs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Half the sample has no meta description.&lt;/strong&gt; Search engines will generate their own snippet, often from the first paragraph of the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility issues are concentrated.&lt;/strong&gt; Only 3 sites have images without &lt;code&gt;alt&lt;/code&gt; and 3 have unlabeled form fields, but those that do have multiple instances. The aggregate numbers are small, which suggests that the basics are often handled — the gap is in consistency, not awareness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The caveat
&lt;/h2&gt;

&lt;p&gt;This is a static audit. It cannot detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contrast ratios&lt;/li&gt;
&lt;li&gt;keyboard traps&lt;/li&gt;
&lt;li&gt;ARIA misuse that only appears at runtime&lt;/li&gt;
&lt;li&gt;screen-reader behaviour&lt;/li&gt;
&lt;li&gt;issues introduced by JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 25% failure rate on image &lt;code&gt;alt&lt;/code&gt; does not mean 25% of the site is inaccessible. It means the quick triage found a fixable issue on 3 of 12 homepages.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I ran it
&lt;/h2&gt;

&lt;p&gt;I used an Apify Actor that crawls a page, runs the checks and returns one JSON item per page with a score and a list of issues. For a bulk run, the same engine accepts a list of URLs.&lt;/p&gt;

&lt;p&gt;The data in this article comes from one run on 15 URLs. The full run took a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd fix first
&lt;/h2&gt;

&lt;p&gt;If I were responsible for one of these sites:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a canonical URL to every page.&lt;/li&gt;
&lt;li&gt;Add a meta description to the templates that lack one.&lt;/li&gt;
&lt;li&gt;Run an &lt;code&gt;alt&lt;/code&gt; audit on the media library, not just the homepage.&lt;/li&gt;
&lt;li&gt;Check every form field has a real &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add JSON-LD for the organisation and the main services.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these require a redesign. They are template-level fixes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The audit was run with the &lt;a href="https://apify.com/calimero-projects/website-accessibility-seo-audit" rel="noopener noreferrer"&gt;Website Accessibility &amp;amp; SEO Audit&lt;/a&gt; Actor on Apify. The sample is small and public; results are indicative, not a compliance assessment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>seo</category>
      <category>webdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
