Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
},
{
"group": "Templates",
"root": "api-reference/overview",
"pages": [
"api-reference/templates/POST/templates",
"api-reference/templates/GET/templates",
Expand Down Expand Up @@ -580,13 +581,8 @@
},
{
"tab": "Release notes",
"groups": [
{
"group": "Release notes",
"pages": [
"release-notes"
]
}
"pages": [
"release-notes"
]
}
]
Expand Down Expand Up @@ -1302,4 +1298,4 @@
"destination": "/serverless/endpoints/send-requests"
}
]
}
}
102 changes: 102 additions & 0 deletions scripts/validate-breadcrumb-jsonld.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Validate breadcrumb JSON-LD in raw served HTML."""

from __future__ import annotations

import argparse
import json
import sys
from html.parser import HTMLParser
from urllib.parse import urljoin
from urllib.request import Request, urlopen


class JsonLdParser(HTMLParser):
def __init__(self) -> None:
super().__init__()
self._capture = False
self._parts: list[str] = []
self.scripts: list[str] = []

def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag != "script":
return
attributes = dict(attrs)
if attributes.get("type") == "application/ld+json":
self._capture = True
self._parts = []

def handle_data(self, data: str) -> None:
if self._capture:
self._parts.append(data)

def handle_endtag(self, tag: str) -> None:
if tag == "script" and self._capture:
self.scripts.append("".join(self._parts))
self._capture = False
self._parts = []


def walk_json(value: object):
if isinstance(value, dict):
yield value
for child in value.values():
yield from walk_json(child)
elif isinstance(value, list):
for child in value:
yield from walk_json(child)


def validate_url(url: str) -> list[str]:
request = Request(url, headers={"User-Agent": "Runpod breadcrumb validator/1.0"})
with urlopen(request, timeout=30) as response:
html = response.read().decode(response.headers.get_content_charset() or "utf-8")

parser = JsonLdParser()
parser.feed(html)
errors: list[str] = []
breadcrumb_count = 0

for raw_script in parser.scripts:
try:
data = json.loads(raw_script)
except json.JSONDecodeError:
continue
for node in walk_json(data):
if node.get("@type") != "BreadcrumbList":
continue
breadcrumb_count += 1
items = node.get("itemListElement")
if not isinstance(items, list) or not items:
errors.append("BreadcrumbList has no itemListElement entries")
continue
for index, item in enumerate(items[:-1], start=1):
if not isinstance(item, dict) or not item.get("item"):
errors.append(f'position {index} is missing required field "item"')

if breadcrumb_count == 0:
errors.append("no BreadcrumbList JSON-LD found in raw HTML")
return errors


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True)
parser.add_argument("paths", nargs="+")
args = parser.parse_args()

failed = False
for path in args.paths:
url = urljoin(args.base_url.rstrip("/") + "/", path.lstrip("/"))
errors = validate_url(url)
if errors:
failed = True
for error in errors:
print(f"FAIL {url}: {error}")
else:
print(f"PASS {url}")
return 1 if failed else 0


if __name__ == "__main__":
sys.exit(main())
Loading