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.

Add the ability to disconnect sites

Disconnecting, or deleting, a site removes our local state of the site without touching any publications or documents.

Also fixed the broken filter buttons on sites

Johanna Larsson (Jul 3, 2026, 4:16 PM +0100) 142a12de 8d479536

+44 -21
+6
lib/annot_at/publishing.ex
··· 83 83 |> Repo.update() 84 84 end 85 85 86 + def delete_site(%Scope{user: %User{id: user_id}}, %Site{} = site) do 87 + verify_user_ownership!(site, user_id) 88 + 89 + Repo.delete(site) 90 + end 91 + 86 92 def list_posts(%Site{id: site_id}) do 87 93 Repo.all(from p in Post, where: p.site_id == ^site_id, order_by: [desc: p.inserted_at]) 88 94 end
+3 -3
lib/annot_at/publishing/site.ex
··· 46 46 |> foreign_key_constraint(:user_id) 47 47 end 48 48 49 - def status(%__MODULE__{published_at: %DateTime{}}), do: :published 50 - def status(%__MODULE__{verified_at: %DateTime{}}), do: :verified 51 - def status(%__MODULE__{}), do: :draft 49 + def status(%__MODULE__{published_at: %DateTime{}}), do: "published" 50 + def status(%__MODULE__{verified_at: %DateTime{}}), do: "verified" 51 + def status(%__MODULE__{}), do: "draft" 52 52 end
+4 -6
lib/annot_at_web/components/core_components.ex
··· 91 91 value disabled) 92 92 attr :class, :any, default: nil 93 93 attr :variant, :string, default: "ghost", values: ~w(primary secondary accent 94 - ghost) 94 + ghost danger) 95 95 attr :shadow, :string, default: nil 96 96 attr :size, :string, default: "md", values: ~w(sm md lg) 97 97 slot :inner_block, required: true ··· 101 101 "primary" => "bg-ink text-paper", 102 102 "secondary" => "bg-peach-bold text-ink", 103 103 "accent" => "bg-sky-bold text-ink", 104 - "ghost" => "bg-paper text-ink hover:bg-ink/5" 104 + "ghost" => "bg-paper text-ink hover:bg-ink/5", 105 + "danger" => "bg-paper hover:text-red-600 hover:bg-red-50" 105 106 } 106 107 107 108 shadows = %{ ··· 119 120 120 121 assigns = 121 122 assign(assigns, :class, [ 122 - "inline-flex cursor-pointer items-center justify-center gap-1.5 123 - rounded-xl border-2 border-ink font-bold transition-all hover:-translate-y-0.5 124 - active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-50 125 - disabled:shadow-none disabled:hover:translate-y-0", 123 + "inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-xl border-2 border-ink font-bold transition-all active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-50 disabled:shadow-none", 126 124 Map.fetch!(sizes, assigns[:size]), 127 125 Map.fetch!(variants, assigns[:variant]), 128 126 Map.fetch!(shadows, assigns[:shadow]),
+1 -1
lib/annot_at_web/components/site_components.ex
··· 42 42 @status == :verified && "bg-sky-light", 43 43 @status == :published && "bg-sky-bold" 44 44 ]}> 45 - {@status |> Atom.to_string() |> String.capitalize()} 45 + {String.capitalize(@status)} 46 46 </span> 47 47 """ 48 48 end
+29 -10
lib/annot_at_web/controllers/live/site_live.ex
··· 25 25 <h1 class="font-display text-3xl font-bold 26 26 tracking-tight">{@site.url}</h1> 27 27 28 - <div :if={phase(@site) == :done} class="flex items-center gap-3"> 29 - <span class="inline-flex items-center gap-1.5 rounded-full border-2 30 - border-ink bg-paper px-3 py-1.5 text-sm font-bold"> 28 + <div class="flex items-center gap-3"> 29 + <span 30 + :if={phase(@site) == :done} 31 + class="inline-flex items-center gap-1.5 rounded-full border-2 32 + border-ink bg-paper px-3 py-1.5 text-sm font-bold" 33 + > 31 34 <.icon name="hero-check-badge" class="size-5 text-green-600" /> Verified 32 35 </span> 33 - <button 34 - :if={is_nil(@site.published_at)} 36 + <.button 37 + :if={is_nil(@site.published_at) and phase(@site) == :done} 35 38 phx-click="open_publish" 36 - class="inline-flex cursor-pointer items-center gap-1.5 rounded-xl 37 - border-2 border-ink bg-ink px-5 py-2.5 text-sm font-bold text-paper 38 - shadow-[4px_4px_0px_0px_var(--color-peach-bold)] transition-all 39 - hover:-translate-y-0.5 active:translate-y-0" 39 + variant="primary" 40 + shadow="secondary" 40 41 > 41 42 <.icon name="hero-paper-airplane" class="size-5" /> Publish 42 - </button> 43 + </.button> 44 + <.button 45 + phx-click="disconnect" 46 + data-confirm="Disconnect this site? annot.at deletes its local data and stops stracking it. Your publication and documents stay in your PDS." 47 + variant="danger" 48 + > 49 + <.icon name="hero-trash" class="size-4" /> Disconnect 50 + </.button> 43 51 </div> 44 52 </div> 45 53 ··· 221 229 222 230 {:noreply, socket} 223 231 end 232 + end 233 + 234 + def handle_event("disconnect", _params, socket) do 235 + {:ok, _site} = Publishing.delete_site(socket.assigns.current_scope, socket.assigns.site) 236 + 237 + socket = 238 + socket 239 + |> put_flash(:info, "Site disconnected.") 240 + |> push_navigate(to: ~p"/sites") 241 + 242 + {:noreply, socket} 224 243 end 225 244 226 245 defp phase(%Site{verified_at: %DateTime{}}), do: :done
+1 -1
lib/annot_at_web/controllers/live/sites_live.ex
··· 58 58 59 59 @impl Phoenix.LiveView 60 60 def handle_event("filter", %{"status" => status}, socket) do 61 - {:noreply, assign(socket, filter: String.to_existing_atom(status))} 61 + {:noreply, assign(socket, filter: status)} 62 62 end 63 63 64 64 defp filter_sites(sites, "all"), do: sites