From 2e7718716bc79efed6b6e53c145d7eb7b773a438 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Fri, 22 Aug 2025 02:19:10 -0400 Subject: [PATCH 01/20] Create `public/lib/html.js` --- public/lib/html.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 public/lib/html.js diff --git a/public/lib/html.js b/public/lib/html.js new file mode 100644 index 0000000..0279555 --- /dev/null +++ b/public/lib/html.js @@ -0,0 +1,87 @@ +// Refined from https://plainvanillaweb.com/lib/html.js + +class Html extends String { } + + +/** + * Marks a string as `HTML` to not encode it + * + * Normally, strings are entity-encoded (e.g. `<` instead of `<`) + * before being inserted into HTML + * + * The following wraps the string into the `Html` class to skip encoding + * + * + * @param {string} str + * @returns {Html} + */ +export const htmlRaw = str => new Html(str); + + +/** + * Escapes special characters for safe insertion into HTML + * + * + * @param {*} value + * @returns {Html} + */ +export const htmlEncode = (value) => { + // Avoid double-encoding if value is already `Html` + if (value instanceof Html) return value; + + /** @type {Record} */ + const map = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }; + + return htmlRaw( + String(value).replace(/[&<>'"]/g, (tag) => map[tag]) + ); +}; + + +/** + * Template literal tag that safely builds HTML by auto-encoding user data + * + * Example usage: + * html`

Hello, ${userName}

` + * + * When you write the above, the function receives: + * strings = ['

Hello, ', '

'] // The literal HTML parts + * values = [userName] // The ${} interpolated values + * + * It then: + * 1. HTML-encodes each user value (so "'; + * html`

Hello, ${userName}

` + * // Result: '

Hello, <script>alert("xss")</script>

' + * + * + * @param {TemplateStringsArray} strings - The literal parts of the template + * @param {...*} values - The interpolated ${} values that need encoding + * @returns {Html} - Safe HTML string marked as already processed + */ +export const html = (strings, ...values) => + htmlRaw( + String.raw( + { raw: strings }, + // Transform each user value into a safe string before inserting + ...values.map(v => String(htmlEncode(v))) + ) + ); + +// `String.raw` does the following: +// String.raw({ raw: ['

Hello, ', '

'] }, 'SAFE_USER_DATA') +// Result: '

Hello, SAFE_USER_DATA

' +// +// Equivalently: strings[0] + values[0] + strings[1] + values[1] + ... +// but preserves literal backslashes in the HTML templates From 6c831738efc1ef26a41f39cd700e0ad7d270fc31 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Fri, 22 Aug 2025 02:32:58 -0400 Subject: [PATCH 02/20] Improve docstring and code for `htmlEncode()` --- public/lib/html.js | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/public/lib/html.js b/public/lib/html.js index 0279555..1dde857 100644 --- a/public/lib/html.js +++ b/public/lib/html.js @@ -18,28 +18,37 @@ class Html extends String { } export const htmlRaw = str => new Html(str); +/** @type {Record} */ +const HTML_ENTITIES = { + '&': '&', + '<': '<', + '>': '>', + "'": ''', // sexier than using `'` + '"': '"' +}; + /** - * Escapes special characters for safe insertion into HTML + * Escapes special HTML characters for XSS protection * + * Converts special HTML characters into their HTML entity equivalents, + * ensuring they display as literal text instead of being interpreted + * as HTML markup * - * @param {*} value - * @returns {Html} + * Examples: + * htmlEncode(' From baee553bbb68ba3b739fd90ba9a52c2185d476c0 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:29:36 -0400 Subject: [PATCH 09/20] Create `pages/blog/index.js` --- public/pages/blog/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 public/pages/blog/index.js diff --git a/public/pages/blog/index.js b/public/pages/blog/index.js new file mode 100644 index 0000000..0fdb818 --- /dev/null +++ b/public/pages/blog/index.js @@ -0,0 +1,8 @@ +import { blogPostsComponent } from "../../components/blog-posts.js"; + +const app = () => { + blogPostsComponent(); +} + +// Ensure that HTML is fully loaded before registering components by using `async` +document.addEventListener('DOMContentLoaded', app); From ad72c31b1c8bb1b8f9ef78312eced34be43f64ea Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:30:15 -0400 Subject: [PATCH 10/20] Remove inaccurate `async` comment in `DOMContentLoaded` --- public/pages/blog/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/pages/blog/index.js b/public/pages/blog/index.js index 0fdb818..fdec0b0 100644 --- a/public/pages/blog/index.js +++ b/public/pages/blog/index.js @@ -4,5 +4,5 @@ const app = () => { blogPostsComponent(); } -// Ensure that HTML is fully loaded before registering components by using `async` +// Ensure that HTML is fully loaded before registering components document.addEventListener('DOMContentLoaded', app); From 13fe2705299ae17f9031bccfc717fcb0d1dfefd8 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:32:10 -0400 Subject: [PATCH 11/20] Fix broken links in navigation bar --- public/index.html | 2 +- public/pages/blog/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index ed28473..165f400 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,7 @@

