From 2c92c3548652d010369d5c42633a0e3d3736a59e Mon Sep 17 00:00:00 2001 From: Sharon Koech Date: Mon, 7 Sep 2026 07:18:32 +0300 Subject: [PATCH 1/2] feat: add migration config for 2024.1 Signed-off-by: Sharon Koech --- docs/_static/js/overwrite_links.js | 173 +++++++++++++++++++++++++++++ docs/conf.py | 19 ++-- 2 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 docs/_static/js/overwrite_links.js diff --git a/docs/_static/js/overwrite_links.js b/docs/_static/js/overwrite_links.js new file mode 100644 index 0000000..fc19054 --- /dev/null +++ b/docs/_static/js/overwrite_links.js @@ -0,0 +1,173 @@ +// Replaces rtd-address with new-address in links + +const rtd_address = 'canonical-openstack-proxy.readthedocs-hosted.com'; +const new_address = 'canonical.com/openstack/docs'; +const new_path = '/' + new_address.split('/').slice(1).join('/'); + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function overwriteMatchingAnchorUrls(container) { + if (!container) return; + + const rtd_addressRegex = new RegExp(escapeRegExp(rtd_address), 'g'); + container.querySelectorAll('a[href], link[href]').forEach((anchor) => { + anchor.href = anchor.href.replace(rtd_addressRegex, new_address); + }); +} + +function prependPathToAnchorUrls(container, path) { + if (!container) return; + + container.querySelectorAll('a[href], link[href]').forEach((anchor) => { + const href = anchor.getAttribute('href'); + // Only prepend to root-relative URLs (e.g. "/en/stable/page.html"). + // Skip absolute URLs ("https://..."), protocol-relative URLs ("//..."), + // anchors ("#..."), and other schemes ("mailto:", "tel:", ...) so they + // aren't broken by having the path prepended. + if ( + href && + href.startsWith('/') && + !href.startsWith('//') && + !href.startsWith(path) + ) { + anchor.setAttribute('href', path + href); + } + }); +} + +function patchFlyout() { + const rtdFlyout = document.querySelector('readthedocs-flyout'); + if (!rtdFlyout) return false; + if (rtdFlyout.dataset.urlOverwritePatched) return true; + rtdFlyout.dataset.urlOverwritePatched = 'true'; + + overwriteMatchingAnchorUrls(rtdFlyout); + overwriteMatchingAnchorUrls(rtdFlyout.shadowRoot); + + rtdFlyout.addEventListener('click', () => { + overwriteMatchingAnchorUrls(rtdFlyout); + overwriteMatchingAnchorUrls(rtdFlyout.shadowRoot); + }); + + return true; +} + +function patchNotification() { + const rtdNotification = document.querySelector('readthedocs-notification'); + if (!rtdNotification) return false; + if (rtdNotification.dataset.urlOverwritePatched) return true; + rtdNotification.dataset.urlOverwritePatched = 'true'; + + const patchAll = () => { + overwriteMatchingAnchorUrls(rtdNotification); + if (rtdNotification.shadowRoot) { + overwriteMatchingAnchorUrls(rtdNotification.shadowRoot); + } + }; + + // Patch any content that already exists. + patchAll(); + + // Notification content is rendered dynamically into the element's shadow DOM + // by Lit when the config is loaded (asynchronously), so the initial patch + // above will usually find nothing. Wait for the shadow root to become + // available (the custom element may not have been upgraded yet) and then + // observe it so that links are patched as soon as they are rendered. + const observeShadowRoot = () => { + if (!rtdNotification.shadowRoot) { + requestAnimationFrame(observeShadowRoot); + return; + } + + patchAll(); + + const observer = new MutationObserver(patchAll); + observer.observe(rtdNotification.shadowRoot, { + childList: true, + subtree: true, + }); + }; + + observeShadowRoot(); + + return true; +} + +function patchSearch() { + const rtdSearch = document.querySelector('readthedocs-search'); + if (!rtdSearch) return false; + if (rtdSearch.dataset.urlOverwritePatched) return true; + rtdSearch.dataset.urlOverwritePatched = 'true'; + + const patchAll = () => { + overwriteMatchingAnchorUrls(rtdSearch); + prependPathToAnchorUrls(rtdSearch, new_path); + if (rtdSearch.shadowRoot) { + overwriteMatchingAnchorUrls(rtdSearch.shadowRoot); + prependPathToAnchorUrls(rtdSearch.shadowRoot, new_path); + } + }; + + // Patch any content that already exists. + patchAll(); + + // Search results are rendered dynamically into the element's shadow DOM by + // Lit when the user performs a search, so the initial patch above will + // usually find nothing. Wait for the shadow root to become available (the + // custom element may not have been upgraded yet) and then observe it so that + // result links are patched as soon as they are rendered. + const observeShadowRoot = () => { + if (!rtdSearch.shadowRoot) { + requestAnimationFrame(observeShadowRoot); + return; + } + + patchAll(); + + const observer = new MutationObserver(patchAll); + observer.observe(rtdSearch.shadowRoot, { + childList: true, + subtree: true, + }); + }; + + observeShadowRoot(); + + // Patch on click as well, to cover keyboard navigation (the Enter key calls + // .click() on the active result) and any case the observer might miss. + rtdSearch.addEventListener('click', patchAll); + + return true; +} + +function init() { + overwriteMatchingAnchorUrls(document.querySelector('header')); + + // Patch each addon independently. Using `&&` short-circuit evaluation here + // would prevent later addons (e.g. search) from being patched at all if an + // earlier one (e.g. flyout or notification) is disabled or not yet loaded. + let flyoutDone = patchFlyout(); + let notificationDone = patchNotification(); + let searchDone = patchSearch(); + + if (flyoutDone && notificationDone && searchDone) return; + + const observer = new MutationObserver(() => { + if (!flyoutDone) flyoutDone = patchFlyout(); + if (!notificationDone) notificationDone = patchNotification(); + if (!searchDone) searchDone = patchSearch(); + if (flyoutDone && notificationDone && searchDone) { + observer.disconnect(); + } + }); + + observer.observe(document.body, { childList: true, subtree: true }); +} + +if (document.body) { + init(); +} else { + document.addEventListener('DOMContentLoaded', init); +} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 45d5b8a..d58c435 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,7 +33,7 @@ html_title = project + " documentation" # Documentation website URL -ogp_site_url = os.environ.get("READTHEDOCS_CANONICAL_URL", "/") +ogp_site_url = f"https://canonical.com/openstack/docs/{os.environ.get('READTHEDOCS_VERSION', 'local')}/" # Preview name of the documentation website ogp_site_name = project @@ -97,17 +97,18 @@ # Project slug # TODO: If your documentation is hosted on https://documentation.ubuntu.com/, # uncomment and set to the RTD slug. -# slug = '' +slug = "openstack/docs" ####################### # Sitemap configuration: https://sphinx-sitemap.readthedocs.io/ ####################### # Use RTD canonical URL to ensure duplicate pages have a specific canonical URL -html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/") +html_baseurl = f"https://canonical.com/openstack/docs/{os.environ.get('READTHEDOCS_VERSION', 'local')}/" # sphinx-sitemap uses html_baseurl to generate the full URL for each page: sitemap_url_scheme = "{link}" +sitemap_filename = "doc-sitemap.xml" # Include `lastmod` dates in the sitemap: sitemap_show_lastmod = True @@ -125,8 +126,8 @@ # Template and asset locations # ################################ -# html_static_path = ["_static"] -# templates_path = ["_templates"] +html_static_path = ["_static"] +templates_path = ["_templates"] ############# # Redirects # @@ -143,7 +144,6 @@ # Strips '/index.html' from destination URLs when building with 'dirhtml' rediraffe_dir_only = True - ############################ # sphinx-llm configuration # ############################ @@ -251,9 +251,10 @@ # ] # Adds custom JavaScript files, located remotely or in 'html_static_path'. -# html_js_files = [ -# "https://assets.ubuntu.com/v1/287a5e8f-bundle.js", -# ] +html_js_files = [ + "js/overwrite_links.js", + "https://assets.ubuntu.com/v1/287a5e8f-bundle.js", +] # Appends extra markup to the end of every document written in reST rst_epilog = """ From 3399a2b36901106a060b0c8cad74547c0c29c8a9 Mon Sep 17 00:00:00 2001 From: Sharon Koech Date: Mon, 7 Sep 2026 17:48:40 +0300 Subject: [PATCH 2/2] fix: add styling for cookie banner Signed-off-by: Sharon Koech --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d58c435..b7f6f23 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -246,9 +246,9 @@ ] # Adds custom CSS files, located remotely or in 'html_static_path'. -# html_css_files = [ -# "https://assets.ubuntu.com/v1/d86746ef-cookie_banner.css", -# ] +html_css_files = [ + "https://assets.ubuntu.com/v1/d86746ef-cookie_banner.css", +] # Adds custom JavaScript files, located remotely or in 'html_static_path'. html_js_files = [