diff --git a/src/current/Gemfile b/src/current/Gemfile
index baf71339668..b3b82646f0d 100644
--- a/src/current/Gemfile
+++ b/src/current/Gemfile
@@ -12,7 +12,11 @@ gem "redcarpet", "~> 3.6"
gem "rss"
gem "webrick"
gem "jekyll-minifier"
-
+gem 'csv'
+gem 'logger'
+gem 'base64'
+gem 'bigdecimal'
+gem 'mutex_m'
group :jekyll_plugins do
gem "jekyll-include-cache"
gem 'jekyll-algolia', "~> 1.0", path: "./jekyll-algolia-dev"
diff --git a/src/current/_config_cockroachdb_local.yml b/src/current/_config_cockroachdb_local.yml
index 3440c9a8df7..98579dde61f 100644
--- a/src/current/_config_cockroachdb_local.yml
+++ b/src/current/_config_cockroachdb_local.yml
@@ -4,7 +4,6 @@ exclude:
- "v2.0"
- "v2.1"
- "v19.1"
-- "v19.2"
- "v20.1"
- "ci"
- "scripts"
diff --git a/src/current/audit.py b/src/current/audit.py
new file mode 100644
index 00000000000..2d2c968735d
--- /dev/null
+++ b/src/current/audit.py
@@ -0,0 +1,408 @@
+#!/usr/bin/env python3
+"""
+audit.py
+
+An audit script that:
+1) Finds cross-version links (categorized by location).
+2) Finds cockroachlabs.com non-docs links.
+3) Finds external (non-cockroachlabs.com) links.
+4) Audits image/CSS/JS/font usage, categorizing them as present, missing, or external.
+
+**This version** uses a "fallback" approach in asset_status() so
+we do *not* unconditionally remove "/docs/" from the path. Instead,
+we generate multiple candidate paths and see if any match the disk.
+"""
+
+import os
+import sys
+import re
+import argparse
+from bs4 import BeautifulSoup
+from urllib.parse import urlparse
+
+def is_cross_version_link(url, current_version):
+ """
+ Return (True, found_version) if `url` is a docs link pointing to a different version.
+ E.g. /docs/v19.2/... vs current_version v21.1
+ """
+ match = re.search(r'/docs/(v\d+\.\d+)', url)
+ if match:
+ version = match.group(1)
+ return (version != current_version, version)
+ return (False, None)
+
+def categorize_cross_version_link(tag):
+ """
+ For cross-version links, figure out if they're in the sidebar, version-switcher, or body.
+ """
+ if tag.find_parent(id="sidebar"):
+ return "Sidebar Navigation"
+ elif tag.find_parent(id="version-switcher"):
+ return "Version Switcher"
+ else:
+ return "Content Body"
+
+def find_assets(soup):
+ """
+ Return a dict: { "images": set(), "css": set(), "js": set(), "fonts": set() }
+ by scanning , ,
+
+'''
+
+ html = re.sub(r"", nav_deps + "\n", html, flags=re.IGNORECASE)
+
+ # Add offline styles
+ offline_styles = f''''''
+
+ html = re.sub(r"", offline_styles + "\n", html, flags=re.IGNORECASE)
+
+ # Add navigation initialization
+ nav_init = """"""
+
+ html = re.sub(r"
This page shows how different link patterns were processed:
+ +This link would be at: v19.2/secure-a-cluster.html
+This link would be at: v19.2/secure-a-cluster.html
+Note: Click each link to verify it works correctly.
+", nav_init + "\n", html, flags=re.IGNORECASE) + + # Write output + dst_path.parent.mkdir(parents=True, exist_ok=True) + dst_path.write_text(html, encoding="utf-8") + + self.processed_files.add(str(rel_path)) + + except Exception as e: + self.log(f"Error processing {src_path}: {e}", "ERROR") + import traceback + traceback.print_exc() + + def fix_css_images(self): + """Fix image paths in CSS files""" + self.log("Fixing CSS image paths...") + + for css_file in (OUTPUT_ROOT / "css").rglob("*.css"): + try: + content = css_file.read_text(encoding="utf-8") + + # Fix various image URL patterns + content = re.sub( + r"url\((['\"]?)/?docs/images/([^)\"']+)\1\)", + r"url(\1../images/\2\1)", + content, + ) + content = re.sub( + r"url\((['\"]?)images/([^)\"']+)\1\)", + r"url(\1../images/\2\1)", + content, + ) + + css_file.write_text(content, encoding="utf-8") + + except Exception as e: + self.log(f"Error fixing CSS {css_file}: {e}", "WARNING") + + def download_google_fonts(self): + """Download and localize Google Fonts""" + self.log("Downloading Google Fonts...") + + fonts_dir = OUTPUT_ROOT / "fonts" + fonts_dir.mkdir(exist_ok=True) + + try: + # Get CSS + headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} + css_response = requests.get(FONTS_CSS_URL, headers=headers, timeout=10) + css_response.raise_for_status() + css_content = css_response.text + + # Extract and download font files + font_urls = set(re.findall(r"url\((https://fonts\.gstatic\.com/[^\)]+)\)", css_content)) + + for url in font_urls: + try: + # Download font + font_response = requests.get(url, headers=headers, timeout=10) + font_response.raise_for_status() + + # Save font + parsed = urlparse(url) + font_path = parsed.path.lstrip("/") + dst = fonts_dir / font_path + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_bytes(font_response.content) + + # Update CSS + css_content = css_content.replace(url, f"../fonts/{font_path}") + + except Exception as e: + self.log(f"Failed to download font from {url}: {e}", "WARNING") + + # Save localized CSS + (OUTPUT_ROOT / "css" / "google-fonts.css").write_text(css_content, encoding="utf-8") + self.log("Google Fonts localized", "SUCCESS") + + except Exception as e: + self.log(f"Error downloading fonts: {e}", "ERROR") + # Create fallback + fallback = """/* Fallback fonts */ +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; } +code, pre { font-family: Consolas, Monaco, "Courier New", monospace; }""" + (OUTPUT_ROOT / "css" / "google-fonts.css").write_text(fallback) + + def create_link_test_page(self): + """Create a test page to verify link processing""" + test_html = f""" + +
+
+ + +
+