Practice auditing live webpages for SEO meta tags, OpenGraph properties, GA4 tracking codes, JSON-LD schemas, and alt attribute coverage directly inside Google Colab.
View on GitHubWant to quickly check if a webpage has all the essential SEO and AI-friendly elements without using paid tools or API keys? In this guide, we’ll build and run a Python-based webpage auditor using BeautifulSoup and requests. The script works offline, requires no authentication, and returns practical, actionable results.
Make sure you have the following packages installed:
pip install requests beautifulsoup4 lxml
simple_auditor.py 🔝Here’s the full working script. Change url_to_audit to the webpage you want to check.
# simple_auditor.py
# Minimal, API-key-free webpage auditor with case-insensitive meta extraction.
# Change `url_to_audit` to target another page.
import re
import requests
from urllib.parse import urlparse
from bs4 import BeautifulSoup
# ====== Change this URL to audit another page ======
url_to_audit = "https://www.plus2net.com/python/pandas.php"
UA = "Mozilla/5.0 (compatible; AI-Ready-Auditor/1.1; +https://example.com)"
TIMEOUT = 30
def fetch_html(url: str) -> tuple[int, str]:
r = requests.get(url, headers={"User-Agent": UA}, timeout=TIMEOUT)
return r.status_code, r.text
def get_meta_by_name(soup: BeautifulSoup, meta_name: str):
tag = soup.find("meta", attrs={"name": re.compile(re.escape(meta_name) + "$", re.I)})
return (tag.get("content") or "").strip() if tag and tag.get("content") else None
def get_meta_by_property(soup: BeautifulSoup, prop_name: str):
tag = soup.find("meta", attrs={"property": re.compile(re.escape(prop_name) + "$", re.I)})
return (tag.get("content") or "").strip() if tag and tag.get("content") else None
def extract_schema_flags(json_ld_blocks: list[str]):
j = "\n".join(json_ld_blocks)
return "FAQPage" in j, "HowTo" in j, "VideoObject" in j
def analyze(url: str):
status, html = fetch_html(url)
soup = BeautifulSoup(html, "lxml")
title = soup.title.string.strip() if soup.title and soup.title.string else None
meta_description = get_meta_by_name(soup, "description") or get_meta_by_property(soup, "og:description")
meta_keywords = get_meta_by_name(soup, "keywords")
meta_viewport = get_meta_by_name(soup, "viewport")
h1_tag = soup.find("h1")
h1 = h1_tag.get_text(strip=True) if h1_tag else None
canonical_tag = soup.find("link", rel=lambda v: v and v.lower() == "canonical")
canonical_url = canonical_tag.get("href") if canonical_tag else None
ga4_ids = list(sorted(set(re.findall("G-[A-Z0-9]{6,12}", html))))
has_ga4 = bool(ga4_ids)
og_tags = {
t.get("property"): t.get("content")
for t in soup.find_all("meta", attrs={"property": re.compile("^og:", re.I)})
if t.get("property") and t.get("content")
}
imgs = soup.find_all("img")
images_total = len(imgs)
images_with_alt = sum(1 for i in imgs if (i.get("alt") or "").strip())
images_alt_coverage_pct = round((images_with_alt / images_total * 100), 2) if images_total else 0.0
parsed = urlparse(url)
base_host = parsed.netloc
internal_links, external_links = 0, 0
for a in soup.find_all("a", href=True):
p = urlparse(a.get("href"))
if not p.netloc or p.netloc == "" or p.netloc == base_host:
internal_links += 1
else:
external_links += 1
json_ld_blocks = [t.get_text() for t in soup.find_all("script", type="application/ld+json")]
has_faq_schema, has_howto_schema, has_video_schema = extract_schema_flags(json_ld_blocks)
breadcrumbs_present = bool(soup.select('[itemtype*="BreadcrumbList" i], nav.breadcrumb, ol.breadcrumb'))
root = f"{parsed.scheme}://{parsed.netloc}"
sitemap_present = False
try:
rr = requests.get(f"{root}/robots.txt", headers={"User-Agent": UA}, timeout=10)
if rr.ok and "sitemap" in rr.text.lower():
sitemap_present = True
except Exception:
pass
return {
"url": url,
"http_status": status,
"title": title,
"title_length": len(title) if title else None,
"meta_description": meta_description,
"meta_description_length": len(meta_description) if meta_description else None,
"h1": h1,
"canonical_url": canonical_url,
"ga4_ids": ga4_ids,
"images_total": images_total,
"images_with_alt": images_with_alt,
"images_alt_coverage_pct": images_alt_coverage_pct,
"internal_links": internal_links,
"external_links": external_links,
"has_faq_schema": has_faq_schema,
"has_howto_schema": has_howto_schema,
"has_videoobject_schema": has_video_schema,
"breadcrumbs_present": breadcrumbs_present,
"sitemap_present": sitemap_present
}
if __name__ == "__main__":
report = analyze(url_to_audit)
print(report)
requests library with a customized User-Agent header.BeautifulSoup and the lxml parser to locate essential tags case-insensitively.=== AI-Ready Webpage Audit Summary ===
URL: https://www.plus2net.com/python/pandas.php
HTTP Status: 200
Title (38 chars): Present
Meta Description (45 chars): Present
Canonical URL: https://www.plus2net.com/python/pandas.php
GA4 IDs: ['G-DXKVCW4XVG']
Images with alt: 5/5 (100.0%)
Internal Links: 12 | External Links: 3
FAQ Schema: False
Breadcrumbs: True
Sitemap in robots.txt: True
======================================
When testing locally (e.g., using XAMPP or WAMP), you can audit development pages before pushing them live. Simply change the target URL variable to point to your local development environment:
# Change target URL for localhost site auditing
url_to_audit = "http://localhost/plus2net/python/pandas.php"
When auditing local environments, `urlparse` properly identifies `localhost` as the `netloc`, maintaining accurate distinction between local internal links and external references.
To inspect the exact URLs of all internal and external hyperlinked resources, use this focused extraction snippet:
import requests
from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup
url_to_audit = "https://www.plus2net.com/python/pandas.php"
response = requests.get(url_to_audit)
soup = BeautifulSoup(response.text, "html.parser")
parsed_base = urlparse(url_to_audit)
base_host = parsed_base.netloc
internal_links = []
external_links = []
for a in soup.find_all("a", href=True):
href = a["href"].strip()
if not href or href.startswith("#") or href.startswith("javascript:"):
continue
full_url = urljoin(url_to_audit, href)
parsed_href = urlparse(full_url)
if parsed_href.netloc == base_host:
internal_links.append(full_url)
else:
external_links.append(full_url)
print("--- Internal Links ---")
for link in set(internal_links):
print(link)
print("\n--- External Links ---")
for link in set(external_links):
print(link)
User-Agent header so web servers do not mistake your script for a malicious scraper.BeautifulSoup(html, "lxml") throws an error, install lxml via pip install lxml or fallback to 'html.parser'.Q: Do I need a paid Google API key to run this audit script?
A: No. The script parses the raw HTML directly via BeautifulSoup, requiring zero API keys or rate-limit tokens.
Q: Can I audit local files instead of live URLs?
A: Yes. You can pass local file paths or read HTML file contents into BeautifulSoup instead of making requests.get() calls.
Want to take your BeautifulSoup auditing to the next level? Check out our Python SQLite Webpage Auditor that stores audit data in a database, allows deeper analysis, and exports reports to Excel for powerful SEO insights.
Learn More →Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.