feat: add seo diagnostics tool with cli commands, config paths, and static versioning
Add SEO_REPORTS_DIR to config, STATIC_VERSION for cache-busting, seo prune/clear CLI subcommands, tools router with seo job endpoints, and boot-versioned static URLs in Dockerfile and Makefile
This commit is contained in:
@@ -566,6 +566,35 @@ ACTIONS: tuple[Action, ...] = (
|
||||
params=(path("uid", "Fork job uid returned by fork_project."),),
|
||||
requires_auth=False,
|
||||
),
|
||||
Action(
|
||||
name="seo_diagnostics",
|
||||
method="POST",
|
||||
path="/tools/seo/run",
|
||||
summary="Run an SEO audit on a URL or sitemap",
|
||||
description=(
|
||||
"Queues a background SEO diagnostics job and returns {uid, status_url}. Poll the "
|
||||
"status_url with seo_status until status is 'done', then share the score, grade and "
|
||||
"report_url. mode='url' audits a single page; mode='sitemap' crawls up to max_pages."
|
||||
),
|
||||
params=(
|
||||
body("url", "The page URL or sitemap.xml URL to audit.", required=True),
|
||||
body("mode", "'url' for a single page or 'sitemap' to crawl a sitemap."),
|
||||
body("max_pages", "Maximum pages to crawl in sitemap mode (1-50)."),
|
||||
),
|
||||
requires_auth=False,
|
||||
),
|
||||
Action(
|
||||
name="seo_status",
|
||||
method="GET",
|
||||
path="/tools/seo/{uid}",
|
||||
summary="Check an SEO audit and obtain its score once finished",
|
||||
description=(
|
||||
"Returns the audit status. When status is 'done', score, grade and report_url are "
|
||||
"populated; while 'pending' or 'running', poll again shortly."
|
||||
),
|
||||
params=(path("uid", "SEO job uid returned by seo_diagnostics."),),
|
||||
requires_auth=False,
|
||||
),
|
||||
Action(
|
||||
name="search_users",
|
||||
method="GET",
|
||||
|
||||
@@ -13,6 +13,7 @@ from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
|
||||
from devplacepy.net_guard import effective_address, is_blocked_address
|
||||
from ..config import Settings
|
||||
from ..errors import NetworkError, ToolInputError, UpstreamError
|
||||
from ..text import html_to_text
|
||||
@@ -49,21 +50,6 @@ TITLE = re.compile(r"<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL)
|
||||
MIN_FETCH_CHARS = 1000
|
||||
ALLOWED_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")
|
||||
|
||||
NAT64_PREFIXES = (
|
||||
ipaddress.ip_network("64:ff9b::/96"),
|
||||
ipaddress.ip_network("64:ff9b:1::/48"),
|
||||
)
|
||||
|
||||
|
||||
def _effective_address(address: Any) -> Any:
|
||||
if isinstance(address, ipaddress.IPv6Address):
|
||||
if address.ipv4_mapped is not None:
|
||||
return address.ipv4_mapped
|
||||
for prefix in NAT64_PREFIXES:
|
||||
if address in prefix:
|
||||
return ipaddress.IPv4Address(int(address) & 0xFFFFFFFF)
|
||||
return address
|
||||
|
||||
|
||||
class FetchController:
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
@@ -218,17 +204,10 @@ class FetchController:
|
||||
except socket.gaierror as exc:
|
||||
raise NetworkError(f"Could not resolve host: {host}", host=host) from exc
|
||||
for info in infos:
|
||||
address = _effective_address(ipaddress.ip_address(info[4][0]))
|
||||
if (
|
||||
address.is_private
|
||||
or address.is_loopback
|
||||
or address.is_link_local
|
||||
or address.is_reserved
|
||||
or address.is_multicast
|
||||
or address.is_unspecified
|
||||
):
|
||||
address = ipaddress.ip_address(info[4][0])
|
||||
if is_blocked_address(address):
|
||||
raise ToolInputError(
|
||||
f"Refusing to fetch a private or local address ({address}). "
|
||||
f"Refusing to fetch a private or local address ({effective_address(address)}). "
|
||||
"Set DEVII_FETCH_ALLOW_PRIVATE=1 to override."
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user