From e9a2fe54f091cab000d186bd68e45d3caaabd8bd Mon Sep 17 00:00:00 2001 From: Jingle Huang <32369333+JingleH@users.noreply.github.com> Date: Mon, 26 Jun 2023 10:35:21 -0700 Subject: [PATCH] Template v2 release fixes (#926) * update fetch limit to point at different cache * adding missed default content replacement * fixing legacy logic --------- Co-authored-by: Qiyun Dai --- express/blocks/template-list/breadcrumbs.js | 151 ++++++++++++++ express/scripts/all-templates-metadata.js | 72 +++++++ express/scripts/content-replace.js | 206 ++++++++++++++++++++ express/scripts/scripts.js | 6 +- 4 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 express/blocks/template-list/breadcrumbs.js create mode 100644 express/scripts/all-templates-metadata.js create mode 100644 express/scripts/content-replace.js diff --git a/express/blocks/template-list/breadcrumbs.js b/express/blocks/template-list/breadcrumbs.js new file mode 100644 index 000000000..ae9da74de --- /dev/null +++ b/express/blocks/template-list/breadcrumbs.js @@ -0,0 +1,151 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { + fetchPlaceholders, + getMetadata, + titleCase, + createTag, +} from '../../scripts/scripts.js'; +import fetchAllTemplatesMetadata from '../../scripts/all-templates-metadata.js'; + +function sanitize(str) { + return str.replaceAll(/[$@%'"]/g, ''); +} + +function getCrumbsForSearch(templatesUrl, allTemplatesMetadata, taskCategories) { + const { search, origin } = window.location; + let { tasks, topics } = new Proxy(new URLSearchParams(search), { + get: (searchParams, prop) => searchParams.get(prop), + }); + tasks = sanitize(tasks); + topics = sanitize(topics); + const crumbs = []; + if (!tasks && !topics) { + return crumbs; + } + const shortTitle = getMetadata('short-title'); + if (!shortTitle) { + return crumbs; + } + + const lastCrumb = createTag('li'); + lastCrumb.textContent = shortTitle; + crumbs.push(lastCrumb); + if (!tasks || !topics) { + return crumbs; + } + + const taskUrl = `${templatesUrl}${tasks}`; + const foundTaskPage = allTemplatesMetadata + .some((t) => t.url === taskUrl.replace(origin, '')); + + if (foundTaskPage) { + const taskCrumb = createTag('li'); + const taskAnchor = createTag('a', { href: taskUrl }); + taskCrumb.append(taskAnchor); + const translatedTasks = Object.entries(taskCategories) + .find(([_, t]) => t === tasks || t === tasks.replace(/-/g, ' ')) + ?.[0]?.toLowerCase() ?? tasks; + taskAnchor.textContent = titleCase(translatedTasks); + crumbs.unshift(taskCrumb); + } + + return crumbs; +} + +function getCrumbsForSEOPage(templatesUrl, allTemplatesMetadata, taskCategories, segments) { + const { origin, pathname } = window.location; + const tasks = getMetadata('tasks') + // TODO: remove templateTasks and allTemplatesMetadata here after all content are updated + ?? getMetadata('templateTasks') + ?? allTemplatesMetadata[pathname]?.tasks + ?? allTemplatesMetadata[pathname]?.templateTasks; + const translatedTasks = Object.entries(taskCategories) + .find(([_, t]) => t === tasks || t === tasks.replace(/-/g, ' ')) + ?.[0]?.toLowerCase() ?? tasks; + // we might have an inconsistent trailing slash problem + let builtUrl = templatesUrl.replace('templates/', 'templates'); + const crumbs = []; + segments + .slice(0, segments.length - 1) + .forEach((currSeg) => { + const seg = sanitize(currSeg); + if (!seg) return; + builtUrl = `${builtUrl}/${seg}`; + // at least translate tasks seg + const translatedSeg = seg === tasks ? translatedTasks : seg; + const segmentCrumb = createTag('li'); + if (allTemplatesMetadata.some((t) => t.url === builtUrl.replace(origin, ''))) { + const segmentLink = createTag('a', { href: builtUrl }); + segmentLink.textContent = titleCase(translatedSeg); + segmentCrumb.append(segmentLink); + } else { + segmentCrumb.textContent = titleCase(translatedSeg); + } + crumbs.push(segmentCrumb); + }); + const lastCrumb = createTag('li'); + lastCrumb.textContent = getMetadata('short-title'); + crumbs.push(lastCrumb); + return crumbs; +} + +// returns null if no breadcrumbs +// returns breadcrumbs as an li element +export default async function getBreadcrumbs() { + // for backward compatibility + // TODO: remove this check after all content are updated + if (getMetadata('sheet-powered') !== 'Y' || !document.querySelector('.search-marquee')) { + return null; + } + const { origin, pathname } = window.location; + const regex = /(.*?\/express\/)templates(.*)/; + const matches = pathname.match(regex); + if (!matches) { + return null; + } + const placeholders = await fetchPlaceholders(); + const [, homePath, children] = matches; + const breadcrumbs = createTag('ol', { class: 'templates-breadcrumbs' }); + + const homeCrumb = createTag('li'); + const homeUrl = `${origin}${homePath}`; + const homeAnchor = createTag('a', { href: homeUrl }); + homeAnchor.textContent = titleCase(placeholders.express || '') || 'Home'; + homeCrumb.append(homeAnchor); + breadcrumbs.append(homeCrumb); + + const templatesCrumb = createTag('li'); + const templatesUrl = `${homeUrl}templates/`; + const templatesAnchor = createTag('a', { href: templatesUrl }); + templatesAnchor.textContent = titleCase(placeholders.templates || '') || 'Templates'; + templatesCrumb.append(templatesAnchor); + breadcrumbs.append(templatesCrumb); + + const nav = createTag('nav', { 'aria-label': 'Breadcrumb' }); + nav.append(breadcrumbs); + + if (!children || children === '/') { + return nav; + } + const taskCategories = JSON.parse(placeholders['task-categories']); + const allTemplatesMetadata = await fetchAllTemplatesMetadata(); + const isSearchPage = children.startsWith('/search?') || getMetadata('template-search-page') === 'Y'; + const crumbs = isSearchPage + ? getCrumbsForSearch(templatesUrl, allTemplatesMetadata, taskCategories) + : getCrumbsForSEOPage(templatesUrl, allTemplatesMetadata, taskCategories, children.split('/')); + + crumbs.forEach((c) => { + breadcrumbs.append(c); + }); + return nav; +} diff --git a/express/scripts/all-templates-metadata.js b/express/scripts/all-templates-metadata.js new file mode 100644 index 000000000..dc5186f24 --- /dev/null +++ b/express/scripts/all-templates-metadata.js @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { getHelixEnv, getLocale } from './scripts.js'; +import { memoize } from './utils.js'; + +const memoizedFetchUrl = memoize((url) => fetch(url).then((r) => (r.ok ? r.json() : null)), { + key: (q) => q, + ttl: 1000 * 60 * 60 * 24, +}); + +let allTemplatesMetadata; + +export default async function fetchAllTemplatesMetadata() { + const locale = getLocale(window.location); + const urlPrefix = locale === 'us' ? '' : `/${locale}`; + + if (!allTemplatesMetadata) { + try { + const env = getHelixEnv(); + const dev = new URLSearchParams(window.location.search).get('dev'); + let sheet; + + if (['yes', 'true', 'on'].includes(dev) && env?.name === 'stage') { + sheet = '/templates-dev.json?sheet=seo-templates&limit=100000'; + } else { + sheet = `${urlPrefix}/express/templates/default/metadata.json?limit=100000`; + } + + let resp = await memoizedFetchUrl(sheet); + allTemplatesMetadata = resp?.data; + + // TODO: remove the > 1 logic after publishing of the split metadata sheet + if (!(allTemplatesMetadata && allTemplatesMetadata.length > 1)) { + resp = await memoizedFetchUrl('/express/templates/content.json?sheet=seo-templates&limit=100000'); + allTemplatesMetadata = resp?.data?.map((p) => ({ + ...p, + // TODO: backward compatibility. Remove when we move away from helix-seo-templates + url: p.path, + title: p.metadataTitle, + description: p.metadataDescription, + 'short-title': p.shortTitle, + ckgid: p.ckgID, + 'hero-title': p.heroAnimationTitle, + 'hero-text': p.heroAnimationText, + locales: p.templateLocale, + premium: p.templatePremium, + animated: p.templateAnimated, + tasks: p.templateTasks, + topics: p.templateTopics, + 'placeholder-format': p.placeholderFormat, + 'create-link': p.createLink, + 'create-text': p.createText, + 'top-templates': p.topTemplates, + 'top-templates-text': p.topTemplatesText, + })) || []; + } + } catch (err) { + allTemplatesMetadata = []; + } + } + return allTemplatesMetadata; +} diff --git a/express/scripts/content-replace.js b/express/scripts/content-replace.js new file mode 100644 index 000000000..ec2802af9 --- /dev/null +++ b/express/scripts/content-replace.js @@ -0,0 +1,206 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + fetchPlaceholders, + getMetadata, + titleCase, + createTag, +} from './scripts.js'; +import fetchAllTemplatesMetadata from './all-templates-metadata.js'; + +async function replaceDefaultPlaceholders(template) { + template.innerHTML = template.innerHTML.replaceAll('https://www.adobe.com/express/templates/default-create-link', getMetadata('create-link') || '/'); + + if (getMetadata('tasks') === '') { + const placeholders = await fetchPlaceholders(); + template.innerHTML = template.innerHTML.replaceAll('default-create-link-text', placeholders['start-from-scratch'] || ''); + } else { + template.innerHTML = template.innerHTML.replaceAll('default-create-link-text', getMetadata('create-text') || ''); + } +} + +async function getReplacementsFromSearch() { + const params = new Proxy(new URLSearchParams(window.location.search), { + get: (searchParams, prop) => searchParams.get(prop), + }); + const { + tasks, + phformat, + topics, + q, + } = params; + if (!tasks && !phformat) { + return null; + } + const placeholders = await fetchPlaceholders(); + const categories = JSON.parse(placeholders['task-categories']); + if (!categories) { + return null; + } + const tasksPair = Object.entries(categories).find((cat) => cat[1] === tasks); + const sanitizedTasks = tasks === "''" ? '' : tasks; + const sanitizedTopics = topics === "''" ? '' : topics; + const sanitizedQuery = q === "''" ? '' : q; + const translatedTasks = tasksPair ? tasksPair[0].toLowerCase() : tasks; + return { + '{{queryTasks}}': sanitizedTasks || '', + '{{QueryTasks}}': titleCase(sanitizedTasks || ''), + '{{translatedTasks}}': translatedTasks || '', + '{{TranslatedTasks}}': titleCase(translatedTasks || ''), + '{{placeholderRatio}}': phformat || '', + '{{QueryTopics}}': titleCase(sanitizedTopics || ''), + '{{queryTopics}}': sanitizedTopics || '', + '{{query}}': sanitizedQuery || '', + }; +} + +const bladeRegex = /\{\{[a-zA-Z_-]+\}\}/g; +function replaceBladesInStr(str, replacements) { + if (!replacements) return str; + return str.replaceAll(bladeRegex, (match) => { + if (match in replacements) { + return replacements[match]; + } + return match; + }); +} + +// for backwards compatibility +// TODO: remove this func after all content is updated +// legacy json -> metadata & dom blades +await (async function updateLegacyContent() { + const searchMarquee = document.querySelector('.search-marquee'); + if (searchMarquee) { + // not legacy + return; + } + const legacyAllTemplatesMetadata = await fetchAllTemplatesMetadata(); + const data = legacyAllTemplatesMetadata.find((p) => p.url === window.location.pathname); + if (!data) return; + if (['yes', 'true', 'on', 'Y'].includes(getMetadata('template-search-page'))) { + const replacements = await getReplacementsFromSearch(); + if (!replacements) return; + for (const key of Object.keys(data)) { + data[key] = replaceBladesInStr(data[key], replacements); + } + } + + const heroAnimation = document.querySelector('.hero-animation.wide'); + const templateList = document.querySelector('.template-list.fullwidth.apipowered'); + + const head = document.querySelector('head'); + Object.keys(data).forEach((metadataKey) => { + const existingMetadataTag = head.querySelector(`meta[name=${metadataKey}]`); + if (existingMetadataTag) { + existingMetadataTag.setAttribute('content', data[metadataKey]); + } else { + head.append(createTag('meta', { name: `${metadataKey}`, content: data[metadataKey] })); + } + }); + + if (heroAnimation) { + if (data.heroAnimationTitle) { + heroAnimation.innerHTML = heroAnimation.innerHTML.replace('Default template title', data.heroAnimationTitle); + } + + if (data.heroAnimationText) { + heroAnimation.innerHTML = heroAnimation.innerHTML.replace('Default template text', data.heroAnimationText); + } + } + + if (templateList) { + const regex = /default-[a-zA-Z_-]+/g; + const replacements = { + 'default-title': data.shortTitle || '', + 'default-tasks': data.templateTasks || '', + 'default-topics': data.templateTopics || '', + 'default-locale': data.templateLocale || 'en', + 'default-premium': data.templatePremium || '', + 'default-animated': data.templateAnimated || '', + 'default-format': data.placeholderFormat || '', + }; + templateList.innerHTML = templateList.innerHTML.replaceAll(regex, (match) => { + if (match in replacements) { + return replacements[match]; + } + return match; + }).replaceAll('https://www.adobe.com/express/templates/default-create-link', data.createLink || '/'); + + if (data.templateTasks === '') { + const placeholders = await fetchPlaceholders(); + templateList.innerHTML = templateList.innerHTML.replaceAll('default-create-link-text', placeholders['start-from-scratch'] || ''); + } else { + templateList.innerHTML = templateList.innerHTML.replaceAll('default-create-link-text', data.createText || ''); + } + } +}()); + +// searchbar -> metadata blades +await (async function updateMetadataForTemplates() { + if (!['yes', 'true', 'on', 'Y'].includes(getMetadata('template-search-page'))) { + return; + } + const head = document.querySelector('head'); + if (head) { + const replacements = await getReplacementsFromSearch(); + if (!replacements) return; + head.innerHTML = replaceBladesInStr(head.innerHTML, replacements); + } +}()); + +// metadata -> dom blades +(function autoUpdatePage() { + const wl = ['{{heading_placeholder}}', '{{type}}', '{{quantity}}']; + // FIXME: deprecate wl + const main = document.querySelector('main'); + if (!main) return; + const regex = /\{\{([a-zA-Z_-]+)\}\}/g; + main.innerHTML = main.innerHTML.replaceAll(regex, (match, p1) => { + if (!wl.includes(match.toLowerCase())) { + return getMetadata(p1); + } + return match; + }); +}()); + +// cleanup remaining dom blades +(async function updateNonBladeContent() { + const templateList = document.querySelector('.template-list.fullwidth.apipowered'); + const templateX = document.querySelector('.template-x'); + const browseByCat = document.querySelector('.browse-by-category'); + const seoNav = document.querySelector('.seo-nav'); + + if (templateList) { + await replaceDefaultPlaceholders(templateList); + } + + if (templateX) { + await replaceDefaultPlaceholders(templateX); + } + + if (seoNav) { + if (getMetadata('top-templates-title')) { + seoNav.innerHTML = seoNav.innerHTML.replace('Default top templates title', getMetadata('top-templates-title')); + } + + if (getMetadata('top-templates-text')) { + seoNav.innerHTML = seoNav.innerHTML.replace('Default top templates text', getMetadata('top-templates-text')); + } else { + seoNav.innerHTML = seoNav.innerHTML.replace('Default top templates text', ''); + } + } + + if (browseByCat && !['yes', 'true', 'on', 'Y'].includes(getMetadata('show-browse-by-category'))) { + browseByCat.remove(); + } +}()); diff --git a/express/scripts/scripts.js b/express/scripts/scripts.js index 82a613022..2df219bcd 100644 --- a/express/scripts/scripts.js +++ b/express/scripts/scripts.js @@ -1773,13 +1773,13 @@ export async function fetchFloatingCta(path) { } if (['yes', 'true', 'on'].includes(dev) && env && env.name === 'stage') { - spreadsheet = '/express/floating-cta-dev.json?limit=10000'; + spreadsheet = '/express/floating-cta-dev.json?limit=100000'; } else { - spreadsheet = '/express/floating-cta.json?limit=10000'; + spreadsheet = '/express/floating-cta.json?limit=100000'; } if (experimentStatus === 'active') { - const expSheet = '/express/experiments/floating-cta-experiments.json?limit=10000'; + const expSheet = '/express/experiments/floating-cta-experiments.json?limit=100000'; floatingBtnData = await fetchFloatingBtnData(expSheet); }