{
    "version": "https://jsonfeed.org/version/1",
    "title": "Jack.is typing",
    "home_page_url": "https://jack.is",
    "description": "I teach sand to think. Also occasionally write blog posts.",
    "author": {
        "name": "Jack Platten"
    },
    "items": [
        {
            "id": "https://jack.is/posts/self-hosting-your-pds/",
            "content_html": "<p>Bluesky is great (as evidenced by my multiple posts on it), but I wanted to own\nmy data more than it living on the Bluesky servers. Now I could have just used\nthe standard deployment as shown <a href=\"https://github.com/bluesky-social/pds\">in the PDS repo</a>,\nbut I wanted to do something a little more fitting my deployment practices.</p>\n<h2>Why</h2>\n<p>A few years ago, I moved my webserver (what runs this site, my PDS, and lots of\nother services) to CentOS Stream back when that was still viable, then Rocky\nLinux as the new replacement when Stream sort of died off. Is it perfect, is it\nthe right way to do things? No, but it’s what I prefer at this point. <sup><a href=\"#user-content-fn-1\">1</a></sup></p>\n<h2>The Problem: part one</h2>\n<p>The default Bluesky PDS is intended for being deployed on Ubuntu 20.04/22.04. It\nwill fail to deploy on anything other than those two distributions (even 24.04\nwhich also works but that’s neither here nor there).</p>\n<h3>Solutioning a problem</h3>\n<p>While I was in the testing phase of deploying my PDS, I was using an Ubuntu\nserver that I could successfully deploy using the installer. I already use Caddy\nthough, so the compose file having Caddy dockerized is not <em>ideal</em>. Fixing that\nwas a pretty easy cleanup to get a proper <code>pds.env</code> file out of it, since\nthere’s no canonical source outside of the installer. Now I had a relatively\nstandard PDS deployment using Docker, but using the Caddy service I use for\neverything else on my server.</p>\n<h2>Best Practices?</h2>\n<p>I have <em>opinions</em> on the way Bluesky PBLLC has made the systemd unit for the PDS\nin GitHub. It deploys as a oneshot, so <code>journalctl</code> is not super useful,\n<code>systemctl</code> will never show it as running, and it is generally just hard to set\nit as a dependency or a dependent of any other services due to it being oneshot.</p>\n<p>Fixing this took me on a rabbit hole / detour on how to do it “better”.<sup><a href=\"#user-content-fn-2\">2</a></sup></p>\n<h3>Enter Sandman Podman</h3>\n<p>I had been watching my coworkers explore Podman a bit at work, and looking into\nit, it had the perfect thing for my needs, the ability to “systemd-ize” a\ncontainer workload in a way that made sense for both the container, and for\nsystemd. I also took this opportunity to set up <a href=\"https://litestream.io\">litestream</a>\nfor backups as well.</p>\n<p>Now that everything’s mostly stable, I can finally document all my configuration\nfor anyone else to use.</p>\n<p>Under the hood it’s using <a href=\"https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html\">Quadlets</a>,\nso all of the unit files we’ll create in <code>/etc/containers/systemd</code>.</p>\n<p>First we create a pod to contain everything and publish the port:</p>\n<div><figure><figcaption><span>pds.pod</span></figcaption><pre><code><div><div><span>[Pod]</span></div></div><div><div><span>Volume</span><span>=</span><span>pds.volume:/pds</span></div></div><div><div><span>PublishPort</span><span>=</span><span>127.0.0.1:3000:3000</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>Then we slot in a volume:</p>\n<div><figure><figcaption><span>pds.volume</span></figcaption><pre><code><div><div><span>[Unit]</span></div></div><div><div><span>Description</span><span>=</span><span>Bluesky PDS Volume</span></div></div><div><div>\n</div></div><div><div><span>[Volume]</span></div></div><div><div><span>Label</span><span>=</span><span>app=pds</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>Now that we’ve got everything organized, we can bring in the PDS container itself:</p>\n<div><figure><figcaption><span>pds.container</span></figcaption><pre><code><div><div><span>[Unit]</span></div></div><div><div><span>Description</span><span>=</span><span>Bluesky Personal Data Server service</span></div></div><div><div><span>Before</span><span>=</span><span>caddy.service</span></div></div><div><div>\n</div></div><div><div><span>[Container]</span></div></div><div><div><span>Label</span><span>=</span><span>app=pds</span></div></div><div><div><span>Image</span><span>=</span><span>ghcr.io/bluesky-social/pds:0.4</span></div></div><div><div><span>AutoUpdate</span><span>=</span><span>registry</span></div></div><div><div><span>Pod</span><span>=</span><span>pds.pod</span></div></div><div><div><span>EnvironmentFile</span><span>=</span><span>/opt/pds/pds.env</span></div></div><div><div>\n</div></div><div><div><span>[Install]</span></div></div><div><div><span>WantedBy</span><span>=</span><span>multi-user.target default.target</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>I make this container boot before my Caddy service which brings up all the\nassociated resources as part of the quadlet.</p>\n<p>We then make the appropriate changes to the Caddyfile:</p>\n<div><figure><figcaption><span>/etc/caddy/Caddyfile</span></figcaption><pre><code><div><div><span>[...]</span></div></div><div><div><span>*.at.jkp.sh, at.jkp.sh {</span></div></div><div><div><span><span>        </span></span><span>tls {</span></div></div><div><div><span><span>                </span></span><span>on_demand</span></div></div><div><div><span><span>        </span></span><span>}</span></div></div><div><div><span><span>        </span></span><span>reverse_proxy http://localhost:3000</span></div></div><div><div><span>}</span></div></div><div><div><span>[...]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>Now I get autoupdates for my PDS without needing to use the watchtower container\nalong with logs that feed right into systemd. Additionally, my PDS storage\nitself ends up abstracted away for me in a podman volume, rather than being\nscattered in a random non-standard place on my VPS. In short, it does basically\nexactly what I want it to do, just the way I want it, with some learnings along\nthe way.</p>\n<section><h2>Footnotes</h2>\n<ol>\n<li>\n<p>I’ve actually moved this server back over to an Ubuntu 24.04 deployment,\ndue to RHEL-likes running a bit slow for me when it comes to package updates —\njust kidding, it’s back to using CentOS Stream now <a href=\"#user-content-fnref-1\">↩︎</a></p>\n</li>\n<li>\n<p>the oneshot method is a totally valid way of doing it to make it “easy”,\nbut it had too many quirks / compromises for my liking <a href=\"#user-content-fnref-2\">↩︎</a></p>\n</li>\n</ol>\n</section>",
            "url": "https://jack.is/posts/self-hosting-your-pds/",
            "title": "Self hosting your PDS with Podman",
            "summary": "A writeup of gotchas and other fun stuff along the way of deploying a PDS",
            "date_modified": "2025-11-24T06:18:20.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/fantastic-favicons/",
            "content_html": "<p>I’ve finally made my own favicon (inspired by the un-imitable <a href=\"https://rknight.me/\">Robb</a>), instead of just using the generic Astro one\nthat everyone gets when you make a website.</p>\n<p>I wanted to go the extra mile though and make sure it matched the theme of the\nsite. After a bit of digging I discovered that SVG supports media queries, so\nall the fun light/dark mode detection we can do with CSS on the web, also works\nin your favicon (at least assuming you’re using a browser that supports SVG favicons. <sup><a href=\"#user-content-fn-1\">1</a></sup>)</p>\n<p>After vectorizing my work, I added some classes to the paths, put it all in a\nsingle SVG like so:</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span><span>    </span></span><span>[...]</span></div></div><div><div><span><span>    </span></span><span>&lt;</span><span>path</span><span> </span><span>d</span><span>=</span><span>\"m213 87h138v140h-89v86h-49zm49 90h39v-40h-39z\"</span><span> </span><span>fill</span><span>=</span><span>\"#eaedf3\"</span><span> </span><span>fill-rule</span><span>=</span><span>\"evenodd\"</span></div></div><div><div><span>            </span><span>class</span><span>=</span><span>\"dark\"</span><span>&gt;&lt;/</span><span>path</span><span>&gt;</span></div></div><div><div><span><span>    </span></span><span>&lt;/</span><span>svg</span><span>&gt;</span></div></div><div><div><span><span>    </span></span><span>&lt;</span><span>style</span><span>&gt;</span></div></div><div><div><span><span>        </span></span><span>@media (prefers-color-scheme: light) { .dark { display:none; } }</span></div></div><div><div><span><span>        </span></span><span>@media (prefers-color-scheme: dark) { .light { display:none; } }</span></div></div><div><div><span><span>    </span></span><span>&lt;/</span><span>style</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>All I have to do is treat it like any other SVG favicon and it just works.</p>\n<p>Sadly, it doesn’t work in Safari, so Safari will always have a light mode\nfavicon. Not the end of the world, just a bit of a bummer.</p>\n<section><h2>Footnotes</h2>\n<ol>\n<li>\n<p><a href=\"https://caniuse.com/link-icon-svg\">Can I use SVG favicon?</a> <a href=\"#user-content-fnref-1\">↩︎</a></p>\n</li>\n</ol>\n</section>",
            "url": "https://jack.is/posts/fantastic-favicons/",
            "title": "Fantastic Favicons and Where To Find Them",
            "summary": "Did you know that CSS in SVGs can use media queries?",
            "date_modified": "2024-12-30T07:31:23.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/verifying-verification/",
            "content_html": "<p>Imagine it’s the late 90s, and you want to see what the New York Times has to\nsay about your favorite political topic this week. Now you’ve heard they’ve\nfinally gotten on the InterNet? The World Wide Web? You’re not quite sure what\nto call it, but you have a Sunday edition in your living room, and it has a web\naddress in the masthead.</p>\n<p>You open up Netscape Navigator, type in <code>http://www.nytimes.com</code> and your DSL\nconnection chugs for a second as more kilobits than a computer from the 80s\nwould know what to do with fly into your shiny new Dell.</p>\n<p>After all of that though, you know you’re at the New York Times’ website, since\nyou went there right from their physical paper. There wasn’t a middleman, or\nanyone telling you that they were in fact the Gray Lady, you just did a little\nbit of leg work.</p>\n<h2>Time Warp</h2>\n<p>Now it’s 2010, and you want to see what the Times has to say about your favorite\npolitical topic this week. You could head to their website, grab the paper\nfrom the guy on the corner, or for your first time you could open up this new\nwebsite called Twitter, and there’d just be a handful of words written by the\nTimes, summarizing a story, and you wouldn’t have to go anywhere. How do you\nknow if it’s actually the Times? Well Twitter thought of that for you, and they\nadded a little blue checkmark next to their account for you. <sup><a href=\"#user-content-fn-1\">1</a></sup></p>\n<p>Now you can trust that @nytimes is the Times on Twitter, right? Right? Well for\na while you used to be able to, but eventually it all fell apart. And in my\nopinion, that’s a large part of why Bluesky is popping off (or “has the juice”\nas the kids say) as Twitter / X slowly begins the doom spiral that any site\nsees in the future as they become no longer relevant.</p>\n<h2>The “problem”</h2>\n<p>So <a href=\"https://bsky.social/about/blog/2-7-2022-overview\">Bluesky</a> / <a href=\"https://atproto.com\">ATProto</a>\nis created to decentralize Twitter (originally, then is spun off entirely pre\nMusk acquisition), and a primary tenet of both Bluesky PBLLC (the corporation)\nand\nATProto (the protocol itself) is to ensure that long term, the ecosystem is\nprotected from any actor in the space from turning hostile. There’s no “special\nsauce” that Bluesky PBLLC has (other than chat/DMs but we’ll set that aside for\nthe time being) that is exclusive to them.</p>\n<p>Every part of the ecosystem then needs to be designed to be hostile-proof,\nfrom where user data lives, to how users consume and create data. This goes as\nfar as how user identities are stored and referenced. There’s been a lot of work\nput in to ensure that in the long run, you are totally in control of your\nidentity, not Bluesky PBLLC.</p>\n<p>So the “problem” here is that Bluesky doesn’t have a concept of verification,\nat least not the way Twitter had. There is no “central arbiter” of trust,\nbecause how can you have a central arbiter of trust when the entire ecosystem\nis designed to avoid that single point of failure?</p>\n<p>Instead all the “verification” is done with domain names, and there’s been\nmore than a few folks upset / weirded out / annoyed that there isn’t as much\nverification as there was on Twitter.</p>\n<p>The more I think about it though, the less I think it is a problem, and more\nus as a social media using society not remembering the olden days.</p>\n<h2>Why this matters</h2>\n<p>We’ve spent the last 10-15 years forgetting that the web is a protocol, not a\nplatform. Mike Masnick touched on this in <a href=\"https://knightcolumbia.org/content/protocols-not-platforms-a-technological-approach-to-free-speech\">Protocols, not Platforms</a>\n(and in fact is now on the board of Bluesky PBLLC). In the example of the 90s,\nthere wasn’t a platform saying whether or not the NYTimes on the web was\n“verified”, there was just a little bit of legwork to check and crosscheck that\nindeed this website was theirs. There was no middleman, just a protocol.</p>\n<p>However, in the next time warp, we’re instead relying on these centralized\nparties to decide who is and isn’t verified, and shifts us to treating the web\nas a platform, not a protocol. People don’t browse the web, they use Facebook,\nthey use Twitter.</p>\n<p>The concept of domain handles, verification, the whole nine on how Bluesky\nhandles this is the first step on getting us as an internet-using society <em>back</em>\nto protocols.</p>\n<p>A domain is the most ownership of anything you can have on the web. If a new\nATProto social site opens up, you know that <code>jack.is</code> there is also me, just\nlike you know it’s my Bluesky account, just like you know it’s the site you’re\nreading right now. I own my identity, not Instagram, or Twitter, or Bluesky, and\nI think that means something more than a little check in a circle that gives me\nthe good feelings.</p>\n<section><h2>Footnotes</h2>\n<ol>\n<li>\n<p>“So you see, that’s where the trouble began. That check; that damned check” <a href=\"#user-content-fnref-1\">↩︎</a></p>\n</li>\n</ol>\n</section>",
            "url": "https://jack.is/posts/verifying-verification/",
            "title": "Verifying verification",
            "summary": "alternatively: how I learned to love did:plc",
            "date_modified": "2024-12-03T15:01:55.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/bluesky-comments/",
            "content_html": "<p>After being inspired by <a href=\"https://emilyliu.me/blog/comments\">Emily Liu’s</a> post on\nhow to do Bluesky comments, I thought I could add it here too, but with one\nminor plus to it.</p>\n<p>Emily’s method was great, but required hardcoding the atproto URI into the post\nitself. That’s a little hard to do with my deployment and post method, where the\nRSS feed is generated, and then that triggers an Echofeed\n(<a href=\"https://echofeed.app\">Not Sponsored</a>) that posts to Bluesky.</p>\n<p>Because of this, I wanted something better. Ideally, I could have it just find\nthe right post without me having to worry about it. Thanks to the power of the\nBluesky API, I’m able to do that!</p>\n<p>The special sauce is right here in <code>getPostAndThreadData</code>. It runs a search for\nthe Echofeed emoji I use for all my posts (📝), looks for my author DID, as well\nas the page URL that would have been posted. It then limits it to 1, and grabs\nthat URI. Once we’ve got a URI, we then pass it to Emily’s code like normal\nalong with some styling and design tweaks.</p>\n<p>Is it perfect? Not yet, but now it’s just fine tuning some CSS styling (and also\ngetting React to be chunked a little better).</p>\n<div><figure><figcaption><span>src/components/react/BlueskyComments.tsx</span></figcaption><pre><code><div><div><span>const</span><span> </span><span>getPostAndThreadData</span><span> </span><span>=</span><span> </span><span>async</span><span> (</span></div></div><div><div><span>  </span><span>slug</span><span>:</span><span> </span><span>string</span><span>,</span></div></div><div><div><span>  </span><span>setThread</span><span>,</span></div></div><div><div><span>  </span><span>setUri</span><span>,</span></div></div><div><div><span>  </span><span>setError</span><span>,</span></div></div><div><div><span>) </span><span>=&gt;</span><span> {</span></div></div><div><div><span>  </span><span>const</span><span> </span><span>agent</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Agent</span><span>(</span><span>\"https://public.api.bsky.app\"</span><span>);</span></div></div><div><div><span>  </span><span>const</span><span> </span><span>assembledUrl</span><span> </span><span>=</span><span> </span><span>\"https://jack.is/posts/\"</span><span> </span><span>+</span><span> slug.</span><span>toString</span><span>();</span></div></div><div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><span>    </span><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> agent.app.bsky.feed.</span><span>searchPosts</span><span>({</span></div></div><div><div><span><span>      </span></span><span>q: </span><span>\"📝\"</span><span>,</span></div></div><div><div><span><span>      </span></span><span>author: </span><span>\"did:plc:cwdkf4xxjpznceembuuspt3d\"</span><span>,</span></div></div><div><div><span><span>      </span></span><span>sort: </span><span>\"latest\"</span><span>,</span></div></div><div><div><span><span>      </span></span><span>limit: </span><span>1</span><span>,</span></div></div><div><div><span><span>      </span></span><span>url: assembledUrl,</span></div></div><div><div><span><span>    </span></span><span>});</span></div></div><div><div><span>    </span><span>const</span><span> </span><span>uri</span><span> </span><span>=</span><span> response.data.posts[</span><span>0</span><span>].uri;</span></div></div><div><div><span>    </span><span>setUri</span><span>(uri);</span></div></div><div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><span>      </span><span>const</span><span> </span><span>thread</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>getPostThread</span><span>(uri);</span></div></div><div><div><span>      </span><span>setThread</span><span>(thread);</span></div></div><div><div><span><span>    </span></span><span>} </span><span>catch</span><span> (err) {</span></div></div><div><div><span>      </span><span>setError</span><span>(</span><span>\"Error loading comments\"</span><span>);</span></div></div><div><div><span><span>    </span></span><span>}</span></div></div><div><div><span><span>  </span></span><span>} </span><span>catch</span><span> (err) {</span></div></div><div><div><span>    </span><span>setError</span><span>(err);</span></div></div><div><div><span><span>  </span></span><span>}</span></div></div><div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>In the words of Brennan Lee Mulligan: “GET IN THE COMMENTS!”</p>",
            "url": "https://jack.is/posts/bluesky-comments/",
            "title": "Blog, now with Bluesky comments",
            "summary": "A show and tell of my new comments section powered by Bluesky",
            "date_modified": "2024-11-26T01:41:27.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/analytics-showdown/",
            "content_html": "<p>Doing an analytics showdown on the site here between a few services to see which\none shall win for 2025.</p>\n<h2>Tinylytics</h2>\n<p>My current provider of some of the juicy info (and provider of the fancy kudos)\nbuttons. It gets the job done, but I’d love to see more info.</p>\n<h2>Goatcounter</h2>\n<p>Offers more information, has a sweet craigslist-era aesthetic, and could be self\nhosted if I want.</p>\n<h2>Umami</h2>\n<p>My current front runner in all honesty. Has everything I want, and then some,\nand is being selfhosted by me right now. Does what I need it to do and I think\nwill win.</p>\n<p>If I switch however, I’m going to have to reimplement my upvote buttons somehow\nbut that’s a problem for future me.</p>\n<h2>Update</h2>\n<p>Umami self-hosted won.</p>",
            "url": "https://jack.is/posts/analytics-showdown/",
            "title": "Analytics Showdown",
            "summary": "Some more analytics in play here",
            "date_modified": "2024-11-21T22:21:02.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/blog-updates-part-2/",
            "content_html": "<p>All the dust <em>should</em> have finally settled from the tweaks I’ve made to the\nsite. Now everything is one page which allows for fancy view transitions from my\nlinks right to the blog. I did break RSS again (sorry Joshua) but no way to do\nthat migration nicely.</p>\n<p>Now I can have my blog and my\n<a href=\"https://docs.bsky.app/docs/advanced-guides/federation-architecture#personal-data-server-pds\">PDS for Bluesky</a>\nall in one place. With that said, I still haven’t moved my Bluesky account to my\nPDS just yet, but it’s ready to go for when I’m ready to push the button.</p>",
            "url": "https://jack.is/posts/blog-updates-part-2/",
            "title": "Blog updates part 2",
            "summary": "Pardon the dust, again",
            "date_modified": "2024-11-19T01:42:09.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/on-bluesky/",
            "content_html": "<p>So, Bluesky is now a thing. I mean it was a thing before today, but the past\nweek or so seems to have been the “screw it i’m outta here” moment for a lot of\nfolks on X, the everything app Twitter<sup><a href=\"#user-content-fn-1\">1</a></sup>. I made my account long before\nthis Eternal September, but I think I might spend more time on there than I had\nbeen previously. Here’s some random-ish thoughts on how I feel about Bluesky,\nATProto, and how I feel about Mastodon / ActivityPub as a consequence.</p>\n<h2>The Vibes</h2>\n<p>Twitter, and I mean Twitter before it turned to hell in a handbasket, had vibes\nfrom various circles. You had Tech Twitter, you had Weird Twitter<sup><a href=\"#user-content-fn-2\">2</a></sup>, etc. Even\nwith all of that though, the vibes and memes were still often in sync because\neveryone was using the same app, and had at least some of the same trending\ntopics they were aware of. Milkshake Duck, bean dad. There was always a main\ncharacter, and often you didn’t really want to be the main character.</p>\n<p>For better or for worse, Mastodon doesn’t always have those vibes in my opinion.\nIt’s often a little bit stuffy. I’ve found Tech Twitter there in many ways, but\nWeird Twitter never seemed to migrate there, even when it was first popping off\nbefore Bluesky was a thing.</p>\n<p>Bluesky though has all your circles, and is picking up at a much faster than\nMastodon ever did. You’ve got your\n<a href=\"https://bsky.app/profile/dril.bsky.social\">dril</a>, you’ve got your\n<a href=\"https://bsky.app/profile/darthbluesky.bsky.social\">darth</a>, you’ve got your\n<a href=\"https://bsky.app/profile/weratedogs.com\">WeRateDogs</a>. Having one of those on\nMastodon was a challenge, and I don’t think there was ever a time even if all\nthree of those accounts had an Mastodon / AP presence at all, were they ever\nposting at the same time.</p>\n<p>So the vibes are there for Bluesky like Old Twitter the way Mastodon just isn’t.\nBut what about the tech?</p>\n<h2>The Tech</h2>\n<p>Now I was an avid ActivityPub defender (and still am to some extent) when\nATProto originally launched, and was feeling like Bluesky was too “corporate”,\nand kind of didn’t look into it much, and only begrudgingly joined.</p>\n<p>Only relatively recently did I do a deeper dive into the pros and cons of\nATProto, and I’ve sort of turned around on how I feel about the two.</p>\n<p>I tried to run my own Mastodon server one time, and to be honest it Kinda\nSucked. Mastodon simply doesn’t scale well down to one or two users. It’s fine\nif you’re on a larger server as you can share the load (shoutout\n<a href=\"https://adam.lol\">Adam</a> for <a href=\"https://home.omg.lol/referred-by/jack\">omg.lol</a>\nand the associated Mastodon server social.lol), but the effort I had to go to to\neven try to get Mastodon working on a server that already had services on it and\nit was not fun.</p>\n<p>Mastodon also has some odd protocol design decisions that make life harder for\nsmall web, which seems oddly counter-intuitive. The most obvious one (and one\nthat I’ve talked about in the past)<sup><a href=\"#user-content-fn-3\">3</a></sup> is the fact that any rich link previews\nfor any posted link (the OpenGraph info) is fetched by <em>every</em> single\nActivityPub instance that sees that post. In other words, if you’re followed by\n1,000 users on 1,000 separate Mastodon / ActivityPub instances, and you post a\nlink to your neat little blog, in the span of a few seconds, you’re going to get\n1,000 requests to your website. Is this necessary? Not really, but it’s the way\nthe protocol has been designed, and continues to stay that way. There’s even a\ncute name for it, a Mastodon stampede<sup><a href=\"#user-content-fn-4\">4</a></sup>. This just isn’t sustainable.</p>\n<p>ActivityPub is also aggressively chatty when it comes to the actual\ncommunication between servers. Migrating between servers is basically just like\nshoving a HTTP 302 at your followers and hoping for the best. You don’t get to\nmove your posts, just your followers and following. Your username doesn’t stay\nthe same either.</p>\n<p>ATProto on the other hand is built on layers of abstraction, so your handle is\njust a reference to a permanent identifier. Change your handle, no problem. Want\nto move your account entirely from one PDS (Personal Data Server) to another? No\nproblem. The protocol has support for it from the beginning. Rather than sending\na bunch of pushes when you post something, your PDS just adds a new post to your\nPDS, and the network aggregates and displays to clients. It just feels cleaner.\nSpinning up a PDS compared to a Mastodon server is like night and day. I was up\nand running with my self-hosted test user in about 10 minutes and that was with\nhaving to tweak the install script slightly to meet my preferences.</p>\n<h2>The Summation</h2>\n<p>So which one should win? It’s not that one can win and the other shouldn’t, they\neach have different design goals. However, I’ve come around on which one is (at\nleast currently), more scalable, and I think it’s ATProto. From a social\nperspective, Bluesky seems to be winning the normal user demographic, which\nMastodon always had trouble getting users to adopt.</p>\n<p>As for my self-hosted PDS, I’m still deciding whether or not to move my current\nBluesky hosted account to it. There’s currently no import path back to Bluesky\n(not because of protocol reasons, but because of giant account reasons), and\nthat makes me feel like this is a permanent commitment if I do. We’ll see I\nguess.</p>\n<section><h2>Footnotes</h2>\n<ol>\n<li>\n<p>considering that you still can’t use X as a bank like was promised, lol <a href=\"#user-content-fnref-1\">↩︎</a></p>\n</li>\n<li>\n<p>see dril, et al. <a href=\"#user-content-fnref-2\">↩︎</a></p>\n</li>\n<li>\n<p>have I overused parentheticals yet? since I used another one in the\nfollowing line, clearly not <a href=\"#user-content-fnref-3\">↩︎</a></p>\n</li>\n<li>\n<p>get it, because mastodons are like elephants and elephants stampede? I’m\ndone with the footnote overuse I promise <a href=\"#user-content-fnref-4\">↩︎</a></p>\n</li>\n</ol>\n</section>",
            "url": "https://jack.is/posts/on-bluesky/",
            "title": "On Bluesky",
            "summary": "Some random thoughts on Bluesky (and Mastodon)",
            "date_modified": "2024-11-16T05:29:26.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/blog-update/",
            "content_html": "<p>We (I? I’m not a fan of talking about myself in the third person, but that’s\nneither here nor there) now have a few cool new things.</p>\n<h2>Author Attribution on Fediverse</h2>\n<p>Thanks to <a href=\"https://rknight.me/blog/setting-up-mastodon-author-tags/\">Robb</a>\nposting it, I now have my fancy fediverse author tags configured, so it shows\nattribution properly when my post is posted.</p>\n<p>It’s simple, but it’s neat.</p>\n<h2>Tri-state dark mode selector</h2>\n<p>And I don’t mean the tri-state area. <sup><a href=\"#user-content-fn-1\">1</a></sup></p>\n<p>The Astro them I use, <a href=\"https://astro-paper.pages.dev\">Astro Paper</a> supports both\nlight and dark mode. However, once you click the selector, it switches from\nusing system selected (<code>prefers-color-scheme</code>) to the user selected option and\ndoesn’t allow a way of switching back. I didn’t love this, so I added a third\noption.</p>\n<p>Now the theme picker rotates between system, light, and dark. Pick whichever\ntheme suits your fancy, no matter what your operating system tells your browser.\nOr pick system and let your operating system be the boss. You decide.</p>\n<section><h2>Footnotes</h2>\n<ol>\n<li>\n<p>boo this man <a href=\"#user-content-fnref-1\">↩︎</a></p>\n</li>\n</ol>\n</section>",
            "url": "https://jack.is/posts/blog-update/",
            "title": "Blog Updates",
            "summary": "A quick update",
            "date_modified": "2024-10-09T23:33:46.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/sorry-in-advance/",
            "content_html": "<p>So turns out that I had some mismatched links (trailing slash vs no trailing\nslash) and keeping track of all the places where they were mixed up was\nimpossible.</p>\n<p>In other words, sorry for your RSS reader getting confused again if it sees all\nmy posts as new entries again since the guid changed. We should be good now\nthough, with less breaking of your readers.</p>",
            "url": "https://jack.is/posts/sorry-in-advance/",
            "title": "Sorry to RSS readers",
            "summary": "an apology for the annoying RSS tomfoolery that's been going on",
            "date_modified": "2024-04-12T13:27:50.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/mastodon-stampede/",
            "content_html": "<p>If you’re running a site that dynamically generates content, you should probably\ndo some sort of caching to prevent your site from exploding whenever someone\nposts it on Mastodon and friends.</p>\n<p>For example, if you use Caddy, it might look something like this:</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span><span>        </span></span><span>@fedi {</span></div></div><div><div><span><span>                </span></span><span>header_regexp User-Agent (http\\.rb/\\S+\\s\\(Mastodon|Friendica|Pleroma\\s|Akkoma\\s|Misskey/|gotosocial)</span></div></div><div><div><span><span>        </span></span><span>}</span></div></div><div><div>\n</div></div><div><div><span><span>        </span></span><span># do something with the detected user agent that's cache-y</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>Since this blog is static, I just tank the Mastodon hits as necessary, but\nsomething to keep in mind if you’re hosting your own thing.</p>",
            "url": "https://jack.is/posts/mastodon-stampede/",
            "title": "Mastodon Stampedes",
            "summary": "\"fun\" fact: each Mastodon server gets their own copy of your OG card",
            "date_modified": "2024-04-09T18:19:02.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/new-domain/",
            "content_html": "<p>After literal years of jonesing for it, I’ve finally gotten <code>jack.is</code> as a\ndomain. Sorry for whoever previously had it, but you did let it go after all.</p>",
            "url": "https://jack.is/posts/new-domain/",
            "title": "A new brand",
            "summary": "A new domain for the blog and brand",
            "date_modified": "2024-04-05T00:00:00.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/blog-setup-2024/",
            "content_html": "<p>A semi-deep dive into the newly deployed plttn.me.</p>\n\n<p>It’s time for the regularly scheduled blog updates.</p>\n<h2>Yesterday (metaphorically speaking)</h2>\n<p>The prior setup was using Hugo and deployed to either Netlify, Cloudflare Pages\nor my own box. This was great, but after thinking through the Small Web ™️\nmovement some more I thought it made more sense to move to something in-house.</p>\n<h2>Today</h2>\n<p>The site now uses <a href=\"https://astro.build\">Astro</a> and deploys right to my server.\nFor deploying, it’s a two-step process. All commits on <code>main</code> on GitHub are\nchecked. After everything checks out, I can trigger a release in GitHub to\ndeploy to my server.</p>\n<p>Here’s part of one workflow:</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span>name</span><span>: </span><span>Check pushes</span></div></div><div><div><span>on</span><span>:</span></div></div><div><div><span>  </span><span>push</span><span>:</span></div></div><div><div><span>    </span><span>branches</span><span>:</span></div></div><div><div><span><span>      </span></span><span>- </span><span>main</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>Here’s the other:</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span>name</span><span>: </span><span>Deploy</span></div></div><div><div><span>on</span><span>:</span></div></div><div><div><span>  </span><span>release</span><span>:</span></div></div><div><div><span>    </span><span>types</span><span>: [</span><span>published</span><span>]</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<h3>DNS Sidenote</h3>\n<p>I’ve also switched to using <a href=\"https://desec.io\">Desec</a> for my DNS hosting, it\nfits my needs nicely, is free, and enforces DNSSEC.</p>\n<p>Hopefully the first of multiple blog posts to come.</p>",
            "url": "https://jack.is/posts/blog-setup-2024/",
            "title": "Blog Setup 2024",
            "summary": "A brief summary of the newly deployed plttn.me",
            "date_modified": "2024-01-28T15:33:51.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/pardon-the-dust/",
            "content_html": "<p>Pardon the dust. Updates in progress.</p>\n\n",
            "url": "https://jack.is/posts/pardon-the-dust/",
            "title": "Pardon the Dust",
            "summary": "More to come soon",
            "date_modified": "2024-01-27T21:15:07.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/yet-another-octopress20-guide/",
            "content_html": "<p>So there’s only about 30 50 thousand pages on this general topic, but I\nfound that about half of them were out of date, so I figured I should try and\ncollect all the info into one big post. I’ve referenced where I’ve found some of\nthe stuff that’s harder to find from Googling using the wonders of Octopress\nfootnotes.</p>\n<p>Unfortunately, as I was finishing up this post, the post announcing Octopress\n3.0 comes out. While I had known it was coming, I wasn’t expecting it to be so\nsoon. Obviously this post will be less useful, but it should still be\ninformative. I’ve included what I think will need to be modified to function on\n3.0 at the bottom.</p>\n<h2>Table of contents</h2>\n<h2>My Site Flow</h2>\n<ul>\n<li>Local</li>\n<li>Private Github Repo</li>\n<li>Built and Deployed using TravisCI to a DigitalOcean droplet</li>\n</ul>\n\n<p>I’ve got a drafts workflow using a couple tasks I found online<sup><a href=\"#user-content-fn-1\">1</a></sup> and then\npromptly modified:</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span>## -- Misc Configs -- ##</span></div></div><div><div>\n</div></div><div><div><span>drafts_dir</span><span>      </span><span>=</span><span> </span><span>\"_drafts\"</span><span> </span><span># directory for draft files</span></div></div><div><div>\n</div></div><div><div><span># usage rake new_draft[my-new-draft] or rake new_draft['my new draft'] #</span></div></div><div><div><span>desc </span><span>\"Begin a new draft in </span><span>#{source_dir}</span><span>/</span><span>#{drafts_dir}</span><span>\"</span></div></div><div><div><span>task </span><span>:new_draft</span><span>, </span><span>:title</span><span> </span><span>do</span><span> |t, args|</span></div></div><div><div><span>if</span><span> args.</span><span>title</span></div></div><div><div><span>title</span><span> </span><span>=</span><span> args.</span><span>title</span></div></div><div><div><span>else</span></div></div><div><div><span>title</span><span> </span><span>=</span><span> </span><span>get_stdin</span><span>(</span><span>\"Enter a title for your post: \"</span><span>)</span></div></div><div><div><span>end</span></div></div><div><div><span>raise</span><span> </span><span>\"### You haven't set anything up yet. First run `rake install` to set up an Octopress theme.\"</span><span> </span><span>unless</span><span> </span><span>File</span><span>.</span><span>directory?</span><span>(source_dir)</span></div></div><div><div><span>mkdir_p </span><span>\"</span><span>#{source_dir}</span><span>/</span><span>#{drafts_dir}</span><span>\"</span></div></div><div><div><span>filename</span><span> </span><span>=</span><span> </span><span>\"</span><span>#{source_dir}</span><span>/</span><span>#{drafts_dir}</span><span>/</span><span>#{title.</span><span>to_url</span><span>}</span><span>.</span><span>#{new_post_ext}</span><span>\"</span></div></div><div><div><span>if</span><span> </span><span>File</span><span>.</span><span>exist?</span><span>(filename)</span></div></div><div><div><span>abort</span><span>(</span><span>\"rake aborted!\"</span><span>) </span><span>if</span><span> </span><span>ask</span><span>(</span><span>\"</span><span>#{filename}</span><span> already exists. Do you want to overwrite?\"</span><span>, [</span><span>'y'</span><span>, </span><span>'n'</span><span>]) </span><span>==</span><span> </span><span>'n'</span></div></div><div><div><span>end</span></div></div><div><div><span>puts</span><span> </span><span>\"Creating new draft: </span><span>#{filename}</span><span>\"</span></div></div><div><div><span>open</span><span>(filename, </span><span>'w'</span><span>) </span><span>do</span><span> |post|</span></div></div><div><div><span>post.</span><span>puts</span><span> </span><span>\"---\"</span></div></div><div><div><span>post.</span><span>puts</span><span> </span><span>\"layout: post\"</span></div></div><div><div><span>post.</span><span>puts</span><span> </span><span>\"title: </span><span>\\\"</span><span>#{title.</span><span>gsub</span><span>(</span><span>/&amp;/</span><span>,</span><span>'&amp;amp;'</span><span>)}</span><span>\\\"</span><span>\"</span></div></div><div><div><span>post.</span><span>puts</span><span> </span><span>\"comments: true\"</span></div></div><div><div><span>post.</span><span>puts</span><span> </span><span>\"#placeholder\"</span></div></div><div><div><span>post.</span><span>puts</span><span> </span><span>\"categories: \"</span></div></div><div><div><span>post.</span><span>puts</span><span> </span><span>\"---\"</span></div></div><div><div><span>end</span></div></div><div><div><span>end</span></div></div><div><div>\n</div></div><div><div><span># usage rake publish_draft</span></div></div><div><div><span>desc </span><span>\"Select a draft to publish from </span><span>#{source_dir}</span><span>/</span><span>#{drafts_dir}</span><span> on the current date.\"</span></div></div><div><div><span>task </span><span>:publish_draft</span><span> </span><span>do</span></div></div><div><div><span>drafts_path</span><span> </span><span>=</span><span> </span><span>\"</span><span>#{source_dir}</span><span>/</span><span>#{drafts_dir}</span><span>\"</span></div></div><div><div><span>drafts</span><span> </span><span>=</span><span> </span><span>Dir</span><span>.</span><span>glob</span><span>(</span><span>\"</span><span>#{drafts_path}</span><span>/*.</span><span>#{new_post_ext}</span><span>\"</span><span>)</span></div></div><div><div><span>drafts.</span><span>each_with_index</span><span> </span><span>do</span><span> |draft, index|</span></div></div><div><div><span>begin</span></div></div><div><div><span>content</span><span> </span><span>=</span><span> </span><span>File</span><span>.</span><span>read</span><span>(draft)</span></div></div><div><div><span>if</span><span> </span><span>content</span><span> =~ </span><span>/</span><span>\\A</span><span>(---</span><span>\\s</span><span>*</span><span>\\n</span><span>.*?</span><span>\\n</span><span>?)^(---</span><span>\\s</span><span>*$</span><span>\\n</span><span>?)/m</span></div></div><div><div><span>data</span><span> </span><span>=</span><span> </span><span>YAML</span><span>.</span><span>load</span><span>($1)</span></div></div><div><div><span>end</span></div></div><div><div><span>rescue</span><span> =&gt; e</span></div></div><div><div><span>puts</span><span> </span><span>\"Error reading file </span><span>#{draft}</span><span>: </span><span>#{e.</span><span>message</span><span>}</span><span>\"</span></div></div><div><div><span>rescue</span><span> </span><span>SyntaxError</span><span> =&gt; e</span></div></div><div><div><span>puts</span><span> </span><span>\"YAML Exception reading </span><span>#{draft}</span><span>: </span><span>#{e.</span><span>message</span><span>}</span><span>\"</span></div></div><div><div><span>end</span></div></div><div><div><span>puts</span><span> </span><span>\"  [</span><span>#{index}</span><span>]  </span><span>#{data[</span><span>'title'</span><span>]}</span><span>\"</span></div></div><div><div><span>end</span></div></div><div><div><span>puts</span><span> </span><span>\"Publish which draft? \"</span></div></div><div><div><span>answer</span><span> </span><span>=</span><span> </span><span>STDIN</span><span>.</span><span>gets</span><span>.</span><span>chomp</span></div></div><div><div><span>if</span><span> </span><span>/</span><span>\\d</span><span>+/</span><span>.</span><span>match</span><span>(answer) </span><span>and</span><span> </span><span>not</span><span> drafts[answer.</span><span>to_i</span><span>].</span><span>nil?</span></div></div><div><div><span>mkdir_p </span><span>\"</span><span>#{source_dir}</span><span>/</span><span>#{posts_dir}</span><span>\"</span></div></div><div><div><span>source</span><span> </span><span>=</span><span> drafts[answer.</span><span>to_i</span><span>]</span></div></div><div><div><span>filename</span><span> </span><span>=</span><span> source.</span><span>gsub</span><span>(</span><span>/</span><span>#{drafts_path}</span><span>\\/</span><span>/</span><span>, </span><span>''</span><span>)</span></div></div><div><div><span>dest</span><span> </span><span>=</span><span> </span><span>\"</span><span>#{source_dir}</span><span>/</span><span>#{posts_dir}</span><span>/</span><span>#{</span><span>Time</span><span>.</span><span>now</span><span>.</span><span>strftime</span><span>(</span><span>'%Y-%m-%d'</span><span>)}</span><span>-</span><span>#{filename}</span><span>\"</span></div></div><div><div><span>puts</span><span> </span><span>\"Publishing post to: </span><span>#{dest}</span><span>\"</span></div></div><div><div><span>File</span><span>.</span><span>open</span><span>(source) { |source_file|</span></div></div><div><div><span>  </span><span>contents</span><span> </span><span>=</span><span> source_file.</span><span>read</span></div></div><div><div><span><span>  </span></span><span>contents.</span><span>gsub!</span><span>(</span><span>/^#placeholder$/</span><span>, </span><span>\"date: </span><span>#{</span><span>Time</span><span>.</span><span>now</span><span>.</span><span>strftime</span><span>(</span><span>'%Y-%m-%d %H:%M %z'</span><span>)}</span><span>\"</span><span>)</span></div></div><div><div><span>  </span><span>File</span><span>.</span><span>open</span><span>(dest, </span><span>\"w+\"</span><span>) { |f| f.</span><span>write</span><span>(contents) }</span></div></div><div><div><span>}</span></div></div><div><div><span>FileUtils</span><span>.</span><span>rm</span><span>(source)</span></div></div><div><div><span>else</span></div></div><div><div><span>puts</span><span> </span><span>\"Index not found!\"</span></div></div><div><div><span>end</span></div></div><div><div><span>end</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>For this to work using <code>rake preview</code>, you’ll need to modify your preview task\nslightly, add the flag <code>--drafts</code> to the jekyll call.</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span>system</span><span> </span><span>\"compass compile --css-dir </span><span>#{source_dir}</span><span>/stylesheets\"</span><span> </span><span>unless</span><span> </span><span>File</span><span>.</span><span>exist?</span><span>(</span><span>\"</span><span>#{source_dir}</span><span>/stylesheets/screen.css\"</span><span>)</span></div></div><div><div><span>jekyllPid</span><span> </span><span>=</span><span> </span><span>Process</span><span>.</span><span>spawn</span><span>({</span><span>\"OCTOPRESS_ENV\"</span><span>=&gt;</span><span>\"preview\"</span><span>}, </span><span>\"jekyll build --watch --drafts\"</span><span>)</span></div></div><div><div><span>compassPid</span><span> </span><span>=</span><span> </span><span>Process</span><span>.</span><span>spawn</span><span>(</span><span>\"compass watch\"</span><span>)</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<h2>The Build Process</h2>\n<p>Personally, I don’t want to have to deal with building and uploading my site.\nInstead I use Travis-CI targeted to a specific Github repo branch, and some\nmodified rsync scripts.</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span>language</span><span>: </span><span>ruby</span></div></div><div><div><span>cache</span><span>: </span><span>bundler</span></div></div><div><div>\n</div></div><div><div><span>rvm</span><span>:</span></div></div><div><div><span><span>  </span></span><span>- </span><span>1.9.3</span></div></div><div><div><span>branches</span><span>:</span></div></div><div><div><span>only</span><span>:</span></div></div><div><div><span><span>  </span></span><span>- </span><span>master</span></div></div><div><div>\n</div></div><div><div><span>before_install</span><span>:</span></div></div><div><div><span><span>  </span></span><span>- </span><span>echo -e \"Host *\\n\\tStrictHostKeyChecking no\\n\" &gt;&gt; ~/.ssh/config</span></div></div><div><div><span><span>  </span></span><span>- </span><span>echo -e \"$DEPLOY_PRIVATE_KEY\" &gt; ~/.ssh/id_rsa</span></div></div><div><div><span><span>  </span></span><span>- </span><span>chmod 600 ~/.ssh/id_rsa</span></div></div><div><div><span><span>  </span></span><span>- </span><span>eval `ssh-agent -s`</span></div></div><div><div><span><span>  </span></span><span>- </span><span>ssh-add ~/.ssh/id_rsa</span></div></div><div><div>\n</div></div><div><div><span>script</span><span>:</span></div></div><div><div><span><span>  </span></span><span>- </span><span>bundle exec rake integrate</span><span> </span><span>#move any stashed posts to source</span></div></div><div><div><span><span>  </span></span><span>- </span><span>bundle exec rake generate</span><span> </span><span>#generate static to /public</span></div></div><div><div><span><span>  </span></span><span>- </span><span>bundle exec rake deploy</span><span> </span><span>#rsync /public to server</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<h2>Rsync Setup</h2>\n<p><img alt=\"Travis CI SSH Key location\" loading=\"lazy\" width=\"375\" height=\"226\" src=\"https://jack.is/_astro/travis-ci1.CEUFGnS6_Z2m649q.webp\" srcset=\"/_astro/travis-ci1.CEUFGnS6_Z2m649q.webp 375w\" /></p>\n<p>You’ll need to configure your deployment account’s SSH private key in Travis-CI\nby replacing all new lines with <code>\\n</code>, and then escaping that to <code>\\\\n</code>, so that\nit ends up being one line with <code>\\\\n</code> in between each original line.<sup><a href=\"#user-content-fn-2\">2</a></sup>\n(<strong>NOTE</strong>: Make sure the key was generated without ssh-agent running, and set a\nblank passphrase.) Next, go to your Travis-CI settings, create a new environment\nvariable, name it whatever you want, I used <code>$DEPLOY_PRIVATE_KEY</code>, then paste in\nyour one-line private key into the variable section. Now that it’s a environment\nvariable in all Travis workers your builds will use, the <code>before_install</code> tasks\nwill disable strict host key checking (requires input, which Travis doesn’t\nallow), and then echo the contents of <code>DEPLOY_PRIVATE_KEY</code> to <code>id_rsa</code>, then add\nthat key to the ssh-agent, in my case, allowing the worker to connect using the\nuser <code>travis-ci</code> to my droplet.</p>\n<p>If the user you’re connecting with in your Rakefile’s settings isn’t the owner\nof your specific directories on your hosting server, rsync will probably fail to\nsync properly. In that case, you’ll need to add <code>--omit-dir-times</code> to\n<code>rsync-args</code> in the Rakefile.</p>\n<p>After all of this, you should be good to go. If you’re using the master deploy\nmethod, push your site up to master, and it should build properly on Travis, and\ndeploy to your rsync destination.</p>\n<h2>Octopress 3.0</h2>\n<p>For the most part, as far as I can tell, these modifications will <em>mostly</em> still\nwork for 3.0. The drafts changes will no longer be needed anymore, as the Jekyll\ndrafts workflow is now using a separate drafts folder. The <code>.travis.yml</code> will\nneed to be tweaked slightly, as well as the <code>_config.yml</code> having an exclude\nadded to keep Jekyll running on Travis-CI from trying to build the templates in\n<code>/vendor</code>, but other than that, this post is mostly forwards compatible to\nOctopress 3.0.</p>\n<section><h2>Footnotes</h2>\n<ol>\n<li>\n<p><a href=\"http://neverstopbuilding.com/how-to-enhance-your-octopress-draft-and-heroku-deploy-process\">Never Stop Building</a> <a href=\"#user-content-fnref-1\">↩︎</a></p>\n</li>\n<li>\n<p><a href=\"http://grosser.it/2014/03/01/allowing-travis-to-git-clone-by-adding-a-public-ssh-key/\">Grosser.it</a> <a href=\"#user-content-fnref-2\">↩︎</a></p>\n</li>\n</ol>\n</section>",
            "url": "https://jack.is/posts/yet-another-octopress20-guide/",
            "title": "Yet Another Octopress 2.0 and TravisCI Guide",
            "summary": "A quick (and outdated guide) on how to use TravisCI to deploy a Octopress blog",
            "date_modified": "2015-01-17T19:18:29.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/atcute-comments/",
            "content_html": "<p>Having <a href=\"https://jack.is/posts/bluesky-comments/\">comments on my blog</a> with Bluesky is pretty nice, but what’s not so nice\nis having to bring in the entire <code>@atproto/api</code> package. It seemingly can’t be\ntree shaken down via Vite, so you end up with a massive JS file for comments.</p>\n<p>How massive is it? More than 700kB.</p>\n<p><img alt=\"screenshot of terminal, with the size of the bluesky comments file highlighted showing 700kB\" loading=\"lazy\" width=\"1972\" height=\"1468\" src=\"https://jack.is/_astro/atcute-1.Y1-f3R1r_ZorkXQ.webp\" srcset=\"/_astro/atcute-1.Y1-f3R1r_ZQV7F4.webp 640w, /_astro/atcute-1.Y1-f3R1r_Z1WLOgg.webp 750w, /_astro/atcute-1.Y1-f3R1r_Z1KPQ4z.webp 828w, /_astro/atcute-1.Y1-f3R1r_20H75m.webp 1080w, /_astro/atcute-1.Y1-f3R1r_1mnNPE.webp 1280w, /_astro/atcute-1.Y1-f3R1r_Z1fCfdy.webp 1668w, /_astro/atcute-1.Y1-f3R1r_ZorkXQ.webp 1972w\" /></p>\n<p>It’s kinda a lot, and as it is I had to tweak my Astro config to make Vite not\nwarn about this:</p>\n<div><figure><figcaption><span>astro.config.ts</span></figcaption><pre><code><div><div><span>  </span><span>vite</span><span>: {</span></div></div><div><div><span>    </span><span>optimizeDeps</span><span>: {</span></div></div><div><div><span>      </span><span>exclude</span><span>: [</span><span>\"@resvg/resvg-js\"</span><span>],</span></div></div><div><div><span><span>    </span></span><span>},</span></div></div><div><div><span>    </span><span>build</span><span>: {</span></div></div><div><div><span>      </span><span>chunkSizeWarningLimit</span><span>: </span><span>768</span><span>,</span></div></div><div><div><span>      </span><span>// TODO find a better way to handle this</span></div></div><div><div><span>      </span><span>// where \"this\" means the bsky react import chunk size</span></div></div><div><div><span><span>    </span></span><span>},</span></div></div><div><div><span><span>  </span></span><span>},</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>I wasn’t a huge fan of this for a few reasons. One, that was a lot of unneeded\nJS being delivered that I didn’t need, and I had to modify a warning setting\nwhich seemed less than ideal.</p>\n<p>I had seen <code>@atcute/client</code> <a href=\"https://bsky.app/profile/mary.my.id/post/3l6uo5yecnsu7\">by Mary</a> float across my\nfeed, and poked at it a bit, but only after I figured out how to make unauthenticated\nrequests with it did it become clear how much better it was going to make my\nbundles.</p>\n<p>Only took a little bit of modifying the work I had done using <a href=\"https://emilyliu.me/blog/comments\">Emily Liu’s example</a>\nto instead use atcute:</p>\n<div><figure><figcaption><span>src/components/react/BlueskyComments.tsx</span></figcaption><pre><code><div><div><span>const</span><span> </span><span>rpc</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>XRPC</span><span>({</span></div></div><div><div><span><span>  </span></span><span>handler: </span><span>simpleFetchHandler</span><span>({ service: </span><span>\"https://public.api.bsky.app\"</span><span> }),</span></div></div><div><div><span>});</span></div></div><div><div>\n</div></div><div><div><span>const</span><span> </span><span>agent</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Agent</span><span>(</span><span>\"https://public.api.bsky.app\"</span><span>);</span></div></div><div><div><span>const</span><span> </span><span>assembledUrl</span><span> </span><span>=</span><span> </span><span>\"https://jack.is/posts/\"</span><span> </span><span>+</span><span> slug.</span><span>toString</span><span>();</span></div></div><div><div>\n</div></div><div><div><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> agent.app.bsky.feed.</span><span>searchPosts</span><span>(</span><span>...</span><span>);</span></div></div><div><div><span>const</span><span> </span><span>response</span><span> </span><span>=</span><span> </span><span>await</span><span> rpc.</span><span>get</span><span>(</span><span>\"app.bsky.feed.searchPosts\"</span><span>, {</span></div></div><div><div><span><span>  </span></span><span>params: {</span></div></div><div><div><span><span>    </span></span><span>q: </span><span>\"📝\"</span><span>,</span></div></div><div><div><span><span>    </span></span><span>author: </span><span>\"did:plc:cwdkf4xxjpznceembuuspt3d\"</span><span>,</span></div></div><div><div><span><span>    </span></span><span>sort: </span><span>\"latest\"</span><span>,</span></div></div><div><div><span><span>    </span></span><span>limit: </span><span>1</span><span>,</span></div></div><div><div><span><span>    </span></span><span>url: assembledUrl,</span></div></div><div><div><span><span>  </span></span><span>},</span></div></div><div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>The unexpected benefit here is that <code>@atproto/api</code> didn’t actually have all the\nhelper functions Emily needed for it to work, so there was a direct call to one\nof the xrpc endpoints. Now that everything just uses <code>rpc.get</code>, it’s a lot more\nconsistent and easier to follow.</p>\n<p>I did have to add a few <code>//@ts-expect-error</code> sprinkled around, but they’re all\nguarded by typeguards so I’m not too worried.</p>\n<p>Now instead of 700kB, my <code>BlueskyComments.js</code> is only 10kB. A vast improvement.</p>\n<p>Thanks again to Emily for posting their code to begin with, and Mary for an\nexcellent suite of tooling that’s much more compact than the full ATProto API.</p>",
            "url": "https://jack.is/posts/atcute-comments/",
            "title": "Optimizing Bluesky comments",
            "summary": "using @atcute/client to make comments better",
            "date_modified": "2025-01-12T06:31:38.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/editor-setup-2020/",
            "content_html": "<h2>Goodbye Atom</h2>\n<p>I wanted to like Atom, and I was one of those stubborn ones who had been putting\noff trying VS Code for probably years and continued to use Atom and putting up\nwith the subpar performance, even on a relatively overpowered computer.</p>\n<p>A few weeks back though, I decided to bite the bullet and give VS Code a fair\ntry, and after using it for just a few days, I didn’t see myself going back to\nAtom anytime soon. There’s just so much more performance in Code compared to\nAtom (which confuses me because they’re both Electron), and included in that\nperformance is more features included standard, instead of needing 100\nextensions to get what I want out of it.</p>\n<p>Looking at the writing on the wall, I don’t foresee Atom lasting too much\nlonger, I haven’t seen a new feature added to Atom since right around the time\nAtom Teletype came out, and then that’s been pretty much it since then.</p>\n<p>Sadly this means my theme that I’ve spent the better part of five years tweaking\nand modifying will end up slowly dying, but given that Pubster really isn’t\nprimarily my work when you boil it down, I don’t feel too bad about it.</p>\n\n<h2>Hello Code</h2>\n<p>So what’s my Code look like now?</p>\n<p><img alt=\"Editor Setup\" loading=\"lazy\" width=\"2558\" height=\"1406\" src=\"https://jack.is/_astro/editor2020-1.CEUXMxXM_1FmOFK.webp\" srcset=\"/_astro/editor2020-1.CEUXMxXM_Z1wNdJc.webp 640w, /_astro/editor2020-1.CEUXMxXM_ZRwble.webp 750w, /_astro/editor2020-1.CEUXMxXM_Z2htFX0.webp 828w, /_astro/editor2020-1.CEUXMxXM_Z7qF1m.webp 1080w, /_astro/editor2020-1.CEUXMxXM_Z13n6un.webp 1280w, /_astro/editor2020-1.CEUXMxXM_AlqLq.webp 1668w, /_astro/editor2020-1.CEUXMxXM_ZStis1.webp 2048w, /_astro/editor2020-1.CEUXMxXM_1FmOFK.webp 2558w\" /></p>\n<p>I’ve started using Iosevka as my programming font with it configured to more or\nless match Pragmatica Pro’s font features, and I’m really enjoying it so far.\nI’ve tried a bunch of different fonts, and had tried Iosevka in the past and\nwasn’t sure how I felt about it, but now that it’s got ligature support and\ndecent hinting, I’m a fan. Additionally, for plain text editing (markdown,\nplaintext, commit messages), I switch over to Iosevka Sparkle which is designed\nfor prose use using <code>settings.json</code> overrides for specific languages:</p>\n<div><figure><figcaption></figcaption><pre><code><div><div><span>\"[markdown]\"</span><span>: {</span></div></div><div><div><span>  </span><span>\"editor.defaultFormatter\"</span><span>: </span><span>\"esbenp.prettier-vscode\"</span><span>,</span></div></div><div><div><span>  </span><span>\"editor.fontFamily\"</span><span>: </span><span>\"Iosevka Sparkle, Iosevka\"</span></div></div><div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>As for the color scheme itself, I decided to give\n<a href=\"https://monokai.pro\">Monokai Pro</a> a try, and I’m really loving it. The theme\nitself is nice, and the file icon themes it comes with are also fantastic and\nclear without being obnoxious. While it does cost a few bucks, for how nice it\nlooks I think it’s definitely worth it.</p>",
            "url": "https://jack.is/posts/editor-setup-2020/",
            "title": "Editor Setup 2020",
            "summary": "A quick summary of my current (at the time) VSCode setup.",
            "date_modified": "2020-05-26T19:29:00.000Z",
            "author": {
                "name": "Jack Platten"
            }
        },
        {
            "id": "https://jack.is/posts/comments-api/",
            "content_html": "<p>Have we yak shaved enough yet on comments? I don’t think we have, what’s one\nmore post among friends.</p>\n<h2>The problem</h2>\n<p>As mentioned in <a href=\"https://jack.is/posts/bluesky-comments/\">Bluesky Comments</a>, my posting workflow\ndoesn’t allow me to know the Bluesky post ahead of time. So I thought, well I’ll\njust use the search API to find the post URL based on my echofeed post. That\nworked pretty well, but then I saw this skeet:</p>\n<div> <div> <div> <a href=\"https://bsky.app/profile/bnewbold.net\" target=\"_blank\"> <img src=\"https://cdn.bsky.app/img/avatar/plain/did:plc:44ybard66vv44zksje25o7dz/bafkreiglnysron3h2je7nf6cmvtimuaxi7xe2c7rkxitmks3mzmajnc2ou\" alt=\"bryan newbold (⛱️ sabbatical edition)\" /> </a> <div> <a href=\"https://bsky.app/profile/bnewbold.net\">bryan newbold (⛱️ sabbatical edition)</a> <a href=\"https://bsky.app/profile/bnewbold.net\">@bnewbold.net</a> </div> <a href=\"https://bsky.app/profile/did:plc:44ybard66vv44zksje25o7dz/post/3lg4jigj4dc2v\"> <img alt=\"Bluesky\" /> </a> </div> <p>please don't use the bsky \"search\" endpoints as a generic API or for automation. it is not designed for that, is an expensive waste/abuse of resources, and we will block by IP or user agent.\n\nsearches should come from direct human queries in the moment</p>  <a href=\"https://bsky.app/profile/did:plc:44ybard66vv44zksje25o7dz/post/3lg4jigj4dc2v\"> January 19, 2025 at 7:08 PM UTC </a> </div> </div>\n<p>My first reaction was “Oh it’ll be fine this is just limited use and there was\nanother post that says it’ll be fine for limited use”. It was fine, but after a\nday or so after Bryan’s post, I started getting CORS errors from my search\nfunction, and I figured that was that, and I needed to do this a better way,\nand maybe learn something along the way.</p>\n<h2>The research</h2>\n<p>I had seen Jetstream in my research prior to now, and figured some sort of\ndatabase would probably be the best bet, where it’s just a single lookup, rather\nthan every browser of the site having to do the search (even if it wasn’t blocked).</p>\n<p>My initial thought was using Typescript for a server API, and there’s definitely\nstill benefits to there, being a robust ecosystem of ATProto packages. I however\nwanted to Think Different™ so I decided to write it in Rust, both because I\ncould, and also because I wanted to learn a bit of Rust “systems” programming,\nsince I’ve been spending a lot of my day in Typescript.</p>\n<p>After doing some very detailed Kagi searches <sup><a href=\"#user-content-fn-1\">1</a></sup>, I discovered <a href=\"https://rocket.rs\">Rocket</a>\nand determined it was actually perfect for my needs. Simple declarations of\nhandler functions, and it’s nice and type-safe. What’s not to like.</p>\n<h2>It’s Rust time</h2>\n<p>With just a bit of boilerplate code, I can spin up the API handler needed for\nthe metadata info for my front-end.</p>\n<div><figure><figcaption><span>main.rs</span></figcaption><pre><code><div><div><span>#[get(</span><span>\"/\"</span><span>)]</span></div></div><div><div><span>fn</span><span> </span><span>index</span><span>() </span><span>-&gt;</span><span> </span><span>&amp;</span><span>'</span><span>static</span><span> </span><span>str</span><span> {</span></div></div><div><div><span>    </span><span>\"at-comments database API server\"</span></div></div><div><div><span>}</span></div></div><div><div>\n</div></div><div><div><span>#[get(</span><span>\"/&lt;slug&gt;\"</span><span>)]</span></div></div><div><div><span>fn</span><span> </span><span>post_meta</span><span>(slug</span><span>:</span><span> </span><span>&amp;</span><span>str</span><span>) </span><span>-&gt;</span><span> </span><span>Result</span><span>&lt;</span><span>Json</span><span>&lt;models</span><span>::</span><span>Post</span><span>&gt;, </span><span>NotFound</span><span>&lt;</span><span>String</span><span>&gt;&gt; {</span></div></div><div><div><span>    </span><span>match</span><span> </span><span>db</span><span>::</span><span>post_meta</span><span>(slug) {</span></div></div><div><div><span>        </span><span>Ok</span><span>(post) </span><span>=&gt;</span><span> </span><span>Ok</span><span>(</span><span>Json</span><span>(post)),</span></div></div><div><div><span>        </span><span>Err</span><span>(_) </span><span>=&gt;</span><span> </span><span>Err</span><span>(</span><span>NotFound</span><span>(</span><span>\"Resource was not found.\"</span><span>.</span><span>to_string</span><span>())),</span></div></div><div><div><span><span>    </span></span><span>}</span></div></div><div><div><span>}</span></div></div><div><div>\n</div></div><div><div><span>#[launch]</span></div></div><div><div><span>async</span><span> </span><span>fn</span><span> </span><span>rocket</span><span>() </span><span>-&gt;</span><span> _ {</span></div></div><div><div><span>    </span><span>// setup websocket stuff</span></div></div><div><div><span>    </span><span>tokio</span><span>::</span><span>spawn</span><span>(</span><span>post_listener</span><span>::</span><span>subscribe_posts</span><span>());</span></div></div><div><div><span>    </span><span>// setup server to respond</span></div></div><div><div><span>    </span><span>rocket</span><span>::</span><span>build</span><span>()</span></div></div><div><div><span>        </span><span>.</span><span>mount</span><span>(</span><span>\"/\"</span><span>, </span><span>routes!</span><span>[index])</span></div></div><div><div><span>        </span><span>.</span><span>mount</span><span>(</span><span>\"/\"</span><span>, </span><span>routes!</span><span>[post_meta])</span></div></div><div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>There’s two important parts here in the main server, and I’ve labeled them A and\nB.</p>\n<h3>Part A: Metadata endpoint</h3>\n<p>We’ll say for the sake of argument that somehow the web server has the info\nfor a given post already (since we’ll discuss the Jetstream and DB in part B),\nso <strong>part A</strong> is the thing that looks up info for a given post. One of the nice\nthings about Rocket is that it has lots of type guards, so all I need to do\nis specify that it should be expecting a string input in the path, and it’s\ngoing to return a <code>Result&lt;Json&lt;models::Post&gt;, NotFound&lt;String&gt;&gt;</code>. We call out to\n<code>db::post_meta(slug)</code> which checks the database using the post slug, and returns\nthe Post object. Since Post is tagged as a <code>Serialize</code> struct in <code>db.rs</code>, Rocket\ncan just convert it to JSON using library code and safely return it. If it’s not\nfound, then it just returns a 404 and we move on with our day.</p>\n<p>The comments code now just needs to make a request to the endpoint to get the\nBluesky post rkey:</p>\n<div><figure><figcaption><span>BlueskyComments.tsx</span></figcaption><pre><code><div><div><span>const</span><span> </span><span>getPostAndThreadData</span><span> </span><span>=</span><span> </span><span>async</span><span> (</span></div></div><div><div><span>  </span><span>slug</span><span>:</span><span> </span><span>string</span><span>,</span></div></div><div><div><span>  </span><span>setThread</span><span>:</span><span> (</span><span>thread</span><span>:</span><span> </span><span>AppBskyFeedDefs</span><span>.</span><span>ThreadViewPost</span><span>) </span><span>=&gt;</span><span> </span><span>void</span><span>,</span></div></div><div><div><span>  </span><span>setUri</span><span>:</span><span> (</span><span>uri</span><span>:</span><span> </span><span>string</span><span>) </span><span>=&gt;</span><span> </span><span>void</span><span>,</span></div></div><div><div><span>  </span><span>setError</span><span>:</span><span> (</span><span>errorString</span><span>:</span><span> </span><span>string</span><span>, </span><span>error</span><span>:</span><span> </span><span>string</span><span>) </span><span>=&gt;</span><span> </span><span>void</span></div></div><div><div><span>) </span><span>=&gt;</span><span> {</span></div></div><div><div><span>  </span><span>const</span><span> </span><span>url</span><span> </span><span>=</span><span> </span><span>`https://meta.jack.is/${</span><span>slug</span><span>}`</span><span>;</span></div></div><div><div><span>  </span><span>const</span><span> </span><span>meta</span><span> </span><span>=</span><span> (</span><span>await</span><span> ky.</span><span>get</span><span>(url).</span><span>json</span><span>()) </span><span>as</span><span> </span><span>Meta</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div>\n<p>After getting the metadata, it just acts like normal.</p>\n<h3>Part B: Websockets and databases</h3>\n<p>So how do we <em>get</em> the post info to put in a database for the API route to have\nwhat it needs? Bluesky Jetstream of course. Using <a href=\"https://github.com/videah/jetstream-oxide\">jetstream-oxide</a>,\nas well as the patches from cyypherus, we can now listen to the Jetstream and\nfilter for just my posts. This actually ends up being more robust than the old\nsearch API, as here I can restrict my filter to “just posts with 📝 as the first\ncharacter”, which couldn’t easily be done using the search API.</p>\n<p>Using <a href=\"https://diesel.rs\">Diesel</a> the websocket loop then inserts into a postgres\ndatabase with all the info needed for the front-end to find the post.</p>\n<h2>Next steps</h2>\n<p>There are one or two improvements I’d like to make here, primarily around the\ndatabase handling. It’s not using connection pooling at the moment, but figuring\nout how Rocket handles all of that (and passing a connection to an arbitrary tokio\nfunction) was out of scope for this MVP.</p>\n<p>In case you’re curious, you can find the code here: <a href=\"https://github.com/plttn/at-comments-listener\">at-comments-listener</a>.\nIt’s primarily designed for me, so there’s a lot of stuff in there that’s hard\ncoded, but maybe someone will find it interesting.</p>\n<section><h2>Footnotes</h2>\n<ol>\n<li>\n<p>“rust web server framework” <a href=\"#user-content-fnref-1\">↩︎</a></p>\n</li>\n</ol>\n</section>",
            "url": "https://jack.is/posts/comments-api/",
            "title": "Comments API",
            "summary": "On how I learned something new, and made my comments better in the process",
            "date_modified": "2025-01-26T07:02:45.000Z",
            "author": {
                "name": "Jack Platten"
            }
        }
    ]
}