PageSourceSearch

https://www.westpac.com.au/scripts/seo.js

js westpac.com.au collected 2026-09-24 08:27:05 UTC 5,989 bytes, 207 lines download raw bytes

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.