Skip to content

Commit

Permalink
Improve LCP on article pages.
Browse files Browse the repository at this point in the history
  • Loading branch information
bstopp committed Apr 17, 2024
1 parent d7c97a5 commit 4b140b8
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 166 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
},
rules: {
// allow reassigning param
'max-len': ['error', { code: 220 }],
'no-param-reassign': [2, { props: false }],
'linebreak-style': ['error', 'unix'],
'import/extensions': ['error', {
Expand Down
1 change: 1 addition & 0 deletions cigaradvisor/blocks/article-ldjson/article-ldjson.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* intentionally empty */
62 changes: 62 additions & 0 deletions cigaradvisor/blocks/article-ldjson/article-ldjson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { fetchAuthorInfo, fetchCategoryInfo, fetchPostsInfo } from '../../scripts/scripts.js';
import { getMetadata } from '../../scripts/aem.js';
import { addLdJsonScript } from '../../scripts/linking-data.js';

export default async function decorate(block) {
block.parentElement.remove();

const promises = [];
promises.push(fetchPostsInfo(window.location.pathname));
promises.push(fetchAuthorInfo(getMetadata('author')));
promises.push(fetchCategoryInfo(getMetadata('category')));

let article;
let author;
let category;

await Promise.all(promises).then((results) => {
[[article], author, category] = results;
});
if (!article || !author || !category) {
return;
}

const ldjson = {
'@context': 'https://schema.org/',
'@type': 'BlogPosting',
mainEntityOfPage: {
'@type': 'WebPage',
'@id': window.location.href,
},
url: window.location.href,
headline: article.heading,
datePublished: new Date(article.published * 1000).toISOString(),
dateModified: new Date(article.lastModified * 1000).toISOString(),
publisher: {
'@type': 'Organization',
'@id': 'https://www.famous-smoke.com/cigaradvisor#organization',
name: 'Cigar Advisor',
logo: {
'@type': 'ImageObject',
url: 'https://www.famous-smoke.com/cigaradvisor/styles/images/CALogo_512x512.png',
},
},
image: {
'@type': 'ImageObject',
url: 'https://www.famous-smoke.com/cigaradvisor/wp-content/uploads/2019/10/cigar-advisor-asylum-essential-review-guide-cover.jpeg',
},
articleSection: category.heading,
description: article.description,
author: {
'@type': 'Person',
name: author.name,
url: `https://www.famous-smoke.com${author.path}`,
description: author.intro,
image: {
'@type': 'ImageObject',
url: author.image,
},
},
};
addLdJsonScript(document.querySelector('head'), ldjson);
}
4 changes: 2 additions & 2 deletions cigaradvisor/blocks/article-list/article-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { generatePagination, getCategory } from '../../scripts/util.js';
*
* @param {HTMLElement} wrapper - The wrapper element to render the page into.
* @param {Array} articles - The array of articles to render.
* @param {number} limit - The limit of articles per page.
* @param {Number|String} limit - The limit of articles per page.
* @param {number} articlesCount - The total count of articles. This is passed for pagination
* when full list of articles are not passed.
* @returns {Promise<void>} - A promise that resolves when the page is rendered.
*/
export async function renderPage(wrapper, articles, limit, articlesCount) {
export async function renderPage(wrapper, articles, limit = 10, articlesCount = undefined) {
let pageSize = 10;
if (!articles || articles.length === 0) {
return;
Expand Down
2 changes: 2 additions & 0 deletions cigaradvisor/blocks/article-teaser/article-teaser.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function buildArticleTeaser(parentElement, article) {
decorateSeoPicture(picture, article.path.substring(article.path.lastIndexOf('/') + 1));

const category = (article.category && article.category.heading) ? article.category.heading : '';
/* eslint-disable max-len */
parentElement.innerHTML += `
<article class="article article-thumbnail">
<a class="article-category ${category.toLowerCase().replaceAll(/\s+/g, '-')}" href="${article.category ? article.category.path : ''}" data-category="${category}" title="${category}">${category}</a>
Expand All @@ -59,6 +60,7 @@ export function buildArticleTeaser(parentElement, article) {
<script type="application/ld+json">${JSON.stringify(ldjson)}</script>
</article>
`;
/* eslint-disable max-len */
}

export default async function decorate(block) {
Expand Down
50 changes: 21 additions & 29 deletions cigaradvisor/blocks/articleheader/articleheader.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import { fetchCategoryInfo, fetchAuthorInfo, decorateSeoPicture } from '../../scripts/scripts.js';
import {
a, div, h1, section,
} from '../../scripts/dom-helpers.js';
import { fetchCategoryInfo, fetchAuthorInfo } from '../../scripts/scripts.js';
import { readBlockConfig } from '../../scripts/aem.js';

export default async function decorate(block) {
const section = document.createElement('section');
const imageWrapper = document.createElement('div');
imageWrapper.classList.add('image-wrapper');
const config = readBlockConfig(block);

const picture = block.querySelector('picture');
decorateSeoPicture(picture, window.location.pathname.substring(window.location.pathname.lastIndexOf('/') + 1));
imageWrapper.append(picture);
const articleInfo = document.createElement('div');
articleInfo.classList.add('article-info');
const categoryLink = block.querySelector('p.category').innerText;
const category = await fetchCategoryInfo(categoryLink);
if (category) {
const categoryLinkEl = document.createElement('div');
categoryLinkEl.classList.add('article-category');
categoryLinkEl.innerHTML = `<a href="${categoryLink}">${category.heading}</a>`;
articleInfo.append(categoryLinkEl);
}
articleInfo.append(block.querySelector('h1'));
const authorLink = block.querySelector('p.author').innerText;
const author = await fetchAuthorInfo(authorLink);
const authorLinkEl = document.createElement('div');
authorLinkEl.classList.add('article-author');
if (author) {
authorLinkEl.innerHTML = `<a href="${authorLink}">By ${author.name}</a>`;
articleInfo.append(authorLinkEl);
}
articleInfo.append(authorLinkEl);
section.append(imageWrapper);
section.append(articleInfo);
block.replaceChildren(section);

const category = await fetchCategoryInfo(config.category);
const author = await fetchAuthorInfo(config.author);
const sect = section(
{},
div({ class: 'image-wrapper' }, picture),
div(
{ class: 'article-info' },
div({ class: 'article-category' }, a({ href: config.category }, category.heading)),
h1(config.heading),
div({ class: 'article-author' }, a({ href: config.author }, author.name)),
),
);
block.replaceChildren(sect);
}
10 changes: 10 additions & 0 deletions cigaradvisor/blocks/dynamic-article-list/dynamic-article-list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@import url('../article-list/article-list.css');
@import url('../article-teaser/article-teaser.css');

.dynamic-article-list h3 {
font-family: var(--ff-opensans);
font-weight: var(--font-weight-bold);
font-size: var(--heading-font-size-xxs);
line-height: var(--line-height-m);
margin: 2rem 0 1rem;
}
41 changes: 41 additions & 0 deletions cigaradvisor/blocks/dynamic-article-list/dynamic-article-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { loadPosts } from '../../scripts/scripts.js';
import { readBlockConfig } from '../../scripts/aem.js';
import { h3 } from '../../scripts/dom-helpers.js';
import { renderPage } from '../article-list/article-list.js';

async function loadMustReadPosts() {
const resp = await fetch('/cigaradvisor/must-reads.plain.html');
if (resp.ok) {
const dom = document.createElement('main');
dom.innerHTML = await resp.text();
const mustReadArticleList = dom.querySelectorAll('.article-list li a');
return Array.from(mustReadArticleList).map((a) => a.getAttribute('href'));
}
return [];
}

export default async function decorate(block) {
const config = readBlockConfig(block);
// Build Article's Custom List Article Block
const { author, category } = config;

const posts = await loadPosts();
const articles = [];

// 3 posts from the same author and category “most recent by published date”
articles.push(...posts.filter((post) => author.includes(post.author) && post.path !== window.location.pathname).slice(0, 2));
articles.push(...posts.filter((post) => category.includes(post.category) && !articles.includes(post) && post.path !== window.location.pathname).slice(0, 3 - articles.length));

// 1 random post from must-reads page
const mustReadPosts = await loadMustReadPosts();
const mustReadCandidatePosts = posts.filter((post) => mustReadPosts.includes(post.path) && !articles.includes(post) && post.path !== window.location.pathname);
const randomIndex = Math.floor(Math.random() * mustReadCandidatePosts.length);
articles.push(...mustReadCandidatePosts.slice(randomIndex, randomIndex + 1));

const wrapper = document.createElement('div');
wrapper.classList.add('article-teaser-wrapper');

await renderPage(wrapper, articles);
block.replaceChildren(h3('You Might Also Like...'), wrapper);
block.classList.add('article-list');
}
1 change: 1 addition & 0 deletions cigaradvisor/scripts/dom-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ export function strong(...items) { return domEl('strong', ...items); }
export function select(...items) { return domEl('select', ...items); }
export function option(...items) { return domEl('option', ...items); }
export function hr(...items) { return domEl('hr', ...items); }
export function section(...items) { return domEl('section', ...items); }
48 changes: 26 additions & 22 deletions cigaradvisor/scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {
waitForLCP,
loadBlocks,
loadCSS,
getMetadata, decorateBlock, loadBlock, createOptimizedPicture,
getMetadata,
createOptimizedPicture,
} from './aem.js';
import { a, div, span } from './dom-helpers.js'
import { a, div, span } from './dom-helpers.js';
import addLinkingData from './linking-data.js';

const LCP_BLOCKS = ['hero', 'articleheader'];
const LCP_BLOCKS = ['hero'];
const AUTHOR_INDEX_PATH = '/cigaradvisor/index/author-index.json';
const CATEGORY_INDEX_PATH = '/cigaradvisor/index/category-index.json';
const ARTICLE_INDEX_PATH = '/cigaradvisor/index/article-index.json';
Expand Down Expand Up @@ -64,11 +65,11 @@ function buildHeroBlock(main) {
*/
async function decorateTemplate(main) {
// Nothing to process
const template = getMetadata('template');
let template = getMetadata('template');
if (!template) {
return;
}

template = template.toLowerCase();
// Protect against recursion from fragment block
if (!main.closest(`.${template}`)) {
return;
Expand Down Expand Up @@ -207,18 +208,21 @@ async function loadFonts() {
function addReturnToTop(main) {
const picture = createOptimizedPicture('/cigaradvisor/icons/return-to-top.webp');

const rtt = a({ id: 'return-to-top', class: 'hidden' },
const rtt = a(
{ id: 'return-to-top', class: 'hidden' },
picture,
div({ class: 'icon-container' },
span({ class: 'icon icon-angle-up', alt: 'Return to the top of the page.' })),
div(
{ class: 'icon-container' },
span({ class: 'icon icon-angle-up', alt: 'Return to the top of the page.' }),
),
);
decorateIcons(rtt);
main.append(rtt);
rtt.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
window.scrollTo({ top: 0, behavior: 'smooth' });
})
});

const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
Expand Down Expand Up @@ -283,10 +287,10 @@ export function isInternal(path) {
*/
export function decorateExternalLink(element) {
const anchors = element.querySelectorAll('a');
anchors.forEach((a) => {
if (!isInternal(a.getAttribute('href'))) {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener');
anchors.forEach((link) => {
if (!isInternal(link.getAttribute('href'))) {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
}
});
}
Expand Down Expand Up @@ -322,8 +326,8 @@ export async function loadPosts(path = ARTICLE_INDEX_PATH, recurse = false) {
if (resp.ok) {
jsonData = await resp.json();
}
jsonData.data.forEach((a) => {
articleIndexData.push({ ...a });
jsonData.data.forEach((article) => {
articleIndexData.push({ ...article });
});
// If there are more articles to load, load them
if ((jsonData.total - jsonData.offset) > jsonData.limit) {
Expand All @@ -335,8 +339,8 @@ export async function loadPosts(path = ARTICLE_INDEX_PATH, recurse = false) {
// Protected against callers modifying the objects
const ret = [];
if (articleIndexData) {
articleIndexData.forEach((a) => {
ret.push({ ...a });
articleIndexData.forEach((article) => {
ret.push({ ...article });
});
}
return ret;
Expand Down Expand Up @@ -391,9 +395,9 @@ export async function getAllAuthors(sort = false) {
}
}
if (sort) {
authorIndexData.sort((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
authorIndexData.sort((l, r) => {
const nameA = l.name.toUpperCase();
const nameB = r.name.toUpperCase();
// eslint-disable-next-line no-nested-ternary
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
});
Expand All @@ -402,8 +406,8 @@ export async function getAllAuthors(sort = false) {
// Protected against callers modifying the objects
const ret = [];
if (authorIndexData) {
authorIndexData.forEach((a) => {
ret.push({ ...a });
authorIndexData.forEach((article) => {
ret.push({ ...article });
});
}
return ret;
Expand Down
Loading

0 comments on commit 4b140b8

Please sign in to comment.