diff --git a/tests/test_facet_display.py b/tests/test_facet_display.py
new file mode 100644
index 0000000..9f36223
--- /dev/null
+++ b/tests/test_facet_display.py
@@ -0,0 +1,76 @@
+"""A multi-valued facet must render as separate values, not one blob.
+
+`build.py` filters facets with `str(value).split()`, so a facet is
+multi-valued *by contract*. The package page rendered the raw value instead, so
+a two-value facet came out as `tui web-ui` -- read as one odd token rather than
+two. Filtering and display have to split the same way.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+
+import pytest
+
+from xpkgindex.build import build
+from xpkgindex.render import render
+
+from conftest import commit, init_repo, write_config, write_descriptor, write_plugin
+
+PLUGIN = '''
+ from xpkgindex.models import Facet
+ from xpkgindex.plugins import Plugin
+
+ class P(Plugin):
+ api_version = 1
+ name = "multi"
+
+ def on_package(self, pkg, raw):
+ pkg.facets["kind"] = "tui web-ui"
+ pkg.facets["single"] = "solo"
+
+ def facets(self):
+ return [Facet(key="kind", label="kind", weight=10),
+ Facet(key="single", label="single", weight=20)]
+'''
+
+
+@pytest.fixture
+def site(tmp_path):
+ root = str(tmp_path / "index")
+ os.makedirs(root)
+ init_repo(root)
+ rel = write_plugin(root, PLUGIN)
+ write_config(root, plugins=[rel])
+ write_descriptor(root, "alpha", "widget")
+ commit(root, "add widget", date="2026-01-01")
+ out = str(tmp_path / "site")
+ data, config = build(root, offline=True)
+ render(data, config, out)
+ with open(os.path.join(out, "packages", "widget", "index.html"),
+ encoding="utf-8") as f:
+ return f.read()
+
+
+def _facet_block(html: str, key: str) -> str:
+ m = re.search(rf"
{key}\s*(.*?)", html, re.S)
+ assert m, f"facet {key!r} is not on the page"
+ return m.group(1)
+
+
+def test_multi_valued_facet_renders_each_value_separately(site):
+ block = _facet_block(site, "kind")
+ values = re.findall(r"([^<]+)", block)
+ assert values == ["tui", "web-ui"], \
+ f"expected two rendered values, got {values!r}"
+
+
+def test_a_single_valued_facet_is_unchanged(site):
+ block = _facet_block(site, "single")
+ assert re.findall(r"([^<]+)", block) == ["solo"]
+
+
+def test_the_joined_form_never_reaches_the_page(site):
+ """The bug's signature: both values inside one element."""
+ assert "tui web-ui" not in re.sub(r"\s+", " ", site)
diff --git a/xpkgindex/render.py b/xpkgindex/render.py
index a763d55..4dfbfa6 100644
--- a/xpkgindex/render.py
+++ b/xpkgindex/render.py
@@ -52,6 +52,10 @@ def _env(lang: str = i18n.DEFAULT, default: str = i18n.DEFAULT) -> Environment:
lstrip_blocks=True,
)
env.filters["platform_label"] = lambda p: PLATFORM_LABELS.get(p, p)
+ # Facets are multi-valued by contract: build.py filters them with
+ # `str(value).split()`. Templates must split the same way, or a two-value
+ # facet renders as one blob -- "tui web-ui" reading as a single odd token.
+ env.filters["split_facet"] = lambda v: str(v).split()
# `loc` unwraps any consumer- or plugin-supplied value that was written
# per locale. Applied wherever such text reaches a template.
env.filters["loc"] = lambda v: i18n.localize(v, lang, default)
diff --git a/xpkgindex/templates/package.html b/xpkgindex/templates/package.html
index b6e1426..9eecea5 100644
--- a/xpkgindex/templates/package.html
+++ b/xpkgindex/templates/package.html
@@ -103,7 +103,11 @@ {{ t('pkg.facts') }}
{% for key, value in pkg.facets.items() %}
{{ key }}
- {{ value }}
+ {# A facet is multi-valued by contract -- build.py filters with
+ `str(value).split()`. Rendering it as one blob printed
+ "tui web-ui" as if it were a single odd token, so split it here
+ the same way it is split there. One value stays one chip. #}
+ {% for v in value | string | split_facet %}{{ v }}{% endfor %}
{% if key == 'namespace' and pkg.namespace_implicit %}
{{ t('pkg.index_default') }}
{% endif %}