<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Jack.is typing</title>
        <link>https://jack.is</link>
        <description>I teach sand to think. Also occasionally write blog posts.</description>
        <lastBuildDate>Wed, 06 May 2026 14:27:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026</copyright>
        <atom:link href="https://jack.is/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Self hosting your PDS with Podman]]></title>
            <link>https://jack.is/posts/self-hosting-your-pds/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.self-hosting-your-pds</guid>
            <pubDate>Mon, 24 Nov 2025 06:18:20 GMT</pubDate>
            <description><![CDATA[A writeup of gotchas and other fun stuff along the way of deploying a PDS]]></description>
            <content:encoded><![CDATA[<p>Bluesky is great (as evidenced by my multiple posts on it), but I wanted to own
my data more than it living on the Bluesky servers. Now I could have just used
the standard deployment as shown <a href="https://github.com/bluesky-social/pds">in the PDS repo</a>,
but I wanted to do something a little more fitting my deployment practices.</p>
<h2>Why</h2>
<p>A few years ago, I moved my webserver (what runs this site, my PDS, and lots of
other services) to CentOS Stream back when that was still viable, then Rocky
Linux as the new replacement when Stream sort of died off. Is it perfect, is it
the 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>
<h2>The Problem: part one</h2>
<p>The default Bluesky PDS is intended for being deployed on Ubuntu 20.04/22.04. It
will fail to deploy on anything other than those two distributions (even 24.04
which also works but that’s neither here nor there).</p>
<h3>Solutioning a problem</h3>
<p>While I was in the testing phase of deploying my PDS, I was using an Ubuntu
server that I could successfully deploy using the installer. I already use Caddy
though, so the compose file having Caddy dockerized is not <em>ideal</em>. Fixing that
was a pretty easy cleanup to get a proper <code>pds.env</code> file out of it, since
there’s no canonical source outside of the installer. Now I had a relatively
standard PDS deployment using Docker, but using the Caddy service I use for
everything else on my server.</p>
<h2>Best Practices?</h2>
<p>I have <em>opinions</em> on the way Bluesky PBLLC has made the systemd unit for the PDS
in GitHub. It deploys as a oneshot, so <code>journalctl</code> is not super useful,
<code>systemctl</code> will never show it as running, and it is generally just hard to set
it as a dependency or a dependent of any other services due to it being oneshot.</p>
<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>
<h3>Enter Sandman Podman</h3>
<p>I had been watching my coworkers explore Podman a bit at work, and looking into
it, it had the perfect thing for my needs, the ability to “systemd-ize” a
container workload in a way that made sense for both the container, and for
systemd. I also took this opportunity to set up <a href="https://litestream.io">litestream</a>
for backups as well.</p>
<p>Now that everything’s mostly stable, I can finally document all my configuration
for anyone else to use.</p>
<p>Under the hood it’s using <a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html">Quadlets</a>,
so all of the unit files we’ll create in <code>/etc/containers/systemd</code>.</p>
<p>First we create a pod to contain everything and publish the port:</p>
<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>
<p>Then we slot in a volume:</p>
<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>
</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>
<p>Now that we’ve got everything organized, we can bring in the PDS container itself:</p>
<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>
</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>
</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>
<p>I make this container boot before my Caddy service which brings up all the
associated resources as part of the quadlet.</p>
<p>We then make the appropriate changes to the Caddyfile:</p>
<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>
<p>Now I get autoupdates for my PDS without needing to use the watchtower container
along with logs that feed right into systemd. Additionally, my PDS storage
itself ends up abstracted away for me in a podman volume, rather than being
scattered in a random non-standard place on my VPS. In short, it does basically
exactly what I want it to do, just the way I want it, with some learnings along
the way.</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>I’ve actually moved this server back over to an Ubuntu 24.04 deployment,
due to RHEL-likes running a bit slow for me when it comes to package updates —
just kidding, it’s back to using CentOS Stream now <a href="#user-content-fnref-1">↩︎</a></p>
</li>
<li>
<p>the oneshot method is a totally valid way of doing it to make it “easy”,
but it had too many quirks / compromises for my liking <a href="#user-content-fnref-2">↩︎</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Fantastic Favicons and Where To Find Them]]></title>
            <link>https://jack.is/posts/fantastic-favicons/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.fantastic-favicons</guid>
            <pubDate>Mon, 30 Dec 2024 07:31:23 GMT</pubDate>
            <description><![CDATA[Did you know that CSS in SVGs can use media queries?]]></description>
            <content:encoded><![CDATA[<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
that everyone gets when you make a website.</p>
<p>I wanted to go the extra mile though and make sure it matched the theme of the
site. After a bit of digging I discovered that SVG supports media queries, so
all the fun light/dark mode detection we can do with CSS on the web, also works
in 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>
<p>After vectorizing my work, I added some classes to the paths, put it all in a
single SVG like so:</p>
<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>
<p>All I have to do is treat it like any other SVG favicon and it just works.</p>
<p>Sadly, it doesn’t work in Safari, so Safari will always have a light mode
favicon. Not the end of the world, just a bit of a bummer.</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<p><a href="https://caniuse.com/link-icon-svg">Can I use SVG favicon?</a> <a href="#user-content-fnref-1">↩︎</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Verifying verification]]></title>
            <link>https://jack.is/posts/verifying-verification/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.verifying-verification</guid>
            <pubDate>Tue, 03 Dec 2024 15:01:55 GMT</pubDate>
            <description><![CDATA[alternatively: how I learned to love did:plc]]></description>
            <content:encoded><![CDATA[<p>Imagine it’s the late 90s, and you want to see what the New York Times has to
say about your favorite political topic this week. Now you’ve heard they’ve
finally gotten on the InterNet? The World Wide Web? You’re not quite sure what
to call it, but you have a Sunday edition in your living room, and it has a web
address in the masthead.</p>
<p>You open up Netscape Navigator, type in <code>http://www.nytimes.com</code> and your DSL
connection chugs for a second as more kilobits than a computer from the 80s
would know what to do with fly into your shiny new Dell.</p>
<p>After all of that though, you know you’re at the New York Times’ website, since
you went there right from their physical paper. There wasn’t a middleman, or
anyone telling you that they were in fact the Gray Lady, you just did a little
bit of leg work.</p>
<h2>Time Warp</h2>
<p>Now it’s 2010, and you want to see what the Times has to say about your favorite
political topic this week. You could head to their website, grab the paper
from the guy on the corner, or for your first time you could open up this new
website called Twitter, and there’d just be a handful of words written by the
Times, summarizing a story, and you wouldn’t have to go anywhere. How do you
know if it’s actually the Times? Well Twitter thought of that for you, and they
added a little blue checkmark next to their account for you. <sup><a href="#user-content-fn-1">1</a></sup></p>
<p>Now you can trust that @nytimes is the Times on Twitter, right? Right? Well for
a while you used to be able to, but eventually it all fell apart. And in my
opinion, that’s a large part of why Bluesky is popping off (or “has the juice”
as the kids say) as Twitter / X slowly begins the doom spiral that any site
sees in the future as they become no longer relevant.</p>
<h2>The “problem”</h2>
<p>So <a href="https://bsky.social/about/blog/2-7-2022-overview">Bluesky</a> / <a href="https://atproto.com">ATProto</a>
is created to decentralize Twitter (originally, then is spun off entirely pre
Musk acquisition), and a primary tenet of both Bluesky PBLLC (the corporation)
and
ATProto (the protocol itself) is to ensure that long term, the ecosystem is
protected from any actor in the space from turning hostile. There’s no “special
sauce” that Bluesky PBLLC has (other than chat/DMs but we’ll set that aside for
the time being) that is exclusive to them.</p>
<p>Every part of the ecosystem then needs to be designed to be hostile-proof,
from where user data lives, to how users consume and create data. This goes as
far as how user identities are stored and referenced. There’s been a lot of work
put in to ensure that in the long run, you are totally in control of your
identity, not Bluesky PBLLC.</p>
<p>So the “problem” here is that Bluesky doesn’t have a concept of verification,
at least not the way Twitter had. There is no “central arbiter” of trust,
because how can you have a central arbiter of trust when the entire ecosystem
is designed to avoid that single point of failure?</p>
<p>Instead all the “verification” is done with domain names, and there’s been
more than a few folks upset / weirded out / annoyed that there isn’t as much
verification as there was on Twitter.</p>
<p>The more I think about it though, the less I think it is a problem, and more
us as a social media using society not remembering the olden days.</p>
<h2>Why this matters</h2>
<p>We’ve spent the last 10-15 years forgetting that the web is a protocol, not a
platform. 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>
(and in fact is now on the board of Bluesky PBLLC). In the example of the 90s,
there wasn’t a platform saying whether or not the NYTimes on the web was
“verified”, there was just a little bit of legwork to check and crosscheck that
indeed this website was theirs. There was no middleman, just a protocol.</p>
<p>However, in the next time warp, we’re instead relying on these centralized
parties to decide who is and isn’t verified, and shifts us to treating the web
as a platform, not a protocol. People don’t browse the web, they use Facebook,
they use Twitter.</p>
<p>The concept of domain handles, verification, the whole nine on how Bluesky
handles this is the first step on getting us as an internet-using society <em>back</em>
to protocols.</p>
<p>A domain is the most ownership of anything you can have on the web. If a new
ATProto social site opens up, you know that <code>jack.is</code> there is also me, just
like you know it’s my Bluesky account, just like you know it’s the site you’re
reading right now. I own my identity, not Instagram, or Twitter, or Bluesky, and
I think that means something more than a little check in a circle that gives me
the good feelings.</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>“So you see, that’s where the trouble began. That check; that damned check” <a href="#user-content-fnref-1">↩︎</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Blog, now with Bluesky comments]]></title>
            <link>https://jack.is/posts/bluesky-comments/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.bluesky-comments</guid>
            <pubDate>Tue, 26 Nov 2024 01:41:27 GMT</pubDate>
            <description><![CDATA[A show and tell of my new comments section powered by Bluesky]]></description>
            <content:encoded><![CDATA[<p>After being inspired by <a href="https://emilyliu.me/blog/comments">Emily Liu’s</a> post on
how to do Bluesky comments, I thought I could add it here too, but with one
minor plus to it.</p>
<p>Emily’s method was great, but required hardcoding the atproto URI into the post
itself. That’s a little hard to do with my deployment and post method, where the
RSS feed is generated, and then that triggers an Echofeed
(<a href="https://echofeed.app">Not Sponsored</a>) that posts to Bluesky.</p>
<p>Because of this, I wanted something better. Ideally, I could have it just find
the right post without me having to worry about it. Thanks to the power of the
Bluesky API, I’m able to do that!</p>
<p>The special sauce is right here in <code>getPostAndThreadData</code>. It runs a search for
the Echofeed emoji I use for all my posts (📝), looks for my author DID, as well
as the page URL that would have been posted. It then limits it to 1, and grabs
that URI. Once we’ve got a URI, we then pass it to Emily’s code like normal
along with some styling and design tweaks.</p>
<p>Is it perfect? Not yet, but now it’s just fine tuning some CSS styling (and also
getting React to be chunked a little better).</p>
<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>
<p>In the words of Brennan Lee Mulligan: “GET IN THE COMMENTS!”</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Analytics Showdown]]></title>
            <link>https://jack.is/posts/analytics-showdown/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.analytics-showdown</guid>
            <pubDate>Thu, 21 Nov 2024 22:21:02 GMT</pubDate>
            <description><![CDATA[Some more analytics in play here]]></description>
            <content:encoded><![CDATA[<p>Doing an analytics showdown on the site here between a few services to see which
one shall win for 2025.</p>
<h2>Tinylytics</h2>
<p>My current provider of some of the juicy info (and provider of the fancy kudos)
buttons. It gets the job done, but I’d love to see more info.</p>
<h2>Goatcounter</h2>
<p>Offers more information, has a sweet craigslist-era aesthetic, and could be self
hosted if I want.</p>
<h2>Umami</h2>
<p>My current front runner in all honesty. Has everything I want, and then some,
and is being selfhosted by me right now. Does what I need it to do and I think
will win.</p>
<p>If I switch however, I’m going to have to reimplement my upvote buttons somehow
but that’s a problem for future me.</p>
<h2>Update</h2>
<p>Umami self-hosted won.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Blog updates part 2]]></title>
            <link>https://jack.is/posts/blog-updates-part-2/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.blog-updates-part-2</guid>
            <pubDate>Tue, 19 Nov 2024 01:42:09 GMT</pubDate>
            <description><![CDATA[Pardon the dust, again]]></description>
            <content:encoded><![CDATA[<p>All the dust <em>should</em> have finally settled from the tweaks I’ve made to the
site. Now everything is one page which allows for fancy view transitions from my
links right to the blog. I did break RSS again (sorry Joshua) but no way to do
that migration nicely.</p>
<p>Now I can have my blog and my
<a href="https://docs.bsky.app/docs/advanced-guides/federation-architecture#personal-data-server-pds">PDS for Bluesky</a>
all in one place. With that said, I still haven’t moved my Bluesky account to my
PDS just yet, but it’s ready to go for when I’m ready to push the button.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[On Bluesky]]></title>
            <link>https://jack.is/posts/on-bluesky/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.on-bluesky</guid>
            <pubDate>Sat, 16 Nov 2024 05:29:26 GMT</pubDate>
            <description><![CDATA[Some random thoughts on Bluesky (and Mastodon)]]></description>
            <content:encoded><![CDATA[<p>So, Bluesky is now a thing. I mean it was a thing before today, but the past
week or so seems to have been the “screw it i’m outta here” moment for a lot of
folks on X, the everything app Twitter<sup><a href="#user-content-fn-1">1</a></sup>. I made my account long before
this Eternal September, but I think I might spend more time on there than I had
been previously. Here’s some random-ish thoughts on how I feel about Bluesky,
ATProto, and how I feel about Mastodon / ActivityPub as a consequence.</p>
<h2>The Vibes</h2>
<p>Twitter, and I mean Twitter before it turned to hell in a handbasket, had vibes
from various circles. You had Tech Twitter, you had Weird Twitter<sup><a href="#user-content-fn-2">2</a></sup>, etc. Even
with all of that though, the vibes and memes were still often in sync because
everyone was using the same app, and had at least some of the same trending
topics they were aware of. Milkshake Duck, bean dad. There was always a main
character, and often you didn’t really want to be the main character.</p>
<p>For better or for worse, Mastodon doesn’t always have those vibes in my opinion.
It’s often a little bit stuffy. I’ve found Tech Twitter there in many ways, but
Weird Twitter never seemed to migrate there, even when it was first popping off
before Bluesky was a thing.</p>
<p>Bluesky though has all your circles, and is picking up at a much faster than
Mastodon ever did. You’ve got your
<a href="https://bsky.app/profile/dril.bsky.social">dril</a>, you’ve got your
<a href="https://bsky.app/profile/darthbluesky.bsky.social">darth</a>, you’ve got your
<a href="https://bsky.app/profile/weratedogs.com">WeRateDogs</a>. Having one of those on
Mastodon was a challenge, and I don’t think there was ever a time even if all
three of those accounts had an Mastodon / AP presence at all, were they ever
posting at the same time.</p>
<p>So the vibes are there for Bluesky like Old Twitter the way Mastodon just isn’t.
But what about the tech?</p>
<h2>The Tech</h2>
<p>Now I was an avid ActivityPub defender (and still am to some extent) when
ATProto originally launched, and was feeling like Bluesky was too “corporate”,
and kind of didn’t look into it much, and only begrudgingly joined.</p>
<p>Only relatively recently did I do a deeper dive into the pros and cons of
ATProto, and I’ve sort of turned around on how I feel about the two.</p>
<p>I tried to run my own Mastodon server one time, and to be honest it Kinda
Sucked. Mastodon simply doesn’t scale well down to one or two users. It’s fine
if you’re on a larger server as you can share the load (shoutout
<a href="https://adam.lol">Adam</a> for <a href="https://home.omg.lol/referred-by/jack">omg.lol</a>
and the associated Mastodon server social.lol), but the effort I had to go to to
even try to get Mastodon working on a server that already had services on it and
it was not fun.</p>
<p>Mastodon also has some odd protocol design decisions that make life harder for
small web, which seems oddly counter-intuitive. The most obvious one (and one
that 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
for any posted link (the OpenGraph info) is fetched by <em>every</em> single
ActivityPub instance that sees that post. In other words, if you’re followed by
1,000 users on 1,000 separate Mastodon / ActivityPub instances, and you post a
link to your neat little blog, in the span of a few seconds, you’re going to get
1,000 requests to your website. Is this necessary? Not really, but it’s the way
the protocol has been designed, and continues to stay that way. There’s even a
cute name for it, a Mastodon stampede<sup><a href="#user-content-fn-4">4</a></sup>. This just isn’t sustainable.</p>
<p>ActivityPub is also aggressively chatty when it comes to the actual
communication between servers. Migrating between servers is basically just like
shoving a HTTP 302 at your followers and hoping for the best. You don’t get to
move your posts, just your followers and following. Your username doesn’t stay
the same either.</p>
<p>ATProto on the other hand is built on layers of abstraction, so your handle is
just a reference to a permanent identifier. Change your handle, no problem. Want
to move your account entirely from one PDS (Personal Data Server) to another? No
problem. The protocol has support for it from the beginning. Rather than sending
a bunch of pushes when you post something, your PDS just adds a new post to your
PDS, and the network aggregates and displays to clients. It just feels cleaner.
Spinning up a PDS compared to a Mastodon server is like night and day. I was up
and running with my self-hosted test user in about 10 minutes and that was with
having to tweak the install script slightly to meet my preferences.</p>
<h2>The Summation</h2>
<p>So which one should win? It’s not that one can win and the other shouldn’t, they
each have different design goals. However, I’ve come around on which one is (at
least currently), more scalable, and I think it’s ATProto. From a social
perspective, Bluesky seems to be winning the normal user demographic, which
Mastodon always had trouble getting users to adopt.</p>
<p>As for my self-hosted PDS, I’m still deciding whether or not to move my current
Bluesky hosted account to it. There’s currently no import path back to Bluesky
(not because of protocol reasons, but because of giant account reasons), and
that makes me feel like this is a permanent commitment if I do. We’ll see I
guess.</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>considering that you still can’t use X as a bank like was promised, lol <a href="#user-content-fnref-1">↩︎</a></p>
</li>
<li>
<p>see dril, et al. <a href="#user-content-fnref-2">↩︎</a></p>
</li>
<li>
<p>have I overused parentheticals yet? since I used another one in the
following line, clearly not <a href="#user-content-fnref-3">↩︎</a></p>
</li>
<li>
<p>get it, because mastodons are like elephants and elephants stampede? I’m
done with the footnote overuse I promise <a href="#user-content-fnref-4">↩︎</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Blog Updates]]></title>
            <link>https://jack.is/posts/blog-update/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.blog-update</guid>
            <pubDate>Wed, 09 Oct 2024 23:33:46 GMT</pubDate>
            <description><![CDATA[A quick update]]></description>
            <content:encoded><![CDATA[<p>We (I? I’m not a fan of talking about myself in the third person, but that’s
neither here nor there) now have a few cool new things.</p>
<h2>Author Attribution on Fediverse</h2>
<p>Thanks to <a href="https://rknight.me/blog/setting-up-mastodon-author-tags/">Robb</a>
posting it, I now have my fancy fediverse author tags configured, so it shows
attribution properly when my post is posted.</p>
<p>It’s simple, but it’s neat.</p>
<h2>Tri-state dark mode selector</h2>
<p>And I don’t mean the tri-state area. <sup><a href="#user-content-fn-1">1</a></sup></p>
<p>The Astro them I use, <a href="https://astro-paper.pages.dev">Astro Paper</a> supports both
light and dark mode. However, once you click the selector, it switches from
using system selected (<code>prefers-color-scheme</code>) to the user selected option and
doesn’t allow a way of switching back. I didn’t love this, so I added a third
option.</p>
<p>Now the theme picker rotates between system, light, and dark. Pick whichever
theme suits your fancy, no matter what your operating system tells your browser.
Or pick system and let your operating system be the boss. You decide.</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>boo this man <a href="#user-content-fnref-1">↩︎</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Sorry to RSS readers]]></title>
            <link>https://jack.is/posts/sorry-in-advance/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.sorry-in-advance</guid>
            <pubDate>Fri, 12 Apr 2024 13:27:50 GMT</pubDate>
            <description><![CDATA[an apology for the annoying RSS tomfoolery that's been going on]]></description>
            <content:encoded><![CDATA[<p>So turns out that I had some mismatched links (trailing slash vs no trailing
slash) and keeping track of all the places where they were mixed up was
impossible.</p>
<p>In other words, sorry for your RSS reader getting confused again if it sees all
my posts as new entries again since the guid changed. We should be good now
though, with less breaking of your readers.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Mastodon Stampedes]]></title>
            <link>https://jack.is/posts/mastodon-stampede/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.mastodon-stampede</guid>
            <pubDate>Tue, 09 Apr 2024 18:19:02 GMT</pubDate>
            <description><![CDATA["fun" fact: each Mastodon server gets their own copy of your OG card]]></description>
            <content:encoded><![CDATA[<p>If you’re running a site that dynamically generates content, you should probably
do some sort of caching to prevent your site from exploding whenever someone
posts it on Mastodon and friends.</p>
<p>For example, if you use Caddy, it might look something like this:</p>
<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>
</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>
<p>Since this blog is static, I just tank the Mastodon hits as necessary, but
something to keep in mind if you’re hosting your own thing.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[A new brand]]></title>
            <link>https://jack.is/posts/new-domain/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.new-domain</guid>
            <pubDate>Fri, 05 Apr 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A new domain for the blog and brand]]></description>
            <content:encoded><![CDATA[<p>After literal years of jonesing for it, I’ve finally gotten <code>jack.is</code> as a
domain. Sorry for whoever previously had it, but you did let it go after all.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Blog Setup 2024]]></title>
            <link>https://jack.is/posts/blog-setup-2024/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.blog-setup-2024</guid>
            <pubDate>Sun, 28 Jan 2024 15:33:51 GMT</pubDate>
            <description><![CDATA[A brief summary of the newly deployed plttn.me]]></description>
            <content:encoded><![CDATA[<p>A semi-deep dive into the newly deployed plttn.me.</p>

<p>It’s time for the regularly scheduled blog updates.</p>
<h2>Yesterday (metaphorically speaking)</h2>
<p>The prior setup was using Hugo and deployed to either Netlify, Cloudflare Pages
or my own box. This was great, but after thinking through the Small Web ™️
movement some more I thought it made more sense to move to something in-house.</p>
<h2>Today</h2>
<p>The site now uses <a href="https://astro.build">Astro</a> and deploys right to my server.
For deploying, it’s a two-step process. All commits on <code>main</code> on GitHub are
checked. After everything checks out, I can trigger a release in GitHub to
deploy to my server.</p>
<p>Here’s part of one workflow:</p>
<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>
<p>Here’s the other:</p>
<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>
<h3>DNS Sidenote</h3>
<p>I’ve also switched to using <a href="https://desec.io">Desec</a> for my DNS hosting, it
fits my needs nicely, is free, and enforces DNSSEC.</p>
<p>Hopefully the first of multiple blog posts to come.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Pardon the Dust]]></title>
            <link>https://jack.is/posts/pardon-the-dust/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.pardon-the-dust</guid>
            <pubDate>Sat, 27 Jan 2024 21:15:07 GMT</pubDate>
            <description><![CDATA[More to come soon]]></description>
            <content:encoded><![CDATA[<p>Pardon the dust. Updates in progress.</p>

]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Yet Another Octopress 2.0 and TravisCI Guide]]></title>
            <link>https://jack.is/posts/yet-another-octopress20-guide/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.yet-another-octopress20-guide</guid>
            <pubDate>Sat, 17 Jan 2015 19:18:29 GMT</pubDate>
            <description><![CDATA[A quick (and outdated guide) on how to use TravisCI to deploy a Octopress blog]]></description>
            <content:encoded><![CDATA[<p>So there’s only about 30 50 thousand pages on this general topic, but I
found that about half of them were out of date, so I figured I should try and
collect all the info into one big post. I’ve referenced where I’ve found some of
the stuff that’s harder to find from Googling using the wonders of Octopress
footnotes.</p>
<p>Unfortunately, as I was finishing up this post, the post announcing Octopress
3.0 comes out. While I had known it was coming, I wasn’t expecting it to be so
soon. Obviously this post will be less useful, but it should still be
informative. I’ve included what I think will need to be modified to function on
3.0 at the bottom.</p>
<h2>Table of contents</h2>
<h2>My Site Flow</h2>
<ul>
<li>Local</li>
<li>Private Github Repo</li>
<li>Built and Deployed using TravisCI to a DigitalOcean droplet</li>
</ul>

<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
promptly modified:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>## -- Misc Configs -- ##</span></div></div><div><div>
</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>
</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>
</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>
<p>For this to work using <code>rake preview</code>, you’ll need to modify your preview task
slightly, add the flag <code>--drafts</code> to the jekyll call.</p>
<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>
<h2>The Build Process</h2>
<p>Personally, I don’t want to have to deal with building and uploading my site.
Instead I use Travis-CI targeted to a specific Github repo branch, and some
modified rsync scripts.</p>
<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>
</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>
</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>
</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>
<h2>Rsync Setup</h2>
<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>
<p>You’ll need to configure your deployment account’s SSH private key in Travis-CI
by replacing all new lines with <code>\n</code>, and then escaping that to <code>\\n</code>, so that
it ends up being one line with <code>\\n</code> in between each original line.<sup><a href="#user-content-fn-2">2</a></sup>
(<strong>NOTE</strong>: Make sure the key was generated without ssh-agent running, and set a
blank passphrase.) Next, go to your Travis-CI settings, create a new environment
variable, name it whatever you want, I used <code>$DEPLOY_PRIVATE_KEY</code>, then paste in
your one-line private key into the variable section. Now that it’s a environment
variable in all Travis workers your builds will use, the <code>before_install</code> tasks
will disable strict host key checking (requires input, which Travis doesn’t
allow), and then echo the contents of <code>DEPLOY_PRIVATE_KEY</code> to <code>id_rsa</code>, then add
that key to the ssh-agent, in my case, allowing the worker to connect using the
user <code>travis-ci</code> to my droplet.</p>
<p>If the user you’re connecting with in your Rakefile’s settings isn’t the owner
of your specific directories on your hosting server, rsync will probably fail to
sync properly. In that case, you’ll need to add <code>--omit-dir-times</code> to
<code>rsync-args</code> in the Rakefile.</p>
<p>After all of this, you should be good to go. If you’re using the master deploy
method, push your site up to master, and it should build properly on Travis, and
deploy to your rsync destination.</p>
<h2>Octopress 3.0</h2>
<p>For the most part, as far as I can tell, these modifications will <em>mostly</em> still
work for 3.0. The drafts changes will no longer be needed anymore, as the Jekyll
drafts workflow is now using a separate drafts folder. The <code>.travis.yml</code> will
need to be tweaked slightly, as well as the <code>_config.yml</code> having an exclude
added to keep Jekyll running on Travis-CI from trying to build the templates in
<code>/vendor</code>, but other than that, this post is mostly forwards compatible to
Octopress 3.0.</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<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>
</li>
<li>
<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>
</li>
</ol>
</section>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Optimizing Bluesky comments]]></title>
            <link>https://jack.is/posts/atcute-comments/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.atcute-comments</guid>
            <pubDate>Sun, 12 Jan 2025 06:31:38 GMT</pubDate>
            <description><![CDATA[using @atcute/client to make comments better]]></description>
            <content:encoded><![CDATA[<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
is having to bring in the entire <code>@atproto/api</code> package. It seemingly can’t be
tree shaken down via Vite, so you end up with a massive JS file for comments.</p>
<p>How massive is it? More than 700kB.</p>
<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>
<p>It’s kinda a lot, and as it is I had to tweak my Astro config to make Vite not
warn about this:</p>
<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>
<p>I wasn’t a huge fan of this for a few reasons. One, that was a lot of unneeded
JS being delivered that I didn’t need, and I had to modify a warning setting
which seemed less than ideal.</p>
<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
feed, and poked at it a bit, but only after I figured out how to make unauthenticated
requests with it did it become clear how much better it was going to make my
bundles.</p>
<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>
to instead use atcute:</p>
<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>
</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>
</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>
<p>The unexpected benefit here is that <code>@atproto/api</code> didn’t actually have all the
helper functions Emily needed for it to work, so there was a direct call to one
of the xrpc endpoints. Now that everything just uses <code>rpc.get</code>, it’s a lot more
consistent and easier to follow.</p>
<p>I did have to add a few <code>//@ts-expect-error</code> sprinkled around, but they’re all
guarded by typeguards so I’m not too worried.</p>
<p>Now instead of 700kB, my <code>BlueskyComments.js</code> is only 10kB. A vast improvement.</p>
<p>Thanks again to Emily for posting their code to begin with, and Mary for an
excellent suite of tooling that’s much more compact than the full ATProto API.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Editor Setup 2020]]></title>
            <link>https://jack.is/posts/editor-setup-2020/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.editor-setup-2020</guid>
            <pubDate>Tue, 26 May 2020 19:29:00 GMT</pubDate>
            <description><![CDATA[A quick summary of my current (at the time) VSCode setup.]]></description>
            <content:encoded><![CDATA[<h2>Goodbye Atom</h2>
<p>I wanted to like Atom, and I was one of those stubborn ones who had been putting
off trying VS Code for probably years and continued to use Atom and putting up
with the subpar performance, even on a relatively overpowered computer.</p>
<p>A few weeks back though, I decided to bite the bullet and give VS Code a fair
try, and after using it for just a few days, I didn’t see myself going back to
Atom anytime soon. There’s just so much more performance in Code compared to
Atom (which confuses me because they’re both Electron), and included in that
performance is more features included standard, instead of needing 100
extensions to get what I want out of it.</p>
<p>Looking at the writing on the wall, I don’t foresee Atom lasting too much
longer, I haven’t seen a new feature added to Atom since right around the time
Atom Teletype came out, and then that’s been pretty much it since then.</p>
<p>Sadly this means my theme that I’ve spent the better part of five years tweaking
and modifying will end up slowly dying, but given that Pubster really isn’t
primarily my work when you boil it down, I don’t feel too bad about it.</p>

<h2>Hello Code</h2>
<p>So what’s my Code look like now?</p>
<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>
<p>I’ve started using Iosevka as my programming font with it configured to more or
less match Pragmatica Pro’s font features, and I’m really enjoying it so far.
I’ve tried a bunch of different fonts, and had tried Iosevka in the past and
wasn’t sure how I felt about it, but now that it’s got ligature support and
decent hinting, I’m a fan. Additionally, for plain text editing (markdown,
plaintext, commit messages), I switch over to Iosevka Sparkle which is designed
for prose use using <code>settings.json</code> overrides for specific languages:</p>
<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>
<p>As for the color scheme itself, I decided to give
<a href="https://monokai.pro">Monokai Pro</a> a try, and I’m really loving it. The theme
itself is nice, and the file icon themes it comes with are also fantastic and
clear without being obnoxious. While it does cost a few bucks, for how nice it
looks I think it’s definitely worth it.</p>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
        <item>
            <title><![CDATA[Comments API]]></title>
            <link>https://jack.is/posts/comments-api/</link>
            <guid isPermaLink="false">tag:jack.is,2024:posts.comments-api</guid>
            <pubDate>Sun, 26 Jan 2025 07:02:45 GMT</pubDate>
            <description><![CDATA[On how I learned something new, and made my comments better in the process]]></description>
            <content:encoded><![CDATA[<p>Have we yak shaved enough yet on comments? I don’t think we have, what’s one
more post among friends.</p>
<h2>The problem</h2>
<p>As mentioned in <a href="https://jack.is/posts/bluesky-comments/">Bluesky Comments</a>, my posting workflow
doesn’t allow me to know the Bluesky post ahead of time. So I thought, well I’ll
just use the search API to find the post URL based on my echofeed post. That
worked pretty well, but then I saw this skeet:</p>
<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.

searches 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>
<p>My first reaction was “Oh it’ll be fine this is just limited use and there was
another post that says it’ll be fine for limited use”. It was fine, but after a
day or so after Bryan’s post, I started getting CORS errors from my search
function, and I figured that was that, and I needed to do this a better way,
and maybe learn something along the way.</p>
<h2>The research</h2>
<p>I had seen Jetstream in my research prior to now, and figured some sort of
database would probably be the best bet, where it’s just a single lookup, rather
than every browser of the site having to do the search (even if it wasn’t blocked).</p>
<p>My initial thought was using Typescript for a server API, and there’s definitely
still benefits to there, being a robust ecosystem of ATProto packages. I however
wanted to Think Different™ so I decided to write it in Rust, both because I
could, and also because I wanted to learn a bit of Rust “systems” programming,
since I’ve been spending a lot of my day in Typescript.</p>
<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>
and determined it was actually perfect for my needs. Simple declarations of
handler functions, and it’s nice and type-safe. What’s not to like.</p>
<h2>It’s Rust time</h2>
<p>With just a bit of boilerplate code, I can spin up the API handler needed for
the metadata info for my front-end.</p>
<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>
</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>
</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>
<p>There’s two important parts here in the main server, and I’ve labeled them A and
B.</p>
<h3>Part A: Metadata endpoint</h3>
<p>We’ll say for the sake of argument that somehow the web server has the info
for a given post already (since we’ll discuss the Jetstream and DB in part B),
so <strong>part A</strong> is the thing that looks up info for a given post. One of the nice
things about Rocket is that it has lots of type guards, so all I need to do
is specify that it should be expecting a string input in the path, and it’s
going to return a <code>Result&lt;Json&lt;models::Post&gt;, NotFound&lt;String&gt;&gt;</code>. We call out to
<code>db::post_meta(slug)</code> which checks the database using the post slug, and returns
the Post object. Since Post is tagged as a <code>Serialize</code> struct in <code>db.rs</code>, Rocket
can just convert it to JSON using library code and safely return it. If it’s not
found, then it just returns a 404 and we move on with our day.</p>
<p>The comments code now just needs to make a request to the endpoint to get the
Bluesky post rkey:</p>
<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>
<p>After getting the metadata, it just acts like normal.</p>
<h3>Part B: Websockets and databases</h3>
<p>So how do we <em>get</em> the post info to put in a database for the API route to have
what it needs? Bluesky Jetstream of course. Using <a href="https://github.com/videah/jetstream-oxide">jetstream-oxide</a>,
as well as the patches from cyypherus, we can now listen to the Jetstream and
filter for just my posts. This actually ends up being more robust than the old
search API, as here I can restrict my filter to “just posts with 📝 as the first
character”, which couldn’t easily be done using the search API.</p>
<p>Using <a href="https://diesel.rs">Diesel</a> the websocket loop then inserts into a postgres
database with all the info needed for the front-end to find the post.</p>
<h2>Next steps</h2>
<p>There are one or two improvements I’d like to make here, primarily around the
database handling. It’s not using connection pooling at the moment, but figuring
out how Rocket handles all of that (and passing a connection to an arbitrary tokio
function) was out of scope for this MVP.</p>
<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>.
It’s primarily designed for me, so there’s a lot of stuff in there that’s hard
coded, but maybe someone will find it interesting.</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>“rust web server framework” <a href="#user-content-fnref-1">↩︎</a></p>
</li>
</ol>
</section>]]></content:encoded>
            <author>Jack Platten</author>
        </item>
    </channel>
</rss>