Claude AI

Mixed Content Audit Claude Skill

This skill audits an HTTPS website for mixed content and insecure resources, and produces an actionable report. It crawls pages and inspects every subresource to find active mixed content (http scripts, stylesheets, iframes, and objects browsers block on https pages), insecure form actions that submit over http, passive mixed content (http images, audio, and video), legacy protocol-relative (//) resource URLs, and pages still served over http. Mixed content breaks pages and strips the padlock; the skill finds every instance and groups it by root cause.

What The Skill Checks

  • Active mixed content: http scripts, stylesheets, iframes, and objects that browsers block on https pages, plus insecure form actions that submit over http.
  • Passive mixed content: http images, audio, and video that strip the padlock and trigger the browser "Not secure" warning.
  • Migration gaps: legacy protocol-relative (//) resource URLs and pages still served over http.

How The Skill Is Packaged

The skill follows the standard Claude Agent Skill structure: a SKILL.md file with YAML frontmatter and workflow instructions, a references/ folder with the full audit check definitions and report template, and a scripts/ folder with Python scripts that crawl the site and extract every subresource URL, then audit each one for insecure http and protocol-relative references. Every report can be exported to a shareable Word .docx or a flat .csv of findings via the bundled md_to_docx.py and findings_to_csv.py scripts. Install it with npx claude-seo-skills install mixed-content-audit, or copy the skill folder into your Claude skills directory; Claude invokes it automatically when a request matches its description.

Skill Files

Every file in the skill is shown below, copied from the Claude-SEO-Skills repository, so you can review exactly what the skill instructs Claude to do before installing it.

SKILL.md

The skill definition: frontmatter, inputs to collect, the workflow, and the resources the skill relies on.

SKILL.mdView on GitHub
name
mixed-content-audit
description
Audit an HTTPS website for mixed content and insecure resources. Crawls pages and inspects every subresource to find active mixed content (http scripts, stylesheets, iframes, objects browsers block on https pages), insecure form actions that submit over http, passive mixed content (http images, audio, video), legacy protocol-relative (//) resource URLs, and pages still served over http. Use when the user asks to audit, check, or fix mixed content, insecure resources, the padlock / not-secure warning, an incomplete HTTPS migration, or http resources on an https site.

Mixed Content / HTTPS Audit

Audit an HTTPS site for insecure (http://) resources and produce an actionable report. Mixed content breaks pages and strips the padlock; this skill finds every instance and groups it by root cause.

When to use this skill

Use this skill when the user asks to:

  • Audit or fix mixed content / the browser "Not secure" warning
  • Find http:// scripts, styles, iframes, images, or media on https:// pages
  • Verify an HTTPS migration is complete
  • Find insecure form actions
Inputs to collect
  1. Site URL or URL list — a live https:// site root to crawl (auto-seeds from /sitemap.xml), or a text file with --url-list.
  2. Scope — pages to crawl (default 500, --max-pages).
Workflow
Step 1: Crawl and inventory subresources

Use scripts/extract_resources.py:

python3 scripts/extract_resources.py https://example.com --max-pages 500 --output resource_inventory.json
# already crawled once (e.g. in a full SEO audit)? skip the crawl and reuse the shared cache:
# python3 scripts/fetch_pages.py https://example.com --output page_cache.json
# python3 scripts/extract_resources.py --from-cache page_cache.json --output resource_inventory.json

For every page it records each subresource (scripts, stylesheets, images, iframes, media, embeds, form actions) with its tag, attribute, resolved URL, and scheme.

Step 2: Run the audit checks
python3 scripts/audit_resources.py resource_inventory.json --output audit_report.json
Step 3: Evaluate the audit checks

Evaluate each check in references/audit-checks.md. Core checks:

CheckSeverity
Active mixed content (script/style/iframe/object over http)High
Insecure form action (http)High
Passive mixed content (img/audio/video over http)Medium
Protocol-relative (//) resourceLow
Page served over httpInfo
Step 4: Produce the report

Write a report following references/report-template.md: summary by severity, active mixed content and insecure forms first, then passive, grouped by the host serving the insecure resource, and a prioritized action list.

Step 5: Recommend fixes
  • Active/passive mixed content: load the resource over https:// (most hosts already support it — just change the scheme), self-host it, or remove it.
  • Insecure form action: change the action to https://.
  • Protocol-relative URLs: make them explicit https://.
  • Insecure pages: fix the site-wide HTTPS redirect at the source (cross-check the redirect-audit skill).
  • Group fixes by host — one insecure CDN usually explains many rows.
  • Base every recommendation on the crawled inventory; never invent URLs.
Optional: export the report
As a Word document (.docx)

If the user wants the report as a .docx (for example, to share with stakeholders or attach to a ticket), save the Markdown report to a file and convert it:

python3 scripts/md_to_docx.py report.md --output report.docx

scripts/md_to_docx.py uses only the Python standard library (no pip install) and renders headings, tables, lists, links, bold/italic, and code blocks. Offer this whenever a user asks for a Word doc, a .docx, or a shareable/downloadable report.

As a CSV of findings (.csv)

If the user wants the raw findings as a spreadsheet (for filtering, sorting, or triage in Sheets or Excel), convert the audit's audit_report.json directly:

python3 scripts/findings_to_csv.py audit_report.json --output findings.csv

scripts/findings_to_csv.py is also standard-library only. It writes one row per finding, with check and severity columns prepended and list fields (e.g. the pages sharing a duplicate value) joined with ; . Unlike the .docx, which reformats the written report, the CSV is a direct dump of the structured findings — offer it whenever a user wants the data itself, a spreadsheet, or to slice findings by check or severity.

Resources
  • scripts/md_to_docx.py — convert the Markdown report into a Word (.docx) document (standard library only)
  • scripts/findings_to_csv.py — flatten the audit findings JSON into a CSV, one row per finding (standard library only)
  • references/audit-checks.md — full definitions and rationale
  • references/report-template.md — report output structure
  • scripts/extract_resources.py — crawl and inventory subresources
  • scripts/audit_resources.py — run mixed-content audit checks

Audit Checks Reference (references/audit-checks.md)

Full definitions, severities, and rationale for every check across the high, medium, and low tiers.

references/audit-checks.mdView on GitHub

Mixed Content / HTTPS Audit Checks

Definitions and rationale. "Mixed content" is an insecure (http://) resource loaded by a secure (https://) page. Browsers block or warn on it, which breaks layout and functionality and undermines the padlock — a trust and (indirectly) ranking signal.

High severity
1. Active mixed content
  • Definition: An https:// page loads a <script src>, <link rel=stylesheet>, <iframe>, <object>, or <embed> over http://.
  • Why it matters: Browsers block insecure active content outright. The script doesn't run, the stylesheet doesn't apply, the iframe doesn't load — the page is visibly broken, and the connection is no longer trustworthy.
  • Fix: Load the resource over https:// (most hosts support it — just change the scheme), self-host it, or remove it.
2. Insecure form action
  • Definition: A <form> on an https:// page submits to an http:// action URL.
  • Why it matters: Form data (including credentials) is sent in cleartext; browsers show a prominent "Not secure" warning on submit.
  • Fix: Change the form action to https://.
Medium severity
3. Passive mixed content
  • Definition: An https:// page loads an <img>, <audio>, <video>, <source>, or <track> over http://.
  • Why it matters: Browsers may load it but downgrade the security indicator (no padlock) and can block it under stricter settings. It still signals an incomplete HTTPS migration.
  • Fix: Serve the media over https://.
Low severity
4. Protocol-relative resource
  • Definition: A resource URL beginning with // (inheriting the page's scheme), e.g. //cdn.example.com/app.js.
  • Why it matters: Harmless on an HTTPS page today, but a legacy pattern that breaks if the page is ever viewed over HTTP and obscures intent.
  • Fix: Use explicit https:// URLs.
Informational (note, not counted as a mixed-content issue)
  • Insecure page — the page itself was served over http:// (or didn't redirect to HTTPS). This isn't "mixed" content (there's nothing secure to mix), but it's the bigger problem: the whole page is insecure. Reported so the HTTPS migration / redirect can be fixed at the source. Cross-check with the redirect-audit and site-architecture-audit skills.

Report Template (references/report-template.md)

The output structure the skill follows, including the sections issues are grouped into.

references/report-template.mdView on GitHub

Mixed Content / HTTPS Audit Report Template

Use this structure for the final report delivered to the user.

# Mixed Content / HTTPS Audit — {site}

**Date:** {date}
**Scope:** {pages crawled}
**Pages analyzed:** {n} | **Pages with mixed content:** {n}

## Summary

| Severity | Issues |
|---|---|
| High | {n} |
| Medium | {n} |
| Low | {n} |

{2–3 sentences: is the HTTPS migration complete? Name the dominant source of
insecure resources (often one CDN/host or one template).}

## High-severity issues

### Active mixed content ({n})
Blocked by browsers — these break the page.

| Page | Tag | Insecure resource | Fix |
|---|---|---|---|
| ... | script/link/iframe | http://... | Load over https:// |

### Insecure form actions ({n})
| Page | Form action | Fix |
|---|---|---|
| ... | http://... | Change action to https:// |

## Medium-severity issues

### Passive mixed content ({n})
| Page | Tag | Insecure resource |
|---|---|---|
| ... | img/video/audio | http://... |

## Low-severity issues

{Protocol-relative resources, as a brief list grouped by host.}

## Insecure pages to fix at the source ({n})

| Page served over http | Note |
|---|---|
| ... | redirect to https / fix HSTS |

## Prioritized action list

1. {Highest-impact fix — usually "switch host/CDN X to https", which clears many
   rows at once. Name the host and the affected pages.}
2. ...
Guidance
  • Group insecure resources by host — one CDN or analytics provider on http usually explains most rows and is a single fix.
  • Lead with active mixed content and insecure forms; those break functionality.
  • Every row names the exact page, the tag/attribute, and the insecure URL; the fix is almost always "use https://".
  • If many pages are served over http, the real fix is the site-wide HTTPS redirect — note it and point to the redirect-audit skill.
  • Base everything on the crawled resource inventory; never invent URLs.

Resource Extractor Script (scripts/extract_resources.py)

Crawls pages and inventories every subresource: scripts, styles, iframes, images, media, and form actions.

scripts/extract_resources.pyView on GitHub
#!/usr/bin/env python3
"""Crawl an HTTPS site and inventory every subresource and its protocol.

Usage:
    python3 extract_resources.py https://example.com [--max-pages 500] [--output resource_inventory.json]
    python3 extract_resources.py urls.txt --url-list [--output resource_inventory.json]

For each crawled page it records the page URL/status and every subresource it
loads or links to — scripts, stylesheets, images, iframes, media, embeds, and
form actions — with the tag, attribute, and the (resolved) URL's scheme. The
auditor uses this to find mixed content (http resources on an https page).

Uses only the Python standard library.
"""

import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from collections import deque
from html.parser import HTMLParser

USER_AGENT = "MixedContentAuditBot/1.0 (+claude-skill)"
TIMEOUT = 15

# tag -> attribute that holds a subresource URL, and whether it is "active"
# (executed/applied; browsers block insecure active content) or "passive".
RESOURCE_ATTRS = {
    "script": ("src", "active"),
    "link": ("href", "active"),       # stylesheets / preloads (rel-checked below)
    "iframe": ("src", "active"),
    "embed": ("src", "active"),
    "object": ("data", "active"),
    "img": ("src", "passive"),
    "audio": ("src", "passive"),
    "video": ("src", "passive"),
    "source": ("src", "passive"),
    "track": ("src", "passive"),
    "form": ("action", "form"),
}


class ResourceParser(HTMLParser):
    def __init__(self, base_url):
        super().__init__(convert_charrefs=True)
        self.base_url = base_url
        self.resources = []  # {tag, attr, url, scheme, kind}
        self.links = []

    def _record(self, tag, attr, value, kind):
        if not value:
            return
        resolved = urllib.parse.urljoin(self.base_url, value.strip())
        scheme = urllib.parse.urlparse(resolved).scheme
        protocol_relative = value.strip().startswith("//")
        self.resources.append({
            "tag": tag, "attr": attr, "url": resolved, "scheme": scheme,
            "kind": kind, "protocol_relative": protocol_relative})

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag in RESOURCE_ATTRS:
            attr, kind = RESOURCE_ATTRS[tag]
            if tag == "link":
                rel = (a.get("rel") or "").lower()
                # Only stylesheets/imports are active; icons/manifests are passive-ish.
                kind = "active" if "stylesheet" in rel else "passive"
                if not any(r in rel for r in ("stylesheet", "preload", "icon",
                                              "manifest", "prefetch", "preconnect")):
                    return
            self._record(tag, attr, a.get(attr), kind)
        if tag == "a" and a.get("href"):
            href = a["href"].strip()
            if not href.startswith(("mailto:", "tel:", "javascript:", "#")):
                self.links.append(
                    urllib.parse.urldefrag(urllib.parse.urljoin(self.base_url, href))[0])


def fetch(url):
    req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
    try:
        with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
            ctype = resp.headers.get("Content-Type", "")
            body = resp.read(2_000_000).decode("utf-8", errors="replace") \
                if "html" in ctype else None
            return resp.status, resp.geturl(), body
    except urllib.error.HTTPError as e:
        return e.code, url, None
    except Exception:
        return 0, url, None


def norm_host(url):
    h = urllib.parse.urlparse(url).netloc.lower()
    return h[4:] if h.startswith("www.") else h


def pages_from_sitemap(site):
    status, _f, body = fetch(urllib.parse.urljoin(site, "/sitemap.xml"))
    if status != 200 or not body:
        return []
    try:
        root = ET.fromstring(body)
        return [loc.text.strip() for loc in
                root.iter("{http://www.sitemaps.org/schemas/sitemap/0.9}loc") if loc.text]
    except ET.ParseError:
        return []


def extract_from_cache(cache_path):
    """Pure: turn a shared page cache (from fetch_pages.py) into a resource inventory.

    No network — parses the cached HTML for the subresource references this audit needs.
    """
    with open(cache_path, encoding="utf-8") as f:
        cache = json.load(f)
    pages = {}
    for url, page in cache.get("pages", {}).items():
        html = page.get("html")
        status = page.get("status", 0)
        final_url = page.get("final_url") or url
        if not html:
            pages[url] = {"status": status, "final_url": final_url, "resources": []}
            continue
        parser = ResourceParser(final_url)
        try:
            parser.feed(html)
        except Exception:
            pass
        pages[url] = {"status": status, "final_url": final_url,
                      "resources": parser.resources}
    return cache.get("site", cache_path), pages


def collect(urls, max_pages, crawl_host=None):
    pages, queue, seen = {}, deque(urls), set(urls)
    while queue and len(pages) < max_pages:
        url = queue.popleft()
        status, final_url, html = fetch(url)
        print(f"[{status}] {url}", file=sys.stderr)
        if html is None:
            pages[url] = {"status": status, "final_url": final_url, "resources": []}
            continue
        parser = ResourceParser(final_url)
        try:
            parser.feed(html)
        except Exception:
            pass
        pages[url] = {"status": status, "final_url": final_url,
                      "resources": parser.resources}
        if crawl_host:
            for link in parser.links:
                if norm_host(link) == crawl_host and link not in seen:
                    seen.add(link)
                    queue.append(link)
    return pages


def main():
    ap = argparse.ArgumentParser(description="Inventory subresources and their protocols.")
    ap.add_argument("source", nargs="?")
    ap.add_argument("--from-cache",
                    help="Extract from a shared page cache produced by fetch_pages.py (no network)")
    ap.add_argument("--max-pages", type=int, default=500)
    ap.add_argument("--output", default="resource_inventory.json")
    ap.add_argument("--url-list", action="store_true")
    args = ap.parse_args()

    if args.from_cache:
        site, pages = extract_from_cache(args.from_cache)
    elif not args.source:
        ap.error("provide a source, or --from-cache page_cache.json")
    elif args.url_list:
        with open(args.source, encoding="utf-8") as f:
            urls = [l.strip() for l in f if l.strip() and not l.startswith("#")]
        site, pages = args.source, collect(urls[: args.max_pages], args.max_pages)
    else:
        start = args.source.rstrip("/")
        seeds = pages_from_sitemap(start) or [start]
        site, pages = args.source, collect(seeds[: args.max_pages], args.max_pages,
                                           crawl_host=norm_host(start))

    total = sum(len(p["resources"]) for p in pages.values())
    with open(args.output, "w", encoding="utf-8") as f:
        json.dump({"site": site, "pages": pages}, f, indent=2, ensure_ascii=False)
    print(f"Wrote {len(pages)} pages, {total} resource references to {args.output}",
          file=sys.stderr)


if __name__ == "__main__":
    main()

Resource Auditor Script (scripts/audit_resources.py)

Audits each subresource for insecure http and protocol-relative URLs, then outputs the structured report data grouped by root cause.

scripts/audit_resources.pyView on GitHub
#!/usr/bin/env python3
"""Audit a resource inventory (from extract_resources.py) for mixed content.

Usage:
    python3 audit_resources.py resource_inventory.json [--output audit_report.json]

Checks (see references/audit-checks.md):
- active_mixed_content (high): http script/stylesheet/iframe/object on an https page
- insecure_form_action (high): a form on an https page POSTs/GETs to http://
- passive_mixed_content (medium): http img/audio/video/source on an https page
- protocol_relative_resource (low): // resource URLs (legacy; should be https://)
- insecure_page (info): the page itself was served over http://

Only pages served over https are checked for mixed content (http pages have no
"mixed" content — they are wholly insecure, reported separately as insecure_page).

Uses only the Python standard library.
"""

import argparse
import json
import sys
import urllib.parse


def main():
    ap = argparse.ArgumentParser(description="Run mixed-content audit checks.")
    ap.add_argument("inventory")
    ap.add_argument("--output", default="audit_report.json")
    args = ap.parse_args()

    with open(args.inventory, encoding="utf-8") as f:
        data = json.load(f)
    pages = data.get("pages", {})

    findings = {k: [] for k in (
        "active_mixed_content", "insecure_form_action", "passive_mixed_content",
        "protocol_relative_resource", "insecure_page")}

    for url, page in pages.items():
        if page.get("status", 0) == 0:
            continue
        final_url = page.get("final_url", url)
        page_https = final_url.startswith("https://")

        if not page_https and final_url.startswith("http://"):
            findings["insecure_page"].append({"page": url})
            # http pages aren't "mixed"; skip per-resource mixed checks
            continue

        for r in page.get("resources", []):
            scheme = r.get("scheme")
            entry = {"page": url, "tag": r["tag"], "attr": r["attr"], "resource": r["url"]}
            if r.get("protocol_relative"):
                findings["protocol_relative_resource"].append(entry)
                continue
            if scheme != "http":
                continue
            if r["kind"] == "form":
                findings["insecure_form_action"].append(entry)
            elif r["kind"] == "active":
                findings["active_mixed_content"].append(entry)
            else:
                findings["passive_mixed_content"].append(entry)

    severity = {
        "active_mixed_content": "high", "insecure_form_action": "high",
        "passive_mixed_content": "medium",
        "protocol_relative_resource": "low", "insecure_page": "info",
    }
    pages_with_mixed = len({f["page"] for k in
                            ("active_mixed_content", "passive_mixed_content",
                             "insecure_form_action") for f in findings[k]})
    summary = {
        "pages_analyzed": sum(1 for p in pages.values() if p.get("status", 0) != 0),
        "pages_with_mixed_content": pages_with_mixed,
        "issues_by_check": {k: len(v) for k, v in findings.items()},
        "issues_by_severity": {
            level: sum(len(findings[k]) for k, s in severity.items() if s == level)
            for level in ("high", "medium", "low", "info")},
    }
    report = {"summary": summary, "severity": severity, "findings": findings}
    with open(args.output, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2, ensure_ascii=False)
    print(json.dumps(summary, indent=2))
    print(f"Full report written to {args.output}", file=sys.stderr)


if __name__ == "__main__":
    main()

Explore Other Claude SEO Skills

Open Graph Audit Audits a site's Open Graph and Twitter Card metadata: missing og:title, og:description, og:image, or og:url, broken or relative og:image URLs, og:url that mismatches the canonical, missing twitter:card, over-length social titles, and reused default images. open-graph-audit Open Pagination Audit Audits a site's pagination: paginated pages canonicalizing to page 1 and hiding deep content, noindexed component pages, paginated pages missing a self-canonical, broken or inconsistent rel=next/prev, and redundant ?page=1 first pages. pagination-audit Open Soft 404 Audit Audits a site for soft 404s: error pages served with a 200 status, servers that return 200 for every unknown URL, thin pages matching the site's error template, and empty or near-empty pages. soft-404-audit Open Full SEO Audit The orchestrator skill that runs every individual audit below and consolidates the findings into one prioritized, cross-area report ranked by impact, for a complete technical-SEO review in a single pass. full-seo-audit Open Internal Link Audit Audits a site's internal linking structure for broken links, orphan pages, redirect chains, click depth, and anchor text quality. internal-link-audit Open External Link Audit Audits a site's external links for broken targets, redirects, insecure HTTP links, missing rel attributes on affiliate links, and unsafe target="_blank" usage. external-link-audit Open Meta Data Audit Audits a site's title tags and meta descriptions for missing, duplicate, too-long, too-short, or multiple tags, generic boilerplate titles, and keyword stuffing. meta-data-audit Open Schema Markup Audit Audits a site's structured data: extracting JSON-LD, Microdata, and RDFa, then validating it against schema.org and Google rich result requirements. schema-markup-audit Open Site Architecture Audit Audits a site's architecture: hostname and protocol canonicalization, robots.txt, XML sitemap parity, crawl and index directive conflicts, URL structure, click depth, and directory taxonomy. site-architecture-audit Open Heading Structure Audit Audits a site's heading hierarchy for missing or multiple H1s, skipped heading levels, empty headings, headings that don't start at H1, overly long headings, and duplicate heading text. heading-structure-audit Open Image SEO Audit Audits a site's images for SEO and Core Web Vitals: missing alt text, absent width/height, missing lazy-loading, oversized files, legacy formats, generic filenames, and broken images. image-seo-audit Open Core Web Vitals Audit Audits a site's Core Web Vitals and page-experience signals (LCP, CLS, INP, TTFB), pulling real CrUX field data from PageSpeed Insights and collecting lab proxies like render-blocking resources, page weight, and image hygiene. core-web-vitals-audit Open Redirect Audit Audits a site's redirects for redirect chains and loops, redirects ending in 404s or errors, 302s used for permanent moves, HTTPS-to-HTTP downgrades, client-side redirects, dropped query strings, and canonicals pointing at redirecting URLs. redirect-audit Open Robots.txt Audit Audits a site's robots.txt for a redirected or HTML-served file, a 5xx read as block-everything, a sitewide Disallow, syntax errors, blocked CSS/JS, missing Sitemap directives, unsupported directives, oversized files, and encoding issues. robots-txt-audit Open Sitemap Audit Audits a site's XML sitemaps: discovering them from robots.txt, following sitemap index files, and validating every URL for broken or redirecting targets, noindexed or canonicalized pages, wrong host or protocol, duplicates, oversized sitemaps, and invalid lastmod values. sitemap-audit Open Llms.txt Audit Audits a site's /llms.txt for AI and LLM discoverability: missing files, HTML served instead of markdown, a missing H1 or summary, broken or redirecting links, links missing descriptions, wrong-host or relative URLs, and a companion /llms-full.txt. llms-txt-audit Open Canonical Tag Audit Audits a site's rel=canonical tags for missing canonicals, multiple conflicting canonicals, canonicals pointing at non-200, redirecting, or noindexed URLs, relative or cross-host canonicals, https-to-http downgrades, and Link-header conflicts. canonical-tag-audit Open Content Quality Audit Audits a site's content quality for thin content, exact-duplicate body text across pages, near-duplicate (templated) pages, low text-to-HTML ratio, and pages missing an H1. content-quality-audit Open Keyword Cannibalization Audit Audits a site for keyword cannibalization: pages with duplicate title targets, clusters of pages whose title, H1, and keyword signatures overlap, and pages sharing the same primary keyword phrase, with a recommended hierarchy. keyword-cannibalization-audit Open