Skip to content

Commit e042937

Browse files
committed
fix: sanitize HTML for MDX/JSX compatibility (class→className, self-close img, angle-bracket URLs)
75 guide pages were returning 404 because their <p><img ... class='m-auto'> HTML was invalid JSX. MDX requires className instead of class, and img tags must be self-closed. Also converts <https://url\> angle-bracket URLs to markdown links.
1 parent 40220cf commit e042937

1 file changed

Lines changed: 100 additions & 6 deletions

File tree

scripts/sync.py

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,24 @@ def load_openapi_spec(backend_dir: Path, api_id: str) -> dict | None:
240240
return json.load(f)
241241

242242

243+
def _t_key_to_title(key: str) -> str:
244+
"""Convert a $t() translation key to a clean, concise display title."""
245+
# Strip common prefixes that add no value
246+
for prefix in (
247+
"api_description_",
248+
"service_title_",
249+
"service_description_",
250+
):
251+
if key.startswith(prefix):
252+
key = key[len(prefix) :]
253+
break
254+
return key.replace("_", " ").title()
255+
256+
243257
def resolve_t_keys(obj):
244258
"""Replace $t(key) translation markers with the key itself as a readable title."""
245259
if isinstance(obj, str):
246-
return re.sub(
247-
r"\$t\(([^)]+)\)", lambda m: m.group(1).replace("_", " ").title(), obj
248-
)
260+
return re.sub(r"\$t\(([^)]+)\)", lambda m: _t_key_to_title(m.group(1)), obj)
249261
if isinstance(obj, dict):
250262
return {k: resolve_t_keys(v) for k, v in obj.items()}
251263
if isinstance(obj, list):
@@ -423,6 +435,44 @@ def clean_openapi_spec(spec: dict) -> dict:
423435
return cleaned
424436

425437

438+
_MAX_SUMMARY_LEN = 80
439+
440+
441+
def _path_to_short_title(path: str, method: str) -> str:
442+
"""Generate a concise title from an API path.
443+
444+
Example: POST /face/analyze → 'Face Analyze'
445+
POST /identity/phone/check-3e → 'Phone Check 3E'
446+
"""
447+
segments = [s for s in path.strip("/").split("/") if s]
448+
# Drop generic first segments that match the service name prefix
449+
if len(segments) > 1:
450+
segments = segments[1:] # e.g. /face/analyze → ['analyze']
451+
title = " ".join(s.replace("-", " ").replace("_", " ") for s in segments).title()
452+
return title or f"{method.upper()} {path}"
453+
454+
455+
def _clean_verbose_summaries(spec: dict):
456+
"""Ensure all operation summaries are concise.
457+
458+
If a summary exceeds _MAX_SUMMARY_LEN characters, move the full text to
459+
``description`` (if not already set) and replace the summary with a short
460+
title derived from the path.
461+
"""
462+
for path, methods in spec.get("paths", {}).items():
463+
if not isinstance(methods, dict):
464+
continue
465+
for method, op in methods.items():
466+
if not isinstance(op, dict):
467+
continue
468+
summary = op.get("summary", "")
469+
if len(summary) > _MAX_SUMMARY_LEN:
470+
# Preserve full text as description
471+
if not op.get("description"):
472+
op["description"] = summary
473+
op["summary"] = _path_to_short_title(path, method)
474+
475+
426476
def merge_openapi_specs(backend_dir: Path, service: dict) -> dict | None:
427477
"""Merge multiple per-API OpenAPI specs into one per-service spec."""
428478
apis = service.get("apis", [])
@@ -473,11 +523,48 @@ def merge_openapi_specs(backend_dir: Path, service: dict) -> dict | None:
473523

474524
# Resolve translation keys
475525
merged = resolve_t_keys(merged)
526+
# Clean verbose summaries — move long text to description, generate short title
527+
_clean_verbose_summaries(merged)
476528
# Clean for strict OpenAPI 3.0 compliance (Mintlify validation)
477529
merged = clean_openapi_spec(merged)
478530
return merged
479531

480532

