<?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/</link>
    <description>Posts by Paul Tibbetts published in 2026</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <atom:link href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/feed.xml" rel="self" type="application/rss+xml" /><item>
      <title>Cooked Pulled pork</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/cooks/2026-07-03-pulled-pork/</link>
      <pubDate>Fri, 03 Jul 2026 13:00:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/cooks/2026-07-03-pulled-pork/</guid>
      <description>Pulled pork Duration 9h 34m Time 12:51-22:26 Pulled pork -5C 36C 78C 119C 160C 12:51 16:03 19:14 22:26 Grill Pork Probe First Last Min Max Grill 26.8C 25.9C 25.7C 146C Pork 28.4C 29.7C 9.4C 114.4C</description>
      <content:encoded><![CDATA[<section class="fireboard-archive not-prose max-w-none">
    <div class="fireboard-archive-header">
      <h2>Pulled pork</h2>
      <dl style="display: flex">
        <div>
          <dt>Duration</dt>
          <dd>9h 34m</dd>
        </div>
        <div>
          <dt>Time</dt>
          <dd>12:51-22:26</dd>
        </div>
      </dl>
    </div>

    <figure>
      <svg
        aria-labelledby="fireboard-archive-title-0"
        class="fireboard-archive-chart"
        role="img"
        viewBox="0 0 800 320"
      >
        <title id="fireboard-archive-title-0">Pulled pork</title>
          <line
            class="fireboard-archive-grid"
            x1="48"
            x2="784"
            y1="284"
            y2="284"
          />
          <text class="fireboard-archive-axis" x="0" y="288">-5C</text>
          <line
            class="fireboard-archive-grid"
            x1="48"
            x2="784"
            y1="217.4"
            y2="217.4"
          />
          <text class="fireboard-archive-axis" x="0" y="221.4">36C</text>
          <line
            class="fireboard-archive-grid"
            x1="48"
            x2="784"
            y1="149.2"
            y2="149.2"
          />
          <text class="fireboard-archive-axis" x="0" y="153.2">78C</text>
          <line
            class="fireboard-archive-grid"
            x1="48"
            x2="784"
            y1="82.6"
            y2="82.6"
          />
          <text class="fireboard-archive-axis" x="0" y="86.6">119C</text>
          <line
            class="fireboard-archive-grid"
            x1="48"
            x2="784"
            y1="16"
            y2="16"
          />
          <text class="fireboard-archive-axis" x="0" y="20">160C</text>
          <text
            class="fireboard-archive-axis"
            text-anchor="middle"
            x="48"
            y="312"
          >
            12:51
          </text>
          <text
            class="fireboard-archive-axis"
            text-anchor="middle"
            x="293.3"
            y="312"
          >
            16:03
          </text>
          <text
            class="fireboard-archive-axis"
            text-anchor="middle"
            x="538.7"
            y="312"
          >
            19:14
          </text>
          <text
            class="fireboard-archive-axis"
            text-anchor="middle"
            x="784"
            y="312"
          >
            22:26
          </text>
          <polyline
            class="fireboard-archive-line"
            fill="none"
            points="48,232.3 48.1,232.3 49.4,190.8 49.5,190.8 50.7,167.4 50.8,167.4 52.1,155.5 52.2,155.5 53.4,150.3 53.6,150.3 54.7,147.6 54.8,147.6 56.1,145.6 56.2,145.6 57.4,145 57.5,145 58.8,159.6 58.9,159.6 60.1,187.5 60.2,187.5 61.3,203.4 61.5,203.4 62.7,205.4 62.8,205.4 64.1,193.2 64.2,193.2 65.5,180.5 65.6,180.5 66.8,173.6 66.9,173.6 67,172.3 67.1,172.3 68.1,181.8 68.2,181.8 69.5,185.1 69.6,185.1 70.7,97 70.8,97 72.1,85.2 72.2,85.2 73.4,42.5 73.5,42.5 74.8,53.5 74.9,53.5 76.2,65.7 76.3,65.7 77.6,73 77.7,73 78.9,77.1 79,77.1 80.2,78.9 80.4,78.9 81.6,78.2 81.7,78.2 83,77.7 83.1,77.7 84.3,76.6 84.4,76.6 85.7,76.3 85.8,76.3 87,75.1 87.1,75.1 88.4,73.5 88.5,73.5 89.6,68 89.7,68 90.9,66.4 91,66.4 92.3,65.5 92.4,65.5 93.7,64.2 93.8,64.2 95.1,65.1 95.2,65.1 96.4,65.2 96.5,65.2 97.8,66.2 97.9,66.2 99,65.4 99.1,65.4 100.4,66 100.5,66 101.7,83.6 101.8,83.6 103,172.3 103.1,172.3 104.3,176.6 104.4,176.6 105.7,176 105.8,176 106.9,174.7 107,174.7 108.2,173.4 108.3,173.4 109.5,172.1 109.6,172.1 110.9,171.1 111,171.1 112.2,176.6 112.3,176.6 113.5,121.9 113.6,121.9 114.7,93.3 114.8,93.3 116.1,82.3 116.2,82.3 117.4,81.5 117.5,81.5 118.4,80.5 118.5,80.5 118.7,80.6 118.8,80.6 120,80.2 120.1,80.2 121.2,80.5 121.4,80.5 122.6,80 122.7,80 123.9,79.2 124,79.2 125.2,80 125.3,80 126.5,81.9 126.6,81.9 127.8,84.1 127.9,84.1 129.1,84.9 129.3,84.9 130.4,85.8 130.5,85.8 131.7,88.3 131.8,88.3 133,89.4 133.1,89.4 134.4,91.7 134.5,91.7 135.7,91.5 135.8,91.5 136.9,92.8 137.1,92.8 138.2,95.1 138.3,95.1 139.5,96.2 139.6,96.2 140.9,98.7 141,98.7 142.2,100.6 142.3,100.6 143.5,104 143.6,104 144.7,107.9 144.8,107.9 146.1,110.2 146.2,110.2 147.5,111.8 147.6,111.8 148.8,122.1 148.9,122.1 150.1,137 150.2,137 151.4,166.2 151.5,166.2 152.7,167.7 152.9,167.7 154,125.1 154.1,125.1 155.3,145.1 155.4,145.1 156.6,98.5 156.7,98.5 157.9,96.2 158,96.2 159.3,95.8 159.4,95.8 160.5,96.9 160.6,96.9 161.8,97.4 161.9,97.4 163.1,97.7 163.2,97.7 164.4,98 164.5,98 165.8,99 165.9,99 167.1,97.9 167.2,97.9 168.3,67.2 168.4,67.2 169.6,73.2 169.7,73.2 171,78.5 171.1,78.5 172.3,82.1 172.4,82.1 173.6,84.9 173.7,84.9 174.8,87.5 175,87.5 176,89.3 176.1,89.3 176.2,89.6 176.3,89.6 177.5,102.2 177.6,102.2 178.9,100.9 179,100.9 180.2,101.8 180.3,101.8 181.5,98.8 181.6,98.8 182.8,97.4 182.9,97.4 184.1,96.7 184.2,96.7 185.4,98.3 185.5,98.3 186.7,100 186.8,100 188,103.1 188.1,103.1 189.3,104.8 189.4,104.8 190.7,106.1 190.8,106.1 191.9,111.2 192,111.2 193.3,110.9 193.4,110.9 194.6,118 194.7,118 195.9,119 196,119 197.2,119.5 197.3,119.5 198.6,119.3 198.7,119.3 199.9,111.8 200,111.8 201.2,112.2 201.3,112.2 202.5,106.3 202.6,106.3 203.9,101.9 204,101.9 205.2,96.4 205.3,96.4 206.5,91.9 206.6,91.9 207.7,91.2 207.8,91.2 209,91.7 209.1,91.7 210.4,91.9 210.5,91.9 211.7,93.6 211.8,93.6 213.1,93.2 213.2,93.2 214.4,96.7 214.5,96.7 215.6,97.7 215.7,97.7 216.9,97.5 217,97.5 218.3,98.5 218.4,98.5 219.6,101.1 219.7,101.1 220.9,103.5 221,103.5 222.2,105.5 222.3,105.5 222.5,105.7 222.6,105.7 223.5,106.8 223.6,106.8 224.8,117.8 224.9,117.8 226.1,118.3 226.2,118.3 227.4,117.4 227.5,117.4 228.7,117.4 228.8,117.4 230.1,117 230.2,117 231.3,117 231.4,117 232.6,117.4 232.7,117.4 233.9,116.9 234,116.9 235.3,116.5 235.4,116.5 236.6,116.1 236.7,116.1 238,114.9 238.1,114.9 239.3,114.9 239.4,114.9 240.6,113.9 240.7,113.9 241.9,113.9 242,113.9 243.3,113.1 243.4,113.1 244.6,125.3 244.7,125.3 245.9,102.9 246,102.9 247.2,107 247.3,107 248.5,109.7 248.6,109.7 249.8,110.2 249.9,110.2 251.1,110 251.2,110 252.5,111.3 252.6,111.3 253.8,112.2 253.9,112.2 255.1,111.8 255.3,111.8 256.4,113.3 256.5,113.3 257.7,113.9 257.8,113.9 259,114.9 259.1,114.9 260.4,114.6 260.5,114.6 261.7,114.9 261.8,114.9 262.9,114.6 263,114.6 264.3,116.7 264.4,116.7 265.6,118.5 265.7,118.5 266.9,120.9 267,120.9 268.2,136.8 268.3,136.8 269.6,132.9 269.7,132.9 270.8,130.3 270.9,130.3 272.1,127.7 272.2,127.7 272.4,127.1 272.5,127.1 273.4,125 273.5,125 274.7,123.2 274.8,123.2 276.1,120.6 276.2,120.6 277.4,118.7 277.5,118.7 278.6,116.7 278.7,116.7 279.9,114.6 280,114.6 281.4,112.5 281.5,112.5 282.7,110.4 282.8,110.4 284,110 284.1,110 285.4,110 285.5,110 286.6,111.2 286.7,111.2 287.9,94 288,94 289.2,84.5 289.3,84.5 290.5,84.4 290.6,84.4 291.9,81.1 292,81.1 293.2,74.6 293.3,74.6 294.4,78.7 294.5,78.7 295.7,77.7 295.8,77.7 297,84.7 297.1,84.7 298.4,86.3 298.5,86.3 299.7,87.8 299.8,87.8 300.9,89.6 301.1,89.6 302.3,90.9 302.4,90.9 303.6,99.8 303.7,99.8 305,98.3 305.1,98.3 306.4,98.2 306.5,98.2 307.7,98.2 307.8,98.2 309,97.2 309.1,97.2 310.2,97.5 310.3,97.5 311.5,99 311.6,99 312.9,99.6 313,99.6 314.2,99.2 314.3,99.2 315.5,101.3 315.6,101.3 316.8,102.6 316.9,102.6 318,104.4 318.1,104.4 319.4,104.8 319.5,104.8 320.7,104.8 320.8,104.8 322,103.9 322.1,103.9 323.3,103.5 323.4,103.5 324.5,103.4 324.7,103.4 325.9,103.2 326,103.2 327.2,103.2 327.3,103.2 328.5,102.9 328.6,102.9 329.8,103.1 329.9,103.1 331.1,104.4 331.2,104.4 332.4,107.3 332.6,107.3 333.7,110 333.8,110 335,112.2 335.1,112.2 336.4,113.6 336.5,113.6 337.7,115.4 337.8,115.4 339.1,116.5 339.2,116.5 340.5,117.2 340.6,117.2 341.7,136.5 341.8,136.5 343,133.4 343.1,133.4 344.3,132.3 344.4,132.3 345.5,135.9 345.6,135.5 345.7,135.5 347,133.3 347.1,133.3 348.3,131.6 348.4,131.6 349.5,136.2 349.6,136.2 350.8,125.5 350.9,125.5 352.1,123.2 352.2,123.2 353.5,122.4 353.6,122.4 354.8,121.1 354.9,121.1 356,119.6 356.2,119.6 357.3,117.5 357.4,117.5 358.7,114.6 358.8,114.6 360,112 360.1,112 361.3,106 361.4,106 362.6,103.2 362.7,103.2 364.1,101.3 364.2,101.3 365.4,100.1 365.5,100.1 366.8,104.4 366.9,104.4 368.2,88 368.3,88 369.5,83.7 369.6,83.7 370.9,87.1 371,87.1 372.3,88.1 372.4,88.1 373.6,88.3 373.7,88.3 374.8,85.5 374.9,85.5 376.1,73 376.2,73 377.4,72 377.5,72 378.8,74 378.9,74 380.1,74.8 380.2,74.8 381.4,73.8 381.5,73.8 382.6,72.5 382.7,72.5 383.9,72.2 384,72.2 385.3,72.8 385.4,72.8 386.6,73.3 386.7,73.3 387.9,74.1 388,74.1 389.1,75.1 389.3,75.1 390.4,76.3 390.5,76.3 391.8,75.8 391.9,75.8 393.2,75.1 393.3,75.1 394.5,75.4 394.6,75.4 395.8,77.2 395.9,77.2 397,79.2 397.2,79.2 398.3,79.7 398.4,79.7 399.7,80 399.8,80 401,81 401.1,81 402.4,82.6 402.5,82.6 403.7,83.1 403.8,83.1 404.9,83.4 405.1,83.4 406.2,83.6 406.3,83.6 407.5,84.7 407.6,84.7 408.9,85.7 409,85.7 410.2,86.5 410.3,86.5 411.6,88 411.7,88 412.9,89.3 413,89.3 414.1,90.7 414.2,90.7 415.4,93.2 415.5,93.2 416.8,96.6 416.9,96.6 418.1,98 418.2,98 419.4,99.5 419.5,99.5 420.6,101.3 420.8,101.3 421.9,102.6 422,102.6 423.3,102.7 423.4,102.7 424.6,103.5 424.7,103.5 425.9,103.9 426,103.9 427.2,104.7 427.3,104.7 428.4,105.8 428.5,105.8 429.8,107.3 429.9,107.3 431.1,107.4 431.2,107.4 432.4,108.9 432.5,108.9 433.7,114.1 433.8,114.1 435,110.4 435.1,110.4 436.3,108.9 436.4,108.9 437.6,107.9 437.7,107.9 438.9,108.3 439,108.3 440.3,108.7 440.4,108.7 441.6,109.9 441.7,109.9 442.9,112 443,112 444.1,114.3 444.2,114.3 445.3,126.6 445.4,126.6 445.5,130 445.6,130 446.8,121.4 446.9,121.4 448.1,117 448.2,117 449.5,153.9 449.6,153.9 450.8,65.2 450.9,65.2 452,49.9 452.1,49.9 453.3,47 453.4,47 454.7,56.1 454.8,56.1 456,59 456.1,59 457.4,48.2 457.5,48.2 458.7,53.2 458.8,53.2 459.9,61.3 460,61.3 461.2,52.4 461.3,52.4 462.5,49.1 462.6,49.1 463.9,56 464,56 465.2,62.3 465.3,62.3 466.6,49 466.7,49 467.9,52.5 468.1,52.5 469.2,60 469.3,60 470.5,56.4 470.6,56.4 471.8,48.8 471.9,48.8 473.1,54.3 473.2,54.3 474.5,62.3 474.6,62.3 475.7,52.5 475.8,52.5 477,49.8 477.1,49.8 478.4,56.3 478.5,56.3 479.7,63.3 479.8,63.3 481,50.3 481.1,50.3 482.4,50.9 482.5,50.9 483.6,58.2 483.7,58.2 484.9,62.5 485,62.5 486.3,48.6 486.4,48.6 488,54.5 488.1,54.5 489.3,62.5 489.4,62.5 490.7,69.8 490.8,69.8 492.1,74.5 492.2,74.5 493.4,60.7 493.5,60.7 494.6,62.3 494.7,62.3 496,69.3 496.1,69.3 497.3,75.4 497.4,75.4 498.6,61.5 498.7,61.5 500,60.7 500.1,60.7 501.3,66.7 501.4,66.7 502.6,75 502.8,75 503.9,64.2 504,64.2 505.3,61.3 505.4,61.3 506.6,67.5 506.7,67.5 507.9,74.5 508,74.5 509.4,64.9 509.5,64.9 510.8,62 510.9,62 512.2,68.5 512.3,68.5 513.4,74.5 513.5,74.5 514.5,67.2 514.6,67.2 514.7,64.4 514.8,64.4 516.1,61 516.2,61 517.4,67.2 517.5,67.2 518.9,74.8 519,75.6 519.1,75.6 520.3,62.9 520.4,62.9 521.5,58.9 521.7,58.9 522.8,64.9 522.9,64.9 524.2,72.8 524.3,72.8 525.5,70.6 525.6,70.6 526.8,59.2 526.9,59.2 528.2,63.4 528.3,63.4 529.4,70.4 529.6,70.4 530.8,74.1 530.9,74.1 532.1,61 532.2,61 533.4,64.1 533.5,64.1 534.7,71.1 534.8,71.1 536.1,72.8 536.2,72.8 537.5,60.8 537.6,60.8 538.7,64.2 538.8,64.2 540,70.9 540.1,70.9 541.4,72.2 541.5,72.2 542.7,60.7 542.8,60.7 544,64.7 544.1,64.7 545.5,72.4 545.6,72.4 546.9,69.6 547,69.6 548.1,60.8 548.2,60.8 549.4,65.5 549.5,65.5 550.8,73.3 550.9,73.3 552.1,68 552.2,68 553.4,60.7 553.5,60.7 554.8,66.4 554.9,66.4 556.1,73.5 556.2,73.5 557.4,68.1 557.5,68.1 558.8,60 558.9,60 560.1,65.7 560.2,65.7 561.4,73 561.5,73 562.7,70.2 562.8,70.2 564,60.7 564.2,60.7 565.4,65.7 565.5,65.7 566.8,73.5 566.9,73.5 568.2,67 568.3,67 569.5,61.3 569.6,61.3 570.8,66.8 570.9,66.8 572.2,74.8 572.3,74.8 573.5,64.9 573.7,64.9 574.9,61.6 575,61.6 576.2,68 576.3,68 577.5,75 577.6,75 578.8,65.7 578.9,65.7 580.2,61.8 580.3,61.8 581.4,67.7 581.6,67.7 582.8,74.6 582.9,74.6 584.1,64.9 584.2,64.9 585.4,62 585.5,62 586.7,68 586.8,68 588.1,75.6 588.2,75.6 589.3,64.1 589.5,64.1 590.7,60.8 590.8,60.8 592,67 592.1,67 593.4,74.8 593.5,74.8 594.8,64.7 594.9,64.7 596.1,61 596.2,61 597.4,67 597.5,67 598.9,74.6 599,74.6 600.2,64.7 600.3,64.7 601.5,61.6 601.6,61.6 602.8,67.5 602.9,67.5 604.1,74.8 604.2,74.8 605.5,65.1 605.6,65.1 606.8,61.5 606.9,61.5 608.2,67.8 608.4,67.8 609.6,75.4 609.7,75.4 610.9,65.2 611,65.2 612.2,61.6 612.3,61.6 613.6,68.1 613.7,68.1 614.9,75.1 615,75.1 616.1,65.5 616.3,65.5 617.5,61.8 617.6,61.8 618.9,68.1 619,68.1 620.2,75.1 620.3,75.1 621.5,66.2 621.6,66.2 622.9,61.6 623,61.6 624.2,67.5 624.3,67.5 625.4,74.5 625.5,74.5 626.9,65.1 627,65.1 628.2,54.8 628.3,54.8 628.4,54.8 629.6,60.3 629.7,60.3 630.9,68 631,68 632.3,75.6 632.4,75.6 633.6,63.4 633.7,63.4 634.8,61 634.9,61 636.2,66.8 636.3,66.8 637.5,73.8 637.6,73.8 638.9,68.3 639,68.3 640.2,61.2 640.3,61.2 641.5,66.2 641.6,66.2 642.8,73.7 643,73.7 644.1,70.6 644.2,70.6 645.5,61 645.6,61 646.9,66.4 647,66.4 648.3,73.7 648.4,73.7 649.6,69.8 649.7,69.8 650.9,61.2 651,61.2 652.2,66.5 652.3,66.5 653.5,73.3 653.6,73.3 654.9,71.2 655,71.2 656.2,61 656.3,61 657.5,65.5 657.6,65.5 658.8,72.7 658.9,72.7 660.1,72.8 660.2,72.8 661.4,61.2 661.5,61.2 662.8,64.7 662.9,64.7 664.1,71.9 664.2,71.9 665.4,74.8 665.5,74.8 666.8,61.5 666.9,61.5 668,63.6 668.1,63.6 669.3,70.4 669.4,70.4 670.7,76.4 670.8,76.4 672,62.3 672.1,62.3 673.3,62.5 673.4,62.5 674.6,68.6 674.7,68.6 675.9,75.8 676.1,75.8 677.2,65.1 677.3,65.1 678.5,61.8 678.6,61.8 679.8,67.5 679.9,67.5 681.2,74.6 681.3,74.6 682.5,68.5 682.6,68.5 683.7,61 683.8,61 685,66.2 685.1,66.2 686.4,73.2 686.5,73.2 687.7,64.2 687.8,64.2 689,52.9 689.1,52.9 690.4,57.7 690.5,57.7 691.6,65.2 691.7,65.2 692.9,69.4 693,69.4 694.2,47.7 694.3,47.7 695.6,43 695.7,43 697,50.6 697.1,50.6 698.3,45.4 698.4,45.4 699.2,38.7 699.3,38.7 699.5,39.1 699.6,39.1 700.8,46 700.9,46 702.2,51.6 702.3,51.6 703.5,68.8 703.6,68.8 704.8,156.5 704.9,156.5 706.2,192.9 706.3,192.9 707.4,210.9 707.5,210.9 708.7,219.5 708.8,219.5 710,224.6 710.1,224.6 711.4,227.2 711.5,227.2 712.7,228.3 712.8,228.3 714,229.6 714.1,229.6 715.2,229.9 715.3,229.9 716.6,230.2 716.7,230.2 717.9,229.8 718,229.8 719.2,230.2 719.3,230.2 720.5,231.2 720.6,231.2 721.9,231.5 722,231.5 723.1,230.1 723.2,230.1 724.4,229.6 724.5,229.6 725.7,230.7 725.8,230.7 727.1,229.9 727.2,229.9 728.4,230.6 728.5,230.6 729.7,229.9 729.8,229.9 731,230.2 731.1,230.2 732.3,229.9 732.4,229.9 733.6,231.4 733.7,231.4 734.9,229.6 735,229.6 736.9,230.9 737,230.9 738.2,231.5 738.3,231.5 739.5,231.7 739.6,231.7 740.8,232.2 740.9,232.2 742,232.3 742.1,232.3 743.4,232.8 743.5,232.8 744.7,232.8 744.8,232.8 746,233 746.1,233 747.3,232.7 747.4,232.7 748.7,232.8 748.8,232.8 749.9,232.8 750,232.8 751.2,233.2 751.3,233.2 752.5,233.2 752.6,233.2 753.8,233 753.9,233 755.2,233 755.3,233 756.5,233.2 756.6,233.2 757.7,233.3 757.8,233.3 759,233.5 759.1,233.5 760.4,233.3 760.5,233.3 761.7,233.3 761.8,233.3 762.5,233.3 762.6,233.3 763,233.3 763.1,233.3 764.2,233.5 764.4,233.5 765.6,233.3 765.7,233.3 766.9,233.5 767,233.5 768.2,233.5 768.3,233.5 769.5,233.5 769.6,233.5 770.9,233.6 771,233.6 772.1,233.6 772.3,233.6 773.4,234 773.5,234 774.7,233.8 774.8,233.8 776.1,233.8 776.2,233.8 777.4,233.8 777.5,233.8 778.7,233.8 778.8,233.8 779.9,234.1 780,234.1 781.2,234 781.3,234 782.6,234.1 782.7,234.1 783.9,233.8 784,233.8"
            stroke="#ffb86c"
          />
          <polyline
            class="fireboard-archive-line"
            fill="none"
            points="101.4,229.8 101.5,229.8 102.7,260.6 102.8,260.6 104,260.6 104.1,260.6 105.3,260.6 105.4,260.6 106.6,260.4 106.7,260.4 107.9,260.4 108,260.4 109.2,260.3 109.3,260.3 110.6,260.3 110.7,260.3 111.9,260.1 112,260.1 113.1,260.1 113.2,260.1 114.4,260 114.5,260 115.7,259.8 115.8,259.8 117.1,259.6 117.2,259.6 118.4,259.5 118.5,259.5 119.6,259.3 119.8,259.3 120.9,259.1 121,259.1 122.3,259 122.4,259 123.6,258.8 123.7,258.8 124.7,258.7 124.8,258.7 124.9,258.5 125,258.5 126.2,258.2 126.3,258.2 127.4,257.8 127.5,257.8 128.8,257.5 128.9,257.5 130.1,257.2 130.2,257.2 131.4,256.9 131.5,256.9 132.7,256.4 132.8,256.4 134,255.9 134.1,255.9 135.3,255.4 135.4,255.4 136.6,254.9 136.7,254.9 137.9,254.3 138,254.3 139.2,253.6 139.3,253.6 140.6,253.1 140.7,253.1 141.9,252.5 142,252.5 143.1,251.8 143.2,251.8 144.4,251.2 144.5,251.2 145.7,250.5 145.8,250.5 147.1,249.7 147.2,249.7 148.4,249.1 148.5,249.1 149.6,248.4 149.8,248.4 150.9,247.6 151,247.6 152.2,247 152.3,247 153.6,246.3 153.7,246.3 154.9,245.5 155,245.5 156.2,244.9 156.3,244.9 157.6,244 157.7,244 158.9,243.4 159,243.4 160.2,242.6 160.3,242.6 161.5,241.9 161.6,241.9 162.8,241.3 162.9,241.3 164.1,240.6 164.2,240.6 165.5,239.8 165.6,239.8 166.7,239.2 166.8,239.2 168,238.5 168.1,238.5 169.4,237.7 169.5,237.7 170.8,237.1 170.9,237.1 171.3,236.9 171.4,236.9 172.1,236.4 172.2,236.4 173.4,235.8 173.5,235.8 174.6,235.1 174.7,235.1 175.9,234.5 176,234.5 177.3,233.8 177.4,233.8 178.7,233 178.8,233 180,232.3 180.1,232.3 181.3,231.7 181.4,231.7 182.5,230.9 182.6,230.9 183.8,230.1 183.9,230.1 185.2,229.3 185.3,229.3 186.5,228.5 186.6,228.5 187.8,227.6 187.9,227.6 189,226.8 189.2,226.8 190.4,225.9 190.5,225.9 191.7,224.9 191.8,224.9 193.1,223.9 193.2,223.9 194.4,222.9 194.5,222.9 195.7,222.1 195.8,222.1 197,221.1 197.1,221.1 198.3,220.2 198.4,220.2 199.7,219.2 199.8,219.2 201,218.4 201.1,218.4 202.3,217.6 202.4,217.6 203.7,216.8 203.8,216.8 205,215.9 205.1,215.9 206.2,215.1 206.3,215.1 207.5,214.3 207.6,214.3 208.8,213.5 208.9,213.5 210.2,212.7 210.3,212.7 211.5,212 211.6,212 212.9,211.2 213,211.2 214.1,210.4 214.2,210.4 215.4,209.8 215.5,209.8 216.7,209 216.8,209 218.1,208.3 218.2,208.3 219.4,207.7 219.5,207.7 220.7,206.8 220.8,206.8 221.9,206.2 222,206.2 223.3,205.4 223.4,205.4 224.6,204.7 224.7,204.7 225.9,204.1 226,204.1 227.2,203.3 227.3,203.3 227.4,203.3 227.5,203.3 228.4,202.6 228.6,202.6 229.8,202 229.9,202 231.1,201.3 231.2,201.3 232.4,200.7 232.5,200.7 233.7,200 233.8,200 235.1,199.2 235.2,199.2 236.4,198.6 236.5,198.6 237.7,198.1 237.8,198.1 239.1,197.4 239.2,197.4 240.4,196.8 240.5,196.8 241.7,196.1 241.8,196.1 243.1,195.6 243.2,195.6 244.4,195 244.5,195 245.6,194.5 245.7,194.5 247,193.9 247.1,193.9 248.3,193.4 248.4,193.4 249.6,192.9 249.7,192.9 250.9,192.2 251,192.2 252.3,191.7 252.4,191.7 253.5,191.3 253.6,191.3 254.9,190.8 255,190.8 256.2,190.3 256.3,190.3 257.5,189.8 257.6,189.8 258.8,189.3 258.9,189.3 260.2,188.8 260.3,188.8 261.4,188.3 261.5,188.3 262.7,187.8 262.8,187.8 264.1,187.4 264.2,187.4 265.4,186.9 265.5,186.9 266.5,186.7 266.6,186.7 266.7,186.5 266.8,186.5 268,186.1 268.1,186.1 269.2,185.6 269.3,185.6 270.6,185.2 270.7,185.2 271.9,184.8 272,184.8 273.2,184.3 273.3,184.3 274.5,183.9 274.6,183.9 275.9,183.6 276,183.6 277.1,183.1 277.2,183.1 278.4,182.8 278.5,182.8 279.7,182.5 279.8,182.5 281.4,182 281.5,182 282.7,181.7 282.8,181.7 284,181.3 284.1,181.3 285.4,181 285.5,181 286.6,180.7 286.7,180.7 287.9,180.4 288,180.4 289.2,180 289.3,180 290.5,179.7 290.6,179.7 291.9,179.6 292,179.6 293.2,179.2 293.3,179.2 294.4,178.9 294.5,178.9 295.7,178.6 295.8,178.6 297,178.4 297.1,178.4 298.4,178.1 298.5,178.1 299.7,177.8 299.8,177.8 300.9,177.4 301.1,177.4 302.3,177.1 302.4,177.1 303.6,177 303.7,177 305,176.6 305.1,176.6 306.4,176.3 306.5,176.3 307.7,176.2 307.8,176.2 309,175.8 309.1,175.8 310.2,175.5 310.3,175.5 311.5,175.2 311.6,175.2 312.9,174.9 313,174.9 314.2,174.5 314.3,174.5 315.5,174.2 315.6,174.2 316.8,173.9 316.9,173.9 318,173.6 318.1,173.6 319.4,173.2 319.5,173.2 320.7,172.9 320.8,172.9 322,172.6 322.1,172.6 323.3,172.3 323.4,172.3 324.5,171.9 324.7,171.9 325.9,171.8 326,171.8 327.2,171.6 327.3,171.6 328.5,171.4 328.6,171.4 329.8,171.1 329.9,171.1 331.1,171 331.2,171 332.4,170.8 332.6,170.8 333.7,170.6 333.8,170.6 335,170.3 335.1,170.3 336.4,170.1 336.5,170.1 337.7,170.1 337.8,170.1 339.1,170 339.2,170 339.5,170 339.6,170 340.5,169.8 340.6,169.8 341.7,169.7 341.8,169.7 343,169.5 343.1,169.5 344.3,169.3 344.4,169.3 345.6,169.2 345.7,169.2 347,169 347.1,169 348.3,169 348.4,169 349.5,168.8 349.6,168.8 350.8,168.7 350.9,168.7 352.1,168.7 352.2,168.7 353.5,168.5 353.6,168.5 354.8,168.4 354.9,168.4 356,168.4 356.2,168.4 357.3,168.4 357.4,168.4 358.7,168.2 358.8,168.2 360,168.2 360.1,168.2 361.3,168 361.4,168 362.6,168 362.7,168 364.1,167.9 364.2,167.9 365.4,167.9 365.5,167.9 366.8,167.9 366.9,167.9 368.2,167.7 368.3,167.7 369.5,167.7 369.6,167.7 370.9,167.5 371,167.5 372.3,167.5 372.4,167.5 373.6,167.5 373.7,167.5 374.8,167.4 374.9,167.4 376.1,167.2 376.2,167.2 377.4,167.2 377.5,167.2 378.8,167.1 378.9,167.1 380.1,166.9 380.2,166.9 381.4,166.9 381.5,166.9 382.6,166.7 382.7,166.7 383.9,166.6 384,166.6 385.3,166.4 385.4,166.4 386.6,166.4 386.7,166.4 387.9,166.2 388,166.2 389.1,166.1 389.3,166.1 390.4,165.9 390.5,165.9 391.8,165.8 391.9,165.8 393.2,165.6 393.3,165.6 394.5,165.4 394.6,165.4 395.8,165.3 395.9,165.3 397,165.1 397.2,165.1 398.3,164.9 398.4,164.9 399.7,164.8 399.8,164.8 401,164.5 401.1,164.5 402.4,164.3 402.5,164.3 403.3,164.3 403.5,164.3 403.6,164.3 403.7,164.1 403.8,164.1 404.9,164 405.1,164 406.2,163.8 406.3,163.8 407.5,163.6 407.6,163.6 408.9,163.5 409,163.5 410.2,163.2 410.3,163.2 411.6,163 411.7,163 412.9,162.8 413,162.8 414.1,162.7 414.2,162.7 415.4,162.5 415.5,162.5 416.8,162.5 416.9,162.5 418.1,162.3 418.2,162.3 419.4,162.2 419.5,162.2 420.6,162 420.8,162 421.9,161.9 422,161.9 423.3,161.7 423.4,161.7 424.6,161.5 424.7,161.5 425.9,161.5 426,161.5 427.2,161.4 427.3,161.4 428.4,161.4 428.5,161.4 429.8,161.2 429.9,161.2 431.1,161 431.2,161 432.4,161 432.5,161 433.7,160.9 433.8,160.9 435,160.9 435.1,160.9 436.3,160.7 436.4,160.7 437.6,160.7 437.7,160.7 438.9,160.6 439,160.6 440.3,160.6 440.4,160.6 440.8,160.6 440.9,160.6 441.6,160.4 441.7,160.4 442.9,160.4 443,160.4 444.1,160.4 444.2,160.4 445.4,136.7 445.5,136.7 446.8,98.8 446.9,98.8 448.1,90.1 448.2,90.1 449.5,216.6 449.6,216.6 450.8,165.1 450.9,165.1 452,164.3 452.1,164.3 453.3,164.1 453.4,164.1 454.7,164 454.8,164 456,163.8 456.1,163.8 457.4,163.5 457.5,163.5 458.7,163.3 458.8,163.3 459.9,163.2 460,163.2 461.2,163 461.3,163 462.5,162.7 462.6,162.7 463.9,162.5 464,162.5 465.2,162.3 465.3,162.3 466.6,162.2 466.7,162.2 467.9,161.9 468.1,161.9 469.2,161.7 469.3,161.7 470.5,161.4 470.6,161.4 471.8,161.2 471.9,161.2 473.1,161 473.2,161 474.5,160.7 474.6,160.7 475.7,160.6 475.8,160.6 477,160.2 477.1,160.2 478.4,160.1 478.5,160.1 479.7,159.7 479.8,159.7 481,159.6 481.1,159.6 482.4,159.3 482.5,159.3 483.6,159.1 483.7,159.1 484.9,158.8 485,158.8 486.3,158.6 486.4,158.6 488,157.8 488.1,157.8 489.3,158 489.4,158 490.7,157.8 490.8,157.8 492.1,157.3 492.2,157.3 493.4,157.1 493.5,157.1 494.6,156.7 494.7,156.7 496,156 496.1,156 497.3,155.7 497.4,155.7 498.6,155.8 498.7,155.8 500,155.2 500.1,155.2 501.3,154.7 501.4,154.7 502.6,154.5 502.8,154.5 503.9,154.5 504,154.5 505.3,153.9 505.4,153.9 506.6,153.6 506.7,153.6 507.9,153.4 508,153.4 509.4,153.1 509.5,153.1 510.8,152.6 510.9,152.6 512.2,152.6 512.3,152.6 513.4,151.9 513.5,151.9 514.5,151.6 514.6,151.6 514.7,151.6 514.8,151.6 516.1,151.1 516.2,151.1 517.4,151.3 517.5,151.3 518.9,150.5 519,151.1 519.1,151.1 520.3,150.5 520.4,150.5 521.5,150.3 521.7,150.3 522.8,150 522.9,150 524.2,149.4 524.3,149.4 525.5,149 525.6,149 526.8,148.7 526.9,148.7 528.2,148.4 528.3,148.4 529.4,148.1 529.6,148.1 530.8,148.1 530.9,148.1 532.1,147.6 532.2,147.6 533.4,147.6 533.5,147.6 534.7,147.1 534.8,147.1 536.1,146.4 536.2,146.4 537.5,146.4 537.6,146.4 538.7,146.1 538.8,146.1 540,145.8 540.1,145.8 541.4,145.6 541.5,145.6 542.7,145.1 542.8,145.1 544,145 544.1,145 545.5,144.6 545.6,144.6 546.9,144.5 547,144.5 548.1,144.2 548.2,144.2 549.4,143.8 549.5,143.8 550.8,143.3 550.9,143.3 552.1,143.2 552.2,143.2 553.4,142.9 553.5,142.9 554.8,142.7 554.9,142.7 556.1,142.4 556.2,142.4 557.4,142 557.5,142 558.8,141.7 558.9,141.7 560.1,141.6 560.2,141.6 561.4,141.4 561.5,141.4 562.7,141.2 562.8,141.2 564,140.9 564.2,140.9 565.4,140.4 565.5,140.4 566.8,140.3 566.9,140.3 568.2,140.1 568.3,140.1 569.5,139.8 569.6,139.8 570.8,139.6 570.9,139.6 572.2,139.4 572.3,139.4 573.5,139.1 573.7,139.1 574.9,139 575,139 576.2,138.8 576.3,138.8 577.5,138.5 577.6,138.5 578.8,138.3 578.9,138.3 580.2,138.3 580.3,138.3 581.4,137.7 581.6,137.7 582.8,137.5 582.9,137.5 584.1,137.5 584.2,137.5 585.4,137.2 585.5,137.2 586.7,137.2 586.8,137.2 588.1,137 588.2,137 589.3,136.8 589.5,136.8 590.7,136.7 590.8,136.7 592,136.4 592.1,136.4 593.4,136.4 593.5,136.4 594.8,136 594.9,136 596.1,135.9 596.2,135.9 597.4,135.7 597.5,135.7 598.9,135.5 599,135.5 600.2,135.4 600.3,135.4 601.5,135.2 601.6,135.2 602.8,135.1 602.9,135.1 604.1,135.1 604.2,135.1 605.5,134.9 605.6,134.9 606.8,134.7 606.9,134.7 608.2,134.6 608.4,134.6 609.6,134.2 609.7,134.2 610.9,134.2 611,134.2 612.2,134.1 612.3,134.1 613.6,133.9 613.7,133.9 614.9,133.9 615,133.9 616.1,133.8 616.3,133.8 617.5,133.8 617.6,133.8 618.9,133.6 619,133.6 620.2,133.4 620.3,133.4 621.5,133.4 621.6,133.4 622.9,133.1 623,133.1 624.2,133.1 624.3,133.1 625.4,132.9 625.5,132.9 626.9,132.9 627,132.9 628.3,132.8 628.4,132.8 629.6,132.8 629.7,132.8 630.9,132.6 631,132.6 632.3,132.6 632.4,132.6 633.6,132.5 633.7,132.5 634.5,132.3 634.6,132.3 634.8,132.3 634.9,132.3 636.2,132.1 636.3,132.1 637.5,132.1 637.6,132.1 638.9,132 639,132 640.2,131.8 640.3,131.8 641.5,131.8 641.6,131.8 642.8,131.6 643,131.6 644.1,131.8 644.2,131.8 645.5,131.6 645.6,131.6 646.9,131.5 647,131.5 648.3,131.5 648.4,131.5 649.6,131.3 649.7,131.3 650.9,131.2 651,131.2 652.2,131.3 652.3,131.3 653.5,131.2 653.6,131.2 654.9,131 655,131 656.2,131 656.3,131 657.5,130.8 657.6,130.8 658.8,130.8 658.9,130.8 660.1,130.7 660.2,130.7 661.4,130.7 661.5,130.7 662.8,130.7 662.9,130.7 664.1,130.7 664.2,130.7 665.4,130.5 665.5,130.5 666.8,130.3 666.9,130.3 668,130.3 668.1,130.3 669.3,130.3 669.4,130.3 670.7,130.3 670.8,130.3 672,130.2 672.1,130.2 673.3,130 673.4,130 674.6,130.2 674.7,130.2 675.9,130 676.1,130 677.2,130 677.3,130 678.5,130 678.6,130 679.8,129.9 679.9,129.9 681.2,129.9 681.3,129.9 682.5,129.9 682.6,129.9 683.7,129.7 683.8,129.7 685,129.7 685.1,129.7 686.4,129.7 686.5,129.7 687.7,129.5 687.8,129.5 689,129.5 689.1,129.5 690.4,129.5 690.5,129.5 691.6,129.4 691.7,129.4 692.9,129.4 693,129.4 694.2,129.4 694.3,129.4 695.6,129.2 695.7,129.2 697,129.2 697.1,129.2 698.3,129.2 698.4,129.2 699.2,129.2 699.3,129.2 699.5,129.2 699.6,129.2 700.8,129 700.9,129 702.2,128.9 702.3,128.9 703.5,128.7 703.6,128.7 704.8,128.7 704.9,128.7 705.1,128.7 705.2,128.7 706.2,128.4 706.3,128.4 707.4,128.4 707.5,128.4 708.7,128.4 708.8,128.4 710,128.2 710.1,128.2 711.4,128.4 711.5,128.4 712.7,128.2 712.8,128.2 714,128.4 714.1,128.4 715.2,128.4 715.3,128.4 716.6,128.2 716.7,128.2 717.9,128.6 718,128.6 719.2,128.4 719.3,128.4 720.5,128.6 720.6,128.6 721.9,128.7 722,128.7 723.1,129 723.2,129 724.4,129 724.5,129 725.7,129.4 725.8,129.4 727.1,129.5 727.2,129.5 728.4,130.2 728.5,130.2 729.7,130.5 729.8,130.5 731,130.7 731.1,130.7 732.3,130.8 732.4,130.8 733.6,131.3 733.7,131.3 734.9,131.5 735,131.5 736.9,204.4 737,204.4 738.2,218.4 738.3,218.4 739.5,221.1 739.6,221.1 740.8,222.4 740.9,222.4 742,222.8 742.1,222.8 743.4,223.3 743.5,223.3 744.7,223.4 744.8,223.4 746,223.6 746.1,223.6 747.3,224.1 747.4,224.1 748.7,224.2 748.8,224.2 749.9,224.6 750,224.6 751.2,224.6 751.3,224.6 752.5,224.6 752.6,224.6 753.8,224.7 753.9,224.7 755.2,224.9 755.3,224.9 756.5,224.7 756.6,224.7 757.7,225.4 757.8,225.4 759,225.4 759.1,225.4 760.4,225.4 760.5,225.4 761.7,225.5 761.8,225.5 762.5,225.9 762.6,225.9 763,225.7 763.1,225.7 764.2,226 764.4,226 765.6,225.9 765.7,225.9 766.9,226 767,226 768.2,226.2 768.3,226.2 769.5,226.3 769.6,226.3 770.9,226.5 771,226.5 772.1,226.7 772.3,226.7 773.4,226.7 773.5,226.7 774.7,227 774.8,227 776.1,226.8 776.2,226.8 777.4,227.2 777.5,227.2 778.7,227.2 778.8,227.2 779.9,227.5 780,227.5 781.2,227.3 781.3,227.3 782.6,227.6 782.7,227.6 783.9,227.6 784,227.6"
            stroke="#f51a6f"
          />
      </svg>
      <figcaption>
          <span>
            <span class="fireboard-archive-swatch" style="background-color: #ffb86c"></span>
            Grill
          </span>
          <span>
            <span class="fireboard-archive-swatch" style="background-color: #f51a6f"></span>
            Pork
          </span>
      </figcaption>
    </figure>

    <table>
      <thead>
        <tr>
          <th>Probe</th>
          <th>First</th>
          <th>Last</th>
          <th>Min</th>
          <th>Max</th>
        </tr>
      </thead>
      <tbody>
          <tr>
            <th>Grill</th>
            <td>26.8C</td>
            <td>25.9C</td>
            <td>25.7C</td>
            <td>146C</td>
          </tr>
          <tr>
            <th>Pork</th>
            <td>28.4C</td>
            <td>29.7C</td>
            <td>9.4C</td>
            <td>114.4C</td>
          </tr>
      </tbody>
    </table>
  </section>]]></content:encoded><source:markdown>
{{&lt; fireboard-archive &gt;}}

