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.

Grab feed categories and strip text content

I realized I was passing the HTML string to textContent and it feels more accurate to extract the text instead.

Also, adds feed tag extraction so the standard.site documents get nice tags!

Johanna Larsson (Jul 1, 2026, 7:15 AM +0100) 58fd1bbc 09e71ce3

+30 -8
+1 -1
README.md
··· 14 14 - [x] Maybe some design? 15 15 - [x] Add site including verification 16 16 - [ ] Write a guide for using this 17 - - [ ] Document cover images 17 + - [x] Document cover images 18 18 - [ ] Document content with html content type 19 19 - [ ] RSS poller 20 20 - [ ] standard.site document reader
+3 -1
lib/annot_at/feeds/entry.ex
··· 15 15 :content, 16 16 :rkey, 17 17 :image, 18 + :categories, 18 19 cover_status: :none 19 20 ] 20 21 ··· 29 30 content: String.t() | nil, 30 31 rkey: String.t() | nil, 31 32 image: String.t() | nil, 32 - cover_status: cover_status() 33 + cover_status: cover_status(), 34 + categories: [String.t()] 33 35 } 34 36 35 37 def hash(%__MODULE__{} = entry) do
+14 -5
lib/annot_at/feeds/rss.ex
··· 53 53 {:ok, 54 54 %{ 55 55 state 56 - | entries: [%Entry{} | state.entries], 56 + | entries: [%Entry{categories: []} | state.entries], 57 57 stack: ["item" | state.stack], 58 58 current_text: [] 59 59 }} ··· 105 105 defp apply_entry_field(entry, "description", text), do: %{entry | summary: text} 106 106 defp apply_entry_field(entry, "content:encoded", text), do: %{entry | content: text} 107 107 defp apply_entry_field(entry, "pubDate", text), do: %{entry | published_at: parse_date(text)} 108 + 109 + defp apply_entry_field(entry, "category", text), 110 + do: %{entry | categories: [text | entry.categories]} 111 + 108 112 defp apply_entry_field(entry, _name, _text), do: entry 109 113 110 114 defp apply_feed_field(feed, "title", text), do: %{feed | title: text} ··· 112 116 defp apply_feed_field(feed, "link", text), do: %{feed | url: text} 113 117 defp apply_feed_field(feed, _name, _text), do: feed 114 118 115 - defp finalize_entry(%Entry{id: nil, url: url} = entry) when is_binary(url) do 116 - %{entry | id: url} 117 - end 119 + defp finalize_entry(%Entry{} = entry) do 120 + id = 121 + if is_nil(entry.id) and is_binary(entry.url) do 122 + entry.url 123 + else 124 + entry.id 125 + end 118 126 119 - defp finalize_entry(entry), do: entry 127 + %{entry | id: id, categories: Enum.reverse(entry.categories)} 128 + end 120 129 121 130 defp text(parts) do 122 131 result =
+9 -1
lib/annot_at_web/controllers/live/posts_live.ex
··· 363 363 path: path_of(entry.url), 364 364 published_at: entry.published_at, 365 365 description: entry.summary, 366 - text_content: entry.content, 366 + text_content: text_content_of(entry.content), 367 367 cover_image: fetch_cover(entry) 368 368 } 369 369 end 370 370 371 371 defp path_of(nil), do: nil 372 372 defp path_of(url), do: URI.parse(url).path 373 + 374 + defp text_content_of(nil), do: nil 375 + 376 + defp text_content_of(content) do 377 + content 378 + |> LazyHTML.from_fragment() 379 + |> LazyHTML.text() 380 + end 373 381 374 382 defp create_document(scope, site, entry) do 375 383 document = to_document(entry, site, scope.user)
+1
test/annot_at/feeds/rss_test.exs
··· 27 27 assert "<p>The full <strong>content</strong> of the first post.</p>" == first.content 28 28 assert %DateTime{} = first.published_at 29 29 assert ~U[2024-10-02 13:00:00Z] == first.published_at 30 + assert ["category-1", "category-2"] = first.categories 30 31 end 31 32 32 33 test "falls back to URL as ID when guid is missing" do
+2
test/support/fixtures/feeds/rss_sample.xml
··· 17 17 <content:encoded 18 18 ><![CDATA[<p>The full <strong>content</strong> of the first post.</p>]]></content:encoded> 19 19 <pubDate>Wed, 02 Oct 2024 13:00:00 GMT</pubDate> 20 + <category>category-1</category> 21 + <category>category-2</category> 20 22 </item> 21 23 <item> 22 24 <title>Second Post</title>