@@ -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+
243257def 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+
426476def 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+
481568def 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"""---
509599title: "{ 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"""---
528619title: "{ 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"""---
551643title: "{ 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"""---
673767title: "{ 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