惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
腾讯CDC
Jina AI
Jina AI
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
小众软件
小众软件
M
MIT News - Artificial intelligence
MyScale Blog
MyScale Blog
D
Docker
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
月光博客
月光博客
L
LangChain Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - Franky
C
Check Point Blog
U
Unit 42
人人都是产品经理
人人都是产品经理

Piccalilli - Everything

The Index: Issue #197 Save 35% on all courses for two weeks only The Index: Issue #196 A CLI for adding new music to the collection The Index: Issue #195 A look at the geolocation HTML element and how it works The Index: Issue #194 A little progressive enhancement as a treat The Index: Issue #193 Working with ::highlight() using progressive enhancement The Index: Issue #192 Getting started with implementing my music collection The Index: Issue #191 Use cases for aria-expanded The Index: Issue #190 Proxy and Reflect The Index: Issue #189 Loading AT protocol posts data The Index: Issue #188 Publishing on the Atmosphere with Standard.site The Index: Issue #187 The Index: Issue #186 The Index: Issue #185 A Front-end developer’s guide to the hybrid mobile app development landscape The Index: Issue #184 Navigating the age-old problem of checkmarks in UI with progressive enhancement The Index: Issue #183 Framework-agnostic design systems: a practical approach to web components The Index: Issue #182 Completing the WordPress headless CMS integration
Rendering AT protocol posts on my /feed
AuthorAndy Bell · 2026-07-02 · via Piccalilli - Everything

I’m again, going to be doing a lot of the same sort of work I did for the WordPress integration, but as this is the first iteration of the AT protocol, it’s going to be a lot simpler than that.

Just like with the WordPress integration, I used Astro’s pagination capabilities to build a paginated “feed” of posts, to satisfy the “Basic rendering of my AT protocol posts” part of the core features I outlined at the beginning of this series.

An Obsidian markdown file called "core features and iterations." It lists a development roadmap across four iterations, including tasks like "basic shell version of the site," "look and feel design," "AT protocol integration," and "last.fm integration."

Let’s get stuck into that bit first. I created a pages/feed/[...page].astro file and filled it with the following:

---
import { fetchAllATPosts } from '@repo/data/atPosts';

import PageLayout from 'src/layouts/PageLayout.astro';
import Pagination from '@repo/ui/Pagination';
import ATPostsRegion from '@repo/ui/ATPostsRegion';

export async function getStaticPaths({ paginate }) {
  const posts = await fetchAllATPosts();
  return paginate(posts, { pageSize: 20 });
}

const { page } = Astro.props;
---

<PageLayout title="Feed" summary="" socialImage="" allowRobots={true}>
  <ATPostsRegion posts={page.data} />
  <Pagination previous={page.url.prev} next={page.url.next} />
</PageLayout>

I’m getting all the posts with the functionality written in the last article, limiting them to 20 per page and then instructing Astro what pages need building with getStaticPaths().

From there and for each page, I’ve got a page prop, which just like with the WordPress integration, I’m feeding to the <Pagination> component. I’m also feeding that data to the <ATPostsRegion> which I’ll break down next.

AdvertSave 20% on all of our courses using the code NEXTLEVEL

The AT posts region

I didn’t break down the <PostsRegion> in the WordPress section of this series because it is literally just a list of links, but this one is slightly different, but albeit simple. We are in the early days of this rebuild after all!

---
const { posts } = Astro.props;

import ATPost from '@repo/ui/ATPost';
---

<div class="at-posts-region region">
  <h1 class="visually-hidden">Feed</h1>
  <div class="wrapper flow">
    <p>ℹ️ Posts from my <a href="https://bsky.app/profile/bell.bz">Bluesky profile</a>.</p>
    <ul class="at-posts-region__list" role="list">
      {
        posts.map((post) => (
          <li class="flow">
            <ATPost post={post} />
          </li>
        ))
      }
    </ul>
  </div>
