689349fca5a4865a1891db8cd39d392657b2b09b
jcasper
  Wed Apr 22 02:54:09 2026 -0700
Replacing the subtrackUrl setting for faceted composites with subtrackUrls,
which supports outlinks in multiple fields.  refs #36320

diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index 912f6710d5f..0a54703ad1d 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -116,40 +116,70 @@
         const selectedDataTypes = new Set(  // get dataTypes selected initially
             Object.entries(embeddedData.dataTypes).filter(([_, val]) => val.active === 1)
                 .map(([key]) => key)
         );
         // initialize data type checkboxes (using class instead of 'name')
         document.querySelectorAll("input.cbgroup")
             .forEach(cb => { cb.checked = selectedDataTypes.has(cb.value); });
 
         // Capture initial data type state
         initialState.dataTypes = new Set(selectedDataTypes);
     }
 
     function initTable(allData) {
         const { metadata, rowToIdx, colNames } = allData;
 
+        // Match subtrackUrls trackDb keys against metadata column names
+        // ignoring leading underscores on either side, so authors can toggle
+        // facet visibility by adding/removing a '_' prefix in the metadata
+        // file without having to re-edit trackDb.
+        const stripUnderscores = s => s.replace(/^_+/, "");
+        const subtrackUrls = Object.fromEntries(
+            Object.entries(embeddedData.subtrackUrls || {})
+                  .map(([k, v]) => [stripUnderscores(k), v])
+        );
+
         const ordinaryColumns = colNames.map(key => {
             const col = {
                 data: key,
                 title: toTitleCase(key.replace(/^_/, "")),
             };
-            if (key === embeddedData.primaryKey && embeddedData.subtrackUrl) {
+            const urlTemplate = subtrackUrls[stripUnderscores(key)];
+            if (urlTemplate) {
+                // Mirrors hgc/hgc.c:printIdOrLinks(): split cell on ',', each
+                // token may be 'id|label' (id substitutes $$, label is shown).
+                // urlTemplate is html-encoded server-side (htmlEncode in
+                // hgTrackUi.c), so it's safe to interpolate into an href.
                 col.render = (data, type) => {
                     if (type !== "display") return data;
-                    const url = embeddedData.subtrackUrl.replace("$$", encodeURIComponent(data));
-                    return `<a href="${url}" target="_blank">${data}</a>`;
+                    if (data == null || data === "") return "";
+                    const parts = String(data).split(",")
+                                              .map(s => s.trim())
+                                              .filter(Boolean);
+                    return parts.map(tok => {
+                        let idForUrl = tok, label = tok, encode = true;
+                        const bar = tok.indexOf("|");
+                        if (bar >= 0) {
+                            idForUrl = tok.slice(0, bar);
+                            label    = tok.slice(bar + 1);
+                            encode   = false;
+                        }
+                        if (/^https?:/i.test(label)) encode = false;
+                        const sub = encode ? encodeURIComponent(idForUrl) : idForUrl;
+                        const href = urlTemplate.replace(/\$\$/g, sub);
+                        return `<a href="${href}" target="_blank">${label}</a>`;
+                    }).join(", ");
                 };
             }
             return col;
         });
 
         const checkboxColumn = {
             data: null,
             orderable: false,
             defaultContent: "",
             title: `
             <label title="Select all visible rows">
             <input type="checkbox" id="select-all"/></label>`,
             // no render function needed
         };