A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.

feat: improve record fetching, use slingshot and bluesky appview. ---

+7 -1
lib/components/BlueskyPostList.tsx
···
const actorLabel = resolvedHandle ?? formatDid(did);
const actorPath = resolvedHandle ?? resolvedDid ?? did;
-
const { records, loading, error, hasNext, hasPrev, loadNext, loadPrev, pageIndex, pagesCount } = usePaginatedRecords<FeedPostRecord>({ did, collection: 'app.bsky.feed.post', limit });
+
const { records, loading, error, hasNext, hasPrev, loadNext, loadPrev, pageIndex, pagesCount } = usePaginatedRecords<FeedPostRecord>({
+
did,
+
collection: 'app.bsky.feed.post',
+
limit,
+
preferAuthorFeed: true,
+
authorFeedActor: actorPath
+
});
const pageLabel = useMemo(() => {
const knownTotal = Math.max(pageIndex + 1, pagesCount);
+1 -1
lib/components/LeafletDocument.tsx
···
const href = parsed ? toBlueskyPostUrl(parsed) : undefined;
if (href) return href;
}
-
return `https://bsky.app/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
+
return `at://${encodeURIComponent(did)}/${LEAFLET_DOCUMENT_COLLECTION}/${encodeURIComponent(rkey)}`;
}
export default LeafletDocument;
+33 -9
lib/core/AtProtoRecord.tsx
···
import React from 'react';
import { useAtProtoRecord } from '../hooks/useAtProtoRecord';
-
export interface AtProtoRecordProps<T = unknown> {
-
did: string;
-
collection: string;
-
rkey: string;
+
interface AtProtoRecordRenderProps<T> {
renderer?: React.ComponentType<{ record: T; loading: boolean; error?: Error }>;
fallback?: React.ReactNode;
loadingIndicator?: React.ReactNode;
}
-
export function AtProtoRecord<T = unknown>({ did, collection, rkey, renderer: Renderer, fallback = null, loadingIndicator = 'Loading…' }: AtProtoRecordProps<T>) {
-
const { record, error, loading } = useAtProtoRecord<T>({ did, collection, rkey });
+
type AtProtoRecordFetchProps<T> = AtProtoRecordRenderProps<T> & {
+
did: string;
+
collection: string;
+
rkey: string;
+
record?: undefined;
+
};
-
if (error) return <>{fallback}</>;
-
if (!record) return <>{loading ? loadingIndicator : fallback}</>;
-
if (Renderer) return <Renderer record={record} loading={loading} error={error} />;
+
type AtProtoRecordProvidedRecordProps<T> = AtProtoRecordRenderProps<T> & {
+
record: T;
+
did?: string;
+
collection?: string;
+
rkey?: string;
+
};
+
+
export type AtProtoRecordProps<T = unknown> = AtProtoRecordFetchProps<T> | AtProtoRecordProvidedRecordProps<T>;
+
+
export function AtProtoRecord<T = unknown>(props: AtProtoRecordProps<T>) {
+
const { renderer: Renderer, fallback = null, loadingIndicator = 'Loading…' } = props;
+
const hasProvidedRecord = 'record' in props;
+
const providedRecord = hasProvidedRecord ? props.record : undefined;
+
+
const { record: fetchedRecord, error, loading } = useAtProtoRecord<T>({
+
did: hasProvidedRecord ? undefined : props.did,
+
collection: hasProvidedRecord ? undefined : props.collection,
+
rkey: hasProvidedRecord ? undefined : props.rkey,
+
});
+
+
const record = providedRecord ?? fetchedRecord;
+
const isLoading = loading && !providedRecord;
+
+
if (error && !record) return <>{fallback}</>;
+
if (!record) return <>{isLoading ? loadingIndicator : fallback}</>;
+
if (Renderer) return <Renderer record={record} loading={isLoading} error={error} />;
return <pre style={{ fontSize: 12, padding: 8, background: '#f5f5f5', overflow: 'auto' }}>{JSON.stringify(record, null, 2)}</pre>;
}
+5 -4
lib/hooks/useAtProtoRecord.ts
···
/** Repository DID (or handle prior to resolution) containing the record. */
did?: string;
/** NSID collection in which the record resides. */
-
collection: string;
+
collection?: string;
/** Record key string uniquely identifying the record within the collection. */
-
rkey: string;
+
rkey?: string;
}
/**
···
setState(prev => ({ ...prev, ...next }));
};
-
if (!handleOrDid) {
+
if (!handleOrDid || !collection || !rkey) {
assignState({ loading: false, record: undefined, error: undefined });
return () => { cancelled = true; };
}
···
const record = (res.data as { value: T }).value;
assignState({ record, loading: false });
} catch (e) {
-
assignState({ error: e as Error, loading: false });
+
const err = e instanceof Error ? e : new Error(String(e));
+
assignState({ error: err, loading: false });
}
})();
+161 -38
lib/hooks/usePaginatedRecords.ts
···
collection: string;
/** Maximum page size to request; defaults to `5`. */
limit?: number;
+
/** Prefer the Bluesky appview author feed endpoint before falling back to the PDS. */
+
preferAuthorFeed?: boolean;
+
/** Optional filter applied when fetching from the appview author feed. */
+
authorFeedFilter?: AuthorFeedFilter;
+
/** Whether to include pinned posts when fetching from the author feed. */
+
authorFeedIncludePins?: boolean;
+
/** Override for the appview service base URL used to query the author feed. */
+
authorFeedService?: string;
+
/** Optional explicit actor identifier for the author feed request. */
+
authorFeedActor?: string;
}
/**
···
pagesCount: number;
}
+
const DEFAULT_APPVIEW_SERVICE = 'https://public.api.bsky.app';
+
+
export type AuthorFeedFilter =
+
| 'posts_with_replies'
+
| 'posts_no_replies'
+
| 'posts_with_media'
+
| 'posts_and_author_threads'
+
| 'posts_with_video';
+
/**
* React hook that fetches a repository collection with cursor-based pagination and prefetching.
*
···
* @param limit - Maximum number of records to request per page. Defaults to `5`.
* @returns {UsePaginatedRecordsResult<T>} Object containing the current page, pagination metadata, and navigation callbacks.
*/
-
export function usePaginatedRecords<T>({ did: handleOrDid, collection, limit = 5 }: UsePaginatedRecordsOptions): UsePaginatedRecordsResult<T> {
-
const { did, error: didError, loading: resolvingDid } = useDidResolution(handleOrDid);
+
export function usePaginatedRecords<T>({
+
did: handleOrDid,
+
collection,
+
limit = 5,
+
preferAuthorFeed = false,
+
authorFeedFilter,
+
authorFeedIncludePins,
+
authorFeedService,
+
authorFeedActor
+
}: UsePaginatedRecordsOptions): UsePaginatedRecordsResult<T> {
+
const { did, handle, error: didError, loading: resolvingDid } = useDidResolution(handleOrDid);
const { endpoint, error: endpointError, loading: resolvingEndpoint } = usePdsEndpoint(did);
const [pages, setPages] = useState<PageData<T>[]>([]);
const [pageIndex, setPageIndex] = useState(0);
···
const [error, setError] = useState<Error | undefined>(undefined);
const inFlight = useRef<Set<string>>(new Set());
const requestSeq = useRef(0);
+
const identityRef = useRef<string | undefined>(undefined);
+
const feedDisabledRef = useRef(false);
+
+
const identity = did && endpoint ? `${did}::${endpoint}` : undefined;
+
const normalizedInput = useMemo(() => {
+
if (!handleOrDid) return undefined;
+
const trimmed = handleOrDid.trim();
+
return trimmed || undefined;
+
}, [handleOrDid]);
+
+
const actorIdentifier = useMemo(() => {
+
const explicit = authorFeedActor?.trim();
+
if (explicit) return explicit;
+
if (handle) return handle;
+
if (normalizedInput) return normalizedInput;
+
if (did) return did;
+
return undefined;
+
}, [authorFeedActor, handle, normalizedInput, did]);
const resetState = useCallback(() => {
setPages([]);
···
setError(undefined);
inFlight.current.clear();
requestSeq.current += 1;
+
feedDisabledRef.current = false;
}, []);
-
const fetchPage = useCallback(async (cursor: string | undefined, targetIndex: number, mode: 'active' | 'prefetch') => {
+
const fetchPage = useCallback(async (identityKey: string, cursor: string | undefined, targetIndex: number, mode: 'active' | 'prefetch') => {
if (!did || !endpoint) return;
+
const currentIdentity = `${did}::${endpoint}`;
+
if (identityKey !== currentIdentity) return;
const token = requestSeq.current;
-
const key = `${targetIndex}:${cursor ?? 'start'}`;
+
const key = `${identityKey}:${targetIndex}:${cursor ?? 'start'}`;
if (inFlight.current.has(key)) return;
inFlight.current.add(key);
if (mode === 'active') {
···
setError(undefined);
}
try {
-
const { rpc } = await createAtprotoClient({ service: endpoint });
-
const res = await (rpc as unknown as {
-
get: (
-
nsid: string,
-
opts: { params: Record<string, string | number | boolean | undefined> }
-
) => Promise<{ ok: boolean; data: { records: Array<{ uri: string; rkey?: string; value: T }>; cursor?: string } }>;
-
}).get('com.atproto.repo.listRecords', {
-
params: {
-
repo: did,
-
collection,
-
limit,
-
cursor,
-
reverse: false
+
let nextCursor: string | undefined;
+
let mapped: PaginatedRecord<T>[] | undefined;
+
+
const shouldUseAuthorFeed = preferAuthorFeed && collection === 'app.bsky.feed.post' && !feedDisabledRef.current && !!actorIdentifier;
+
if (shouldUseAuthorFeed) {
+
try {
+
const { rpc } = await createAtprotoClient({ service: authorFeedService ?? DEFAULT_APPVIEW_SERVICE });
+
const res = await (rpc as unknown as {
+
get: (
+
nsid: string,
+
opts: { params: Record<string, string | number | boolean | undefined> }
+
) => Promise<{ ok: boolean; data: { feed?: Array<{ post?: { uri?: string; record?: T } }>; cursor?: string } }>;
+
}).get('app.bsky.feed.getAuthorFeed', {
+
params: {
+
actor: actorIdentifier,
+
limit,
+
cursor,
+
filter: authorFeedFilter,
+
includePins: authorFeedIncludePins
+
}
+
});
+
if (!res.ok) throw new Error('Failed to fetch author feed');
+
const { feed, cursor: feedCursor } = res.data;
+
mapped = (feed ?? []).reduce<PaginatedRecord<T>[]>((acc, item) => {
+
const post = item?.post;
+
if (!post || typeof post.uri !== 'string' || !post.record) return acc;
+
acc.push({
+
uri: post.uri,
+
rkey: extractRkey(post.uri),
+
value: post.record as T
+
});
+
return acc;
+
}, []);
+
nextCursor = feedCursor;
+
} catch (err) {
+
feedDisabledRef.current = true;
+
if (process.env.NODE_ENV !== 'production') {
+
console.warn('[usePaginatedRecords] Author feed unavailable, falling back to PDS', err);
+
}
}
-
});
-
if (!res.ok) throw new Error('Failed to list records');
-
const { records, cursor: nextCursor } = res.data;
-
const mapped: PaginatedRecord<T>[] = records.map((item) => ({
-
uri: item.uri,
-
rkey: item.rkey ?? extractRkey(item.uri),
-
value: item.value
-
}));
-
if (token !== requestSeq.current) {
+
}
+
+
if (!mapped) {
+
const { rpc } = await createAtprotoClient({ service: endpoint });
+
const res = await (rpc as unknown as {
+
get: (
+
nsid: string,
+
opts: { params: Record<string, string | number | boolean | undefined> }
+
) => Promise<{ ok: boolean; data: { records: Array<{ uri: string; rkey?: string; value: T }>; cursor?: string } }>;
+
}).get('com.atproto.repo.listRecords', {
+
params: {
+
repo: did,
+
collection,
+
limit,
+
cursor,
+
reverse: false
+
}
+
});
+
if (!res.ok) throw new Error('Failed to list records');
+
const { records, cursor: repoCursor } = res.data;
+
mapped = records.map((item) => ({
+
uri: item.uri,
+
rkey: item.rkey ?? extractRkey(item.uri),
+
value: item.value
+
}));
+
nextCursor = repoCursor;
+
}
+
+
if (token !== requestSeq.current || identityKey !== identityRef.current) {
return nextCursor;
}
if (mode === 'active') setPageIndex(targetIndex);
setPages(prev => {
const next = [...prev];
-
next[targetIndex] = { records: mapped, cursor: nextCursor };
+
next[targetIndex] = { records: mapped!, cursor: nextCursor };
return next;
});
return nextCursor;
} catch (e) {
-
if (mode === 'active') setError(e as Error);
+
if (mode === 'active' && token === requestSeq.current && identityKey === identityRef.current) {
+
setError(e as Error);
+
}
} finally {
-
if (mode === 'active') setLoading(false);
+
if (mode === 'active' && token === requestSeq.current && identityKey === identityRef.current) {
+
setLoading(false);
+
}
inFlight.current.delete(key);
}
return undefined;
-
}, [did, endpoint, collection, limit]);
+
}, [
+
did,
+
endpoint,
+
collection,
+
limit,
+
preferAuthorFeed,
+
actorIdentifier,
+
authorFeedService,
+
authorFeedFilter,
+
authorFeedIncludePins
+
]);
useEffect(() => {
if (!handleOrDid) {
+
identityRef.current = undefined;
resetState();
setLoading(false);
setError(undefined);
···
}
if (didError) {
+
identityRef.current = undefined;
resetState();
setLoading(false);
setError(didError);
···
}
if (endpointError) {
+
identityRef.current = undefined;
resetState();
setLoading(false);
setError(endpointError);
return;
}
-
if (resolvingDid || resolvingEndpoint || !did || !endpoint) {
-
resetState();
-
setLoading(true);
+
if (resolvingDid || resolvingEndpoint || !identity) {
+
if (identityRef.current !== identity) {
+
identityRef.current = identity;
+
resetState();
+
}
+
setLoading(!!handleOrDid);
setError(undefined);
return;
}
-
resetState();
-
fetchPage(undefined, 0, 'active').catch(() => {
+
if (identityRef.current !== identity) {
+
identityRef.current = identity;
+
resetState();
+
}
+
+
fetchPage(identity, undefined, 0, 'active').catch(() => {
/* error handled in state */
});
-
}, [handleOrDid, did, endpoint, fetchPage, resetState, resolvingDid, resolvingEndpoint, didError, endpointError]);
+
}, [handleOrDid, identity, fetchPage, resetState, resolvingDid, resolvingEndpoint, didError, endpointError]);
const currentPage = pages[pageIndex];
const hasNext = !!currentPage?.cursor || !!pages[pageIndex + 1];
const hasPrev = pageIndex > 0;
const loadNext = useCallback(() => {
+
const identityKey = identityRef.current;
+
if (!identityKey) return;
const page = pages[pageIndex];
if (!page?.cursor && !pages[pageIndex + 1]) return;
if (pages[pageIndex + 1]) {
setPageIndex(pageIndex + 1);
return;
}
-
fetchPage(page.cursor, pageIndex + 1, 'active').catch(() => {
+
fetchPage(identityKey, page.cursor, pageIndex + 1, 'active').catch(() => {
/* handled via error state */
});
}, [fetchPage, pageIndex, pages]);
···
const cursor = pages[pageIndex]?.cursor;
if (!cursor) return;
if (pages[pageIndex + 1]) return;
-
fetchPage(cursor, pageIndex + 1, 'prefetch').catch(() => {
+
const identityKey = identityRef.current;
+
if (!identityKey) return;
+
fetchPage(identityKey, cursor, pageIndex + 1, 'prefetch').catch(() => {
/* ignore prefetch errors */
});
}, [fetchPage, pageIndex, pages]);
+2 -1
lib/renderers/BlueskyPostRenderer.tsx
···
},
text: {
margin: 0,
-
whiteSpace: 'pre-wrap'
+
whiteSpace: 'pre-wrap',
+
overflowWrap: 'anywhere'
},
facets: {
marginTop: 8,
+104 -16
lib/utils/atproto-client.ts
···
-
import { Client, simpleFetchHandler } from '@atcute/client';
+
import { Client, simpleFetchHandler, type FetchHandler } from '@atcute/client';
import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver, XrpcHandleResolver } from '@atcute/identity-resolver';
import type { DidDocument } from '@atcute/identity';
import type { Did, Handle } from '@atcute/lexicons/syntax';
···
const SUPPORTED_DID_METHODS = ['plc', 'web'] as const;
type SupportedDidMethod = (typeof SUPPORTED_DID_METHODS)[number];
type SupportedDid = Did<SupportedDidMethod>;
+
+
export const SLINGSHOT_BASE_URL = 'https://slingshot.microcosm.blue';
export const normalizeBaseUrl = (input: string): string => {
const trimmed = input.trim();
···
private plc: string;
private didResolver: CompositeDidDocumentResolver<SupportedDidMethod>;
private handleResolver: XrpcHandleResolver;
+
private fetchImpl: typeof fetch;
constructor(opts: ServiceResolverOptions = {}) {
const plcSource = opts.plcDirectory && opts.plcDirectory.trim() ? opts.plcDirectory : DEFAULT_PLC;
const identitySource = opts.identityService && opts.identityService.trim() ? opts.identityService : DEFAULT_IDENTITY_SERVICE;
this.plc = normalizeBaseUrl(plcSource);
const identityBase = normalizeBaseUrl(identitySource);
-
const fetchImpl = opts.fetch ?? fetch;
-
const plcResolver = new PlcDidDocumentResolver({ apiUrl: this.plc, fetch: fetchImpl });
-
const webResolver = new WebDidDocumentResolver({ fetch: fetchImpl });
+
this.fetchImpl = bindFetch(opts.fetch);
+
const plcResolver = new PlcDidDocumentResolver({ apiUrl: this.plc, fetch: this.fetchImpl });
+
const webResolver = new WebDidDocumentResolver({ fetch: this.fetchImpl });
this.didResolver = new CompositeDidDocumentResolver({ methods: { plc: plcResolver, web: webResolver } });
-
this.handleResolver = new XrpcHandleResolver({ serviceUrl: identityBase, fetch: fetchImpl });
+
this.handleResolver = new XrpcHandleResolver({ serviceUrl: identityBase, fetch: this.fetchImpl });
}
async resolveDidDoc(did: string): Promise<DidDocument> {
···
async resolveHandle(handle: string): Promise<string> {
const normalized = handle.trim().toLowerCase();
if (!normalized) throw new Error('Handle cannot be empty');
-
return this.handleResolver.resolve(normalized as Handle);
+
let slingshotError: Error | undefined;
+
try {
+
const url = new URL('/xrpc/com.atproto.identity.resolveHandle', SLINGSHOT_BASE_URL);
+
url.searchParams.set('handle', normalized);
+
const response = await this.fetchImpl(url);
+
if (response.ok) {
+
const payload = await response.json() as { did?: string } | null;
+
if (payload?.did) {
+
console.info('[slingshot] resolveHandle cache hit', { handle: normalized });
+
return payload.did;
+
}
+
slingshotError = new Error('Slingshot resolveHandle response missing DID');
+
console.warn('[slingshot] resolveHandle payload missing DID; falling back', { handle: normalized });
+
} else {
+
slingshotError = new Error(`Slingshot resolveHandle failed with status ${response.status}`);
+
const body = response.body;
+
if (body) {
+
body.cancel().catch(() => {});
+
}
+
console.info('[slingshot] resolveHandle cache miss', { handle: normalized, status: response.status });
+
}
+
} catch (err) {
+
if (err instanceof DOMException && err.name === 'AbortError') throw err;
+
slingshotError = err instanceof Error ? err : new Error(String(err));
+
console.warn('[slingshot] resolveHandle error; falling back to identity service', { handle: normalized, error: slingshotError });
+
}
+
+
try {
+
const did = await this.handleResolver.resolve(normalized as Handle);
+
if (slingshotError) {
+
console.info('[slingshot] resolveHandle fallback succeeded', { handle: normalized });
+
}
+
return did;
+
} catch (err) {
+
if (slingshotError && err instanceof Error) {
+
const prior = err.message;
+
err.message = `${prior}; Slingshot resolveHandle failed: ${slingshotError.message}`;
+
if (slingshotError) {
+
console.warn('[slingshot] resolveHandle fallback failed', { handle: normalized, error: slingshotError });
+
}
+
}
+
throw err;
+
}
}
}
export interface CreateClientOptions extends ServiceResolverOptions {
-
did?: string; // optional to create a DID-scoped client
-
service?: string; // override service base url
+
did?: string; // optional to create a DID-scoped client
+
service?: string; // override service base url
}
export async function createAtprotoClient(opts: CreateClientOptions = {}) {
-
let service = opts.service;
-
const resolver = new ServiceResolver(opts);
-
if (!service && opts.did) {
-
service = await resolver.pdsEndpointForDid(opts.did);
-
}
+
const fetchImpl = bindFetch(opts.fetch);
+
let service = opts.service;
+
const resolver = new ServiceResolver({ ...opts, fetch: fetchImpl });
+
if (!service && opts.did) {
+
service = await resolver.pdsEndpointForDid(opts.did);
+
}
if (!service) throw new Error('service or did required');
-
const handler = simpleFetchHandler({ service: normalizeBaseUrl(service) });
-
const rpc = new Client({ handler });
-
return { rpc, service, resolver };
+
const normalizedService = normalizeBaseUrl(service);
+
const handler = createSlingshotAwareHandler(normalizedService, fetchImpl);
+
const rpc = new Client({ handler });
+
return { rpc, service: normalizedService, resolver };
}
export type AtprotoClient = Awaited<ReturnType<typeof createAtprotoClient>>['rpc'];
+
+
const SLINGSHOT_RETRY_PATHS = [
+
'/xrpc/com.atproto.repo.getRecord',
+
'/xrpc/com.atproto.identity.resolveHandle',
+
];
+
+
function createSlingshotAwareHandler(service: string, fetchImpl: typeof fetch): FetchHandler {
+
const primary = simpleFetchHandler({ service, fetch: fetchImpl });
+
const slingshot = simpleFetchHandler({ service: SLINGSHOT_BASE_URL, fetch: fetchImpl });
+
return async (pathname, init) => {
+
const matched = SLINGSHOT_RETRY_PATHS.find(candidate => pathname === candidate || pathname.startsWith(`${candidate}?`));
+
if (matched) {
+
try {
+
const slingshotResponse = await slingshot(pathname, init);
+
if (slingshotResponse.ok) {
+
console.info(`[slingshot] cache hit for ${matched}`);
+
return slingshotResponse;
+
}
+
const body = slingshotResponse.body;
+
if (body) {
+
body.cancel().catch(() => {});
+
}
+
console.info(`[slingshot] cache miss ${slingshotResponse.status} for ${matched}, falling back to ${service}`);
+
} catch (err) {
+
if (err instanceof DOMException && err.name === 'AbortError') {
+
throw err;
+
}
+
console.warn(`[slingshot] fetch error for ${matched}, falling back to ${service}`, err);
+
}
+
}
+
return primary(pathname, init);
+
};
+
}
+
+
function bindFetch(fetchImpl?: typeof fetch): typeof fetch {
+
const impl = fetchImpl ?? globalThis.fetch;
+
if (typeof impl !== 'function') {
+
throw new Error('fetch implementation not available');
+
}
+
return impl.bind(globalThis);
+
}
+1 -1
package.json
···
{
"name": "atproto-ui",
-
"version": "0.2.1",
+
"version": "0.3.0",
"type": "module",
"description": "React components and hooks for rendering AT Protocol records.",
"main": "./lib-dist/index.js",
+9 -7
src/App.tsx
···
};
const LatestPostSummary: React.FC<{ did: string; handle?: string; colorScheme: ColorSchemePreference }> = ({ did, colorScheme }) => {
-
const { rkey, loading, error } = useLatestRecord<FeedPostRecord>(did, BLUESKY_POST_COLLECTION);
+
const { record, rkey, loading, error } = useLatestRecord<FeedPostRecord>(did, BLUESKY_POST_COLLECTION);
const scheme = useColorScheme(colorScheme);
const palette = scheme === 'dark' ? latestSummaryPalette.dark : latestSummaryPalette.light;
···
if (error) return <div style={palette.error}>Failed to load the latest post.</div>;
if (!rkey) return <div style={palette.muted}>No posts published yet.</div>;
+
const atProtoProps = record
+
? { record }
+
: { did, collection: 'app.bsky.feed.post', rkey };
+
return (
-
<AtProtoRecord<FeedPostRecord>
-
did={did}
-
collection="app.bsky.feed.post"
-
rkey={rkey}
-
renderer={({ record }) => (
+
<AtProtoRecord<FeedPostRecord>
+
{...atProtoProps}
+
renderer={({ record: resolvedRecord }) => (
<article data-color-scheme={scheme}>
-
<strong>{record?.text ?? 'Empty post'}</strong>
+
<strong>{resolvedRecord?.text ?? 'Empty post'}</strong>
</article>
)}
/>