<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="/pretty-feed-v3.xsl"?>
<rss
  version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:source="https://source.scripting.com/"
>
  <channel>
    <title>Paul Tibbetts</title>
    <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/</link>
    <description>Posts by Paul Tibbetts published in March 2026</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <atom:link href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/feed.xml" rel="self" type="application/rss+xml" /><item>
      <title>Yes, I&#39;m going to Homebrew Website Club London tonight. I get inspired by many …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/18/2026-03-18-153902-i-m-going-to-homebrew-website-club-london-tonight/</link>
      <pubDate>Wed, 18 Mar 2026 16:00:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/18/2026-03-18-153902-i-m-going-to-homebrew-website-club-london-tonight/</guid>
      <category>Homebrew Website Club</category>
      <description>Yes, I&amp;#39;m going to Homebrew Website Club London tonight.</description>
      <content:encoded><![CDATA[<p class="p-summary">
	<span class="hidden"><span class="p-rsvp">Yes</span>, </span>I'm going to  
	<a href="https://events.indieweb.org/2026/03/homebrew-website-club-europe-london-0EMPkdPMzZKX" class="u-in-reply-to">Homebrew Website Club London tonight</a>.
</p>
<p>I get inspired by many things mentioned at <a href="https://indieweb.org/Homebrew_Website_Club">HWC</a>
, but somehow the first to make it to my blog, based on a comment from the last meetup, is this:</p>
<style>
  .h-entry.notes [data-morse-start],
  .h-entry.notes [data-morse-stop] {
    cursor: pointer;
  }

  .h-entry.notes [data-morse-start]:disabled,
  .h-entry.notes [data-morse-stop]:disabled {
    cursor: default;
    opacity: 0.5;
  }

  .h-entry.notes pre:has(code.language-morse),
  .h-entry.notes pre:has(code[data-lang="morse"]) {
    overflow-x: hidden;
  }

  .h-entry.notes pre code.language-morse,
  .h-entry.notes pre code[data-lang="morse"] {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    word-break: break-word;
  }
</style>
<div class="mt-2">
    <div class="flex items-center rounded-md bg-white/5 pl-3 outline-1 -outline-offset-1 outline-gray-600 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-indigo-500">
		<textarea
		    id="morse-input"
		    data-morse-input
		    rows="2"
		    style="width: 100%; margin-top: 0.25rem;"
		    class="block min-w-0 grow py-1.5 pr-3 pl-1 text-base text-white placeholder:text-gray-500 focus:outline-none sm:text-sm/6"
	>Hello, World!</textarea>
	</div>
</div>
<pre><code class="language-morse"></code></pre>
<p>
  <button type="button" data-morse-start>📡 Broadcast</button>
</p>
<p>
  <button type="button" data-morse-stop>⏹️ Stop</button>
</p>
<script type="text/javascript">
// It would be cool to say I wrote all of this myself
// but I asked an LLM to do it
// and then I realised it could have been a fun little project.
//
// I will learn from this, and choose my LLM usage
// more carefully in the future.
//
// This is the LLM code.
// I take no credit for writing it, other than
// writing the prompts that resulted in this.
//
// It seems to have come from one or two examples
// that one or two people have made in the past.
// There is one line where the LLM seems to argue with
// itself in the comments (#315).
(() => {
  // International Morse (letters+digits; extend as needed)
  const MORSE = {
    A: ".-",
    B: "-...",
    C: "-.-.",
    D: "-..",
    E: ".",
    F: "..-.",
    G: "--.",
    H: "....",
    I: "..",
    J: ".---",
    K: "-.-",
    L: ".-..",
    M: "--",
    N: "-.",
    O: "---",
    P: ".--.",
    Q: "--.-",
    R: ".-.",
    S: "...",
    T: "-",
    U: "..-",
    V: "...-",
    W: ".--",
    X: "-..-",
    Y: "-.--",
    Z: "--..",
    0: "-----",
    1: ".----",
    2: "..---",
    3: "...--",
    4: "....-",
    5: ".....",
    6: "-....",
    7: "--...",
    8: "---..",
    9: "----.",
    ".": ".-.-.-",
    ",": "--..--",
    "?": "..--..",
    "'": ".----.",
    "!": "-.-.--",
    "/": "-..-.",
    "(": "-.--.",
    ")": "-.--.-",
    "&": ".-...",
    ":": "---...",
    ";": "-.-.-.",
    "=": "-...-",
    "+": ".-.-.",
    "-": "-....-",
    "_": "..--.-",
    '"': ".-..-.",
    "$": "...-..-",
    "@": ".--.-."
  };

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  function getMorseCodeElement() {
    return document.querySelector("code.language-morse, code[data-lang='morse']");
  }

  function getMorseInputElement() {
    return document.querySelector("[data-morse-input]");
  }

  function getMorseSourceText() {
    const inputEl = getMorseInputElement();
    if (inputEl) {
      const inputText = (inputEl.value || "").trim();
      return inputText || null;
    }

    const codeEl = getMorseCodeElement();
    if (!codeEl) return null;

    if (!codeEl.dataset.morseSource) {
      codeEl.dataset.morseSource = (codeEl.textContent || "").trim();
    }

    return codeEl.dataset.morseSource || null;
  }

  function renderMorseExample() {
    const codeEl = getMorseCodeElement();
    const sourceText = getMorseSourceText();
    if (!codeEl || !sourceText) return null;

    codeEl.textContent = encodeMorse(sourceText);
    return sourceText;
  }

  function encodeMorse(text) {
    const up = text.toUpperCase();
    // Words separated by " / ", letters by spaces
    const words = up.split(/\s+/).filter(Boolean);
    return words.map(word => {
      return word.split("").map(ch => MORSE[ch] ?? "").filter(Boolean).join(" ");
    }).join(" / ");
  }

  function makeBeepEngine() {
    const ctx = new (window.AudioContext || window.webkitAudioContext)();
    const gain = ctx.createGain();
    gain.gain.value = 0; // start silent
    gain.connect(ctx.destination);

    const osc = ctx.createOscillator();
    osc.type = "sine";
    osc.frequency.value = 650; // Hz
    osc.connect(gain);
    osc.start();

    return {
      ctx,
      async ensureRunning() {
        if (ctx.state !== "running") await ctx.resume();
      },
      on() { gain.gain.setTargetAtTime(0.12, ctx.currentTime, 0.005); },
      off() { gain.gain.setTargetAtTime(0.0, ctx.currentTime, 0.005); },
      stop() {
        try { osc.stop(); } catch {}
        ctx.close();
      }
    };
  }

  /**
   * Tap Morse live.
   * Timing is based on "unit" length (dot = 1u, dash = 3u).
   * - intra-symbol gap: 1u
   * - letter gap: 3u
   * - word gap: 7u (we approximate with "/" token)
   */
  async function tapMorse(morse, {
    unitMs = 90,
    useAudio = true,
    useVibrate = false,
    log = true,
    signal = undefined
  } = {}) {
    const engine = useAudio ? makeBeepEngine() : null;
    try {
      if (engine) await engine.ensureRunning();

      const tokens = morse.split(" "); // includes ".", "-", "/" as tokens
      if (log) console.log("[morse] tapping:", morse);

      for (let i = 0; i < tokens.length; i++) {
        if (signal?.aborted) throw new DOMException("Aborted", "AbortError");

        const t = tokens[i];

        if (t === ".") {
          engine?.on();
          if (useVibrate && navigator.vibrate) navigator.vibrate(unitMs);
          await sleep(unitMs);
          engine?.off();
          await sleep(unitMs); // intra-symbol gap
          continue;
        }

        if (t === "-") {
          engine?.on();
          if (useVibrate && navigator.vibrate) navigator.vibrate(unitMs * 3);
          await sleep(unitMs * 3);
          engine?.off();
          await sleep(unitMs); // intra-symbol gap
          continue;
        }

        if (t === "/") {
          // word gap: 7 units (we already likely had 1 unit after prior symbol)
          await sleep(unitMs * 6);
          continue;
        }

        // Letter boundary: when we see a multi-symbol chunk like ".-.." we won't,
        // because we tokenized by spaces. But we *do* need letter gaps.
        // Our encoding already put spaces between letters, so the token stream
        // is just symbols and "/" — the letter gap is handled by a longer pause
        // when the next token is a symbol but we are at a letter boundary.
        //
        // Easiest: detect original morse string boundaries by using the fact
        // that encodeMorse uses single spaces between letters, and "/" is separate.
        //
        // We already include the 1u intra-symbol gap after each dot/dash, so for
        // letter gap (3u) we add 2u extra when the *next* token is a symbol and
        // we are between letters. We can approximate by looking at the raw string.
      }
    } finally {
      engine?.stop();
    }
  }

  // Slightly better timing: tap by parsing the original morse string
  async function tapMorsePrecise(morse, opts = {}) {
    const {
      unitMs = 90, useAudio = true, useVibrate = false, log = true, signal
    } = opts;

    const engine = useAudio ? makeBeepEngine() : null;
    try {
      if (engine) await engine.ensureRunning();
      if (log) console.log("[morse] tapping:", morse);

      // Parse words split by " / "
      const words = morse.split(" / ").map(w => w.trim()).filter(Boolean);

      for (let w = 0; w < words.length; w++) {
        const letters = words[w].split(" ").map(l => l.trim()).filter(Boolean); // each letter is like ".-"
        for (let l = 0; l < letters.length; l++) {
          if (signal?.aborted) throw new DOMException("Aborted", "AbortError");

          const symbols = letters[l].split("");
          for (let s = 0; s < symbols.length; s++) {
            const sym = symbols[s];
            if (sym === ".") {
              engine?.on();
              if (useVibrate && navigator.vibrate) navigator.vibrate(unitMs);
              await sleep(unitMs);
              engine?.off();
            } else if (sym === "-") {
              engine?.on();
              if (useVibrate && navigator.vibrate) navigator.vibrate(unitMs * 3);
              await sleep(unitMs * 3);
              engine?.off();
            }
            // intra-symbol gap (1u) between parts of same letter
            if (s < symbols.length - 1) await sleep(unitMs);
          }
          // letter gap: 3u (but we've already waited 1u after last symbol? no)
          if (l < letters.length - 1) await sleep(unitMs * 3);
        }
        // word gap: 7u
        if (w < words.length - 1) await sleep(unitMs * 7);
      }
    } finally {
      engine?.stop();
    }
  }

  const START_BUTTON_SELECTOR = "[data-morse-start]";
  const STOP_BUTTON_SELECTOR = "[data-morse-stop]";
  const startButton = document.querySelector(START_BUTTON_SELECTOR);
  const stopButton = document.querySelector(STOP_BUTTON_SELECTOR);

  function setPlayingState(isPlaying) {
    if (startButton) startButton.disabled = isPlaying;
    if (stopButton) stopButton.disabled = !isPlaying;
  }

  // Controller so user can stop it
  let current = null;

  async function playMorse({
    unitMs = 90,
    useAudio = true,
    useVibrate = false,
    log = true
  } = {}) {
    const text = getMorseSourceText();

    if (!text) {
      console.warn("[morse] No morse source code block found.");
      return;
    }

    const morse = encodeMorse(text);

    // Abort previous run if any
    if (current?.controller) current.controller.abort();
    const controller = new AbortController();
    current = { controller };
    setPlayingState(true);

    try {
      await tapMorsePrecise(morse, { unitMs, useAudio, useVibrate, log, signal: controller.signal });
    } catch (e) {
      if (e?.name === "AbortError") {
        if (log) console.log("[morse] stopped.");
        return;
      }
      throw e;
    } finally {
      setPlayingState(false);
    }
  }

  function morseStop() {
    if (current?.controller) current.controller.abort();
    setPlayingState(false);
  }

  const sourceText = renderMorseExample();
  if (!sourceText) {
    console.warn("[morse] Could not render morse example from code block.");
  }

  const inputEl = getMorseInputElement();
  if (inputEl) {
    inputEl.addEventListener("input", () => {
      renderMorseExample();
    });
  }

  setPlayingState(false);

  if (startButton) {
    startButton.addEventListener("click", () => {
      playMorse();
    });
  } else {
    console.warn(`[morse] Start button not found (${START_BUTTON_SELECTOR}).`);
  }

  if (stopButton) {
    stopButton.addEventListener("click", morseStop);
  } else {
    console.warn(`[morse] Stop button not found (${STOP_BUTTON_SELECTOR}).`);
  }
})();
</script>
<p>To make this I was going to need to look up the Morse code alphabet, and then use it as part of a script.</p>
<p>I was feeling lazy so I used an LLM to do that for me, and it ended up writing the whole thing, including turning it into sound, which I hadn&rsquo;t even asked it to do.</p>
<p>It didn&rsquo;t just make it all, in seconds, it ruined any fun I might have had experimenting on a new thing, maybe even learning a thing or two.</p>
<p>So I present it here begrudgingly.</p>
<hr>
<p>I am using this note to test I can send an <a href="https://indieweb.org/rsvp">RSVP</a>
.</p>
<p>If you see my name <a href="https://events.indieweb.org/2026/03/homebrew-website-club-europe-london-0EMPkdPMzZKX#rsvps">on this list</a>
 then it worked.</p>]]></content:encoded><source:markdown>
&lt;p class=&#34;p-summary&#34;&gt;
	&lt;span class=&#34;hidden&#34;&gt;&lt;span class=&#34;p-rsvp&#34;&gt;Yes&lt;/span&gt;, &lt;/span&gt;I&#39;m going to  
	&lt;a href=&#34;https://events.indieweb.org/2026/03/homebrew-website-club-europe-london-0EMPkdPMzZKX&#34; class=&#34;u-in-reply-to&#34;&gt;Homebrew Website Club London tonight&lt;/a&gt;.
&lt;/p&gt;

&lt;!--more--&gt;