Luis Victoria

diff --git a/public/pages/blog/index.html b/public/pages/blog/index.html index a95464e..243e717 100644 --- a/public/pages/blog/index.html +++ b/public/pages/blog/index.html @@ -21,7 +21,7 @@

Luis Victoria

From 006cb8f6b06c713351eca2f8ea4665849cd258d8 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:40:05 -0400 Subject: [PATCH 12/20] Eliminate use of `import.meta` --- public/components/blog-posts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/blog-posts.js b/public/components/blog-posts.js index 23d1f07..5626484 100644 --- a/public/components/blog-posts.js +++ b/public/components/blog-posts.js @@ -11,7 +11,7 @@ class LatestPosts extends HTMLElement { // Fallback text to display while content loads this.textContent = "Loading..."; - fetch(import.meta.resolve('../feed.atom')) + fetch('/feed.atom') .then(response => response.text()) .then(xmlText => { // Convert the XML file string into queryable DOM From 69018056815e0e48f19868be6bcda14c30204799 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:41:18 -0400 Subject: [PATCH 13/20] Remove unused `index.js` --- public/index.js | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 public/index.js diff --git a/public/index.js b/public/index.js deleted file mode 100644 index 6f788da..0000000 --- a/public/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { helloWorldComponent } from "./components/hello-world.js"; - -helloWorldComponent(); From 70d5c38680dff21822e6d624e6f5fcf3e7fd8721 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:43:56 -0400 Subject: [PATCH 14/20] Move from `pages/blog/` to `blog/` --- public/{pages => }/blog/index.html | 10 +++++----- public/{pages => }/blog/index.js | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename public/{pages => }/blog/index.html (85%) rename public/{pages => }/blog/index.js (100%) diff --git a/public/pages/blog/index.html b/public/blog/index.html similarity index 85% rename from public/pages/blog/index.html rename to public/blog/index.html index 243e717..12d8861 100644 --- a/public/pages/blog/index.html +++ b/public/blog/index.html @@ -6,10 +6,10 @@ - - - - + + + + @@ -21,7 +21,7 @@

Luis Victoria

diff --git a/public/pages/blog/index.js b/public/blog/index.js similarity index 100% rename from public/pages/blog/index.js rename to public/blog/index.js From 448b9880ac534e754810cbcb00d1d200ee496285 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:44:09 -0400 Subject: [PATCH 15/20] Use absolute file locations --- public/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/index.html b/public/index.html index 165f400..b9dfa90 100644 --- a/public/index.html +++ b/public/index.html @@ -6,10 +6,10 @@ - - - - + + + + @@ -22,7 +22,7 @@

Luis Victoria

From f042c0272e423229164da666a32fd8babfc738d2 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:56:53 -0400 Subject: [PATCH 16/20] Add `feedgen.py` TODO in `todo.txt` --- todo.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/todo.txt b/todo.txt index 0f2e12f..f8fbf4d 100644 --- a/todo.txt +++ b/todo.txt @@ -1,2 +1,3 @@ TODO: - Force all requests to be done in HTTPS -> https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security +- Add a PR branch protection check that checks if running `feedgen.py` creates a diff from your current `feed.atom` file From 0829a96ddc40850dcffdbc06070f9e8f49e072ea Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:57:05 -0400 Subject: [PATCH 17/20] Paste entries to console --- public/components/blog-posts.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/public/components/blog-posts.js b/public/components/blog-posts.js index 5626484..09f40ea 100644 --- a/public/components/blog-posts.js +++ b/public/components/blog-posts.js @@ -15,16 +15,31 @@ class LatestPosts extends HTMLElement { .then(response => response.text()) .then(xmlText => { // Convert the XML file string into queryable DOM + /** @type {Document} */ const xmlDoc = new DOMParser().parseFromString(xmlText, "text/xml"); + /** @type {Element | null} */ const parserError = xmlDoc.querySelector('parsererror'); if (parserError) { throw new Error('Invalid XML'); } // Iterate across all `` tags in the feed + /** @type {NodeListOf} */ const entries = xmlDoc.querySelectorAll('entry'); console.log(`Found ${entries.length} entries`); + + const feedItems = Array.from(entries) + .slice(0, ENTRIES_TO_SHOW) + .map(entry => ({ + title: entry.querySelector('title')?.textContent, + link: entry.querySelector('link')?.getAttribute('href'), + published: entry.querySelector('published')?.textContent, + summary: entry.querySelector('summary')?.textContent + })) + .filter(item => item.title && item.link); + + console.log('Feed items:', feedItems); }) .catch(error => { this.textContent = `Error: ${error.message}`; From a4e056a298b31705cc8cec2987be201693f4f9a0 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 00:58:12 -0400 Subject: [PATCH 18/20] Use absolute path for linking to `feed.atom` --- public/blog/index.html | 2 +- public/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/blog/index.html b/public/blog/index.html index 12d8861..3e92c5f 100644 --- a/public/blog/index.html +++ b/public/blog/index.html @@ -42,7 +42,7 @@

Luis Victoria

GitHub LinkedIn Lichess - RSS + RSS

© 2025 Luis Victoria

diff --git a/public/index.html b/public/index.html index b9dfa90..7e9bf08 100644 --- a/public/index.html +++ b/public/index.html @@ -50,7 +50,7 @@

Luis Victoria

GitHub LinkedIn Lichess - RSS + RSS

© 2025 Luis Victoria

From 1f0fa34578f5e4ff598080dd1776960f64cfcc0b Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 01:17:07 -0400 Subject: [PATCH 19/20] Create `blog/index.css` --- public/blog/index.css | 126 +++++++++++++++++++++++++++++++++++++++++ public/blog/index.html | 2 +- 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 public/blog/index.css diff --git a/public/blog/index.css b/public/blog/index.css new file mode 100644 index 0000000..52bea15 --- /dev/null +++ b/public/blog/index.css @@ -0,0 +1,126 @@ +@import "/index.css"; + +.cards { + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-gap: 2em; + row-gap: 0; + grid-auto-flow: row; + grid-auto-rows: minmax(100px, auto); +} + +.card { + display: block; + position: relative; + list-style: none; + padding: 1em; + margin: 0 -1em; + font-size: 0.9em; + border-radius: 5px; + container-type: inline-size; + container-name: card; +} + +.card img { + width: 100%; + height: 120px; + object-fit: cover; + margin-bottom: 0.5em; +} + +.card h3 { + margin-top: 0; + margin-bottom: 0.5em; +} + +.card a { + text-decoration: none; + color: inherit; +} + +/* make the whole card focusable */ +.card:focus-within { + box-shadow: 0 0 0 0.25rem; +} + +.card:focus-within a:focus { + text-decoration: none; +} + +/* turn the whole card into the clickable area */ +.card h3 a::after { + display: block; + position: absolute; + content: ""; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +/* make byline links clickable */ +.card small { + position: relative; + z-index: 10; + color: var(--text-color-mute); +} + +.card small a { + text-decoration: underline; +} + +.card small a:hover { + color: var(--text-color); +} + +/* for hero cards (full width), move the image to the left */ +@container card ( min-width: 500px ) { + .card img { + float: left; + width: calc(50% - 1em); + height: 200px; + margin-right: 2em; + margin-bottom: 0; + } +} + +.archive { + text-align: center; + color: var(--text-color-mute); + + a:not(:hover) { + color: inherit; + } +} + +.byline { + color: var(--text-color-mute); + font-size: 0.8em; + margin-bottom: 1.5em; +} + +blog-header { + display: block; + margin-bottom: 1.5em; + text-align: center; +} + +blog-header nav { + margin-bottom: 2em; +} + +main img { + margin: 0.5em 0; +} + +@media (scripting: none) { + blog-header::before { + content: 'Please enable scripting to view the navigation' + } +} + +.comments { + margin-top: 2em; + text-align: center; +} diff --git a/public/blog/index.html b/public/blog/index.html index 3e92c5f..860bf09 100644 --- a/public/blog/index.html +++ b/public/blog/index.html @@ -7,7 +7,7 @@ - + From c90afc91419b7f943a8bac71065c5f9c29ed6899 Mon Sep 17 00:00:00 2001 From: Luis Victoria Date: Tue, 26 Aug 2025 01:17:27 -0400 Subject: [PATCH 20/20] [NOT WORKING] Render HTML per card on `feed.atom` --- public/components/blog-posts.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/public/components/blog-posts.js b/public/components/blog-posts.js index 09f40ea..d56956f 100644 --- a/public/components/blog-posts.js +++ b/public/components/blog-posts.js @@ -27,7 +27,6 @@ class LatestPosts extends HTMLElement { // Iterate across all `` tags in the feed /** @type {NodeListOf} */ const entries = xmlDoc.querySelectorAll('entry'); - console.log(`Found ${entries.length} entries`); const feedItems = Array.from(entries) .slice(0, ENTRIES_TO_SHOW) @@ -39,7 +38,26 @@ class LatestPosts extends HTMLElement { })) .filter(item => item.title && item.link); - console.log('Feed items:', feedItems); + if (feedItems.length === 0) { + this.innerHTML = 'No blog posts found.'; + return; + } + + this.innerHTML = String(html` +
    + ${feedItems.map(item => html` +
  • +

    ${item.title}

    +

    ${item.summary}

    + + + +
  • + `).join('')} +
+ `); }) .catch(error => { this.textContent = `Error: ${error.message}`;