tangled
alpha
login
or
join now
knotbin.com
/
blog
0
fork
atom
Leaflet Blog in Deno Fresh
0
fork
atom
overview
issues
pulls
pipelines
fixy
knotbin.com
10 months ago
b70a936d
e68f382c
+199
-127
16 changed files
expand all
collapse all
unified
split
.github
workflows
deploy.yml
components
bsky-comments
Comment.tsx
CommentFilters.tsx
PostSummary.tsx
footer.tsx
post-info.tsx
typography.tsx
deno.json
islands
CommentSection.tsx
layout.tsx
post-list.tsx
lib
api.ts
bsky.ts
routes
post
[slug].tsx
rss.ts
static
styles.css
-2
.github/workflows/deploy.yml
···
32
32
project: "roscoerubin-blog-23"
33
33
entrypoint: "main.ts"
34
34
root: "."
35
35
-
36
36
-
+18
-17
components/bsky-comments/Comment.tsx
···
1
1
-
import { AppBskyFeedDefs, AppBskyFeedPost } from 'npm:@atproto/api';
1
1
+
import { AppBskyFeedDefs, AppBskyFeedPost } from "npm:@atproto/api";
2
2
3
3
type CommentProps = {
4
4
comment: AppBskyFeedDefs.ThreadViewPost;
···
12
12
if (!AppBskyFeedPost.isRecord(comment.post.record)) return null;
13
13
// filter out replies that match any of the commentFilters, by ensuring they all return false
14
14
if (filters && !filters.every((filter) => !filter(comment))) return null;
15
15
-
16
15
17
16
const styles = `
18
17
.container {
···
173
172
target="_blank"
174
173
rel="noreferrer noopener"
175
174
>
176
176
-
{author.avatar ? (
177
177
-
<img
178
178
-
src={comment.post.author.avatar}
179
179
-
alt="avatar"
180
180
-
className={avatarClassName}
181
181
-
/>
182
182
-
) : (
183
183
-
<div className={avatarClassName} />
184
184
-
)}
175
175
+
{author.avatar
176
176
+
? (
177
177
+
<img
178
178
+
src={comment.post.author.avatar}
179
179
+
alt="avatar"
180
180
+
className={avatarClassName}
181
181
+
/>
182
182
+
)
183
183
+
: <div className={avatarClassName} />}
185
184
<p className="authorName">
186
186
-
{author.displayName ?? author.handle}{' '}
185
185
+
{author.displayName ?? author.handle}{" "}
187
186
<span className="handle">@{author.handle}</span>
188
187
</p>
189
188
</a>
190
189
<a
191
191
-
href={`https://bsky.app/profile/${author.did}/post/${comment.post.uri
192
192
-
.split('/')
193
193
-
.pop()}`}
190
190
+
href={`https://bsky.app/profile/${author.did}/post/${
191
191
+
comment.post.uri
192
192
+
.split("/")
193
193
+
.pop()
194
194
+
}`}
194
195
target="_blank"
195
196
rel="noreferrer noopener"
196
197
>
···
272
273
if (
273
274
!AppBskyFeedDefs.isThreadViewPost(a) ||
274
275
!AppBskyFeedDefs.isThreadViewPost(b) ||
275
275
-
!('post' in a) ||
276
276
-
!('post' in b)
276
276
+
!("post" in a) ||
277
277
+
!("post" in b)
277
278
) {
278
279
return 0;
279
280
}
+26
-18
components/bsky-comments/CommentFilters.tsx
···
1
1
-
import { AppBskyFeedPost, type AppBskyFeedDefs } from 'npm:@atproto/api';
1
1
+
import { type AppBskyFeedDefs, AppBskyFeedPost } from "npm:@atproto/api";
2
2
3
3
const MinLikeCountFilter = (
4
4
-
min: number
5
5
-
): ((comment: AppBskyFeedDefs.ThreadViewPost) => boolean) => {
4
4
+
min: number,
5
5
+
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
6
6
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
7
7
return (comment.post.likeCount ?? 0) < min;
8
8
};
9
9
};
10
10
11
11
const MinCharacterCountFilter = (
12
12
-
min: number
13
13
-
): ((comment: AppBskyFeedDefs.ThreadViewPost) => boolean) => {
12
12
+
min: number,
13
13
+
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
14
14
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
15
15
if (!AppBskyFeedPost.isRecord(comment.post.record)) {
16
16
return false;
···
21
21
};
22
22
23
23
const TextContainsFilter = (
24
24
-
text: string
25
25
-
): ((comment: AppBskyFeedDefs.ThreadViewPost) => boolean) => {
24
24
+
text: string,
25
25
+
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
26
26
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
27
27
if (!AppBskyFeedPost.isRecord(comment.post.record)) {
28
28
return false;
···
33
33
};
34
34
35
35
const ExactMatchFilter = (
36
36
-
text: string
37
37
-
): ((comment: AppBskyFeedDefs.ThreadViewPost) => boolean) => {
36
36
+
text: string,
37
37
+
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
38
38
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
39
39
if (!AppBskyFeedPost.isRecord(comment.post.record)) {
40
40
return false;
···
44
44
};
45
45
};
46
46
47
47
-
/*
48
48
-
* This function allows you to filter out comments based on likes,
49
49
-
* characters, text, pins, or exact matches.
50
50
-
*/
47
47
+
/*
48
48
+
* This function allows you to filter out comments based on likes,
49
49
+
* characters, text, pins, or exact matches.
50
50
+
*/
51
51
export const Filters: {
52
52
-
MinLikeCountFilter: (min: number) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
53
53
-
MinCharacterCountFilter: (min: number) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
54
54
-
TextContainsFilter: (text: string) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
55
55
-
ExactMatchFilter: (text: string) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
52
52
+
MinLikeCountFilter: (
53
53
+
min: number,
54
54
+
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
55
55
+
MinCharacterCountFilter: (
56
56
+
min: number,
57
57
+
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
58
58
+
TextContainsFilter: (
59
59
+
text: string,
60
60
+
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
61
61
+
ExactMatchFilter: (
62
62
+
text: string,
63
63
+
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
56
64
NoLikes: (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
57
65
NoPins: (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
58
66
} = {
···
61
69
TextContainsFilter,
62
70
ExactMatchFilter,
63
71
NoLikes: MinLikeCountFilter(0),
64
64
-
NoPins: ExactMatchFilter('📌'),
72
72
+
NoPins: ExactMatchFilter("📌"),
65
73
};
66
74
67
75
export default Filters;
+2
-2
components/bsky-comments/PostSummary.tsx
···
1
1
-
import { AppBskyFeedDefs } from 'npm:@atproto/api';
1
1
+
import { AppBskyFeedDefs } from "npm:@atproto/api";
2
2
3
3
type PostSummaryProps = {
4
4
postUrl: string;
···
216
216
</a>
217
217
<h2 className="commentsTitle">Comments</h2>
218
218
<p className="replyText">
219
219
-
Join the conversation by{' '}
219
219
+
Join the conversation by{" "}
220
220
<a
221
221
className="link"
222
222
href={postUrl}
+10
-3
components/footer.tsx
···
1
1
-
import { siBluesky as BlueskyIcon, siGithub as GithubIcon } from "npm:simple-icons";
1
1
+
import {
2
2
+
siBluesky as BlueskyIcon,
3
3
+
siGithub as GithubIcon,
4
4
+
} from "npm:simple-icons";
2
5
import { useState } from "preact/hooks";
3
6
import { env } from "../lib/env.ts";
4
7
···
25
28
>
26
29
<path d={BlueskyIcon.path} />
27
30
</svg>
28
28
-
<span class="opacity-50 group-hover:opacity-100 transition-opacity">Bluesky</span>
31
31
+
<span class="opacity-50 group-hover:opacity-100 transition-opacity">
32
32
+
Bluesky
33
33
+
</span>
29
34
<div class="absolute bottom-0 left-0 w-full h-px bg-current scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out group-hover:origin-left group-data-[hovered=false]:origin-right" />
30
35
</a>
31
36
<a
···
45
50
>
46
51
<path d={GithubIcon.path} />
47
52
</svg>
48
48
-
<span class="opacity-50 group-hover:opacity-100 transition-opacity">GitHub</span>
53
53
+
<span class="opacity-50 group-hover:opacity-100 transition-opacity">
54
54
+
GitHub
55
55
+
</span>
49
56
<div class="absolute bottom-0 left-0 w-full h-px bg-current scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out group-hover:origin-left group-data-[hovered=false]:origin-right" />
50
57
</a>
51
58
</footer>
+7
-5
components/post-info.tsx
···
31
31
children?: ComponentChildren;
32
32
}) {
33
33
const readingTime = getReadingTime(content);
34
34
-
34
34
+
35
35
return (
36
36
<Paragraph className={className}>
37
37
{includeAuthor && (
···
47
47
)}
48
48
{createdAt && (
49
49
<>
50
50
-
<time dateTime={createdAt}>{date(new Date(createdAt))}</time>
51
51
-
{" "}·{" "}
50
50
+
<time dateTime={createdAt}>{date(new Date(createdAt))}</time>{" "}
51
51
+
·{" "}
52
52
</>
53
53
)}
54
54
-
<span >
55
55
-
<span style={{ lineHeight: 1, marginRight: '0.25rem' }}>{readingTime} min read</span>
54
54
+
<span>
55
55
+
<span style={{ lineHeight: 1, marginRight: "0.25rem" }}>
56
56
+
{readingTime} min read
57
57
+
</span>
56
58
</span>
57
59
{children}
58
60
</Paragraph>
+9
-2
components/typography.tsx
···
48
48
className,
49
49
...props
50
50
}: h.JSX.HTMLAttributes<HTMLParagraphElement>) {
51
51
-
return <p className={cx("font-sans text-pretty", className?.toString())} {...props} />;
51
51
+
return (
52
52
+
<p
53
53
+
className={cx("font-sans text-pretty", className?.toString())}
54
54
+
{...props}
55
55
+
/>
56
56
+
);
52
57
}
53
58
54
54
-
export function Code({ className, ...props }: h.JSX.HTMLAttributes<HTMLElement>) {
59
59
+
export function Code(
60
60
+
{ className, ...props }: h.JSX.HTMLAttributes<HTMLElement>,
61
61
+
) {
55
62
return (
56
63
<code
57
64
className={cx(
+3
deno.json
···
22
22
],
23
23
"imports": {
24
24
"$fresh/": "https://deno.land/x/fresh@1.7.3/",
25
25
+
"@atcute/atproto": "npm:@atcute/atproto@^3.0.1",
26
26
+
"@atcute/client": "npm:@atcute/client@^4.0.1",
25
27
"@atcute/whitewind": "npm:@atcute/whitewind@^3.0.1",
26
28
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",
27
29
"@preact-icons/cg": "jsr:@preact-icons/cg@^1.0.13",
···
31
33
"preact/": "https://esm.sh/preact@10.22.0/",
32
34
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
33
35
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
36
36
+
"rss": "npm:rss@^1.2.2",
34
37
"tailwindcss": "npm:tailwindcss@3.4.1",
35
38
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
36
39
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
+66
-40
islands/CommentSection.tsx
···
1
1
-
import { useState, useEffect } from 'preact/hooks';
2
2
-
import { AppBskyFeedDefs, type AppBskyFeedGetPostThread } from 'npm:@atproto/api';
3
3
-
import { CommentOptions } from '../components/bsky-comments/types.tsx';
4
4
-
import { PostSummary } from '../components/bsky-comments/PostSummary.tsx';
5
5
-
import { Comment } from '../components/bsky-comments/Comment.tsx';
1
1
+
import { useEffect, useState } from "preact/hooks";
2
2
+
import {
3
3
+
AppBskyFeedDefs,
4
4
+
type AppBskyFeedGetPostThread,
5
5
+
} from "npm:@atproto/api";
6
6
+
import { CommentOptions } from "../components/bsky-comments/types.tsx";
7
7
+
import { PostSummary } from "../components/bsky-comments/PostSummary.tsx";
8
8
+
import { Comment } from "../components/bsky-comments/Comment.tsx";
6
9
7
10
const getAtUri = (uri: string): string => {
8
8
-
if (!uri.startsWith('at://') && uri.includes('bsky.app/profile/')) {
11
11
+
if (!uri.startsWith("at://") && uri.includes("bsky.app/profile/")) {
9
12
const match = uri.match(/profile\/([\w:.]+)\/post\/([\w]+)/);
10
13
if (match) {
11
14
const [, did, postId] = match;
···
16
19
};
17
20
18
21
/**
19
19
-
* This component displays a comment section for a post.
20
20
-
* It fetches the comments for a post and displays them in a threaded format.
21
21
-
*/
22
22
+
* This component displays a comment section for a post.
23
23
+
* It fetches the comments for a post and displays them in a threaded format.
24
24
+
*/
22
25
export const CommentSection = ({
23
26
uri: propUri,
24
27
author,
···
26
29
commentFilters,
27
30
}: CommentOptions): any => {
28
31
const [uri, setUri] = useState<string | null>(null);
29
29
-
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(null);
32
32
+
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
33
33
+
null,
34
34
+
);
30
35
const [error, setError] = useState<string | null>(null);
31
36
const [visibleCount, setVisibleCount] = useState(5);
32
37
···
223
228
224
229
useEffect(() => {
225
230
let isSubscribed = true;
226
226
-
231
231
+
227
232
const initializeUri = async () => {
228
233
if (propUri) {
229
234
setUri(propUri);
···
233
238
if (author) {
234
239
try {
235
240
const currentUrl = window.location.href;
236
236
-
const apiUrl = `https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=*&url=${encodeURIComponent(
237
237
-
currentUrl
238
238
-
)}&author=${author}&sort=top`;
239
239
-
241
241
+
const apiUrl =
242
242
+
`https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=*&url=${
243
243
+
encodeURIComponent(
244
244
+
currentUrl,
245
245
+
)
246
246
+
}&author=${author}&sort=top`;
247
247
+
240
248
const response = await fetch(apiUrl);
241
249
const data = await response.json();
242
250
···
245
253
const post = data.posts[0];
246
254
setUri(post.uri);
247
255
} else {
248
248
-
setError('No matching post found');
256
256
+
setError("No matching post found");
249
257
if (onEmpty) {
250
250
-
onEmpty({ code: 'not_found', message: 'No matching post found' });
258
258
+
onEmpty({
259
259
+
code: "not_found",
260
260
+
message: "No matching post found",
261
261
+
});
251
262
}
252
263
}
253
264
}
254
265
} catch (err) {
255
266
if (isSubscribed) {
256
256
-
setError('Error fetching post');
267
267
+
setError("Error fetching post");
257
268
if (onEmpty) {
258
258
-
onEmpty({ code: 'fetching_error', message: 'Error fetching post' });
269
269
+
onEmpty({
270
270
+
code: "fetching_error",
271
271
+
message: "Error fetching post",
272
272
+
});
259
273
}
260
274
}
261
275
}
···
274
288
275
289
const fetchThreadData = async () => {
276
290
if (!uri) return;
277
277
-
291
291
+
278
292
try {
279
293
const thread = await getPostThread(uri);
280
294
if (isSubscribed) {
···
282
296
}
283
297
} catch (err) {
284
298
if (isSubscribed) {
285
285
-
setError('Error loading comments');
299
299
+
setError("Error loading comments");
286
300
if (onEmpty) {
287
301
onEmpty({
288
288
-
code: 'comment_loading_error',
289
289
-
message: 'Error loading comments',
302
302
+
code: "comment_loading_error",
303
303
+
message: "Error loading comments",
290
304
});
291
305
}
292
306
}
···
307
321
if (!uri) return null;
308
322
309
323
if (error) {
310
310
-
return <div className="container"><style>{styles}</style><p className="errorText">{error}</p></div>;
324
324
+
return (
325
325
+
<div className="container">
326
326
+
<style>{styles}</style>
327
327
+
<p className="errorText">{error}</p>
328
328
+
</div>
329
329
+
);
311
330
}
312
331
313
332
if (!thread) {
314
314
-
return <div className="container"><style>{styles}</style><p className="loadingText">Loading comments...</p></div>;
333
333
+
return (
334
334
+
<div className="container">
335
335
+
<style>{styles}</style>
336
336
+
<p className="loadingText">Loading comments...</p>
337
337
+
</div>
338
338
+
);
315
339
}
316
340
317
341
let postUrl: string = uri;
318
318
-
if (uri.startsWith('at://')) {
319
319
-
const [, , did, _, rkey] = uri.split('/');
342
342
+
if (uri.startsWith("at://")) {
343
343
+
const [, , did, _, rkey] = uri.split("/");
320
344
postUrl = `https://bsky.app/profile/${did}/post/${rkey}`;
321
345
}
322
346
···
328
352
</div>
329
353
);
330
354
}
331
331
-
355
355
+
332
356
// Safe sort - ensure we're working with valid objects
333
333
-
const sortedReplies = [...thread.replies].filter(reply =>
357
357
+
const sortedReplies = [...thread.replies].filter((reply) =>
334
358
AppBskyFeedDefs.isThreadViewPost(reply)
335
359
).sort(sortByLikes);
336
360
···
360
384
);
361
385
};
362
386
363
363
-
const getPostThread = async (uri: string): Promise<AppBskyFeedDefs.ThreadViewPost> => {
387
387
+
const getPostThread = async (
388
388
+
uri: string,
389
389
+
): Promise<AppBskyFeedDefs.ThreadViewPost> => {
364
390
const atUri = getAtUri(uri);
365
391
const params = new URLSearchParams({ uri: atUri });
366
392
367
393
const res = await fetch(
368
368
-
'https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?' +
394
394
+
"https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?" +
369
395
params.toString(),
370
396
{
371
371
-
method: 'GET',
397
397
+
method: "GET",
372
398
headers: {
373
373
-
Accept: 'application/json',
399
399
+
Accept: "application/json",
374
400
},
375
375
-
cache: 'no-store',
376
376
-
}
401
401
+
cache: "no-store",
402
402
+
},
377
403
);
378
404
379
405
if (!res.ok) {
380
406
console.error(await res.text());
381
381
-
throw new Error('Failed to fetch post thread');
407
407
+
throw new Error("Failed to fetch post thread");
382
408
}
383
409
384
410
const data = (await res.json()) as AppBskyFeedGetPostThread.OutputSchema;
385
411
386
412
if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) {
387
387
-
throw new Error('Could not find thread');
413
413
+
throw new Error("Could not find thread");
388
414
}
389
415
390
416
return data.thread;
···
394
420
if (
395
421
!AppBskyFeedDefs.isThreadViewPost(a) ||
396
422
!AppBskyFeedDefs.isThreadViewPost(b) ||
397
397
-
!('post' in a) ||
398
398
-
!('post' in b)
423
423
+
!("post" in a) ||
424
424
+
!("post" in b)
399
425
) {
400
426
return 0;
401
427
}
402
428
const aPost = a as AppBskyFeedDefs.ThreadViewPost;
403
429
const bPost = b as AppBskyFeedDefs.ThreadViewPost;
404
430
return (bPost.post.likeCount ?? 0) - (aPost.post.likeCount ?? 0);
405
405
-
};
431
431
+
};
+3
-3
islands/layout.tsx
···
42
42
</a>
43
43
<div class="h-4 w-px bg-slate-200 dark:bg-slate-700"></div>
44
44
<div class="text-base flex items-center gap-7">
45
45
-
<a
46
46
-
href="/"
47
47
-
class="relative group"
45
45
+
<a
46
46
+
href="/"
47
47
+
class="relative group"
48
48
data-current={isActive("/")}
49
49
data-hovered={blogHovered}
50
50
onMouseEnter={() => setBlogHovered(true)}
+3
-1
islands/post-list.tsx
···
7
7
uri: string;
8
8
}
9
9
10
10
-
export default function PostList({ posts: initialPosts }: { posts: PostRecord[] }) {
10
10
+
export default function PostList(
11
11
+
{ posts: initialPosts }: { posts: PostRecord[] },
12
12
+
) {
11
13
const posts = useSignal(initialPosts);
12
14
13
15
useEffect(() => {
+9
-3
lib/api.ts
···
1
1
import { bsky } from "./bsky.ts";
2
2
import { env } from "./env.ts";
3
3
4
4
-
import { type ComAtprotoRepoListRecords } from "npm:@atcute/client/lexicons";
4
4
+
import { type ActorIdentifier } from "npm:@atcute/lexicons";
5
5
import { type ComWhtwndBlogEntry } from "@atcute/whitewind";
6
6
+
import { type ComAtprotoRepoListRecords } from "npm:@atcute/atproto";
6
7
7
8
export async function getPosts() {
8
9
const posts = await bsky.get("com.atproto.repo.listRecords", {
9
10
params: {
10
10
-
repo: env.NEXT_PUBLIC_BSKY_DID,
11
11
+
repo: env.NEXT_PUBLIC_BSKY_DID as ActorIdentifier,
11
12
collection: "com.whtwnd.blog.entry",
12
13
// todo: pagination
13
14
},
14
15
});
16
16
+
17
17
+
if ('error' in posts.data) {
18
18
+
throw new Error(posts.data.error);
19
19
+
}
20
20
+
15
21
return posts.data.records.filter(
16
22
drafts,
17
23
) as (ComAtprotoRepoListRecords.Record & {
···
28
34
export async function getPost(rkey: string) {
29
35
const post = await bsky.get("com.atproto.repo.getRecord", {
30
36
params: {
31
31
-
repo: env.NEXT_PUBLIC_BSKY_DID,
37
37
+
repo: env.NEXT_PUBLIC_BSKY_DID as ActorIdentifier,
32
38
rkey: rkey,
33
39
collection: "com.whtwnd.blog.entry",
34
40
},
+4
-5
lib/bsky.ts
···
1
1
-
import { CredentialManager, XRPC } from "npm:@atcute/client";
1
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
2
3
3
import { env } from "./env.ts";
4
4
5
5
-
const handler = new CredentialManager({
6
6
-
service: env.NEXT_PUBLIC_BSKY_PDS,
7
7
-
fetch,
5
5
+
const handler = simpleFetchHandler({
6
6
+
service: env.NEXT_PUBLIC_BSKY_PDS
8
7
});
9
9
-
export const bsky = new XRPC({ handler });
8
8
+
export const bsky = new Client({ handler });
+4
-1
routes/post/[slug].tsx
···
111
111
<>
112
112
<Head>
113
113
<title>{post.value.title} — knotbin</title>
114
114
-
<meta name="description" content={post.value.subtitle || "by Roscoe Rubin-Rottenberg"} />
114
114
+
<meta
115
115
+
name="description"
116
116
+
content={post.value.subtitle || "by Roscoe Rubin-Rottenberg"}
117
117
+
/>
115
118
{/* Merge GFM's default styles with our dark-mode overrides */}
116
119
<style
117
120
dangerouslySetInnerHTML={{ __html: CSS + transparentDarkModeCSS }}
+13
-13
routes/rss.ts
···
21
21
});
22
22
23
23
for (const post of posts) {
24
24
-
const description = post.value.subtitle
24
24
+
const description = post.value.subtitle
25
25
? `${post.value.subtitle}\n\n${await unified()
26
26
-
.use(remarkParse)
27
27
-
.use(remarkRehype)
28
28
-
.use(rehypeFormat)
29
29
-
.use(rehypeStringify)
30
30
-
.process(post.value.content)
31
31
-
.then((v) => v.toString())}`
26
26
+
.use(remarkParse)
27
27
+
.use(remarkRehype)
28
28
+
.use(rehypeFormat)
29
29
+
.use(rehypeStringify)
30
30
+
.process(post.value.content)
31
31
+
.then((v) => v.toString())}`
32
32
: await unified()
33
33
-
.use(remarkParse)
34
34
-
.use(remarkRehype)
35
35
-
.use(rehypeFormat)
36
36
-
.use(rehypeStringify)
37
37
-
.process(post.value.content)
38
38
-
.then((v) => v.toString());
33
33
+
.use(remarkParse)
34
34
+
.use(remarkRehype)
35
35
+
.use(rehypeFormat)
36
36
+
.use(rehypeStringify)
37
37
+
.process(post.value.content)
38
38
+
.then((v) => v.toString());
39
39
40
40
rss.item({
41
41
title: post.value.title ?? "Untitled",
+22
-12
static/styles.css
···
1
1
-
@import url('https://api.fonts.coollabs.io/css2?family=Inter:wght@400;700&display=swap');
2
2
-
@import url('https://api.fonts.coollabs.io/css2?family=Libre+Bodoni:ital,wght@0,400;0,700;1,400&display=swap');
1
1
+
@import url("https://api.fonts.coollabs.io/css2?family=Inter:wght@400;700&display=swap");
2
2
+
@import url("https://api.fonts.coollabs.io/css2?family=Libre+Bodoni:ital,wght@0,400;0,700;1,400&display=swap");
3
3
@font-face {
4
4
-
font-family: 'Berkeley Mono';
5
5
-
src: url('/path/to/local/fonts/BerkeleyMono-Regular.woff2') format('woff2'),
6
6
-
url('/path/to/local/fonts/BerkeleyMono-Regular.woff') format('woff');
4
4
+
font-family: "Berkeley Mono";
5
5
+
src:
6
6
+
url("/path/to/local/fonts/BerkeleyMono-Regular.woff2") format("woff2"),
7
7
+
url("/path/to/local/fonts/BerkeleyMono-Regular.woff") format("woff");
7
8
font-weight: 400;
8
9
font-style: normal;
9
10
}
···
18
19
}
19
20
20
21
:root {
21
21
-
--font-sans: 'Inter', sans-serif;
22
22
-
--font-serif: 'Libre Bodoni', serif;
23
23
-
--font-mono: 'Berkeley Mono', monospace;
22
22
+
--font-sans: "Inter", sans-serif;
23
23
+
--font-serif: "Libre Bodoni", serif;
24
24
+
--font-mono: "Berkeley Mono", monospace;
24
25
}
25
26
26
26
-
.font-sans { font-family: var(--font-sans); }
27
27
-
.font-serif { font-family: var(--font-serif); }
28
28
-
.font-mono { font-family: var(--font-mono); }
29
29
-
.font-serif-italic { font-family: var(--font-serif); font-style: italic;}
27
27
+
.font-sans {
28
28
+
font-family: var(--font-sans);
29
29
+
}
30
30
+
.font-serif {
31
31
+
font-family: var(--font-serif);
32
32
+
}
33
33
+
.font-mono {
34
34
+
font-family: var(--font-mono);
35
35
+
}
36
36
+
.font-serif-italic {
37
37
+
font-family: var(--font-serif);
38
38
+
font-style: italic;
39
39
+
}
30
40
31
41
/*
32
42
The default border color has changed to `currentColor` in Tailwind CSS v4,