diff --git a/express/blocks/blog-posts/blog-posts.css b/express/blocks/blog-posts/blog-posts.css new file mode 100644 index 0000000..4d9eb2c --- /dev/null +++ b/express/blocks/blog-posts/blog-posts.css @@ -0,0 +1,297 @@ +.blog main .section:has(.blog-posts-wrapper) > div:first-child { + padding: 0; + margin-top: 64px; +} + +main .section:has(.blog-posts-wrapper) > div { + max-width: 375px; + margin: auto; + padding: 0; +} + +main .section:has(.blog-posts-wrapper) > .content:first-child, main .section > .blog-posts-wrapper:first-child { + padding-top: 60px +} + +.blog main .section:has(.blog-posts-wrapper) > div > h2, main .section:has(.blog-posts) div.blog-posts-decoration { + max-width: 280px; +} + +main .section:has(.blog-posts-wrapper) div.blog-posts-decoration { + display: flex; + justify-content: space-between; +} + +main .section:has(.blog-posts) div.blog-posts-decoration p { + margin: 0; + font-family: var(--body-serif-font-family); +} + +main .section:has(.blog-posts) div.blog-posts-decoration p a { + margin: 0; + font-family: var(--body-font-family); +} + +main .blog-posts .blog-cards { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 0 16px 64px 16px; + width: unset; +} + +main .blog-posts { + text-align: center; +} + +main .blog-posts .blog-card, main .blog-posts .blog-hero-card { + cursor: pointer; + text-decoration: unset; + color: unset; + text-align: left; +} + +main .blog-posts .blog-card { + display: grid; + grid-template-areas: 'title title' 'teaser image' 'date image'; + width: 350px; + margin: 28px; +} + +main .blog-posts .blog-hero-card { + display: flex; + flex-direction: column-reverse; + width: 100%; + max-width: 530px; + margin: 80px auto; +} + +main .blog-posts .blog-card > p, main .blog-posts .blog-hero-card > p { + margin: 0; +} + +main .blog-posts .blog-card .blog-card-image { + line-height: 0; + grid-area: image; + margin: 0; + margin-left: 16px; + height: 92px; + width: 126px; +} + +main .blog-posts .blog-hero-card .blog-card-image { + line-height: 0; + grid-area: image; + margin: 0; + height: 276px; +} + +main .blog-posts .blog-card .blog-card-image img { + object-fit: cover; + height: 100%; + width: 100%; + border-radius: 6px; +} + +main .blog-posts .blog-hero-card .blog-card-image img { + height: 100%; + width: 100%; + margin: auto; + display: block; + object-fit: cover; +} + +main .blog-posts .blog-card h3.blog-card-title { + font-size: var(--heading-font-size-s); + line-height: var(--heading-line-height); + text-align: left; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + grid-area: title; + margin-bottom: 16px; + margin-top: 0; +} + +main .blog-posts .blog-hero-card h3.blog-card-title { + font-size: var(--heading-font-size-xl); + line-height: var(--heading-line-height); + text-align: left; + grid-area: title; + margin: 0 24px; +} + +main .blog-posts .blog-card p.blog-card-teaser { + font-size: var(--body-font-size-m); + line-height: var(--body-line-height); + font-weight: normal; + font-family: var(--body-serif-font-family); + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + grid-area: teaser; +} + + +main .blog-posts .blog-hero-card p.blog-card-teaser { + font-family: var(--body-serif-font-family); + font-size: var(--body-font-size-l); + text-align: left; + grid-area: teaser; + font-weight: normal; + margin: 24px; +} + +main .blog-posts p.blog-card-date { + font-size: var(--body-font-size-s); + line-height: var(--body-line-height); + font-family: var(--body-serif-font-family); + font-weight: var(--heading-font-weight); + grid-area: date; +} + +main .blog-posts .blog-hero-card p.blog-card-date { + margin: 32px 0 32px 24px; +} + +main .blog-posts .blog-hero-card p.blog-card-cta { + grid-area: cta; + text-align: left; + margin-top: 14px; + margin-bottom: 30px; + margin-left: 24px; +} + +main .blog-posts .blog-hero-card p.blog-card-cta a { + margin-left: 0; +} + +main .blog-posts .card { + display: flex; + flex-direction: column; + width: 290px; + margin: 10px; + text-align: left; + cursor: pointer; +} +main .blog-posts .card .card-image { + line-height: 0; +} + +main .blog-posts .card .card-image img { + height: 200px; + object-fit: cover; + width: 100%; +} + +main .blog-posts .card .card-body { + padding: 20px; + border: 1px solid lightgrey; + border-radius: 0 0 5px 5px; + border-top: none; + height: 300px; +} + +main .blog-posts .card .card-body p { + font-size: 1rem; +} + +main .section:has(.blog-posts) > .content >h2 { + margin-top: 80px; +} + +main .section:has(.blog-posts) > .content >h3 { + margin-top: 64px; +} + +main .section:has(.blog-posts) > .content { + padding: 0; +} + +@media (min-width: 900px) { + main .blog-posts .blog-card .blog-card-image { + margin: 0 0 24px 0; + border-radius: 10px; + height: 235px; + width: 325px; + } + + main .blog-posts .blog-hero-card .blog-card-image { + min-width: 488px; + max-width: 488px; + height: 274px; + } + + main .blog-posts .blog-card { + grid-template-areas: 'image' 'title' 'teaser' 'date'; + width: 325px; + } + + main .blog-posts .blog-hero-card { + flex-direction: unset; + max-width: 1200px; + align-items: center; + margin: 56px auto; + } + + main .blog-posts .blog-card h3.blog-card-title { + font-size: var(--heading-font-size-m); + margin: 0 0 24px 0; + } + + main .blog-posts .blog-hero-card p.blog-card-cta { + margin-bottom: 0px; + } + + main .blog-posts .blog-hero-card p.blog-card-cta a { + margin-bottom: 0px; + } + + main .section:has(.blog-posts) > div, main .section:has(.blog-posts) > .content { + max-width: 900px; + } + + .blog main .section:has(.blog-posts) > div > h2, main .section:has(.blog-posts) div.blog-posts-decoration { + max-width: 700px; + } +} + +@media (min-width: 1200px) { + main .section:has(.blog-posts) > div, main .section:has(.blog-posts) > .content { + max-width: 1200px; + } + + main .blog-posts .blog-hero-card .blog-card-image { + min-width: 600px; + max-width: 600px; + height: 338px; + } + + .blog main .section:has(.blog-posts) > div > h2, main .section:has(.blog-posts) div.blog-posts-decoration { + max-width: 1088px; + } +} + +/* Japanese font styles */ +:lang(ja) main .section:has(.blog-posts) div.blog-posts-decoration p { + font-family: var(--body-font-family); +} + +:lang(ja) main .blog-posts .blog-card p.blog-card-teaser { + font-family: var(--body-font-family); +} + +:lang(ja) main .blog-posts .blog-hero-card p.blog-card-teaser { + font-family: var(--body-font-family); +} + +:lang(ja) main .blog-posts p.blog-card-date { + font-family: var(--body-font-family); + font-weight: var(--body-font-weight); +} + +:lang(ja) main .blog-posts-container div.blog-posts-decoration p { + font-family: var(--body-font-family); +} diff --git a/express/blocks/blog-posts/blog-posts.js b/express/blocks/blog-posts/blog-posts.js new file mode 100755 index 0000000..dd07d15 --- /dev/null +++ b/express/blocks/blog-posts/blog-posts.js @@ -0,0 +1,348 @@ +/* eslint-disable import/named, import/extensions */ +import { + getLibs, + readBlockConfig, +} from '../../scripts/utils.js'; +import { createOptimizedPicture } from '../../scripts/utils/media.js'; +import { addTempWrapperDeprecated } from '../../scripts/utils/decorate.js'; + +const [{ replaceKey }, { getConfig, createTag, getLocale }] = await Promise.all([import(`${getLibs()}/features/placeholders.js`), import(`${getLibs()}/utils/utils.js`)]); + +const blogPosts = []; +let blogResults; +let blogResultsLoaded; +let blogIndex; + +async function fetchBlogIndex(locales) { + const jointData = []; + const urls = locales.map((l) => `${l}/express/learn/blog/query-index.json`); + + const resp = await Promise.all(urls.map((url) => fetch(url) + .then((res) => res.ok && res.json()))) + .then((res) => res); + resp.forEach((item) => jointData.push(...item.data)); + + const byPath = {}; + jointData.forEach((post) => { + if (post.tags) { + const tags = JSON.parse(post.tags); + tags.push(post.category); + post.tags = JSON.stringify(tags); + } + + byPath[post.path.split('.')[0]] = post; + }); + + return { + data: jointData, + byPath, + }; +} + +function getFeatured(index, urls) { + const paths = urls.map((url) => new URL(url).pathname.split('.')[0]); + const results = []; + paths.forEach((path) => { + const post = index.byPath[path]; + if (post) { + results.push(post); + } + }); + + return results; +} + +function isDuplicate(path) { + return blogPosts.includes(path); +} + +function filterBlogPosts(config, index) { + const result = []; + + if (config.featured) { + if (!Array.isArray(config.featured)) config.featured = [config.featured]; + const featured = getFeatured(index, config.featured); + result.push(...featured); + featured.forEach((post) => { + if (!isDuplicate(post.path)) blogPosts.push(post.path); + }); + } + + if (!config.featuredOnly) { + /* filter posts by tag and author */ + const f = {}; + for (const name of Object.keys(config)) { + const filterNames = ['tags', 'author', 'category']; + if (filterNames.includes(name)) { + const vals = config[name]; + let v = vals; + if (!Array.isArray(vals)) { + v = [vals]; + } + f[name] = v.map((e) => e.toLowerCase().trim()); + } + } + const limit = config['page-size'] || 12; + let numMatched = 0; + /* filter and ignore if already in result */ + const feed = index.data.filter((post) => { + let matchedAll = true; + for (const name of Object.keys(f)) { + let matched = false; + f[name].forEach((val) => { + if (post[name] && post[name].toLowerCase().includes(val)) { + matched = true; + } + }); + if (!matched) { + matchedAll = false; + break; + } + } + if (matchedAll && numMatched < limit) { + if (!isDuplicate(post.path)) { + blogPosts.push(post.path); + } else { + matchedAll = false; + } + } + if (matchedAll) numMatched += 1; + return (matchedAll); + }); + + result.push(...feed); + } + + return result; +} + +// Given a block element, construct a config object from all the links that children of the block. +function getBlogPostsConfig(block) { + let config = {}; + + const rows = [...block.children]; + const firstRow = [...rows[0].children]; + + if (rows.length === 1 && firstRow.length === 1) { + /* handle links */ + const links = [...block.querySelectorAll('a')].map((a) => a.href); + config = { + featured: links, + featuredOnly: true, + }; + } else { + config = readBlockConfig(block); + } + return config; +} + +async function filterAllBlogPostsOnPage() { + if (!blogResultsLoaded) { + let resolve; + blogResultsLoaded = new Promise((r) => { + resolve = r; + }); + const results = []; + const blocks = [...document.querySelectorAll('.blog-posts')]; + + if (!blogIndex) { + const locales = [getConfig().locale.prefix]; + const allBlogLinks = document.querySelectorAll('.blog-posts a'); + allBlogLinks.forEach((l) => { + const blogLocale = getLocale(getConfig().locales, new URL(l).pathname).prefix; + if (!locales.includes(blogLocale)) { + locales.push(blogLocale); + } + }); + + blogIndex = await fetchBlogIndex(locales); + } + + for (let i = 0; i < blocks.length; i += 1) { + const block = blocks[i]; + const config = getBlogPostsConfig(block); + const posts = filterBlogPosts(config, blogIndex); + results.push({ config, posts }); + } + blogResults = results; + resolve(); + } else { + await blogResultsLoaded; + } + return (blogResults); +} + +async function getFilteredResults(config) { + const results = await filterAllBlogPostsOnPage(); + const configStr = JSON.stringify(config); + let matchingResult = {}; + results.forEach((res) => { + if (JSON.stringify(res.config) === configStr) { + matchingResult = res.posts; + } + }); + return (matchingResult); +} + +// Translates the Read More string into the local language +async function getReadMoreString() { + let readMoreString = await replaceKey('read-more', getConfig()); + if (readMoreString === 'read more') { + const locale = getConfig().locale.region; + const readMore = { + us: 'Read More', + uk: 'Read More', + jp: 'もっと見る', + fr: 'En savoir plus', + de: 'Mehr dazu', + }; + readMoreString = readMore[locale] || '   →   '; + } + return readMoreString; +} + +// Given a post, get all the required parameters from it to construct a card or hero card +function getCardParameters(post, dateFormatter) { + const path = post.path.split('.')[0]; + const { title, teaser, image } = post; + const publicationDate = new Date(post.date * 1000); + const dateString = dateFormatter.format(publicationDate); + const filteredTitle = title.replace(/(\s?)(||\|)(\s?Adobe\sExpress\s?)$/g, ''); + const imagePath = image.split('?')[0].split('_')[1]; + return { + path, title, teaser, dateString, filteredTitle, imagePath, + }; +} + +// For configs with a single featuredd post, get a hero sized card +async function getHeroCard(post, dateFormatter) { + const readMoreString = await getReadMoreString(); + const { + path, title, teaser, dateString, filteredTitle, imagePath, + } = getCardParameters(post, dateFormatter); + const heroPicture = createOptimizedPicture(`./media_${imagePath}?format=webply&optimize=medium&width=750`, title, false); + const card = createTag('a', { + class: 'blog-hero-card', + href: path, + }); + const pictureTag = heroPicture.outerHTML; + card.innerHTML = `
+ ${pictureTag} +
+
+