533+
def _sanitize_html_for_mdx(content: str) -> str:
534+
"""Sanitize HTML in markdown content so it is valid JSX for MDX.
535+
536+
Fixes:
537+
- ``class=`` → ``className=`` (reserved word in JSX)
538+
- Self-close void elements: ``<img ...>`` → ``<img ... />``
539+
- Angle-bracket URLs: ``<https://...>`` → ``[https://...](https://...)``
540+
"""
541+
# Only transform HTML outside of fenced code blocks
542+
parts = re.split(r"(^```.*?^```)", content, flags=re.MULTILINE | re.DOTALL)
543+
for i, part in enumerate(parts):
544+
if part.startswith("```"):
545+
continue # skip code blocks
546+
# class → className in HTML tags
547+
part = re.sub(
548+
r"(<[a-zA-Z][^>]*)\bclass=",
549+
r"\1className=",
550+
part,
551+
)
552+
# Self-close <img ...> tags that aren't already self-closed
553+
part = re.sub(
554+
r"(<img\b[^>]*?)(?<!/)>",
555+
r"\1 />",
556+
part,
557+
)
558+
# Angle-bracket URLs → markdown links
559+
part = re.sub(
560+
r"<(https?://[^>]+)>",
561+
r"[\1](\1)",
562+
part,
563+
)
564+
parts[i] = part
565+
return "".join(parts)
566+
567+
481568
def convert_dev_doc_to_mdx(content: str, doc_key: str, service_name: str) -> str:
482569
"""Convert a PlatformBackend development markdown doc to Mintlify MDX format."""
483570
# Extract a title from the first heading or generate one
@@ -504,6 +591,9 @@ def convert_dev_doc_to_mdx(content: str, doc_key: str, service_name: str) -> str
504591
r"\$t\(([^)]+)\)", lambda m: m.group(1).replace("_", " ").title(), content
505592
)
506593

594+
# Sanitize HTML for MDX/JSX compatibility
595+
content = _sanitize_html_for_mdx(content)
596+
507597
# Build frontmatter
508598
frontmatter = f"""---
509599
title: "{title}"
@@ -523,6 +613,7 @@ def generate_mcp_doc(content: str, mcp_name: str) -> str:
523613
content = re.sub(
524614
r"\$t\(([^)]+)\)", lambda m: m.group(1).replace("_", " ").title(), content
525615
)
616+
content = _sanitize_html_for_mdx(content)
526617

527618
return f"""---
528619
title: "{title}"
@@ -546,6 +637,7 @@ def generate_extra_doc(content: str, doc_key: str) -> str:
546637
content = re.sub(
547638
r"\$t\(([^)]+)\)", lambda m: m.group(1).replace("_", " ").title(), content
548639
)
640+
content = _sanitize_html_for_mdx(content)
549641

550642
return f"""---
551643
title: "{title}"
@@ -651,7 +743,8 @@ def _write_seo_mdx(src: Path, dst: Path):
651743
content = src.read_text(encoding="utf-8")
652744

653745
if content.startswith("---"):
654-
# Already has frontmatter
746+
# Already has frontmatter — still sanitize HTML for MDX
747+
content = _sanitize_html_for_mdx(content)
655748
dst.write_text(content, encoding="utf-8")
656749
return
657750

@@ -669,6 +762,7 @@ def _write_seo_mdx(src: Path, dst: Path):
669762
title = src.stem.replace("_", " ").replace("-", " ").title()
670763

671764
body = "\n".join(lines[body_start:]).strip()
765+
body = _sanitize_html_for_mdx(body)
672766
mdx = f"""---
673767
title: "{title}"
674768
---
@@ -1350,10 +1444,10 @@ def main():
13501444
Midjourney、Flux、Seedream、DALL·E、QR Art、人脸工具
13511445
</Card>
13521446
<Card title="AI 视频" icon="video">
1353-
Sora、Veo、Luma、Kling、Hailuo、Seedance、Wan、Pika
1447+
Sora、Veo、Luma、Kling、Hailuo、Seedance、Wan
13541448
</Card>
13551449
<Card title="AI 音频" icon="music">
1356-
Suno、Fish Audio、Producer、Riffusion、Udio
1450+
Suno、Fish Audio、Producer
13571451
</Card>
13581452
</CardGroup>
13591453
"""

0 commit comments

Comments
 (0)