1/** 2 * SEO Structured Data â JSON-LD injection 3 * 4 * Reads page metadata to determine which structured data type to render, 5 * then injects the appropriate <script type="application/ld+json"> into <head>. 6 * 7 * Supports: Article, Product, Organization (homepage), BreadcrumbList (non-home). 8 */ 9 10import { getMetadata } from './aem.js'; 11 12/** 13 * Inject a JSON-LD script into the document head. 14 * 15 * @param {Object} data - Structured data object 16 */ 17function injectJsonLd(data) { 18 const script = document.createElement('script'); 19 script.type = 'application/ld+json'; 20 script.textContent = JSON.stringify(data); 21 document.head.appendChild(script); 22} 23 24/** 25 * Build Article structured data from page metadata. 26 * 27 * @returns {Object} Article JSON-LD 28 */ 29function buildArticleData() { 30 const title = getMetadata('og:title') || document.title; 31 const publishedTime = getMetadata('published-time') || ''; 32 const modifiedTime = getMetadata('modified-time') || ''; 33 34 return { 35 '@context': 'https://schema.org', 36 '@type': 'Article', 37 headline: title, 38 ...(publishedTime && { datePublished: publishedTime }), 39 ...(modifiedTime && { dateModified: modifiedTime }), 40 author: [ 41 { 42 '@type': 'Organization', 43 name: 'Westpac', 44 url: 'https://www.westpac.com.au/', 45 }, 46 ], 47 }; 48} 49 50function normalizeProductType(productType) { 51 const namespaceSeparatorIndex = Math.max( 52 productType.lastIndexOf(':'), 53 productType.lastIndexOf('/'), 54 ); 55 return productType.slice(namespaceSeparatorIndex + 1) || 'Product'; 56} 57 58async function resolveProductImageUrl(imageUrl) { 59 if (!imageUrl) return ''; 60 61 const resolvedUrl = new URL(imageUrl, window.location.origin); 62 const isSameOriginDamPath = resolvedUrl.origin === window.location.origin 63 && resolvedUrl.pathname.startsWith('/content/dam/'); 64 65 if (!isSameOriginDamPath) return resolvedUrl.href; 66 67 try { 68 const response = await fetch(resolvedUrl.href, { method: 'HEAD' }); 69 return response.ok && response.url ? response.url : resolvedUrl.href; 70 } catch { 71 return resolvedUrl.href; 72 } 73} 74 75/** 76 * Build Product structured data from page metadata. 77 * 78 * @returns {Promise<Object>} Product JSON-LD 79 */ 80async function buildProductData() { 81 const productType = normalizeProductType(getMetadata('producttype') || 'Product'); 82 const title = getMetadata('og:title') || document.title; 83 const description = getMetadata('description') || ''; 84 const imageUrl = await resolveProductImageUrl(getMetadata('productimageurl') || ''); 85 86 // Derive category from first URL segment title 87 const segments = window.location.pathname 88 .replace(/^\/|\/$/g, '') 89 .split('/') 90 .filter(Boolean); 91 const category = segments[0] 92 ? segments[0].replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) 93 : ''; 94 95 return { 96 '@context': 'https://schema.org', 97 '@type': productType, 98 brand: 'Westpac', 99 name: title, 100 ...(description && { description }), 101 ...(category && { category }), 102 ...(imageUrl && { image: imageUrl }), 103 }; 104} 105 106/** 107 * Build Organization structured data for the homepage. 108 * 109 * @returns {Object} Organization JSON-LD 110 */ 111function buildOrganizationData() { 112 const logo = getMetadata('seologo') || ''; 113 const worldwidePhone = getMetadata('worldwidephonenumber') || ''; 114 const auPhone = getMetadata('auphonenumber') || ''; 115 116 const contactPoints = []; 117 if (worldwidePhone) { 118 contactPoints.push({ 119 '@type': 'ContactPoint', 120 telephone: worldwidePhone, 121 contactType: 'customer service', 122 contactOption: 'TollFree', 123 areaServed: 'Worldwide', 124 }); 125 } 126 if (auPhone) { 127 contactPoints.push({ 128 '@type': 'ContactPoint', 129 telephone: auPhone, 130 contactType: 'customer service', 131 contactOption: 'TollFree', 132 areaServed: 'AU', 133 }); 134 } 135 136 return { 137 '@context': 'https://schema.org', 138 '@type': 'Organization', 139 url: 'https://www.westpac.com.au/', 140 ...(logo && { logo }), 141 ...(contactPoints.length > 0 && { contactPoint: contactPoints }), 142 }; 143} 144 145/** 146 * Build BreadcrumbList structured data from URL segments. 147 * Uses segment slugs formatted as title-case names. 148 * 149 * @returns {Object} BreadcrumbList JSON-LD 150 */ 151function buildBreadcrumbData() { 152 const { origin, pathname } = window.location; 153 const segments = pathname.replace(/^\/|\/$/g, '').split('/').filter(
153Boolean); 154 155 const items = segments.map((segment, index) => { 156 const path = `/${segments.slice(0, index + 1).join('/')}/`; 157 const name = segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); 158 return { 159 '@type': 'ListItem', 160 position: index + 1, 161 item: { 162 '@id': `${origin}${path}`, 163 name, 164 }, 165 }; 166 }); 167 168 return { 169 '@context': 'https://schema.org', 170 '@type': 'BreadcrumbList', 171 itemListElement: items, 172 }; 173} 174 175/** 176 * Initialize SEO structured data injection. 177 * Should be called during loadLazy() in scripts.js. 178 */ 179// eslint-disable-next-line import/prefer-default-export 180export async function initSeo() { 181 const { pathname } = window.location; 182 const segments = pathname.replace(/^\/|\/$/g, '').split('/').filter(Boolean); 183 const isHomepage = segments.length === 0; 184 185 const displaySeoScript = getMetadata('displayseoscript'); 186 const structuredDataType = getMetadata('structureddatatype') || ''; 187 const templateType = getMetadata('templatetype') || ''; 188 189 // Organization data for homepage (when SEO is enabled) 190 if (displaySeoScript === 'true' && (isHomepage || templateType === 'homepage')) { 191 injectJsonLd(buildOrganizationData()); 192 } 193 194 // Article or Product based on structuredDataType 195 if (displaySeoScript === 'true' && structuredDataType === 'article') { 196 injectJsonLd(buildArticleData()); 197 } 198 199 if (displaySeoScript === 'true' && structuredDataType === 'product') { 200 injectJsonLd(await buildProductData()); 201 } 202 203 // BreadcrumbList for all non-home pages 204 if (!isHomepage) { 205 injectJsonLd(buildBreadcrumbData()); 206 } 207}
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.