@@ -189,22 +189,30 @@ def _get_fragment(raw: str) -> str:
189189_DOC_HTML_TO_DOCNAME = {v : v .replace ('.html' , '' ) for v in _DOC_MD_TO_HTML .values ()}
190190
191191
192+ def _images_path_to_static (uri : str ) -> str :
193+ """Convert ../images/... or ../../../images/... to _static/... for ReadTheDocs.
194+
195+ On ReadTheDocs, Spanish docs are at /es/latest/; relative paths like ../../../images/
196+ escape the site root and break. Using _static/ (copied via html_static_path) works
197+ for all URL depths (en/latest/, es/latest/, etc.).
198+ """
199+ for prefix in ('../images/' , '../../images/' , '../../../images/' ):
200+ if uri .startswith (prefix ):
201+ return '_static/' + uri [len (prefix ):]
202+ return uri
203+
204+
192205def _rewrite_locale_asset_paths (doctree : nodes .document ) -> None :
193- """Rewrite image paths so they work in both normal and gettext (locale) builds.
206+ """Rewrite image paths so they work on ReadTheDocs for both en and es builds.
194207
195- In locale builds, content comes from .po files; relative paths like ../images/...
196- are resolved from the document dir (source/). Use ../../images/ (2 levels up from
197- source/ = project root). If Read the Docs resolves from locale/es/, try
198- setting LOCALE_IMAGE_LEVELS=3 in the build environment.
208+ Use _static/ for doc images (copied via html_static_path) so paths work regardless
209+ of URL depth (/es/latest/, /en/latest/, etc.).
199210 """
200- # Allow override: LOCALE_IMAGE_LEVELS=3 if Read the Docs resolves from locale/es/
201- levels = max (1 , int (os .environ .get ("LOCALE_IMAGE_LEVELS" , "3" )))
202- prefix = '../images/'
203- replacement = ("../" * levels ) + "images/"
204211 for node in doctree .traverse (nodes .image ):
205212 uri = node .get ('uri' , '' )
206- if uri .startswith (prefix ):
207- node ['uri' ] = replacement + uri [len (prefix ):]
213+ new_uri = _images_path_to_static (uri )
214+ if new_uri != uri :
215+ node ['uri' ] = new_uri
208216
209217
210218# Regex for paragraphs that are only markdown image or link (locale builds sometimes
@@ -238,10 +246,9 @@ def _is_image_path(target: str) -> bool:
238246
239247
240248def _image_uri_from_path (path : str ) -> str :
241- """Normalize image path to correct depth for locale builds."""
242- levels = int (os .environ .get ("LOCALE_IMAGE_LEVELS" , "3" ))
249+ """Normalize image path to _static/ for ReadTheDocs compatibility."""
243250 if "images/" in path :
244- return ( "../" * levels ) + "images /" + path .split ("images/" , 1 )[1 ]
251+ return "_static /" + path .split ("images/" , 1 )[1 ]
245252 return path
246253
247254
@@ -272,12 +279,7 @@ def _convert_literal_markdown_paragraphs(app, doctree: nodes.document, docname:
272279 if match :
273280 alt = match .group (1 ).strip ()
274281 path = match .group (2 ).strip ()
275- # Normalize path (same logic as _rewrite_locale_asset_paths)
276- levels = int (os .environ .get ("LOCALE_IMAGE_LEVELS" , "3" ))
277- if "images/" in path :
278- uri = ("../" * levels ) + "images/" + path .split ("images/" , 1 )[1 ]
279- else :
280- uri = path
282+ uri = _image_uri_from_path (path ) if "images/" in path else path
281283 img = nodes .image (uri = uri , alt = alt )
282284 # Sphinx post_process_images expects image nodes to have 'candidates'
283285 img ["candidates" ] = {"*" : uri }
@@ -596,7 +598,9 @@ def add_filters(_app):
596598# Ensure _static exists (Sphinx does not create it)
597599_static_dir = os .path .join (os .path .dirname (__file__ ), '_static' )
598600os .makedirs (_static_dir , exist_ok = True )
599- html_static_path = ['_static' ]
601+ # Doc images from project root: copied to _static/ so paths work on ReadTheDocs (/es/latest/, etc.)
602+ _images_src = os .path .join (os .path .dirname (__file__ ), '..' , '..' , 'images' )
603+ html_static_path = ['_static' , _images_src ] if os .path .isdir (_images_src ) else ['_static' ]
600604
601605# Theme options
602606html_theme_options = {
0 commit comments