diff --git a/connect/customer/content_matcher.py b/connect/customer/content_matcher.py
index 416ba2c..0827b49 100644
--- a/connect/customer/content_matcher.py
+++ b/connect/customer/content_matcher.py
@@ -14,7 +14,7 @@ def _find_best_matching_content(
) -> _ContentBlock | None:
parsed = urllib.parse.urlparse(resource_url)
host = parsed.netloc
- path = parsed.path
+ path = parsed.path or "/"
if not parsed.scheme or not host:
debug_log(debug, f"Cannot parse resource URL: {resource_url}")
return None
@@ -25,23 +25,30 @@ def _find_best_matching_content(
best_specificity = -1
for block in content_blocks:
- pattern = urllib.parse.urlparse(block.url_pattern)
- if not pattern.scheme or not pattern.netloc:
- debug_log(debug, f"Skipping block with invalid URL pattern: {block.url_pattern}")
- continue
-
- if pattern.netloc != host:
- debug_log(
- debug,
- f"Skipping block: host mismatch (pattern={pattern.netloc}, resource={host})",
- )
- continue
-
- if pattern.path == path:
+ is_path_only = block.url_pattern.startswith("/")
+
+ if is_path_only:
+ pattern_path = block.url_pattern
+ else:
+ pattern = urllib.parse.urlparse(block.url_pattern)
+ if not pattern.scheme or not pattern.netloc:
+ debug_log(debug, f"Skipping block with invalid URL pattern: {block.url_pattern}")
+ continue
+
+ if pattern.netloc != host:
+ debug_log(
+ debug,
+ f"Skipping block: host mismatch (pattern={pattern.netloc}, resource={host})",
+ )
+ continue
+
+ pattern_path = pattern.path or "/"
+
+ if pattern_path == path:
debug_log(debug, f"Exact match found: {block.url_pattern}")
return block
- specificity = _score_path_pattern(pattern.path or "/", path or "/")
+ specificity = _score_path_pattern(pattern_path, path)
if specificity > best_specificity:
best_specificity = specificity
best_match = block
diff --git a/tests/customer/test_content_matcher.py b/tests/customer/test_content_matcher.py
index a38772a..8a1bf5e 100644
--- a/tests/customer/test_content_matcher.py
+++ b/tests/customer/test_content_matcher.py
@@ -73,6 +73,68 @@ def test_find_best_matching_content_handles_port_specific_host_matching() -> Non
assert _find_best_matching_content(blocks, "http://localhost:3001/page") is None
+def test_find_best_matching_content_matches_path_only_pattern() -> None:
+ """Path-only patterns (starting with /) match regardless of host."""
+ blocks = [
+ _ContentBlock(url_pattern="/article/*", server="http://127.0.0.1:8787", license_xml=""),
+ ]
+
+ match = _find_best_matching_content(blocks, "http://127.0.0.1:7676/article/foo")
+
+ assert match is not None
+ assert match.url_pattern == "/article/*"
+
+
+def test_find_best_matching_content_exact_path_only_wins_over_wildcard() -> None:
+ """Exact path-only match beats a path-only wildcard."""
+ blocks = [
+ _ContentBlock(url_pattern="/*", server="http://127.0.0.1:8787", license_xml=""),
+ _ContentBlock(url_pattern="/article/foo", server="http://127.0.0.1:8787", license_xml=""),
+ ]
+
+ match = _find_best_matching_content(blocks, "http://127.0.0.1:7676/article/foo")
+
+ assert match is not None
+ assert match.url_pattern == "/article/foo"
+
+
+def test_find_best_matching_content_path_only_matches_any_host() -> None:
+ """Path-only patterns match any host."""
+ blocks = [
+ _ContentBlock(url_pattern="/article/*", server="http://127.0.0.1:8787", license_xml=""),
+ ]
+
+ match = _find_best_matching_content(blocks, "http://totally-different-host.com/article/foo")
+
+ assert match is not None
+ assert match.url_pattern == "/article/*"
+
+
+def test_find_best_matching_content_mixes_full_url_and_path_only() -> None:
+ """Path-only pattern with higher specificity wins over full-URL pattern."""
+ blocks = [
+ _ContentBlock(url_pattern="http://127.0.0.1:7676/*", server="http://127.0.0.1:8787", license_xml=""),
+ _ContentBlock(url_pattern="/article/*", server="http://127.0.0.1:8787", license_xml=""),
+ ]
+
+ match = _find_best_matching_content(blocks, "http://127.0.0.1:7676/article/foo")
+
+ assert match is not None
+ assert match.url_pattern == "/article/*"
+
+
+def test_find_best_matching_content_matches_root_url_without_trailing_slash() -> None:
+ """A resource URL with no path (e.g. http://host) matches a / pattern."""
+ blocks = [
+ _ContentBlock(url_pattern="/", server="http://127.0.0.1:8787", license_xml=""),
+ ]
+
+ match = _find_best_matching_content(blocks, "http://127.0.0.1:7676")
+
+ assert match is not None
+ assert match.url_pattern == "/"
+
+
def test_find_best_matching_content_returns_none_for_empty_blocks() -> None:
"""Returns None when no content blocks are provided."""
assert _find_best_matching_content([], "http://example.com/page") is None