</div>

The main reason I wanted to tackle this region was because of that visually hidden <h1>. First here’s the page, at the time of writing.

My feed page, looking very basic, showing posts in chronological order

I didn’t want a big ol’ heading on this page (at least initially), but I do need a top level heading, for assistive tech users, so my ever-useful CSS utility helps a tonne here.

The rest of this file is pretty self explanatory, so let’s dig into the component that renders each post.

---
import {
  formatDate,
  convertATPrototocalURIToBlueskyURL,
} from '@repo/utils/helpers';

import MarkdownText from '@repo/ui/MarkdownText';

const { post } = Astro.props;
---

<div class="at-post flow">
  <MarkdownText content={post.content} className="flow" />
  {
    post.media.length
      ? post.media.map((media) => (
          <>
            {media.type === 'image' && <img src={media.src} alt={media.alt} />}

            {/* Like images, but this is specifically an open graph image */}
            {media.type === 'external' && media.thumb && (
              <p>
                <a href={media.uri}>
                  <img src={media.thumb} alt={media.title} />
                </a>
              </p>
            )}

            {media.type === 'video' && (
              <video width="352" height="198" controls poster={media.thumbnail}>
                <source src={media.playlist} type="application/x-mpegURL" />
              </video>
            )}
          </>
        ))
      : null
  }

  <p class="at-post__meta">
    <time datetime={post.date}>{formatDate(post.date, true)}</time>
  </p>

  <dl class="at-post__stats cluster">
    <dt>Likes</dt>
    <dd>{post.likes}</dd>
    <dt>Reposts</dt>
    <dd>{post.reposts}</dd>
    <dt>Replies</dt>
    <dd>{post.replies}</dd>
  </dl>

  <p class="at-post__original-link">
    <a href={convertATPrototocalURIToBlueskyURL(post.uri)}>Original</a>
  </p>
</div>

The first thing I do here is render the markdown text, generated in the last article, with my existing component. With the easy part done, I swiftly move on to looping over the post.media array and rendering appropriate markdown per embed type.

Following that, the post’s date is rendered with the <time> element with like, repost and reply count rightly using a description list (<dl>) element to articulate the data appropriately.

The last part is a link out to Bluesky, which I’ll bring in to show you:

/**
 * Converts an AT Protocol URI to a Bluesky Web URL
 * @param {string} atUri - The at:// uri (e.g., at://did:plc.../app.bsky.feed.post/...)
 * @returns {string} The formatted bsky.app URL
 */
export function convertATPrototocalURIToBlueskyURL(uri) {
  // Pattern: at://(DID)/(COLLECTION)/(RKEY)
  const regex = /^at:\/\/(did:[^/]+)\/app\.bsky\.feed\.post\/([^/]+)$/;
  const match = uri.match(regex);

  if (!match) {
    return 'Invalid AT Protocol post URI';
  }

  // Matches are in order because of array destructuring, so we use `_` to capture the un-needed part
  const [_, did, rkey] = match;
  return `https://bsky.app/profile/${did}/post/${rkey}`;
}

The aim of the game with this utility is to first match the parts of an AT protocol URI, then use those parts to return a web URL. Because every record in your Personal Data Server (PDS) has a DID (user ID) and rkey, it’s a case of applying those URL parts and Bluesky does the rest. Handy.

AdvertSave 20% on all courses using the code NEXTLEVEL.

Wrapping up

That’s it! The AT protocol stuff looks incredibly complicated on the surface, but once you understand how PDS records work, it’s a really elegant, straightforward system.

There will be a full AT protocol focused iteration of this website, once I’ve designed and implemented the new UI, so this stuff serves as a basic basis to build on. Hopefully it’ll make the idea of implementing the AT protocol on your website more appealing too.

With AT protocol done, let’s move on to my favourite part: the music collection, next.

Enjoyed this article? You can support us by leaving a tip via Open Collective