</source:markdown></item><item>
      <title>HomeOps Tour: Hardware</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/21/homeops-tour-hardware/</link>
      <pubDate>Thu, 21 May 2026 13:35:06 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/21/homeops-tour-hardware/</guid>
      <category>homelab</category>
      <category>homeops</category>
      <description>The first stop on my HomeOps Tour covers the hardware I use.</description>
      <content:encoded><![CDATA[<p>The first stop on my HomeOps Tour covers the hardware I use.</p>
<p>It&rsquo;s a mix of &ldquo;too expensive&rdquo; and &ldquo;I should have spent more on this&rdquo;.</p>
<p>None of this was designed all at once. It&rsquo;s the result of buying things for one reason, repurposing them for another, and slowly discovering a stable setup.</p>
<h2 id="what-its-for">What it&rsquo;s for</h2>
<p>HomeOps is the boring half of my homelab: the part that runs the services I actually depend on.</p>
<p>It started as an all-in-one server, but I eventually moved away from that. It was convenient until it wasn&rsquo;t; every change was risky.</p>
<p>Splitting it up gives me more to manage, but also a smaller blast radius when something goes wrong.</p>
<h2 id="overview">Overview</h2>
<table>
	<thead>
			<tr>
					<th>Device</th>
					<th>Role</th>
					<th>Purpose</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>NAS</td>
					<td>Storage</td>
					<td>archiving, media, network shares</td>
			</tr>
			<tr>
					<td>NUC</td>
					<td>Server</td>
					<td>apps, databases, services</td>
			</tr>
			<tr>
					<td>VPS</td>
					<td>Tunnel</td>
					<td>external entry point for tunnel access</td>
			</tr>
			<tr>
					<td>Pi</td>
					<td>Monitoring</td>
					<td>monitors everything else</td>
			</tr>
	</tbody>
</table>
<p>Each of these devices could do more than they currently do, but I&rsquo;ve tried to keep their roles separate.</p>
<p>They are more like pets than cattle: the software is reproducible but the machines are not disposable.</p>
<h2 id="nas">NAS</h2>
<p>I have a dedicated machine for Network Attached Storage. I built it myself; the specs are <a href="https://github.com/paultibbetts/homeops/tree/main/hardware/nas">in the repo</a>
. It runs <a href="https://www.truenas.com/">TrueNAS</a>
 and is the central place for storage, backups, archiving, and media.</p>
<p>Storage is shared over NFS for Mac and Linux, and SMB for Windows. The only app the NAS runs is <a href="https://www.min.io/">minIO</a>
, which provides S3-compatible storage. minIO changed its license recently so I am eventually going to swap it for something else.</p>
<p>It has 4TB of NVMe storage, which is the pool I use most often. My NAS and PC are connected through a small 10GbE switch, so from my PC it feels like the drives are plugged in directly. These drives are <strong>not</strong> set up in a RAID array, so if one failed I would lose data.</p>
<p>For archiving and longer-term storage it has 4 x 6TB HDD set up in a RAIDZ2 array, which means 2 of the drives could fail before I lost data. Anything important is copied to the RAIDZ2 pool, which is why I&rsquo;m comfortable running the NVMe drives without redundancy. <a href="https://www.raidisnotabackup.com/">RAID is not a backup</a>
, so I <em>should</em> back this data up to an external location, but I&rsquo;m yet to set this up.</p>
<p>It was originally made up of spare parts from an old gaming PC but I decided I wanted <abbr title="Error Correcting Code">ECC</abbr> memory. That meant replacing the CPU and motherboard with parts that supported it, and the affordable options were older server components. At this point the only gaming PC parts still in use are the case and the PSU.</p>
<h3 id="review">Review</h3>
<p>Those older server components are less efficient and more power-hungry than newer ones would be.</p>
<p>It was expensive. I made the decision that storage was important enough to justify ECC memory, but plenty of people choose to go without it. ECC is there to protect against memory-related corruption, and I don&rsquo;t know whether it&rsquo;s ever actually done that.</p>
<p>It&rsquo;s overpowered and underutilised for its current role, mostly because it used to do everything and now it just does storage.</p>
<p>However, it plays a major part in my HomeOps setup, so it&rsquo;s definitely worth its place.</p>
<p>If you have a homelab, or even just multiple computers in one household, you would benefit from a NAS. It doesn&rsquo;t need to be an expensive one like mine though.</p>
<h2 id="nuc">NUC</h2>
<p>I use an Intel NUC as my server. Specs are <a href="https://github.com/paultibbetts/homeops/tree/main/hardware/nuc">in the repo</a>
.</p>
<p>Originally I used my NAS as a server, until I found that messing with it made it unstable, so I stopped doing that and looked for something else. I bought something from Kickstarter, and in true Kickstarter fashion it was delayed multiple times, and eventually I got impatient and bought the NUC.</p>
<p>It&rsquo;s modern and pretty power efficient, which means running it is much cheaper than the NAS made out of older components.</p>
<p>The CPU has 12 cores and 16 threads, which gives me plenty of headroom for virtualisation. I have <a href="https://www.proxmox.com/en/products/proxmox-virtual-environment/overview">Proxmox</a>
 installed and use it to run 9 virtual machines and 1 LXC. This lets me split services across separate environments instead of installing everything directly on one host, so a problem with one service is less likely to affect the others.</p>
<p>The LXC is for Jellyfin, which is self-hosted Netflix for movies I store on my NAS. The NUC has an integrated Intel GPU that supports <em>Quick Sync</em>, which Jellyfin uses for hardware-accelerated transcoding. This means I can watch my movies from any device and let the GPU do the transcoding, leaving the CPU available for the other services hosted by the NUC.</p>
<h3 id="review-1">Review</h3>
<p>The NUC is an excellent home server. It&rsquo;s not enterprise hardware with redundant power supplies and room for expansion, but its size and efficiency make it a great fit at home.</p>
<p>The trade-off is that it&rsquo;s not a box for lots of disks. Mine has room for an SSD boot drive and the NVMe I use for local data. After that I needed to pair it with my NAS for extra storage.</p>
<p>It has a 2.5GbE NIC. That is not as fast as the 10GbE link to the NAS, but is more than enough for what I use it for.</p>
<p>If I started again and didn&rsquo;t need much local storage, a machine like this might be all I needed.</p>
<p>Unfortunately Intel has stopped making these but places like <a href="https://www.minisforum.uk/">Minisforum</a>
 make ones that are similar.</p>
<h2 id="vps">VPS</h2>
<p>The VPS is the only machine in this setup that I don&rsquo;t physically own. It&rsquo;s rented from <a href="https://www.hetzner.com/">Hetzner</a>
.</p>
<p>It is the public side of my &ldquo;tunnel&rdquo; setup, which I&rsquo;ll explain in a future post. The services I run are hosted at home and this VPS gives me an entry point without directly exposing my home network.</p>
<p>There isn&rsquo;t much to say about it. Hetzner is some of the cheapest European hosting I could find, and even though the VPS is located in Germany it doesn&rsquo;t add any real delay to my connection.</p>
<h2 id="monpi">MonPi</h2>
<p>The final part of the setup is the Raspberry Pi that monitors everything else.</p>
<p>The reason it exists is separation. I didn&rsquo;t want to run monitoring on the same machines that host the services. If the NAS or the NUC has a problem, a separate device has a better chance of catching it.</p>
<p>It runs two types of monitoring: Uptime Kuma for service uptime checks, and Beszel for host metrics. I&rsquo;ll cover these in more detail later in the series.</p>
<h3 id="review-2">Review</h3>
<p>The Pi works great for monitoring, but for this particular model that&rsquo;s about all it can do.</p>
<p>It was originally called &ldquo;DeskPi&rdquo; and was plugged into a screen to show a live output, but because it has only 2GB of RAM it struggled to run the desktop environment and I quickly gave up on that idea.</p>
<h2 id="whats-missing">What&rsquo;s missing</h2>
<p>There are two pieces of hardware people might expect to see listed here that I don&rsquo;t yet have: a custom router and a <abbr title="Uninterrupted Power Supply">UPS</abbr>.</p>
<p>I still use the router my ISP gave me, instead of something like OPNsense or pfSense. I&rsquo;ve worked around some of its limitations by letting Pi-hole handle DHCP - more details on that in the Pi-hole post. I will be moving soon, and when I get settled I&rsquo;ll start working on the router.</p>
<p>The UPS will come after. It is risky running a NAS without one, but I live in the UK, and in the five years I&rsquo;ve lived in this house there&rsquo;s been exactly one power cut, so I haven&rsquo;t felt the need to prioritise it.</p>
<h2 id="not-highly-available">Not Highly Available</h2>
<p>The obvious limitation of my setup is that it&rsquo;s not highly available.</p>
<p>Splitting storage, compute and monitoring across different machines gives me cleaner roles and a smaller blast radius than the all-in-one server I started with. It does not mean the setup can survive any of those machines breaking.</p>
<p>If the NUC dies, my services go offline. If the NAS fails, the storage layer is unavailable. If the VPS has a problem, friends and family can&rsquo;t get through the tunnel. MonPi monitors the setup, but nothing is monitoring MonPi.</p>
<p>There is an old redundancy saying that &ldquo;two is one and one is none&rdquo;. That applies here, but in different ways at different layers. The NAS has redundant disks, but it is still a single machine with one motherboard, one power supply and one network path. The NUC is a single host, so the VMs and containers running on it have nowhere else to fail over to.</p>
<p>Making the NUC highly available would mean running multiple Proxmox nodes. That means not just two machines, but three, so Proxmox can <a href="https://pve.proxmox.com/wiki/High_Availability">maintain quorum</a>
.</p>
<p>That would be a better <abbr title="High Availability">HA</abbr> design, but it would also mean more hardware and more power usage.</p>
<p>For now, I have accepted that my setup might have downtime because that is the trade-off that keeps it affordable and manageable at home.</p>
<h2 id="next">Next</h2>
<p>That&rsquo;s the hardware covered for my setup.</p>
<p>The important thing is not that any individual machine is special, it&rsquo;s that each one has a clear job. The NAS stores things, the NUC runs things, the VPS provides the public entry point, and the Pi monitors it all.</p>
<p>It&rsquo;s not perfect, and it&rsquo;s not highly available, but it&rsquo;s much easier to work with than the all-in-one server I started with.</p>
<p>Next I&rsquo;ll look at the storage layer in more detail: how I use TrueNAS for network shares, archives, and the storage services that support the rest of the setup.</p>]]></content:encoded><source:markdown>
The first stop on my HomeOps Tour covers the hardware I use.

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

