From cbc27fd99a7c7ae5debf133e3957946df7da2c67 Mon Sep 17 00:00:00 2001 From: Ryan Huang Date: Fri, 19 Jun 2026 00:26:18 +0800 Subject: [PATCH] Fix Serve LLM template doc links Signed-off-by: Ryan Huang --- doc/source/conf.py | 94 +++++++++++++++++++++++++++++++-- doc/source/custom_directives.py | 26 +++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 94c1380eba1b..5fb17a451d2f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,3 +1,4 @@ +import html import io import json import logging @@ -273,6 +274,37 @@ def _urlopen_read_with_retries(url, timeout): }, } +_DEPLOYMENT_SERVE_LLM_COLLECTION = ( + "_collections/serve/tutorials/deployment-serve-llm" +) +_DEPLOYMENT_SERVE_LLM_TUTORIALS = ( + "small-size-llm", + "medium-size-llm", + "large-size-llm", + "vision-llm", + "reasoning-llm", + "hybrid-reasoning-llm", + "gpt-oss", +) +_DEPLOYMENT_SERVE_LLM_TUTORIAL_RE = "|".join( + re.escape(name) for name in _DEPLOYMENT_SERVE_LLM_TUTORIALS +) +_DEPLOYMENT_SERVE_LLM_DOC_URL_RE = re.compile( + "https://docs\\.ray\\.io/en/[^/]+/" + "serve/tutorials/deployment-serve-llm/" + f"(?:content/)?(?P{_DEPLOYMENT_SERVE_LLM_TUTORIAL_RE})" + "(?:/content)?/README\\.html" +) +_DEPLOYMENT_SERVE_LLM_LEGACY_REDIRECTS = { + legacy: f"{_DEPLOYMENT_SERVE_LLM_COLLECTION}/{tutorial}/README.html" + for tutorial in _DEPLOYMENT_SERVE_LLM_TUTORIALS + for legacy in ( + f"serve/tutorials/deployment-serve-llm/{tutorial}/README.html", + f"serve/tutorials/deployment-serve-llm/content/{tutorial}/README.html", + f"serve/tutorials/deployment-serve-llm/content/{tutorial}/content/README.html", + ) +} + def _resolve_template_url(name): """Fetch the build zip URL for a template from the channel API.""" @@ -331,6 +363,56 @@ def _fetch_and_extract_zip(config): shutil.rmtree(target, ignore_errors=True) +def _rewrite_deployment_serve_llm_links(source: str, docname: str) -> str: + relative_docname = docname.removeprefix( + f"{_DEPLOYMENT_SERVE_LLM_COLLECTION}/" + ) + parent_depth = len(pathlib.PurePosixPath(relative_docname).parent.parts) + relative_prefix = "../" * parent_depth + return _DEPLOYMENT_SERVE_LLM_DOC_URL_RE.sub( + lambda match: f"{relative_prefix}{match.group('tutorial')}/README.md", + source, + ) + + +def _redirect_html(target_url: str) -> str: + escaped_target = html.escape(target_url, quote=True) + js_target = json.dumps(target_url) + return f""" + + + + Redirecting... + + + + + +

Redirecting to {escaped_target}.

+ + +""" + + +def write_legacy_template_redirects(app, exception): + if exception is not None or app.builder.format != "html": + return + + outdir = pathlib.Path(app.outdir) + for legacy_path, target_path in _DEPLOYMENT_SERVE_LLM_LEGACY_REDIRECTS.items(): + target_file = outdir / target_path + if not target_file.exists(): + continue + + redirect_file = outdir / legacy_path + redirect_file.parent.mkdir(parents=True, exist_ok=True) + relative_target = os.path.relpath(target_file, redirect_file.parent) + relative_target = relative_target.replace(os.sep, "/") + redirect_file.write_text(_redirect_html(relative_target), encoding="utf-8") + + collections = { name: { "driver": "function", @@ -968,11 +1050,17 @@ def mark_orphans(app, docname, _source): # lines render as shell. _MAGIC_CODE_BLOCK_RE = re.compile(r"```python\n((?:#[^\n]*\n)*)([!%]\S)") - def fix_collections_code_blocks(app, docname, source): + def fix_collections_markdown(app, docname, source): if docname.startswith("_collections/"): source[0] = _MAGIC_CODE_BLOCK_RE.sub(r"```ipython3\n\1\2", source[0]) - - app.connect('source-read', fix_collections_code_blocks) + if ( + docname.startswith(f"{_DEPLOYMENT_SERVE_LLM_COLLECTION}/") + and docname.endswith("/README") + ): + source[0] = _rewrite_deployment_serve_llm_links(source[0], docname) + + app.connect('source-read', fix_collections_markdown) + app.connect("build-finished", write_legacy_template_redirects) app.add_config_value("ipython3_lexer_patterns", [], "env") app.add_config_value("ipython3_lexer_exclude_patterns", [], "env") diff --git a/doc/source/custom_directives.py b/doc/source/custom_directives.py index 3ca1b43a0995..3d4828a81c99 100644 --- a/doc/source/custom_directives.py +++ b/doc/source/custom_directives.py @@ -67,6 +67,31 @@ .split("\n") ) +_LEGACY_COLLECTION_HREF_REWRITES = ( + ( + "serve/tutorials/deployment-serve-llm/content/", + "_collections/serve/tutorials/deployment-serve-llm/", + ), + ( + "serve/tutorials/deployment-serve-llm/", + "_collections/serve/tutorials/deployment-serve-llm/", + ), +) +_DEPLOYMENT_SERVE_LLM_COLLECTION_PREFIX = ( + "_collections/serve/tutorials/deployment-serve-llm/" +) + + +def canonicalize_collection_href(href: str) -> str: + """Map legacy in-tree template links to fetched _collections pages.""" + for old_prefix, new_prefix in _LEGACY_COLLECTION_HREF_REWRITES: + if href.startswith(old_prefix): + href = new_prefix + href[len(old_prefix) :] + break + if href.startswith(_DEPLOYMENT_SERVE_LLM_COLLECTION_PREFIX): + href = href.replace("/content/README.html", "/README.html") + return href + def feedback_form_url(project, page): """Create a URL for feedback on a particular page in a project.""" @@ -350,6 +375,7 @@ def preload_sidebar_nav( for a in soup.select("a"): absolute_href = re.sub(r"^(\.\.\/)*", "", a["href"]) + absolute_href = canonicalize_collection_href(absolute_href) a["href"] = to_root_prefix + absolute_href if absolute_href == f"{pagename}.html":