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

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Last Watchdog
The Last Watchdog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
C
Check Point Blog
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
V
Vulnerabilities – Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
S
Security @ Cisco Blogs
IT之家
IT之家
T
The Exploit Database - CXSecurity.com
The GitHub Blog
The GitHub Blog
D
Docker
Engineering at Meta
Engineering at Meta
AWS News Blog
AWS News Blog
S
Security Affairs
U
Unit 42
P
Palo Alto Networks Blog
V
Visual Studio Blog
Y
Y Combinator Blog
D
DataBreaches.Net
Forbes - Security
Forbes - Security
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
Security Latest
Security Latest
aimingoo的专栏
aimingoo的专栏
Simon Willison's Weblog
Simon Willison's Weblog
A
Arctic Wolf
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hacker News: Front Page
博客园 - 司徒正美
博客园 - Franky
宝玉的分享
宝玉的分享
TaoSecurity Blog
TaoSecurity Blog
Latest news
Latest news
Scott Helme
Scott Helme
MongoDB | Blog
MongoDB | Blog
量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
P
Privacy International News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog

Piccalilli - Everything

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 Getting started with the HTML only build
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