${filteredTitle}

+

${teaser}

+

${dateString}

+

+ ${readMoreString}

+
`; + return card; +} +// For configs with more than one post, get regular cards +function getCard(post, dateFormatter) { + const { + path, title, teaser, dateString, filteredTitle, imagePath, + } = getCardParameters(post, dateFormatter); + const cardPicture = createOptimizedPicture(`./media_${imagePath}?format=webply&optimize=medium&width=750`, title, false, [{ width: '750' }]); + const card = createTag('a', { + class: 'blog-card', + href: path, + }); + const pictureTag = cardPicture.outerHTML; + card.innerHTML = `
+ ${pictureTag} +
+

${filteredTitle}

+

${teaser}

+

${dateString}

`; + return card; +} +// Cached language and dateFormatter since creating a Dateformatter is an expensive operation +let language; +let dateFormatter; + +function getDateFormatter(newLanguage) { + language = newLanguage; + dateFormatter = Intl.DateTimeFormat(language, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + timeZone: 'UTC', + }); +} + +// Given a blog post element and a config, append all posts defined in the config to blogPosts +async function decorateBlogPosts(blogPostsElements, config, offset = 0) { + const posts = await getFilteredResults(config); + // If a blog config has only one featured item, then build the item as a hero card. + const isHero = config.featured && config.featured.length === 1; + + const limit = config['page-size'] || 12; + + let cards = blogPostsElements.querySelector('.blog-cards'); + if (!cards) { + blogPostsElements.innerHTML = ''; + cards = createTag('div', { class: 'blog-cards' }); + blogPostsElements.appendChild(cards); + } + + const pageEnd = offset + limit; + let count = 0; + const images = []; + + const newLanguage = getConfig().locale.ietf; + if (!dateFormatter || newLanguage !== language) { + getDateFormatter(newLanguage); + } + + if (isHero) { + const card = await getHeroCard(posts[0], dateFormatter); + blogPostsElements.prepend(card); + images.push(card.querySelector('img')); + count = 1; + } else { + for (let i = offset; i < posts.length && count < limit; i += 1) { + const post = posts[i]; + const card = getCard(post, dateFormatter); + cards.append(card); + images.push(card.querySelector('img')); + count += 1; + } + } + + if (posts.length > pageEnd && config['load-more']) { + const loadMore = createTag('a', { class: 'load-more button secondary', href: '#' }); + loadMore.innerHTML = config['load-more']; + blogPostsElements.append(loadMore); + loadMore.addEventListener('click', (event) => { + event.preventDefault(); + loadMore.remove(); + decorateBlogPosts(blogPostsElements, config, pageEnd); + }); + } +} + +function checkStructure(element, querySelectors) { + let matched = false; + querySelectors.forEach((querySelector) => { + if (element.querySelector(`:scope > ${querySelector}`)) matched = true; + }); + return matched; +} + +export default async function decorate(block) { + addTempWrapperDeprecated(block, 'blog-posts'); + const config = getBlogPostsConfig(block); + + // wrap p in parent section + if (checkStructure(block.parentNode, ['h2 + p + p + div.blog-posts', 'h2 + p + div.blog-posts', 'h2 + div.blog-posts'])) { + const wrapper = createTag('div', { class: 'blog-posts-decoration' }); + block.parentNode.insertBefore(wrapper, block); + const allP = block.parentNode.querySelectorAll(':scope > p'); + allP.forEach((p) => { + wrapper.appendChild(p); + }); + } + + await decorateBlogPosts(block, config); +} diff --git a/express/blocks/faq/faq.css b/express/blocks/faq/faq.css index f34f916..7edc4de 100644 --- a/express/blocks/faq/faq.css +++ b/express/blocks/faq/faq.css @@ -1,6 +1,9 @@ .section:has(.faq) { background-color: var(--color-gray-200); - padding: 80px 0; +} + +.section:has(.faq) > div:first-child { + padding-top: 80px; } .section:has(.faq) > div { diff --git a/express/blocks/image-list/image-list.css b/express/blocks/image-list/image-list.css index fa6620e..0f787e5 100644 --- a/express/blocks/image-list/image-list.css +++ b/express/blocks/image-list/image-list.css @@ -1,5 +1,4 @@ .image-list { - margin: 25px; display: flex; align-items: center; flex-wrap: wrap; diff --git a/express/blocks/link-list/link-list.css b/express/blocks/link-list/link-list.css index ad49b9f..d32e330 100644 --- a/express/blocks/link-list/link-list.css +++ b/express/blocks/link-list/link-list.css @@ -20,13 +20,6 @@ width: max-content; } -.link-list.shaded { - margin-top: 8px; - margin-bottom: 24px; - background-color: #EDEDED; - border-radius: 100px; -} - .link-list h2, .link-list h3, .link-list h4 { @@ -55,10 +48,6 @@ margin-bottom: 0; } -.link-list.shaded .carousel-container .carousel-platform { - padding: 4px; -} - .link-list.centered .carousel-container .carousel-platform { box-sizing: border-box; } @@ -79,26 +68,6 @@ margin: auto auto 20px auto; } -/* fullwidth variant */ - -.link-list-fullwidth-container > div.link-list-wrapper { - box-sizing: border-box; - width: fit-content; - max-width: 100%; -} - -.link-list-fullwidth-container > div.link-list-wrapper .link-list, -.link-list-fullwidth-container > div.link-list-wrapper .link-list h2, -.link-list-fullwidth-container > div.link-list-wrapper .link-list h3, -.link-list-fullwidth-container > div.link-list-wrapper .link-list h4 { - text-align: center; -} - -.link-list-fullwidth-container > div.link-list-wrapper .link-list .carousel-platform { - margin-left: auto; - margin-right: auto; -} - .hero-animation-wrapper + .link-list-wrapper .link-list { margin-top: 16px; } @@ -126,42 +95,6 @@ padding: 3px 14px; } -.link-list.shaded p.button-container { - margin: 0 2px; -} - -.link-list.shaded p.button-container .button { - margin: 0; - padding: 2px 16px; - background-color: transparent; - color: var(--color-black); - border: none; -} - -.link-list.shaded p.button-container .button:hover, -.link-list.shaded p.button-container.active .button { - background-color: var(--color-white); -} - -/* fullwidth + leftalign variant */ - -.link-list-fullwidth-leftalign-container { - padding-top: 0; -} - -.link-list-fullwidth-leftalign-container > div.link-list-wrapper { - margin-left: 0; -} - -.link-list-fullwidth-leftalign-container ~ .section { - padding-top: 0; -} - -.link-list.large p.button-container .button { - padding: 12px 24px; - border-radius: 24px; -} - @media (min-width: 900px) { .link-list-wrapper { padding-left: 50px; @@ -181,17 +114,6 @@ .link-list .carousel-platform { scroll-padding: 0; } - - .link-list-fullwidth-leftalign-container > div.link-list-wrapper .link-list, - .link-list-fullwidth-leftalign-container > div.link-list-wrapper .link-list h2, - .link-list-fullwidth-leftalign-container > div.link-list-wrapper .link-list h3, - .link-list-fullwidth-leftalign-container > div.link-list-wrapper .link-list h4 { - padding-left: 0; - } - - .link-list-fullwidth-leftalign-container .link-list .carousel-platform p.button-container:first-child { - margin-left: 10px; - } } @media (min-width: 1200px) { diff --git a/express/blocks/link-list/link-list.js b/express/blocks/link-list/link-list.js index 978ac46..72df891 100644 --- a/express/blocks/link-list/link-list.js +++ b/express/blocks/link-list/link-list.js @@ -126,9 +126,7 @@ export default async function decorate(block) { if (links.length) { links.forEach((p) => { const link = p.querySelector('a'); - if (!block.classList.contains('shaded')) { - link.classList.add('secondary'); - } + link.classList.add('secondary'); link.classList.add('medium'); link.classList.remove('accent'); @@ -138,10 +136,6 @@ export default async function decorate(block) { await buildCarousel('p.button-container, a.con-button', block, options); } - if (block.classList.contains('shaded')) { - toggleLinksHighlight(links, variant); - } - window.addEventListener('popstate', () => { toggleLinksHighlight(links, variant); }); diff --git a/express/blocks/quotes/quotes.css b/express/blocks/quotes/quotes.css index c3a24ec..f0f0c16 100644 --- a/express/blocks/quotes/quotes.css +++ b/express/blocks/quotes/quotes.css @@ -88,20 +88,6 @@ line-height: 1.2; } -.quotes-highlight-container { - background: var(--gradient-highlight-vertical); -} - -.quotes-dark-container { - background-color: var(--color-black); -} - -.quotes-inverted-container .quote { - background-color: var(--color-black); - color: var(--color-white); -} - - .quotes h2, .quotes h3, .quotes h4, @@ -109,17 +95,6 @@ color: var(--color-black); } -.quotes-highlight-container h2, -.quotes-highlight-container h3, -.quotes-highlight-container h4, -.quotes-highlight-container h5, -.quotes-dark-container h2, -.quotes-dark-container h3, -.quotes-dark-container h4, -.quotes-dark-container h5 { - color: var(--color-white); -} - @media (min-width: 900px) { .quotes-wrapper, .section .content:has(+ .quotes-wrapper) { max-width: 724px; diff --git a/express/blocks/steps/steps.css b/express/blocks/steps/steps.css index 4aeb8d2..ed2a1a1 100644 --- a/express/blocks/steps/steps.css +++ b/express/blocks/steps/steps.css @@ -10,7 +10,7 @@ margin-right: auto; } -.steps-highlight-container h2, .steps-dark-container h2, .steps-container h2, .steps h2 { +.steps h2 { text-align: center; } diff --git a/express/blocks/template-list/template-list.css b/express/blocks/template-list/template-list.css index edc2642..38293fb 100644 --- a/express/blocks/template-list/template-list.css +++ b/express/blocks/template-list/template-list.css @@ -1,7 +1,11 @@ -.template-list { +.template-list-wrapper { padding-top: 60px; } +.template-list-wrapper { + margin: auto; +} + .template-list-horizontal-fullwidth-container, .template-list-fullwidth-apipowered-container { padding-top: 0; diff --git a/express/scripts/scripts.js b/express/scripts/scripts.js index c6ba41b..8987913 100644 --- a/express/scripts/scripts.js +++ b/express/scripts/scripts.js @@ -94,8 +94,82 @@ removeIrrelevantSections(document); }); }()); +function decorateHeroLCP(loadStyle, config, createTag, getMetadata) { + const template = getMetadata('template'); + const h1 = document.querySelector('main h1'); + if (template !== 'blog') { + if (h1 && !h1.closest('main > div > div')) { + const heroPicture = h1.parentElement.querySelector('picture'); + let heroSection; + const main = document.querySelector('main'); + if (main.children.length === 1) { + heroSection = createTag('div', { id: 'hero' }); + const div = createTag('div'); + heroSection.append(div); + if (heroPicture) { + div.append(heroPicture); + } + div.append(h1); + main.prepend(heroSection); + } else { + heroSection = h1.closest('main > div'); + heroSection.id = 'hero'; + heroSection.removeAttribute('style'); + } + if (heroPicture) { + heroPicture.classList.add('hero-bg'); + } else { + heroSection.classList.add('hero-noimage'); + } + } + } else if (template === 'blog' && h1 && getMetadata('author') && getMetadata('publication-date')) { + loadStyle(`${config.codeRoot}/templates/blog/blog.css`); + document.body.style.visibility = 'hidden'; + const heroSection = createTag('div', { id: 'hero' }); + const main = document.querySelector('main'); + main.prepend(heroSection); + // split sections for template-list + const blocks = document.querySelectorAll('main > div > .template-list'); + blocks.forEach((block) => { + const $section = block.parentNode; + const $elems = [...$section.children]; + + if ($elems.length <= 1) return; + + const $blockSection = createTag('div'); + const $postBlockSection = createTag('div'); + const $nextSection = $section.nextElementSibling; + $section.parentNode.insertBefore($blockSection, $nextSection); + $section.parentNode.insertBefore($postBlockSection, $nextSection); + + let $appendTo; + $elems.forEach(($e) => { + if ($e === block || ($e.className === 'section-metadata')) { + $appendTo = $blockSection; + } + + if ($appendTo) { + $appendTo.appendChild($e); + $appendTo = $postBlockSection; + } + }); + + if (!$postBlockSection.hasChildNodes()) { + $postBlockSection.remove(); + } + }); + } +} + (async function loadPage() { - const { loadArea, setConfig, getMetadata, loadLana, createTag } = await import(`${miloLibs}/utils/utils.js`); + const { + loadArea, + setConfig, + getMetadata, + loadLana, + createTag, + loadStyle, + } = await import(`${miloLibs}/utils/utils.js`); const jarvisVisibleMeta = getMetadata('jarvis-immediately-visible')?.toLowerCase(); const desktopViewport = window.matchMedia('(min-width: 900px)').matches; @@ -128,6 +202,7 @@ removeIrrelevantSections(document); // listenMiloEvents(); buildAutoBlocks(); + decorateHeroLCP(loadStyle, config, createTag, getMetadata); if (urlParams.get('martech') !== 'off' && getMetadata('martech') !== 'off') { import('./instrument.js').then((mod) => { mod.default(); }); } diff --git a/express/styles/styles.css b/express/styles/styles.css index 95a69fe..7d13683 100644 --- a/express/styles/styles.css +++ b/express/styles/styles.css @@ -123,6 +123,10 @@ body > header { background-color: #fff; } +body > main { + text-align: center; +} + .feds-topnav-spacing, body > header:not(.feds-header-wrapper) { height: var(--header-height); @@ -154,8 +158,12 @@ header #feds-header a.feds-navLink:hover .feds-navLink-text { color: var(--color-info-accent-hover); } +a:hover { + text-decoration: none; +} + /* buttons */ -a.button:any-link { +a.button:any-link, a.con-button:any-link { text-decoration: none; border-radius: 18px; padding: 5px 1.2em 6px; @@ -172,24 +180,6 @@ a.button:any-link { overflow: hidden; text-overflow: ellipsis; display: inline-block; -} - -a.con-button:any-link { - text-decoration: none; - border-radius: 999px; - padding: 4px 1em; - text-align: center; - font-size: var(--body-font-size-xs); - font-style: normal; - font-weight: 600; - line-height: var(--body-line-height); - cursor: pointer; - transition: background-color 0.3s, color 0.3s, border 0.3s; - border-width: 2px; - border-style: solid; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; min-height: initial; } @@ -231,7 +221,7 @@ a.con-button.xxl-button:any-link, line-height: var(--body-line-height); } -a.button.small:any-link, a.con-button.small:any-link { +a.button.small:any-link, a.con-button.small:any-link, .small a.con-button:any-link { padding: 4px 1em; border-radius: 15px; font-size: var(--body-font-size-xs); @@ -557,7 +547,7 @@ a:any-link { padding-top: 0.1px; } -.section:last-of-type > .content { +.section:last-of-type>.content:not(:has(+ .template-list-wrapper)) { padding-bottom: 40px; } @@ -601,10 +591,6 @@ a:any-link { .section p.button-container { text-align: unset; } - - .section.cards-container { - background-color: unset; - } } @media (min-width: 1200px) { @@ -624,55 +610,55 @@ a:any-link { } /* hero */ -.section.hero { +.section#hero { color: var(--color-white); position: relative; padding: 120px 15px; } -.hero.hero-noimage { +#hero.hero-noimage { color: var(--body-color); padding-top: 64px; padding-bottom: 0; } -.hero.hero-noimage + .section { +#hero.hero-noimage + .section { padding-top: 0; } -.hero.hero-noimage > div { +#hero.hero-noimage > div { max-width: 1024px; } -.hero.hero-noimage a.button:any-link { +#hero.hero-noimage a.button:any-link { color: white; } -.hero.hero-noimage a:any-link { +#hero.hero-noimage a:any-link { color: black; } -.hero h1 { +#hero h1 { font-size: var(--heading-font-size-xl); line-height: var(--heading-line-height); text-align: center; margin: 0; } -.hero h2 { +#hero h2 { font-size: var(--body-font-size-xl); font-weight: var(--body-font-weight); margin: 32px; } -.hero h5 { +#hero h5 { font-size: var(--body-font-size-l); font-weight: var(--body-font-weight); margin: auto; margin-top: 32px; } -.hero.hero-noimage p { +#hero.hero-noimage p { font-size: var(--body-font-size-s); } @@ -682,7 +668,7 @@ a:any-link { color: currentColor; } -.hero .icon { +#hero .icon { height: 2em; width: 2em; } @@ -697,7 +683,7 @@ a:any-link { fill: #1877f2; } -.hero .hero-bg { +#hero .hero-bg { position: absolute; top: 0; bottom: 0; @@ -707,22 +693,22 @@ a:any-link { z-index: -1; } -.hero .hero-bg img { +#hero .hero-bg img { height: 100%; width: 100%; object-fit: cover; } -.hero > div { +#hero > div { padding: 0; margin: auto; } -.hero a:any-link { +#hero a:any-link { color: currentColor; } -.hero a.button:any-link { +#hero a.button:any-link { text-shadow: none; padding: 10px 1.5em; border-radius: 22px; @@ -734,7 +720,7 @@ a:any-link { } @media (min-width: 600px) { - .hero h5 { + #hero h5 { font-size: 22px; font-weight: 400; max-width: 672px; @@ -742,8 +728,8 @@ a:any-link { margin-top: 32px; } - .hero h2, - .hero p { + #hero h2, + #hero p { font-size: var(--body-font-size-xl); line-height: var(--body-line-height); text-align: center; @@ -752,25 +738,25 @@ a:any-link { } @media (min-width: 900px) { - .hero { + #hero { padding-left: 50px; padding-right: 50px; } - .hero h2, - .hero p { + #hero h2, + #hero p { font-size: var(--body-font-size-xxl); margin: 24px 50px 0; } } @media (min-width: 1200px) { - .hero h1 { + #hero h1 { font-size: var(--heading-font-size-xxl); margin: 0; } - .hero .columns > div > div { + #hero .columns > div > div { margin: 8px; } } @@ -786,7 +772,7 @@ a:any-link { margin-bottom: 0; } -.section .hero { +.section #hero { color: initial; } @@ -897,8 +883,8 @@ body[data-device="mobile"] main .floating-button-wrapper[data-audience="mobile"] display: block; } -.section:not(.banner-container) > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-as-floating-button-CTA, -.section:not(.banner-container) > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-as-floating-button-CTA { +.section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-as-floating-button-CTA, +.section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-as-floating-button-CTA { display: none; } @@ -925,8 +911,8 @@ body[data-device="mobile"] main .floating-button-wrapper[data-audience="mobile"] text-align: left; } - .section:not(.banner-container) > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-as-floating-button-CTA, - .section:not(.banner-container) > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-as-floating-button-CTA { + .section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.button.same-as-floating-button-CTA, + .section > div:not(.pricing-summary, .pricing-cards, .pricing-table, .puf, .split-action, .link-list, .wayfinder, .ratings) a.con-button.same-as-floating-button-CTA { display: inline-block; } } @@ -974,22 +960,12 @@ body[data-device="mobile"] main .floating-button-wrapper[data-audience="mobile"] max-width: 1200px; } - .columns-fullsize-container > div, - .columns-fullsize-center-container > div, - .template-list-sixcols-container > div { - max-width: 1122px; - } - .free-plan-bullet .free-plan-bullet-container, .free-plan-widget-placeholder { max-width: 400px; } } -.template-list-sixcols-container > div { - padding: 0; -} - @-webkit-keyframes freePlanHighlight { 0% { transform: translateX(0%); @@ -1071,40 +1047,40 @@ wbr.wbr-off { } /* Japanese font sizing styles */ -:lang(ja) h1.heading-long { +:lang(ja) h1.heading-long, :lang(ja) #hero h1.heading-long { font-size: var(--heading-font-size-xl); } -:lang(ja) h1.heading-very-long { +:lang(ja) h1.heading-very-long, :lang(ja) #hero h1.heading-very-long { font-size: var(--heading-font-size-l); } -:lang(ja) h1.heading-x-long { +:lang(ja) h1.heading-x-long, :lang(ja) #hero h1.heading-x-long { font-size: var(--heading-font-size-l); } -:lang(ja) h2.heading-long { +:lang(ja) h2.heading-long, :lang(ja) #hero h2.heading-long { font-size: var(--heading-font-size-l); } -:lang(ja) h2.heading-very-long { +:lang(ja) h2.heading-very-long, :lang(ja) #hero h2.heading-very-long { font-size: var(--heading-font-size-l); } -:lang(ja) h2.heading-x-long { +:lang(ja) h2.heading-x-long, :lang(ja) #hero h2.heading-x-long { font-size: var(--heading-font-size-m); } @media (max-width: 600px) { - :lang(ja) h2.heading-long { + :lang(ja) h2.heading-long, :lang(ja) #hero h2.heading-long { font-size: var(--heading-font-size-l); } - :lang(ja) h2.heading-very-long { + :lang(ja) h2.heading-very-long, :lang(ja) #hero h2.heading-very-long { font-size: var(--heading-font-size-m); } - :lang(ja) h2.heading-x-long { + :lang(ja) h2.heading-x-long, :lang(ja) #hero h2.heading-x-long { font-size: var(--heading-font-size-m); } } diff --git a/express/templates/blog/blog.css b/express/templates/blog/blog.css new file mode 100644 index 0000000..6686ed4 --- /dev/null +++ b/express/templates/blog/blog.css @@ -0,0 +1,249 @@ +/* blog styling */ + +.blog #hero { + margin: 0; + padding: 0; + } + +.blog main div.template-list-wrapper + div { + padding-top: 60px; +} + +.blog main .section:has(.image-list) > div { + margin: auto; + max-width: 1200px; + padding: 0; +} + +.blog main div>h3:first-child { + margin-top: 0px; +} + +.blog .section:not(:has(.faq), :has(.blog-posts)) > div:first-child:not(.banner) { + padding-top: 60px; +} + +.blog .section > .content p:has(.con-button) { + margin-bottom: 0; +} + +.blog .section h1, .blog .section h2, .blog .section h3, .blog .section h4, .blog .section h5 { + margin: 0; +} + +.blog .section > div > h2 { + margin: 80px 0 0; +} + + + +.blog .section > .content { + margin: auto; + max-width: 375px; +} + + .blog .section > div > p { + font-family: var(--body-serif-font-family); + } + + .blog .section > div > p .button, .blog .section > div > p .con-button { + font-family: var(--body-font-family); + } + + .blog #hero > div { + margin: auto; + max-width: 1000px; + width: 100%; + } + + .blog #hero { + color: black; + } + + .blog #hero .blog-header .eyebrow { + margin: 0; + margin-bottom: 12px; + font-weight: 700; + color: #696969; + line-height: 20px; + font-size: var(--body-font-size-s); + text-transform: capitalize; + } + + .blog #hero .blog-header .author { + margin-top: 24px; + display: flex; + align-items: center; + font-size: var(--body-font-size-s); + } + + .blog #hero .blog-header .author div.author-social { + flex-grow: 1; + text-align: right; + } + + .blog #hero .blog-header .author div.author-social svg { + margin: 0 0 6px 6px; + height: 18px; + width: 18px; + } + + .blog #hero .blog-header img { + width: 64px; + height: 64px; + object-fit: cover; + border-radius: 50%; + margin-right: 20px; + } + + .blog #hero .blog-header h1 { + text-align: left; + font-size: var(--heading-font-size-l); + } + + .blog #hero .blog-header { + font-family: var(--body-serif-font-family); + } + + .blog #hero .blog-header h1 { + font-family: var(--body-font-family); + } + + .blog #hero .blog-header p.subheading { + text-align: left; + font-size: var(--body-font-size-l); + margin-left: 0; + } + + .blog #hero .blog-header { + text-align: left; + margin: 80px 32px 32px; + background-image: url(/express/icons/cc-express-logo.svg); + padding-top: 58px; + background-repeat: no-repeat; + background-size: min(311px, 100%); + } + + .blog div.section { + /*text-align: left;*/ + margin: 0; + } + + .blog .content p.action-area { + text-align: center; + } + + .blog-article div.section > div > p:not(.button-container, .action-area), .blog-article div.section > div > h2, + .blog-article div.section > div > h3, .blog-article div.section > div > h4, + .blog-article div.section > div > h5, .blog-article div.section > div > ul, + .blog-article div.section > div > ol, .blog-article div.section > div > .ax-table-of-contents { + max-width: 530px; + margin-left: auto; + margin-right: auto; + text-align: left; + } + + + + + .blog div.section.fullwidth > div { + margin: auto; + max-width: 1200px; + padding: 0; + } + + + .blog div.section h2, .blog div.section h3, + .blog div.section h2 + p:not(.button-container), .blog div.section h3 + p:not(.button-container) { + text-align: left; + } + + .blog .section h3 { + font-size: 36px; + line-height: 40px; + margin-top: 64px; + text-align: left; + } + + .blog .section h4 { + text-align: left; + } + + .blog .hero-animation p { + font-family: var(--body-serif-font-family); + } + + .blog .banner { + margin-top: 64px; + } + + .blog .banner > div > div > h2 { + text-align: center; + } + + .blog svg.copy-success { + fill: var(--color-info-accent); + } + + .blog svg.copy-failure { + fill: var(--color-gray-400); + } + + .blog div.sticky-promo-bar { + text-align: center; + } + +@media (min-width: 600px) { + .blog #hero .blog-header p.subheading { + font-size: var(--body-font-size-xl); + } + + .blog #hero .blog-header h1 { + font-size: var(--heading-font-size-xl); + } + + .blog #hero .blog-header { + padding-top: 66px; + } + + .blog #hero .blog-header .eyebrow { + margin-bottom: 24px; + } + .blog #hero .blog-header .author { + margin-top: 32px; + } +} + +@media (min-width: 900px) { + .blog div.section > div > p.intro-text { + max-width: unset; + margin-top: 0; + } + + .blog .section > .content { + max-width: 830px; + } +} + +@media (min-width:1200px) { + .blog .section > .content { + max-width: 1024px; + } +} + +/* Japanese font styles */ +:lang(ja) .blog .section h3 { + font-size: var(--heading-font-size-m); +} + +:lang(ja) .blog .hero-animation p { + font-family: var(--body-font-family); +} + +:lang(ja) .blog .section > div > p { + font-family: var(--body-font-family); +} + +:lang(ja) .blog #hero .blog-header { + font-family: var(--body-font-family); +} diff --git a/express/templates/blog/blog.js b/express/templates/blog/blog.js new file mode 100644 index 0000000..acc6694 --- /dev/null +++ b/express/templates/blog/blog.js @@ -0,0 +1,164 @@ +/* eslint-disable import/named, import/extensions */ +import { getLibs, toClassName } from '../../scripts/utils.js'; +import { createOptimizedPicture } from '../../scripts/utils/media.js'; +import { decorateButtonsDeprecated } from '../../scripts/utils/decorate.js'; + +const { createTag, getConfig, getMetadata } = await import(`${getLibs()}/utils/utils.js`); + +async function fetchAuthorImage($image, author) { + const resp = await fetch(`/express/learn/blog/authors/${toClassName(author)}.plain.html`); + const main = await resp.text(); + if (resp.status === 200) { + const $div = createTag('div'); + $div.innerHTML = main; + const $img = $div.querySelector('img'); + const newPicture = createOptimizedPicture($img.src, $img.alt, false, [{ width: '200' }]); + $image.parentElement.replaceChild(newPicture, $image); + } +} + +function decorateBlogLinkedImages() { + document.querySelectorAll('main div.section > div > p > a').forEach((a) => { + if (a.textContent.trim().startsWith('https://')) { + const prevSib = a.parentElement.previousElementSibling; + if (prevSib) { + const picture = prevSib.lastElementChild; + if (picture && (picture.tagName === 'PICTURE')) { + prevSib.appendChild(a); + a.innerHTML = ''; + a.className = ''; + a.appendChild(picture); + } + } + } + }); +} + +function copyToClipboard(copyButton) { + navigator.clipboard.writeText(window.location.href).then(() => { + copyButton.classList.add('copy-success'); + }, () => { + copyButton.classList.add('copy-failure'); + }); +} + +const loadImage = (img) => new Promise((resolve) => { + if (img.complete && img.naturalHeight !== 0) resolve(); + else { + img.onload = () => { + resolve(); + }; + } +}); + +export default async function decorateBlogPage() { + const $main = document.querySelector('main'); + const $h1 = document.querySelector('main h1'); + const author = getMetadata('author'); + const date = getMetadata('publication-date'); + if ($h1 && author && date) { + const $heroPicture = $h1.parentElement.querySelector('picture'); + const heroSection = document.querySelector('#hero'); + const $div = createTag('div'); + heroSection.append($div); + $div.append($h1); + $main.prepend(heroSection); + + document.body.classList.add('blog-article'); + const $blogHeader = createTag('div', { class: 'blog-header' }); + $div.append($blogHeader); + const $eyebrow = createTag('div', { class: 'eyebrow' }); + const { prefix } = getConfig().locale; + $eyebrow.innerHTML = `${getMetadata('category')}`; + $blogHeader.append($eyebrow); + $blogHeader.append($h1); + const publicationDate = new Date(date); + const language = getConfig().locale.ietf; + const dateString = publicationDate.toLocaleDateString(language, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + timeZone: 'UTC', + }); + + const subheading = getMetadata('subheading'); + if (subheading) { + const $subheading = createTag('p', { class: 'subheading' }); + $subheading.innerHTML = subheading; + $blogHeader.append($subheading); + } + if (author) { + const $author = createTag('div', { class: 'author' }); + const url = encodeURIComponent(window.location.href); + $author.innerHTML = `
+
+
${author}
+
${dateString}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
`; + fetchAuthorImage($author.querySelector('img'), author); + $blogHeader.append($author); + const copyButton = document.getElementById('copy-to-clipboard'); + copyButton.addEventListener('click', () => { + copyToClipboard(copyButton); + }); + } + $div.append($blogHeader); + if ($heroPicture) { + $div.append($heroPicture); + } + decorateBlogLinkedImages(); + if ($heroPicture) { + const img = $heroPicture.querySelector('img'); + await loadImage(img).then(() => { + document.body.style.visibility = 'visible'; + }); + } else { + document.body.style.visibility = 'visible'; + } + } else { + document.body.style.visibility = 'visible'; + } + + const pictures = document.querySelectorAll('main div.section > div > picture'); + pictures.forEach((picture) => { + const section = picture.closest('.section'); + section.classList.add('fullwidth'); + }); + + const content = $main.querySelectorAll(':scope > .section > .content'); + content.forEach((c) => { + decorateButtonsDeprecated(c, 'medium'); + }); +} + +await decorateBlogPage();