From a3749e2d53c48b2f1127e194c53d5ec4e55abff6 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Mon, 10 Nov 2025 11:45:55 -0800 Subject: [PATCH 1/2] feat: add blog-header block with auto-blocking - Auto-blocks blog metadata (date, author, tags) from page head - Inserts blog-header block after H1 automatically - Displays date with RSS icon, author with avatar, and tag pills - Responsive design for desktop and mobile - No manual authoring required for blog posts --- blocks/blog-header/blog-header.css | 131 +++++++++++++++++++++++++++++ blocks/blog-header/blog-header.js | 93 ++++++++++++++++++++ scripts/scripts.js | 24 ++++++ 3 files changed, 248 insertions(+) create mode 100644 blocks/blog-header/blog-header.css create mode 100644 blocks/blog-header/blog-header.js diff --git a/blocks/blog-header/blog-header.css b/blocks/blog-header/blog-header.css new file mode 100644 index 0000000..566b872 --- /dev/null +++ b/blocks/blog-header/blog-header.css @@ -0,0 +1,131 @@ +.blog-header { + max-width: 1200px; + margin: 0 auto; + padding: 40px 24px; +} + +.blog-header-container h1 { + font-size: 48px; + line-height: 1.2; + margin: 0 0 24px; + font-weight: 700; + color: var(--text-color); +} + +.blog-header-meta { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 24px; +} + +.blog-header-date { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + color: #666; + font-size: 16px; +} + +.blog-header-date a { + display: inline-flex; + align-items: center; + color: #666; + text-decoration: none; +} + +.blog-header-date a:hover { + color: var(--link-color); +} + +.blog-header-date svg { + width: 16px; + height: 16px; +} + +.blog-header-author { + list-style: none; + margin: 0; + padding: 0; +} + +.blog-header-author li { + display: inline-block; +} + +.blog-header-author a { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + color: var(--text-color); +} + +.blog-header-author a:hover { + color: var(--link-color); +} + +.blog-header-author img { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; +} + +.blog-header-tags { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.blog-header-tags li { + display: inline-block; +} + +.blog-header-tags a { + display: inline-block; + padding: 8px 16px; + border: 1px solid #dadada; + border-radius: 4px; + text-decoration: none; + color: var(--text-color); + font-size: 14px; + transition: border-color 0.2s, color 0.2s; +} + +.blog-header-tags a:hover { + border-color: var(--link-color); + color: var(--link-color); +} + +/* Desktop styles */ +@media (width >= 900px) { + .blog-header { + padding: 60px 32px; + } + + .blog-header-container h1 { + font-size: 64px; + margin-bottom: 32px; + } + + .blog-header-meta { + flex-direction: row; + align-items: center; + gap: 0; + } + + .blog-header-date { + margin-right: auto; + } + + .blog-header-author img { + width: 40px; + height: 40px; + } +} + diff --git a/blocks/blog-header/blog-header.js b/blocks/blog-header/blog-header.js new file mode 100644 index 0000000..64d8d09 --- /dev/null +++ b/blocks/blog-header/blog-header.js @@ -0,0 +1,93 @@ +import { getMetadata } from '../../scripts/aem.js'; + +export default function decorate(block) { + // Extract date, author, tags from the auto-blocked structure + const [dateCell, authorCell, tagsCell] = [...block.children[0].children]; + const date = dateCell.textContent.trim(); + const author = authorCell.textContent.trim(); + const tags = tagsCell.textContent.trim(); + + // Create new structure + const container = document.createElement('div'); + container.className = 'blog-header-container'; + + // Meta section (date and author) + const meta = document.createElement('div'); + meta.className = 'blog-header-meta'; + + // Date + if (date) { + const dateWrapper = document.createElement('p'); + dateWrapper.className = 'blog-header-date'; + + const time = document.createElement('time'); + time.textContent = date; + dateWrapper.appendChild(time); + + // Add RSS icon + const rssLink = document.createElement('a'); + rssLink.href = '/feed'; + rssLink.setAttribute('aria-label', 'Atom Feed'); + rssLink.innerHTML = ''; + dateWrapper.appendChild(rssLink); + + meta.appendChild(dateWrapper); + } + + // Author + if (author) { + const authorWrapper = document.createElement('ul'); + authorWrapper.className = 'blog-header-author'; + + const li = document.createElement('li'); + const authorLink = document.createElement('a'); + + // Convert author name to GitHub username format (lowercase, no spaces) + const githubUsername = author.toLowerCase().replace(/\s+/g, ''); + authorLink.href = `https://github.com/${githubUsername}`; + + // Add author image from GitHub + const img = document.createElement('img'); + img.src = `https://avatars.githubusercontent.com/u/2760139?v=4`; // This could be enhanced to lookup actual user + img.alt = author; + img.width = 40; + img.height = 40; + img.loading = 'lazy'; + authorLink.appendChild(img); + + // Add author name + const nameSpan = document.createElement('span'); + nameSpan.textContent = author; + authorLink.appendChild(nameSpan); + + li.appendChild(authorLink); + authorWrapper.appendChild(li); + meta.appendChild(authorWrapper); + } + + container.appendChild(meta); + + // Tags + if (tags) { + const tagsWrapper = document.createElement('ul'); + tagsWrapper.className = 'blog-header-tags'; + + // Split by comma in case there are multiple tags + const tagList = tags.split(',').map((t) => t.trim()).filter((t) => t); + tagList.forEach((tag) => { + const li = document.createElement('li'); + const tagLink = document.createElement('a'); + tagLink.href = `/blog?tag=${tag.toLowerCase()}`; + tagLink.textContent = tag; + li.appendChild(tagLink); + tagsWrapper.appendChild(li); + }); + + container.appendChild(tagsWrapper); + } + + // Replace block content + block.textContent = ''; + block.appendChild(container); +} + diff --git a/scripts/scripts.js b/scripts/scripts.js index 5bd20cf..cc79cb7 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -55,12 +55,36 @@ function autolinkModals(doc) { }); } +/** + * Builds blog header block and inserts after H1. + * @param {Element} main The container element + */ +function buildBlogHeaderBlock(main) { + const date = getMetadata('date'); + const author = getMetadata('author'); + const tags = getMetadata('article:tag'); + + // Only build blog header if we have blog metadata + if (!date && !author && !tags) return; + + const h1 = main.querySelector('h1'); + if (!h1) return; + + // Build the blog header block structure + const cells = [date || '', author || '', tags || '']; + const blogHeader = buildBlock('blog-header', [cells]); + + // Insert right after H1 + h1.parentElement.insertBefore(blogHeader, h1.nextSibling); +} + /** * Builds all synthetic blocks in a container element. * @param {Element} main The container element */ function buildAutoBlocks(main) { try { + buildBlogHeaderBlock(main); if (!main.querySelector('.hero')) buildHeroBlock(main); } catch (error) { // eslint-disable-next-line no-console From aab228809103e3d03cf5b31b4988223c6dc07cdb Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Mon, 10 Nov 2025 11:49:19 -0800 Subject: [PATCH 2/2] fix: resolve linting errors - Remove unused getMetadata import - Fix CSS selector specificity ordering - Fix trailing spaces - Change hex color from #ffffff to #fff --- blocks/blog-header/blog-header.css | 46 +++++++++++++++--------------- blocks/blog-header/blog-header.js | 39 ++++++++++++------------- scripts/scripts.js | 8 +++--- styles/styles.css | 2 +- 4 files changed, 46 insertions(+), 49 deletions(-) diff --git a/blocks/blog-header/blog-header.css b/blocks/blog-header/blog-header.css index 566b872..cf70127 100644 --- a/blocks/blog-header/blog-header.css +++ b/blocks/blog-header/blog-header.css @@ -28,17 +28,6 @@ font-size: 16px; } -.blog-header-date a { - display: inline-flex; - align-items: center; - color: #666; - text-decoration: none; -} - -.blog-header-date a:hover { - color: var(--link-color); -} - .blog-header-date svg { width: 16px; height: 16px; @@ -54,18 +43,6 @@ display: inline-block; } -.blog-header-author a { - display: inline-flex; - align-items: center; - gap: 8px; - text-decoration: none; - color: var(--text-color); -} - -.blog-header-author a:hover { - color: var(--link-color); -} - .blog-header-author img { width: 32px; height: 32px; @@ -86,6 +63,21 @@ display: inline-block; } +.blog-header-date a { + display: inline-flex; + align-items: center; + color: #666; + text-decoration: none; +} + +.blog-header-author a { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + color: var(--text-color); +} + .blog-header-tags a { display: inline-block; padding: 8px 16px; @@ -97,6 +89,14 @@ transition: border-color 0.2s, color 0.2s; } +.blog-header-date a:hover { + color: var(--link-color); +} + +.blog-header-author a:hover { + color: var(--link-color); +} + .blog-header-tags a:hover { border-color: var(--link-color); color: var(--link-color); diff --git a/blocks/blog-header/blog-header.js b/blocks/blog-header/blog-header.js index 64d8d09..6a69c61 100644 --- a/blocks/blog-header/blog-header.js +++ b/blocks/blog-header/blog-header.js @@ -1,77 +1,75 @@ -import { getMetadata } from '../../scripts/aem.js'; - export default function decorate(block) { // Extract date, author, tags from the auto-blocked structure const [dateCell, authorCell, tagsCell] = [...block.children[0].children]; const date = dateCell.textContent.trim(); const author = authorCell.textContent.trim(); const tags = tagsCell.textContent.trim(); - + // Create new structure const container = document.createElement('div'); container.className = 'blog-header-container'; - + // Meta section (date and author) const meta = document.createElement('div'); meta.className = 'blog-header-meta'; - + // Date if (date) { const dateWrapper = document.createElement('p'); dateWrapper.className = 'blog-header-date'; - + const time = document.createElement('time'); time.textContent = date; dateWrapper.appendChild(time); - + // Add RSS icon const rssLink = document.createElement('a'); rssLink.href = '/feed'; rssLink.setAttribute('aria-label', 'Atom Feed'); rssLink.innerHTML = ''; dateWrapper.appendChild(rssLink); - + meta.appendChild(dateWrapper); } - + // Author if (author) { const authorWrapper = document.createElement('ul'); authorWrapper.className = 'blog-header-author'; - + const li = document.createElement('li'); const authorLink = document.createElement('a'); - + // Convert author name to GitHub username format (lowercase, no spaces) const githubUsername = author.toLowerCase().replace(/\s+/g, ''); authorLink.href = `https://github.com/${githubUsername}`; - + // Add author image from GitHub const img = document.createElement('img'); - img.src = `https://avatars.githubusercontent.com/u/2760139?v=4`; // This could be enhanced to lookup actual user + img.src = 'https://avatars.githubusercontent.com/u/2760139?v=4'; // This could be enhanced to lookup actual user img.alt = author; img.width = 40; img.height = 40; img.loading = 'lazy'; authorLink.appendChild(img); - + // Add author name const nameSpan = document.createElement('span'); nameSpan.textContent = author; authorLink.appendChild(nameSpan); - + li.appendChild(authorLink); authorWrapper.appendChild(li); meta.appendChild(authorWrapper); } - + container.appendChild(meta); - + // Tags if (tags) { const tagsWrapper = document.createElement('ul'); tagsWrapper.className = 'blog-header-tags'; - + // Split by comma in case there are multiple tags const tagList = tags.split(',').map((t) => t.trim()).filter((t) => t); tagList.forEach((tag) => { @@ -82,12 +80,11 @@ export default function decorate(block) { li.appendChild(tagLink); tagsWrapper.appendChild(li); }); - + container.appendChild(tagsWrapper); } - + // Replace block content block.textContent = ''; block.appendChild(container); } - diff --git a/scripts/scripts.js b/scripts/scripts.js index cc79cb7..4dbbe81 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -63,17 +63,17 @@ function buildBlogHeaderBlock(main) { const date = getMetadata('date'); const author = getMetadata('author'); const tags = getMetadata('article:tag'); - + // Only build blog header if we have blog metadata if (!date && !author && !tags) return; - + const h1 = main.querySelector('h1'); if (!h1) return; - + // Build the blog header block structure const cells = [date || '', author || '', tags || '']; const blogHeader = buildBlock('blog-header', [cells]); - + // Insert right after H1 h1.parentElement.insertBefore(blogHeader, h1.nextSibling); } diff --git a/styles/styles.css b/styles/styles.css index 1acdbf1..bffc183 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -12,7 +12,7 @@ :root { /* colors */ - --white: #ffffff; + --white: #fff; --background-color: var(--white); --light-color: #ecf3fd; --dark-color: #505050;