annot.at is a service for syncing static websites, such as blogs, to atproto and standard.site annot.at
elixir atproto standardsite
3

Configure Feed

Select the types of activity you want to include in your feed.

Adopt existing documents

Now that we fetch the aturi from the page links we can "discover" the documents for them. For now we just assume it's correct and the user can choose to republish if they wish, but in the future we could also fix broken records and stuff. Neat.

Johanna Larsson (Jun 27, 2026, 10:36 PM +0100) 4c12500b 96d2cfa6

+31 -2
+13
lib/annot_at/publishing.ex
··· 10 10 alias AnnotAt.Accounts.Scope 11 11 alias AnnotAt.Accounts.User 12 12 alias AnnotAt.Atproto.TID 13 + alias AnnotAt.Feeds.Entry 13 14 alias AnnotAt.Publishing.Post 14 15 alias AnnotAt.Publishing.Site 15 16 alias AnnotAt.Repo ··· 96 97 post 97 98 |> Post.changeset(attrs) 98 99 |> Repo.update() 100 + end 101 + 102 + def adopt(%Site{} = site, entries) do 103 + tracked = MapSet.new(list_posts(site), & &1.guid) 104 + 105 + entries 106 + |> Enum.filter(fn entry -> is_binary(entry.rkey) and entry.id not in tracked end) 107 + |> Enum.each(fn entry -> 108 + create_post(site, %{guid: entry.id, rkey: entry.rkey, content_hash: Entry.hash(entry)}) 109 + end) 110 + 111 + Map.new(list_posts(site), &{&1.guid, &1}) 99 112 end 100 113 101 114 defp verify_user_ownership!(%Site{user_id: user_id}, user_id), do: :ok
+18 -2
lib/annot_at_web/controllers/live/posts_live.ex
··· 307 307 {:noreply, socket} 308 308 end 309 309 310 + def handle_async(:load_feed, {:ok, {:ok, %{feed: feed, posts: posts}}}, socket) do 311 + {:noreply, 312 + assign(socket, 313 + feed: AsyncResult.ok(socket.assigns.feed, feed), 314 + posts: posts 315 + )} 316 + end 317 + 318 + def handle_async(:load_feed, {:ok, reason}, socket) do 319 + Logger.warning("PostsLive: failed to load feed", reason: inspect(reason)) 320 + {:noreply, assign(socket, feed: AsyncResult.failed(socket.assigns.feed, reason))} 321 + end 322 + 310 323 defp load_feed(socket, site) do 311 324 if connected?(socket) do 312 325 user_did = socket.assigns.current_scope.user.did 313 326 314 - assign_async(socket, :feed, fn -> 327 + socket 328 + |> assign(feed: AsyncResult.loading()) 329 + |> start_async(:load_feed, fn -> 315 330 with {:ok, feed} <- Client.load(site.feed_url) do 316 331 feed = Client.resolve_documents(feed, user_did) 317 - {:ok, %{feed: feed}} 332 + posts = Publishing.adopt(site, feed.entries) 333 + {:ok, %{feed: feed, posts: posts}} 318 334 end 319 335 end) 320 336 else