From d29d1fbb1b960c996b14abe5349728e9c6addc5c Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 15 Jan 2026 17:39:32 +0000 Subject: [PATCH 1/3] Adding some webmentions code --- assets/js/webmentions.js | 121 ++++++++++++++++++++ content/resources/button-generator/index.md | 2 +- {layouts => static}/humans.txt | 3 +- 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 assets/js/webmentions.js rename {layouts => static}/humans.txt (83%) diff --git a/assets/js/webmentions.js b/assets/js/webmentions.js new file mode 100644 index 0000000..251791c --- /dev/null +++ b/assets/js/webmentions.js @@ -0,0 +1,121 @@ +/** + * Webmention utilities for ritual.sh + * Fetches and formats webmentions from the API + */ + +class WebmentionUtils { + constructor() { + this.apiUrl = "https://api.ritual.sh"; + } + + /** + * Fetch webmentions for a given target URL + * @param {string} targetUrl - The page URL to get webmentions for + * @param {number} limit - Maximum number of webmentions to fetch (default: 100) + * @returns {Promise} Array of webmention objects + */ + async fetch(targetUrl, limit = 100) { + try { + const params = new URLSearchParams({ + target: targetUrl, + limit: limit.toString(), + }); + + const response = await fetch(`${this.apiUrl}/webmentions?${params}`); + + if (!response.ok) { + console.error(`Webmention API error: ${response.status}`); + return []; + } + + const data = await response.json(); + return data.mentions || []; + } catch (error) { + console.error("Error fetching webmentions:", error); + return []; + } + } + + /** + * Format webmentions as a comma-separated list of links + * @param {Array} mentions - Array of webmention objects + * @returns {string} HTML string of comma-separated links + */ + asCommaSeparatedList(mentions) { + if (!mentions || mentions.length === 0) { + return ""; + } + + return mentions + .map((mention) => { + const url = mention.author_url || mention.source; + const domain = this.formatDomain(url); + const escapedUrl = this.escapeHtml(url); + const escapedDomain = this.escapeHtml(domain); + return `${escapedDomain}`; + }) + .join(", "); + } + + /** + * Format a URL to display just the domain (no protocol, www, or path) + * @param {string} url - The URL to format + * @returns {string} Clean domain name + */ + formatDomain(url) { + if (!url) return ""; + + try { + const urlObj = new URL(url); + let domain = urlObj.hostname; + // Remove www. prefix if present + if (domain.startsWith("www.")) { + domain = domain.substring(4); + } + return domain; + } catch (e) { + // Fallback: strip protocol and www manually + return url + .replace(/^https?:\/\//, "") + .replace(/^www\./, "") + .split("/")[0]; + } + } + + /** + * Escape HTML to prevent XSS + * @param {string} text - Text to escape + * @returns {string} Escaped HTML string + */ + escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + /** + * Auto-initialize elements with data-webmention-list attribute + */ + autoInit() { + const elements = document.querySelectorAll("[data-webmention-list]"); + + elements.forEach(async (element) => { + const targetUrl = + element.dataset.webmentionList || window.location.href; + const mentions = await this.fetch(targetUrl); + element.innerHTML = this.asCommaSeparatedList(mentions); + }); + } +} + +// Create global instance +window.WebmentionUtils = new WebmentionUtils(); + +// Auto-initialize on DOM ready +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + window.WebmentionUtils.autoInit(); + }); +} else { + window.WebmentionUtils.autoInit(); +} diff --git a/content/resources/button-generator/index.md b/content/resources/button-generator/index.md index f17c143..ccb5bee 100644 --- a/content/resources/button-generator/index.md +++ b/content/resources/button-generator/index.md @@ -36,6 +36,6 @@ Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x3 [fyr.io](https://fyr.io/scrap/2026-01-09), [brennan.day](https://brennan.day/resources-for-the-personal-web-a-follow-up-guide/), [kuemmerle.name](https://kuemmerle.name/bastelglueck/), -[craney.uk](https://craney.uk/posts/stuff-this-week-74) +[craney.uk](https://craney.uk/posts/stuff-this-week-74), Am I missing you? Email me or send a webmention! diff --git a/layouts/humans.txt b/static/humans.txt similarity index 83% rename from layouts/humans.txt rename to static/humans.txt index 68c8639..f07f372 100644 --- a/layouts/humans.txt +++ b/static/humans.txt @@ -6,10 +6,9 @@ From: ritual.sh /* THANKS */ Hugo - Static site generator The IndieWeb community -Everyone still making personal websites in {{ now.Year }} +Everyone still making personal websites in 2026 /* SITE */ -Last update: {{ .Lastmod.Format "2006/01/02" }} Standards: HTML5, CSS3, RSS Components: Hugo, YAML, Markdown Software: Built with care From b34b16d8e07366eb4c569b4b8872252291e9dd27 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 15 Jan 2026 18:30:49 +0000 Subject: [PATCH 2/3] Adding webmentions to blog --- assets/js/webmentions.js | 98 ++++++++++++++++++++++++++++++++++++---- layouts/blog/single.html | 14 +++++- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/assets/js/webmentions.js b/assets/js/webmentions.js index 251791c..ba1f0a6 100644 --- a/assets/js/webmentions.js +++ b/assets/js/webmentions.js @@ -94,17 +94,99 @@ class WebmentionUtils { } /** - * Auto-initialize elements with data-webmention-list attribute + * Auto-initialize all webmention elements */ - autoInit() { - const elements = document.querySelectorAll("[data-webmention-list]"); + async autoInit() { + // Group elements by target URL to avoid duplicate API calls + const targetGroups = new Map(); - elements.forEach(async (element) => { - const targetUrl = - element.dataset.webmentionList || window.location.href; - const mentions = await this.fetch(targetUrl); - element.innerHTML = this.asCommaSeparatedList(mentions); + const selectors = [ + "[data-webmention-list]", + "[data-webmention-show]", + "[data-webmention-hide]", + "[data-webmention-text]", + ]; + + selectors.forEach((selector) => { + document.querySelectorAll(selector).forEach((element) => { + const targetUrl = this.getTargetUrl(element) || window.location.href; + if (!targetGroups.has(targetUrl)) { + targetGroups.set(targetUrl, []); + } + targetGroups.get(targetUrl).push(element); + }); }); + + // Fetch and process each target URL + for (const [targetUrl, elements] of targetGroups) { + const mentions = await this.fetch(targetUrl); + const hasMentions = mentions.length > 0; + + elements.forEach((element) => { + this.processElement(element, mentions, hasMentions); + }); + } + } + + /** + * Get the target URL from an element's data attributes or parent container + */ + getTargetUrl(element) { + // First check the element's own attributes for a non-empty value + const ownUrl = + element.dataset.webmentionList || + element.dataset.webmentionShow || + element.dataset.webmentionHide || + element.dataset.webmentionText || + ""; + + if (ownUrl) { + return ownUrl; + } + + // Check for parent container with data-webmention-target + const container = element.closest("[data-webmention-target]"); + if (container) { + return container.dataset.webmentionTarget; + } + + return ""; + } + + /** + * Process a single element based on its data attributes + */ + processElement(element, mentions, hasMentions) { + // Handle data-webmention-list + if (element.hasAttribute("data-webmention-list")) { + element.innerHTML = this.asCommaSeparatedList(mentions); + } + + // Handle data-webmention-show (visible only if mentions exist) + if (element.hasAttribute("data-webmention-show")) { + element.style.display = hasMentions ? "" : "none"; + } + + // Handle data-webmention-hide (hidden if mentions exist) + if (element.hasAttribute("data-webmention-hide")) { + element.style.display = hasMentions ? "none" : ""; + } + + // Handle data-webmention-text (different text based on count) + if (element.hasAttribute("data-webmention-text")) { + const textConfig = element.dataset.webmentionText; + // Format: "none text|single text|plural text" or just check for pipe + if (textConfig.includes("|")) { + const parts = textConfig.split("|"); + if (mentions.length === 0) { + element.textContent = parts[0] || ""; + } else if (mentions.length === 1) { + element.textContent = parts[1] || parts[0] || ""; + } else { + element.textContent = parts[2] || parts[1] || parts[0] || ""; + } + } + } } } diff --git a/layouts/blog/single.html b/layouts/blog/single.html index 0aa3eaa..3c6cf81 100644 --- a/layouts/blog/single.html +++ b/layouts/blog/single.html @@ -22,7 +22,19 @@ {{ end }}
{{ .Content }}
From b7ed65e14c1b7bcb0ebc0b6d289fa7a3acfdacf8 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 15 Jan 2026 18:53:03 +0000 Subject: [PATCH 3/3] Updating blog content --- content/blog/2026-01-12-week-4-got-webmentions/index.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/content/blog/2026-01-12-week-4-got-webmentions/index.md b/content/blog/2026-01-12-week-4-got-webmentions/index.md index a4c0718..1cae573 100644 --- a/content/blog/2026-01-12-week-4-got-webmentions/index.md +++ b/content/blog/2026-01-12-week-4-got-webmentions/index.md @@ -7,10 +7,14 @@ tags: draft: false --- -- 🗨️ Added the ability to send and receive [webmentions](https://indieweb.org/Webmention) to the blog. Haven't automated displaying of received ones yet, but I'll get there. +- 🗨️ Added the ability to send and receive [webmentions](https://indieweb.org/Webmention) to the blog. +- ✉️ Received an actual real life email from [Dan](https://danq.me/) to let me know he had sent the first one and it wasn't showing up, so I actually had to rush out the code to display webmentions, too! - 🧰 Setup a little personal API to handle the guestbook, visitor counter, and aforementioned webmentions. - 📺 Setup a dashboard on my homelab for monitoring all of the above. - 🛜 Added an RSS feed for the blog. I've made my full posts available via RSS so you can consume them however you please. +- 💏 Made some significant progress towards our plans for moving house, and life in general! +- 🎁 Found some awesome little bits of tech and my wife kindly agreed to get them for my birthday next month, they will absolutely be showing up on the blog some time after that. +- 🧑‍💻 Wrote a script that can take a video with subtitles and a string of text, and return the gif of the clip. It'll be online shortly on GitHub and my resources section. ## Links I Found Interesting @@ -18,6 +22,8 @@ draft: false - [Zootopia 2](https://blog.yiningkarlli.com/2025/12/zootopia-2.html) - CG Engineer from Walt Disney Studios talks about some of the technical stuff from making Zootopia 2. +- [What I Have Learned Being on the IndieWeb for a Month](https://brennan.day/what-i-have-learned-being-on-the-indieweb-for-a-month/) - Brennan reviews what he has learnt from being on the IndieWeb for a month, and I definitely feel like I should post something similar very soon - I still have a few week or so! + ## Music - 📺 [Mortal Kombat x Rhythm is a Dancer](https://www.youtube.com/watch?v=vKxn6P947PE)