+181
-12
src/routes/blog/[rkey]/+server.ts
+181
-12
src/routes/blog/[rkey]/+server.ts
···
1
1
import type { RequestHandler } from '@sveltejs/kit';
2
+
import {
3
+
PUBLIC_ATPROTO_DID,
4
+
PUBLIC_LEAFLET_BASE_PATH,
5
+
PUBLIC_LEAFLET_BLOG_PUBLICATION,
6
+
PUBLIC_BLOG_FALLBACK_URL
7
+
} from '$env/static/public';
8
+
import { withFallback } from '$lib/services/atproto';
9
+
import { fetchLeafletPublications } from '$lib/services/atproto';
2
10
3
-
export const GET: RequestHandler = ({ params, url }) => {
4
-
const tid = params.rkey;
5
-
const tidPattern = /^[a-zA-Z0-9]{12,16}$/;
11
+
/**
12
+
* Smart blog post redirect handler
13
+
*
14
+
* Automatically detects whether the post is from WhiteWind or Leaflet
15
+
* and redirects to the appropriate URL.
16
+
*
17
+
* WhiteWind: https://whtwnd.com/{DID}/{rkey}
18
+
* Leaflet: {LEAFLET_BASE_PATH}/{rkey} or https://leaflet.pub/{DID}/{rkey}
19
+
*
20
+
* If detection fails, falls back to PUBLIC_BLOG_FALLBACK_URL or returns 404.
21
+
*
22
+
* Supports multiple Leaflet publications:
23
+
* - If PUBLIC_LEAFLET_BLOG_PUBLICATION is set, only checks that specific publication
24
+
* - Otherwise, checks all publications for the document
25
+
*/
26
+
27
+
async function detectPostPlatform(
28
+
rkey: string
29
+
): Promise<{ platform: 'whitewind' | 'leaflet' | 'unknown'; url?: string }> {
30
+
try {
31
+
// Check WhiteWind first using atproto services
32
+
const whiteWindRecord = await withFallback(
33
+
PUBLIC_ATPROTO_DID,
34
+
async (agent) => {
35
+
try {
36
+
const response = await agent.com.atproto.repo.getRecord({
37
+
repo: PUBLIC_ATPROTO_DID,
38
+
collection: 'com.whtwnd.blog.entry',
39
+
rkey
40
+
});
41
+
return response.data;
42
+
} catch (err) {
43
+
// Record not found
44
+
return null;
45
+
}
46
+
},
47
+
true // Use PDS first for custom collections
48
+
);
49
+
50
+
if (whiteWindRecord) {
51
+
const value = whiteWindRecord.value as any;
52
+
// Skip drafts and non-public posts
53
+
if (!value?.isDraft && (!value?.visibility || value.visibility === 'public')) {
54
+
return {
55
+
platform: 'whitewind',
56
+
url: `https://whtwnd.com/${PUBLIC_ATPROTO_DID}/${rkey}`
57
+
};
58
+
}
59
+
}
60
+
61
+
// Check Leaflet using atproto services
62
+
const leafletRecord = await withFallback(
63
+
PUBLIC_ATPROTO_DID,
64
+
async (agent) => {
65
+
try {
66
+
const response = await agent.com.atproto.repo.getRecord({
67
+
repo: PUBLIC_ATPROTO_DID,
68
+
collection: 'pub.leaflet.document',
69
+
rkey
70
+
});
71
+
return response.data;
72
+
} catch (err) {
73
+
// Record not found
74
+
return null;
75
+
}
76
+
},
77
+
true // Use PDS first for custom collections
78
+
);
79
+
80
+
if (leafletRecord) {
81
+
const value = leafletRecord.value as any;
82
+
const publicationUri = value?.publication;
83
+
84
+
// Fetch publications to get base path
85
+
const { publications } = await fetchLeafletPublications();
86
+
const publication = publicationUri
87
+
? publications.find((p) => p.uri === publicationUri)
88
+
: null;
89
+
90
+
// If a specific blog publication is configured, check if this document belongs to it
91
+
if (PUBLIC_LEAFLET_BLOG_PUBLICATION && publication) {
92
+
if (publication.rkey !== PUBLIC_LEAFLET_BLOG_PUBLICATION) {
93
+
// Document belongs to a different publication, not the blog
94
+
return { platform: 'unknown' };
95
+
}
96
+
}
97
+
98
+
// Determine URL based on priority: env var → publication base_path → Leaflet /lish format
99
+
let url: string;
100
+
const publicationRkey = publication?.rkey || '';
6
101
7
-
if (!tid || !tidPattern.test(tid)) {
8
-
return new Response('Invalid TID', { status: 400 });
9
-
}
102
+
if (PUBLIC_LEAFLET_BASE_PATH) {
103
+
url = `${PUBLIC_LEAFLET_BASE_PATH}/${rkey}`;
104
+
} else if (publication?.basePath) {
105
+
url = `${publication.basePath}/${rkey}`;
106
+
} else if (publicationRkey) {
107
+
url = `https://leaflet.pub/lish/${PUBLIC_ATPROTO_DID}/${publicationRkey}/${rkey}`;
108
+
} else {
109
+
url = `https://leaflet.pub/${PUBLIC_ATPROTO_DID}/${rkey}`;
110
+
}
10
111
11
-
const queryString = url.search;
12
-
const targetUrl = `https://blog.ewancroft.uk/${tid}${queryString}`;
112
+
return {
113
+
platform: 'leaflet',
114
+
url
115
+
};
116
+
}
13
117
14
-
return new Response(null, {
15
-
status: 301,
16
-
headers: { Location: targetUrl }
17
-
});
118
+
return { platform: 'unknown' };
119
+
} catch (error) {
120
+
console.error('Error detecting post platform:', error);
121
+
return { platform: 'unknown' };
122
+
}
123
+
}
124
+
125
+
export const GET: RequestHandler = async ({ params, url }) => {
126
+
const rkey = params.rkey;
127
+
128
+
// Validate TID format (AT Protocol record key)
129
+
const tidPattern = /^[a-zA-Z0-9]{12,16}$/;
130
+
131
+
if (!rkey || !tidPattern.test(rkey)) {
132
+
return new Response('Invalid TID format. Expected 12-16 alphanumeric characters.', {
133
+
status: 400,
134
+
headers: {
135
+
'Content-Type': 'text/plain; charset=utf-8'
136
+
}
137
+
});
138
+
}
139
+
140
+
// Detect platform and get appropriate URL
141
+
const detection = await detectPostPlatform(rkey);
142
+
143
+
let targetUrl: string | null = null;
144
+
let statusCode = 301;
145
+
146
+
if (detection.platform !== 'unknown' && detection.url) {
147
+
// Found the post on WhiteWind or Leaflet
148
+
targetUrl = detection.url;
149
+
} else if (PUBLIC_BLOG_FALLBACK_URL) {
150
+
// Use fallback URL from environment variable
151
+
targetUrl = `${PUBLIC_BLOG_FALLBACK_URL}/${rkey}`;
152
+
} else {
153
+
// No fallback configured, return 404
154
+
const blogPublicationNote = PUBLIC_LEAFLET_BLOG_PUBLICATION
155
+
? `\n\nNote: Only checking Leaflet publication: ${PUBLIC_LEAFLET_BLOG_PUBLICATION}`
156
+
: '';
157
+
158
+
return new Response(
159
+
`Blog post not found: ${rkey}
160
+
161
+
This post could not be found on WhiteWind or Leaflet platforms.${blogPublicationNote}
162
+
163
+
Please check:
164
+
- WhiteWind: https://whtwnd.com
165
+
- Leaflet: https://leaflet.pub`,
166
+
{
167
+
status: 404,
168
+
headers: {
169
+
'Content-Type': 'text/plain; charset=utf-8'
170
+
}
171
+
}
172
+
);
173
+
}
174
+
175
+
// Preserve query string
176
+
const queryString = url.search;
177
+
targetUrl += queryString;
178
+
179
+
// Use 301 for permanent redirect (better for SEO)
180
+
return new Response(null, {
181
+
status: statusCode,
182
+
headers: {
183
+
Location: targetUrl,
184
+
'Cache-Control': 'public, max-age=31536000, immutable'
185
+
}
186
+
});
18
187
};