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 syntax highlighting of code blocks

Not sure about the span stripping, feels a bit janky and not super performant.

Johanna Larsson (Jul 5, 2026, 6:24 PM +0100) 80442f2e 2f6fccf0

+231 -68
+189 -66
assets/css/app.css
··· 2 2 https://tailwindcss.com/docs/configuration */ 3 3 4 4 @import "tailwindcss" source(none); 5 + @import "highlight.js/styles/github-dark.css"; 6 + 5 7 @source "../css"; 6 8 @source "../js"; 7 9 @source "../../lib/annot_at_web"; ··· 12 14 13 15 /* daisyUI Tailwind Plugin. */ 14 16 @plugin "../vendor/daisyui" { 15 - themes: false; 17 + themes: false; 16 18 } 17 19 18 20 /* Self-hosted variable fonts (latin, weight axis). Syne has no italic axis, 19 21 so italic renders as a synthesized oblique, matching the prototype. */ 20 22 @font-face { 21 - font-family: "Syne"; 22 - font-style: normal; 23 - font-display: swap; 24 - font-weight: 400 800; 25 - src: url("/fonts/syne-latin-wght-normal.woff2") format("woff2"); 23 + font-family: "Syne"; 24 + font-style: normal; 25 + font-display: swap; 26 + font-weight: 400 800; 27 + src: url("/fonts/syne-latin-wght-normal.woff2") format("woff2"); 26 28 } 27 29 28 30 @font-face { 29 - font-family: "Plus Jakarta Sans"; 30 - font-style: normal; 31 - font-display: swap; 32 - font-weight: 200 800; 33 - src: url("/fonts/plus-jakarta-sans-latin-wght-normal.woff2") format("woff2"); 31 + font-family: "Plus Jakarta Sans"; 32 + font-style: normal; 33 + font-display: swap; 34 + font-weight: 200 800; 35 + src: url("/fonts/plus-jakarta-sans-latin-wght-normal.woff2") format("woff2"); 34 36 } 35 37 36 38 /* Brand theme: source of truth for the palette + the utilities the landing 37 39 page uses (bg-paper, text-ink, bg-sky-bold, font-display, animate-float). */ 38 40 @theme { 39 - --color-paper: #fdfcfb; 40 - --color-ink: #1a1a1a; 41 - --color-sky-light: #e0f2fe; 42 - --color-sky-bold: #7dd3fc; 43 - --color-peach-light: #fecaca; 44 - --color-peach-bold: #f9a8a8; 41 + --color-paper: #fdfcfb; 42 + --color-ink: #1a1a1a; 43 + --color-sky-light: #e0f2fe; 44 + --color-sky-bold: #7dd3fc; 45 + --color-peach-light: #fecaca; 46 + --color-peach-bold: #f9a8a8; 45 47 46 - --font-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif; 47 - --font-display: "Syne", ui-sans-serif, system-ui, sans-serif; 48 + --font-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif; 49 + --font-display: "Syne", ui-sans-serif, system-ui, sans-serif; 48 50 49 - --animate-float: float 6s ease-in-out infinite; 50 - --animate-entrance: entrance 0.8s ease-out both; 51 + --animate-float: float 6s ease-in-out infinite; 52 + --animate-entrance: entrance 0.8s ease-out both; 51 53 52 - @keyframes float { 53 - 0%, 100% { transform: translateY(0) rotate(-1deg); } 54 - 50% { transform: translateY(-8px) rotate(1deg); } 55 - } 54 + @keyframes float { 55 + 0%, 56 + 100% { 57 + transform: translateY(0) rotate(-1deg); 58 + } 59 + 50% { 60 + transform: translateY(-8px) rotate(1deg); 61 + } 62 + } 56 63 57 - @keyframes entrance { 58 - 0% { opacity: 0; transform: translateY(20px) rotate(-2deg); } 59 - 100% { opacity: 1; transform: translateY(0) rotate(0); } 60 - } 64 + @keyframes entrance { 65 + 0% { 66 + opacity: 0; 67 + transform: translateY(20px) rotate(-2deg); 68 + } 69 + 100% { 70 + opacity: 1; 71 + transform: translateY(0) rotate(0); 72 + } 73 + } 61 74 } 62 75 63 76 /* Respect reduced-motion preferences. */ 64 77 @media (prefers-reduced-motion: reduce) { 65 - .animate-float, 66 - .animate-entrance { 67 - animation: none; 68 - } 78 + .animate-float, 79 + .animate-entrance { 80 + animation: none; 81 + } 69 82 } 70 83 71 84 /* daisyUI light theme, recolored to the brand palette so forms and flashes 72 85 stay consistent with the landing page. */ 73 86 @plugin "../vendor/daisyui-theme" { 74 - name: "light"; 75 - default: true; 76 - prefersdark: false; 77 - color-scheme: "light"; 78 - --color-base-100: #fdfcfb; 79 - --color-base-200: #f6f4f1; 80 - --color-base-300: #eae6e1; 81 - --color-base-content: #1a1a1a; 82 - --color-primary: #1a1a1a; 83 - --color-primary-content: #fdfcfb; 84 - --color-secondary: #f9a8a8; 85 - --color-secondary-content: #1a1a1a; 86 - --color-accent: #7dd3fc; 87 - --color-accent-content: #1a1a1a; 88 - --color-neutral: #1a1a1a; 89 - --color-neutral-content: #fdfcfb; 90 - --color-info: oklch(58% 0.158 241.966); 91 - --color-info-content: oklch(97% 0.013 236.62); 92 - --color-success: oklch(60% 0.118 184.704); 93 - --color-success-content: oklch(98% 0.014 180.72); 94 - --color-warning: oklch(66% 0.179 58.318); 95 - --color-warning-content: oklch(98% 0.022 95.277); 96 - --color-error: oklch(58% 0.253 17.585); 97 - --color-error-content: oklch(96% 0.015 12.422); 98 - --radius-selector: 0.625rem; 99 - --radius-field: 0.625rem; 100 - --radius-box: 1rem; 101 - --size-selector: 0.21875rem; 102 - --size-field: 0.21875rem; 103 - --border: 1.5px; 104 - --depth: 1; 105 - --noise: 0; 87 + name: "light"; 88 + default: true; 89 + prefersdark: false; 90 + color-scheme: "light"; 91 + --color-base-100: #fdfcfb; 92 + --color-base-200: #f6f4f1; 93 + --color-base-300: #eae6e1; 94 + --color-base-content: #1a1a1a; 95 + --color-primary: #1a1a1a; 96 + --color-primary-content: #fdfcfb; 97 + --color-secondary: #f9a8a8; 98 + --color-secondary-content: #1a1a1a; 99 + --color-accent: #7dd3fc; 100 + --color-accent-content: #1a1a1a; 101 + --color-neutral: #1a1a1a; 102 + --color-neutral-content: #fdfcfb; 103 + --color-info: oklch(58% 0.158 241.966); 104 + --color-info-content: oklch(97% 0.013 236.62); 105 + --color-success: oklch(60% 0.118 184.704); 106 + --color-success-content: oklch(98% 0.014 180.72); 107 + --color-warning: oklch(66% 0.179 58.318); 108 + --color-warning-content: oklch(98% 0.022 95.277); 109 + --color-error: oklch(58% 0.253 17.585); 110 + --color-error-content: oklch(96% 0.015 12.422); 111 + --radius-selector: 0.625rem; 112 + --radius-field: 0.625rem; 113 + --radius-box: 1rem; 114 + --size-selector: 0.21875rem; 115 + --size-field: 0.21875rem; 116 + --border: 1.5px; 117 + --depth: 1; 118 + --noise: 0; 106 119 } 107 120 108 121 /* Add variants based on LiveView classes */ ··· 111 124 @custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &); 112 125 113 126 /* Make LiveView wrapper divs transparent for layout */ 114 - [data-phx-session], [data-phx-teleported-src] { display: contents } 127 + [data-phx-session], 128 + [data-phx-teleported-src] { 129 + display: contents; 130 + } 115 131 116 132 /* This file is for your main application CSS */ 133 + 134 + /* Rendered document content — sanitized blog HTML, no per-element classes. */ 135 + .doc-prose { 136 + line-height: 1.7; 137 + color: color-mix(in oklab, var(--color-ink) 88%, transparent); 138 + } 139 + .doc-prose > * + * { 140 + margin-top: 1rem; 141 + } 142 + 143 + .doc-prose h2, 144 + .doc-prose h3, 145 + .doc-prose h4 { 146 + font-family: var(--font-display); 147 + font-weight: 700; 148 + line-height: 1.2; 149 + color: var(--color-ink); 150 + } 151 + .doc-prose h2 { 152 + font-size: 1.5rem; 153 + margin-top: 2.5rem; 154 + } 155 + .doc-prose h3 { 156 + font-size: 1.25rem; 157 + margin-top: 2rem; 158 + } 159 + .doc-prose h4 { 160 + font-size: 1.1rem; 161 + margin-top: 1.5rem; 162 + } 163 + 164 + .doc-prose a { 165 + color: var(--color-ink); 166 + font-weight: 600; 167 + text-decoration: underline; 168 + text-decoration-color: var(--color-peach-bold); 169 + text-decoration-thickness: 2px; 170 + text-underline-offset: 2px; 171 + } 172 + .doc-prose a:hover { 173 + background: var(--color-peach-light); 174 + } 175 + 176 + .doc-prose ul { 177 + list-style: disc; 178 + padding-left: 1.5rem; 179 + } 180 + .doc-prose ol { 181 + list-style: decimal; 182 + padding-left: 1.5rem; 183 + } 184 + .doc-prose li { 185 + margin-top: 0.375rem; 186 + } 187 + 188 + .doc-prose blockquote { 189 + border-left: 3px solid var(--color-ink); 190 + padding-left: 1rem; 191 + font-style: italic; 192 + color: color-mix(in oklab, var(--color-ink) 65%, transparent); 193 + } 194 + 195 + /* inline code */ 196 + .doc-prose code { 197 + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; 198 + font-size: 0.875em; 199 + background: var(--color-sky-light); 200 + border: 1.5px solid var(--color-ink); 201 + border-radius: 0.375rem; 202 + padding: 0.1em 0.35em; 203 + } 204 + 205 + /* code blocks */ 206 + .doc-prose pre { 207 + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; 208 + font-size: 0.875rem; 209 + background: var(--color-ink); 210 + color: var(--color-paper); 211 + border-radius: 0.75rem; 212 + padding: 1rem 1.25rem; 213 + overflow-x: auto; 214 + } 215 + .doc-prose pre code { 216 + background: transparent; 217 + border: 0; 218 + padding: 0; 219 + color: inherit; 220 + font-size: inherit; 221 + } 222 + 223 + .doc-prose img { 224 + max-width: 100%; 225 + height: auto; 226 + border: 2px solid var(--color-ink); 227 + border-radius: 0.75rem; 228 + } 229 + 230 + .doc-prose hr { 231 + border: 0; 232 + border-top: 2px solid color-mix(in oklab, var(--color-ink) 15%, transparent); 233 + margin: 2rem 0; 234 + } 235 + 236 + .doc-prose strong { 237 + font-weight: 700; 238 + color: var(--color-ink); 239 + }
+10
assets/js/app.js
··· 24 24 import { LiveSocket } from "phoenix_live_view"; 25 25 import { hooks as colocatedHooks } from "phoenix-colocated/annot_at"; 26 26 import topbar from "../vendor/topbar"; 27 + import hljs from "highlight.js/lib/common"; 28 + import elixir from "highlight.js/lib/languages/elixir"; 29 + 30 + hljs.registerLanguage("elixir", elixir); 27 31 28 32 const csrfToken = document 29 33 .querySelector("meta[name='csrf-token']") ··· 89 93 }, 90 94 ); 91 95 } 96 + 97 + document.addEventListener("DOMContentLoaded", () => { 98 + document.querySelectorAll(".doc-prose pre code").forEach((el) => { 99 + hljs.highlightElement(el); 100 + }); 101 + });
+21
assets/package-lock.json
··· 1 + { 2 + "name": "assets", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": { 6 + "": { 7 + "dependencies": { 8 + "highlight.js": "^11.11.1" 9 + } 10 + }, 11 + "node_modules/highlight.js": { 12 + "version": "11.11.1", 13 + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", 14 + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", 15 + "license": "BSD-3-Clause", 16 + "engines": { 17 + "node": ">=12.0.0" 18 + } 19 + } 20 + } 21 + }
+5
assets/package.json
··· 1 + { 2 + "dependencies": { 3 + "highlight.js": "^11.11.1" 4 + } 5 + }
+6 -2
lib/annot_at_web/components/document_components.ex
··· 42 42 </span> 43 43 </div> 44 44 45 - <div class="mt-8 space-y-4 leading-relaxed text-ink/85"> 45 + <div class="doc-prose mt-8"> 46 46 <%= case @doc["content"] do %> 47 47 <% %{"$type" => "org.wordpress.html", "html" => html} -> %> 48 48 {raw(sanitize(html))} ··· 54 54 """ 55 55 end 56 56 57 - defp sanitize(html), do: HtmlSanitizeEx.markdown_html(html) 57 + defp sanitize(html) do 58 + html 59 + |> HtmlSanitizeEx.markdown_html() 60 + |> String.replace(~r{</?span>}, "") 61 + end 58 62 59 63 defp preview(nil), do: nil 60 64 defp preview(text) when byte_size(text) <= 280, do: text