Support some syntax in comment bodies
2de8c32ea2a1bce4ecc8a9c974fe97cd4c4a880f
1 parent
6167f627
scripts/seed-forum.sh
+43 -15
| 17 | 17 | ||
| 18 | 18 | sqlite3 "$DB" <<'SQL' |
|
| 19 | 19 | -- Discussion 1: welcome / intro |
|
| 20 | 20 | INSERT INTO discussions (repo, title, body, author, created_at) |
|
| 21 | 21 | VALUES ('_site', 'Welcome to the Radiant forum', |
|
| 22 | - | 'Hey everyone, this is the general discussion space for all things Radiant. |
|
| 22 | + | 'Hey everyone, this is the general discussion space for *all things Radiant*. |
|
| 23 | 23 | ||
| 24 | - | Feel free to introduce yourself, ask questions, or share what you are working on. |
|
| 24 | + | Feel free to introduce yourself, ask questions, or share what you are working on. Check out the project at <https://radiant.dev> if you have not already. |
|
| 25 | 25 | ||
| 26 | 26 | A few ground rules: |
|
| 27 | 27 | - Be kind and constructive. |
|
| 28 | 28 | - Stay on topic (use per-repo discussions for repo-specific issues). |
|
| 29 | 29 | - Have fun.', |
|
| 30 | 30 | 'cloudhead.io', datetime('now', '-14 days')); |
|
| 31 | 31 | ||
| 32 | 32 | -- Replies to discussion 1 |
|
| 33 | 33 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 34 | - | VALUES (last_insert_rowid(), 'Thanks for setting this up! Excited to be here.', |
|
| 34 | + | VALUES (last_insert_rowid(), 'Thanks for setting this up! *Excited* to be here.', |
|
| 35 | 35 | 'alice.bsky.social', datetime('now', '-13 days')); |
|
| 36 | 36 | ||
| 37 | 37 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 38 | - | VALUES ((SELECT MAX(id) FROM discussions), 'Great to see a forum that works without JavaScript.', |
|
| 38 | + | VALUES ((SELECT MAX(id) FROM discussions), 'Great to see a forum that works without JavaScript. The source is really clean too.', |
|
| 39 | 39 | 'bob.bsky.social', datetime('now', '-12 days')); |
|
| 40 | 40 | ||
| 41 | 41 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 42 | - | VALUES ((SELECT MAX(id) FROM discussions), 'Agreed, this is refreshing. Looking forward to contributing.', |
|
| 42 | + | VALUES ((SELECT MAX(id) FROM discussions), 'Agreed, this is refreshing. I found the contributing guide at <https://radiant.dev/contributing> very helpful. Looking forward to contributing.', |
|
| 43 | 43 | 'alice.bsky.social', datetime('now', '-11 days')); |
|
| 44 | 44 | ||
| 45 | 45 | -- Discussion 2: build question |
|
| 46 | 46 | INSERT INTO discussions (repo, title, body, author, created_at) |
|
| 47 | 47 | VALUES ('_site', 'Cross-compiling forge for ARM?', |
|
| 48 | 48 | 'Has anyone tried cross-compiling the forge binary for ARM (e.g. Raspberry Pi)? |
|
| 49 | 49 | ||
| 50 | - | I tried GOARCH=arm64 go build . and it built fine, but I am wondering if there are any gotchas with the SQLite dependency on musl vs glibc.', |
|
| 50 | + | I tried `GOARCH=arm64 go build .` and it built fine, but I am wondering if there are any gotchas with the SQLite dependency on *musl* vs *glibc*.', |
|
| 51 | 51 | 'bob.bsky.social', datetime('now', '-10 days')); |
|
| 52 | 52 | ||
| 53 | 53 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 54 | - | VALUES (last_insert_rowid(), 'I run it on a Pi 4 with no issues. Just make sure you have CGO_ENABLED=1 and a cross-compiler installed for the SQLite bits.', |
|
| 54 | + | VALUES (last_insert_rowid(), 'I run it on a Pi 4 with no issues. Here is what I use: |
|
| 55 | + | ||
| 56 | + | ``` |
|
| 57 | + | export CGO_ENABLED=1 |
|
| 58 | + | export CC=aarch64-linux-gnu-gcc |
|
| 59 | + | GOARCH=arm64 go build . |
|
| 60 | + | ``` |
|
| 61 | + | ||
| 62 | + | See <https://go.dev/doc/install/source#environment> for the full list of env vars.', |
|
| 55 | 63 | 'alice.bsky.social', datetime('now', '-9 days')); |
|
| 56 | 64 | ||
| 57 | 65 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 58 | - | VALUES ((SELECT MAX(id) FROM discussions), 'Thanks, that did the trick!', |
|
| 66 | + | VALUES ((SELECT MAX(id) FROM discussions), 'Thanks, that did the trick! Setting `CGO_ENABLED=1` was the key part I was missing.', |
|
| 59 | 67 | 'bob.bsky.social', datetime('now', '-8 days')); |
|
| 60 | 68 | ||
| 61 | 69 | -- Discussion 3: feature idea |
|
| 62 | 70 | INSERT INTO discussions (repo, title, body, author, created_at) |
|
| 63 | 71 | VALUES ('_site', 'Idea: RSS feed for repository updates', |
|
| 64 | - | 'It would be nice to have an RSS/Atom feed at /feed or /rss that lists recent commits across all public repos. That way people could subscribe and stay up to date without polling the web UI. |
|
| 72 | + | 'It would be nice to have an RSS/Atom feed at `/feed` or `/rss` that lists recent commits across all public repos. That way people could subscribe and stay up to date without polling the web UI. |
|
| 65 | 73 | ||
| 66 | - | Anyone else interested in this?', |
|
| 74 | + | The Atom spec is at <https://www.rfc-editor.org/rfc/rfc4287>. Anyone else interested in this?', |
|
| 67 | 75 | 'alice.bsky.social', datetime('now', '-7 days')); |
|
| 68 | 76 | ||
| 69 | 77 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 70 | - | VALUES (last_insert_rowid(), 'Yes! I would use this. Per-repo feeds would be great too.', |
|
| 78 | + | VALUES (last_insert_rowid(), '*Yes!* I would use this. Per-repo feeds would be great too.', |
|
| 71 | 79 | 'bob.bsky.social', datetime('now', '-6 days')); |
|
| 72 | 80 | ||
| 73 | 81 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 74 | - | VALUES ((SELECT MAX(id) FROM discussions), 'I might take a crack at this. Seems straightforward to add a handler that shells out to git log --all.', |
|
| 82 | + | VALUES ((SELECT MAX(id) FROM discussions), 'I might take a crack at this. The handler would be something like: |
|
| 83 | + | ||
| 84 | + | ```go |
|
| 85 | + | func (s *server) handleFeed(w http.ResponseWriter, r *http.Request) { |
|
| 86 | + | commits := s.getRecentCommits(20) |
|
| 87 | + | w.Header().Set("Content-Type", "application/atom+xml") |
|
| 88 | + | renderAtomFeed(w, commits) |
|
| 89 | + | } |
|
| 90 | + | ``` |
|
| 91 | + | ||
| 92 | + | Seems straightforward to shell out to `git log --all` for the data.', |
|
| 75 | 93 | 'cloudhead.io', datetime('now', '-4 days')); |
|
| 76 | 94 | ||
| 77 | 95 | -- Discussion 4: a shorter thread |
|
| 78 | 96 | INSERT INTO discussions (repo, title, body, author, created_at) |
|
| 79 | 97 | VALUES ('_site', 'Syntax highlighting for more languages?', |
|
| 80 | - | 'Currently forge ships hirad.js and hiril.js for Radiance and RIL. Any plans to support other languages, or is the intent to keep it minimal?', |
|
| 98 | + | 'Currently forge ships `hirad.js` and `hiril.js` for Radiance and RIL. Any plans to support other languages, or is the intent to keep it *minimal*?', |
|
| 81 | 99 | 'bob.bsky.social', datetime('now', '-3 days')); |
|
| 82 | 100 | ||
| 83 | 101 | INSERT INTO replies (discussion_id, body, author, created_at) |
|
| 84 | - | VALUES (last_insert_rowid(), 'I think keeping it minimal is the right call. You can always add your own highlighter scripts if needed.', |
|
| 102 | + | VALUES (last_insert_rowid(), 'I think keeping it minimal is the right call. You can always drop your own highlighter scripts into `static/js/` and rebuild. The embed directive picks them up automatically.', |
|
| 85 | 103 | 'cloudhead.io', datetime('now', '-2 days')); |
|
| 86 | 104 | ||
| 87 | 105 | -- Discussion 5: no replies yet |
|
| 88 | 106 | INSERT INTO discussions (repo, title, body, author, created_at) |
|
| 89 | 107 | VALUES ('_site', 'Deploying behind nginx reverse proxy', |
|
| 90 | - | 'I am trying to set up forge behind nginx with a /git base URL. I pass -base-url /git but static assets (CSS, fonts) are not loading. Has anyone got a working nginx config they can share?', |
|
| 108 | + | 'I am trying to set up forge behind nginx with a `/git` base URL. I pass `-base-url /git` but static assets (CSS, fonts) are not loading. Here is what I have so far: |
|
| 109 | + | ||
| 110 | + | ``` |
|
| 111 | + | location /git/ { |
|
| 112 | + | proxy_pass http://127.0.0.1:8080/; |
|
| 113 | + | proxy_set_header Host $host; |
|
| 114 | + | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|
| 115 | + | } |
|
| 116 | + | ``` |
|
| 117 | + | ||
| 118 | + | I followed the reverse proxy guide at <https://nginx.org/en/docs/http/ngx_http_proxy_module.html> but *something* is still off. Has anyone got a working config they can share?', |
|
| 91 | 119 | 'alice.bsky.social', datetime('now', '-1 days')); |
|
| 92 | 120 | ||
| 93 | 121 | -- Avatars |
|
| 94 | 122 | INSERT OR IGNORE INTO avatars (handle, url) VALUES |
|
| 95 | 123 | ('cloudhead.io', '/avatars/cloudhead.svg'), |
static/style.css
+24 -7
| 251 | 251 | .ref-list th:last-child, |
|
| 252 | 252 | .ref-list td:last-child { padding-right: 0.5rem; } |
|
| 253 | 253 | ||
| 254 | 254 | .hash { font-family: var(--mono); white-space: nowrap; } |
|
| 255 | 255 | .date { white-space: nowrap; color: var(--fg-dim); } |
|
| 256 | - | .author { color: var(--fg-dim); white-space: nowrap; } |
|
| 256 | + | .author { color: var(--fg-dim); white-space: nowrap; display: inline-flex; align-items: center; gap: 0.25rem; } |
|
| 257 | 257 | .subject { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
|
| 258 | 258 | .mode { font-family: var(--mono); font-size: 1rem; color: var(--fg-dim); white-space: nowrap; } |
|
| 259 | 259 | .size { font-family: var(--mono); font-size: 0.875rem; color: var(--fg-dim); text-align: right; white-space: nowrap; } |
|
| 260 | 260 | ||
| 261 | 261 | .clone-url { |
| 817 | 817 | .discussions-header h2 { margin-bottom: 0; } |
|
| 818 | 818 | .discussions-actions { |
|
| 819 | 819 | display: flex; |
|
| 820 | 820 | align-items: center; |
|
| 821 | 821 | gap: 0.75rem; |
|
| 822 | + | margin-left: auto; |
|
| 822 | 823 | } |
|
| 823 | 824 | .signed-in { |
|
| 824 | 825 | font-family: var(--mono); |
|
| 825 | 826 | font-size: 0.875rem; |
|
| 826 | 827 | color: var(--fg-dim); |
| 840 | 841 | } |
|
| 841 | 842 | .btn:hover { background: var(--bg-alt); text-decoration: none; } |
|
| 842 | 843 | .btn:disabled { opacity: 0.5; cursor: default; } |
|
| 843 | 844 | main .btn { font-size: 1rem; } |
|
| 844 | 845 | ||
| 845 | - | .discussion-list td { border-bottom: none; } |
|
| 846 | + | .discussion-list td { border-bottom: none; vertical-align: middle; } |
|
| 846 | 847 | .discussion-list tbody tr:hover { background: var(--bg-alt); } |
|
| 847 | 848 | .discussion-list th:first-child, |
|
| 848 | 849 | .discussion-list td:first-child { padding-left: 0.5rem; } |
|
| 849 | 850 | .discussion-list th:last-child, |
|
| 850 | - | .discussion-list td:last-child { padding-right: 0.5rem; } |
|
| 851 | + | .discussion-list td:last-child { padding-right: 0.5rem; text-align: right; } |
|
| 851 | 852 | .discussion-list .discussion-title a { color: var(--link); } |
|
| 853 | + | .discussion-list .avatar { width: 16px; height: 16px; margin-right: 0; } |
|
| 852 | 854 | .discussion-list .discussion-replies { |
|
| 853 | 855 | white-space: nowrap; |
|
| 854 | 856 | color: var(--fg-dim); |
|
| 855 | - | text-align: right; |
|
| 857 | + | text-align: left; |
|
| 856 | 858 | width: 1%; |
|
| 857 | 859 | } |
|
| 858 | 860 | ||
| 859 | 861 | .comment { |
|
| 860 | 862 | border: 1px solid var(--border); |
| 867 | 869 | margin: 1rem 0; |
|
| 868 | 870 | } |
|
| 869 | 871 | .comment-header { |
|
| 870 | 872 | display: flex; |
|
| 871 | 873 | align-items: center; |
|
| 872 | - | gap: 0.75rem; |
|
| 873 | 874 | padding: 0.75rem 0.75rem 0.5rem; |
|
| 874 | 875 | font-size: 1rem; |
|
| 875 | 876 | color: var(--fg); |
|
| 876 | 877 | } |
|
| 877 | 878 | .comment-date { |
|
| 878 | - | margin-left: auto; |
|
| 879 | - | font-size: 0.875rem; |
|
| 879 | + | font-size: 1rem; |
|
| 880 | 880 | color: var(--fg-dim); |
|
| 881 | 881 | white-space: nowrap; |
|
| 882 | + | margin-left: 0.25rem; |
|
| 882 | 883 | } |
|
| 883 | 884 | .comment-author { |
|
| 884 | 885 | display: inline-flex; |
|
| 885 | 886 | align-items: center; |
|
| 886 | 887 | gap: 0.25rem; |
| 906 | 907 | margin-top: 0; |
|
| 907 | 908 | } |
|
| 908 | 909 | .post-body p:last-child { |
|
| 909 | 910 | margin-bottom: 0; |
|
| 910 | 911 | } |
|
| 912 | + | .post-body code { |
|
| 913 | + | background: var(--bg-alt); |
|
| 914 | + | padding: 0.125rem 0.25rem; |
|
| 915 | + | border-radius: 0.25rem; |
|
| 916 | + | } |
|
| 917 | + | .post-body pre { |
|
| 918 | + | background: var(--bg-alt); |
|
| 919 | + | padding: 0.75rem; |
|
| 920 | + | border-radius: 0.25rem; |
|
| 921 | + | margin: 0.5rem 0; |
|
| 922 | + | overflow-x: auto; |
|
| 923 | + | } |
|
| 924 | + | .post-body pre code { |
|
| 925 | + | padding: 0; |
|
| 926 | + | background: none; |
|
| 927 | + | } |
|
| 911 | 928 | ||
| 912 | 929 | .discussion-form { |
|
| 913 | 930 | margin-top: 1rem; |
|
| 914 | 931 | } |
|
| 915 | 932 | .discussion-form label { |
template.go
+106 -25
| 180 | 180 | "formatBody": func(s string) template.HTML { |
|
| 181 | 181 | s = strings.TrimSpace(s) |
|
| 182 | 182 | if s == "" { |
|
| 183 | 183 | return "" |
|
| 184 | 184 | } |
|
| 185 | - | paragraphs := strings.Split(s, "\n\n") |
|
| 186 | 185 | var b strings.Builder |
|
| 187 | - | for _, p := range paragraphs { |
|
| 188 | - | p = strings.TrimSpace(p) |
|
| 189 | - | if p == "" { |
|
| 190 | - | continue |
|
| 186 | + | // Split into alternating prose / fenced-code-block segments. |
|
| 187 | + | for s != "" { |
|
| 188 | + | // Find the next opening fence. |
|
| 189 | + | openIdx := strings.Index(s, "```") |
|
| 190 | + | if openIdx < 0 { |
|
| 191 | + | formatParagraphs(&b, s) |
|
| 192 | + | break |
|
| 191 | 193 | } |
|
| 192 | - | // Apply autolink-style URL linking within each paragraph. |
|
| 193 | - | var pb strings.Builder |
|
| 194 | - | last := 0 |
|
| 195 | - | for _, loc := range urlRe.FindAllStringIndex(p, -1) { |
|
| 196 | - | end := loc[1] |
|
| 197 | - | for end > loc[0] && strings.ContainsRune(".,;:!?)]", rune(p[end-1])) { |
|
| 198 | - | end-- |
|
| 199 | - | } |
|
| 200 | - | pb.WriteString(html.EscapeString(p[last:loc[0]])) |
|
| 201 | - | u := p[loc[0]:end] |
|
| 202 | - | pb.WriteString(`<a href="`) |
|
| 203 | - | pb.WriteString(html.EscapeString(u)) |
|
| 204 | - | pb.WriteString(`">`) |
|
| 205 | - | pb.WriteString(html.EscapeString(u)) |
|
| 206 | - | pb.WriteString(`</a>`) |
|
| 207 | - | last = end |
|
| 194 | + | // Prose before the fence. |
|
| 195 | + | if openIdx > 0 { |
|
| 196 | + | formatParagraphs(&b, s[:openIdx]) |
|
| 208 | 197 | } |
|
| 209 | - | pb.WriteString(html.EscapeString(p[last:])) |
|
| 210 | - | b.WriteString("<p>") |
|
| 211 | - | b.WriteString(pb.String()) |
|
| 212 | - | b.WriteString("</p>") |
|
| 198 | + | // Skip the opening ``` and any language tag on the same line. |
|
| 199 | + | rest := s[openIdx+3:] |
|
| 200 | + | if nl := strings.Index(rest, "\n"); nl >= 0 { |
|
| 201 | + | rest = rest[nl+1:] |
|
| 202 | + | } else { |
|
| 203 | + | // ``` at end of input with nothing after — treat as prose. |
|
| 204 | + | formatParagraphs(&b, s[openIdx:]) |
|
| 205 | + | break |
|
| 206 | + | } |
|
| 207 | + | // Find the closing fence. |
|
| 208 | + | closeIdx := strings.Index(rest, "```") |
|
| 209 | + | var code string |
|
| 210 | + | if closeIdx < 0 { |
|
| 211 | + | code = rest |
|
| 212 | + | s = "" |
|
| 213 | + | } else { |
|
| 214 | + | code = rest[:closeIdx] |
|
| 215 | + | s = rest[closeIdx+3:] |
|
| 216 | + | } |
|
| 217 | + | code = strings.TrimRight(code, "\n") |
|
| 218 | + | b.WriteString("<pre><code>") |
|
| 219 | + | b.WriteString(html.EscapeString(code)) |
|
| 220 | + | b.WriteString("</code></pre>") |
|
| 213 | 221 | } |
|
| 214 | 222 | return template.HTML(b.String()) |
|
| 215 | 223 | }, |
|
| 216 | 224 | "discussionPath": func(baseURL, repo string) string { |
|
| 217 | 225 | if repo == "" { |
| 242 | 250 | }, |
|
| 243 | 251 | } |
|
| 244 | 252 | ||
| 245 | 253 | var urlRe = regexp.MustCompile(`https?://[^\s<>"'` + "`" + `\x00-\x1f]+`) |
|
| 246 | 254 | ||
| 255 | + | // inlineRe matches (in order): backtick code, angle-bracket links, bare URLs, or *italic*. |
|
| 256 | + | var inlineRe = regexp.MustCompile("`" + `([^` + "`" + `\n]+)` + "`" + |
|
| 257 | + | `|<(https?://[^\s<>]+)>` + |
|
| 258 | + | `|(https?://[^\s<>"'` + "`" + `\x00-\x1f]+)` + |
|
| 259 | + | `|\*([^\s*][^*]*[^\s*])\*|\*([^\s*])\*`) |
|
| 260 | + | ||
| 261 | + | // formatParagraphs splits prose text on blank lines and writes <p> elements |
|
| 262 | + | // with inline formatting into b. |
|
| 263 | + | func formatParagraphs(b *strings.Builder, s string) { |
|
| 264 | + | for _, p := range strings.Split(s, "\n\n") { |
|
| 265 | + | p = strings.TrimSpace(p) |
|
| 266 | + | if p == "" { |
|
| 267 | + | continue |
|
| 268 | + | } |
|
| 269 | + | b.WriteString("<p>") |
|
| 270 | + | b.WriteString(formatInline(p)) |
|
| 271 | + | b.WriteString("</p>") |
|
| 272 | + | } |
|
| 273 | + | } |
|
| 274 | + | ||
| 275 | + | // formatInline applies inline formatting to a paragraph of raw text. |
|
| 276 | + | // It handles backtick `code`, *italic*, <https://...> links, and bare URLs. |
|
| 277 | + | func formatInline(p string) string { |
|
| 278 | + | var b strings.Builder |
|
| 279 | + | last := 0 |
|
| 280 | + | for _, m := range inlineRe.FindAllStringSubmatchIndex(p, -1) { |
|
| 281 | + | start, end := m[0], m[1] |
|
| 282 | + | b.WriteString(html.EscapeString(p[last:start])) |
|
| 283 | + | ||
| 284 | + | switch { |
|
| 285 | + | case m[2] >= 0: // backtick code: group 1 |
|
| 286 | + | code := p[m[2]:m[3]] |
|
| 287 | + | b.WriteString("<code>") |
|
| 288 | + | b.WriteString(html.EscapeString(code)) |
|
| 289 | + | b.WriteString("</code>") |
|
| 290 | + | case m[4] >= 0: // angle-bracket link: group 2 |
|
| 291 | + | u := p[m[4]:m[5]] |
|
| 292 | + | b.WriteString(`<a href="`) |
|
| 293 | + | b.WriteString(html.EscapeString(u)) |
|
| 294 | + | b.WriteString(`">`) |
|
| 295 | + | b.WriteString(html.EscapeString(u)) |
|
| 296 | + | b.WriteString("</a>") |
|
| 297 | + | case m[6] >= 0: // bare URL: group 3 |
|
| 298 | + | // Strip trailing punctuation. |
|
| 299 | + | urlEnd := m[7] |
|
| 300 | + | for urlEnd > m[6] && strings.ContainsRune(".,;:!?)]", rune(p[urlEnd-1])) { |
|
| 301 | + | urlEnd-- |
|
| 302 | + | } |
|
| 303 | + | u := p[m[6]:urlEnd] |
|
| 304 | + | b.WriteString(`<a href="`) |
|
| 305 | + | b.WriteString(html.EscapeString(u)) |
|
| 306 | + | b.WriteString(`">`) |
|
| 307 | + | b.WriteString(html.EscapeString(u)) |
|
| 308 | + | b.WriteString("</a>") |
|
| 309 | + | // Anything between stripped punctuation and regex end is plain text. |
|
| 310 | + | b.WriteString(html.EscapeString(p[urlEnd:end])) |
|
| 311 | + | case m[8] >= 0: // italic (multi-char): group 4 |
|
| 312 | + | inner := p[m[8]:m[9]] |
|
| 313 | + | b.WriteString("<em>") |
|
| 314 | + | b.WriteString(html.EscapeString(inner)) |
|
| 315 | + | b.WriteString("</em>") |
|
| 316 | + | case m[10] >= 0: // italic (single-char): group 5 |
|
| 317 | + | inner := p[m[10]:m[11]] |
|
| 318 | + | b.WriteString("<em>") |
|
| 319 | + | b.WriteString(html.EscapeString(inner)) |
|
| 320 | + | b.WriteString("</em>") |
|
| 321 | + | } |
|
| 322 | + | last = end |
|
| 323 | + | } |
|
| 324 | + | b.WriteString(html.EscapeString(p[last:])) |
|
| 325 | + | return b.String() |
|
| 326 | + | } |
|
| 327 | + | ||
| 247 | 328 | func loadTemplates() (*templateSet, error) { |
|
| 248 | 329 | layoutContent, err := templateFS.ReadFile("templates/layout.html") |
|
| 249 | 330 | if err != nil { |
|
| 250 | 331 | return nil, fmt.Errorf("read layout: %w", err) |
|
| 251 | 332 | } |
templates/discussion.html
+2 -4
| 2 | 2 | {{$dpath := discussionPath .BaseURL .Repo}} |
|
| 3 | 3 | <h2 class="topic-title">{{.Data.Discussion.Title}}</h2> |
|
| 4 | 4 | ||
| 5 | 5 | <div class="comment"> |
|
| 6 | 6 | <div class="comment-header"> |
|
| 7 | - | <span class="comment-author">{{if .Data.Discussion.Avatar}}<img class="avatar" src="{{.Data.Discussion.Avatar}}" alt="" width="20" height="20"> {{end}}<a href="https://bsky.app/profile/{{.Data.Discussion.Author}}">{{.Data.Discussion.Author}}</a></span> |
|
| 8 | - | <span class="comment-date">{{timeAgo .Data.Discussion.CreatedAt}} ago</span> |
|
| 7 | + | <span class="comment-author">{{if .Data.Discussion.Avatar}}<img class="avatar" src="{{.Data.Discussion.Avatar}}" alt="" width="20" height="20"> {{end}}<a href="https://bsky.app/profile/{{.Data.Discussion.Author}}">{{.Data.Discussion.Author}}</a> <span class="comment-date">{{timeAgo .Data.Discussion.CreatedAt}} ago</span></span> |
|
| 9 | 8 | </div> |
|
| 10 | 9 | {{if .Data.Discussion.Body}}<div class="post-body">{{formatBody .Data.Discussion.Body}}</div>{{end}} |
|
| 11 | 10 | </div> |
|
| 12 | 11 | ||
| 13 | 12 | {{range .Data.Replies}} |
|
| 14 | 13 | <div class="comment" id="reply-{{.ID}}"> |
|
| 15 | 14 | <div class="comment-header"> |
|
| 16 | - | <span class="comment-author">{{if .Avatar}}<img class="avatar" src="{{.Avatar}}" alt="" width="20" height="20"> {{end}}<a href="https://bsky.app/profile/{{.Author}}">{{.Author}}</a></span> |
|
| 17 | - | <span class="comment-date">{{timeAgo .CreatedAt}} ago</span> |
|
| 15 | + | <span class="comment-author">{{if .Avatar}}<img class="avatar" src="{{.Avatar}}" alt="" width="20" height="20"> {{end}}<a href="https://bsky.app/profile/{{.Author}}">{{.Author}}</a> <span class="comment-date">{{timeAgo .CreatedAt}} ago</span></span> |
|
| 18 | 16 | </div> |
|
| 19 | 17 | <div class="post-body">{{formatBody .Body}}</div> |
|
| 20 | 18 | </div> |
|
| 21 | 19 | {{end}} |
|
| 22 | 20 |
templates/discussions.html
+1 -1
| 22 | 22 | </thead> |
|
| 23 | 23 | <tbody> |
|
| 24 | 24 | {{range .Data.Discussions}} |
|
| 25 | 25 | <tr> |
|
| 26 | 26 | <td class="discussion-title"><a href="{{$dpath}}/{{.ID}}">{{.Title}}</a></td> |
|
| 27 | - | <td class="author desktop">{{if .Avatar}}<img class="avatar" src="{{.Avatar}}" alt="" width="20" height="20"> {{end}}<a href="https://bsky.app/profile/{{.Author}}">{{.Author}}</a></td> |
|
| 27 | + | <td class="author desktop">{{if .Avatar}}<img class="avatar" src="{{.Avatar}}" alt="" width="16" height="16"> {{end}}<a href="https://bsky.app/profile/{{.Author}}">{{.Author}}</a></td> |
|
| 28 | 28 | <td class="discussion-replies desktop">{{.ReplyCount}}</td> |
|
| 29 | 29 | <td class="date">{{timeAgo .LastActive}}</td> |
|
| 30 | 30 | </tr> |
|
| 31 | 31 | {{end}} |
|
| 32 | 32 | </tbody> |