diff --git a/blocks/breadcrumb/breadcrumb.js b/blocks/breadcrumb/breadcrumb.js index e2dc305ac..4b0b8f573 100644 --- a/blocks/breadcrumb/breadcrumb.js +++ b/blocks/breadcrumb/breadcrumb.js @@ -2,12 +2,13 @@ import { a, div, li, ul, } from '../../scripts/dom-builder.js'; import ffetch from '../../scripts/ffetch.js'; +import { getEdgeDeliveryPath } from '../../scripts/scripts.js'; const TEMPLATE_PATH_PATTERN = /\/us\/en\/[^/]+\/topics-template/; async function getItems() { + const path = getEdgeDeliveryPath(window.location.pathname); // get the breadcrumb items from the page path, only after '/us/en' - const path = window.location.pathname; const pathParts = path.split('/'); const itemPaths = pathParts.length > 2 ? pathParts.slice(3).map((_, i) => pathParts.slice(0, i + 4).join('/')) : []; const articles = await ffetch('/us/en/article-index.json') @@ -18,7 +19,8 @@ async function getItems() { return itemPaths.map((itemPath) => { // get the title from the article, based on its path const article = articles.find((entry) => entry.path === itemPath); - const title = (article && article.navTitle !== '') ? article.navTitle : itemPath.split('/').pop(); + let title = (article && article.navTitle !== '') ? article.navTitle : itemPath.split('/').pop(); + title = title.charAt(0).toUpperCase() + title.slice(1); return { title, href: `${itemPath}.html`, diff --git a/blocks/card-list/card-list.js b/blocks/card-list/card-list.js index ec5d0fe25..61ea21b5d 100644 --- a/blocks/card-list/card-list.js +++ b/blocks/card-list/card-list.js @@ -8,6 +8,7 @@ import createArticleCard from './articleCard.js'; import createLibraryCard from './libraryCard.js'; import createApplicationCard from './applicationCard.js'; import { makePublicUrl } from '../../scripts/scripts.js'; +import { buildItemListSchema } from '../../scripts/schema.js'; const getSelectionFromUrl = () => (window.location.pathname.indexOf('topics') > -1 ? toClassName(window.location.pathname.replace('.html', '').split('/').pop()) : ''); const getPageFromUrl = () => toClassName(new URLSearchParams(window.location.search).get('page')) || ''; @@ -162,7 +163,7 @@ export default async function decorate(block) { (item) => toClassName(item.topics).toLowerCase().indexOf(activeTagFilter) > -1, ); } - + buildItemListSchema(filteredArticles, 'resources'); // render cards application style if (articleType === 'application' || articleType === 'info') { filteredArticles.sort((card1, card2) => card1.title.localeCompare(card2.title)); diff --git a/blocks/product-card/product-card.js b/blocks/product-card/product-card.js index 0f87c8732..87166b608 100644 --- a/blocks/product-card/product-card.js +++ b/blocks/product-card/product-card.js @@ -2,6 +2,7 @@ import { getProductsOnSolutionsResponse, onClickCoveoAnalyticsResponse } from '. import { ul, a, p, div, span, h4, li, } from '../../scripts/dom-builder.js'; +import { buildItemListSchema } from '../../scripts/schema.js'; import { makePublicUrl, imageHelper } from '../../scripts/scripts.js'; export function createCard(product, idx, firstCard = false) { @@ -29,6 +30,9 @@ export function createCard(product, idx, firstCard = false) { export default async function decorate(block) { const response = await getProductsOnSolutionsResponse(); if (response?.results.length > 0) { + if (window.location.pathname.includes('process-steps')) buildItemListSchema(response?.results, 'solution-products-steps'); + else buildItemListSchema(response?.results, 'solution-products'); + const cardList = ul({ class: 'container grid max-w-7xl w-full mx-auto gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 px-4 pt-8 sm:px-0 justify-items-center mt-3 mb-3', }); diff --git a/blocks/product-category/product-category.js b/blocks/product-category/product-category.js index c93ae88e1..ea1de857f 100644 --- a/blocks/product-category/product-category.js +++ b/blocks/product-category/product-category.js @@ -4,7 +4,7 @@ import { } from '../../scripts/dom-builder.js'; import { makePublicUrl, imageHelper } from '../../scripts/scripts.js'; import { getMetadata } from '../../scripts/lib-franklin.js'; -import { buildProductCategorySchema } from '../../scripts/schema.js'; +import { buildItemListSchema } from '../../scripts/schema.js'; export function createCard(product, firstCard = false) { const cardWrapper = a( @@ -52,7 +52,7 @@ export default async function decorate(block) { products.forEach((product, index) => { cardList.append(createCard(product, index === 0)); }); - if (products.length > 0) buildProductCategorySchema(products); + if (products.length > 0) buildItemListSchema(products, 'product-category'); block.textContent = ''; block.append(cardList); diff --git a/blocks/product-family/product-family.js b/blocks/product-family/product-family.js index e80d29ef3..7cf73f865 100644 --- a/blocks/product-family/product-family.js +++ b/blocks/product-family/product-family.js @@ -7,7 +7,7 @@ import { div, span, button, fieldset, ul, li, input, a, img, p, } from '../../scripts/dom-builder.js'; import { decorateIcons } from '../../scripts/lib-franklin.js'; -import { buildProductCategorySchema } from '../../scripts/schema.js'; +import { buildItemListSchema } from '../../scripts/schema.js'; const productSkeleton = div( { class: 'coveo-skeleton flex flex-col w-full lg:flex-row grid-rows-1 lg:grid-cols-5 gap-x-10 gap-y-4' }, @@ -523,7 +523,7 @@ export async function decorateProductList(block) { block.removeChild(productSkeleton); return; } - if (res.totalCount > 0) buildProductCategorySchema(res.results); + if (res.totalCount > 0) buildItemListSchema(res.results, 'product-family'); facets(res, facetDiv); resultList(res, categoryDiv); block.removeChild(productSkeleton); diff --git a/blocks/recent-articles/recent-articles.js b/blocks/recent-articles/recent-articles.js index 557e43101..f773fce47 100644 --- a/blocks/recent-articles/recent-articles.js +++ b/blocks/recent-articles/recent-articles.js @@ -2,16 +2,17 @@ import ffetch from '../../scripts/ffetch.js'; import { div, ul, li, a, p, span, } from '../../scripts/dom-builder.js'; -import { formatDateUTCSeconds, makePublicUrl } from '../../scripts/scripts.js'; +import { formatDateUTCSeconds, getEdgeDeliveryPath, makePublicUrl } from '../../scripts/scripts.js'; import { getMetadata } from '../../scripts/lib-franklin.js'; export default async function decorate(block) { if (block.className.includes('recent-articles')) block.classList.add(...'flex-shrink-0 bg-danaherpurple-25'.split(' ')); const articleType = getMetadata('template').toLowerCase(); - const url = new URL(getMetadata('og:url')); + const url = new URL(getMetadata('og:url'), window.location.origin); + const path = getEdgeDeliveryPath(url.pathname); let articles = await ffetch('/us/en/article-index.json') .filter(({ type }) => type.toLowerCase() === articleType) - .filter((article) => url.pathname !== article.path) + .filter((article) => path !== article.path) .all(); articles = articles.sort((item1, item2) => item2.publishDate - item1.publishDate).slice(0, 6); diff --git a/blocks/related-articles/related-articles.js b/blocks/related-articles/related-articles.js index c6605f4d0..4cac593d7 100644 --- a/blocks/related-articles/related-articles.js +++ b/blocks/related-articles/related-articles.js @@ -4,15 +4,17 @@ import { ul, span, } from '../../scripts/dom-builder.js'; import createCard from '../card-list/articleCard.js'; +import { getEdgeDeliveryPath } from '../../scripts/scripts.js'; export default async function decorate(block) { const articleType = getMetadata('template').toLowerCase(); const articleTopics = getMetadata('topics')?.toLowerCase(); - const url = new URL(getMetadata('og:url')); + const url = new URL(getMetadata('og:url'), window.location.origin); + const path = getEdgeDeliveryPath(url.pathname); let articles = await ffetch('/us/en/article-index.json') .filter(({ type }) => type.toLowerCase() === articleType) .filter(({ topics }) => topics.toLowerCase() === articleTopics) - .filter((article) => url.pathname !== article.path) + .filter((article) => path !== article.path) .all(); articles = articles.sort((item1, item2) => item2.publishDate - item1.publishDate).slice(0, 3); diff --git a/blocks/tags-list/tags-list.js b/blocks/tags-list/tags-list.js index 3ae74dd0e..48ce76dee 100644 --- a/blocks/tags-list/tags-list.js +++ b/blocks/tags-list/tags-list.js @@ -5,15 +5,17 @@ import { import ffetch from '../../scripts/ffetch.js'; import { getMetadata, decorateIcons } from '../../scripts/lib-franklin.js'; import { createFilters } from '../card-list/card-list.js'; +import { getEdgeDeliveryPath } from '../../scripts/scripts.js'; export default async function decorate(block) { const articleType = getMetadata('template').toLowerCase(); const articleTopics = getMetadata('topics')?.toLowerCase(); - const url = new URL(getMetadata('og:url')); + const url = new URL(getMetadata('og:url'), window.location.origin); + const path = getEdgeDeliveryPath(url.pathname); let articles = await ffetch('/us/en/article-index.json') .filter(({ type }) => type.toLowerCase() === articleType) .filter(({ topics }) => topics.toLowerCase() === articleTopics) - .filter((article) => url.pathname === article.path) + .filter((article) => path === article.path) .all(); articles = articles.sort((item1, item2) => item2.publishDate - item1.publishDate).slice(0, 1); diff --git a/blocks/timeline/timeline.js b/blocks/timeline/timeline.js index d77a6b681..1666b7cdd 100644 --- a/blocks/timeline/timeline.js +++ b/blocks/timeline/timeline.js @@ -6,6 +6,7 @@ import { label, span, } from '../../scripts/dom-builder.js'; +import { buildItemListSchema } from '../../scripts/schema.js'; function updateMenu(target, block) { const clickedMenu = target.closest('.menu-item'); @@ -98,6 +99,7 @@ export default function decorate(block) { if (type !== 'menu') { block.classList.add(...'w-full h-full top-14 bottom-0'.split(' ')); const items = block.children; + buildItemListSchema([...block.children], 'process-steps'); [...items].forEach((item, idx) => { const picture = item.querySelector('div:last-child > p > picture'); const timeline = (idx % 2 === 0) diff --git a/blocks/workflow-tabs/workflow-tabs.js b/blocks/workflow-tabs/workflow-tabs.js index 62f912a11..a7b90c532 100644 --- a/blocks/workflow-tabs/workflow-tabs.js +++ b/blocks/workflow-tabs/workflow-tabs.js @@ -1,7 +1,8 @@ import { div, li, ul, a, span, } from '../../scripts/dom-builder.js'; -import { processEmbedFragment } from '../../scripts/scripts.js'; +import { processEmbedFragment, getFragmentFromFile } from '../../scripts/scripts.js'; +import { buildItemListSchema } from '../../scripts/schema.js'; const classActive = 'active'; const danaherPurpleClass = 'bg-danaherpurple-500'; @@ -74,6 +75,16 @@ async function buildTabs(block) { const tabPanes = block.querySelectorAll('.workflow-tabs > div > div:last-child'); const tabList = div({ class: 'tabs-list' }); const decoratedPanes = await Promise.all([...tabPanes].map(async (pane, index) => { + // Added for SEO Schema generation + if (pane.textContent?.includes('workflow-carousels/master')) { + const fragment = await getFragmentFromFile(`${pane.textContent}.plain.html`); + const wfCarousel = document.createElement('div'); + wfCarousel.innerHTML = fragment; + const childs = wfCarousel.querySelector('div.workflow-carousel').children; + buildItemListSchema([...childs].splice(1, childs.length), 'workflow'); + } + // End of SEO Schema generation + const isActive = index === 0; pane.classList.add('tab-pane', isActive ? classActive : 'off-screen'); const decoratedPane = await processEmbedFragment(pane); diff --git a/component-models.json b/component-models.json index 692aaebe5..df435f339 100644 --- a/component-models.json +++ b/component-models.json @@ -28,7 +28,6 @@ "component": "aem-tag", "valueType": "string", "name": "cq:tags", - "multi": true, "label": "AEM Tag Picker" }, { @@ -71,7 +70,7 @@ }, { "component": "date-time", - "valueType": "date", + "valueType": "date-time", "name": "publishDate", "label": "Publish Date", "placeholder": "YYYY-MM-DD", @@ -83,6 +82,13 @@ "valueType": "string", "name": "navTitle", "label": "Navigation Title" + }, + { + "component": "text", + "valueType": "string", + "name": "template", + "label": "Template (temporary)", + "description": "Until we get the metadata spreadsheet fixed" } ] } diff --git a/paths.json b/paths.json index cbb8084b1..d123f7b8e 100644 --- a/paths.json +++ b/paths.json @@ -8,7 +8,10 @@ "/content/dam/danaher/franklin/metadata.json:/metadata.json", "/content/dam/danaher/franklin/metadata-products.json:/metadata-products.json", "/content/dam/danaher/franklin/metadata-articles.json:/metadata-articles.json", - "/content/dam/danaher/franklin/redirects.json:/redirects.json" + "/content/dam/danaher/franklin/redirects.json:/redirects.json", + "/content/danaher.resource/us/en/article-index.json:/us/en/article-index.json", + "/content/danaher.resource/fragments/header/master.plain.html:/fragments/header/master.plain.html", + "/content/danaher.resource/fragments/footer.html:/fragments/footer.html" ], "includes": [ "/content/danaher/ls/us/en", diff --git a/scripts/lib-franklin-dev.js b/scripts/lib-franklin-dev.js index 9e2146fed..5d99d804c 100644 --- a/scripts/lib-franklin-dev.js +++ b/scripts/lib-franklin-dev.js @@ -175,7 +175,7 @@ export async function loadScript(src, attrs) { */ export function getMetadata(name) { const attr = name && name.includes(':') ? 'property' : 'name'; - const meta = [...document.head.querySelectorAll(`meta[${attr}="${name}"]`)].map((m) => m.content).join(', '); + const meta = [...document.head.querySelectorAll(`meta[${attr}="${name}" i]`)].map((m) => m.content).join(', '); return meta || ''; } @@ -862,7 +862,12 @@ export function setup() { const scriptEl = document.querySelector('script[src$="/scripts/scripts.js"]'); if (scriptEl) { try { - [window.hlx.codeBasePath] = new URL(scriptEl.src).pathname.split('/scripts/scripts.js'); + const scriptURL = new URL(scriptEl.src, window.location); + if (scriptURL.host === window.location.host) { + [window.hlx.codeBasePath] = scriptURL.pathname.split('/scripts/scripts.js'); + } else { + [window.hlx.codeBasePath] = scriptURL.href.split('/scripts/scripts.js'); + } } catch (error) { // eslint-disable-next-line no-console console.log(error); diff --git a/scripts/lib-franklin.js b/scripts/lib-franklin.js index 39e172cab..1f8994b3b 100644 --- a/scripts/lib-franklin.js +++ b/scripts/lib-franklin.js @@ -1 +1 @@ -export function sampleRUM(checkpoint,data={}){sampleRUM.defer=sampleRUM.defer||[];const defer=fnname=>{sampleRUM[fnname]=sampleRUM[fnname]||((...args)=>sampleRUM.defer.push({fnname:fnname,args:args}))};sampleRUM.drain=sampleRUM.drain||((dfnname,fn)=>{sampleRUM[dfnname]=fn;sampleRUM.defer.filter((({fnname:fnname})=>dfnname===fnname)).forEach((({fnname:fnname,args:args})=>sampleRUM[fnname](...args)))});sampleRUM.always=sampleRUM.always||[];sampleRUM.always.on=(chkpnt,fn)=>{sampleRUM.always[chkpnt]=fn};sampleRUM.on=(chkpnt,fn)=>{sampleRUM.cases[chkpnt]=fn};defer("observe");defer("cwv");try{window.hlx=window.hlx||{};if(!window.hlx.rum){const usp=new URLSearchParams(window.location.search);const weight=usp.get("rum")==="on"?1:20;const id=Array.from({length:75},((_,i)=>String.fromCharCode(48+i))).filter((a=>/\d|[A-Z]/i.test(a))).filter((()=>Math.random()*75>70)).join("");const random=Math.random();const isSelected=random*weight<1;const firstReadTime=Date.now();const urlSanitizers={full:()=>window.location.href,origin:()=>window.location.origin,path:()=>window.location.href.replace(/\?.*$/,"")};window.hlx.rum={weight:weight,id:id,random:random,isSelected:isSelected,firstReadTime:firstReadTime,sampleRUM:sampleRUM,sanitizeURL:urlSanitizers[window.hlx.RUM_MASK_URL||"path"]}}const{weight:weight,id:id,firstReadTime:firstReadTime}=window.hlx.rum;if(window.hlx&&window.hlx.rum&&window.hlx.rum.isSelected){const knownProperties=["weight","id","referer","checkpoint","t","source","target","cwv","CLS","FID","LCP","INP"];const sendPing=(pdata=data)=>{const body=JSON.stringify({weight:weight,id:id,referer:window.hlx.rum.sanitizeURL(),checkpoint:checkpoint,t:Date.now()-firstReadTime,...data},knownProperties);const url=`https://rum.hlx.page/.rum/${weight}`;navigator.sendBeacon(url,body);console.debug(`ping:${checkpoint}`,pdata)};sampleRUM.cases=sampleRUM.cases||{cwv:()=>sampleRUM.cwv(data)||true,lazy:()=>{const script=document.createElement("script");script.src="https://rum.hlx.page/.rum/@adobe/helix-rum-enhancer@^1/src/index.js";document.head.appendChild(script);return true}};sendPing(data);if(sampleRUM.cases[checkpoint]){sampleRUM.cases[checkpoint]()}}if(sampleRUM.always[checkpoint]){sampleRUM.always[checkpoint](data)}}catch(error){}}export async function loadCSS(href){return new Promise(((resolve,reject)=>{if(!document.querySelector(`head > link[href="${href}"]`)){const link=document.createElement("link");link.rel="stylesheet";link.href=href;link.onload=resolve;link.onerror=reject;document.head.append(link)}else{resolve()}}))}export async function loadScript(src,attrs){return new Promise(((resolve,reject)=>{if(!document.querySelector(`head > script[src="${src}"]`)){const script=document.createElement("script");script.src=src;if(attrs){for(const attr in attrs){script.setAttribute(attr,attrs[attr])}}script.onload=resolve;script.onerror=reject;document.head.append(script)}else{resolve()}}))}export function getMetadata(name){const attr=name&&name.includes(":")?"property":"name";const meta=[...document.head.querySelectorAll(`meta[${attr}="${name}"]`)].map((m=>m.content)).join(", ");return meta||""}export function toClassName(name){return typeof name==="string"?name.toLowerCase().replace(/[^0-9a-z]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,""):""}export function toCamelCase(name){return toClassName(name).replace(/-([a-z])/g,(g=>g[1].toUpperCase()))}export function getAllMetadata(scope){return[...document.head.querySelectorAll(`meta[property^="${scope}:"],meta[name^="${scope}-"]`)].reduce(((res,meta)=>{const id=toClassName(meta.name?meta.name.substring(scope.length+1):meta.getAttribute("property").split(":")[1]);res[id]=meta.getAttribute("content");return res}),{})}const ICONS_CACHE={};export async function decorateIcons(element){let svgSprite=document.getElementById("franklin-svg-sprite");if(!svgSprite){const div=document.createElement("div");div.innerHTML='';svgSprite=div.firstElementChild;document.body.append(div.firstElementChild)}const icons=[...element.querySelectorAll("span.icon")];await Promise.all(icons.map((async span=>{const iconName=Array.from(span.classList).find((c=>c.startsWith("icon-"))).substring(5);if(!ICONS_CACHE[iconName]){ICONS_CACHE[iconName]=true;try{let iconSource=`${window.hlx.codeBasePath}/icons/${iconName}.svg`;if(iconName.startsWith("dam-")){const isPublicDomain=window.location.hostname.includes("lifesciences.danaher.com");iconSource=isPublicDomain?"":"https://lifesciences.danaher.com";iconSource+=`/content/dam/danaher/system/icons/${iconName.substring(4).replace("_"," ")}.svg`}const response=await fetch(iconSource);if(!response.ok){ICONS_CACHE[iconName]=false;return}const svg=await response.text();if(svg.match(/(