1/** 2 * Accordion FAQ Schema Injector 3 * 4 * Reads per-accordion Question[] payloads from `data-faq-schema` 5 * rendered server-side by AccordionModel (Sling Model), then builds one 6 * merged FAQPage schema as <script type="application/ld+json"> in <head>. 7 * 8 * This approach is preferred over client-side DOM parsing because: 9 * - The Sling Model has direct access to JCR node structure 10 * - HTML stripping & text normalization happens once on the server 11 * - The JS is trivially simple and extremely fast (~1ms) 12 * - Works even if CSS hides or lazy-loads accordion panels 13 * 14 * Schema spec: https://schema.org/FAQPage 15 */ 16(function () { 17 'use strict'; 18 19 var WRAPPER_SELECTOR = '.accordion-wrapper'; 20 var SCHEMA_SELECTOR = '[data-cmp-is="accordion"][data-faq-schema]'; 21 var SCRIPT_TYPE = 'application/ld+json'; 22 var SCRIPT_ID = 'accordion-faq-schema-jsonld'; 23 var HOOK_ATTR = 'data-cmp-hook-accordion'; 24 var HOOK_VALUE = 'schema'; 25 26 function parseSchema(raw) { 27 if (!raw) { 28 return null; 29 } 30 try { 31 var parsed = JSON.parse(raw); 32 if (!Array.isArray(parsed)) { 33 if (typeof console !== 'undefined') { 34 console.warn('[AccordionSchema] Expected Question[] payload in data-faq-schema.'); 35 } 36 return null; 37 } 38 return parsed; 39 } catch (e) { 40 if (typeof console !== 'undefined') { 41 console.error('[AccordionSchema] Invalid JSON in data-faq-schema:', e); 42 } 43 return null; 44 } 45 } 46 47 function getSchemaIdFromLocation() { 48 return window.location.origin + window.location.pathname + '#faq'; 49 } 50 51 function getAccordions() { 52 var wrappers = document.querySelectorAll(WRAPPER_SELECTOR); 53 if (!wrappers.length) { 54 return document.querySelectorAll(SCHEMA_SELECTOR); 55 } 56 57 var all = []; 58 for (var i = 0; i < wrappers.length; i++) { 59 var inside = wrappers[i].querySelectorAll(SCHEMA_SELECTOR); 60 for (var j = 0; j < inside.length; j++) { 61 all.push(inside[j]); 62 } 63 } 64 return all; 65 } 66 67 function mergeSchemas(entityArrays) { 68 if (!entityArrays.length) { 69 return null; 70 } 71 72 var mergedMainEntity = []; 73 var schemaId = getSchemaIdFromLocation(); 74 75 for (var i = 0; i < entityArrays.length; i++) { 76 var entities = entityArrays[i]; 77 if (!entities || !Array.isArray(entities)) { 78 continue; 79 } 80 81 for (var j = 0; j < entities.length; j++) { 82 var entity = entities[j]; 83 var name = entity && entity.name ? String(entity.name).trim() : ''; 84 var answer = entity && entity.acceptedAnswer && entity.acceptedAnswer.text 85 ? String(entity.acceptedAnswer.text).trim() 86 : ''; 87 88 if (!name) { 89 continue; 90 } 91 92 mergedMainEntity.push(entity); 93 } 94 } 95 96 if (!mergedMainEntity.length) { 97 return null; 98 } 99 100 return { 101 '@context': 'https://schema.org', 102 '@type': 'FAQPage', 103 '@id': schemaId, 104 mainEntity: mergedMainEntity 105 }; 106 } 107 108 function safeSerialize(schemaObject) { 109 try { 110 var payload = JSON.stringify(schemaObject); 111 // Validate that the produced payload is well-formed JSON before writing to DOM. 112 JSON.parse(payload); 113 return payload; 114 } catch (e) { 115 if (typeof console !== 'undefined') { 116 console.error('[AccordionSchema] Failed to serialize merged FAQ schema:', e); 117 } 118 return null; 119 } 120 } 121 122 function upsertSchemaScript(schemaObject) { 123 var existing = document.getElementById(SCRIPT_ID); 124 125 if (!schemaObject) { 126 if (existing && existing.parentNode) { 127 existing.parentNode.removeChild(existing); 128 } 129 return; 130 } 131 132 var payload = safeSerialize(schemaObject); 133 if (!payload) { 134 if (existing && existing.parentNode) { 135 existing.parentNode.removeChild(existing); 136 } 137 return; 138 } 139 140 if (!existing) { 141 existing = document.createElement('script'); 142 existing.type = SCRIPT_TYPE; 143 existing.id = SCRIPT_ID; 144 existing.setAttribute(HOOK_ATTR, HOOK_VALUE); 145 document.head.appendChild(existing); 146 } 147 148 existing.textContent = payload; 149 } 150 151 // Collect all accordion schemas and inject a single merged FAQPage script. 152 function injectAll() { 153 var accordions = getAccordions();
154 var entityArrays = []; 155 156 for (var i = 0; i < accordions.length; i++) { 157 var raw = accordions[i].getAttribute('data-faq-schema'); 158 var parsed = parseSchema(raw); 159 if (parsed) { 160 entityArrays.push(parsed); 161 } 162 } 163 164 upsertSchemaScript(mergeSchemas(entityArrays)); 165 } 166 167 window.AccordionSchemaInjector = { 168 /** 169 * Re-scans the page and injects any schemas not yet in <head>. 170 * Call this after dynamically adding accordion markup. 171 */ 172 refresh: function () { 173 injectAll(); 174 }, 175 176 injectForElement: function (accordionElement) { 177 if (!accordionElement) { 178 return; 179 } 180 // Single element injection now merges with all others to keep one schema script. 181 injectAll(); 182 } 183 }; 184 185 // Run on DOMContentLoaded (or immediately if DOM is already ready) 186 if (document.readyState === 'loading') { 187 document.addEventListener('DOMContentLoaded', injectAll); 188 } else { 189 injectAll(); 190 } 191 192}());
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.