It&#39;s a mix of &#34;too expensive&#34; and &#34;I should have spent more on this&#34;.

None of this was designed all at once. It&#39;s the result of buying things for one reason, repurposing them for another, and slowly discovering a stable setup.

## What it&#39;s for

HomeOps is the boring half of my homelab: the part that runs the services I actually depend on.

It started as an all-in-one server, but I eventually moved away from that. It was convenient until it wasn&#39;t; every change was risky.

Splitting it up gives me more to manage, but also a smaller blast radius when something goes wrong.

## Overview

| Device | Role       | Purpose                                |
| ------ | ---------- | -------------------------------------- |
| NAS    | Storage    | archiving, media, network shares       |
| NUC    | Server     | apps, databases, services              |
| VPS    | Tunnel     | external entry point for tunnel access |
| Pi     | Monitoring | monitors everything else               |

Each of these devices could do more than they currently do, but I&#39;ve tried to keep their roles separate.

They are more like pets than cattle: the software is reproducible but the machines are not disposable.

## NAS

I have a dedicated machine for Network Attached Storage. I built it myself; the specs are [in the repo](https://github.com/paultibbetts/homeops/tree/main/hardware/nas). It runs [TrueNAS](https://www.truenas.com/) and is the central place for storage, backups, archiving, and media.

Storage is shared over NFS for Mac and Linux, and SMB for Windows. The only app the NAS runs is [minIO](https://www.min.io/), which provides S3-compatible storage. minIO changed its license recently so I am eventually going to swap it for something else.

It has 4TB of NVMe storage, which is the pool I use most often. My NAS and PC are connected through a small 10GbE switch, so from my PC it feels like the drives are plugged in directly. These drives are **not** set up in a RAID array, so if one failed I would lose data.

For archiving and longer-term storage it has 4 x 6TB HDD set up in a RAIDZ2 array, which means 2 of the drives could fail before I lost data. Anything important is copied to the RAIDZ2 pool, which is why I&#39;m comfortable running the NVMe drives without redundancy. [RAID is not a backup](https://www.raidisnotabackup.com/), so I _should_ back this data up to an external location, but I&#39;m yet to set this up.

It was originally made up of spare parts from an old gaming PC but I decided I wanted &lt;abbr title=&#34;Error Correcting Code&#34;&gt;ECC&lt;/abbr&gt; memory. That meant replacing the CPU and motherboard with parts that supported it, and the affordable options were older server components. At this point the only gaming PC parts still in use are the case and the PSU.

### Review

Those older server components are less efficient and more power-hungry than newer ones would be.

It was expensive. I made the decision that storage was important enough to justify ECC memory, but plenty of people choose to go without it. ECC is there to protect against memory-related corruption, and I don&#39;t know whether it&#39;s ever actually done that.

It&#39;s overpowered and underutilised for its current role, mostly because it used to do everything and now it just does storage.

However, it plays a major part in my HomeOps setup, so it&#39;s definitely worth its place.

If you have a homelab, or even just multiple computers in one household, you would benefit from a NAS. It doesn&#39;t need to be an expensive one like mine though.

## NUC

I use an Intel NUC as my server. Specs are [in the repo](https://github.com/paultibbetts/homeops/tree/main/hardware/nuc).

Originally I used my NAS as a server, until I found that messing with it made it unstable, so I stopped doing that and looked for something else. I bought something from Kickstarter, and in true Kickstarter fashion it was delayed multiple times, and eventually I got impatient and bought the NUC.

It&#39;s modern and pretty power efficient, which means running it is much cheaper than the NAS made out of older components.

The CPU has 12 cores and 16 threads, which gives me plenty of headroom for virtualisation. I have [Proxmox](https://www.proxmox.com/en/products/proxmox-virtual-environment/overview) installed and use it to run 9 virtual machines and 1 LXC. This lets me split services across separate environments instead of installing everything directly on one host, so a problem with one service is less likely to affect the others.

The LXC is for Jellyfin, which is self-hosted Netflix for movies I store on my NAS. The NUC has an integrated Intel GPU that supports _Quick Sync_, which Jellyfin uses for hardware-accelerated transcoding. This means I can watch my movies from any device and let the GPU do the transcoding, leaving the CPU available for the other services hosted by the NUC.

### Review

The NUC is an excellent home server. It&#39;s not enterprise hardware with redundant power supplies and room for expansion, but its size and efficiency make it a great fit at home.

The trade-off is that it&#39;s not a box for lots of disks. Mine has room for an SSD boot drive and the NVMe I use for local data. After that I needed to pair it with my NAS for extra storage.

It has a 2.5GbE NIC. That is not as fast as the 10GbE link to the NAS, but is more than enough for what I use it for.

If I started again and didn&#39;t need much local storage, a machine like this might be all I needed.

Unfortunately Intel has stopped making these but places like [Minisforum](https://www.minisforum.uk/) make ones that are similar.

## VPS

The VPS is the only machine in this setup that I don&#39;t physically own. It&#39;s rented from [Hetzner](https://www.hetzner.com/).

It is the public side of my &#34;tunnel&#34; setup, which I&#39;ll explain in a future post. The services I run are hosted at home and this VPS gives me an entry point without directly exposing my home network.

There isn&#39;t much to say about it. Hetzner is some of the cheapest European hosting I could find, and even though the VPS is located in Germany it doesn&#39;t add any real delay to my connection.

## MonPi

The final part of the setup is the Raspberry Pi that monitors everything else.

The reason it exists is separation. I didn&#39;t want to run monitoring on the same machines that host the services. If the NAS or the NUC has a problem, a separate device has a better chance of catching it.

It runs two types of monitoring: Uptime Kuma for service uptime checks, and Beszel for host metrics. I&#39;ll cover these in more detail later in the series.

### Review

The Pi works great for monitoring, but for this particular model that&#39;s about all it can do.

It was originally called &#34;DeskPi&#34; and was plugged into a screen to show a live output, but because it has only 2GB of RAM it struggled to run the desktop environment and I quickly gave up on that idea.

## What&#39;s missing

There are two pieces of hardware people might expect to see listed here that I don&#39;t yet have: a custom router and a &lt;abbr title=&#34;Uninterrupted Power Supply&#34;&gt;UPS&lt;/abbr&gt;.

I still use the router my ISP gave me, instead of something like OPNsense or pfSense. I&#39;ve worked around some of its limitations by letting Pi-hole handle DHCP - more details on that in the Pi-hole post. I will be moving soon, and when I get settled I&#39;ll start working on the router.

The UPS will come after. It is risky running a NAS without one, but I live in the UK, and in the five years I&#39;ve lived in this house there&#39;s been exactly one power cut, so I haven&#39;t felt the need to prioritise it.

## Not Highly Available

The obvious limitation of my setup is that it&#39;s not highly available.

Splitting storage, compute and monitoring across different machines gives me cleaner roles and a smaller blast radius than the all-in-one server I started with. It does not mean the setup can survive any of those machines breaking.

If the NUC dies, my services go offline. If the NAS fails, the storage layer is unavailable. If the VPS has a problem, friends and family can&#39;t get through the tunnel. MonPi monitors the setup, but nothing is monitoring MonPi.

There is an old redundancy saying that &#34;two is one and one is none&#34;. That applies here, but in different ways at different layers. The NAS has redundant disks, but it is still a single machine with one motherboard, one power supply and one network path. The NUC is a single host, so the VMs and containers running on it have nowhere else to fail over to.

Making the NUC highly available would mean running multiple Proxmox nodes. That means not just two machines, but three, so Proxmox can [maintain quorum](https://pve.proxmox.com/wiki/High_Availability).

That would be a better &lt;abbr title=&#34;High Availability&#34;&gt;HA&lt;/abbr&gt; design, but it would also mean more hardware and more power usage.

For now, I have accepted that my setup might have downtime because that is the trade-off that keeps it affordable and manageable at home.

## Next

That&#39;s the hardware covered for my setup.

The important thing is not that any individual machine is special, it&#39;s that each one has a clear job. The NAS stores things, the NUC runs things, the VPS provides the public entry point, and the Pi monitors it all.

It&#39;s not perfect, and it&#39;s not highly available, but it&#39;s much easier to work with than the all-in-one server I started with.

Next I&#39;ll look at the storage layer in more detail: how I use TrueNAS for network shares, archives, and the storage services that support the rest of the setup.
</source:markdown></item><item>
      <title>Liked: a quote reposted by Ricardo Chávez</title>
      <link>https://www.ricardochavezt.com/2026/05/14/816614305386708992/</link>
      <pubDate>Thu, 21 May 2026 10:35:23 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/21/a-quote-reposted-by-ricardo-chavez/</guid>
      <description></description>
      <content:encoded><![CDATA[]]></content:encoded></item><item>
      <title>Replied to: paulopinto.xyz</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/20/obsidian-paulo-pinto/</link>
      <pubDate>Wed, 20 May 2026 22:01:06 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/20/obsidian-paulo-pinto/</guid>
      <description>This captures what makes these tools appealing to me too. I renamed my “notes” directory to “thoughts” because it’s where I now do all of my thinking.
Logseq seems to suit my brain more than Obsidian, but with either tool the best feature is the cross-linking of ideas. I will sometimes forget where I wrote a note, but I remember that it linked to other ideas, so I can go to them to find the one I am looking for.</description>
      <content:encoded><![CDATA[<p>This captures what makes these tools appealing to me too. I renamed my &ldquo;notes&rdquo; directory to &ldquo;thoughts&rdquo; because it&rsquo;s where I now do all of my thinking.</p>
<p><a href="https://logseq.com/">Logseq</a>
 seems to suit my brain more than Obsidian, but with either tool the best feature is the cross-linking of ideas. I will sometimes forget where I wrote a note, but I remember that it linked to other ideas, so I can go to them to find the one I am looking for.</p>
<p>And I especially like finding out I have written about an idea before, and that I can see how my thinking has evolved since then.</p>]]></content:encoded><source:markdown>
This captures what makes these tools appealing to me too. I renamed my &#34;notes&#34; directory to &#34;thoughts&#34; because it&#39;s where I now do all of my thinking.

[Logseq](https://logseq.com/) seems to suit my brain more than Obsidian, but with either tool the best feature is the cross-linking of ideas. I will sometimes forget where I wrote a note, but I remember that it linked to other ideas, so I can go to them to find the one I am looking for.

And I especially like finding out I have written about an idea before, and that I can see how my thinking has evolved since then.
</source:markdown></item><item>
      <title>Liked: On the quiet pleasures of the small web — Paulo Pinto</title>
      <link>https://paulopinto.xyz/notes/2026/05/2026-05-17-on-the-small-web/</link>
      <pubDate>Wed, 20 May 2026 21:42:11 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/20/on-the-quiet-pleasures-of-the-small-web-paulo-pinto/</guid>
      <category>IndieWeb</category>
      <description></description>
      <content:encoded><![CDATA[]]></content:encoded></item><item>
      <title>HomeOps Tour: Overview</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/06/homeops-tour-overview/</link>
      <pubDate>Wed, 06 May 2026 21:05:00 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/05/06/homeops-tour-overview/</guid>
      <category>homelab</category>
      <category>homeops</category>
      <description>Welcome to my HomeOps Tour, where I’ll be your guide to the stable part of my Homelab.</description>
      <content:encoded><![CDATA[<p>Welcome to my HomeOps Tour, where I&rsquo;ll be your guide to the stable part of my Homelab.</p>
<h2 id="what-is-homeops">What is HomeOps?</h2>
<p>HomeOps isn&rsquo;t a widely used term, but I have seen a few people use it. To explain what I mean by it I should first define &ldquo;Homelab&rdquo;.</p>
<p>A homelab is a setup of computers you control and use for experiments - like a laboratory. It&rsquo;s usually at home, hence the name, but not always. You might own all the equipment, rent it, or even send your own machines to a provider so they can run them for you - called &ldquo;colocation&rdquo;.</p>
<p>The problem is that homelab seems to cover experimentation as well as self-hosting services you use every day, which have different needs.</p>
<p>HomeOps, short for &ldquo;Home Operations&rdquo;, is my attempt at labelling the stable part of my homelab.</p>
<h2 id="purpose">Purpose</h2>
<p>My HomeOps setup is used to host the apps and services a few people depend on, so I can&rsquo;t be breaking it every five minutes.</p>
<p>Some people call this &ldquo;HomeProd&rdquo;, short for &ldquo;Home Production&rdquo;, which separates it from the &ldquo;development&rdquo; or &ldquo;staging&rdquo; environment you use for experiments.</p>
<p>As such I treat it like a real production system: stable, reproducible, and intentionally boring to operate.</p>
<h2 id="architecture">Architecture</h2>
<p>The system is split over different machines, each with a different purpose.</p>
<ul>
<li>a <abbr title="Network Attached Storage">NAS</abbr> provides storage over the network</li>
<li>a server hosts all the apps and services</li>
<li>a rented <abbr title="Virtual Private Server">VPS</abbr> lets external users in securely through a &ldquo;tunnel&rdquo;</li>
<li>a Raspberry Pi monitors it all</li>
</ul>
<p>The whole thing is written down as Infrastructure as Code, with Terraform defining the infrastructure and Ansible for configuring it. This would let me reproduce it if a machine breaks and I need to set up a new one.</p>
<p>The server uses a hypervisor as an operating system and services generally get their own virtual machine. One of those VMs uses Docker to run most of the apps.</p>
<h2 id="key-decisions">Key Decisions</h2>
<p>Each of these deserve their own post later in the series.</p>
<h3 id="separating-homeops-from-homelab">Separating HomeOps from Homelab</h3>
<p>I made this split after <em>the data loss incident</em> which taught me not to let others depend on apps I&rsquo;m only trialing in my lab, especially without working backups.</p>
<h3 id="infrastructure-as-code">Infrastructure as code</h3>
<p>Everything is written down as code, so there&rsquo;s as little manual setup as possible. This means I can see what&rsquo;s going on by inspecting the code, and I can automate configuration.</p>
<h3 id="terraform-and-ansible-not-kubernetes">Terraform and Ansible, not Kubernetes</h3>
<p>When I started I wanted to compare Terraform and Ansible against Kubernetes for running a homelab.</p>
<p>I thought I&rsquo;d prove that Kubernetes was obviously the better choice, but two years later I&rsquo;ve found the Terraform and Ansible setup works just fine, so that&rsquo;s what powers my HomeOps.</p>
<p>I use Kubernetes in its own environment where I run experiments.</p>
<h3 id="not-highly-available">Not highly available</h3>
<p>One downside to my setup is that it&rsquo;s not highly available. If a machine breaks then there&rsquo;s nothing to fill in for it until I replace it. I could fix this by running more machines, but that increases the cost.</p>
<p>For now I can live with the downtime. So far nothing&rsquo;s broken, although one day something will.</p>
<h2 id="what-this-series-will-cover">What this series will cover</h2>
<p>In this series I&rsquo;ll go over each part of the system in more detail, including the hardware, IaC, services that run on top, and how I let people access it from outside my home.</p>
<p>The aim is not to present this as the correct way to run a homelab, but to explain the choices I made, how they fit together, and why I use the apps that I do.</p>
<p>It will be split up so you can dive in and out at any point to read just the parts that interest you, with links to anything written earlier that provides necessary context.</p>
<p>It might be awkward in that the system builds up in layers, and things may only make sense for my particular setup, but I&rsquo;ll try my best to generalise each part so it&rsquo;s relevant on its own.</p>
<h2 id="next">Next</h2>
<p>Coming up first is the hardware I use.</p>
<p>Check back soon for the link, or subscribe to the <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/series/homeops-tour/feed.xml">feed for this series</a>
 in your feed reader.</p>
<p>Or jump straight into the code, which is now on <a href="https://github.com/paultibbetts/homeops">GitHub</a>
.</p>]]></content:encoded><source:markdown>
Welcome to my HomeOps Tour, where I&#39;ll be your guide to the stable part of my Homelab.

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

## What is HomeOps?

HomeOps isn&#39;t a widely used term, but I have seen a few people use it. To explain what I mean by it I should first define &#34;Homelab&#34;.

A homelab is a setup of computers you control and use for experiments - like a laboratory. It&#39;s usually at home, hence the name, but not always. You might own all the equipment, rent it, or even send your own machines to a provider so they can run them for you - called &#34;colocation&#34;.

The problem is that homelab seems to cover experimentation as well as self-hosting services you use every day, which have different needs.

HomeOps, short for &#34;Home Operations&#34;, is my attempt at labelling the stable part of my homelab.

## Purpose

My HomeOps setup is used to host the apps and services a few people depend on, so I can&#39;t be breaking it every five minutes.

Some people call this &#34;HomeProd&#34;, short for &#34;Home Production&#34;, which separates it from the &#34;development&#34; or &#34;staging&#34; environment you use for experiments.

As such I treat it like a real production system: stable, reproducible, and intentionally boring to operate.

## Architecture

The system is split over different machines, each with a different purpose.

- a &lt;abbr title=&#34;Network Attached Storage&#34;&gt;NAS&lt;/abbr&gt; provides storage over the network
- a server hosts all the apps and services
- a rented &lt;abbr title=&#34;Virtual Private Server&#34;&gt;VPS&lt;/abbr&gt; lets external users in securely through a &#34;tunnel&#34;
- a Raspberry Pi monitors it all

The whole thing is written down as Infrastructure as Code, with Terraform defining the infrastructure and Ansible for configuring it. This would let me reproduce it if a machine breaks and I need to set up a new one.

The server uses a hypervisor as an operating system and services generally get their own virtual machine. One of those VMs uses Docker to run most of the apps.

## Key Decisions

Each of these deserve their own post later in the series.

### Separating HomeOps from Homelab

I made this split after _the data loss incident_ which taught me not to let others depend on apps I&#39;m only trialing in my lab, especially without working backups.

### Infrastructure as code

Everything is written down as code, so there&#39;s as little manual setup as possible. This means I can see what&#39;s going on by inspecting the code, and I can automate configuration.

### Terraform and Ansible, not Kubernetes

When I started I wanted to compare Terraform and Ansible against Kubernetes for running a homelab.

I thought I&#39;d prove that Kubernetes was obviously the better choice, but two years later I&#39;ve found the Terraform and Ansible setup works just fine, so that&#39;s what powers my HomeOps.

I use Kubernetes in its own environment where I run experiments.

### Not highly available

One downside to my setup is that it&#39;s not highly available. If a machine breaks then there&#39;s nothing to fill in for it until I replace it. I could fix this by running more machines, but that increases the cost.

For now I can live with the downtime. So far nothing&#39;s broken, although one day something will.

## What this series will cover

In this series I&#39;ll go over each part of the system in more detail, including the hardware, IaC, services that run on top, and how I let people access it from outside my home.

The aim is not to present this as the correct way to run a homelab, but to explain the choices I made, how they fit together, and why I use the apps that I do.

It will be split up so you can dive in and out at any point to read just the parts that interest you, with links to anything written earlier that provides necessary context.

It might be awkward in that the system builds up in layers, and things may only make sense for my particular setup, but I&#39;ll try my best to generalise each part so it&#39;s relevant on its own.

## Next

Coming up first is the hardware I use.

Check back soon for the link, or subscribe to the [feed for this series](/series/homeops-tour/feed.xml) in your feed reader.

Or jump straight into the code, which is now on [GitHub](https://github.com/paultibbetts/homeops).
</source:markdown></item><item>
      <title>Turning a page into a feed</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/30/turning-a-page-into-a-feed/</link>
      <pubDate>Thu, 30 Apr 2026 16:46:04 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/30/turning-a-page-into-a-feed/</guid>
      <category>IndieWebCamp</category>
      <category>RSS</category>
      <description>Why and how I added a feed to my indie web directories directory.</description>
      <content:encoded><![CDATA[<p>During a session on feeds at <a href="https://events.indieweb.org/2026/04/-indiewebcamp-d%C3%BCsseldorf-ewHCZehNA3gg">IndieWebCamp Düsseldorf</a>
 a question came up: how do you signal updates to a page?</p>
<p>For pages that are really just lists you can treat them like posts on a blog.</p>
<p>My <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/indieweb-directories">directory of indie web directories</a>
 is one of those, it&rsquo;s a collection of links that grows over time.</p>
<p>Since my site is built with Hugo, I:</p>
<ul>
<li>changed the page into a <a href="https://gohugo.io/content-management/sections/">section</a>
</li>
<li>made a file for each entry</li>
</ul>
<p>and Hugo made a feed for it.</p>
<p>The only tweak it needed was in the RSS template, so that for this new section the <code>&lt;item&gt;&lt;link&gt;</code> would point to the directory instead of my own site, which for me looked a bit like:</p>
<pre><code class="language-go">{{- $itemLink := .Permalink -}}
{{- if eq .Section &#34;indieweb-directories&#34; -}}
	{{- with .Params.link -}}
		{{- $itemLink = . -}}
	{{- end -}}
{{- end -}}</code></pre>
<pre><code class="language-xml">&lt;item&gt;
  &lt;title&gt;{{ .Title }}&lt;/title&gt;
  &lt;link&gt;{{ $itemLink }}&lt;/link&gt;</code></pre>
<p>This works well for pages where updates are incremental additions, such as changelogs or collections.</p>
<p>It works less well for pages that are edited in place. You&rsquo;d need to post what changed, which means keeping a history of revisions - something wikis do by publishing diffs.</p>
<p>Those updates can be harder to read, and while you could do it with Hugo, you&rsquo;d have to write them manually. There&rsquo;s no built-in way of creating them.</p>
<p>In my case it works, not because I&rsquo;ve solved how to signal updates to a page, but because it turns out my page wasn&rsquo;t even a page in the first place.</p>]]></content:encoded><source:markdown>
During a session on feeds at [IndieWebCamp Düsseldorf](https://events.indieweb.org/2026/04/-indiewebcamp-d%C3%BCsseldorf-ewHCZehNA3gg) a question came up: how do you signal updates to a page?

For pages that are really just lists you can treat them like posts on a blog.

My [directory of indie web directories](/indieweb-directories) is one of those, it&#39;s a collection of links that grows over time.

Since my site is built with Hugo, I:

- changed the page into a [section](https://gohugo.io/content-management/sections/)
- made a file for each entry

and Hugo made a feed for it.

The only tweak it needed was in the RSS template, so that for this new section the `&lt;item&gt;&lt;link&gt;` would point to the directory instead of my own site, which for me looked a bit like:

```go
{{- $itemLink := .Permalink -}}
{{- if eq .Section &#34;indieweb-directories&#34; -}}
	{{- with .Params.link -}}
		{{- $itemLink = . -}}
	{{- end -}}
{{- end -}}
```

```xml
&lt;item&gt;
  &lt;title&gt;{{ .Title }}&lt;/title&gt;
  &lt;link&gt;{{ $itemLink }}&lt;/link&gt;
```

This works well for pages where updates are incremental additions, such as changelogs or collections.

It works less well for pages that are edited in place. You&#39;d need to post what changed, which means keeping a history of revisions - something wikis do by publishing diffs.

Those updates can be harder to read, and while you could do it with Hugo, you&#39;d have to write them manually. There&#39;s no built-in way of creating them.

In my case it works, not because I&#39;ve solved how to signal updates to a page, but because it turns out my page wasn&#39;t even a page in the first place.
</source:markdown></item><item>
      <title>Ghostty Is Leaving GitHub – Mitchell Hashimoto I too have been annoyed by …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/30/2026-04-30-142715-ghostty-is-leaving-github-mitchell-hashimoto/</link>
      <pubDate>Thu, 30 Apr 2026 14:27:15 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/30/2026-04-30-142715-ghostty-is-leaving-github-mitchell-hashimoto/</guid>
      <category>GitHub</category>
      <description>Ghostty Is Leaving GitHub – Mitchell Hashimoto I too have been annoyed by GitHub’s performance lately, and that’s just from using it personally.
I can’t imagine what’s it’s like for those relying on it for paid work right now.
Curious to see where Ghostty ends up, and what impact this has on its contributions.
Related:
The Missing GitHub Status Page Before GitHub</description>
      <content:encoded><![CDATA[<p><a href="https://mitchellh.com/writing/ghostty-leaving-github">Ghostty Is Leaving GitHub – Mitchell Hashimoto</a>
</p>
<p>I too have been annoyed by GitHub&rsquo;s performance lately, and that&rsquo;s just from using it personally.</p>
<p>I can&rsquo;t imagine what&rsquo;s it&rsquo;s like for those relying on it for paid work right now.</p>
<p>Curious to see where Ghostty ends up, and what impact this has on its contributions.</p>
<p>Related:</p>
<ul>
<li><a href="https://mrshu.github.io/github-statuses/">The Missing GitHub Status Page</a>
</li>
<li><a href="https://lucumr.pocoo.org/2026/4/28/before-github/">Before GitHub</a>
</li>
</ul>]]></content:encoded><source:markdown>
[Ghostty Is Leaving GitHub – Mitchell Hashimoto](https://mitchellh.com/writing/ghostty-leaving-github)

I too have been annoyed by GitHub&#39;s performance lately, and that&#39;s just from using it personally.

I can&#39;t imagine what&#39;s it&#39;s like for those relying on it for paid work right now.

Curious to see where Ghostty ends up, and what impact this has on its contributions.

Related:

- [The Missing GitHub Status Page](https://mrshu.github.io/github-statuses/)
- [Before GitHub](https://lucumr.pocoo.org/2026/4/28/before-github/)
</source:markdown></item><item>
      <title>My IndieWebCamp Düsseldorf Demo:  Extended Edition</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/</link>
      <pubDate>Mon, 27 Apr 2026 22:30:42 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/</guid>
      <category>IndieWebCamp</category>
      <category>Micropub</category>
      <description>Featuring never-before-seen coverage and a look behind the scenes.</description>
      <content:encoded><![CDATA[<p>The second part of <a href="https://events.indieweb.org/2026/04/-indiewebcamp-d%C3%BCsseldorf-ewHCZehNA3gg">IndieWebCamp Düsseldorf 2026</a>
 was &ldquo;create day&rdquo; where you take your inspiration from day one, make it a reality, and show it to everyone else.</p>
<p>My inspiration was from a few weeks ago when <a href="https://ricardochavezt.com">Ricardo</a>
 demonstrated his new setup at <a href="https://hwclondon.co.uk/">Homebrew Website Club London</a>
. I guess I was prompted into action by the <strong>How to unblock yourself and actually post more often</strong> session this weekend, but I&rsquo;m trying to give credit where it&rsquo;s due, since my demo was basically the same as his.</p>
<p>What I didn&rsquo;t show, however, was all the work that&rsquo;s been happening over the last few months to make my demo possible, which is why I&rsquo;m posting this.</p>
<p>Also, I wanted to regain some nerd-cred because my demo was followed by <a href="https://burgeonlab.com/">Naty</a>
 who showed us an updated <a href="https://burgeonlab.com/stats/">stats page</a>
 , which looks awesome and is something I&rsquo;m going to have to steal.</p>
<aside>
<p><small>Hi Naty 👋, after we spoke at IWC I went on your site and realised who you were. Your site was a big inspiration for mine when I was redesigning it. <em>That&rsquo;s</em> why they look similar!</small></p>
</aside>
<h2 id="intro">Intro</h2>
<p>My site&rsquo;s powered by Hugo, which is a static site generator that turns files into a website.</p>
<p>I write posts on my laptop and push them up to GitHub, where the site gets generated with the new content and then deployed to my server. I&rsquo;ve written about that <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/">before</a>
, which you don&rsquo;t need to read for this, I&rsquo;m just setting the scene.</p>
<p>With that setup I couldn&rsquo;t publish from my phone, which is sometimes all I have on me.</p>
<p>The indie web has a solution for this called <a href="https://indieweb.org/micropub">Micropub</a>
, which is an API for creating posts on your site, even if you use a static site generator like I do.</p>
<h2 id="indiekit">IndieKit</h2>
<p>As mentioned, a few weeks ago at HWC Ricardo showed us his new setup, to which he&rsquo;d added a Micropub server called <a href="https://getindiekit.com">IndieKit</a>
.</p>
<p>It was created by <a href="https://paulrobertlloyd.com/">Paul Robert Lloyd</a>
, who I met (briefly) at an IndieWebCamp <strong>eleven</strong> years ago.</p>
<p>Side note - isn&rsquo;t it strange that there&rsquo;s millions of things on the web but you prefer using the things made by people you&rsquo;ve met in real life?</p>
<p>IndieKit works by connecting to the place you store your posts, which for me is GitHub, and lets you write new ones from the web:</p>
<img
      src="https://paultibbetts.uk/media/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/indiekit.png"
      width="1200"
      height="802"
      alt="IndieKit user interface"
      loading="lazy"
      decoding="async"
      class="w-full h-auto rounded-lg"
    />
<h2 id="-sparkles-">✨ Sparkles ✨</h2>
<p>The other thing a Micropub server lets you do is use any Micropub client you want. You don&rsquo;t have to use the one in Indiekit - which is perfectly fine by the way - you can use a different one if you like.</p>
<p>Or you could use multiple. <a href="https://epilogue.micro.blog/">Epilogue</a>
 and <a href="https://indiebookclub.biz/">indiebookclub</a>
 both let you post about books you&rsquo;ve read.</p>
<p>Or you could post from a native app, like <a href="https://ia.net/writer/">iA Writer</a>
, a markdown editor that lets you post to WordPress, Ghost, and with Micropub: your own website.</p>
<p>The one Ricardo showed us in his demo, because it&rsquo;s got a good interface for posting about songs and albums, was <a href="https://sparkles.sploot.com/">Sparkles</a>
.</p>
<p>I don&rsquo;t post about songs or albums, but I do like how it looks, so I included it in my demo:</p>
<img
      src="https://paultibbetts.uk/media/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/sparkles.png"
      width="1200"
      height="749"
      alt="Sparkles user interface"
      loading="lazy"
      decoding="async"
      class="w-full h-auto rounded-lg"
    />
<h2 id="bonus-content">Bonus content</h2>
<p>And that&rsquo;s where my demo ended.</p>
<p>Here&rsquo;s the extra footage.</p>
<h3 id="home-server">Home server</h3>
<p>I first tried running IndieKit on the Raspberry Pi that hosts my site.</p>
<p>The Pi doesn&rsquo;t have any direct storage attached. That means I can&rsquo;t reliably run a database on it and IndieKit needs one for some of the features I want from it.</p>
<p>I didn&rsquo;t want to rent another server. I already have one at home, so I installed IndieKit there instead.</p>
<p>It works, I can access it from my phone, but only on my home network. Not much of an upgrade.</p>
<h3 id="tunnel">Tunnel</h3>
<p>The next step then was to add it to the &ldquo;tunnel&rdquo; setup I have.</p>
<p>I rent a VPS, which lives in Nürnberg by the way, and have <a href="https://pangolin.net/">Pangolin</a>
 running on it, which is a reverse proxy that sends traffic down a Wireguard tunnel that ends in my home.</p>
<img
      src="https://paultibbetts.uk/media/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/indiekit-phone.png"
      width="1179"
      height="2556"
      alt="IndieKit mobile interface"
      loading="lazy"
      decoding="async"
      class="rounded-lg"
    />
<p>and now I can post to my site, wherever I am 🎉.</p>
<h2 id="deleted-scenes">Deleted scenes</h2>
<p>Dan&rsquo;s indie feed reader <a href="https://feed.city/">feed.city</a>
 - which I discovered through IWC this weekend - caught me testing all of this and <a href="https://feed.city/feed?url=https://paultibbetts.uk/feed.xml#946987">recorded my test posts</a>
.</p>
<p>Sometimes deleted scenes are deleted for a reason. They&rsquo;re boring.</p>
<p>I only included them here as part of the Extended Edition theme I&rsquo;ve chosen to use. Let&rsquo;s skip forward.</p>
<h2 id="behind-the-scenes">Behind the scenes</h2>
<p>When I use Sparkles to post to my site:</p>
<ul>
<li>I go to <a href="https://sparkles.sploot.com/">sparkles.sploot.com</a>
 and enter my website&rsquo;s address</li>
<li>Sparkles sees the address of the Micropub endpoint I declare in the <code>&lt;head&gt;</code> of my site
<ul>
<li>which points at IndieKit</li>
</ul>
</li>
<li>Sparkles connects to IndieKit
<ul>
<li>by going to the VPS in Germany</li>
<li>through the tunnel into my home in the UK</li>
<li>and finds IndieKit on my home server</li>
</ul>
</li>
<li>I write a new post</li>
<li>IndieKit sends it to GitHub
<ul>
<li>GitHub creates a new markdown file</li>
</ul>
</li>
<li>the change triggers my deployment pipeline
<ul>
<li>which builds the site</li>
<li>and pushes it up to the Raspberry Pi</li>
</ul>
</li>
</ul>
<p>This is more complex than a typical hosted solution but it means I can self-host IndieKit at home and access it from anywhere.</p>
<p>If you&rsquo;re into that sort of stuff, stick around, I&rsquo;ll be posting about the setup and sharing the code soon.</p>
<h2 id="sequel">Sequel?</h2>
<p>If you&rsquo;re not, then I might see you at a future IndieWebCamp, hopefully in-person that time.</p>
<p>Maybe that&rsquo;ll be <a href="https://events.indieweb.org/2026/06/-indiewebcamp-nuremberg-7EIKg0lqfg93">IndieWebCamp Nürnberg 2026</a>
 and I&rsquo;ll get to meet my VPS for the first time, as well as some of the wonderful people who made this weekend possible.</p>]]></content:encoded><source:markdown>
The second part of [IndieWebCamp Düsseldorf 2026](https://events.indieweb.org/2026/04/-indiewebcamp-d%C3%BCsseldorf-ewHCZehNA3gg) was &#34;create day&#34; where you take your inspiration from day one, make it a reality, and show it to everyone else.

My inspiration was from a few weeks ago when [Ricardo](https://ricardochavezt.com) demonstrated his new setup at [Homebrew Website Club London](https://hwclondon.co.uk/). I guess I was prompted into action by the **How to unblock yourself and actually post more often** session this weekend, but I&#39;m trying to give credit where it&#39;s due, since my demo was basically the same as his.

What I didn&#39;t show, however, was all the work that&#39;s been happening over the last few months to make my demo possible, which is why I&#39;m posting this.

Also, I wanted to regain some nerd-cred because my demo was followed by [Naty](https://burgeonlab.com/) who showed us an updated [stats page](https://burgeonlab.com/stats/) , which looks awesome and is something I&#39;m going to have to steal.

&lt;aside&gt;

&lt;small&gt;Hi Naty 👋, after we spoke at IWC I went on your site and realised who you were. Your site was a big inspiration for mine when I was redesigning it. &lt;em&gt;That&#39;s&lt;/em&gt; why they look similar!&lt;/small&gt;

&lt;/aside&gt;

## Intro

My site&#39;s powered by Hugo, which is a static site generator that turns files into a website.

I write posts on my laptop and push them up to GitHub, where the site gets generated with the new content and then deployed to my server. I&#39;ve written about that [before](/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/), which you don&#39;t need to read for this, I&#39;m just setting the scene.

With that setup I couldn&#39;t publish from my phone, which is sometimes all I have on me.

The indie web has a solution for this called [Micropub](https://indieweb.org/micropub), which is an API for creating posts on your site, even if you use a static site generator like I do.

## IndieKit

As mentioned, a few weeks ago at HWC Ricardo showed us his new setup, to which he&#39;d added a Micropub server called [IndieKit](https://getindiekit.com).

It was created by [Paul Robert Lloyd](https://paulrobertlloyd.com/), who I met (briefly) at an IndieWebCamp **eleven** years ago.

Side note - isn&#39;t it strange that there&#39;s millions of things on the web but you prefer using the things made by people you&#39;ve met in real life?

IndieKit works by connecting to the place you store your posts, which for me is GitHub, and lets you write new ones from the web:

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/indiekit.png&#34; alt=&#34;IndieKit user interface&#34; class=&#34;w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;802&#34; &gt;}}

## ✨ Sparkles ✨

The other thing a Micropub server lets you do is use any Micropub client you want. You don&#39;t have to use the one in Indiekit - which is perfectly fine by the way - you can use a different one if you like.

Or you could use multiple. [Epilogue](https://epilogue.micro.blog/) and [indiebookclub](https://indiebookclub.biz/) both let you post about books you&#39;ve read.

Or you could post from a native app, like [iA Writer](https://ia.net/writer/), a markdown editor that lets you post to WordPress, Ghost, and with Micropub: your own website.

The one Ricardo showed us in his demo, because it&#39;s got a good interface for posting about songs and albums, was [Sparkles](https://sparkles.sploot.com/).

I don&#39;t post about songs or albums, but I do like how it looks, so I included it in my demo:

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/sparkles.png&#34; alt=&#34;Sparkles user interface&#34; class=&#34;w-full h-auto rounded-lg&#34; width=&#34;1200&#34; height=&#34;749&#34; &gt;}}

## Bonus content

And that&#39;s where my demo ended.

Here&#39;s the extra footage.

### Home server

I first tried running IndieKit on the Raspberry Pi that hosts my site.

The Pi doesn&#39;t have any direct storage attached. That means I can&#39;t reliably run a database on it and IndieKit needs one for some of the features I want from it.

I didn&#39;t want to rent another server. I already have one at home, so I installed IndieKit there instead.

It works, I can access it from my phone, but only on my home network. Not much of an upgrade.

### Tunnel

The next step then was to add it to the &#34;tunnel&#34; setup I have.

I rent a VPS, which lives in Nürnberg by the way, and have [Pangolin](https://pangolin.net/) running on it, which is a reverse proxy that sends traffic down a Wireguard tunnel that ends in my home.

{{&lt; img src=&#34;https://paultibbetts.uk/media/2026/04/27/my-indiewebcamp-dusseldorf-demo-extended-edition/indiekit-phone.png&#34; alt=&#34;IndieKit mobile interface&#34; class=&#34;rounded-lg&#34; width=&#34;1179&#34; height=&#34;2556&#34; &gt;}}

and now I can post to my site, wherever I am 🎉.

## Deleted scenes

Dan&#39;s indie feed reader [feed.city](https://feed.city/) - which I discovered through IWC this weekend - caught me testing all of this and [recorded my test posts](https://feed.city/feed?url=https://paultibbetts.uk/feed.xml#946987).

Sometimes deleted scenes are deleted for a reason. They&#39;re boring.

I only included them here as part of the Extended Edition theme I&#39;ve chosen to use. Let&#39;s skip forward.

## Behind the scenes

When I use Sparkles to post to my site:

- I go to [sparkles.sploot.com](https://sparkles.sploot.com/) and enter my website&#39;s address
- Sparkles sees the address of the Micropub endpoint I declare in the `&lt;head&gt;` of my site
  - which points at IndieKit
- Sparkles connects to IndieKit
  - by going to the VPS in Germany
  - through the tunnel into my home in the UK
  - and finds IndieKit on my home server
- I write a new post
- IndieKit sends it to GitHub
  - GitHub creates a new markdown file
- the change triggers my deployment pipeline
  - which builds the site
  - and pushes it up to the Raspberry Pi

This is more complex than a typical hosted solution but it means I can self-host IndieKit at home and access it from anywhere.

If you&#39;re into that sort of stuff, stick around, I&#39;ll be posting about the setup and sharing the code soon.

## Sequel?

If you&#39;re not, then I might see you at a future IndieWebCamp, hopefully in-person that time.

Maybe that&#39;ll be [IndieWebCamp Nürnberg 2026](https://events.indieweb.org/2026/06/-indiewebcamp-nuremberg-7EIKg0lqfg93) and I&#39;ll get to meet my VPS for the first time, as well as some of the wonderful people who made this weekend possible.
</source:markdown></item><item>
      <title>Guten Morgen IndieWebCamp Düsseldorf. Today I’m working on: setting up IndieKit …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/26/iwc-dus-sun/</link>
      <pubDate>Sun, 26 Apr 2026 09:43:42 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/26/iwc-dus-sun/</guid>
      <category>IndieWebCamp</category>
      <description>Guten Morgen IndieWebCamp Düsseldorf.
Today I’m working on:
setting up IndieKit so I can post from my phone changing my /links page so posts tagged as link and links are also included I’ll hopefully have at least one ready for demos later.</description>
      <content:encoded><![CDATA[<p>Guten Morgen IndieWebCamp Düsseldorf.</p>
<p>Today I&rsquo;m working on:</p>
<ul>
<li>setting up <a href="https://getindiekit.com">IndieKit</a>
 so I can post from my phone</li>
<li>changing my <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/links">/links</a>
 page so posts tagged as <code>link</code> and <code>links</code> are also included</li>
</ul>
<p>I&rsquo;ll hopefully have at least one ready for demos later.</p>]]></content:encoded><source:markdown>
Guten Morgen IndieWebCamp Düsseldorf.

Today I&#39;m working on:

- setting up [IndieKit](https://getindiekit.com) so I can post from my phone
- changing my [/links](/links) page so posts tagged as `link` and `links` are also included

I&#39;ll hopefully have at least one ready for demos later.
</source:markdown></item><item>
      <title>An IndieWebCamp Düsseldorf 2026 Saturday Summary</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/25/an-indiewebcamp-dusseldorf-2026-saturday-summary/</link>
      <pubDate>Sat, 25 Apr 2026 20:58:04 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/25/an-indiewebcamp-dusseldorf-2026-saturday-summary/</guid>
      <category>IndieWebCamp</category>
      <description>Feeling inspired after day one of IndieWebCamp Düsseldorf 2026 .</description>
      <content:encoded><![CDATA[<p>Feeling inspired after day one of <a href="https://events.indieweb.org/2026/04/-indiewebcamp-d%C3%BCsseldorf-ewHCZehNA3gg">IndieWebCamp Düsseldorf 2026</a>
.</p>
<h2 id="sessions">Sessions</h2>
<p>The first session of the day was <strong>Running your own LLM to get out of big tech</strong>. The word was inevitably going to come up, so it was good to hear an indie approach that lets you keep your privacy whilst using it.</p>
<p>The second session was <strong>Private posts</strong>, which didn&rsquo;t have a microphone nearby, so it wasn&rsquo;t recorded.</p>
<p>After a long lunch the third and fourth sessions were about <strong>Feeds</strong> and <strong>Canvas animations</strong>. It was good to hear creative talks happening, especially about boring tech like RSS.</p>
<aside>
<p>Did you know you can <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/follow">follow my blog</a>
?</p>
</aside>
<p>The last sessions of the day were <strong>Organising data</strong> and <strong>Homepage design</strong>. It turns out nobody really likes their current homepage, which was weirdly reassuring.</p>
<h2 id="bonus-session">Bonus session</h2>
<p>I say they were the last sessions, but there was actually a secret session right at the end of the day on <strong>How to unblock yourself and actually post more often</strong>. There were lots of good ideas mentioned, like <a href="https://polaroids.danielpietzsch.com/">Dan&rsquo;s Polaroids project</a>
 where he takes and publishes a Polaroid every day.</p>
<p>The one that stuck with me is the idea that you can write a brief summary about something that happened and then hit publish without spending too much time on it.</p>]]></content:encoded><source:markdown>
Feeling inspired after day one of [IndieWebCamp Düsseldorf 2026](https://events.indieweb.org/2026/04/-indiewebcamp-d%C3%BCsseldorf-ewHCZehNA3gg).

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

## Sessions

The first session of the day was **Running your own LLM to get out of big tech**. The word was inevitably going to come up, so it was good to hear an indie approach that lets you keep your privacy whilst using it.

The second session was **Private posts**, which didn&#39;t have a microphone nearby, so it wasn&#39;t recorded.

After a long lunch the third and fourth sessions were about **Feeds** and **Canvas animations**. It was good to hear creative talks happening, especially about boring tech like RSS.

&lt;aside&gt;

Did you know you can [follow my blog](/follow)?

&lt;/aside&gt;

The last sessions of the day were **Organising data** and **Homepage design**. It turns out nobody really likes their current homepage, which was weirdly reassuring.

## Bonus session

I say they were the last sessions, but there was actually a secret session right at the end of the day on **How to unblock yourself and actually post more often**. There were lots of good ideas mentioned, like [Dan&#39;s Polaroids project](https://polaroids.danielpietzsch.com/) where he takes and publishes a Polaroid every day.

The one that stuck with me is the idea that you can write a brief summary about something that happened and then hit publish without spending too much time on it.
</source:markdown></item><item>
      <title>Hallo IndieWebCamp Düsseldorf! 👋 I’ll be joining in remotely this weekend. I do …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/25/iwc-dus-sat-hallo/</link>
      <pubDate>Sat, 25 Apr 2026 09:30:54 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/25/iwc-dus-sat-hallo/</guid>
      <category>IndieWebCamp</category>
      <description>Hallo IndieWebCamp Düsseldorf! 👋
I’ll be joining in remotely this weekend. I do wish I was there with you all, maybe next time!</description>
      <content:encoded><![CDATA[<p>Hallo IndieWebCamp Düsseldorf! 👋</p>
<p>I&rsquo;ll be joining in remotely this weekend. I do wish I was there with you all, maybe next time!</p>]]></content:encoded><source:markdown>
Hallo IndieWebCamp Düsseldorf! 👋

I&#39;ll be joining in remotely this weekend. I do wish I was there with you all, maybe next time!
</source:markdown></item><item>
      <title>Liked: The Colonization of Confidence., Sightless Scribbles</title>
      <link>https://sightlessscribbles.com/posts/the-colonization-of-confidence/</link>
      <pubDate>Thu, 23 Apr 2026 11:58:50 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/23/2026-04-23-the-colonization-of-confidence-sightless-scribbles/</guid>
      <category>ai</category>
      <category>writing</category>
      <category>nonfiction</category>
      <description>“They are selling you a solution to a problem they created. They want you to feel insecure. If you feel insecure, you pay the subscription.”
“You are not bad. You are a jazz musician in a world trying to sell ringtones.”</description>
      <content:encoded><![CDATA[<blockquote>
<p>&ldquo;They are selling you a solution to a problem they created. They want you to feel insecure. If you feel insecure, you pay the subscription.&rdquo;</p>
</blockquote>
<blockquote>
<p>&ldquo;You are not bad. You are a jazz musician in a world trying to sell ringtones.&rdquo;</p>
</blockquote>]]></content:encoded><source:markdown>
&gt; &#34;They are selling you a solution to a problem they created. They want you to feel insecure. If you feel insecure, you pay the subscription.&#34;

&gt; &#34;You are not bad. You are a jazz musician in a world trying to sell ringtones.&#34;
</source:markdown></item><item>
      <title>Bookmarked: The internet used to be fun</title>
      <link>https://projects.kwon.nyc/internet-is-fun/</link>
      <pubDate>Wed, 22 Apr 2026 16:42:53 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/22/2026-04-22-the-internet-used-to-be-fun/</guid>
      <category>IndieWeb</category>
      <description>here’s a collection of articles that to some degree answer the question “Why have a personal website?” with “Because it’s fun, and the internet used to be fun.&amp;#34;
(It still is, but it used to be, too)</description>
      <content:encoded><![CDATA[<blockquote>
<p>here&rsquo;s a collection of articles that to some degree answer the question &ldquo;Why have a personal website?&rdquo; with “Because it&rsquo;s fun, and the internet used to be fun.&quot;</p>
</blockquote>
<blockquote>
<p>(It still is, but it used to be, too)</p>
</blockquote>]]></content:encoded><source:markdown>
&gt; here&#39;s a collection of articles that to some degree answer the question &#34;Why have a personal website?&#34; with “Because it&#39;s fun, and the internet used to be fun.&#34;

&gt; (It still is, but it used to be, too)
</source:markdown></item><item>
      <title>Liked: Open Tabs - Ricardo Chávez</title>
      <link>https://www.ricardochavezt.com/open_tabs</link>
      <pubDate>Mon, 20 Apr 2026 14:13:31 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/20/2026-04-20-open-tabs-ricardo-chavez/</guid>
      <category>digital garden</category>
      <description>Every time I visit Ricardo’s garden his open tabs become my open tabs.</description>
      <content:encoded><![CDATA[<p>Every time I visit Ricardo&rsquo;s garden his open tabs become my open tabs.</p>]]></content:encoded><source:markdown>
Every time I visit Ricardo&#39;s garden his open tabs become my open tabs.
</source:markdown></item><item>
      <title>I’ve seen a few indie web directories popping up lately, so in an attempt to …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/16/2026-04-16-151643-indie-web-directories/</link>
      <pubDate>Thu, 16 Apr 2026 15:16:43 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/16/2026-04-16-151643-indie-web-directories/</guid>
      <category>IndieWeb</category>
      <category>Homebrew Website Club</category>
      <description>I’ve seen a few indie web directories popping up lately, so in an attempt to keep track of them all I’ve started on a directory of indie web directories .
Then I was introduced to Russel’s paradox , which asks if a directory should include itself?
The answer is no, you make another one .</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve seen a few indie web directories popping up lately, so in an attempt to keep track of them all I&rsquo;ve started on a <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/indieweb-directories">directory of indie web directories</a>
.</p>
<p>Then I was introduced to <a href="https://en.wikipedia.org/wiki/Russell%27s_paradox">Russel&rsquo;s paradox</a>
, which asks if a directory should include itself?</p>
<p>The answer is <strong>no</strong>, you make <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/directory-of-indieweb-directories">another one</a>
.</p>]]></content:encoded><source:markdown>
I&#39;ve seen a few indie web directories popping up lately, so in an attempt to keep track of them all I&#39;ve started on a [directory of indie web directories](/indieweb-directories).

Then I was introduced to [Russel&#39;s paradox](https://en.wikipedia.org/wiki/Russell&#39;s_paradox), which asks if a directory should include itself?

The answer is **no**, you make [another one](/directory-of-indieweb-directories).
</source:markdown></item><item>
      <title>Bookmarked: Endgame for the Open Web - Anil Dash</title>
      <link>https://www.anildash.com/2026/03/27/endgame-open-web/</link>
      <pubDate>Thu, 16 Apr 2026 13:17:46 +0100</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/04/16/2026-04-16-endgame-for-the-open-web-anil-dash/</guid>
      <category>ai</category>
      <description>all the signs are pointing to the fact that we might be in endgame for “open” as we’ve known it on the Internet over the last few decades
See also:
Prepping for the endgame of the open web Some Thoughts on the Open Web</description>
      <content:encoded><![CDATA[<blockquote>
<p>all the signs are pointing to the fact that we might be in endgame for &ldquo;open&rdquo; as we&rsquo;ve known it on the Internet over the last few decades</p>
</blockquote>
<p>See also:</p>
<ul>
<li><a href="https://thehistoryoftheweb.com/prepping-for-the-endgame/">Prepping for the endgame of the open web</a>
</li>
<li><a href="https://www.mnot.net/blog/2026/01/20/open_web">Some Thoughts on the Open Web</a>
</li>
</ul>]]></content:encoded><source:markdown>
&gt; all the signs are pointing to the fact that we might be in endgame for &#34;open&#34; as we&#39;ve known it on the Internet over the last few decades

See also:

- [Prepping for the endgame of the open web](https://thehistoryoftheweb.com/prepping-for-the-endgame/)
- [Some Thoughts on the Open Web](https://www.mnot.net/blog/2026/01/20/open_web)
</source:markdown></item><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><item>
      <title>I Made an Ansible Role for Caddy</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/24/i-made-an-ansible-role-for-caddy/</link>
      <pubDate>Tue, 24 Feb 2026 12:10:45 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/24/i-made-an-ansible-role-for-caddy/</guid>
      <category>Ansible</category>
      <category>Caddy</category>
      <category>code</category>
      <description>paultibbetts/ansible-role-caddy is an Ansible role for Caddy web server that lets you install Caddy with plugins.
I use it for my personal site and in my homelab.
This is what it does and how to use it.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/paultibbetts/ansible-role-caddy">paultibbetts/ansible-role-caddy</a>
 is an Ansible role for Caddy web server that lets you install Caddy with plugins.</p>
<p>I use it for my personal site and in my homelab.</p>
<p>This is what it does and how to use it.</p>
<h2 id="what-it-does">What it does</h2>
<p>An Ansible role is like a plugin that extends Ansible with the ability to do something, and in this case it&rsquo;s to install and manage <a href="https://caddyserver.com/">Caddy</a>
.</p>
<p>Specifically it:</p>
<ul>
<li>installs Caddy</li>
<li>
<ul>
<li>defaults to using the <code>apt</code> package manager</li>
</ul>
</li>
<li>
<ul>
<li>or it can be given the URL to a specific binary</li>
</ul>
</li>
<li>
<ul>
<li>or it can be given a list of plugins</li>
</ul>
</li>
<li>can be used to manage Caddy</li>
<li>
<ul>
<li>or completely uninstall it</li>
</ul>
</li>
<li>can write the Caddyfile from a template</li>
</ul>
<h2 id="why-caddy">Why Caddy?</h2>
<p>I use Caddy because it manages TLS certificates automatically and, with a plugin, supports <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge">DNS-01 challenges</a>
.</p>
<p>DNS-01 is useful when services aren&rsquo;t reachable by Lets Encrypt, like in a homelab, so private networks can also have TLS.</p>
<h2 id="design-goals">Design goals</h2>
<p>The role was designed with the following in mind:</p>
<ul>
<li>idempotent and safe to re-run</li>
<li>Debian and Ubuntu</li>
<li>works on x86 and ARM</li>
<li>supports installing with plugins</li>
<li>can optionally write the Caddyfile</li>
</ul>
<p>There are already Ansible roles for Caddy, like the one listed in <a href="https://caddyserver.com/docs/install#ansible">the docs</a>
, but I needed a role that could install Caddy with plugins.</p>
<h2 id="how-to-use-it">How to use it</h2>
<p>The role is available on <a href="https://galaxy.ansible.com/ui/standalone/roles/paultibbetts/caddy/">Ansible Galaxy</a>
.</p>
<h3 id="install">Install</h3>
<p>You can install it by adding:</p>
<pre><code class="language-yaml">- name: paultibbetts.caddy
  version: 1.0.0</code></pre>
<p>to your <code>requirements.yml</code> file and then running:</p>
<pre><code class="language-sh">ansible-galaxy role install -r requirements.yml</code></pre>
<h3 id="variables">Variables</h3>
<p>The variables for the role, as well as the default settings, can all be overridden.</p>
<p>The most important ones are the install method, the plugins to include, and whether the role writes the Caddyfile for you.</p>
<pre><code class="language-yaml">caddy_install_method: apt</code></pre>
<p>The role defaults to installing Caddy via <code>apt</code>, the package manager for Debian/Ubuntu.</p>
<p>This means Caddy does not include any plugins but it does get updated when you run <code>apt upgrade</code>.</p>
<pre><code class="language-yaml">caddy_install_method: download</code></pre>
<p>Alternatively you can change the install method to <code>download</code>.</p>
<p>Now you can pass in the URL of a specific binary using:</p>
<pre><code class="language-yaml">caddy_download_url: &#34;&#34;</code></pre>
<p>Using this you could build a binary for Caddy (including plugins), host it, then pass that URL to the role and that specific binary will be installed.</p>
<p>This is currently the only way to install a specific version of Caddy that isn&rsquo;t &ldquo;latest&rdquo;.</p>
<p>If you want to install plugins for Caddy and don&rsquo;t want to build your own binary you can leave the download URL empty and pass in the plugins:</p>
<pre><code class="language-yaml">caddy_plugins:
  - github.com/caddy-dns/cloudflare</code></pre>
<p>and the role will generate a URL from the <a href="https://caddyserver.com/download">download page</a>
.</p>
<pre><code class="language-yaml">caddy_caddyfile_template: &#34;&#34; | &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;</code></pre>
<p>The Caddyfile template variable lets you pass in a template of a Caddyfile for Caddy to use and it will be written to the appropriate place.</p>
<p>Letting the role write the Caddyfile means you can import the role, set its variables, and have all of Caddy configured by one thing - you don&rsquo;t need to write extra tasks after installation to get it up and running.</p>
<h3 id="examples">Examples</h3>
<p>The following examples show how the role can be used for a public website and for a private homelab reverse proxy.</p>
<p>In both instances Caddy acquires TLS certificates from Lets Encrypt, using Cloudflare to do DNS-01 challenges.</p>
<h4 id="example-personal-website">Example: Personal website</h4>
<p>The role can be used to setup Caddy to host a personal website.</p>
<pre><code class="language-yaml">- hosts: server
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;</code></pre>
<p>The <code>download</code> install method is used to install Caddy with the <a href="https://github.com/caddy-dns/cloudflare">Cloudflare plugin</a>
, the Cloudflare API token is passed to Caddy from Ansible vault, and a template is provided for the Caddyfile, which could look like:</p>
<pre><code class="language-Caddyfile">(cf_tls) {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
}

{% for site in web_sites %}
{{ site }} {
  import cf_tls
  root * {{ web_sites_root }}/{{ site }}/current
  file_server
  encode zstd gzip
  log
}
{% endfor %}</code></pre>
<p>This sets up Caddy to use Cloudflare to do DNS-01 challenges with Lets Encrypt to get a TLS certificate for each <code>site in web_sites</code>.</p>
<h4 id="example-homelab">Example: Homelab</h4>
<p>The role can also be used to setup Caddy in a private network, like a homelab.</p>
<pre><code class="language-yaml">- hosts: homelab
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;</code></pre>
<pre><code class="language-Caddyfile">{
        acme_dns cloudflare {env.CF_API_TOKEN}
}

service.homelab.example {
        reverse_proxy &lt;ip&gt;:&lt;port&gt;
}</code></pre>
<p>where the Cloudflare plugin is configured once and is then used to do DNS-01 challenges for any TLS certificates Caddy requests. Note this style has been deprecated and you should do it per host, like the earlier example for a personal website.</p>
<p>DNS-01 challenges are solved by writing a TXT record to prove domain ownership. This means Lets Encrypt is able to issue certificates for domains it can&rsquo;t reach, which makes DNS-01 ideal when Caddy is running in a homelab.</p>
<h2 id="molecule-scenarios">Molecule Scenarios</h2>
<p>Ansible Molecule is a way to run tests for an Ansible role.</p>
<p>The role includes molecule scenarios that test it works on x86 as well as Arm, because I use the role on a Raspberry Pi 4, as well as scenarios for the Cloudflare plugin and the Caddyfile templating.</p>
<h2 id="in-use">In use</h2>
<p>I use this role as part of the code that sets up the infrastructure for my website.</p>
<p>Terraform is used to provision the server, then Ansible is used to configure it, with this role setting up Caddy and the Caddyfile it uses.</p>
<p>The complete setup - Terraform, Ansible, Caddy, and the rest of the files - is documented at <a href="https://infra.paultibbetts.uk">infra.paultibbetts.uk</a>
, where I explain how it all fits together.</p>]]></content:encoded><source:markdown>
[paultibbetts/ansible-role-caddy](https://github.com/paultibbetts/ansible-role-caddy) is an Ansible role for Caddy web server that lets you install Caddy with plugins.

I use it for my personal site and in my homelab.

This is what it does and how to use it.

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

## What it does

An Ansible role is like a plugin that extends Ansible with the ability to do something, and in this case it&#39;s to install and manage [Caddy](https://caddyserver.com/).

Specifically it:

- installs Caddy
- - defaults to using the `apt` package manager
- - or it can be given the URL to a specific binary
- - or it can be given a list of plugins
- can be used to manage Caddy
- - or completely uninstall it
- can write the Caddyfile from a template

## Why Caddy?

I use Caddy because it manages TLS certificates automatically and, with a plugin, supports [DNS-01 challenges](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge).

DNS-01 is useful when services aren&#39;t reachable by Lets Encrypt, like in a homelab, so private networks can also have TLS.

## Design goals

The role was designed with the following in mind:

- idempotent and safe to re-run
- Debian and Ubuntu
- works on x86 and ARM
- supports installing with plugins
- can optionally write the Caddyfile

There are already Ansible roles for Caddy, like the one listed in [the docs](https://caddyserver.com/docs/install#ansible), but I needed a role that could install Caddy with plugins.

## How to use it

The role is available on [Ansible Galaxy](https://galaxy.ansible.com/ui/standalone/roles/paultibbetts/caddy/).

### Install

You can install it by adding:

```yaml
- name: paultibbetts.caddy
  version: 1.0.0
```

to your `requirements.yml` file and then running:

```sh
ansible-galaxy role install -r requirements.yml
```

### Variables

The variables for the role, as well as the default settings, can all be overridden.

The most important ones are the install method, the plugins to include, and whether the role writes the Caddyfile for you.

```yaml
caddy_install_method: apt
```

The role defaults to installing Caddy via `apt`, the package manager for Debian/Ubuntu.

This means Caddy does not include any plugins but it does get updated when you run `apt upgrade`.

```yaml
caddy_install_method: download
```

Alternatively you can change the install method to `download`.

Now you can pass in the URL of a specific binary using:

```yaml
caddy_download_url: &#34;&#34;
```

Using this you could build a binary for Caddy (including plugins), host it, then pass that URL to the role and that specific binary will be installed.

This is currently the only way to install a specific version of Caddy that isn&#39;t &#34;latest&#34;.

If you want to install plugins for Caddy and don&#39;t want to build your own binary you can leave the download URL empty and pass in the plugins:

```yaml
caddy_plugins:
  - github.com/caddy-dns/cloudflare
```

and the role will generate a URL from the [download page](https://caddyserver.com/download).

```yaml
caddy_caddyfile_template: &#34;&#34; | &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
```

The Caddyfile template variable lets you pass in a template of a Caddyfile for Caddy to use and it will be written to the appropriate place.

Letting the role write the Caddyfile means you can import the role, set its variables, and have all of Caddy configured by one thing - you don&#39;t need to write extra tasks after installation to get it up and running.

### Examples

The following examples show how the role can be used for a public website and for a private homelab reverse proxy.

In both instances Caddy acquires TLS certificates from Lets Encrypt, using Cloudflare to do DNS-01 challenges.

#### Example: Personal website

The role can be used to setup Caddy to host a personal website.

```yaml
- hosts: server
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;
```

The `download` install method is used to install Caddy with the [Cloudflare plugin](https://github.com/caddy-dns/cloudflare), the Cloudflare API token is passed to Caddy from Ansible vault, and a template is provided for the Caddyfile, which could look like:

```Caddyfile
(cf_tls) {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
}

{% for site in web_sites %}
{{ site }} {
  import cf_tls
  root * {{ web_sites_root }}/{{ site }}/current
  file_server
  encode zstd gzip
  log
}
{% endfor %}
```

This sets up Caddy to use Cloudflare to do DNS-01 challenges with Lets Encrypt to get a TLS certificate for each `site in web_sites`.

#### Example: Homelab

The role can also be used to setup Caddy in a private network, like a homelab.

```yaml
- hosts: homelab
  become: true
  roles:
    - role: paultibbetts.caddy
      vars:
        caddy_caddyfile_template: &#34;{{ playbook_dir }}/templates/Caddyfile.j2&#34;
        caddy_install_method: download
        caddy_plugins:
          - github.com/caddy-dns/cloudflare
        caddy_manage_systemd_env_file: true
        caddy_systemd_env:
          CF_API_TOKEN: &#34;{{ vault_cf_api_token }}&#34;
```

```Caddyfile
{
        acme_dns cloudflare {env.CF_API_TOKEN}
}

service.homelab.example {
        reverse_proxy &lt;ip&gt;:&lt;port&gt;
}
```

where the Cloudflare plugin is configured once and is then used to do DNS-01 challenges for any TLS certificates Caddy requests. Note this style has been deprecated and you should do it per host, like the earlier example for a personal website.

DNS-01 challenges are solved by writing a TXT record to prove domain ownership. This means Lets Encrypt is able to issue certificates for domains it can&#39;t reach, which makes DNS-01 ideal when Caddy is running in a homelab.

## Molecule Scenarios

Ansible Molecule is a way to run tests for an Ansible role.

The role includes molecule scenarios that test it works on x86 as well as Arm, because I use the role on a Raspberry Pi 4, as well as scenarios for the Cloudflare plugin and the Caddyfile templating.

## In use

I use this role as part of the code that sets up the infrastructure for my website.

Terraform is used to provision the server, then Ansible is used to configure it, with this role setting up Caddy and the Caddyfile it uses.

The complete setup - Terraform, Ansible, Caddy, and the rest of the files - is documented at [infra.paultibbetts.uk](https://infra.paultibbetts.uk), where I explain how it all fits together.
</source:markdown></item><item>
      <title>Moved My Website From GitHub Pages to a Raspberry Pi</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/</link>
      <pubDate>Thu, 19 Feb 2026 17:40:37 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/19/moved-my-website-from-github-pages-to-a-raspberry-pi/</guid>
      <category>GitHub Pages</category>
      <category>Raspberry Pi</category>
      <description>My site was hosted on GitHub Pages.
It was simple and free, but it didn’t really feel like it was mine.
I’ve fixed that with a Raspberry Pi.</description>
      <content:encoded><![CDATA[<p>My site was hosted on GitHub Pages.</p>
<p>It was simple and free, but it didn&rsquo;t really feel like it was mine.</p>
<p>I&rsquo;ve fixed that with a Raspberry Pi.</p>
<h2 id="why-leave-github-pages">Why leave GitHub Pages?</h2>
<p><a href="https://docs.github.com/en/pages">GitHub Pages</a>
 is excellent at what it does.</p>
<p>You upload the code for your site to GitHub and they will build and host it for you.<br>
All for free.</p>
<p>So why leave?</p>
<h3 id="independence">Independence</h3>
<p>GitHub is owned by Microsoft, one of the biggest companies on the planet.</p>
<p>I have no problem with Microsoft, or using their economies of scale to do something for free that would otherwise cost me money, but ultimately I didn&rsquo;t feel very indie hosting my site on something I didn&rsquo;t control.</p>
<h3 id="sovereignty">Sovereignty</h3>
<p>GitHub and Microsoft are companies based in the USA, and those of us in Europe are re-considering our dependence on US tech.</p>
<p>Moving has brought my website closer to home - physically, legally, and ethically.</p>
<h3 id="portability">Portability</h3>
<p>The work to move from GitHub Pages to <em>anything else</em> has also made it easier to move it all again to another provider, should I ever choose to do so.</p>
<p>I&rsquo;ve taken the script to build the site out of GitHub Actions and made it part of the code itself. This decouples the site from any specific CI platform and makes both hosting and deployment replaceable.</p>
<h2 id="what-it-now-runs-on">What it now runs on</h2>
<p>There&rsquo;s no good reason for using a Raspberry Pi for this, I just thought it would be cool.</p>
<p>It is some of the cheapest hosting I could find from a UK-based provider, but there are cheaper options and hosting in Europe works just the same and is even cheaper.</p>
<p>The Pi is from <a href="https://www.mythic-beasts.com/">Mythic Beasts</a>
, a UK company based in Cambridge. It lives in London and runs on <a href="https://www.mythic-beasts.com/article/environment">green energy</a>
.</p>
<p>I provision it with Terraform, which lets me write the Pi down as code, and then I configure it using Ansible, another tool that sets up the software it runs.</p>
<p>Ansible installs and manages Caddy, which is a web server that has been configured to serve my site.</p>
<p>Deployment is still handled by GitHub Actions but now it goes to the Pi instead of Pages.</p>
<p>From the outside it all works exactly the same, but now it&rsquo;s on hardware I control, with a setup that&rsquo;s more portable than it was before.</p>
<h2 id="under-the-hood">Under the hood</h2>
<p>To make this all possible I ended up writing more code than I set out to.</p>
<p>There wasn&rsquo;t a Terraform provider for Mythic Beasts, so <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/09/2026-02-09-151500/">I wrote one</a>
.</p>
<p>For that to work it needed a client to talk to their APIs, so <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/06/2026-02-06-130000/">I wrote that too</a>
.</p>
<p>And to set up Caddy using Ansible <a href="https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/03/2026-02-03-175500/">I wrote my own role</a>
. There were already others that did this but none of them did exactly what I wanted, so I made my own.</p>
<p>None of this was really required, I could have moved to the Pi without writing any code, but the change from a manual setup to an automated one - driven by code - has forced me to treat the infrastructure of my website as a real system, that can change and evolve over time.</p>
<h2 id="what-this-means">What this means</h2>
<p>As a result my setup is now reproducible, portable and more independent.</p>
<p>I can see all the parts that make up my infrastructure and swap them out for alternatives. I&rsquo;m not tied down to a provider I can replace with a few lines of code.</p>
<p>And ultimately, there&rsquo;s no difference to visitors of the site, who still get the same experience as before.</p>
<h2 id="what-comes-next">What comes next</h2>
<p>Through doing this I can see that I might also be able to move my domain from Cloudflare to Mythic Beasts, another win for sovereignty.</p>
<p>If I did that, and moved the code hosting away from GitHub, I&rsquo;d have a website that&rsquo;s fully independent.</p>
<p>But that&rsquo;s for another time.</p>
<p>Over the next few posts I&rsquo;ll go over:</p>
<ul>
<li>how to use the Ansible role</li>
<li>making a provider for Terraform</li>
<li>the code that ties it all together</li>
<li>the site that explains it all in-depth</li>
</ul>
<h2 id="now">Now</h2>
<p>For now this post marks that this site&rsquo;s no longer hosted by GitHub Pages.</p>
<p>It&rsquo;s on a tiny computer, closer to home, set up by my own code.</p>
<p>That feels a bit more mine.</p>]]></content:encoded><source:markdown>
My site was hosted on GitHub Pages.

It was simple and free, but it didn&#39;t really feel like it was mine.

I&#39;ve fixed that with a Raspberry Pi.

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

## Why leave GitHub Pages?

[GitHub Pages](https://docs.github.com/en/pages) is excellent at what it does.

You upload the code for your site to GitHub and they will build and host it for you.  
All for free.

So why leave?

### Independence

GitHub is owned by Microsoft, one of the biggest companies on the planet.

I have no problem with Microsoft, or using their economies of scale to do something for free that would otherwise cost me money, but ultimately I didn&#39;t feel very indie hosting my site on something I didn&#39;t control.

### Sovereignty

GitHub and Microsoft are companies based in the USA, and those of us in Europe are re-considering our dependence on US tech.

Moving has brought my website closer to home - physically, legally, and ethically.

### Portability

The work to move from GitHub Pages to _anything else_ has also made it easier to move it all again to another provider, should I ever choose to do so.

I&#39;ve taken the script to build the site out of GitHub Actions and made it part of the code itself. This decouples the site from any specific CI platform and makes both hosting and deployment replaceable.

## What it now runs on

There&#39;s no good reason for using a Raspberry Pi for this, I just thought it would be cool.

It is some of the cheapest hosting I could find from a UK-based provider, but there are cheaper options and hosting in Europe works just the same and is even cheaper.

The Pi is from [Mythic Beasts](https://www.mythic-beasts.com/), a UK company based in Cambridge. It lives in London and runs on [green energy](https://www.mythic-beasts.com/article/environment).

I provision it with Terraform, which lets me write the Pi down as code, and then I configure it using Ansible, another tool that sets up the software it runs.

Ansible installs and manages Caddy, which is a web server that has been configured to serve my site.

Deployment is still handled by GitHub Actions but now it goes to the Pi instead of Pages.

From the outside it all works exactly the same, but now it&#39;s on hardware I control, with a setup that&#39;s more portable than it was before.

## Under the hood

To make this all possible I ended up writing more code than I set out to.

There wasn&#39;t a Terraform provider for Mythic Beasts, so [I wrote one](/2026/02/09/2026-02-09-151500/).

For that to work it needed a client to talk to their APIs, so [I wrote that too](/2026/02/06/2026-02-06-130000/).

And to set up Caddy using Ansible [I wrote my own role](/2026/02/03/2026-02-03-175500/). There were already others that did this but none of them did exactly what I wanted, so I made my own.

None of this was really required, I could have moved to the Pi without writing any code, but the change from a manual setup to an automated one - driven by code - has forced me to treat the infrastructure of my website as a real system, that can change and evolve over time.

## What this means

As a result my setup is now reproducible, portable and more independent.

I can see all the parts that make up my infrastructure and swap them out for alternatives. I&#39;m not tied down to a provider I can replace with a few lines of code.

And ultimately, there&#39;s no difference to visitors of the site, who still get the same experience as before.

## What comes next

Through doing this I can see that I might also be able to move my domain from Cloudflare to Mythic Beasts, another win for sovereignty.

If I did that, and moved the code hosting away from GitHub, I&#39;d have a website that&#39;s fully independent.

But that&#39;s for another time.

Over the next few posts I&#39;ll go over:

- how to use the Ansible role
- making a provider for Terraform
- the code that ties it all together
- the site that explains it all in-depth

## Now

For now this post marks that this site&#39;s no longer hosted by GitHub Pages.

It&#39;s on a tiny computer, closer to home, set up by my own code.

That feels a bit more mine.
</source:markdown></item><item>
      <title>Launched: infra.paultibbetts.uk A documentation site for the infrastructure that …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/15/2026-02-15-145458/</link>
      <pubDate>Sun, 15 Feb 2026 14:54:58 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/15/2026-02-15-145458/</guid>
      <category>Ansible</category>
      <category>Terraform</category>
      <category>meta</category>
      <description>Launched: infra.paultibbetts.uk A documentation site for the infrastructure that runs paultibbetts.uk.</description>
      <content:encoded><![CDATA[<p>Launched: <a href="https://infra.paultibbetts.uk/">infra.paultibbetts.uk</a>
</p>
<p>A documentation site for the infrastructure that runs <code>paultibbetts.uk</code>.</p>]]></content:encoded><source:markdown>
Launched: [infra.paultibbetts.uk](https://infra.paultibbetts.uk/)

A documentation site for the infrastructure that runs `paultibbetts.uk`.
</source:markdown></item><item>
      <title>Published: paultibbetts/terraform-provider-mythicbeasts A Terraform provider for …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/09/2026-02-09-151500/</link>
      <pubDate>Mon, 09 Feb 2026 15:15:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/09/2026-02-09-151500/</guid>
      <category>Terraform</category>
      <category>go</category>
      <category>code</category>
      <category>Mythic Beasts</category>
      <description>Published: paultibbetts/terraform-provider-mythicbeasts A Terraform provider for Mythic Beasts.</description>
      <content:encoded><![CDATA[<p>Published: <a href="https://github.com/paultibbetts/terraform-provider-mythicbeasts">paultibbetts/terraform-provider-mythicbeasts</a>
</p>
<p>A Terraform provider for Mythic Beasts.</p>]]></content:encoded><source:markdown>
Published: [paultibbetts/terraform-provider-mythicbeasts](https://github.com/paultibbetts/terraform-provider-mythicbeasts)

A Terraform provider for Mythic Beasts.
</source:markdown></item><item>
      <title>Published: paultibbetts/mythic-beasts-client-go A client for the Mythic Beasts …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/06/2026-02-06-130000/</link>
      <pubDate>Fri, 06 Feb 2026 13:00:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/06/2026-02-06-130000/</guid>
      <category>go</category>
      <category>code</category>
      <category>Mythic Beasts</category>
      <description>Published: paultibbetts/mythic-beasts-client-go A client for the Mythic Beasts APIs, written in Go.</description>
      <content:encoded><![CDATA[<p>Published: <a href="https://github.com/paultibbetts/mythicbeasts-client-go/">paultibbetts/mythic-beasts-client-go</a>
</p>
<p>A client for the Mythic Beasts APIs, written in Go.</p>]]></content:encoded><source:markdown>
Published: [paultibbetts/mythic-beasts-client-go](https://github.com/paultibbetts/mythicbeasts-client-go/)

A client for the Mythic Beasts APIs, written in Go.
</source:markdown></item><item>
      <title>Published: paultibbetts/ansible-role-caddy An Ansible role to install and …</title>
      <link>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/03/2026-02-03-175500/</link>
      <pubDate>Tue, 03 Feb 2026 17:55:00 +0000</pubDate>
      <guid>https://paultibbetts-uk-feat-cooks.preview.lab.paultibbetts.uk/2026/02/03/2026-02-03-175500/</guid>
      <category>Ansible</category>
      <category>code</category>
      <category>Caddy</category>
      <description>Published: paultibbetts/ansible-role-caddy An Ansible role to install and operate Caddy web server.</description>
      <content:encoded><![CDATA[<p>Published: <a href="https://github.com/paultibbetts/ansible-role-caddy">paultibbetts/ansible-role-caddy</a>
</p>
<p>An Ansible role to install and operate Caddy web server.</p>]]></content:encoded><source:markdown>
Published: [paultibbetts/ansible-role-caddy](https://github.com/paultibbetts/ansible-role-caddy)

An Ansible role to install and operate Caddy web server.
</source:markdown></item>
  </channel>
</rss>
