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