diff --git a/blocks/anchor-nav/anchor-nav.js b/blocks/anchor-nav/anchor-nav.js index 7f5236d..daea308 100644 --- a/blocks/anchor-nav/anchor-nav.js +++ b/blocks/anchor-nav/anchor-nav.js @@ -88,7 +88,7 @@ export default function decorate(block) { e.preventDefault(); const target = document.getElementById(href.substring(1)); if (target) { - // Swapna: Move active class to clicked link for red underline + // Move active class to clicked link for red underline block.querySelectorAll('.anchor-nav-link:not(.primary, .secondary)').forEach((l) => l.classList.remove('active')); link.classList.add('active'); @@ -100,7 +100,7 @@ export default function decorate(block) { }); }); - // Swapna: Set first anchor link as active by default on page load + // Set first anchor link as active by default on page load const firstAnchorLink = block.querySelector('.anchor-nav-link:not(.primary, .secondary)'); if (firstAnchorLink) { firstAnchorLink.classList.add('active'); diff --git a/blocks/footer/footer.css b/blocks/footer/footer.css index 230279b..2660170 100644 --- a/blocks/footer/footer.css +++ b/blocks/footer/footer.css @@ -1,7 +1,6 @@ footer { font-size: var(--body-font-size-xs); box-sizing: border-box; - -webkit-font-smoothing: antialiased; } .footer a { @@ -188,7 +187,6 @@ footer .footer>div .section>div { .footer>div>div .accordion-wrapper .accordion .accordion-item-body ul li { margin-top: var(--spacing-xs); list-style: none; - line-height: 1; } .footer>div>div .accordion-wrapper .accordion>div>div:nth-child(2) ul li a, diff --git a/blocks/hero-heritage-cc/hero-heritage-cc.css b/blocks/hero-heritage-cc/hero-heritage-cc.css index c3617f3..a14a95b 100644 --- a/blocks/hero-heritage-cc/hero-heritage-cc.css +++ b/blocks/hero-heritage-cc/hero-heritage-cc.css @@ -1,10 +1,6 @@ /* Section container - fixed height to prevent CLS */ .hero-heritage-cc-container { position: relative; - min-height: calc(100vh - var(--spacing-xl)); /* fallback for older browsers */ - min-height: calc(100dvh - var(--spacing-xl)); - height: calc(100vh - var(--spacing-xl)); - height: calc(100dvh - var(--spacing-xl)); contain: strict; overflow: hidden; } @@ -492,6 +488,7 @@ color: var(--color-pewter); .hero-heritage-cc p.hero-heritage-cc-banner-image { margin: 10px 0 0; transition: opacity 0.35s ease-out, transform 0.35s ease-out; + min-height: 226px; } .hero-heritage-cc .hero-heritage-cc-banner { @@ -518,6 +515,11 @@ color: var(--color-pewter); .hero-heritage-cc p { font-size: var(--body-font-size-m); } + + .hero-heritage-cc-intro .hero-heritage-cc-intro-logo-hindi, + .hero-heritage-cc-intro .hero-heritage-cc-intro-logo-english { + min-height: 133px; + } } /* Desktop: 900px and up */ @@ -536,6 +538,18 @@ color: var(--color-pewter); display: flex; } + /* Logo images at native size with reserved space to prevent CLS */ +.hero-heritage-cc-intro .hero-heritage-cc-intro-logo-hindi picture, +.hero-heritage-cc-intro .hero-heritage-cc-intro-logo-english picture { + min-height: 252px; +} + +.hero-heritage-cc-intro .hero-heritage-cc-intro-logo-hindi img, +.hero-heritage-cc-intro .hero-heritage-cc-intro-logo-english img { + width: auto; + min-height: 252px; +} + .hero-heritage-cc .hero-heritage-cc-banner-cta-group { margin-top: var(--spacing-l); } @@ -544,6 +558,10 @@ color: var(--color-pewter); font-size: var(--body-font-size-s); } + .hero-heritage-cc p.hero-heritage-cc-banner-image { + min-height: 291px; + } + .hero-heritage-cc-banner-cta .button { font-size: var(--body-font-size-l); } @@ -558,10 +576,8 @@ color: var(--color-pewter); } .hero-heritage-cc-container { - min-height: calc(100vh - 120px); - min-height: calc(100dvh - 120px); - height: calc(100vh - 120px); - height: calc(100dvh - 120px); + height: 100%; + width: 100%; } /* Decoration image - top right */ diff --git a/blocks/hero-heritage-cc/hero-heritage-cc.js b/blocks/hero-heritage-cc/hero-heritage-cc.js index a64b198..054e0b1 100644 --- a/blocks/hero-heritage-cc/hero-heritage-cc.js +++ b/blocks/hero-heritage-cc/hero-heritage-cc.js @@ -1,4 +1,4 @@ -import { decorateButtons } from '../../scripts/aem.js'; +import { decorateButtons, pickPicturePreloadUrl } from '../../scripts/aem.js'; import { loadFragment } from '../../scripts/scripts.js'; /* eslint-disable secure-coding/no-hardcoded-credentials -- CSS classes/style props only */ @@ -107,6 +107,18 @@ function decorateHeaderCta(block, row) { decorateButtons(row); } +/** Insert hero intro image preload if missing (href must match viewport-selected picture). */ +function appendHeroIntroImagePreload(href, matchedWebpSource) { + if (document.querySelector(`head > link[rel="preload"][as="image"][href="${href}"]`)) return; + const preloadLink = document.createElement('link'); + preloadLink.rel = 'preload'; + preloadLink.as = 'image'; + preloadLink.href = href; + preloadLink.fetchPriority = 'high'; + if (matchedWebpSource || href.includes('format=webply')) preloadLink.type = 'image/webp'; + document.head.insertBefore(preloadLink, document.head.firstChild); +} + /** Apply background image from first picture to section (or UE preview). Uses an img for LCP. */ function applyIntroBackground(block, introContent, pictures) { const bgPicture = pictures[0]; @@ -122,11 +134,8 @@ function applyIntroBackground(block, introContent, pictures) { return; } - const webpSource = bgPicture.querySelector('source[type="image/webp"]'); - let bgUrl = webpSource?.srcset?.split(',')[0]?.trim()?.split(' ')[0] || bgImg?.src; - if (bgUrl?.includes('optimize=medium')) { - bgUrl = bgUrl.replace('optimize=medium', 'optimize=large'); - } + const { url: bgUrlRaw, source: matchedWebpSource } = pickPicturePreloadUrl(bgPicture, 'image/webp'); + const bgUrl = bgUrlRaw || bgImg?.src || ''; const sectionContainer = block.closest('.section'); if (!sectionContainer || !bgUrl) { @@ -135,17 +144,7 @@ function applyIntroBackground(block, introContent, pictures) { return; } - /* Preload only if not already added by scripts.js (early LCP preload) */ - const existingPreload = document.querySelector(`head > link[rel="preload"][as="image"][href="${bgUrl}"]`); - if (!existingPreload) { - const preloadLink = document.createElement('link'); - preloadLink.rel = 'preload'; - preloadLink.as = 'image'; - preloadLink.href = bgUrl; - preloadLink.fetchPriority = 'high'; - if (webpSource) preloadLink.type = 'image/webp'; - document.head.insertBefore(preloadLink, document.head.firstChild); - } + appendHeroIntroImagePreload(bgUrl, matchedWebpSource); /* Keep an img in the DOM as the LCP element (better than CSS background for PageSpeed) */ const layer = document.createElement('div'); diff --git a/blocks/hotspot/hotspot.css b/blocks/hotspot/hotspot.css index 40a54cb..4f1847f 100644 --- a/blocks/hotspot/hotspot.css +++ b/blocks/hotspot/hotspot.css @@ -8,7 +8,6 @@ /* Prevent CLS: hide until fully decorated, reserve space */ .hotspot:not([data-block-status="loaded"]) { visibility: hidden; - min-height: 400px; } /* Two-column layout */ @@ -17,7 +16,6 @@ align-items: stretch; justify-self: start; margin: 0 auto; - min-height: 400px; position: relative; transition: opacity 0.1s ease; } @@ -227,38 +225,9 @@ opacity: 1; } -/* Responsive adjustments */ -@media (width <= 1024px) { - .hotspot:not([data-block-status="loaded"]) { - min-height: 350px; - } - - .hotspot-container { - min-height: 350px; - } - - .hotspot-tooltip-panel { - flex: 0 0 35%; - padding: 30px; - } - - .hotspot-tooltip-content { - font-size: 16px; - } - - .hotspot-image-section { - flex: 0 0 65%; - } -} - @media (width <= 768px) { - .hotspot:not([data-block-status="loaded"]) { - min-height: 350px; - } - .hotspot-container { flex-direction: column; - min-height: 350px; } .hotspot-tooltip-panel { @@ -285,6 +254,22 @@ } } +/* Responsive adjustments */ +@media (width <= 1024px) { + .hotspot-tooltip-panel { + flex: 0 0 35%; + padding: 30px; + } + + .hotspot-tooltip-content { + font-size: 16px; + } + + .hotspot-image-section { + flex: 0 0 65%; + } +} + /* Hide empty panel */ .hotspot-tooltip-panel:not(.visible) { pointer-events: none; diff --git a/head.html b/head.html index c613248..5001fbf 100644 --- a/head.html +++ b/head.html @@ -1,13 +1,12 @@ - + - - - - + + + + diff --git a/scripts/aem.js b/scripts/aem.js index 669c284..71deade 100644 --- a/scripts/aem.js +++ b/scripts/aem.js @@ -356,6 +356,43 @@ function createOptimizedPicture( return picture; } +/** Cap scans (legitimate pictures use few sources; limits loop work for CWE-400). */ +const MAX_PICTURE_SOURCES_PRELOAD = 64; + +/** + * First matching URL for type + media (document order), else src. + * Matches browser behavior so preloads use the same URL as the chosen art direction + * (e.g. width=750 on mobile vs width=2000 for (min-width: 600px)). + * @param {HTMLPictureElement} picture + * @param {string} [typePrefix] e.g. image/webp + * @returns {{ url: string, source: Element|null, img: HTMLImageElement|null }} + */ +function pickPicturePreloadUrl(picture, typePrefix = 'image/webp') { + const needle = typePrefix.toLowerCase(); + const img = picture?.querySelector('img') ?? null; + if (!picture) { + return { url: '', source: null, img }; + } + const sources = picture.querySelectorAll('source'); + const n = Math.min(sources.length, MAX_PICTURE_SOURCES_PRELOAD); + for (let i = 0; i < n; i += 1) { + const source = sources[i]; + const t = (source.getAttribute('type') || '').toLowerCase(); + if (t.startsWith(needle)) { + const media = source.getAttribute('media'); + if (!media || window.matchMedia(media).matches) { + const srcset = source.getAttribute('srcset'); + if (srcset) { + const url = srcset.split(',')[0].trim().split(/\s+/)[0]; + if (url) return { url, source, img }; + } + } + } + } + const fallback = img?.getAttribute('src') || img?.src || ''; + return { url: fallback, source: null, img }; +} + /** * Set template (page structure) and theme (page styles). */ @@ -787,6 +824,7 @@ export { loadScript, loadSection, loadSections, + pickPicturePreloadUrl, readBlockConfig, sampleRUM, setup, diff --git a/scripts/scripts.js b/scripts/scripts.js index 85df236..84c5a11 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -16,6 +16,7 @@ import { getMetadata, DOMPURIFY, // readBlockConfig, + pickPicturePreloadUrl, toCamelCase, } from './aem.js'; @@ -280,30 +281,43 @@ function decorateButtonGroups(element) { } /** - * Preload hero-heritage-cc LCP background image as soon as the DOM is ready, - * before the block JS loads, to reduce resource load delay (e.g. in Chrome Performance). - * Runs synchronously after decorateMain so the image request starts before loadSection. + * Preload hero-heritage-cc LCP background image before block JS and before template work, + * matching URL/row logic in blocks/hero-heritage-cc/hero-heritage-cc.js (intro = 2nd DIV row). + * Also sets fetchpriority/loading on the intro img so the request competes earlier than loadBlock. */ function preloadHeroHeritageCcLcpImage(main) { - const block = main?.querySelector('.hero-heritage-cc'); + if (!main || document.querySelector('main[data-aue-resource]')) { + return; + } + const block = main.querySelector('.hero-heritage-cc'); if (!block) return; - const introRow = block.children[1]; + const rows = [...block.children].filter((c) => c.tagName === 'DIV'); + const introRow = rows[1]; if (!introRow) return; const introContent = introRow.querySelector(':scope > div'); const firstPicture = introContent?.querySelector('picture'); if (!firstPicture) return; - const webpSource = firstPicture.querySelector('source[type="image/webp"]'); const img = firstPicture.querySelector('img'); - let url = webpSource?.srcset?.split(',')[0]?.trim()?.split(' ')[0] || img?.src; + const { url: urlRaw, source: matchedWebpSource } = pickPicturePreloadUrl(firstPicture, 'image/webp'); + const url = urlRaw || img?.src || ''; if (!url) return; - if (url.includes('optimize=medium')) url = url.replace('optimize=medium', 'optimize=large'); - const link = document.createElement('link'); - link.rel = 'preload'; - link.as = 'image'; - link.href = url; - link.fetchPriority = 'high'; - if (webpSource) link.type = 'image/webp'; - document.head.insertBefore(link, document.head.firstChild); + const existingPreload = document.querySelector(`head > link[rel="preload"][as="image"][href="${url}"]`); + if (!existingPreload) { + const link = document.createElement('link'); + link.rel = 'preload'; + link.as = 'image'; + link.href = url; + /* Property + attribute: Lighthouse reads the DOM attribute on the preload hint. */ + link.fetchPriority = 'high'; + link.setAttribute('fetchpriority', 'high'); + if (matchedWebpSource || url.includes('format=webply')) link.type = 'image/webp'; + document.head.insertBefore(link, document.head.firstChild); + } + + if (img) { + img.setAttribute('fetchpriority', 'high'); + img.setAttribute('loading', 'eager'); + } } function prepareHeroForCLS(main) { @@ -1168,13 +1182,14 @@ async function loadEager(doc) { } } + const main = doc.querySelector('main'); const templateName = getMetadata('template'); if (templateName) { await loadTemplate(doc, templateName); } - const main = doc.querySelector('main'); if (main) { decorateMain(main); + /* Second pass if hero was missing at module init. Idempotent via preload dedupe. */ preloadHeroHeritageCcLcpImage(main); const h1Title = getMetadata('h1-title'); if (h1Title) { @@ -1812,4 +1827,7 @@ async function loadPage() { loadDelayed(); } +/* Earliest point in this module: before loadEager (fonts, template, decorateMain). */ +preloadHeroHeritageCcLcpImage(document.querySelector('main')); + loadPage(); diff --git a/styles/styles.css b/styles/styles.css index 39a302f..59baef7 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -162,10 +162,17 @@ html { /* -webkit-font-smoothing: antialiased; */ + scroll-behavior: smooth; text-size-adjust: 100%; -webkit-tap-highlight-color: rgb(0 0 0 / 0%); } +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } +} + body { display: none; margin: 0; @@ -531,6 +538,15 @@ main>.section>div { padding: 0 var(--grid-gutter-width); } +main > .section.hero-heritage-cc-container { + width: 100%; + height: 100vh; + + > div.hero-heritage-cc-wrapper { + max-width: 100%; + } +} + main>.section:first-of-type { margin-top: 0; }