I get inspired by many things mentioned at [HWC](https://indieweb.org/Homebrew_Website_Club), but somehow the first to make it to my blog, based on a comment from the last meetup, is this:

&lt;style&gt;
  .h-entry.notes [data-morse-start],
  .h-entry.notes [data-morse-stop] {
    cursor: pointer;
  }

  .h-entry.notes [data-morse-start]:disabled,
  .h-entry.notes [data-morse-stop]:disabled {
    cursor: default;
    opacity: 0.5;
  }

  .h-entry.notes pre:has(code.language-morse),
  .h-entry.notes pre:has(code[data-lang=&#34;morse&#34;]) {
    overflow-x: hidden;
  }

  .h-entry.notes pre code.language-morse,
  .h-entry.notes pre code[data-lang=&#34;morse&#34;] {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    word-break: break-word;
  }
&lt;/style&gt;

&lt;div class=&#34;mt-2&#34;&gt;
    &lt;div class=&#34;flex items-center rounded-md bg-white/5 pl-3 outline-1 -outline-offset-1 outline-gray-600 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-indigo-500&#34;&gt;
		&lt;textarea
		    id=&#34;morse-input&#34;
		    data-morse-input
		    rows=&#34;2&#34;
		    style=&#34;width: 100%; margin-top: 0.25rem;&#34;
		    class=&#34;block min-w-0 grow py-1.5 pr-3 pl-1 text-base text-white placeholder:text-gray-500 focus:outline-none sm:text-sm/6&#34;
	&gt;Hello, World!&lt;/textarea&gt;
	&lt;/div&gt;
&lt;/div&gt;

```morse

```

&lt;p&gt;
  &lt;button type=&#34;button&#34; data-morse-start&gt;📡 Broadcast&lt;/button&gt;
&lt;/p&gt;
&lt;p&gt;
  &lt;button type=&#34;button&#34; data-morse-stop&gt;⏹️ Stop&lt;/button&gt;
&lt;/p&gt;

&lt;script type=&#34;text/javascript&#34;&gt;
// It would be cool to say I wrote all of this myself
// but I asked an LLM to do it
// and then I realised it could have been a fun little project.
//
// I will learn from this, and choose my LLM usage
// more carefully in the future.
//
// This is the LLM code.
// I take no credit for writing it, other than
// writing the prompts that resulted in this.
//
// It seems to have come from one or two examples
// that one or two people have made in the past.
// There is one line where the LLM seems to argue with
// itself in the comments (#315).
(() =&gt; {
  // International Morse (letters+digits; extend as needed)
  const MORSE = {
    A: &#34;.-&#34;,
    B: &#34;-...&#34;,
    C: &#34;-.-.&#34;,
    D: &#34;-..&#34;,
    E: &#34;.&#34;,
    F: &#34;..-.&#34;,
    G: &#34;--.&#34;,
    H: &#34;....&#34;,
    I: &#34;..&#34;,
    J: &#34;.---&#34;,
    K: &#34;-.-&#34;,
    L: &#34;.-..&#34;,
    M: &#34;--&#34;,
    N: &#34;-.&#34;,
    O: &#34;---&#34;,
    P: &#34;.--.&#34;,
    Q: &#34;--.-&#34;,
    R: &#34;.-.&#34;,
    S: &#34;...&#34;,
    T: &#34;-&#34;,
    U: &#34;..-&#34;,
    V: &#34;...-&#34;,
    W: &#34;.--&#34;,
    X: &#34;-..-&#34;,
    Y: &#34;-.--&#34;,
    Z: &#34;--..&#34;,
    0: &#34;-----&#34;,
    1: &#34;.----&#34;,
    2: &#34;..---&#34;,
    3: &#34;...--&#34;,
    4: &#34;....-&#34;,
    5: &#34;.....&#34;,
    6: &#34;-....&#34;,
    7: &#34;--...&#34;,
    8: &#34;---..&#34;,
    9: &#34;----.&#34;,
    &#34;.&#34;: &#34;.-.-.-&#34;,
    &#34;,&#34;: &#34;--..--&#34;,
    &#34;?&#34;: &#34;..--..&#34;,
    &#34;&#39;&#34;: &#34;.----.&#34;,
    &#34;!&#34;: &#34;-.-.--&#34;,
    &#34;/&#34;: &#34;-..-.&#34;,
    &#34;(&#34;: &#34;-.--.&#34;,
    &#34;)&#34;: &#34;-.--.-&#34;,
    &#34;&amp;&#34;: &#34;.-...&#34;,
    &#34;:&#34;: &#34;---...&#34;,
    &#34;;&#34;: &#34;-.-.-.&#34;,
    &#34;=&#34;: &#34;-...-&#34;,
    &#34;+&#34;: &#34;.-.-.&#34;,
    &#34;-&#34;: &#34;-....-&#34;,
    &#34;_&#34;: &#34;..--.-&#34;,
    &#39;&#34;&#39;: &#34;.-..-.&#34;,
    &#34;$&#34;: &#34;...-..-&#34;,
    &#34;@&#34;: &#34;.--.-.&#34;
  };

  const sleep = (ms) =&gt; new Promise(r =&gt; setTimeout(r, ms));

  function getMorseCodeElement() {
    return document.querySelector(&#34;code.language-morse, code[data-lang=&#39;morse&#39;]&#34;);
  }

  function getMorseInputElement() {
    return document.querySelector(&#34;[data-morse-input]&#34;);
  }

  function getMorseSourceText() {
    const inputEl = getMorseInputElement();
    if (inputEl) {
      const inputText = (inputEl.value || &#34;&#34;).trim();
      return inputText || null;
    }

    const codeEl = getMorseCodeElement();
    if (!codeEl) return null;

    if (!codeEl.dataset.morseSource) {
      codeEl.dataset.morseSource = (codeEl.textContent || &#34;&#34;).trim();
    }

    return codeEl.dataset.morseSource || null;
  }

  function renderMorseExample() {
    const codeEl = getMorseCodeElement();
    const sourceText = getMorseSourceText();
    if (!codeEl || !sourceText) return null;

    codeEl.textContent = encodeMorse(sourceText);
    return sourceText;
  }

  function encodeMorse(text) {
    const up = text.toUpperCase();
    // Words separated by &#34; / &#34;, letters by spaces
    const words = up.split(/\s+/).filter(Boolean);
    return words.map(word =&gt; {
      return word.split(&#34;&#34;).map(ch =&gt; MORSE[ch] ?? &#34;&#34;).filter(Boolean).join(&#34; &#34;);
    }).join(&#34; / &#34;);
  }

  function makeBeepEngine() {
    const ctx = new (window.AudioContext || window.webkitAudioContext)();
    const gain = ctx.createGain();
    gain.gain.value = 0; // start silent
    gain.connect(ctx.destination);

    const osc = ctx.createOscillator();
    osc.type = &#34;sine&#34;;
    osc.frequency.value = 650; // Hz
    osc.connect(gain);
    osc.start();

    return {
      ctx,
      async ensureRunning() {
        if (ctx.state !== &#34;running&#34;) await ctx.resume();
      },
      on() { gain.gain.setTargetAtTime(0.12, ctx.currentTime, 0.005); },
      off() { gain.gain.setTargetAtTime(0.0, ctx.currentTime, 0.005); },
      stop() {
        try { osc.stop(); } catch {}
        ctx.close();
      }
    };
  }

  /**
   * Tap Morse live.
   * Timing is based on &#34;unit&#34; length (dot = 1u, dash = 3u).
   * - intra-symbol gap: 1u
   * - letter gap: 3u
   * - word gap: 7u (we approximate with &#34;/&#34; token)
   */
  async function tapMorse(morse, {
    unitMs = 90,
    useAudio = true,
    useVibrate = false,
    log = true,
    signal = undefined
  } = {}) {
    const engine = useAudio ? makeBeepEngine() : null;
    try {
      if (engine) await engine.ensureRunning();

      const tokens = morse.split(&#34; &#34;); // includes &#34;.&#34;, &#34;-&#34;, &#34;/&#34; as tokens
      if (log) console.log(&#34;[morse] tapping:&#34;, morse);

      for (let i = 0; i &lt; tokens.length; i++) {
        if (signal?.aborted) throw new DOMException(&#34;Aborted&#34;, &#34;AbortError&#34;);

        const t = tokens[i];

        if (t === &#34;.&#34;) {
          engine?.on();
          if (useVibrate &amp;&amp; navigator.vibrate) navigator.vibrate(unitMs);
          await sleep(unitMs);
          engine?.off();
          await sleep(unitMs); // intra-symbol gap
          continue;
        }

        if (t === &#34;-&#34;) {
          engine?.on();
          if (useVibrate &amp;&amp; navigator.vibrate) navigator.vibrate(unitMs * 3);
          await sleep(unitMs * 3);
          engine?.off();
          await sleep(unitMs); // intra-symbol gap
          continue;
        }

        if (t === &#34;/&#34;) {
          // word gap: 7 units (we already likely had 1 unit after prior symbol)
          await sleep(unitMs * 6);
          continue;
        }

        // Letter boundary: when we see a multi-symbol chunk like &#34;.-..&#34; we won&#39;t,
        // because we tokenized by spaces. But we *do* need letter gaps.
        // Our encoding already put spaces between letters, so the token stream
        // is just symbols and &#34;/&#34; — the letter gap is handled by a longer pause
        // when the next token is a symbol but we are at a letter boundary.
        //
        // Easiest: detect original morse string boundaries by using the fact
        // that encodeMorse uses single spaces between letters, and &#34;/&#34; is separate.
        //
        // We already include the 1u intra-symbol gap after each dot/dash, so for
        // letter gap (3u) we add 2u extra when the *next* token is a symbol and
        // we are between letters. We can approximate by looking at the raw string.
      }
    } finally {
      engine?.stop();
    }
  }

  // Slightly better timing: tap by parsing the original morse string
  async function tapMorsePrecise(morse, opts = {}) {
    const {
      unitMs = 90, useAudio = true, useVibrate = false, log = true, signal
    } = opts;

    const engine = useAudio ? makeBeepEngine() : null;
    try {
      if (engine) await engine.ensureRunning();
      if (log) console.log(&#34;[morse] tapping:&#34;, morse);

      // Parse words split by &#34; / &#34;
      const words = morse.split(&#34; / &#34;).map(w =&gt; w.trim()).filter(Boolean);

      for (let w = 0; w &lt; words.length; w++) {
        const letters = words[w].split(&#34; &#34;).map(l =&gt; l.trim()).filter(Boolean); // each letter is like &#34;.-&#34;
        for (let l = 0; l &lt; letters.length; l++) {
          if (signal?.aborted) throw new DOMException(&#34;Aborted&#34;, &#34;AbortError&#34;);

          const symbols = letters[l].split(&#34;&#34;);
          for (let s = 0; s &lt; symbols.length; s++) {
            const sym = symbols[s];
            if (sym === &#34;.&#34;) {
              engine?.on();
              if (useVibrate &amp;&amp; navigator.vibrate) navigator.vibrate(unitMs);
              await sleep(unitMs);
              engine?.off();
            } else if (sym === &#34;-&#34;) {
              engine?.on();
              if (useVibrate &amp;&amp; navigator.vibrate) navigator.vibrate(unitMs * 3);
              await sleep(unitMs * 3);
              engine?.off();
            }
            // intra-symbol gap (1u) between parts of same letter
            if (s &lt; symbols.length - 1) await sleep(unitMs);
          }
          // letter gap: 3u (but we&#39;ve already waited 1u after last symbol? no)
          if (l &lt; letters.length - 1) await sleep(unitMs * 3);
        }
        // word gap: 7u
        if (w &lt; words.length - 1) await sleep(unitMs * 7);
      }
    } finally {
      engine?.stop();
    }
  }

  const START_BUTTON_SELECTOR = &#34;[data-morse-start]&#34;;
  const STOP_BUTTON_SELECTOR = &#34;[data-morse-stop]&#34;;
  const startButton = document.querySelector(START_BUTTON_SELECTOR);
  const stopButton = document.querySelector(STOP_BUTTON_SELECTOR);

  function setPlayingState(isPlaying) {
    if (startButton) startButton.disabled = isPlaying;
    if (stopButton) stopButton.disabled = !isPlaying;
  }

  // Controller so user can stop it
  let current = null;

  async function playMorse({
    unitMs = 90,
    useAudio = true,
    useVibrate = false,
    log = true
  } = {}) {
    const text = getMorseSourceText();

    if (!text) {
      console.warn(&#34;[morse] No morse source code block found.&#34;);
      return;
    }

    const morse = encodeMorse(text);

    // Abort previous run if any
    if (current?.controller) current.controller.abort();
    const controller = new AbortController();
    current = { controller };
    setPlayingState(true);

    try {
      await tapMorsePrecise(morse, { unitMs, useAudio, useVibrate, log, signal: controller.signal });
    } catch (e) {
      if (e?.name === &#34;AbortError&#34;) {
        if (log) console.log(&#34;[morse] stopped.&#34;);
        return;
      }
      throw e;
    } finally {
      setPlayingState(false);
    }
  }

  function morseStop() {
    if (current?.controller) current.controller.abort();
    setPlayingState(false);
  }

  const sourceText = renderMorseExample();
  if (!sourceText) {
    console.warn(&#34;[morse] Could not render morse example from code block.&#34;);
  }

  const inputEl = getMorseInputElement();
  if (inputEl) {
    inputEl.addEventListener(&#34;input&#34;, () =&gt; {
      renderMorseExample();
    });
  }

  setPlayingState(false);

  if (startButton) {
    startButton.addEventListener(&#34;click&#34;, () =&gt; {
      playMorse();
    });
  } else {
    console.warn(`[morse] Start button not found (${START_BUTTON_SELECTOR}).`);
  }

  if (stopButton) {
    stopButton.addEventListener(&#34;click&#34;, morseStop);
  } else {
    console.warn(`[morse] Stop button not found (${STOP_BUTTON_SELECTOR}).`);
  }
})();
&lt;/script&gt;

To make this I was going to need to look up the Morse code alphabet, and then use it as part of a script.

I was feeling lazy so I used an LLM to do that for me, and it ended up writing the whole thing, including turning it into sound, which I hadn&#39;t even asked it to do.

It didn&#39;t just make it all, in seconds, it ruined any fun I might have had experimenting on a new thing, maybe even learning a thing or two.

So I present it here begrudgingly.

---

I am using this note to test I can send an [RSVP](https://indieweb.org/rsvp).

If you see my name [on this list](https://events.indieweb.org/2026/03/homebrew-website-club-europe-london-0EMPkdPMzZKX#rsvps) then it worked.
</source:markdown></item><item>
      <title>Added support for photos and videos</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/17/added-support-for-photos-and-videos/</link>
      <pubDate>Tue, 17 Mar 2026 23:30:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/17/added-support-for-photos-and-videos/</guid>
      <category>meta</category>
      <description>Three days after posting Changes to the Site and it’s already out of date.
I wrote that I was going to optimise my photos so they would be smaller and I could include them in my posts, since that’s all I was planning to do.
Then I realised I needed a video in my post. If you’ve read Museum memories you’ll know why.</description>
      <content:encoded><![CDATA[<p>Three days after posting <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/14/changes-to-the-site/">Changes to the Site</a>
 and it&rsquo;s already out of date.</p>
<p>I wrote that I was going to optimise my photos so they would be smaller and I could include them in my posts, since that&rsquo;s all I was planning to do.</p>
<p>Then I realised I <em>needed</em> a video in my post. If you&rsquo;ve read <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/17/museum-memories">Museum memories</a>
 you&rsquo;ll know why.</p>
<h2 id="the-problem">The problem</h2>
<p>My website is powered by Hugo, a static site generator that takes an input, like my markdown files where I write my posts, and outputs a website.</p>
<p>I store both my site code and content in Git, which also drives my deployment process: every change triggers a new build and deploy.</p>
<p>This is where it gets tricky with media files.</p>
<h3 id="photos">Photos</h3>
<p>To include my own photos in a blog post I need to host them. I can do that in Hugo, but I would need to save the photos with Git so it&rsquo;s part of the site and the deployment process. The problem with this is photos can be quite large, so they bloat the Git repository.</p>
<p>This isn&rsquo;t really a problem with one blog post with a few photos, but it does become a problem when you do it over and over for many years.</p>
<p>I don&rsquo;t know if I&rsquo;ll be posting lots of photos, or doing it for many years, but I don&rsquo;t like the idea of one day having a scaling problem, I&rsquo;d rather fix it now.</p>
<h3 id="videos">Videos</h3>
<p>Videos are even more of a problem. They&rsquo;re much bigger than photos, and they don&rsquo;t compress down as effectively.</p>
<p>A 7 MB photo can compress down to 200 KB, which might be tolerable in a Git repo.</p>
<p>A 15 second video might be 30 MB and only compress down to 15 MB.</p>
<h2 id="a-solution">A solution</h2>
<p>Instead of keeping my photos and videos inside the Git repo I am moving towards separate media hosting for them. I wanted to publish <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/17/museum-memories/">my photo post</a>
 before fully solving media hosting, so I&rsquo;m using a temporary solution for now.</p>
<p>I have made a directory on my server and uploaded the videos there. I then changed my web server to serve the files in this directory at <code>https://paultibbetts.uk/media/</code> and updated my post to use this URL for videos.</p>
<p>You can see the change in infrastructure code <a href="https://github.com/paultibbetts/infra.paultibbetts.uk/commit/72240f724e786a189c2138d73a643c3b2ae4b4d9">on GitHub</a>
.</p>
<h2 id="its-temporary">It&rsquo;s Temporary™</h2>
<p>My web server has 50 GB of storage so there is a limit to how many photos and videos I can upload. This type of storage is relatively expensive and it&rsquo;s awkward changing the setup.</p>
<p>A better solution for media hosting would be a storage bucket and a Content Delivery Network to serve the files, as mentioned in <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/14/changes-to-the-site/">Changes to the Site</a>
.</p>
<p>I don&rsquo;t know when that&rsquo;s happening.</p>]]></content:encoded><source:markdown>
Three days after posting [Changes to the Site](/2026/03/14/changes-to-the-site/) and it&#39;s already out of date.

I wrote that I was going to optimise my photos so they would be smaller and I could include them in my posts, since that&#39;s all I was planning to do.

Then I realised I _needed_ a video in my post. If you&#39;ve read [Museum memories](/2026/03/17/museum-memories) you&#39;ll know why.

&lt;!--more--&gt;

## The problem

My website is powered by Hugo, a static site generator that takes an input, like my markdown files where I write my posts, and outputs a website.

I store both my site code and content in Git, which also drives my deployment process: every change triggers a new build and deploy.

This is where it gets tricky with media files.

### Photos

To include my own photos in a blog post I need to host them. I can do that in Hugo, but I would need to save the photos with Git so it&#39;s part of the site and the deployment process. The problem with this is photos can be quite large, so they bloat the Git repository.

This isn&#39;t really a problem with one blog post with a few photos, but it does become a problem when you do it over and over for many years.

I don&#39;t know if I&#39;ll be posting lots of photos, or doing it for many years, but I don&#39;t like the idea of one day having a scaling problem, I&#39;d rather fix it now.

### Videos

Videos are even more of a problem. They&#39;re much bigger than photos, and they don&#39;t compress down as effectively.

A 7 MB photo can compress down to 200 KB, which might be tolerable in a Git repo.

A 15 second video might be 30 MB and only compress down to 15 MB.

## A solution

Instead of keeping my photos and videos inside the Git repo I am moving towards separate media hosting for them. I wanted to publish [my photo post](/2026/03/17/museum-memories/) before fully solving media hosting, so I&#39;m using a temporary solution for now.

I have made a directory on my server and uploaded the videos there. I then changed my web server to serve the files in this directory at `https://paultibbetts.uk/media/` and updated my post to use this URL for videos.

You can see the change in infrastructure code [on GitHub](https://github.com/paultibbetts/infra.paultibbetts.uk/commit/72240f724e786a189c2138d73a643c3b2ae4b4d9).

## It&#39;s Temporary™

My web server has 50 GB of storage so there is a limit to how many photos and videos I can upload. This type of storage is relatively expensive and it&#39;s awkward changing the setup.

A better solution for media hosting would be a storage bucket and a Content Delivery Network to serve the files, as mentioned in [Changes to the Site](/2026/03/14/changes-to-the-site/).

I don&#39;t know when that&#39;s happening.
</source:markdown></item><item>
      <title>Museum memories</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/17/museum-memories/</link>
      <pubDate>Tue, 17 Mar 2026 14:00:22 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/17/museum-memories/</guid>
      <category>IndieWeb Carnival</category>
      <category>Barcelona</category>
      <description>This is my entry for this month’s IndieWeb Carnival: Museum memories.
I have not been to many museums or galleries. I am not a cultured person.
My favourite museum is the city of Barcelona.</description>
      <content:encoded><![CDATA[<p>This is my entry for this month&rsquo;s IndieWeb Carnival: <a class="u-in-reply-to" href="https://jamesg.blog/2026/03/01/indieweb-carnival-museum-memories/">Museum memories</a>.</p>
<p>I have not been to many museums or galleries. I am not a cultured person.</p>
<p>My favourite museum is the city of Barcelona.</p>
<h2 id="museums">Museums</h2>
<p>I grew up in Birmingham, UK, and I have been to a few museums there. We went on a school trip to the <a href="https://www.birminghammuseums.org.uk/thinktank">Science Museum</a>
 and I&rsquo;m pretty sure I&rsquo;ve been down to London to go to the Natural History Museum.</p>
<p>They&rsquo;re all great.</p>
<p>My favourite museum trips were all in Barcelona, and there must be something to that city because out of the three entries to this month&rsquo;s carnival I&rsquo;ve read there&rsquo;s already <a href="https://jeffbridgforth.com/two-museum-memories/">one about a museum there</a>
.</p>
<h2 id="barcelona">Barcelona</h2>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/barcelona.jpg"
        width="1200"
        height="800"
        alt="Barcelona&#39;s Gothic Quarter with ornate balconies and Sagrada Família visible in the distance"
        loading="lazy"
        decoding="async"
        class="w-full h-auto rounded-lg"
      />
      <figcaption>not my photo</figcaption>
    </figure>
<p>I first went in 2017, staying for a week in a hotel in Eixample, a giant extension to the city that was started in the 1860s.</p>
<p>The Old Quarter, the historic core, dates back over 2,000 years. It was founded by the Romans as Barcino and some of their walls are still standing.</p>
<p>The development of other parts of the city, like Barri Gòtic and El Born, happened during the medieval period and were built right on top of what came before.</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/old_wall.jpeg"
      width="1200"
      height="1600"
      alt="Ancient stone foundations and walls preserved beneath buildings in Barcelona"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<p>It&rsquo;s chaotic and organic.</p>
<h3 id="eixample">Eixample</h3>
<p>Eixample, known in English as &ldquo;extension&rdquo;, is the complete opposite. It was designed as a uniform grid layout, and turned out to be very forward-thinking design.</p>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/eixample.jpg"
        width="1200"
        height="827"
        alt="Aerial view of Barcelona&#39;s Eixample district showing its distinctive grid layout with octagonal blocks"
        loading="lazy"
        decoding="async"
        class="w-full h-auto rounded-lg"
      />
      <figcaption>also not my photo</figcaption>
    </figure>
<p>The octagonal blocks give better visibility at intersections and make turning easier for vehicles, even ones we use now, which didn&rsquo;t exist when the design was first conceived.</p>
<h3 id="museums-1">Museums</h3>
<p>I mention all of this because the city itself is a giant museum. You can step back in time, in design, and urban planning, and wander through the narrow cobbled streets of the Old Quarter, then walk ten minutes to Eixample to see an extension, started two hundred years ago, that&rsquo;s still celebrated for how modern it is.</p>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/museum.jpeg"
        width="1200"
        height="900"
        alt="Museum"
        loading="lazy"
        decoding="async"
        class="rounded-lg"
      />
      <figcaption>Castle of the Three Dragons</figcaption>
    </figure>
<p>Barcelona has free museum visits on Sundays (in most city-run museums), with free entry from 3pm onwards and all day on the first Sunday of the month.</p>
<p>I made a second trip later in 2017 for a month, and ended up staying six weeks. I visited most of <a href="https://www.barcelonaturisme.com/wv3/en/enjoy/25/a-zero-cost-cultural-afternoon.html">the free museums</a>
 whilst I was there,</p>
<p>as well as Castell de Montjuïc, the fort on the hill.</p>
<div class="not-prose image-grid my-6">
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/fort_garden.jpeg"
      width="1200"
      height="900"
      alt="Gardens and stone walls at Castell de Montjuïc overlooking Barcelona"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />

<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/fort_entrance.jpeg"
      width="1200"
      height="900"
      alt="Entrance to Castell de Montjuïc with stone walls and walkway"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
</div>

<p>It was interesting to see how it was being used.</p>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/fort_inside.jpeg"
        width="1200"
        height="675"
        alt="Interior courtyard of Castell de Montjuïc with white flags showing messages of peace"
        loading="lazy"
        decoding="async"
        class="not-prose wide-photo rounded-lg"
      />
      <figcaption><blockquote>Do economic and military interests help educate for peace?</blockquote></figcaption>
    </figure>
<p>Barcelona does a really good job of preserving layers of its past, from Castell de Montjuïc, to Roman walls in the Gothic Quarter and entire neighbourhoods from the 1700s, like those uncovered in El Born.</p>
<p>These are curated spaces that preserve and display the history of the city itself.</p>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/old_walls.jpeg"
        width="1200"
        height="1600"
        alt="Preserved walls from the 18th century displayed under a covered walkway"
        loading="lazy"
        decoding="async"
        class="rounded-lg"
      />
      <figcaption>El Born Centre de Cultura i Memòria</figcaption>
    </figure>
<p>I don&rsquo;t have a favourite museum in Barcelona. Barcelona is my favourite museum.</p>
<h3 id="galleries">Galleries</h3>
<p>It&rsquo;s also a giant street art gallery.</p>
<p>The residents communicate with posters, stickers and slogans:</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/street_art.jpeg"
      width="1200"
      height="1600"
      alt="Street posters and stickers covering a wall"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<p>To deter graffiti many property owners commission work for their shutters:</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/painted_door.jpeg"
      width="1200"
      height="900"
      alt="Colourful mural painted on a metal shop shutter"
      loading="lazy"
      decoding="async"
      class="col-span-2 w-full h-auto rounded-lg"
    />
<div class="not-prose image-grid my-6">
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/painted_door_bird.jpeg"
      width="1200"
      height="1600"
      alt="Street art mural of a bird painted on a shop shutter"
      loading="lazy"
      decoding="async"
      class="w-full h-auto rounded-lg"
    />

<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/painted_door_woman.jpeg"
      width="1200"
      height="1600"
      alt="Street art mural of a woman painted on a shop shutter"
      loading="lazy"
      decoding="async"
      class="w-full h-auto rounded-lg"
    />
</div>

<p>which is respected and left untouched.</p>
<p>The streets become a gallery, with guides offering tours of their own curated favourites.</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/gato.jpeg"
      width="1200"
      height="900"
      alt="Street art of 2 cat heads formed into one"
      loading="lazy"
      decoding="async"
      class="w-full h-auto rounded-lg"
    />
<h2 id="architecture">Architecture</h2>
<p>Speaking of streets, some of the best views are of the architecture.</p>
<p>Whether it&rsquo;s an open public space…</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/parque.jpeg"
      width="1200"
      height="960"
      alt="Open public space around the Arc de Triomf"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<p>or one of the many gold statues…</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/park.jpeg"
      width="1200"
      height="960"
      alt="Ornate golden statue in a Barcelona park surrounded by greenery"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<p>or an ornate entrance to a church…</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/sagrada_familia_entrance.jpeg"
      width="1200"
      height="900"
      alt="Highly detailed sculpted entrance of the Sagrada Família basilica"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<p>it&rsquo;s hard not to admire the intricate details and extravagance of Barcelona.</p>
<p>A lot of that is the result of Antoni Gaudí, a Catalan architect and designer, whose designs stretched from parks and gardens:</p>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/fantasy_house.jpeg"
        width="1200"
        height="1200"
        alt="Colourful Gaudí-designed house in Park Güell"
        loading="lazy"
        decoding="async"
        class="rounded-lg"
      />
      <figcaption>Park Güell</figcaption>
    </figure>
<p>to one of the most ambitious Catholic churches in the world, started 143 years ago and still under construction:</p>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/sagrada_familia.jpeg"
        width="1200"
        height="1600"
        alt="The towering spires of the Sagrada Família basilica in Barcelona"
        loading="lazy"
        decoding="async"
        class="rounded-lg"
      />
      <figcaption>Sagrada Família</figcaption>
    </figure>
<h2 id="culture">Culture</h2>
<p>Perhaps the biggest highlight of my first trip was its timing. I&rsquo;d accidentally arrived during La Mercè, Barcelona&rsquo;s annual festival that&rsquo;s been going on since 1871.</p>
<p>Every corner of the city was filled with art and music…</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/music_show.jpeg"
      width="1200"
      height="960"
      alt="Outdoor music performance during the La Mercè festival in Barcelona"
      loading="lazy"
      decoding="async"
      class="wide-photo rounded-lg"
    />
<p>as well as papier-mâché giants and castells (human pyramids, or &ldquo;castles&rdquo;)…</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/la_merce.jpeg"
      width="1200"
      height="675"
      alt="Human pyramid and giant papier-mâché figures during La Mercè festival"
      loading="lazy"
      decoding="async"
      class="wide-photo rounded-lg"
    />
<p>and the wildest fireworks show…</p>
<video controls playsinline preload="none" poster="https://paultibbetts.uk/media/2026/03/17/museum-memories/fireworks_poster.jpeg" width="1200" height="675">
  <source src="https://paultibbetts.uk/media/2026/03/17/museum-memories/fireworks.webm" type="video/webm">
  <a href="https://paultibbetts.uk/media/2026/03/17/museum-memories/fireworks.webm">Open the fireworks video directly</a>.
</video>
<p>I&rsquo;ve ever been to…</p>
<video controls playsinline preload="none" poster="https://paultibbetts.uk/media/2026/03/17/museum-memories/more_fireworks_poster.jpeg" width="1200" height="675">
  <source src="https://paultibbetts.uk/media/2026/03/17/museum-memories/more_fireworks.webm" type="video/webm">
  <a href="https://paultibbetts.uk/media/2026/03/17/museum-memories/more_fireworks.webm">Open the second fireworks video directly</a>.
</video>
<h2 id="2017">2017</h2>
<p>What made 2017 special was the result of hundreds of years of history - it marked the first independence referendum hosted by the Catalan government.</p>
<figure>
      <img
        src="https://paultibbetts.uk/media/2026/03/17/museum-memories/democracy_month.jpeg"
        width="1200"
        height="900"
        alt="Poster reading &#39;Democracy Month&#39; displayed during Catalonia&#39;s independence movement"
        loading="lazy"
        decoding="async"
        class="rounded-lg"
      />
      <figcaption>democracy month</figcaption>
    </figure>
<p>The city was covered in flags, and signs that said &ldquo;Si&rdquo; - which meant yes, if given the chance to vote.</p>
<div class="not-prose image-grid my-6">
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/want_to_vote.jpeg"
      width="900"
      height="720"
      alt="People surround a poster that reads &#39;We want to vote&#39;"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/wants_to_vote.jpeg"
      width="1200"
      height="960"
      alt="Poster on a balcony that reads &#39;Catalonia wants to vote&#39;"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
</div>

<p>There were plenty of Si supporters…</p>
<img
      src="https://paultibbetts.uk/media/2026/03/17/museum-memories/flags_parade.jpeg"
      width="1200"
      height="675"
      alt="Lots of Catalan flags"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<p>They once interrupted a conversation I was having, and it was the highlight of my trip.</p>
<p>At, I think it was, 6pm, supporters of the vote leaned out their windows and started smashing pots and pans. It was a DIY siren to let everyone else know they existed, and that they wanted to vote.</p>
<p>It was deafeningly loud and an extremely effective way of communicating.</p>
<h2 id="history">History</h2>
<p>I&rsquo;m no expert, but a brief history is:</p>
<ul>
<li>in the Middle Ages, Catalonia was part of the Crown of Aragon</li>
<li>the War of Spanish Succession (1701-1714) saw Catalonia become part of Spain</li>
<li>the dictatorship of Francisco Franco restricted Catalan language and culture from 1939 to 1975</li>
<li>in 2006 Catalonia negotiated a new agreement with Spain</li>
<li>Spain&rsquo;s conservative party opposed it and it went to court for 4 years</li>
<li>in 2010 the court ruled that parts of the agreement should be annulled</li>
<li>Catalans saw the agreement as settled and not to be changed</li>
<li>this sparked a resurgence in the independence movement</li>
</ul>
<p>which led to the referendum in 2017.</p>
<h2 id="the-result">The result</h2>
<p>The Catalan government claimed that 90% of voters voted &ldquo;Yes&rdquo;, however they also recognised that it had only a 42% turnout.</p>
<p>Because of this, Spain ignored the result and then called it illegal anyway.</p>
<p>A month later Catalonia&rsquo;s parliament declared independence.</p>
<p>The next day Spain dissolved the Catalan government, arrested several of its leaders, and the regional president Carles Puigdemont fled to Belgium.</p>
<h2 id="since-then">Since then</h2>
<p>Unfortunately, since then, the only notable things to have happened are trials and prison sentences followed by a new amnesty law that pardons some of those involved in the push for independence.</p>
<p>I wish this had a better ending, and that the Catalan people were given a legal vote to decide what they want to do. I don&rsquo;t know if independence is a good or bad thing for Catalonia, I&rsquo;m just a tourist with an admiration for its capital.</p>
<h2 id="memories">Memories</h2>
<p>I met a Spanish man on a hiking trail once and tried to tell him how nice Barcelona is. It turned out he did not want Catalonia to become independent, so he didn&rsquo;t want to talk to me, and he definitely didn&rsquo;t want to see my photos.</p>
<p>Thanks for the IndieWeb Carnival prompt <a href="https://jamesg.blog">James</a>
. It gave me an excuse to put this on my blog and add support for photos and videos.</p>
<p>And it was good to reminisce.</p>]]></content:encoded><source:markdown>
This is my entry for this month&#39;s IndieWeb Carnival: &lt;a class=&#34;u-in-reply-to&#34; href=&#34;https://jamesg.blog/2026/03/01/indieweb-carnival-museum-memories/&#34;&gt;Museum memories&lt;/a&gt;.

I have not been to many museums or galleries. I am not a cultured person.

My favourite museum is the city of Barcelona.

&lt;!--more--&gt;

## Museums

I grew up in Birmingham, UK, and I have been to a few museums there. We went on a school trip to the [Science Museum](https://www.birminghammuseums.org.uk/thinktank) and I&#39;m pretty sure I&#39;ve been down to London to go to the Natural History Museum.

They&#39;re all great.

My favourite museum trips were all in Barcelona, and there must be something to that city because out of the three entries to this month&#39;s carnival I&#39;ve read there&#39;s already [one about a museum there](https://jeffbridgforth.com/two-museum-memories/).

## Barcelona

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/barcelona.jpg&#34; alt=&#34;Barcelona&#39;s Gothic Quarter with ornate balconies and Sagrada Família visible in the distance&#34; class=&#34;w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;800&#34; figcaption=&#34;not my photo&#34; &gt;}}

I first went in 2017, staying for a week in a hotel in Eixample, a giant extension to the city that was started in the 1860s.

The Old Quarter, the historic core, dates back over 2,000 years. It was founded by the Romans as Barcino and some of their walls are still standing.

The development of other parts of the city, like Barri Gòtic and El Born, happened during the medieval period and were built right on top of what came before.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/old_wall.jpeg&#34; alt=&#34;Ancient stone foundations and walls preserved beneath buildings in Barcelona&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;1600&#34; &gt;}}

It&#39;s chaotic and organic.

### Eixample

Eixample, known in English as &#34;extension&#34;, is the complete opposite. It was designed as a uniform grid layout, and turned out to be very forward-thinking design.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/eixample.jpg&#34; alt=&#34;Aerial view of Barcelona&#39;s Eixample district showing its distinctive grid layout with octagonal blocks&#34; class=&#34;w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;827&#34; figcaption=&#34;also not my photo&#34; &gt;}}

The octagonal blocks give better visibility at intersections and make turning easier for vehicles, even ones we use now, which didn&#39;t exist when the design was first conceived.

### Museums

I mention all of this because the city itself is a giant museum. You can step back in time, in design, and urban planning, and wander through the narrow cobbled streets of the Old Quarter, then walk ten minutes to Eixample to see an extension, started two hundred years ago, that&#39;s still celebrated for how modern it is.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/museum.jpeg&#34; alt=&#34;Museum&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;900&#34; figcaption=&#34;Castle of the Three Dragons&#34; &gt;}}

Barcelona has free museum visits on Sundays (in most city-run museums), with free entry from 3pm onwards and all day on the first Sunday of the month.

I made a second trip later in 2017 for a month, and ended up staying six weeks. I visited most of [the free museums](https://www.barcelonaturisme.com/wv3/en/enjoy/25/a-zero-cost-cultural-afternoon.html) whilst I was there,

as well as Castell de Montjuïc, the fort on the hill.

{{&lt; image_grid &gt;}}
{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/fort_garden.jpeg&#34; alt=&#34;Gardens and stone walls at Castell de Montjuïc overlooking Barcelona&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;900&#34; &gt;}}

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/fort_entrance.jpeg&#34; alt=&#34;Entrance to Castell de Montjuïc with stone walls and walkway&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;900&#34; &gt;}}
{{&lt; /image_grid &gt;}}

It was interesting to see how it was being used.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/fort_inside.jpeg&#34; alt=&#34;Interior courtyard of Castell de Montjuïc with white flags showing messages of peace&#34; class=&#34;not-prose wide-photo rounded-lg&#34; width=&#34;1200&#34; height=&#34;675&#34; figcaption=&#34;&lt;blockquote&gt;Do economic and military interests help educate for peace?&lt;/blockquote&gt;&#34; &gt;}}

Barcelona does a really good job of preserving layers of its past, from Castell de Montjuïc, to Roman walls in the Gothic Quarter and entire neighbourhoods from the 1700s, like those uncovered in El Born.

These are curated spaces that preserve and display the history of the city itself.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/old_walls.jpeg&#34; alt=&#34;Preserved walls from the 18th century displayed under a covered walkway&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;1600&#34; figcaption=&#34;El Born Centre de Cultura i Memòria&#34; &gt;}}

I don&#39;t have a favourite museum in Barcelona. Barcelona is my favourite museum.

### Galleries

It&#39;s also a giant street art gallery.

The residents communicate with posters, stickers and slogans:

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/street_art.jpeg&#34; alt=&#34;Street posters and stickers covering a wall&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;1600&#34; &gt;}}

To deter graffiti many property owners commission work for their shutters:

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/painted_door.jpeg&#34; alt=&#34;Colourful mural painted on a metal shop shutter&#34; class=&#34;col-span-2 w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;900&#34; &gt;}}

{{&lt; image_grid &gt;}}
{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/painted_door_bird.jpeg&#34; alt=&#34;Street art mural of a bird painted on a shop shutter&#34; class=&#34;w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;1600&#34; &gt;}}

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/painted_door_woman.jpeg&#34; alt=&#34;Street art mural of a woman painted on a shop shutter&#34; class=&#34;w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;1600&#34; &gt;}}
{{&lt; /image_grid &gt;}}

which is respected and left untouched.

The streets become a gallery, with guides offering tours of their own curated favourites.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/gato.jpeg&#34; alt=&#34;Street art of 2 cat heads formed into one&#34; class=&#34;w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;900&#34; &gt;}}

## Architecture

Speaking of streets, some of the best views are of the architecture.

Whether it&#39;s an open public space…

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/parque.jpeg&#34; alt=&#34;Open public space around the Arc de Triomf&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;960&#34; &gt;}}

or one of the many gold statues…

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/park.jpeg&#34; alt=&#34;Ornate golden statue in a Barcelona park surrounded by greenery&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;960&#34; &gt;}}

or an ornate entrance to a church…

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/sagrada_familia_entrance.jpeg&#34; alt=&#34;Highly detailed sculpted entrance of the Sagrada Família basilica&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;900&#34; &gt;}}

it&#39;s hard not to admire the intricate details and extravagance of Barcelona.

A lot of that is the result of Antoni Gaudí, a Catalan architect and designer, whose designs stretched from parks and gardens:

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/fantasy_house.jpeg&#34; alt=&#34;Colourful Gaudí-designed house in Park Güell&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;1200&#34; figcaption=&#34;Park Güell&#34; &gt;}}

to one of the most ambitious Catholic churches in the world, started 143 years ago and still under construction:

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/sagrada_familia.jpeg&#34; alt=&#34;The towering spires of the Sagrada Família basilica in Barcelona&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;1600&#34; figcaption=&#34;Sagrada Família&#34; &gt;}}

## Culture

Perhaps the biggest highlight of my first trip was its timing. I&#39;d accidentally arrived during La Mercè, Barcelona&#39;s annual festival that&#39;s been going on since 1871.

Every corner of the city was filled with art and music…

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/music_show.jpeg&#34; alt=&#34;Outdoor music performance during the La Mercè festival in Barcelona&#34; class=&#34;wide-photo rounded-lg&#34; width=&#34;1200&#34; height=&#34;960&#34; &gt;}}

as well as papier-mâché giants and castells (human pyramids, or &#34;castles&#34;)…

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/la_merce.jpeg&#34; alt=&#34;Human pyramid and giant papier-mâché figures during La Mercè festival&#34; class=&#34;wide-photo rounded-lg&#34; width=&#34;1200&#34; height=&#34;675&#34; &gt;}}

and the wildest fireworks show…

&lt;video controls playsinline preload=&#34;none&#34; poster=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/fireworks_poster.jpeg&#34; width=&#34;1200&#34; height=&#34;675&#34;&gt;
  &lt;source src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/fireworks.webm&#34; type=&#34;video/webm&#34;&gt;
  &lt;a href=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/fireworks.webm&#34;&gt;Open the fireworks video directly&lt;/a&gt;.
&lt;/video&gt;

I&#39;ve ever been to…

&lt;video controls playsinline preload=&#34;none&#34; poster=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/more_fireworks_poster.jpeg&#34; width=&#34;1200&#34; height=&#34;675&#34;&gt;
  &lt;source src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/more_fireworks.webm&#34; type=&#34;video/webm&#34;&gt;
  &lt;a href=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/more_fireworks.webm&#34;&gt;Open the second fireworks video directly&lt;/a&gt;.
&lt;/video&gt;

## 2017

What made 2017 special was the result of hundreds of years of history - it marked the first independence referendum hosted by the Catalan government.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/democracy_month.jpeg&#34; alt=&#34;Poster reading &#39;Democracy Month&#39; displayed during Catalonia&#39;s independence movement&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;900&#34; figcaption=&#34;democracy month&#34; &gt;}}

The city was covered in flags, and signs that said &#34;Si&#34; - which meant yes, if given the chance to vote.

{{&lt; image_grid &gt;}}
{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/want_to_vote.jpeg&#34; alt=&#34;People surround a poster that reads &#39;We want to vote&#39;&#34; class=&#34;rounded-lg&#34; width=&#34;900&#34; height=&#34;720&#34; &gt;}}
{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/wants_to_vote.jpeg&#34; alt=&#34;Poster on a balcony that reads &#39;Catalonia wants to vote&#39;&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;960&#34; &gt;}}
{{&lt;/image_grid &gt;}}

There were plenty of Si supporters…

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/03/17/museum-memories/flags_parade.jpeg&#34; alt=&#34;Lots of Catalan flags&#34; class=&#34;rounded-lg&#34; width=&#34;1200&#34; height=&#34;675&#34; &gt;}}

They once interrupted a conversation I was having, and it was the highlight of my trip.

At, I think it was, 6pm, supporters of the vote leaned out their windows and started smashing pots and pans. It was a DIY siren to let everyone else know they existed, and that they wanted to vote.

It was deafeningly loud and an extremely effective way of communicating.

## History

I&#39;m no expert, but a brief history is:

- in the Middle Ages, Catalonia was part of the Crown of Aragon
- the War of Spanish Succession (1701-1714) saw Catalonia become part of Spain
- the dictatorship of Francisco Franco restricted Catalan language and culture from 1939 to 1975
- in 2006 Catalonia negotiated a new agreement with Spain
- Spain&#39;s conservative party opposed it and it went to court for 4 years
- in 2010 the court ruled that parts of the agreement should be annulled
- Catalans saw the agreement as settled and not to be changed
- this sparked a resurgence in the independence movement

which led to the referendum in 2017.

## The result

The Catalan government claimed that 90% of voters voted &#34;Yes&#34;, however they also recognised that it had only a 42% turnout.

Because of this, Spain ignored the result and then called it illegal anyway.

A month later Catalonia&#39;s parliament declared independence.

The next day Spain dissolved the Catalan government, arrested several of its leaders, and the regional president Carles Puigdemont fled to Belgium.

## Since then

Unfortunately, since then, the only notable things to have happened are trials and prison sentences followed by a new amnesty law that pardons some of those involved in the push for independence.

I wish this had a better ending, and that the Catalan people were given a legal vote to decide what they want to do. I don&#39;t know if independence is a good or bad thing for Catalonia, I&#39;m just a tourist with an admiration for its capital.

## Memories

I met a Spanish man on a hiking trail once and tried to tell him how nice Barcelona is. It turned out he did not want Catalonia to become independent, so he didn&#39;t want to talk to me, and he definitely didn&#39;t want to see my photos.

Thanks for the IndieWeb Carnival prompt [James](https://jamesg.blog). It gave me an excuse to put this on my blog and add support for photos and videos.

And it was good to reminisce.
</source:markdown></item><item>
      <title>Changes to the Site</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/14/changes-to-the-site/</link>
      <pubDate>Sat, 14 Mar 2026 23:07:25 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/14/changes-to-the-site/</guid>
      <category>meta</category>
      <description>I’ve made some recent changes to the site and have a few ideas for what comes next.
Changes:
Notes: shorter posts that don’t need titles Bookmarks and likes: a way to share links without writing full posts Replies: posts that are part of a conversation with another site Webmentions: now appear below posts Ideas:
serving media through a CDN dynamic image resizing with imgproxy publishing from my phone using micropub</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve made some recent changes to the site and have a few ideas for what comes next.</p>
<p>Changes:</p>
<ul>
<li>Notes: shorter posts that don&rsquo;t need titles</li>
<li>Bookmarks and likes: a way to share links without writing full posts</li>
<li>Replies: posts that are part of a conversation with another site</li>
<li>Webmentions: now appear below posts</li>
</ul>
<p>Ideas:</p>
<ul>
<li>serving media through a CDN</li>
<li>dynamic image resizing with imgproxy</li>
<li>publishing from my phone using micropub</li>
</ul>
<h2 id="notes">Notes</h2>
<p>My site now has <a href="https://indieweb.org/note">notes</a>
. They are still posts but I treat them a bit differently.</p>
<p>Notes don&rsquo;t need a title, and they&rsquo;re usually shorter, but they&rsquo;re effectively the same as the articles I&rsquo;ve already been posting. The real difference will be what I end up classing as an article instead of a note.</p>
<p>I added them because I wanted to post moments in time without needing to write several paragraphs like I would in an article. This post was nearly a note, until I realised it was more than just recent changes and that I could write about future ones as well.</p>
<p>I think I will probably end up writing less articles now I have notes but when I do write an article it should mean that it&rsquo;s taken more effort to write, and is therefore of higher quality, but I&rsquo;m not guaranteeing anything.</p>
<h2 id="bookmarks">Bookmarks</h2>
<p>I&rsquo;ve also added <a href="https://indieweb.org/bookmark">bookmarks</a>
, which I&rsquo;m going to use to share links to interesting things around the web.</p>
<p>These are notes with a <code>bookmark_of</code> field set to the URL I&rsquo;ve bookmarked. They are then displayed a little differently, with the link going to the bookmarked site instead of my own post.</p>
<pre><code class="language-html">&lt;a class=&#34;u-bookmark-of&#34; href=&#34;https://example.com/post&#34;&gt;Example&lt;/a&gt;</code></pre>
<p>Using a different post kind for this makes it easier for me to share links, since I don&rsquo;t need to write an article talking about it, or even a note saying what I like about it. I can just share the link.</p>
<h2 id="likes">Likes</h2>
<p>Another type of link I&rsquo;m going to share is a <a href="https://indieweb.org/like">like</a>
, which is both a reaction and me sharing something. I don&rsquo;t know if there&rsquo;s going to be a system for deciding whether something is a bookmark or a like so I will make it up as I go along.</p>
<pre><code class="language-html">&lt;a class=&#34;u-like-of&#34; href=&#34;https://example.com/post&#34;&gt;I liked this&lt;/a&gt;</code></pre>
<p>Because I don&rsquo;t know what the difference will end up being, and they&rsquo;re both just a link to something, I have also added a <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/links">/links</a>
 feed which includes both bookmarks and likes.</p>
<p>This will be my own curated version of the internet I want others to know about.</p>
<h2 id="webmentions">Webmentions</h2>
<p>Speaking of likes, I got my first webmention like! Thank you <a href="https://artlung.com/">Joe</a>
!</p>
<p>I was checking Google Search Console and found a page showing me what sites linked to mine when I saw Joe had <a href="https://artlung.com/likes/2f08886085fe293d93bce727c2f75051">liked one of my posts</a>
.</p>
<p>I run my own site because I enjoy doing so, but that interaction made it all seem worth it, and I want to post likes from my own site to share that with others going forward.</p>
<h3 id="webmentionio">webmention.io</h3>
<p>Joe was able to send me a like because I signed up to <a href="https://webmention.io">webmention.io</a>
, which now collects my <a href="https://indieweb.org/webmention/">webmentions</a>
 for me.</p>
<p>It also provides a feed showing all the webmentions my site has received, which I&rsquo;ve added to my reader so I&rsquo;m notified of new ones.</p>
<p>To display the webmentions on my site I&rsquo;m using <a href="https://github.com/PlaidWeb/webmention.js/">webmention.js</a>
 to show them below the post they are mentioning.</p>
<h2 id="replies">Replies</h2>
<p>The other post type I&rsquo;ve added recently is a <a href="https://indieweb.org/reply/">reply</a>
.</p>
<p>These are notes that are in response to someone else&rsquo;s post. I will use webmentions to tell them that I&rsquo;ve written it, after which they could display my reply as a comment.</p>
<p>This now means that to write about a link I could:</p>
<ul>
<li>write an article about it</li>
<li>share it as a bookmark</li>
<li>share it as a like</li>
<li>write a note about it</li>
<li>reply to it directly</li>
</ul>
<p>So to try and make a rule for them: replies are notes that are part of a conversation.</p>
<p>Where it gets confusing is that any type of post can also be a reply. I could publish a post as an article, and then include a line like</p>
<pre><code class="language-html">my entry for
&lt;a class=&#34;u-in-reply-to&#34; href=&#34;https://carnival.host/prompt.html&#34;&gt;this month&#39;s IndieWeb Carnival&lt;/a&gt;</code></pre>
<p>and the post would be both an article and a reply.</p>
<p>If the site I&rsquo;m replying to displayed my article in full, it might be as long as their original post. Because of that they might show only an excerpt, or just link to it instead.</p>
<p>So, for long articles that are technically also replies I will have to make sure I write a shorter summary that works well as a comment, which could be displayed in full.</p>
<p>The replies I write using the reply post type will be written as I want them to appear in the conversation.</p>
<h2 id="planned-changes">Planned changes</h2>
<p>My entry for this month&rsquo;s <a href="https://indieweb.org/IndieWeb_Carnival/">IndieWeb Carnival</a>
 is taking longer than I thought because it&rsquo;s turned into a holiday photo dump.</p>
<p>This has had me questioning my setup, which is annoying because I thought it would last a bit longer before it needed changing.</p>
<img src="whiteboard.png" title="whiteboard of ideas for the site">
<p>I have not even finished my post about <a href="https://infra.paultibbetts.uk/">infra.paultibbetts.uk</a>
, which documents my infrastructure, and I am already thinking of changing it.</p>
<p>I do enjoy working on my site, maybe even more than actually using it, but I have other things I should be working on.</p>
<p>I don&rsquo;t know when the following will get done.</p>
<h2 id="media">Media</h2>
<p>My draft Carnival post was starting to include photos from my phone, which were about 7 MB each. I hadn&rsquo;t thought about photos before and didn&rsquo;t have a process for them.</p>
<p>My site uses git for version control, which is great for text like code and content but it starts to get bloated when you fill it with 7 MB photos. I started thinking up a better system for this, which is coming up next, until I realised I could optimise my images before saving them in git.</p>
<p>The solution for now is to run a script that replaces the original image with a compressed version. The 7 MB photos shrink down to ~200 kB. I am still bloating my repo with images, but at a much more tolerable size.</p>
<h2 id="cdn">CDN</h2>
<p>A better solution is to store my photos separately, which lets me keep the repo slim by tracking only text files.</p>
<p>I could use <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/">my Raspberry Pi</a>
 to host the photos, but it uses Network File Storage which is a bit slow and has only 50 GB of storage.</p>
<p>Instead I&rsquo;m thinking of using a storage bucket, which could grow to any size, and then using a Content Delivery Network to serve the assets.</p>
<h3 id="edit">Edit</h3>
<p>I needed a temporary solution, so in <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/17/added-support-for-photos-and-videos/">Added support for photos and videos</a>
 I explain how I&rsquo;ve set up my Raspberry Pi to host my media files until I get a storage bucket and CDN sorted out.</p>
<h2 id="dynamic-image-resizing">Dynamic Image Resizing</h2>
<p>I might also use something like <a href="https://imgproxy.net/">imgproxy</a>
 to dynamically resize images.</p>
<p>When a user requests a smaller version of a photo, imgproxy would generate it on demand. That resized image would then be cached by the CDN, so future visitors can receive the optimised version immediately.</p>
<p>This <em>might</em> mean I can stop compressing photos manually and just use the originals.</p>
<h2 id="micropub">Micropub</h2>
<p>The final change to my site, which will really elevate it from a simple blog to a personal publishing system, is using <a href="https://indieweb.org/micropub">micropub</a>
 to let me post from my phone.</p>
<p>This would need hosting and hooking up to my git repo to manage my content. I would have the same problem as I do right now if I used it for photos, so I will need to wait until I have the storage bucket and CDN sorted before tackling this.</p>
<p>I&rsquo;m considering using <a href="https://getindiekit.com/">Indiekit</a>
 as my micropub server, as it works with Hugo (which powers my site), and can also <a href="https://indieweb.org/cross-posting">syndicate</a>
 my posts out to social networks. It doesn&rsquo;t look like it has a Micro.blog plugin yet, so I&rsquo;d need to write one before I can selectively choose which posts to syndicate there.</p>
<h2 id="microblog">Micro.blog</h2>
<p>Which leaves me with a site that can do what I currently use my <a href="https://micro.blog">Micro.blog</a>
 <a href="https://micro.paultibbetts.uk/">microblog</a>
 for. I want to stay as part of the Micro.blog community, and will continue using their apps, but I can see myself migrating the hosting of my content to my own site in the future.</p>
<p>I could merge my Micro.blog posts into the notes on my own site and then syndicate future posts to the Micro.blog timeline, and for Micro.blog users nothing will seem to have changed, only where my content is hosted.</p>
<p>My feed might even get better, as I could filter out posts that I know the Micro.blog community wouldn&rsquo;t be interested in.</p>
<p>This isn&rsquo;t a remark against Micro.blog by the way - I think it&rsquo;s an excellent service - I just believe that I was destined to outgrow it, since I always intended on working on my own site. I think of it more as graduating as a socially-capable indie website, and because Micro.blog is part of the indieweb I can stay part of its community.</p>
<p>I would be missing out on lots of features that Micro.blog has that I don&rsquo;t yet support, so it wouldn&rsquo;t be a seamless transition, but after implementing all the above ideas my site should be capable of hosting all of my post kinds in one place.</p>]]></content:encoded><source:markdown>
I&#39;ve made some recent changes to the site and have a few ideas for what comes next.

Changes:

- Notes: shorter posts that don&#39;t need titles
- Bookmarks and likes: a way to share links without writing full posts
- Replies: posts that are part of a conversation with another site
- Webmentions: now appear below posts

Ideas:

- serving media through a CDN
- dynamic image resizing with imgproxy
- publishing from my phone using micropub

&lt;!--more--&gt;

## Notes

My site now has [notes](https://indieweb.org/note). They are still posts but I treat them a bit differently.

Notes don&#39;t need a title, and they&#39;re usually shorter, but they&#39;re effectively the same as the articles I&#39;ve already been posting. The real difference will be what I end up classing as an article instead of a note.

I added them because I wanted to post moments in time without needing to write several paragraphs like I would in an article. This post was nearly a note, until I realised it was more than just recent changes and that I could write about future ones as well.

I think I will probably end up writing less articles now I have notes but when I do write an article it should mean that it&#39;s taken more effort to write, and is therefore of higher quality, but I&#39;m not guaranteeing anything.

## Bookmarks

I&#39;ve also added [bookmarks](https://indieweb.org/bookmark), which I&#39;m going to use to share links to interesting things around the web.

These are notes with a `bookmark_of` field set to the URL I&#39;ve bookmarked. They are then displayed a little differently, with the link going to the bookmarked site instead of my own post.

```html
&lt;a class=&#34;u-bookmark-of&#34; href=&#34;https://example.com/post&#34;&gt;Example&lt;/a&gt;
```

Using a different post kind for this makes it easier for me to share links, since I don&#39;t need to write an article talking about it, or even a note saying what I like about it. I can just share the link.

## Likes

Another type of link I&#39;m going to share is a [like](https://indieweb.org/like), which is both a reaction and me sharing something. I don&#39;t know if there&#39;s going to be a system for deciding whether something is a bookmark or a like so I will make it up as I go along.

```html
&lt;a class=&#34;u-like-of&#34; href=&#34;https://example.com/post&#34;&gt;I liked this&lt;/a&gt;
```

Because I don&#39;t know what the difference will end up being, and they&#39;re both just a link to something, I have also added a [/links](/links) feed which includes both bookmarks and likes.

This will be my own curated version of the internet I want others to know about.

## Webmentions

Speaking of likes, I got my first webmention like! Thank you [Joe](https://artlung.com/)!

I was checking Google Search Console and found a page showing me what sites linked to mine when I saw Joe had [liked one of my posts](https://artlung.com/likes/2f08886085fe293d93bce727c2f75051).

I run my own site because I enjoy doing so, but that interaction made it all seem worth it, and I want to post likes from my own site to share that with others going forward.

### webmention.io

Joe was able to send me a like because I signed up to [webmention.io](https://webmention.io), which now collects my [webmentions](https://indieweb.org/webmention/) for me.

It also provides a feed showing all the webmentions my site has received, which I&#39;ve added to my reader so I&#39;m notified of new ones.

To display the webmentions on my site I&#39;m using [webmention.js](https://github.com/PlaidWeb/webmention.js/) to show them below the post they are mentioning.

## Replies

The other post type I&#39;ve added recently is a [reply](https://indieweb.org/reply/).

These are notes that are in response to someone else&#39;s post. I will use webmentions to tell them that I&#39;ve written it, after which they could display my reply as a comment.

This now means that to write about a link I could:

- write an article about it
- share it as a bookmark
- share it as a like
- write a note about it
- reply to it directly

So to try and make a rule for them: replies are notes that are part of a conversation.

Where it gets confusing is that any type of post can also be a reply. I could publish a post as an article, and then include a line like

```html
my entry for
&lt;a class=&#34;u-in-reply-to&#34; href=&#34;https://carnival.host/prompt.html&#34;&gt;this month&#39;s IndieWeb Carnival&lt;/a&gt;
```

and the post would be both an article and a reply.

If the site I&#39;m replying to displayed my article in full, it might be as long as their original post. Because of that they might show only an excerpt, or just link to it instead.

So, for long articles that are technically also replies I will have to make sure I write a shorter summary that works well as a comment, which could be displayed in full.

The replies I write using the reply post type will be written as I want them to appear in the conversation.

## Planned changes

My entry for this month&#39;s [IndieWeb Carnival](https://indieweb.org/IndieWeb_Carnival/) is taking longer than I thought because it&#39;s turned into a holiday photo dump.

This has had me questioning my setup, which is annoying because I thought it would last a bit longer before it needed changing.

&lt;img src=&#34;whiteboard.png&#34; title=&#34;whiteboard of ideas for the site&#34;&gt;

I have not even finished my post about [infra.paultibbetts.uk](https://infra.paultibbetts.uk/), which documents my infrastructure, and I am already thinking of changing it.

I do enjoy working on my site, maybe even more than actually using it, but I have other things I should be working on.

I don&#39;t know when the following will get done.

## Media

My draft Carnival post was starting to include photos from my phone, which were about 7 MB each. I hadn&#39;t thought about photos before and didn&#39;t have a process for them.

My site uses git for version control, which is great for text like code and content but it starts to get bloated when you fill it with 7 MB photos. I started thinking up a better system for this, which is coming up next, until I realised I could optimise my images before saving them in git.

The solution for now is to run a script that replaces the original image with a compressed version. The 7 MB photos shrink down to ~200 kB. I am still bloating my repo with images, but at a much more tolerable size.

## CDN

A better solution is to store my photos separately, which lets me keep the repo slim by tracking only text files.

I could use [my Raspberry Pi](/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/) to host the photos, but it uses Network File Storage which is a bit slow and has only 50 GB of storage.

Instead I&#39;m thinking of using a storage bucket, which could grow to any size, and then using a Content Delivery Network to serve the assets.

### Edit

I needed a temporary solution, so in [Added support for photos and videos](/2026/03/17/added-support-for-photos-and-videos/) I explain how I&#39;ve set up my Raspberry Pi to host my media files until I get a storage bucket and CDN sorted out.

## Dynamic Image Resizing

I might also use something like [imgproxy](https://imgproxy.net/) to dynamically resize images.

When a user requests a smaller version of a photo, imgproxy would generate it on demand. That resized image would then be cached by the CDN, so future visitors can receive the optimised version immediately.

This _might_ mean I can stop compressing photos manually and just use the originals.

## Micropub

The final change to my site, which will really elevate it from a simple blog to a personal publishing system, is using [micropub](https://indieweb.org/micropub) to let me post from my phone.

This would need hosting and hooking up to my git repo to manage my content. I would have the same problem as I do right now if I used it for photos, so I will need to wait until I have the storage bucket and CDN sorted before tackling this.

I&#39;m considering using [Indiekit](https://getindiekit.com/) as my micropub server, as it works with Hugo (which powers my site), and can also [syndicate](https://indieweb.org/cross-posting) my posts out to social networks. It doesn&#39;t look like it has a Micro.blog plugin yet, so I&#39;d need to write one before I can selectively choose which posts to syndicate there.

## Micro.blog

Which leaves me with a site that can do what I currently use my [Micro.blog](https://micro.blog) [microblog](https://micro.paultibbetts.uk/) for. I want to stay as part of the Micro.blog community, and will continue using their apps, but I can see myself migrating the hosting of my content to my own site in the future.

I could merge my Micro.blog posts into the notes on my own site and then syndicate future posts to the Micro.blog timeline, and for Micro.blog users nothing will seem to have changed, only where my content is hosted.

My feed might even get better, as I could filter out posts that I know the Micro.blog community wouldn&#39;t be interested in.

This isn&#39;t a remark against Micro.blog by the way - I think it&#39;s an excellent service - I just believe that I was destined to outgrow it, since I always intended on working on my own site. I think of it more as graduating as a socially-capable indie website, and because Micro.blog is part of the indieweb I can stay part of its community.

I would be missing out on lots of features that Micro.blog has that I don&#39;t yet support, so it wouldn&#39;t be a seamless transition, but after implementing all the above ideas my site should be capable of hosting all of my post kinds in one place.
</source:markdown></item><item>
      <title>Liked: IndieWeb.org Principles</title>
      <link>https://indieweb.org/principles</link>
      <pubDate>Wed, 04 Mar 2026 17:00:44 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/04/2026-03-04-principles-indieweb/</guid>
      <category>IndieWeb</category>
      <description></description>
      <content:encoded><![CDATA[]]></content:encoded></item><item>
      <title>Bookmarked: A Website to End All Websites</title>
      <link>https://henry.codes/writing/a-website-to-destroy-all-websites/</link>
      <pubDate>Wed, 04 Mar 2026 14:19:08 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/04/2026-03-04-a-website-to-end-all-websites-henry-from-online/</guid>
      <category>IndieWeb</category>
      <description>[…] the path back to feeling like you have some control is to un-spin yourself from the Five Apps of the Apocalypse and reclaim the Internet as a set of tools you use to build something you can own &amp;amp; be proud of</description>
      <content:encoded><![CDATA[<blockquote>
<p>[&hellip;] the path back to feeling like you have some control is to un-spin yourself from the Five Apps of the Apocalypse and reclaim the Internet as a set of tools you use to build something you can own &amp; be proud of</p>
</blockquote>]]></content:encoded><source:markdown>
&gt; [...] the path back to feeling like you have some control is to un-spin yourself from the Five Apps of the Apocalypse and reclaim the Internet as a set of tools you use to build something you can own &amp; be proud of
</source:markdown></item><item>
      <title>I Made a Terraform Provider for Mythic Beasts</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/03/i-made-a-terraform-provider-for-mythic-beasts/</link>
      <pubDate>Tue, 03 Mar 2026 12:00:17 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/03/i-made-a-terraform-provider-for-mythic-beasts/</guid>
      <category>Terraform</category>
      <category>Mythic Beasts</category>
      <category>go</category>
      <category>code</category>
      <description>paultibbetts/terraform-provider-mythicbeasts is a Terraform provider that lets you declare infrastructure from the company Mythic Beasts as code.
I use it to provision the infrastructure for my personal website.
This is why I built it, what it does, and how to use it.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/paultibbetts/terraform-provider-mythicbeasts">paultibbetts/terraform-provider-mythicbeasts</a>
 is a Terraform provider that lets you declare infrastructure from the company <a href="https://www.mythic-beasts.com/">Mythic Beasts</a>
 as code.</p>
<p>I use it to provision the infrastructure for my personal website.</p>
<p>This is why I built it, what it does, and how to use it.</p>
<h2 id="why-i-built-it">Why I built it</h2>
<p>I prefer writing down my infrastructure as code. It&rsquo;s something I&rsquo;ve been doing for years now, and I don&rsquo;t think I could go back to doing things manually, no matter how small or simple the setup.</p>
<p>I&rsquo;d decided to use a Pi from Mythic Beasts to host my site and saw that they had APIs available, but no Terraform provider.</p>
<p>Terraform is a tool I&rsquo;ve been using for infrastructure for a while now, and I wanted to use it with Mythic Beasts because:</p>
<ul>
<li>I wanted a fully reproducible setup</li>
<li>I didn&rsquo;t want to click around the control panel</li>
<li>their Pis are IPv6-only, so I&rsquo;d need to also set up the proxy</li>
<li>
<ul>
<li>I wanted to declare the Pi and the endpoints for the proxy in the same place</li>
</ul>
</li>
</ul>
<p>Plus, I&rsquo;d never written a Terraform provider before, and I wanted to see what it was like.</p>
<h2 id="what-it-does">What it does</h2>
<p>Terraform is a tool for writing your infrastructure as code, and a provider is a plugin that lets it work with different infrastructure providers.</p>
<p>For example you can write:</p>
<pre><code class="language-hcl">terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;pi&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}</code></pre>
<p>then run <code>terraform apply</code>, and you would get a Raspberry Pi 4 from Mythic Beasts.</p>
<h3 id="resources">Resources</h3>
<p>The provider uses the Mythic Beasts APIs to configure the following resources:</p>
<ul>
<li>VPS</li>
<li>Raspberry Pi</li>
<li>Proxy endpoint</li>
</ul>
<p>The Raspberry Pis from Mythic Beasts are on an IPv6-only network, so the Proxy endpoints are to allow users trying to access from an IPv4-only network.</p>
<h3 id="data-sources">Data sources</h3>
<p>Some of the settings for the resources require specific values, like the <code>os_image</code> to use, and data sources make it easy to get this info from the API.</p>
<p>For example, the <code>mythicbeasts_pi_operating_systems</code> data source can be used to get all valid <code>os_image</code> values for a Raspberry Pi 4:</p>
<pre><code class="language-hcl">data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}</code></pre>
<p>where you would inspect the data to choose the right value to use.</p>
<p>Alternatively you could use Terraform to find the right value for you, for example to use &ldquo;Bookworm&rdquo; for &ldquo;Arm64&rdquo;:</p>
<pre><code class="language-hcl">data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}

locals {
  bookworm_arm64 = one([
    for image in data.mythicbeasts_pi_operating_systems.four.images :
    image.id
    if strcontains(image.id, &#34;bookworm&#34;) &amp;&amp;
    strcontains(image.id, &#34;arm64&#34;)
  ])
}

resource &#34;mythicbeasts_pi&#34; &#34;bookworm&#34; {
  model    = 4
  os_image = local.bookworm_arm64
  ...
}</code></pre>
<p>where Terraform will find the image with both &ldquo;bookworm&rdquo; and &ldquo;arm64&rdquo; in the ID.</p>
<h3 id="attributes">Attributes</h3>
<p>Using Terraform for your infrastructure not only lets you write it down as code, with all the <a href="https://infra.paultibbetts.uk/decisions/why-iac/">benefits that brings</a>
, but you can use the attributes of one thing as inputs for another.</p>
<p>For example: the Pis from Mythic Beasts are on an IPv6-only network and need endpoints set up on the proxy to let users access them from IPv4-only networks.</p>
<p>To do this you can get the attribute from a Pi, like the IP address, and then use that as the input for the proxy endpoint resource:</p>
<pre><code class="language-hcl">locals {
  domain = &#34;example.com&#34;
  subdomains = {
    apex  = &#34;@&#34;
    www   = &#34;www&#34;
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;web&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}

resource &#34;mythicbeasts_proxy_endpoint&#34; &#34;endpoint&#34; {
  for_each = local.subdomains

  domain         = local.domain
  hostname       = each.value
  address        = mythicbeasts_pi.web.ip
  site           = &#34;all&#34;
  proxy_protocol = true
}</code></pre>
<h2 id="how-to-use-it">How to use it</h2>
<p>The provider is available on <a href="https://registry.terraform.io/providers/paultibbetts/mythicbeasts/">the Terraform registry</a>
.</p>
<h3 id="authentication">Authentication</h3>
<p>The Mythic Beasts APIs require an <a href="https://www.mythic-beasts.com/customer/api-users">API key</a>
 for authentication.</p>
<p>When creating the key you must add permissions to work with the APIs you wish to use, such as:</p>
<ul>
<li>&ldquo;Virtual Server Provisioning&rdquo; for VPS</li>
<li>&ldquo;Raspberry Pi provisioning&rdquo; for Pis</li>
<li>&ldquo;IPv4 to IPv6 Proxy API&rdquo; for Proxy endpoints</li>
</ul>
<h3 id="proxy-endpoint-domain-management">Proxy endpoint domain management</h3>
<p>The domain used for proxy endpoints must be registered with the Mythic Beasts control panel.</p>
<p>This can be done by registering the domain using their <a href="https://www.mythic-beasts.com/customer/domains">domain management</a>
 or by <a href="https://www.mythic-beasts.com/customer/3rdpartydomain">adding it as a 3rd party domain</a>
.</p>
<h3 id="installation">Installation</h3>
<pre><code class="language-hcl">terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

provider &#34;mythicbeasts&#34; {
  keyid  = &#34;your-keyid&#34;
  secret = &#34;your-secret&#34;
}</code></pre>
<p>Alternatively, credentials can be supplied via environment variables:</p>
<ul>
<li><code>MYTHICBEASTS_KEYID</code></li>
<li><code>MYTHICBEASTS_SECRET</code></li>
</ul>
<h2 id="examples">Examples</h2>
<p>Examples of each resource and data source can be found at <a href="https://github.com/paultibbetts/terraform-provider-mythicbeasts/blob/main/examples">/examples</a>
.</p>
<p>These are also used to generate the <a href="https://registry.terraform.io/providers/paultibbetts/mythicbeasts/latest/docs">documentation hosted by the Terraform registry</a>
.</p>
<h2 id="how-it-works">How it works</h2>
<p>The provider uses the newer <a href="https://github.com/hashicorp/terraform-plugin-framework">Terraform Plugin Framework</a>
.</p>
<p>For it to work with the Mythic Beasts APIs I had to create a Go client, which can be found <a href="https://github.com/paultibbetts/mythicbeasts-client-go/">on GitHub</a>
. The client handles the APIs and means that the provider only needs to focus on Terraform itself.</p>
<p>The Mythic Beasts VPS API needs values that are only ever used when creating a VPS and so the provider uses the new <a href="https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only">write-only</a>
 arguments feature, which is only available in Terraform v1.11.0 and later. Write-only is normally used for sensitive values like passwords but the VPS API doesn&rsquo;t return every attribute so write-only is used for these values when creating the VPS and then they are forgotten.</p>
<h2 id="support">Support</h2>
<p>To make it clear, this is an unofficial provider that is not endorsed by Mythic Beasts.</p>
<p>It&rsquo;s currently at version <code>v0.1</code> and I wouldn&rsquo;t consider it stable until it hits <code>v1.0</code>.</p>
<p>Right now it supports the APIs I need it to. Further features will be added as needed.</p>
<h2 id="in-use">In use</h2>
<p>I use the provider to provision a Raspberry Pi 4 from Mythic Beasts and set up the proxy endpoints for my personal website.</p>
<p>The whole setup, with Terraform to provision the Pi and then Ansible to configure it, is documented at <a href="https://infra.paultibbetts.uk">infra.paultibbetts.uk</a>
.</p>
<h2 id="whats-next">What&rsquo;s next?</h2>
<p>The provider works with the VPS, Pi, and Proxy APIs, as these are what I currently use or have used in the past.</p>
<p>Mythic Beasts also have a <a href="https://www.mythic-beasts.com/support/api/dnsv2">DNS API</a>
 which lets you manage the DNS records for domains.</p>
<p>If I were to add this to the provider I could change the management of my domain from Cloudflare over to Mythic Beasts, which would improve my website&rsquo;s <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/">sovereignty</a>
.</p>]]></content:encoded><source:markdown>
[paultibbetts/terraform-provider-mythicbeasts](https://github.com/paultibbetts/terraform-provider-mythicbeasts) is a Terraform provider that lets you declare infrastructure from the company [Mythic Beasts](https://www.mythic-beasts.com/) as code.

I use it to provision the infrastructure for my personal website.

This is why I built it, what it does, and how to use it.

&lt;!--more--&gt;

## Why I built it

I prefer writing down my infrastructure as code. It&#39;s something I&#39;ve been doing for years now, and I don&#39;t think I could go back to doing things manually, no matter how small or simple the setup.

I&#39;d decided to use a Pi from Mythic Beasts to host my site and saw that they had APIs available, but no Terraform provider.

Terraform is a tool I&#39;ve been using for infrastructure for a while now, and I wanted to use it with Mythic Beasts because:

- I wanted a fully reproducible setup
- I didn&#39;t want to click around the control panel
- their Pis are IPv6-only, so I&#39;d need to also set up the proxy
- - I wanted to declare the Pi and the endpoints for the proxy in the same place

Plus, I&#39;d never written a Terraform provider before, and I wanted to see what it was like.

## What it does

Terraform is a tool for writing your infrastructure as code, and a provider is a plugin that lets it work with different infrastructure providers.

For example you can write:

```hcl
terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;pi&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}
```

then run `terraform apply`, and you would get a Raspberry Pi 4 from Mythic Beasts.

### Resources

The provider uses the Mythic Beasts APIs to configure the following resources:

- VPS
- Raspberry Pi
- Proxy endpoint

The Raspberry Pis from Mythic Beasts are on an IPv6-only network, so the Proxy endpoints are to allow users trying to access from an IPv4-only network.

### Data sources

Some of the settings for the resources require specific values, like the `os_image` to use, and data sources make it easy to get this info from the API.

For example, the `mythicbeasts_pi_operating_systems` data source can be used to get all valid `os_image` values for a Raspberry Pi 4:

```hcl
data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}
```

where you would inspect the data to choose the right value to use.

Alternatively you could use Terraform to find the right value for you, for example to use &#34;Bookworm&#34; for &#34;Arm64&#34;:

```hcl
data &#34;mythicbeasts_pi_operating_systems&#34; &#34;four&#34; {
  model = 4
}

locals {
  bookworm_arm64 = one([
    for image in data.mythicbeasts_pi_operating_systems.four.images :
    image.id
    if strcontains(image.id, &#34;bookworm&#34;) &amp;&amp;
    strcontains(image.id, &#34;arm64&#34;)
  ])
}

resource &#34;mythicbeasts_pi&#34; &#34;bookworm&#34; {
  model    = 4
  os_image = local.bookworm_arm64
  ...
}
```

where Terraform will find the image with both &#34;bookworm&#34; and &#34;arm64&#34; in the ID.

### Attributes

Using Terraform for your infrastructure not only lets you write it down as code, with all the [benefits that brings](https://infra.paultibbetts.uk/decisions/why-iac/), but you can use the attributes of one thing as inputs for another.

For example: the Pis from Mythic Beasts are on an IPv6-only network and need endpoints set up on the proxy to let users access them from IPv4-only networks.

To do this you can get the attribute from a Pi, like the IP address, and then use that as the input for the proxy endpoint resource:

```hcl
locals {
  domain = &#34;example.com&#34;
  subdomains = {
    apex  = &#34;@&#34;
    www   = &#34;www&#34;
  }
}

resource &#34;mythicbeasts_pi&#34; &#34;web&#34; {
	identifier   = &#34;web&#34;
	disk_size    = 10
	model        = 4
	memory       = 4096
	os_image     = &#34;rpi-bookworm-arm64&#34;
    ssh_key      = &#34;ssh-ed25519 ...&#34;
    wait_for_dns = true
}

resource &#34;mythicbeasts_proxy_endpoint&#34; &#34;endpoint&#34; {
  for_each = local.subdomains

  domain         = local.domain
  hostname       = each.value
  address        = mythicbeasts_pi.web.ip
  site           = &#34;all&#34;
  proxy_protocol = true
}
```

## How to use it

The provider is available on [the Terraform registry](https://registry.terraform.io/providers/paultibbetts/mythicbeasts/).

### Authentication

The Mythic Beasts APIs require an [API key](https://www.mythic-beasts.com/customer/api-users) for authentication.

When creating the key you must add permissions to work with the APIs you wish to use, such as:

- &#34;Virtual Server Provisioning&#34; for VPS
- &#34;Raspberry Pi provisioning&#34; for Pis
- &#34;IPv4 to IPv6 Proxy API&#34; for Proxy endpoints

### Proxy endpoint domain management

The domain used for proxy endpoints must be registered with the Mythic Beasts control panel.

This can be done by registering the domain using their [domain management](https://www.mythic-beasts.com/customer/domains) or by [adding it as a 3rd party domain](https://www.mythic-beasts.com/customer/3rdpartydomain).

### Installation

```hcl
terraform {
  required_version = &#34;&gt;= 1.11.0&#34;

  required_providers {
    mythicbeasts = {
      source = &#34;paultibbetts/mythicbeasts&#34;
    }
  }
}

provider &#34;mythicbeasts&#34; {
  keyid  = &#34;your-keyid&#34;
  secret = &#34;your-secret&#34;
}
```

Alternatively, credentials can be supplied via environment variables:

- `MYTHICBEASTS_KEYID`
- `MYTHICBEASTS_SECRET`

## Examples

Examples of each resource and data source can be found at [/examples](https://github.com/paultibbetts/terraform-provider-mythicbeasts/blob/main/examples).

These are also used to generate the [documentation hosted by the Terraform registry](https://registry.terraform.io/providers/paultibbetts/mythicbeasts/latest/docs).

## How it works

The provider uses the newer [Terraform Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework).

For it to work with the Mythic Beasts APIs I had to create a Go client, which can be found [on GitHub](https://github.com/paultibbetts/mythicbeasts-client-go/). The client handles the APIs and means that the provider only needs to focus on Terraform itself.

The Mythic Beasts VPS API needs values that are only ever used when creating a VPS and so the provider uses the new [write-only](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only) arguments feature, which is only available in Terraform v1.11.0 and later. Write-only is normally used for sensitive values like passwords but the VPS API doesn&#39;t return every attribute so write-only is used for these values when creating the VPS and then they are forgotten.

## Support

To make it clear, this is an unofficial provider that is not endorsed by Mythic Beasts.

It&#39;s currently at version `v0.1` and I wouldn&#39;t consider it stable until it hits `v1.0`.

Right now it supports the APIs I need it to. Further features will be added as needed.

## In use

I use the provider to provision a Raspberry Pi 4 from Mythic Beasts and set up the proxy endpoints for my personal website.

The whole setup, with Terraform to provision the Pi and then Ansible to configure it, is documented at [infra.paultibbetts.uk](https://infra.paultibbetts.uk).

## What&#39;s next?

The provider works with the VPS, Pi, and Proxy APIs, as these are what I currently use or have used in the past.

Mythic Beasts also have a [DNS API](https://www.mythic-beasts.com/support/api/dnsv2) which lets you manage the DNS records for domains.

If I were to add this to the provider I could change the management of my domain from Cloudflare over to Mythic Beasts, which would improve my website&#39;s [sovereignty](/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/).
</source:markdown></item><item>
      <title>My RSS feeds now contain content:encoded and source:markdown. These are used by …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/02/2026-03-02-133227-rss-feed-changes/</link>
      <pubDate>Mon, 02 Mar 2026 13:32:27 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/02/2026-03-02-133227-rss-feed-changes/</guid>
      <category>RSS</category>
      <description>My RSS feeds now contain content:encoded and source:markdown.
These are used by some feed readers to show the full content, which means my posts are now fully readable through RSS.</description>
      <content:encoded><![CDATA[<p>My RSS feeds now contain <code>content:encoded</code> and <code>source:markdown</code>.</p>
<p>These are used by some feed readers to show the full content, which means my posts are now fully readable through RSS.</p>]]></content:encoded><source:markdown>
My RSS feeds now contain `content:encoded` and `source:markdown`.

These are used by some feed readers to show the full content, which means my posts are now fully readable through RSS.
</source:markdown></item><item>
      <title>How to create an RSS Feed</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/01/how-to-create-an-rss-feed/</link>
      <pubDate>Sun, 01 Mar 2026 16:33:46 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/01/how-to-create-an-rss-feed/</guid>
      <category>IndieWeb</category>
      <category>RSS</category>
      <description>In my last post I recommended starting with RSS to make your site followable.
This is how to do that.</description>
      <content:encoded><![CDATA[<p>In my last post I recommended starting with RSS to make your site followable.</p>
<p>This is how to do that.</p>
<h2 id="you-might-already-have-one">You might already have one</h2>
<p>If you&rsquo;re using a static site generator like Hugo, or a CMS like WordPress, then you might already have an RSS feed.</p>
<p>If not, and you&rsquo;re writing your site manually, or just want to understand what&rsquo;s going on, here&rsquo;s what an RSS feed looks like.</p>
<h2 id="a-minimal-rss-feed">A minimal RSS feed</h2>
<p>At a minimum an RSS feed contains:</p>
<pre><code class="language-xml">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;
&lt;rss version=&#34;2.0&#34;&gt;
  &lt;channel&gt;
    &lt;title&gt;Example Blog&lt;/title&gt;
    &lt;link&gt;https://example.com/&lt;/link&gt;
    &lt;description&gt;Updates from my blog.&lt;/description&gt;

    &lt;item&gt;
      &lt;title&gt;Hello World&lt;/title&gt;
      &lt;link&gt;https://example.com/hello-world&lt;/link&gt;
      &lt;description&gt;My first post.&lt;/description&gt;
    &lt;/item&gt;

  &lt;/channel&gt;
&lt;/rss&gt;</code></pre>
<h3 id="required">Required</h3>
<pre><code class="language-xml">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</code></pre>
<p>This line declares the XML version of the file and the encoding that it uses. <code>UTF-8</code> supports text and emoji.</p>
<p>It&rsquo;s strongly recommended to include this line so the file is read correctly.</p>
<pre><code class="language-xml">&lt;rss version=&#34;2.0&#34;&gt;</code></pre>
<p>This is the start of the <code>&lt;rss&gt;</code> element. The <code>version=&quot;2.0&quot;</code> tells consumers that the feed conforms to the RSS 2.0 specification, the most widely supported version.</p>
<pre><code class="language-xml">&lt;channel&gt;
  &lt;title&gt;Example Blog&lt;/title&gt;
  &lt;link&gt;https://example.com/&lt;/link&gt;
  &lt;description&gt;Updates from my blog.&lt;/description&gt;</code></pre>
<p>The <code>&lt;channel&gt;</code> declares the feed itself, so <code>&lt;title&gt;</code>, <code>&lt;link&gt;</code> and <code>&lt;description&gt;</code> are used to describe the feed and its contents.</p>
<p>These details should match what&rsquo;s displayed on your site and will show up for users when adding or managing your feed in their reader.</p>
<p>The <code>&lt;link&gt;</code> should point to the URL of the <code>&lt;channel&gt;</code>, which is the site itself.</p>
<pre><code class="language-xml">&lt;item&gt;
  &lt;title&gt;Hello World&lt;/title&gt;
  &lt;link&gt;https://example.com/hello-world&lt;/link&gt;
  &lt;description&gt;My first post.&lt;/description&gt;
&lt;/item&gt;</code></pre>
<p>The <code>&lt;item&gt;</code> tag is used for each post in the feed.</p>
<p>An <code>&lt;item&gt;</code> <strong>must</strong> contain at least one of:</p>
<ul>
<li><code>&lt;title&gt;</code></li>
<li><code>&lt;description&gt;</code></li>
</ul>
<p>which means you could do:</p>
<pre><code class="language-xml">&lt;item&gt;
  &lt;title&gt;My first post&lt;/title&gt;
&lt;/item&gt;</code></pre>
<p>which is valid, but not very useful, or:</p>
<pre><code class="language-xml">&lt;item&gt;
  &lt;description&gt;Hello, World!&lt;/description&gt;
&lt;/item&gt;</code></pre>
<p>which is also valid, and slightly more useful, if only for microblogs and notes and other posts without a title.</p>
<pre><code class="language-xml">&lt;item&gt;
  &lt;title&gt;An example&lt;/title&gt;
  &lt;link&gt;https://example.com/posts/1&lt;/link&gt;
&lt;/item&gt;</code></pre>
<p>Title + link is more practical, and may be all you need, but:</p>
<pre><code class="language-xml">&lt;item&gt;
  &lt;title&gt;An example&lt;/title&gt;
  &lt;link&gt;https://example.com/posts/1&lt;/link&gt;
  &lt;description&gt;Hello, World!&lt;/description&gt;
&lt;/item&gt;</code></pre>
<p>using all three is most common.</p>
<p>You could use the <code>&lt;description&gt;</code> for the summary of your post or for the whole thing.</p>
<p>There&rsquo;s no upper limit on the amount of <code>&lt;item&gt;</code>s you can include in a feed, but there are some practical things to consider:</p>
<ul>
<li>RSS does not do pagination</li>
<li>
<ul>
<li>everything is listed in one file</li>
</ul>
</li>
<li>if the file gets really big then it takes longer to download</li>
<li>
<ul>
<li>aim to keep it under 500KB</li>
</ul>
</li>
<li>maybe only show your latest posts if you have lots of them</li>
</ul>
<h3 id="optional">Optional</h3>
<p>The fields mentioned above are all that is required for a minimal feed, but there are some optional extras that are recommended.</p>
<pre><code class="language-xml">&lt;item&gt;
  ...
  &lt;guid isPermaLink=&#34;true&#34;&gt;
    https://example.com/posts/1
  &lt;/guid&gt;
&lt;/item&gt;</code></pre>
<p>A <code>guid</code>, for &ldquo;Globally Unique IDentifier&rdquo;, gives each item in the feed a stable identity. This means feed readers can detect and remove duplicates correctly.</p>
<p>This is usually the permalink of the post. If so, <code>isPermaLink</code> defaults to true and can be omitted, however it it&rsquo;s not the permalink, like a database ID, or the exact datetime the post was published, you would change <code>isPermaLink</code> to <code>false</code>.</p>
<p>Note that the <code>L</code> in <code>PermaLink</code> is also capitalised.</p>
<pre><code class="language-xml">&lt;item&gt;
  ...
  &lt;pubDate&gt;Sun, 1 Mar 2026 14:00:00 GMT&lt;/pubDate&gt;
&lt;/item&gt;</code></pre>
<p>The <code>&lt;pubDate&gt;</code> is used to show when an item in the feed was published. It can help feed readers understand the order of your posts and should be added.</p>
<pre><code class="language-xml">&lt;rss version=&#34;2.0&#34;
xmlns:atom=&#34;http://www.w3.org/2005/Atom&#34;&gt;</code></pre>
<p>Changing the <code>&lt;rss&gt;</code> tag to include <code>xmlns:atom=&quot;http://www.w3.org/2005/Atom&quot;</code> is also optional but recommended.</p>
<p>This tells whatever is reading the feed that anything defined with <code>atom:</code> comes from the Atom specification, and lets you use extra tags defined by the Atom spec.</p>
<p>This isn&rsquo;t useful by itself, but it is used when you also add:</p>
<pre><code class="language-xml">&lt;atom:link
href=&#34;https://example.com/index.xml&#34;
rel=&#34;self&#34;
type=&#34;application/rss+xml&#34; /&gt;</code></pre>
<p>which defines the URL of the feed and helps feed readers know the canonical URL of the feed itself, which helps prevent duplicate subscriptions.</p>
<pre><code class="language-xml">&lt;lastBuildDate&gt;Sun, 1 Mar 2026 14:00:00 GMT&lt;/lastBuildDate&gt;</code></pre>
<p>The <code>&lt;lastBuildDate&gt;</code> can be used by feed readers to know if the feed has changed.</p>
<p>Set it to the date of the most recent post.</p>
<pre><code class="language-xml">&lt;language&gt;en-gb&lt;/language&gt;</code></pre>
<p>You should declare the language of the feed. It&rsquo;s small, but it&rsquo;s technically correct.</p>
<h2 id="things-to-remember">Things to remember</h2>
<p>RSS is pretty simple but dealing with XML means you have to remember a few things.</p>
<h3 id="xml-must-be-well-formed">XML must be well-formed</h3>
<p>Writing an RSS feed is still writing XML, and so it must obey the rules of an XML file.</p>
<p>This means the file must not have any missing closing tags, or mix ups with the order in which you close them.</p>
<p>It also means you must handle special characters, otherwise the file is invalid and won&rsquo;t be understood.</p>
<p>For example:</p>
<pre><code class="language-xml">&lt;title&gt;Special &amp; Character&lt;/title&gt;</code></pre>
<p>won&rsquo;t work, because of the <code>&amp;</code>, which must be escaped:</p>
<pre><code class="language-xml">&lt;title&gt;Special &amp;amp; Character&lt;/title&gt;</code></pre>
<h4 id="no-unescaped-html-in-the-description">No unescaped HTML in the description</h4>
<p>The same happens with the <code>&lt;description&gt;</code> tag, which usually holds the summary or the contents of the post, and must not include unescaped HTML characters.</p>
<p>The first option is to escape all of the HTML in the description:</p>
<pre><code class="language-xml">&lt;description&gt;&amp;lt;p&amp;gt;Hello, World!&amp;lt;/p&amp;gt;&lt;/description&gt;</code></pre>
<p>or the more common option is to wrap the HTML content in the description in a <code>CDATA</code> tag:</p>
<pre><code class="language-xml">&lt;description&gt;&lt;![CDATA[
&lt;p&gt;Hello, World!&lt;/p&gt;
]]&gt;&lt;/description&gt;</code></pre>
<p>where CDATA tells whatever is reading the XML file that everything inside this is raw text.</p>
<h4 id="dates-must-be-written-as-rfc-822">Dates must be written as RFC 822</h4>
<p>If you do use <code>&lt;pubDate&gt;</code> or <code>&lt;lastBuildDate&gt;</code> to show dates you must ensure they are written using the <a href="https://www.w3.org/Protocols/rfc822/#z28">RFC 822 format</a>
:</p>
<pre><code class="language-xml">&lt;pubDate&gt;Sun, 1 Mar 2026 14:00:00 GMT&lt;/pubDate&gt;</code></pre>
<h3 id="serve-the-file-with-the-correct-content-type">Serve the file with the correct Content-Type</h3>
<p>When your server responds to a request to show the feed it should send:</p>
<pre><code class="language-text">Content-Type: application/rss+xml</code></pre>
<p>so that feed readers recognise it as an RSS feed.</p>
<p>Most static web hosting handles this for you, I only mention it in case you are setting up a server and are messing with the Content-Type of the files you serve.</p>
<h2 id="how-to-check-your-feed">How to check your feed</h2>
<p>There are a few ways to check you&rsquo;ve written your feed correctly.</p>
<h3 id="open-it-in-a-browser">Open it in a browser</h3>
<p>The file should open and look similar to what you wrote it as. If it doesn&rsquo;t open, there&rsquo;s a problem, and if it shows an error then you need to fix it.</p>
<h3 id="use-a-validator">Use a validator</h3>
<p>You can use <a href="https://validator.w3.org/feed/">validator.w3.org/feed</a>
 to validate your feed. It can work with the URL of your feed online or you can paste in its contents if you&rsquo;re still working on it.</p>
<h3 id="add-it-to-your-reader">Add it to your reader</h3>
<p>The best test is to add your own feed to your reader and check it looks the way you want.</p>
<p>Make sure it&rsquo;s showing in the order you expect and you can edit posts without making duplicates in your reader.</p>
<h2 id="full-example">Full example</h2>
<p>Here is a condensed version of my own RSS feed, containing two articles and a note.</p>
<p>The note doesn&rsquo;t have a <code>&lt;title&gt;</code>, which influenced the URL used for its <code>&lt;guid&gt;</code>.</p>
<pre><code class="language-xml">&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34; standalone=&#34;yes&#34;?&gt;
&lt;rss version=&#34;2.0&#34;
	xmlns:atom=&#34;http://www.w3.org/2005/Atom&#34;&gt;
	&lt;channel&gt;
		&lt;title&gt;Paul Tibbetts&lt;/title&gt;
		&lt;link&gt;https://paultibbetts.uk/&lt;/link&gt;
		&lt;description&gt;Recent posts by Paul Tibbetts&lt;/description&gt;
		&lt;generator&gt;Hugo&lt;/generator&gt;
		&lt;language&gt;en-gb&lt;/language&gt;
		&lt;lastBuildDate&gt;Sun, 01 Mar 2026 12:41:46 +0000&lt;/lastBuildDate&gt;
		&lt;atom:link href=&#34;https://paultibbetts.uk/feed.xml&#34; rel=&#34;self&#34; type=&#34;application/rss+xml&#34; /&gt;
		&lt;item&gt;
			&lt;title&gt;How to create an RSS Feed&lt;/title&gt;
			&lt;link&gt;https://paultibbetts.uk/2026/03/01/how-to-create-an-rss-feed/&lt;/link&gt;
			&lt;pubDate&gt;Sun, 01 Mar 2026 16:33:46 +0000&lt;/pubDate&gt;
			&lt;guid&gt;https://paultibbetts.uk/2026/03/01/how-to-create-an-rss-feed/&lt;/guid&gt;
			&lt;description&gt;In my last post I recommended starting with RSS to make your site followable. This is how to do that.&lt;/description&gt;
		&lt;/item&gt;
		&lt;item&gt;
			&lt;title&gt;Ways to make your feed followable&lt;/title&gt;
			&lt;link&gt;https://paultibbetts.uk/2026/03/01/ways-to-make-your-feed-followable/&lt;/link&gt;
			&lt;pubDate&gt;Sun, 01 Mar 2026 11:36:03 +0000&lt;/pubDate&gt;
			&lt;guid&gt;https://paultibbetts.uk/2026/03/01/ways-to-make-your-feed-followable/&lt;/guid&gt;
			&lt;description&gt;You’ve got 3 posts on your site and you want people to be notified about new ones. Here’s all the ways I can think of doing that.&lt;/description&gt;
			&lt;/item&gt;
		&lt;item&gt;
			&lt;title&gt;2026-02-15 14:54:58&lt;/title&gt;
			&lt;link&gt;https://paultibbetts.uk/2026/02/15/2026-02-15-145458/&lt;/link&gt;
			&lt;pubDate&gt;Sun, 15 Feb 2026 14:54:58 +0000&lt;/pubDate&gt;
			&lt;guid&gt;https://paultibbetts.uk/2026/02/15/2026-02-15-145458/&lt;/guid&gt;
			&lt;description&gt;Launched: infra.paultibbetts.uk A documentation site for the infrastructure that runs paultibbetts.uk.&lt;/description&gt;
		&lt;/item&gt;
	&lt;/channel&gt;
&lt;/rss&gt;</code></pre>]]></content:encoded><source:markdown>
In my last post I recommended starting with RSS to make your site followable.

This is how to do that.

&lt;!--more--&gt;

## You might already have one

If you&#39;re using a static site generator like Hugo, or a CMS like WordPress, then you might already have an RSS feed.

If not, and you&#39;re writing your site manually, or just want to understand what&#39;s going on, here&#39;s what an RSS feed looks like.

## A minimal RSS feed

At a minimum an RSS feed contains:

```xml
&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;
&lt;rss version=&#34;2.0&#34;&gt;
  &lt;channel&gt;
    &lt;title&gt;Example Blog&lt;/title&gt;
    &lt;link&gt;https://example.com/&lt;/link&gt;
    &lt;description&gt;Updates from my blog.&lt;/description&gt;

    &lt;item&gt;
      &lt;title&gt;Hello World&lt;/title&gt;
      &lt;link&gt;https://example.com/hello-world&lt;/link&gt;
      &lt;description&gt;My first post.&lt;/description&gt;
    &lt;/item&gt;

  &lt;/channel&gt;
&lt;/rss&gt;
```

### Required

```xml
&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;
```

This line declares the XML version of the file and the encoding that it uses. `UTF-8` supports text and emoji.

It&#39;s strongly recommended to include this line so the file is read correctly.

```xml
&lt;rss version=&#34;2.0&#34;&gt;
```

This is the start of the `&lt;rss&gt;` element. The `version=&#34;2.0&#34;` tells consumers that the feed conforms to the RSS 2.0 specification, the most widely supported version.

```xml
&lt;channel&gt;
  &lt;title&gt;Example Blog&lt;/title&gt;
  &lt;link&gt;https://example.com/&lt;/link&gt;
  &lt;description&gt;Updates from my blog.&lt;/description&gt;
```

The `&lt;channel&gt;` declares the feed itself, so `&lt;title&gt;`, `&lt;link&gt;` and `&lt;description&gt;` are used to describe the feed and its contents.

These details should match what&#39;s displayed on your site and will show up for users when adding or managing your feed in their reader.

The `&lt;link&gt;` should point to the URL of the `&lt;channel&gt;`, which is the site itself.

```xml
&lt;item&gt;
  &lt;title&gt;Hello World&lt;/title&gt;
  &lt;link&gt;https://example.com/hello-world&lt;/link&gt;
  &lt;description&gt;My first post.&lt;/description&gt;
&lt;/item&gt;
```

The `&lt;item&gt;` tag is used for each post in the feed.

An `&lt;item&gt;` **must** contain at least one of:

- `&lt;title&gt;`
- `&lt;description&gt;`

which means you could do:

```xml
&lt;item&gt;
  &lt;title&gt;My first post&lt;/title&gt;
&lt;/item&gt;
```

which is valid, but not very useful, or:

```xml
&lt;item&gt;
  &lt;description&gt;Hello, World!&lt;/description&gt;
&lt;/item&gt;
```

which is also valid, and slightly more useful, if only for microblogs and notes and other posts without a title.

```xml
&lt;item&gt;
  &lt;title&gt;An example&lt;/title&gt;
  &lt;link&gt;https://example.com/posts/1&lt;/link&gt;
&lt;/item&gt;
```

Title + link is more practical, and may be all you need, but:

```xml
&lt;item&gt;
  &lt;title&gt;An example&lt;/title&gt;
  &lt;link&gt;https://example.com/posts/1&lt;/link&gt;
  &lt;description&gt;Hello, World!&lt;/description&gt;
&lt;/item&gt;
```

using all three is most common.

You could use the `&lt;description&gt;` for the summary of your post or for the whole thing.

There&#39;s no upper limit on the amount of `&lt;item&gt;`s you can include in a feed, but there are some practical things to consider:

- RSS does not do pagination
- - everything is listed in one file
- if the file gets really big then it takes longer to download
- - aim to keep it under 500KB
- maybe only show your latest posts if you have lots of them

### Optional

The fields mentioned above are all that is required for a minimal feed, but there are some optional extras that are recommended.

```xml
&lt;item&gt;
  ...
  &lt;guid isPermaLink=&#34;true&#34;&gt;
    https://example.com/posts/1
  &lt;/guid&gt;
&lt;/item&gt;
```

A `guid`, for &#34;Globally Unique IDentifier&#34;, gives each item in the feed a stable identity. This means feed readers can detect and remove duplicates correctly.

This is usually the permalink of the post. If so, `isPermaLink` defaults to true and can be omitted, however it it&#39;s not the permalink, like a database ID, or the exact datetime the post was published, you would change `isPermaLink` to `false`.

Note that the `L` in `PermaLink` is also capitalised.

```xml
&lt;item&gt;
  ...
  &lt;pubDate&gt;Sun, 1 Mar 2026 14:00:00 GMT&lt;/pubDate&gt;
&lt;/item&gt;
```

The `&lt;pubDate&gt;` is used to show when an item in the feed was published. It can help feed readers understand the order of your posts and should be added.

```xml
&lt;rss version=&#34;2.0&#34;
xmlns:atom=&#34;http://www.w3.org/2005/Atom&#34;&gt;
```

Changing the `&lt;rss&gt;` tag to include `xmlns:atom=&#34;http://www.w3.org/2005/Atom&#34;` is also optional but recommended.

This tells whatever is reading the feed that anything defined with `atom:` comes from the Atom specification, and lets you use extra tags defined by the Atom spec.

This isn&#39;t useful by itself, but it is used when you also add:

```xml
&lt;atom:link
href=&#34;https://example.com/index.xml&#34;
rel=&#34;self&#34;
type=&#34;application/rss+xml&#34; /&gt;
```

which defines the URL of the feed and helps feed readers know the canonical URL of the feed itself, which helps prevent duplicate subscriptions.

```xml
&lt;lastBuildDate&gt;Sun, 1 Mar 2026 14:00:00 GMT&lt;/lastBuildDate&gt;
```

The `&lt;lastBuildDate&gt;` can be used by feed readers to know if the feed has changed.

Set it to the date of the most recent post.

```xml
&lt;language&gt;en-gb&lt;/language&gt;
```

You should declare the language of the feed. It&#39;s small, but it&#39;s technically correct.

## Things to remember

RSS is pretty simple but dealing with XML means you have to remember a few things.

### XML must be well-formed

Writing an RSS feed is still writing XML, and so it must obey the rules of an XML file.

This means the file must not have any missing closing tags, or mix ups with the order in which you close them.

It also means you must handle special characters, otherwise the file is invalid and won&#39;t be understood.

For example:

```xml
&lt;title&gt;Special &amp; Character&lt;/title&gt;
```

won&#39;t work, because of the `&amp;`, which must be escaped:

```xml
&lt;title&gt;Special &amp;amp; Character&lt;/title&gt;
```

#### No unescaped HTML in the description

The same happens with the `&lt;description&gt;` tag, which usually holds the summary or the contents of the post, and must not include unescaped HTML characters.

The first option is to escape all of the HTML in the description:

```xml
&lt;description&gt;&amp;lt;p&amp;gt;Hello, World!&amp;lt;/p&amp;gt;&lt;/description&gt;
```

or the more common option is to wrap the HTML content in the description in a `CDATA` tag:

```xml
&lt;description&gt;&lt;![CDATA[
&lt;p&gt;Hello, World!&lt;/p&gt;
]]&gt;&lt;/description&gt;
```

where CDATA tells whatever is reading the XML file that everything inside this is raw text.

#### Dates must be written as RFC 822

If you do use `&lt;pubDate&gt;` or `&lt;lastBuildDate&gt;` to show dates you must ensure they are written using the [RFC 822 format](https://www.w3.org/Protocols/rfc822/#z28):

```xml
&lt;pubDate&gt;Sun, 1 Mar 2026 14:00:00 GMT&lt;/pubDate&gt;
```

### Serve the file with the correct Content-Type

When your server responds to a request to show the feed it should send:

```
Content-Type: application/rss+xml
```

so that feed readers recognise it as an RSS feed.

Most static web hosting handles this for you, I only mention it in case you are setting up a server and are messing with the Content-Type of the files you serve.

## How to check your feed

There are a few ways to check you&#39;ve written your feed correctly.

### Open it in a browser

The file should open and look similar to what you wrote it as. If it doesn&#39;t open, there&#39;s a problem, and if it shows an error then you need to fix it.

### Use a validator

You can use [validator.w3.org/feed](https://validator.w3.org/feed/) to validate your feed. It can work with the URL of your feed online or you can paste in its contents if you&#39;re still working on it.

### Add it to your reader

The best test is to add your own feed to your reader and check it looks the way you want.

Make sure it&#39;s showing in the order you expect and you can edit posts without making duplicates in your reader.

## Full example

Here is a condensed version of my own RSS feed, containing two articles and a note.

The note doesn&#39;t have a `&lt;title&gt;`, which influenced the URL used for its `&lt;guid&gt;`.

```xml
&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34; standalone=&#34;yes&#34;?&gt;
&lt;rss version=&#34;2.0&#34;
	xmlns:atom=&#34;http://www.w3.org/2005/Atom&#34;&gt;
	&lt;channel&gt;
		&lt;title&gt;Paul Tibbetts&lt;/title&gt;
		&lt;link&gt;https://paultibbetts.uk/&lt;/link&gt;
		&lt;description&gt;Recent posts by Paul Tibbetts&lt;/description&gt;
		&lt;generator&gt;Hugo&lt;/generator&gt;
		&lt;language&gt;en-gb&lt;/language&gt;
		&lt;lastBuildDate&gt;Sun, 01 Mar 2026 12:41:46 +0000&lt;/lastBuildDate&gt;
		&lt;atom:link href=&#34;https://paultibbetts.uk/feed.xml&#34; rel=&#34;self&#34; type=&#34;application/rss+xml&#34; /&gt;
		&lt;item&gt;
			&lt;title&gt;How to create an RSS Feed&lt;/title&gt;
			&lt;link&gt;https://paultibbetts.uk/2026/03/01/how-to-create-an-rss-feed/&lt;/link&gt;
			&lt;pubDate&gt;Sun, 01 Mar 2026 16:33:46 +0000&lt;/pubDate&gt;
			&lt;guid&gt;https://paultibbetts.uk/2026/03/01/how-to-create-an-rss-feed/&lt;/guid&gt;
			&lt;description&gt;In my last post I recommended starting with RSS to make your site followable. This is how to do that.&lt;/description&gt;
		&lt;/item&gt;
		&lt;item&gt;
			&lt;title&gt;Ways to make your feed followable&lt;/title&gt;
			&lt;link&gt;https://paultibbetts.uk/2026/03/01/ways-to-make-your-feed-followable/&lt;/link&gt;
			&lt;pubDate&gt;Sun, 01 Mar 2026 11:36:03 +0000&lt;/pubDate&gt;
			&lt;guid&gt;https://paultibbetts.uk/2026/03/01/ways-to-make-your-feed-followable/&lt;/guid&gt;
			&lt;description&gt;You’ve got 3 posts on your site and you want people to be notified about new ones. Here’s all the ways I can think of doing that.&lt;/description&gt;
			&lt;/item&gt;
		&lt;item&gt;
			&lt;title&gt;2026-02-15 14:54:58&lt;/title&gt;
			&lt;link&gt;https://paultibbetts.uk/2026/02/15/2026-02-15-145458/&lt;/link&gt;
			&lt;pubDate&gt;Sun, 15 Feb 2026 14:54:58 +0000&lt;/pubDate&gt;
			&lt;guid&gt;https://paultibbetts.uk/2026/02/15/2026-02-15-145458/&lt;/guid&gt;
			&lt;description&gt;Launched: infra.paultibbetts.uk A documentation site for the infrastructure that runs paultibbetts.uk.&lt;/description&gt;
		&lt;/item&gt;
	&lt;/channel&gt;
&lt;/rss&gt;
```
</source:markdown></item><item>
      <title>Ways to make your feed followable</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/01/ways-to-make-your-feed-followable/</link>
      <pubDate>Sun, 01 Mar 2026 11:36:03 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/03/01/ways-to-make-your-feed-followable/</guid>
      <category>IndieWeb</category>
      <category>RSS</category>
      <description>You’ve got 3 posts on your site and you want people to be notified about new ones.
Here’s all the ways I can think of doing that.</description>
      <content:encoded><![CDATA[<p>You&rsquo;ve got 3 posts on your site and you want people to be notified about new ones.</p>
<p>Here&rsquo;s all the ways I can think of doing that.</p>
<h2 id="email">Email</h2>
<p>Some sites let you send out your posts as emails.</p>
<p>Don&rsquo;t do it. It can be tempting, because it&rsquo;s easy, but emails are for emails.</p>
<p>There are better options for your feed.</p>
<h2 id="rss">RSS</h2>
<p>RSS stands for &ldquo;Really Simple Syndication&rdquo;.</p>
<p>It&rsquo;s a standard format for publishing a feed so apps can subscribe to your updates.</p>
<p>Feed readers and podcast apps use it.</p>
<p>It looks a bit like this:</p>
<pre><code class="language-xml">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;
&lt;rss version=&#34;2.0&#34;&gt;
  &lt;channel&gt;
    &lt;title&gt;RSS Example Site&lt;/title&gt;
    &lt;link&gt;https://example.com/&lt;/link&gt;
    &lt;description&gt;Example site description.&lt;/description&gt;

    &lt;item&gt;
      &lt;title&gt;Hello, World!&lt;/title&gt;
      &lt;link&gt;https://example.com/hello-world.html&lt;/link&gt;
      &lt;description&gt;My first post.&lt;/description&gt;
    &lt;/item&gt;

  &lt;/channel&gt;
&lt;/rss&gt;</code></pre>
<p>You would host a file like this, link to it from the <code>&lt;head&gt;</code> part of your pages, and readers would check it occasionally for updates.</p>
<p>Its simplicity means it&rsquo;s supported by almost all feed reading apps.</p>
<p>Because of this I&rsquo;d say RSS is the best starting point for letting users subscribe to your posts and is worth adding to your site first, before the other alternatives.</p>
<h2 id="atom">Atom</h2>
<p>RSS isn&rsquo;t the only way to create an XML feed. Atom is another standard which is more formally specified.</p>
<p>In the early 2000&rsquo;s, after Atom&rsquo;s launch, there was a time called the &ldquo;RSS vs Atom wars&rdquo;. There was no real winner. Atom was decided to be technically better, but by then RSS had already become dominant.</p>
<p>These days you can add a few Atom elements to your RSS feed, like a <code>rel=self</code> link, to get the best of both worlds.</p>
<h2 id="json">JSON</h2>
<p><a href="https://www.jsonfeed.org/">JSON feed</a>
 is the one of the newer standards. It works in the same way as RSS and Atom but the data is presented as JSON instead of XML.</p>
<p>Web development has moved on from XML and these days JSON-based things are much more common. The idea is that by using JSON we allow developers to build new things like feed readers using modern techniques and tooling, instead of ones used only for legacy publishing steps like an XML-based feed.</p>
<p>It looks a bit like this:</p>
<pre><code class="language-json">{
  &#34;version&#34;: &#34;https://jsonfeed.org/version/1.1&#34;,
  &#34;title&#34;: &#34;Example JSON Feed Site&#34;,
  &#34;home_page_url&#34;: &#34;https://example.com/&#34;,
  &#34;feed_url&#34;: &#34;https://example.com/feed.json&#34;,
  &#34;items&#34;: [
    {
      &#34;id&#34;: &#34;2026-02-28-153000&#34;,
      &#34;url&#34;: &#34;https://paultibbetts.uk/notes/2026-02-28-153000/&#34;,
      &#34;content_html&#34;: &#34;&lt;p&gt;Hello world&lt;/p&gt;&#34;,
      &#34;date_published&#34;: &#34;2026-02-28T15:30:00Z&#34;
    }
  ]
}</code></pre>
<p>JSON feed is already supported by many feed readers and, at least in theory, makes it easier than XML-based feeds to make new ones.</p>
<p>I&rsquo;d say it&rsquo;s worth adding to your site after you&rsquo;ve got an RSS feed, since those are more universally supported right now.</p>
<h2 id="microformats">Microformats</h2>
<p><a href="https://indieweb.org/microformats">Microformats</a>
 is a standard for marking up the contents of your posts, and in doing so makes your feed followable without any other files.</p>
<p>Classes like <code>h-entry</code> and <code>h-card</code> help others to understand the content you&rsquo;re publishing, and the <code>h-feed</code> class is used to identify feeds. This means readers using apps that understand microformats can subscribe to your content directly, without needing an XML or JSON file.</p>
<p>Right now most feed readers expect RSS or Atom feeds, so whilst I would suggest adding microformats, I would do it after adding an RSS feed.</p>
<h2 id="social-media">Social media</h2>
<p>There&rsquo;s also social media, where you would post a link to your content on another site, like Facebook or Instagram.</p>
<p>It&rsquo;s a great way to get your posts in front of users across the web on the apps they&rsquo;re already using, but it&rsquo;s no longer on your own site and you&rsquo;re now at the mercy of whatever the algorithms think of your content.</p>
<p>I&rsquo;d consider doing this as an extra, and only after making your own feed available.</p>
<p>For more information on posting out to social media the IndieWeb.org wiki has a great page called <a href="https://indieweb.org/POSSE">Publish Own Site, Syndicate Elsewhere (POSSE)</a>
.</p>
<h2 id="activitypub">ActivityPub</h2>
<p>Another option is to use the ActivityPub protocol for your content.</p>
<p>ActivityPub is what powers Fediverse apps like Mastodon and making your content available through it makes your site part of the Fediverse itself. Readers can then subscribe using whatever Fediverse app they want.</p>
<p>This involves a lot of work, and is difficult to implement for static sites, so hosted solutions like <a href="https://fed.brid.gy/">Bridgy Fed</a>
 can be used to do this for you.</p>
<h2 id="all-of-the-above">All of the above</h2>
<p>The best option could even be to do all of the above, to give your readers freedom to choose the method that works for them.</p>
<p>If you want the most compatible way to let others follow your posts: start with RSS.</p>
<p>Then add alternatives after.</p>]]></content:encoded><source:markdown>
You&#39;ve got 3 posts on your site and you want people to be notified about new ones.

Here&#39;s all the ways I can think of doing that.

&lt;!--more--&gt;

## Email

Some sites let you send out your posts as emails.

Don&#39;t do it. It can be tempting, because it&#39;s easy, but emails are for emails.

There are better options for your feed.

## RSS

RSS stands for &#34;Really Simple Syndication&#34;.

It&#39;s a standard format for publishing a feed so apps can subscribe to your updates.

Feed readers and podcast apps use it.

It looks a bit like this:

```xml
&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;
&lt;rss version=&#34;2.0&#34;&gt;
  &lt;channel&gt;
    &lt;title&gt;RSS Example Site&lt;/title&gt;
    &lt;link&gt;https://example.com/&lt;/link&gt;
    &lt;description&gt;Example site description.&lt;/description&gt;

    &lt;item&gt;
      &lt;title&gt;Hello, World!&lt;/title&gt;
      &lt;link&gt;https://example.com/hello-world.html&lt;/link&gt;
      &lt;description&gt;My first post.&lt;/description&gt;
    &lt;/item&gt;

  &lt;/channel&gt;
&lt;/rss&gt;
```

You would host a file like this, link to it from the `&lt;head&gt;` part of your pages, and readers would check it occasionally for updates.

Its simplicity means it&#39;s supported by almost all feed reading apps.

Because of this I&#39;d say RSS is the best starting point for letting users subscribe to your posts and is worth adding to your site first, before the other alternatives.

## Atom

RSS isn&#39;t the only way to create an XML feed. Atom is another standard which is more formally specified.

In the early 2000&#39;s, after Atom&#39;s launch, there was a time called the &#34;RSS vs Atom wars&#34;. There was no real winner. Atom was decided to be technically better, but by then RSS had already become dominant.

These days you can add a few Atom elements to your RSS feed, like a `rel=self` link, to get the best of both worlds.

## JSON

[JSON feed](https://www.jsonfeed.org/) is the one of the newer standards. It works in the same way as RSS and Atom but the data is presented as JSON instead of XML.

Web development has moved on from XML and these days JSON-based things are much more common. The idea is that by using JSON we allow developers to build new things like feed readers using modern techniques and tooling, instead of ones used only for legacy publishing steps like an XML-based feed.

It looks a bit like this:

```json
{
  &#34;version&#34;: &#34;https://jsonfeed.org/version/1.1&#34;,
  &#34;title&#34;: &#34;Example JSON Feed Site&#34;,
  &#34;home_page_url&#34;: &#34;https://example.com/&#34;,
  &#34;feed_url&#34;: &#34;https://example.com/feed.json&#34;,
  &#34;items&#34;: [
    {
      &#34;id&#34;: &#34;2026-02-28-153000&#34;,
      &#34;url&#34;: &#34;https://paultibbetts.uk/notes/2026-02-28-153000/&#34;,
      &#34;content_html&#34;: &#34;&lt;p&gt;Hello world&lt;/p&gt;&#34;,
      &#34;date_published&#34;: &#34;2026-02-28T15:30:00Z&#34;
    }
  ]
}
```

JSON feed is already supported by many feed readers and, at least in theory, makes it easier than XML-based feeds to make new ones.

I&#39;d say it&#39;s worth adding to your site after you&#39;ve got an RSS feed, since those are more universally supported right now.

## Microformats

[Microformats](https://indieweb.org/microformats) is a standard for marking up the contents of your posts, and in doing so makes your feed followable without any other files.

Classes like `h-entry` and `h-card` help others to understand the content you&#39;re publishing, and the `h-feed` class is used to identify feeds. This means readers using apps that understand microformats can subscribe to your content directly, without needing an XML or JSON file.

Right now most feed readers expect RSS or Atom feeds, so whilst I would suggest adding microformats, I would do it after adding an RSS feed.

## Social media

There&#39;s also social media, where you would post a link to your content on another site, like Facebook or Instagram.

It&#39;s a great way to get your posts in front of users across the web on the apps they&#39;re already using, but it&#39;s no longer on your own site and you&#39;re now at the mercy of whatever the algorithms think of your content.

I&#39;d consider doing this as an extra, and only after making your own feed available.

For more information on posting out to social media the IndieWeb.org wiki has a great page called [Publish Own Site, Syndicate Elsewhere (POSSE)](https://indieweb.org/POSSE).

## ActivityPub

Another option is to use the ActivityPub protocol for your content.

ActivityPub is what powers Fediverse apps like Mastodon and making your content available through it makes your site part of the Fediverse itself. Readers can then subscribe using whatever Fediverse app they want.

This involves a lot of work, and is difficult to implement for static sites, so hosted solutions like [Bridgy Fed](https://fed.brid.gy/) can be used to do this for you.

## All of the above

The best option could even be to do all of the above, to give your readers freedom to choose the method that works for them.

If you want the most compatible way to let others follow your posts: start with RSS.

Then add alternatives after.
</source:markdown></item>
  </channel>
</rss>
