David Makin
banner
sleep-er.bsky.social
David Makin
@sleep-er.bsky.social
Lives in #Manchester #UK 🇬🇧 🏂

Messes around with computers

Likes #coffee, #cats, #scifi, #fantasy & #snowboarding

PRONOUNS he/him
SITE https://www.sleep-er.co.uk
CATS 2
GITHUB DavidMakin.github.io
Why yes I do have a large tower of Lavabread. (https://www.sleep-er.co.uk/notes/2026-09-10-19-23/)
September 10, 2026 at 6:23 PM
Reposted by David Makin
Agreena Lands Major Forward Deal For Kazakh Soil Carbon Credits

Agreena has secured a seven-year agreement to sell 4.45 million tonnes of soil carbon credits to one of the ...

carbonherald.com/agreena-la...

Via: Antonie.ai
#SoilHealth #Biodiversity #Ecosystem #Carbon #Agriculture #Farming
September 8, 2026 at 9:22 AM
I use n8n to poll Hardcover's GraphQL API and post finished books to this site via Micropub. Here's how that workflow runs, how I bulk-imported 805 books, and how I fixed the dates afterwards. https://www.sleep-er.co.uk/2026/09/08/posting-finished-books-from-hardcover-to-my-own-site/
September 8, 2026 at 3:38 PM
What is this? Not something I've ordered and not something I've sent so it would be returned. No idea why I have been sent this parcel (https://www.sleep-er.co.uk/notes/2026-09-08-14-46/)
September 8, 2026 at 1:50 PM
I'm a valued Lavabread customer. My life has reached it's pinnacle.

Added via Quill
September 5, 2026 at 7:08 PM
Micropub + OwnYourSwarm: posting checkins from your phone to your own site
Micropub + OwnYourSwarm: posting checkins from your phone to your own site
Every time I check in somewhere on Swarm, a post appears on my blog automatically. Micropub helps make this happen. Micropub is an open standard for posting to your own site from third-party clients. Instead of logging into WordPress, you authenticate once via IndieAuth and then use any Micropub-capable app to publish notes, articles, checkins, and more — directly to your site. OwnYourSwarm is a service that takes your Swarm (Foursquare) checkins and posts them to your site via Micropub. Your checkins live on your own site, with a static map image, and optionally syndicate out to Mastodon or Bluesky. ## Micropub on WordPress The **Micropub** plugin by the IndieWeb community adds a Micropub endpoint to WordPress. Install it and your site immediately advertises the endpoint via `rel="micropub"` in the page head. Authentication goes through **IndieAuth** , which is handled by a separate plugin. IndieAuth lets you use your own domain as your identity — apps redirect to your site’s authorization endpoint, you log in with your WordPress credentials, and the app receives a token it uses for future requests. ## OwnYourSwarm setup 1. Visit ownyourswarm.p3k.io 2. Connect your Foursquare/Swarm account 3. Sign in with your site URL via IndieAuth 4. OwnYourSwarm is now authorised to post to your site From this point, every Swarm checkin triggers a Micropub POST to your WordPress site. The post includes the venue name, location coordinates, and a Swarm checkin URL. ## Shaping the checkin post The raw Micropub payload from OwnYourSwarm needs some processing before it makes a readable WordPress post. I handle this with a WordPress mu-plugin using the `pre_insert_micropub_post` filter. The filter sets the post format to `status`, generates post content from the checkin data (venue name, any note you added in Swarm), and sets a short excerpt for card previews. Checkin cards on the archive: map thumbnail, venue name, location, and a Read More link. ## The static map Simple Location is a plugin that handles geo data on WordPress posts. When OwnYourSwarm sends coordinates, Simple Location picks them up and can display a map on the post. I use a static map image in the card excerpt area on archive pages — a small 16:9 thumbnail showing the location. This comes from Simple Location’s map provider integration. Mapbox works well and has a free tier sufficient for a personal site. On single post pages, Simple Location renders a full map and the location text (“Manchester, Greater Manchester”). The map loads at priority 11 in `the_content` filter — something to know if you’re wrapping content in `e-content` for microformats (that wrapper needs to go at a lower priority, after the map). ## Per-checkin syndication When posting via Micropub, syndication targets are available as checkboxes in supporting clients. Quill queries `q=syndicate-to` from your Micropub endpoint and shows your configured Mastodon and Bluesky targets. You tick which ones you want per post. OwnYourSwarm doesn’t expose a syndication target picker — it just posts. If you want checkins to syndicate automatically, configure that in Syndication Links’ global settings. If you want per-checkin control, post manually via Quill instead. Check in somewhere on Swarm. A post appears on your site within minutes, with venue name, your note, a location tag, and a static map thumbnail. From there it can syndicate to Mastodon or Bluesky if you want. Likes and replies from those platforms come back as webmentions. My checkin history lives on my own site. If Facebook shuts down Swarm, I still have my own copy.
www.sleep-er.co.uk
September 4, 2026 at 10:35 PM
August Bank Holiday weekends for the past couple of years have always been glamping somewhere in Wales. Sometime they have Pizza oven or even a visiting cat. This one was a Geodome with loads of space and a bath with a fantastic view.

Added via Quill
August 31, 2026 at 8:53 PM
My wife pegged me in the garden this morning

Added via Quill
August 13, 2026 at 11:21 AM
Custom post types on a personal WordPress site
Custom post types on a personal WordPress site
This site runs on WordPress, and until recently every post, articles, short notes, checkins, and 805 book posts, lived as the default `post` type. The homepage sorted everything by date and rendered each item with the same Blocksy card layout. Book posts showed up as cards with a title, an excerpt, and no cover image. Checkins had a map thumbnail but also a featured image slot that made no sense for a venue checkin. Notes had empty title slots because notes don’t have titles. Separating them into custom post types fixed the rendering mismatch. One for `book`, one for `checkin`, one for `note`. Each gets its own archive URL. Blocksy stores card layout settings per CPT as `{cpt}_archive_archive_order` theme mods, so each type can be rendered differently. The homepage still shows everything mixed together, that’s intentional, but Blocksy now knows what it’s rendering. Before CPTs: everything was a `post`. After: Blocksy renders each type with its own card layout. ## Registering the types All three are registered in a `post-types.php` mu-plugin. The common args: `public => true`, `has_archive => true`, `show_in_rest => true` (Gutenberg won’t work without it), and a `rewrite` slug for the archive URL. register_post_type( 'book', [ 'public' => true, 'has_archive' => true, 'rewrite' => [ 'slug' => 'books' ], 'show_in_rest' => true, 'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ], 'labels' => [ 'name' => 'Books', 'singular_name' => 'Book' ], ] ); register_post_type( 'checkin', [ 'public' => true, 'has_archive' => true, 'rewrite' => [ 'slug' => 'checkins' ], 'show_in_rest' => true, 'supports' => [ 'title', 'editor', 'custom-fields' ], 'labels' => [ 'name' => 'Checkins', 'singular_name' => 'Checkin' ], ] ); register_post_type( 'note', [ 'public' => true, 'has_archive' => true, 'rewrite' => [ 'slug' => 'notes' ], 'show_in_rest' => true, 'supports' => [ 'editor', 'custom-fields' ], 'labels' => [ 'name' => 'Notes', 'singular_name' => 'Note' ], ] );Code language: PHP (php) Notes skip `title` from `supports`, they’re short, unheadlined posts, so the title field would just stay empty. Blocksy also skips the `read_more` slot for titleless post types. The button never renders, even when enabled in the Customizer. The workaround is to inject it manually via the `blocksy:archive:render-card-layers` filter, checking that `$outputs['read_more']` is empty before setting it. The /books/ archive after migration: each card shows cover, title, author, reading status, and date. ## Migrating 847 posts Everything was already tagged with `book`, `checkin`, and `note`, so migration was a WP-CLI one-liner per type. Books first, since there were 805 of them: wp post list --post_type=post --tag=book --field=ID | xargs -I{} wp post update {} --post_type=bookCode language: PHP (php) Same pattern for checkins and notes. The posts move; the tags, meta, and content stay intact. After migration, the Blocksy theme mods needed patching. If the CPT-specific key, say, `note_archive_archive_order`, doesn’t exist in the database, Blocksy falls back to a hardcoded default with `read_more` disabled. The card layout you configured in the Customizer gets silently ignored. Copy `blog_archive_order` into each CPT-specific key via WP-CLI: wp eval "set_theme_mod('note_archive_archive_order', get_theme_mod('blog_archive_order'));" wp eval "set_theme_mod('book_archive_archive_order', get_theme_mod('blog_archive_order'));" wp eval "set_theme_mod('checkin_archive_archive_order', get_theme_mod('blog_archive_order'));"Code language: JavaScript (javascript) Notes archive: short, titleless cards. Blocksy skips the read_more slot for titleless types, with the filter injecting it manually. ## Routing Micropub posts to the right type Checkins arrive via OwnYourSwarm and Micropub. Books arrive via Hardcover and n8n, also through Micropub. The Micropub plugin creates `post` type by default, which would immediately undo the migration. The `pre_insert_micropub_post` filter runs before the post is inserted and lets you modify the post array. For a checkin payload, set `post_type` to `checkin`. For note-format posts, `note`. Checkin content gets generated from the venue name and locality at the same point. add_filter( 'pre_insert_micropub_post', function( $post_data, $input ) { if ( isset( $input['properties']['checkin'] ) ) { $post_data['post_type'] = 'checkin'; // generate post content from venue name + locality } elseif ( in_array( 'post-format-note', (array) ( $input['properties']['category'] ?? [] ), true ) ) { $post_data['post_type'] = 'note'; } return $post_data; }, 10, 2 );Code language: PHP (php) New checkins and notes now land in the right type without manual intervention. The homepage picks them up because `WP_Query` on `is_home()` includes all three CPTs alongside standard posts. The `/books/`, `/notes/`, and `/checkins/` archives exist now. Each renders with its own card layout. ## What the /books/ archive grew into Three features emerged on the archive page. ### Year headings Books are sorted by finish date. The archive injects a year label whenever the year changes between cards via the `blocksy:archive:render-card-layers` filter, then hoisted out of the card wrapper by a small inline script. ### Status filter `?status=finished`, `?status=reading`, `?status=want-to-read` filter via `pre_get_posts` and a `meta_query` on `mf2_read-status`. The filter bar is injected before the card grid via `wp_footer` JS. Server-side, so it works across pagination and is bookmarkable. ### Note badge Notes on the homepage have no title. A small “Note” pill label distinguishes them from articles in the mixed stream.
www.sleep-er.co.uk
August 5, 2026 at 10:02 AM
DNS Sinkhole on a Phone: AdGuardHome + Unbound on Android (Termux)
DNS Sinkhole on a Phone: AdGuardHome + Unbound on Android (Termux)
post_content I wanted network-wide ad blocking and due to the current price of hardware I installed it on an old Pixel 5 phone purchased for less that a stick of 8GB DDR4 ram. ## Architecture flowchart LR Client["LAN Device<br/>DNS: 192.168.1.70"] -->|"port 53"| iptables["Linux Kernel<br/>iptables NAT"] iptables -->|"53 → 5353"| AGH["AdGuardHome<br/>port 5353"] AGH -->|"clean queries"| Unbound["Unbound<br/>port 5335, recursive"] Unbound -->|"root resolution"| Root["Root DNS Servers"] style iptables fill:#f96,color:#000 style AGH fill:#6af,color:#000 style Unbound fill:#6f9,color:#000 AdGuardHome listens on port 5353 – not 53. Android’s network daemon (`netd`) already claims that port. Instead of fighting it, iptables redirects port 53 traffic to 5353 transparently. AGH applies blocklists, then forwards clean queries to Unbound running on localhost:5335. Unbound does full recursive resolution – no forwarding to Google or Cloudflare, straight up to the root servers. ## The Easy Part Termux gives you a proper Linux environment without root. Though for DNS on a privileged port, you do need root access. That’s what `tsu` is for. The setup is pretty standard: * Install Termux, termux-services, and `tsu` * `pkg install unbound` * Grab the arm64 AdGuardHome binary, extract it to `~/services/adguard/` * Write a config pointing upstream to `127.0.0.1:5335`, listening on port 5353 * Set up boot scripts in `~/.termux/boot/` ## The Problem: Nobody Trusts Anybody AdGuardHome needs to download blocklists from HTTPS URLs. The catch: when you run AGH under `su -c`, the root shell doesn’t know about Termux’s CA certificate store. Android’s root environment doesn’t look at `/etc/ssl/certs/` the way you’d expect – those certs live in Termux’s private data directory. The log spit this out: tls: failed to verify certificate: x509: certificate signed by unknown authorityCode language: HTTP (http) AGH couldn’t reach its blocklist URLs because its TLS stack didn’t know who to trust. The blocklist showed up in the admin UI with `rules_count: 0` – registered, but never actually downloaded. Silent failure. ### The Fix Termux keeps its CA bundle at: /data/data/com.termux/files/usr/etc/tls/cert.pem You need to set `SSL_CERT_FILE` before AdGuardHome starts, and make sure the `su` environment sees it too: export SSL_CERT_FILE=/data/data/com.termux/files/usr/etc/tls/cert.pem su -c "export SSL_CERT_FILE=$SSL_CERT_FILE; $HOME_DIR/services/adguard/AdGuardHome ..."Code language: JavaScript (javascript) The trick: `su -c` strips most environment variables. By exporting `SSL_CERT_FILE` in the outer shell and re-exporting it inside the `su -c` string, it passes through to the root process. Miss this, and any HTTPS download from AdGuardHome silently fails. The blocklist will register with zero rules and no errors in the UI – you have to dig into the daemon logs to spot it. ## Port 53: Android Already Claimed It Android’s `netd` binds port 53 for system DNS. Running AGH on port 53 via `su -c` works, but it’s fragile – `netd` can reclaim it and you’re fighting the OS. The cleaner approach: run AGH on a high port and let the kernel redirect. Two iptables rules in the NAT table, one for UDP and one for TCP: iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 5353 iptables -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 5353 `PREROUTING` intercepts packets as they arrive – before the routing decision – and rewrites the destination port. Traffic from any device on the LAN pointing DNS at the Pixel’s IP is transparently handed to AGH on 5353. Clients never know it’s not port 53. For local queries (a `dig @127.0.0.1` from Termux, for example), the `OUTPUT` chain handles it, with one tweak: AGH runs as root, and its own upstream queries also use port 53 to resolve bootstrap DNS. Excluding the root UID prevents those queries from looping back to AGH: iptables -t nat -A OUTPUT -p udp --dport 53 -m owner ! --uid-owner 0 -j REDIRECT --to-port 5353 iptables -t nat -A OUTPUT -p tcp --dport 53 -m owner ! --uid-owner 0 -j REDIRECT --to-port 5353 These rules live in a boot script at `~/.termux/boot/02-iptables.sh`, which runs 15 seconds after the phone starts – enough time for the network stack to be ready – then AGH starts a few seconds later. The rules are ephemeral (iptables doesn’t persist across reboots on Android), so the boot script recreates them every time. With AGH now on 5353, there’s no conflict with `netd`. ## Boot Scripts Four boot scripts in `~/.termux/boot/`, staggered so they don’t trip over each other, plus a helper that keeps sshd alive: # 01-unbound.sh - sleep 30, kill stale process, start Unbound on port 5335 su -c "pkill unbound" su -c "/data/data/com.termux/files/usr/bin/unbound -c /data/data/com.termux/files/usr/etc/unbound/unbound.conf &" # 02-iptables.sh - sleep 15, flush existing rules, apply 53→5353 redirect su -c "iptables -t nat -F PREROUTING iptables -t nat -F OUTPUT iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 5353 iptables -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-port 5353 iptables -t nat -A OUTPUT -p udp --dport 53 -m owner ! --uid-owner 0 -j REDIRECT --to-port 5353 iptables -t nat -A OUTPUT -p tcp --dport 53 -m owner ! --uid-owner 0 -j REDIRECT --to-port 5353" # Then verifies rules took effect and logs success/failure # 03-adguard.sh - sleep 35, kill stale process, start AGH with SSL_CERT_FILE export SSL_CERT_FILE=/data/data/com.termux/files/usr/etc/tls/cert.pem sleep 35 su -c "pkill AdGuardHome" su -c "export SSL_CERT_FILE=$SSL_CERT_FILE; $HOME_DIR/services/adguard/AdGuardHome -c $HOME_DIR/services/adguard/AdGuardHome.yaml -w $HOME_DIR/services/adguard --no-check-update &" # 100-check-services.sh - sleep 45, then monitor every 60s # Restarts Unbound or AdGuardHome if either process diesCode language: PHP (php) The iptables script runs before AGH so the redirect is in place before the DNS server starts listening. It flushes any existing rules first so stale rules from a previous boot don’t accumulate, then verifies the redirect is active and logs the result. AGH gets 35 seconds to let the network settle, then starts on port 5353 with the `SSL_CERT_FILE` fix from the previous section. There’s also a monitoring script at `100-check-services.sh` that checks both services every 60 seconds and restarts them if they’ve died — boot is unreliable on a phone and processes can crash silently. sequenceDiagram participant Boot as Phone Boot participant Unbound as Unbound participant IPT as iptables participant AGH as AdGuardHome participant Mon as check-services Boot->>+IPT: sleep 15 → apply NAT rules (53→5353) Boot->>+Unbound: sleep 30 → start on :5335 Boot->>+AGH: sleep 35 → start AGH on :5353 AGH-->>Unbound: forward DNS queries Mon->>AGH: health check :5353 every 60s Mon->>Unbound: health check :5335 every 60s ## Android Doze: The Phone Went to Sleep Everything worked – until the screen went off and I came back to a phone that wasn’t resolving DNS. The services were there, the iptables rules were loaded, but AGH and Unbound were stone dead. Android’s Doze mode had put them to sleep and they never woke up. ### Why They Died Android’s Doze mode activates when the device is stationary, unplugged, and the screen is off. At that point, wake locks are mostly ignored, network access gets suspended, and the CPU sleeps. When the phone loses Wi-Fi and comes back, the boot scripts don’t re-run – they’re triggered by Termux:Boot, not by network reconnection. The services stay dead until you reboot or manually start them. ### The Fixes Three layers of defence, from least to most invasive: 1. **Android Settings.** Set Termux battery to **Unrestricted** (Settings > Apps > Termux > Battery). In Developer Options, set **Keep Wi-Fi on during sleep** to **Always**. These tell Android not to optimise Termux away, and keep the Wi-Fi radio alive when the phone idles. 2. **Whitelist and wakelocks.** `termux-wake-lock` acquires a partial CPU wakelock that keeps the Termux process alive. The boot script calls it at startup, and you can verify it’s held with `cat /proc/wakelocks | grep termux`. Additionally, Termux is in Android’s Doze whitelist so it gets maintenance windows even in deep Doze. For Android 12+, `device_config put activity_manager max_phantom_processes 2147483647` prevents the Phantom Process Killer from silently murdering background processes. 3. **Health check loop.** The `100-check-services.sh` script doesn’t just log – it loops every 60 seconds, checks if Unbound and AdGuardHome are still running on their respective ports, and restarts them if they’re gone. This handles the common case: phone drops offline briefly, comes back, services restart within a minute. With all three in place, the phone recovers on its own after a Doze-induced dropout. The only thing you lose is query coverage during the offline window itself – AGH can’t log what it can’t see. Once the Wi-Fi comes back, the health check catches it within 60 seconds. ## Android Private DNS: The Silent Bypass This one’s easy to overlook. Android 12+ has Private DNS (DoT) baked in. If it’s set to “Automatic” or points to a specific provider, the OS bypasses your local resolver entirely – it sends DNS-over-TLS directly to the configured server, completely ignoring whatever you’ve got running on port 53. You need to go to **Settings > Network & Internet > Private DNS** and set it to **Off**. Otherwise AdGuardHome sits there happily logging zero traffic while your phone talks directly to Cloudflare behind your back. ## Syncing from Home In my setup the Pixel phone is actually a secondary DNS server, the main AdGuardHome runs in Docker on a home server (a repurposed office PC running mostly Docker and this WordPress site). It has the blocklists I want, the rewrites I’ve accumulated, the client settings. I don’t want to manuallycopy everything over every time it changes. adguardhome-sync does exactly this: a Go binary that pushes config from an origin AdGuardHome to one or more replicas. It runs as a Docker container on the home server, hitting the Pixel’s admin API every two hours. flowchart LR Origin["Home Server AGH<br/>docker, port :85"] Cron["adguardhome-sync<br/>every 2 hours"] Replica["Pixel 5 AGH<br/>192.168.1.70:3000"] Origin --> Cron Cron --> Replica Replica -.->|"serverConfig: false"| Upstream["Unbound upstream<br/>(not overwritten)"] style Origin fill:#6af,color:#000 style Replica fill:#6af,color:#000 style Cron fill:#f96,color:#000 The sync config is straightforward: origin is the home server’s AGH, replica is the Pixel at `192.168.1.70:3000`. Most features are on – filters, rewrites, client settings, protection status. TLS and DHCP are off since the Pixel has no cert and doesn’t serve DHCP. What doesn’t sync: TLS certs. The Pixel serves LAN-only DNS, no DoT or DoH. It doesn’t need them, and the sync tool doesn’t copy cert files anyway – they’re on-disk, not in the API. The sync has a web dashboard for checking status – when it last ran, any errors, what features are synced. The only regular error is one dead filter list on the origin (`malwaredomainlist.com` is offline). The sync completes regardless – it’s not a problem, just noisy. ## The Full Chain After setup, `dig doubleclick.net @192.168.1.70` returns `0.0.0.0` – blocked. `dig example.com` resolves normally through to Cloudflare DoT. The admin dashboard at `192.168.1.70:3000` shows queries flowing, blocked percentage climbing. And when I add a new blocklist or rewrite rule on the main server, it shows up on the Pixel within two hours automatically. If you’re doing something similar, especially the `su -c` plus HTTPS filter download combo, triple-check that `SSL_CERT_FILE` is set. And double-check you’re not depending on port 53 being free – it probably isn’t.
www.sleep-er.co.uk
July 22, 2026 at 4:39 PM
GPU update. It was not a brick but it was also not a GPU. It was some skin cream

Added via Quill
July 13, 2026 at 5:31 PM
GPU or Brick update. Went to collect the parcel but they could not find it. The system says it is in there. I'll head back later on when they are not as busy

Added via Quill
July 10, 2026 at 5:18 PM
Ordered a 2nd hand GPU from Vinted for a lot less than it should cost. I was expecting the purchase to not be accepted. I was then expecting the delivery to somehow not happen. I'm expecting the parcel to be a nice brick

Added via Quill
July 10, 2026 at 12:09 PM
Getting webmentions working end to end
Getting webmentions working end to end
A webmention is a notification. When someone links to your post from their site, their site can send your site a short HTTP request saying “hey, this URL now links to that URL of yours.” Your site verifies it, fetches the source and records the mention. Comments, likes, and reposts from the IndieWeb, Mastodon and Bluesky via Brid.gy. ## Receiving webmentions The Webmention plugin (by Matthias Pfefferle) handles incoming webmentions. It exposes a `rel="webmention"` endpoint in your site’s `<head>`, receives the HTTP POST, verifies the source URL actually links to your content, and stores the mention. ## Displaying them as comments Raw webmentions aren’t very readable. The **Semantic Linkbacks** plugin transforms them into structured display: likes show as avatar rows, replies show as threaded comments, reposts show as boosts. It reads microformats2 from the source page to understand the context. Semantic Linkbacks is listed on WordPress.org but the GitHub version is sometimes more current. Either works: wp plugin install semantic-linkbacks --activate # or, for the latest from GitHub: wp plugin install https://github.com/pfefferle/semantic-linkbacks/archive/refs/heads/master.zip --activateCode language: PHP (php) ## Getting backfeed from Mastodon and Bluesky **Brid.gy** polls Mastodon and Bluesky for interactions on posts it knows about, then sends webmentions to your site for each one. Likes, replies, and boosts all come through. Setup: 1. Visit brid.gy 2. Sign in with Mastodon and Bluesky separately 3. Authorise Brid.gy to read your interactions Brid.gy discovers which posts to watch because when you syndicate to Mastodon or Bluesky, the syndicated post URL gets stored on your WordPress post. Brid.gy follows those URLs back to your site. It polls on a schedule. Interactions don’t arrive immediately — expect a delay of minutes to hours depending on Brid.gy’s queue. A like I got on Mastodon took about 40 minutes to show up on the post the first time I tested this. Worth knowing so you don’t think it’s broken. ## Microformats on your site For Brid.gy to correctly attribute webmentions back to your posts, and for Semantic Linkbacks to parse incoming mentions correctly, your pages need proper microformats2 markup: * `h-entry` on the post container * `e-content` on the post body * `u-url` on the permalink * `p-author` with `h-card` (name + URL) Blocksy doesn’t add these. I add them in mu-plugins/microformats.php. The `h-entry` class goes on via `post_class`; the title gets wrapped in `p-name` via `the_title`; and the author card is added during `the_content`: add_filter('post_class', function($classes) { $classes[] = 'h-entry'; return $classes; }); add_filter('the_title', function($title) { return '<span class="p-name">' . $title . '</span>'; });Code language: PHP (php) The content gets wrapped in `<div class="e-content">` with `dt-published` and `u-url` metadata prepended. This runs at the_content priority 20 — after Simple Location’s map output at 11/12 — so the map lands inside the e-content scope: add_filter('the_content', function($content) { $hidden = '<div style="display:none">' . '<time class="dt-published">' . $iso . '</time>' . '<a class="u-url">' . $url . '</a>' . '</div>'; return $hidden . '<div class="e-content">' . $content . '</div>'; }, 20);Code language: PHP (php) Categories are output as invisible `p-category` `<data>` elements at priority 0, before the content filter chain starts. Bridgy picks these up and converts them to hashtags on syndicated copies. ## The rel=”me” web of trust For IndieAuth and for Mastodon to verify your identity, your site needs `rel="me"` links pointing to your social profiles, and those profiles need to link back to your site. <link rel="me" href="https://hachyderm.io/@yourusername" /> <link rel="me" href="https://bsky.app/profile/yourusername.bsky.social" />Code language: HTML, XML (xml) Mastodon shows a green verified checkmark on your profile once it confirms the back-link. Bluesky doesn’t have the same mechanism yet but having the link doesn’t hurt. The full backfeed loop: your post goes out, interactions come back as webmentions, Semantic Linkbacks renders them. ## Testing the whole chain webmention.rocks lets you test that your endpoint is working. Send a test webmention and check it appears in your WordPress comments. For the full backfeed loop: publish a post, syndicate it to Mastodon, boost or like it from another account, then wait for Brid.gy to poll. The like should appear on your post within an hour.
www.sleep-er.co.uk
July 8, 2026 at 9:40 AM
POSSE in practice: syndicating to Mastodon and Bluesky from your own site
POSSE in practice: syndicating to Mastodon and Bluesky from your own site
POSSE stands for Publish on Own Site, Syndicate Elsewhere. Your site is the canonical home for everything you write. Social media platforms get copies, not originals. If a platform shuts down or you leave due to crazy new owners, your content survives. ## The pieces **Syndication Links** is a WordPress plugin that manages the list of places a post has been syndicated to. It stores those URLs as post meta and can display them as icons in your post footer. It also integrates with Micropub so clients can request syndication at post time. **Brid.gy Publish** is a service that takes a webmention from your site and cross-posts to Mastodon or Bluesky on your behalf. You authorise Brid.gy with each platform once, and after that you trigger a cross-post by sending a webmention to `https://brid.gy/publish/mastodon` or `https://brid.gy/publish/bluesky`. Syndication Links handles the webmention sending. You configure the providers in wp-admin and it fires them automatically when a post is published. Syndication Links sends a webmention to Brid.gy on publish. Brid.gy cross-posts and returns the syndicated URL as another webmention. ## What actually gets sent For Mastodon, Brid.gy reads your post’s excerpt or the first paragraph of content and posts it as a toot, with a link back to your original. For Bluesky, the behaviour is similar. Brid.gy creates a Bluesky post with your excerpt and a link card preview if your site has appropriate OpenGraph meta tags. The syndicated URLs come back from Brid.gy as webmentions and Syndication Links stores them, so your post gains a “also on Mastodon” / “also on Bluesky” footer automatically. If you want those webmentions to display as comments and likes on your posts, that requires some extra setup — covered in Getting webmentions working end to end. ## Microformats matter Brid.gy reads your post content using microformats2. For it to correctly identify your title, content, and author it needs: * `h-entry` on the post article element * `p-name` on the post title * `e-content` wrapping the post body * `p-author` with `h-card` somewhere on the page Blocksy doesn’t add these by default. I add them via a mu-plugin using `post_class` and `the_content` filters. Without them, Brid.gy either fails silently or posts something garbled. add_filter( 'post_class', 'possee_add_hentry_class' ); function possee_add_hentry_class( $classes ) { $classes[] = 'h-entry'; return $classes; } add_filter( 'the_title', 'possee_title_pname', 10, 2 ); function possee_title_pname( $title, $id = null ) { if ( ! is_singular() ) { return $title; } return '<span class="p-name">' . $title . '</span>'; } add_filter( 'the_content', 'possee_wrap_econtent', 20 ); function possee_wrap_econtent( $content ) { if ( ! is_singular() || ! in_the_loop() ) { return $content; } return '<div class="e-content">' . $content . '</div>'; }Code language: PHP (php) Three filters, about fifteen lines — full source here. The `p-author` `h-card` lives in the theme template — every page already has an author byline, it just needs the right class names. ## Per-post control Not every post should go to every platform. A checkin probably doesn’t need to go to Bluesky. A long article probably should go everywhere. Syndication Links lets you choose per post which providers to fire. In the Gutenberg editor there’s a panel in the sidebar where you tick Mastodon and/or Bluesky before publishing. Via Micropub clients like Quill you get checkboxes that query the same list of targets. Micropub also enables other posting workflows — including automatic checkin posts from Swarm. ## What breaks ### Images Brid.gy attaches images when it finds an `img` with `class="u-photo"` inside the `e-content` div. Gutenberg’s image block doesn’t add this automatically — add `u-photo` via the block’s Additional CSS class field, or via a `the_content` filter. For Micropub posts, a mu-plugin handles it: photos are rendered with the correct class automatically. add_filter( 'the_content', 'possee_render_micropub_photos', 21 ); // reads mf2_photo post meta, appends after e-content: // <figure class="micropub-photo"> // <img class="u-photo" src="..." loading="lazy"> // </figure>Code language: HTML, XML (xml) ### Post kinds Brid.gy uses the post’s excerpt (`p-summary`) as the syndicated text, falling back to the full content. Articles work fine with an explicit excerpt set in the editor. Short notes work naturally — the content is the post. Checkins are the awkward case: you want the venue name and note text, not the full rendered HTML. Without intervention, Syndication Links sends the entire page excerpt which includes map thumbnails, location icons, and weather text. A `get_the_excerpt` filter handles this. For checkins it reads venue name, locality, and weather directly from post meta and renders them in a compact format: add_filter( 'get_the_excerpt', 'possee_checkin_excerpt', 5, 2 ); function possee_checkin_excerpt( $excerpt, $post ) { if ( ! has_tag( 'checkin', $post ) ) { return $excerpt; } $venue = get_post_meta( $post->ID, 'mf2_venue-name', true ); $place = get_post_meta( $post->ID, 'geo_address', true ); $temp = get_post_meta( $post->ID, 'weather_temperature', true ); return 'Checked into ' . $venue . ' — ' . $place . ( $temp ? ' (' . round( (float) $temp ) . '°C)' : '' ); }Code language: PHP (php) For short notes, a similar filter strips location data from the excerpt and renders it as a separate card element instead. Brid.gy sees only the note text. Without these filters, Brid.gy posts garbled text to Mastodon and Bluesky — the syndicated post ends up reading like “Bishop’s Meadow, Llanddew, POW 17°C scattered clouds” instead of the note itself. ### Timing Brid.gy processes webmentions asynchronously. The syndicated URL doesn’t appear immediately — it arrives minutes later as a return webmention. Made me think it wasn’t working at first. Turns out I was too impatient.
blog.sleep-er.co.uk
June 23, 2026 at 9:53 AM
Not sure how that will happen

Added via Quill
June 22, 2026 at 4:33 PM
Enjoying life in a fancy shepherd's hut with Alpaca next door for neighbours

Bishop's Meadow, Llanddew, POW

17 °C Scattered Clouds

Added via Quill
June 16, 2026 at 12:17 PM
Hardening a WordPress Docker container you can’t shell into (https://blog.sleep-er.co.uk/?p=294)
Hardening a WordPress Docker container you can’t shell into
That title makes this sound a lot grander than it is. I didn’t harden a Docker container — I merely used a preexisting one. The Docker image I set up for WordPress is a hardened build from DHI. No shell. Harder to hack, and as this is running on my home server, I like the sound of that. The broader setup — Cloudflare Tunnel, Nginx, Docker Compose — is covered in a separate post. ## WP-CLI via throwaway container WP-CLI manages WordPress — plugins, database queries, updates, options. With a hardened container, you run it in a throwaway container that mounts the same volume: WordPress has no shell. WP-CLI runs in a separate container that mounts the same `wp_data` volume and connects to the same database. Two things will catch you out. docker run --rm --user 65532 -v wp-possee_wp_data:/var/www/html -v /path/to/mu-plugins:/var/www/html/wp-content/mu-plugins --network db -e WORDPRESS_DB_HOST=mariadb -e WORDPRESS_DB_USER=wordpress -e WORDPRESS_DB_PASSWORD=yourpassword -e WORDPRESS_DB_NAME=wordpress wordpress:cli-php8.3 wp --allow-root <command>Code language: JavaScript (javascript) Two things catch you out. First: the user flag. My uploads directory is owned by UID 65532 — the non-root user the hardened image runs as. If you run WP-CLI as root (the default in the CLI image), any files it creates will be owned by root and the web process can’t write to them. The Hardened Images catalog | WordPress | Docker Hub page mentions this. I didn’t read it until I was having problems — my own fault. Second: the mu-plugins mount. My mu-plugins directory is bind-mounted from the host, not baked into the named volume. Skip the `-v` flag and WP-CLI starts without your mu-plugins loaded. ## Reading files inside the container Sometimes you need to inspect a theme file or grep for a pattern in a plugin. Since you can’t exec in, Alpine works as a read-only shim: docker run --rm -v wp-possee_wp_data:/data alpine:latest cat /data/wp-content/themes/blocksy/functions.phpCode language: JavaScript (javascript) docker run --rm -v wp-possee_wp_data:/data alpine:latest grep -rn "your_pattern" /data/wp-content/plugins/Code language: JavaScript (javascript) One gotcha: Alpine ships BusyBox grep, which doesn’t support `--include`. Search by path rather than trying to filter by file extension. ## OPcache means your changes don’t take effect immediately PHP OPcache is enabled and caches compiled PHP. The default `revalidate_freq` is 60 seconds — after deploying a change you’d be waiting up to a minute before the new code runs. I set `opcache.revalidate_freq = 0` in `uploads.ini` so changes are picked up immediately. Worth doing; the performance cost is negligible on a personal site. ## More caches than you’d think This site runs WP-Optimize for page caching and minification. The nginx FastCGI cache clears quickly — just restart the nginx container. The WP-Optimize page cache lives on disk under `wp-content/cache/wpo-cache/`, has a 24-hour TTL, and survives container restarts entirely. You have to delete it manually. There’s a third cache: the WP-Optimize minify bundle. This one I learned to leave alone. Clearing it causes Blocksy to regenerate its dynamic CSS on the next page load. That process is memory-intensive enough that it crashed the site when I was running the 128MB PHP default. I’ve since removed WP-Optimize entirely — for a small personal blog, it’s more complexity than it’s worth. ## Secrets in environment variables The hardened image reads WordPress config from environment variables passed to the container. Database credentials, site URL, and PHP constants all go in `docker-compose.yml` via the `WORDPRESS_CONFIG_EXTRA` key. Nothing sensitive ends up in files you can accidentally commit. The one wrinkle: `WORDPRESS_CONFIG_EXTRA` also sets `WP_DEBUG`, but the base image sets it first. You’ll see a harmless “constant already defined” warning in logs — ignore it. My house
blog.sleep-er.co.uk
June 9, 2026 at 7:13 PM
Reposted by David Makin
Every year for Pride, I repost Hue. I made him to send love and comfort to those who feel Pride isn’t for them because they aren’t out. You matter, you are valid, and it’s ok if you aren’t ready yet. Coming out is not a requirement for being part of this community. Happy Pride 🌈
June 1, 2026 at 3:05 PM
Self-hosting WordPress behind Cloudflare Tunnel with no open ports (https://blog.sleep-er.co.uk/?p=293)
Self-hosting WordPress behind Cloudflare Tunnel with no open ports
Most self-hosting guides assume you have a public IP and ports 80 and 443 forwarded to your router. That works. Your home IP ends up in DNS, you’re running certbot on a cron job, and when your ISP changes your address you’re hoping the dynamic DNS updater caught it. I don’t have the luxury of that option. My ISP puts me behind CGNAT, which means I share a public IP with other customers. Inbound connections never reach my router. Port forwarding is pointless 🙁 I run this site through a Cloudflare Tunnel instead — an outbound connection my server makes to Cloudflare’s edge. Nothing listening for inbound traffic, no cert to renew. ## How Cloudflare Tunnel works The `cloudflared` daemon runs as a Docker container alongside WordPress. On startup it makes an outbound HTTPS connection to Cloudflare using a tunnel token you generate in the Zero Trust dashboard. After that, requests for your domain hit Cloudflare’s edge and get forwarded through that connection to your containers. Your server never accepts an inbound connection. To anyone outside, it looks like any other site behind Cloudflare. ## The Docker setup Traffic flows from Cloudflare’s edge through an outbound tunnel to nginx, then to PHP-FPM. Nothing listens for inbound connections. The stack is three containers: WordPress (PHP-FPM), Nginx, and Cloudflared. Cloudflared talks only to Nginx, which handles FastCGI proxying and caching. services: wordpress: # dhi.io is a hardened image registry — swap for wordpress:php8.3-fpm if you prefer the official image image: dhi.io/wordpress:6.9.4-php8.3-fpm volumes: - wp_data:/var/www/html - ./php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro environment: WORDPRESS_DB_HOST: mariadb:3306 WORDPRESS_DB_NAME: ${MYSQL_DATABASE} WORDPRESS_DB_USER: ${MYSQL_USER} WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD} WORDPRESS_CONFIG_EXTRA: | define('WP_HOME', 'https://${DOMAIN}'); define('WP_SITEURL', 'https://${DOMAIN}'); define('FORCE_SSL_ADMIN', true); define('DISALLOW_FILE_EDIT', true); define('WP_DEBUG', false); if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; } restart: unless-stopped networks: - wp-possee - db nginx: # dhi.io hardened nginx — swap for nginx:alpine if preferred image: dhi.io/nginx:1 depends_on: - wordpress volumes: - wp_data:/var/www/html - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro restart: unless-stopped networks: - wp-possee cloudflared: image: cloudflare/cloudflared:latest command: tunnel --no-autoupdate --config /etc/cloudflared/config.yml run --token ${CLOUDFLARE_TUNNEL_TOKEN} volumes: - ./cloudflared/config.yml:/etc/cloudflared/config.yml restart: unless-stopped networks: - wp-possee networks: wp-possee: db: external: true volumes: wp_data:Code language: PHP (php) The config file points cloudflared at Nginx: ingress: - hostname: yourdomain.com service: http://nginx:80 - service: http_status:404Code language: JavaScript (javascript) Cloudflare terminates TLS at the edge. Nginx only needs to listen on port 80 internally. One consequence of this setup: the WordPress container runs without a shell. You cannot exec into it to run commands or inspect files. I have written about the workarounds separately — WP-CLI in throwaway containers, reading files via Alpine. ## Nginx still matters Even without handling TLS, Nginx does real work here: FastCGI caching, gzip, rate limiting on wp-admin, blocking user enumeration. Don’t skip it and proxy directly to PHP-FPM. WordPress also needs to know requests are arriving over HTTPS, even though Nginx only sees HTTP from cloudflared. Add this to wp-config: if (isset($_SERVER[HTTP_X_FORWARDED_PROTO]) && $_SERVER[HTTP_X_FORWARDED_PROTO] === https) { $_SERVER[HTTPS] = on; }Code language: PHP (php) Without it, WordPress generates HTTP URLs and the admin redirects loop. ## The tradeoffs Cloudflare sees all your traffic and the tunnel adds a little delay. For a personal blog I’m fine with that. The other risk: if Cloudflare goes down, your server stays up but nobody can reach it. This happened to me once. Half the internet was also down at the time, so I didn’t worry about it. WebSockets need an extra toggle in the Zero Trust dashboard. WordPress doesn’t use them, so it didn’t affect me but worth knowing if you’re routing anything else through the same tunnel. The operational side is genuinely lighter. No cert renewal, no dynamic DNS, all handled via Cloudflare. When something breaks it’s always been something I did, not the tunnel.
blog.sleep-er.co.uk
June 2, 2026 at 10:37 AM
Auto Draft
Auto Draft
hello Berlin. I am in you. In your transport system. Hopefully easy to navigate
blog.sleep-er.co.uk
June 16, 2026 at 12:40 PM
I'm a rebel

M56, Ringway, ENG

28 °C Clear Sky

Added via Quill
May 25, 2026 at 10:05 AM
Off to Berlin with work. Bit of a rollercoaster start. First the train was delayed, then it was cancelled and finally it was uncancelled and arrived on time. Next stop the airport ✈️

Mauldeth Road, Manchester, ENG

21 °C Clear Sky

Added via Quill
May 25, 2026 at 7:50 AM
2026-05-21T15:05:21+00:00https://blog.sleep-er.co.uk/notes/2026-05-21-16-05/

Lets go all in on AI they say and we want it by next week. Never trust anything when the following phrase is used, "It is actually that simple."

#aihell

Added via Quill (https://blog.sleep-er.co.uk/?p=3493)
May 21, 2026 at 3:35 PM
2026-05-16T11:13:47+00:00https://blog.sleep-er.co.uk/2026/05/16/367/

Local parkrun was not in today so we headed to Burnage Parkrun. Really nice course by the River Mersey with only a small hill
May 16, 2026 at 11:13 AM