tangled
alpha
login
or
join now
margin.at
/
margin
87
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
87
fork
atom
overview
issues
4
pulls
1
pipelines
fix OG
scanash.com
4 weeks ago
9097c40e
d3bdf09a
+63
-103
6 changed files
expand all
collapse all
unified
split
backend
cmd
server
main.go
web
src
lib
og.ts
pages
[handle]
annotation
[rkey].astro
bookmark
[rkey].astro
collection
[rkey].astro
highlight
[rkey].astro
+2
-64
backend/cmd/server/main.go
···
6
6
"net/http"
7
7
"os"
8
8
"os/signal"
9
9
-
"path/filepath"
10
10
-
"strings"
11
9
"syscall"
12
10
"time"
13
11
···
109
107
r.Get("/api/profile/{did}", handler.GetProfile)
110
108
r.Post("/api/profile/avatar", handler.UploadAvatar)
111
109
112
112
-
staticDir := getEnv("STATIC_DIR", "../web/dist")
113
113
-
serveStatic(r, staticDir)
114
114
-
115
115
-
port := getEnv("PORT", "8080")
110
110
+
port := getEnv("PORT", "8081")
116
111
server := &http.Server{
117
112
Addr: ":" + port,
118
113
Handler: r,
119
114
}
120
115
121
121
-
baseURL := getEnv("BASE_URL", "http://localhost:"+port)
122
116
go func() {
123
123
-
log.Printf("🚀 Margin server running on %s", baseURL)
124
124
-
log.Printf("📝 App: %s", baseURL)
125
125
-
log.Printf("🔗 API: %s/api/annotations", baseURL)
117
117
+
log.Printf("Margin API server running on :%s", port)
126
118
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
127
119
log.Fatalf("Server error: %v", err)
128
120
}
···
151
143
}
152
144
return fallback
153
145
}
154
154
-
155
155
-
func serveStatic(r chi.Router, staticDir string) {
156
156
-
absPath, err := filepath.Abs(staticDir)
157
157
-
if err != nil {
158
158
-
log.Printf("Warning: Could not resolve static directory: %v", err)
159
159
-
return
160
160
-
}
161
161
-
162
162
-
if _, err := os.Stat(absPath); os.IsNotExist(err) {
163
163
-
log.Printf("Warning: Static directory does not exist: %s", absPath)
164
164
-
log.Printf("Run 'npm run build' in the web directory first")
165
165
-
return
166
166
-
}
167
167
-
168
168
-
log.Printf("📂 Serving static files from: %s", absPath)
169
169
-
170
170
-
fileServer := http.FileServer(http.Dir(absPath))
171
171
-
172
172
-
r.Get("/*", func(w http.ResponseWriter, req *http.Request) {
173
173
-
path := req.URL.Path
174
174
-
175
175
-
if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/auth/") {
176
176
-
http.NotFound(w, req)
177
177
-
return
178
178
-
}
179
179
-
180
180
-
filePath := filepath.Join(absPath, path)
181
181
-
if _, err := os.Stat(filePath); err == nil {
182
182
-
fileServer.ServeHTTP(w, req)
183
183
-
return
184
184
-
}
185
185
-
186
186
-
if strings.HasPrefix(path, "/.well-known/") {
187
187
-
http.NotFound(w, req)
188
188
-
return
189
189
-
}
190
190
-
191
191
-
lastSlash := strings.LastIndex(path, "/")
192
192
-
lastSegment := path
193
193
-
if lastSlash >= 0 {
194
194
-
lastSegment = path[lastSlash+1:]
195
195
-
}
196
196
-
197
197
-
staticExts := []string{".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".map"}
198
198
-
for _, ext := range staticExts {
199
199
-
if strings.HasSuffix(lastSegment, ext) {
200
200
-
http.NotFound(w, req)
201
201
-
return
202
202
-
}
203
203
-
}
204
204
-
205
205
-
http.ServeFile(w, req, filepath.Join(absPath, "index.html"))
206
206
-
})
207
207
-
}
+16
-10
web/src/lib/og.ts
···
32
32
}
33
33
34
34
interface APIAnnotation {
35
35
-
uri: string;
35
35
+
id?: string;
36
36
+
uri?: string;
36
37
author?: { did: string; handle?: string };
37
38
creator?: { did: string; handle?: string };
38
39
target?: { source?: string; title?: string; selector?: { exact?: string } };
···
50
51
}
51
52
52
53
interface APICollection {
53
53
-
uri: string;
54
54
+
id?: string;
55
55
+
uri?: string;
54
56
name: string;
55
57
description?: string;
56
58
icon?: string;
···
105
107
)) as APIAnnotation | null;
106
108
if (!item) return null;
107
109
110
110
+
const itemURI = item.id || item.uri || uri;
108
111
const author = getAuthorHandle(item);
109
112
const source = item.target?.source || item.url || item.source || "";
110
113
const domain = extractDomain(source);
···
130
133
return {
131
134
title,
132
135
description,
133
133
-
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(item.uri)}`,
136
136
+
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`,
134
137
author,
135
135
-
pageURL: `${BASE_URL}/at/${encodeURIComponent(item.uri.slice(5))}`,
138
138
+
pageURL: `${BASE_URL}/at/${encodeURIComponent(itemURI.slice(5))}`,
136
139
};
137
140
}
138
141
···
142
145
)) as APIAnnotation | null;
143
146
if (!item) return null;
144
147
148
148
+
const itemURI = item.id || item.uri || uri;
145
149
const author = getAuthorHandle(item);
146
150
const source = item.target?.source || item.url || item.source || "";
147
151
const domain = extractDomain(source);
···
164
168
return {
165
169
title,
166
170
description,
167
167
-
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(item.uri)}`,
171
171
+
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`,
168
172
author,
169
169
-
pageURL: `${BASE_URL}/at/${encodeURIComponent(item.uri.slice(5))}`,
173
173
+
pageURL: `${BASE_URL}/at/${encodeURIComponent(itemURI.slice(5))}`,
170
174
};
171
175
}
172
176
···
176
180
)) as APIAnnotation | null;
177
181
if (!item) return null;
178
182
183
183
+
const itemURI = item.id || item.uri || uri;
179
184
const author = getAuthorHandle(item);
180
185
const source = item.target?.source || item.url || item.source || "";
181
186
const domain = extractDomain(source);
···
189
194
return {
190
195
title,
191
196
description,
192
192
-
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(item.uri)}`,
197
197
+
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`,
193
198
author,
194
194
-
pageURL: `${BASE_URL}/at/${encodeURIComponent(item.uri.slice(5))}`,
199
199
+
pageURL: `${BASE_URL}/at/${encodeURIComponent(itemURI.slice(5))}`,
195
200
};
196
201
}
197
202
···
201
206
)) as APICollection | null;
202
207
if (!item) return null;
203
208
209
209
+
const itemURI = item.id || item.uri || uri;
204
210
const author = getAuthorHandle(item);
205
211
const icon = item.icon || "📁";
206
212
const title = `${icon} ${item.name}`;
···
215
221
return {
216
222
title,
217
223
description,
218
218
-
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(item.uri)}`,
224
224
+
image: `${BASE_URL}/og-image?uri=${encodeURIComponent(itemURI)}`,
219
225
author,
220
220
-
pageURL: `${BASE_URL}/collection/${encodeURIComponent(item.uri)}`,
226
226
+
pageURL: `${BASE_URL}/collection/${encodeURIComponent(itemURI)}`,
221
227
};
222
228
}
223
229
+11
-7
web/src/pages/[handle]/annotation/[rkey].astro
···
11
11
let image = 'https://margin.at/og.png';
12
12
13
13
if (handle && rkey) {
14
14
-
const did = await resolveHandle(handle);
15
15
-
if (did) {
16
16
-
const data = await fetchOGForRoute(did, rkey, 'at.margin.annotation');
17
17
-
if (data) {
18
18
-
title = data.title;
19
19
-
description = data.description;
20
20
-
image = data.image;
14
14
+
try {
15
15
+
const did = await resolveHandle(handle);
16
16
+
if (did) {
17
17
+
const data = await fetchOGForRoute(did, rkey, 'at.margin.annotation');
18
18
+
if (data) {
19
19
+
title = data.title;
20
20
+
description = data.description;
21
21
+
image = data.image;
22
22
+
}
21
23
}
24
24
+
} catch (e) {
25
25
+
console.error('OG fetch error (annotation):', e);
22
26
}
23
27
}
24
28
---
+11
-7
web/src/pages/[handle]/bookmark/[rkey].astro
···
11
11
let image = 'https://margin.at/og.png';
12
12
13
13
if (handle && rkey) {
14
14
-
const did = await resolveHandle(handle);
15
15
-
if (did) {
16
16
-
const data = await fetchOGForRoute(did, rkey, 'at.margin.bookmark');
17
17
-
if (data) {
18
18
-
title = data.title;
19
19
-
description = data.description;
20
20
-
image = data.image;
14
14
+
try {
15
15
+
const did = await resolveHandle(handle);
16
16
+
if (did) {
17
17
+
const data = await fetchOGForRoute(did, rkey, 'at.margin.bookmark');
18
18
+
if (data) {
19
19
+
title = data.title;
20
20
+
description = data.description;
21
21
+
image = data.image;
22
22
+
}
21
23
}
24
24
+
} catch (e) {
25
25
+
console.error('OG fetch error (bookmark):', e);
22
26
}
23
27
}
24
28
---
+12
-8
web/src/pages/[handle]/collection/[rkey].astro
···
11
11
let image = 'https://margin.at/og.png';
12
12
13
13
if (handle && rkey) {
14
14
-
const did = await resolveHandle(handle);
15
15
-
if (did) {
16
16
-
const uri = `at://${did}/at.margin.collection/${rkey}`;
17
17
-
const data = await fetchCollectionOG(uri);
18
18
-
if (data) {
19
19
-
title = data.title;
20
20
-
description = data.description;
21
21
-
image = data.image;
14
14
+
try {
15
15
+
const did = await resolveHandle(handle);
16
16
+
if (did) {
17
17
+
const uri = `at://${did}/at.margin.collection/${rkey}`;
18
18
+
const data = await fetchCollectionOG(uri);
19
19
+
if (data) {
20
20
+
title = data.title;
21
21
+
description = data.description;
22
22
+
image = data.image;
23
23
+
}
22
24
}
25
25
+
} catch (e) {
26
26
+
console.error('OG fetch error (collection):', e);
23
27
}
24
28
}
25
29
---
+11
-7
web/src/pages/[handle]/highlight/[rkey].astro
···
11
11
let image = 'https://margin.at/og.png';
12
12
13
13
if (handle && rkey) {
14
14
-
const did = await resolveHandle(handle);
15
15
-
if (did) {
16
16
-
const data = await fetchOGForRoute(did, rkey, 'at.margin.highlight');
17
17
-
if (data) {
18
18
-
title = data.title;
19
19
-
description = data.description;
20
20
-
image = data.image;
14
14
+
try {
15
15
+
const did = await resolveHandle(handle);
16
16
+
if (did) {
17
17
+
const data = await fetchOGForRoute(did, rkey, 'at.margin.highlight');
18
18
+
if (data) {
19
19
+
title = data.title;
20
20
+
description = data.description;
21
21
+
image = data.image;
22
22
+
}
21
23
}
24
24
+
} catch (e) {
25
25
+
console.error('OG fetch error (highlight):', e);
22
26
}
23
27
}
24
28
---