Hi, we have a release coming up soon, and it would be great if some of you could share your stories at the same time as us.
I noticed this post three months ago on my favorite npmx.dev. And this time, beyond simply liking the project, I actually had something to share - an exciting month, wonderful connections, and a sense of shared values and goals.
Even with that motivation, I hesitated for a long time before supporting the idea. Not because I lacked the desire or the ability to tell my story, but because I no longer had a place that felt right for it. For the first time, I carried thoughts that had nowhere to land.
The search for a home for my articles led me not just to yet another platform, but to the idea of my own blog - built on top of the AT Protocol.
Background
This wasn’t the first time I had faced this question. A few years earlier, I had moved to Medium. At the time, it felt like a solid place to store my articles and connect with other writers. But after two years, the same question resurfaced. The feeds were increasingly filled with empty content, while I was interacting even more with the wider world.
The search for a new home for my articles led only to stagnation. But this notification managed to break through the [already practically closed] door and bring me back to this question. The difficulty was compounded by the fact that I wasn’t just looking for a place to publish a new article, but a place where my future story wouldn’t become an archive of someone else’s service.
In the end, the answer was waiting where I had first returned - the AT Protocol.
Atproto
The Authenticated Transfer Protocol (AT Protocol or atproto) is an open-source, decentralized network protocol designed for social media applications (including Bluesky). It enables user-controlled identities, account portability, and algorithmic choice, allowing users to move between providers without losing data
In fact, this is far from my first encounter with atproto. Besides having long ago chosen Bluesky as my sole social network, I also love this protocol as a tool that solves real-world problems. For instance, two years ago I started working on atsky.app (formerly pinsky.app - an appview for Bluesky), and now I’m actively involved in developing npmx.dev, which leverages the protocol’s capabilities to add a social layer to our everyday experience of interacting with packages.
But the most interesting use case for the protocol in the context of this article is, of course, writing platforms. But before we talk about platforms, it’s worth saying a few words about the articles themselves.
Structure
First, about the article itself - or more precisely, its structure. To put it simply, an article is just content that has an author and a publication venue (a personal website or a platform).
This model fits easily into atproto. The author is the owner of the publication (atmosphere account), the data is stored on their own server (PDS), and the article itself is one of many items (record), following specific storage instructions (lexicon).
{
"$type": "site.standard.document",
"title": "Facets as a Formatting Engine",
"path": "/facets",
"site": "at://did:plc:er6erflnnxcozlbqmrpflt6h/site.standard.publication/3miclijid6r24",
"content": {
"$type": "net.atview.document",
"facets": ["..."]
},
"bskyPostRef": {
"cid": "bafyreigulnvsiacn6otqx5kdepqhcwqiu5u5cho3kp53z5esf2hpbry3nu",
"uri": "at://did:plc:er6erflnnxcozlbqmrpflt6h/app.bsky.feed.post/3mk3ldrvx3c2f"
},
"description": "...",
"textContent": "..."
}
This is an abbreviated record of my previous article, you can view the full article in pdsls or by clicking the button at the bottom of the article page. And yes, as soon as I publish an article, it immediately becomes public and accessible to all atproto users and services. Anyone who supports the standard under which I publish articles will be able to display it on their site.
If you’ve worked with a CMS, you’ll likely find this entry quite familiar and standard. And that’s the beauty of this integration.
One of the few things you’ll find new is $type. This field tells the reader which standard is used in the post (also known as the lexicon). For example, I use the site.standard, just like most blogging platforms within atproto.
Standards
For me personally, standards have become one of the most interesting parts of the protocol. It seems we’ve started talking less about services and more about standards. It is into these that platform authors are pouring enormous effort, making their ideas and views accessible to everyone.
The authors of leaflet, pckt, offprint, and other services have already done incredible work to standardize blogging and develop the infrastructure. The lexicons they support cover all the needs of modern services.
I also use existing standards on my blog. For article metadata, as mentioned earlier, I use site.standard - it’s widely adopted and allows any service to discover my article and display key information. For the content itself, most of my articles use the pub.leaflet standard - thanks to this, articles are fully accessible in Leaflet immediately after publication on my PDS.
Offprint and pckt are equally excellent standards and services. At the same time, we can extend the standard as needed, refine its details, or add our own types. net.atview, described in the previous article, also does a good job (though, of course, it won’t work fully in other services).
How it works
Each article is stored in my PDS. During the build, all I have to do is retrieve the desired articles and render them.
Since, in addition to the main articles, I also sometimes experiment with different ideas, I needed to decide exactly what to display on the site. And as such a filter, I use a specific publication (pdsls) from the already familiar site.standard:
{
"$type": "site.standard.publication",
"url": "<https://alexdln.com/blog>",
"name": "Alex's blog",
"description": "Publications about development and atproto"
}
The “site” field in the post entry above links directly to this publication. During the build, I retrieve my data, connect to my PDS, and find the articles associated with this publication
const fetchPublicationDocuments = async ({ rkey, did }) => {
try {
const url = new URL("<https://constellation.microcosm.blue/links>");
url.searchParams.set("target", `at://${did}/site.standard.publication/${rkey}`);
url.searchParams.set("collection", "site.standard.document");
url.searchParams.set("path", ".site");
url.searchParams.set("did", did);
const links = await fetch(url);
const linksJson = await links.json();
return linksJson.linking_records;
} catch {
return [];
}
};
After that, I load detailed information about all existing articles to display them in a general list and for the static site build
export const fetchRecord = async (agent, { rkey, did }) => {
const { data } = await agent.com.atproto.repo.getRecord({
repo: did,
collection: "site.standard.document",
rkey,
});
return data.value;
};
export const loadPosts = async (agent, { rkey, did }) => {
const links = await fetchPublicationDocuments({ rkey, did });
const records = await concurrentPool(
links,
(link) => fetchRecord(agent, { rkey: link.rkey, did }),
8,
);
return records.filter(isStandardSiteDetailed);
};
And finally, all that’s left is to display the content
const currentPost = posts.find((post) => post.path === `/${slug}`);
const jsx = dataToJsx(currentPost, {
authorDid: profile.did,
blockElements: { heading: AnchorHeading },
});
return (
<Container>
<AnchorHeading>
{currentPost.title}
</AnchorHeading>
<ContainerBody>{jsx}</ContainerBody>
</Container>
)
Now that I’m in full control of the articles on my end, I can gradually expand and improve the interface.
For example, I can add related articles to post entries
{isSeeAlsoContent(currentPost.content) && (
<RelatedStories list={currentPost.content.additional.seeAlso} />
)}
Display likes for a related post:
const { data } = await agent.app.bsky.feed.getLikes({
uri: currentPost.bskyPostRef.uri,
limit: 100,
});
// ...
{data.likes.map((like) => <Avatar person={like.actor} key={like.actor.did} />
Display comments:
const { data } = await agent.app.bsky.feed.getPostThread({
uri: currentPost.bskyPostRef.uri,
});
const replies = data?.thread.replies?.filter(isThreadViewPost)
// ...
{replies?.map((reply) => (
<ThreadViewPost key={reply.post.cid} item={{ thread: reply }} />
))}
And many more.
Final stage
As a result, the current infrastructure offers limitless possibilities for my ideas, as well as great potential for integrations with other atproto services - even those not related to blogging - such as stream.place, npmx.dev, grain.social, and hundreds of others.
In addition, I was delighted to discover that this approach allows me to integrate stories from social media that interest me, more frequently and easily. I used to embed posts with screenshots, but the ability to add a full post - including the option to read comments - creates a completely different reader experience.
Another interesting detail I’m actively incorporating into my blog is alt text. I now add alt text (or sometimes a caption) to all images with meaningful content. The text should be equally “visible” to everyone, and this is one of the many things the protocol has taught us.
The migration itself took about a month of preparation and work. Now, within the protocol, such transitions between platforms can be done with a single click. But more on that in future articles.