tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
fix post links to standard site blogs
awarm.space
4 weeks ago
856e0c8f
c81be9ad
+123
-76
16 changed files
expand all
collapse all
unified
split
app
(home-pages)
notifications
BskyPostEmbedNotification.tsx
CommentMentionNotification.tsx
CommentNotication.tsx
MentionNotification.tsx
QuoteNotification.tsx
RecommendNotification.tsx
ReplyNotification.tsx
[leaflet_id]
actions
ShareOptions
index.tsx
api
rpc
[command]
search_publication_documents.ts
lish
[did]
[publication]
[rkey]
getPostPageData.ts
dashboard
PublishedPostsLists.tsx
generateFeed.ts
page.tsx
createPub
getPublicationURL.ts
components
PageSWRDataProvider.tsx
PostListing.tsx
+2
-5
app/(home-pages)/notifications/BskyPostEmbedNotification.tsx
···
2
2
import { ContentLayout, Notification } from "./Notification";
3
3
import { HydratedBskyPostEmbedNotification } from "src/notifications";
4
4
import { AtUri } from "@atproto/api";
5
5
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
5
6
6
7
export const BskyPostEmbedNotification = (
7
8
props: HydratedBskyPostEmbedNotification,
···
11
12
12
13
if (!docRecord) return null;
13
14
14
14
-
const docUri = new AtUri(props.document.uri);
15
15
-
const rkey = docUri.rkey;
16
16
-
const did = docUri.host;
17
17
-
18
18
-
const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`;
15
15
+
const href = getDocumentURL(docRecord, props.document.uri, pubRecord);
19
16
20
17
const embedder = props.documentCreatorHandle
21
18
? `@${props.documentCreatorHandle}`
+4
-6
app/(home-pages)/notifications/CommentMentionNotification.tsx
···
8
8
Notification,
9
9
} from "./Notification";
10
10
import { AtUri } from "@atproto/api";
11
11
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
11
12
12
13
export const CommentMentionNotification = (
13
14
props: HydratedCommentMentionNotification,
···
19
20
const profileRecord = props.commentData.bsky_profiles
20
21
?.record as AppBskyActorProfile.Record;
21
22
const pubRecord = props.normalizedPublication;
22
22
-
const docUri = new AtUri(props.commentData.documents?.uri!);
23
23
-
const rkey = docUri.rkey;
24
24
-
const did = docUri.host;
25
23
26
26
-
const href = pubRecord
27
27
-
? `${pubRecord.url}/${rkey}?interactionDrawer=comments`
28
28
-
: `/p/${did}/${rkey}?interactionDrawer=comments`;
24
24
+
const href =
25
25
+
getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) +
26
26
+
"?interactionDrawer=comments";
29
27
30
28
const commenter = props.commenterHandle
31
29
? `@${props.commenterHandle}`
+4
-6
app/(home-pages)/notifications/CommentNotication.tsx
···
10
10
Notification,
11
11
} from "./Notification";
12
12
import { AtUri } from "@atproto/api";
13
13
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
13
14
14
15
export const CommentNotification = (props: HydratedCommentNotification) => {
15
16
const docRecord = props.normalizedDocument;
···
24
25
props.commentData.bsky_profiles?.handle ||
25
26
"Someone";
26
27
const pubRecord = props.normalizedPublication;
27
27
-
const docUri = new AtUri(props.commentData.documents?.uri!);
28
28
-
const rkey = docUri.rkey;
29
29
-
const did = docUri.host;
30
28
31
31
-
const href = pubRecord
32
32
-
? `${pubRecord.url}/${rkey}?interactionDrawer=comments`
33
33
-
: `/p/${did}/${rkey}?interactionDrawer=comments`;
29
29
+
const href =
30
30
+
getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) +
31
31
+
"?interactionDrawer=comments";
34
32
35
33
return (
36
34
<Notification
+2
-7
app/(home-pages)/notifications/MentionNotification.tsx
···
2
2
import { ContentLayout, Notification } from "./Notification";
3
3
import { HydratedMentionNotification } from "src/notifications";
4
4
import { AtUri } from "@atproto/api";
5
5
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
5
6
6
7
export const MentionNotification = (props: HydratedMentionNotification) => {
7
8
const docRecord = props.normalizedDocument;
···
9
10
10
11
if (!docRecord) return null;
11
12
12
12
-
const docUri = new AtUri(props.document.uri);
13
13
-
const rkey = docUri.rkey;
14
14
-
const did = docUri.host;
15
15
-
16
16
-
const href = pubRecord
17
17
-
? `${pubRecord.url}/${rkey}`
18
18
-
: `/p/${did}/${rkey}`;
13
13
+
const href = getDocumentURL(docRecord, props.document.uri, pubRecord);
19
14
20
15
let actionText: React.ReactNode;
21
16
let mentionedItemName: string | undefined;
+2
-6
app/(home-pages)/notifications/QuoteNotification.tsx
···
3
3
import { HydratedQuoteNotification } from "src/notifications";
4
4
import { AtUri } from "@atproto/api";
5
5
import { Avatar } from "components/Avatar";
6
6
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
6
7
7
8
export const QuoteNotification = (props: HydratedQuoteNotification) => {
8
9
const postView = props.bskyPost.post_view as any;
···
13
14
14
15
if (!docRecord) return null;
15
16
16
16
-
const docUri = new AtUri(props.document.uri);
17
17
-
const rkey = docUri.rkey;
18
18
-
const did = docUri.host;
19
17
const postText = postView.record?.text || "";
20
18
21
21
-
const href = pubRecord
22
22
-
? `${pubRecord.url}/${rkey}`
23
23
-
: `/p/${did}/${rkey}`;
19
19
+
const href = getDocumentURL(docRecord, props.document.uri, pubRecord);
24
20
25
21
return (
26
22
<Notification
+2
-5
app/(home-pages)/notifications/RecommendNotification.tsx
···
5
5
import { Avatar } from "components/Avatar";
6
6
import { AtUri } from "@atproto/api";
7
7
import { RecommendTinyFilled } from "components/Icons/RecommendTiny";
8
8
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
8
9
9
10
export const RecommendNotification = (
10
11
props: HydratedRecommendNotification,
···
26
27
27
28
if (!docRecord) return null;
28
29
29
29
-
const docUri = new AtUri(props.document.uri);
30
30
-
const rkey = docUri.rkey;
31
31
-
const did = docUri.host;
32
32
-
33
33
-
const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`;
30
30
+
const href = getDocumentURL(docRecord, props.document.uri, pubRecord);
34
31
35
32
return (
36
33
<Notification
+4
-6
app/(home-pages)/notifications/ReplyNotification.tsx
···
10
10
import { PubLeafletComment } from "lexicons/api";
11
11
import { AppBskyActorProfile, AtUri } from "@atproto/api";
12
12
import { blobRefToSrc } from "src/utils/blobRefToSrc";
13
13
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
13
14
14
15
export const ReplyNotification = (props: HydratedCommentNotification) => {
15
16
const docRecord = props.normalizedDocument;
···
32
33
props.parentData?.bsky_profiles?.handle ||
33
34
"Someone";
34
35
35
35
-
const docUri = new AtUri(props.commentData.documents?.uri!);
36
36
-
const rkey = docUri.rkey;
37
37
-
const did = docUri.host;
38
36
const pubRecord = props.normalizedPublication;
39
37
40
40
-
const href = pubRecord
41
41
-
? `${pubRecord.url}/${rkey}?interactionDrawer=comments`
42
42
-
: `/p/${did}/${rkey}?interactionDrawer=comments`;
38
38
+
const href =
39
39
+
getDocumentURL(docRecord, props.commentData.documents?.uri!, pubRecord) +
40
40
+
"?interactionDrawer=comments";
43
41
44
42
return (
45
43
<Notification
+9
-7
app/[leaflet_id]/actions/ShareOptions/index.tsx
···
14
14
useLeafletPublicationData,
15
15
} from "components/PageSWRDataProvider";
16
16
import { ShareSmall } from "components/Icons/ShareSmall";
17
17
-
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
17
17
+
import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL";
18
18
import { AtUri } from "@atproto/syntax";
19
19
import { useIsMobile } from "src/hooks/isMobile";
20
20
···
89
89
let { permission_token } = useReplicache();
90
90
let { data: pub, normalizedDocument } = useLeafletPublicationData();
91
91
92
92
-
let docURI = pub?.documents ? new AtUri(pub?.documents.uri) : null;
93
93
-
let postLink = !docURI
94
94
-
? null
95
95
-
: pub?.publications
96
96
-
? `${getPublicationURL(pub.publications)}/${docURI.rkey}`
97
97
-
: `p/${docURI.host}/${docURI.rkey}`;
92
92
+
let postLink =
93
93
+
pub?.documents && normalizedDocument
94
94
+
? getDocumentURL(
95
95
+
normalizedDocument,
96
96
+
pub.documents.uri,
97
97
+
pub?.publications || null,
98
98
+
)
99
99
+
: null;
98
100
let publishLink = useReadOnlyShareLink();
99
101
let [collabLink, setCollabLink] = useState<null | string>(null);
100
102
useEffect(() => {
+7
-5
app/api/rpc/[command]/search_publication_documents.ts
···
2
2
import { z } from "zod";
3
3
import { makeRoute } from "../lib";
4
4
import type { Env } from "./route";
5
5
-
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
5
5
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
6
6
+
import { normalizeDocumentRecord } from "src/utils/normalizeRecords";
6
7
7
8
export type SearchPublicationDocumentsReturnType = Awaited<
8
9
ReturnType<(typeof search_publication_documents)["handler"]>
···
37
38
}
38
39
39
40
const result = documents.map((d) => {
40
40
-
const docUri = new AtUri(d.documents.uri);
41
41
-
const pubUrl = getPublicationURL(d.publications);
41
41
+
const normalizedDoc = normalizeDocumentRecord(d.documents.data, d.documents.uri);
42
42
43
43
return {
44
44
uri: d.documents.uri,
45
45
-
title: (d.documents.data as { title?: string })?.title || "Untitled",
46
46
-
url: `${pubUrl}/${docUri.rkey}`,
45
45
+
title: normalizedDoc?.title || (d.documents.data as { title?: string })?.title || "Untitled",
46
46
+
url: normalizedDoc
47
47
+
? getDocumentURL(normalizedDoc, d.documents.uri, d.publications)
48
48
+
: `${d.documents.uri}`,
47
49
};
48
50
});
49
51
+7
-5
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
···
8
8
} from "src/utils/normalizeRecords";
9
9
import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api";
10
10
import { documentUriFilter } from "src/utils/uriHelpers";
11
11
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
11
12
12
13
export async function getPostPageData(did: string, rkey: string) {
13
14
let { data: documents } = await supabaseServerClient
···
46
47
);
47
48
48
49
// Fetch constellation backlinks for mentions
49
49
-
let aturi = new AtUri(document.uri);
50
50
-
const postUrl = normalizedPublication
51
51
-
? `${normalizedPublication.url}/${aturi.rkey}`
52
52
-
: `https://leaflet.pub/p/${aturi.host}/${aturi.rkey}`;
53
53
-
const constellationBacklinks = await getConstellationBacklinks(postUrl);
50
50
+
const postUrl = getDocumentURL(normalizedDocument, document.uri, normalizedPublication);
51
51
+
// Constellation needs an absolute URL
52
52
+
const absolutePostUrl = postUrl.startsWith("/")
53
53
+
? `https://leaflet.pub${postUrl}`
54
54
+
: postUrl;
55
55
+
const constellationBacklinks = await getConstellationBacklinks(absolutePostUrl);
54
56
55
57
// Deduplicate constellation backlinks (same post could appear in both links and embeds)
56
58
const uniqueBacklinks = Array.from(
+4
-3
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
···
9
9
} from "./PublicationSWRProvider";
10
10
import { Fragment } from "react";
11
11
import { useParams } from "next/navigation";
12
12
-
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
12
12
+
import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL";
13
13
import { SpeedyLink } from "components/SpeedyLink";
14
14
import { InteractionPreview } from "components/InteractionsPreview";
15
15
import { useLocalizedDate } from "src/hooks/useLocalizedDate";
···
71
71
const leaflet = publication.leaflets_in_publications.find(
72
72
(l) => l.doc === doc.uri,
73
73
);
74
74
+
const docUrl = getDocumentURL(doc.record, doc.uri, publication);
74
75
75
76
return (
76
77
<Fragment>
···
87
88
<a
88
89
className="hover:no-underline!"
89
90
target="_blank"
90
90
-
href={`${getPublicationURL(publication)}/${uri.rkey}`}
91
91
+
href={docUrl}
91
92
>
92
93
<h3 className="text-primary grow leading-snug">
93
94
{doc.record.title}
···
144
145
showComments={pubRecord?.preferences?.showComments !== false}
145
146
showMentions={pubRecord?.preferences?.showMentions !== false}
146
147
showRecommends={pubRecord?.preferences?.showRecommends !== false}
147
147
-
postUrl={`${getPublicationURL(publication)}/${uri.rkey}`}
148
148
+
postUrl={docUrl}
148
149
/>
149
150
</div>
150
151
</div>
+4
-2
app/lish/[did]/[publication]/generateFeed.ts
···
11
11
hasLeafletContent,
12
12
} from "src/utils/normalizeRecords";
13
13
import { publicationNameOrUriFilter } from "src/utils/uriHelpers";
14
14
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
14
15
15
16
export async function generateFeed(
16
17
did: string,
···
84
85
}
85
86
}
86
87
88
88
+
const docUrl = getDocumentURL(record, doc.documents.uri, pubRecord);
87
89
feed.addItem({
88
90
title: record.title,
89
91
description: record.description,
90
92
date: record.publishedAt ? new Date(record.publishedAt) : new Date(),
91
91
-
id: `${pubRecord.url}/${rkey}`,
92
92
-
link: `${pubRecord.url}/${rkey}`,
93
93
+
id: docUrl,
94
94
+
link: docUrl,
93
95
content: chunks.join(""),
94
96
});
95
97
}),
+4
-3
app/lish/[did]/[publication]/page.tsx
···
1
1
import { supabaseServerClient } from "supabase/serverClient";
2
2
import { AtUri } from "@atproto/syntax";
3
3
-
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
3
3
+
import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL";
4
4
import { BskyAgent } from "@atproto/api";
5
5
import { publicationNameOrUriFilter } from "src/utils/uriHelpers";
6
6
import { SubscribeWithBluesky } from "app/lish/Subscribe";
···
135
135
doc.documents.recommends_on_documents?.[0]?.count || 0;
136
136
let tags = doc_record.tags || [];
137
137
138
138
+
const docUrl = getDocumentURL(doc_record, doc.documents.uri, publication);
138
139
return (
139
140
<React.Fragment key={doc.documents?.uri}>
140
141
<div className="flex w-full grow flex-col ">
141
142
<SpeedyLink
142
142
-
href={`${getPublicationURL(publication)}/${uri.rkey}`}
143
143
+
href={docUrl}
143
144
className="publishedPost hover:no-underline! flex flex-col"
144
145
>
145
146
<h3 className="text-primary">{doc_record.title}</h3>
···
168
169
recommendsCount={recommends}
169
170
documentUri={doc.documents.uri}
170
171
tags={tags}
171
171
-
postUrl={`${getPublicationURL(publication)}/${uri.rkey}`}
172
172
+
postUrl={docUrl}
172
173
showComments={
173
174
record?.preferences?.showComments !== false
174
175
}
+45
app/lish/createPub/getPublicationURL.ts
···
5
5
import {
6
6
normalizePublicationRecord,
7
7
isLeafletPublication,
8
8
+
hasLeafletContent,
9
9
+
type NormalizedDocument,
8
10
type NormalizedPublication,
9
11
} from "src/utils/normalizeRecords";
10
12
···
44
46
const name = aturi.rkey || normalized?.name;
45
47
return `/lish/${aturi.host}/${encodeURIComponent(name || "")}`;
46
48
}
49
49
+
50
50
+
/**
51
51
+
* Gets the full URL for a document.
52
52
+
* Always appends the document's path property.
53
53
+
* For non-leaflet documents (content.$type !== "pub.leaflet.content"),
54
54
+
* always uses the full publication site URL, not internal /lish/ URLs.
55
55
+
*/
56
56
+
export function getDocumentURL(
57
57
+
doc: NormalizedDocument,
58
58
+
docUri: string,
59
59
+
publication?: PublicationInput | NormalizedPublication | null,
60
60
+
): string {
61
61
+
const path = doc.path || "/" + new AtUri(docUri).rkey;
62
62
+
const aturi = new AtUri(docUri);
63
63
+
64
64
+
const isNormalized =
65
65
+
!!publication &&
66
66
+
(publication as NormalizedPublication).$type === "site.standard.publication";
67
67
+
const normPub = isNormalized
68
68
+
? (publication as NormalizedPublication)
69
69
+
: publication
70
70
+
? normalizePublicationRecord((publication as PublicationInput).record)
71
71
+
: null;
72
72
+
const pubInput = isNormalized ? null : (publication as PublicationInput | null);
73
73
+
74
74
+
// Non-leaflet documents always use the full publication site URL
75
75
+
if (doc.content && !hasLeafletContent(doc) && normPub?.url) {
76
76
+
return normPub.url + path;
77
77
+
}
78
78
+
79
79
+
// For leaflet documents, use getPublicationURL (may return /lish/ internal paths)
80
80
+
if (pubInput) {
81
81
+
return getPublicationURL(pubInput) + path;
82
82
+
}
83
83
+
84
84
+
// When we only have a normalized publication, use its URL directly
85
85
+
if (normPub?.url) {
86
86
+
return normPub.url + path;
87
87
+
}
88
88
+
89
89
+
// Standalone document fallback
90
90
+
return `/p/${aturi.host}${path}`;
91
91
+
}
+21
-7
components/PageSWRDataProvider.tsx
···
8
8
import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data";
9
9
import { createContext, useContext, useMemo } from "react";
10
10
import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData";
11
11
-
import { getPublicationURL } from "app/lish/createPub/getPublicationURL";
11
11
+
import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL";
12
12
import { AtUri } from "@atproto/syntax";
13
13
import {
14
14
normalizeDocumentRecord,
···
119
119
// Compute the full post URL for sharing
120
120
let postShareLink: string | undefined;
121
121
if (publishedInPublication?.publications && publishedInPublication.documents) {
122
122
-
// Published in a publication - use publication URL + document rkey
123
123
-
const docUri = new AtUri(publishedInPublication.documents.uri);
124
124
-
postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`;
122
122
+
const normalizedDoc = normalizeDocumentRecord(
123
123
+
publishedInPublication.documents.data,
124
124
+
publishedInPublication.documents.uri,
125
125
+
);
126
126
+
if (normalizedDoc) {
127
127
+
postShareLink = getDocumentURL(
128
128
+
normalizedDoc,
129
129
+
publishedInPublication.documents.uri,
130
130
+
publishedInPublication.publications,
131
131
+
);
132
132
+
}
125
133
} else if (publishedStandalone?.document) {
126
126
-
// Standalone published post - use /p/{did}/{rkey} format
127
127
-
const docUri = new AtUri(publishedStandalone.document);
128
128
-
postShareLink = `/p/${docUri.host}/${docUri.rkey}`;
134
134
+
const normalizedDoc = publishedStandalone.documents
135
135
+
? normalizeDocumentRecord(publishedStandalone.documents.data, publishedStandalone.document)
136
136
+
: null;
137
137
+
if (normalizedDoc) {
138
138
+
postShareLink = getDocumentURL(normalizedDoc, publishedStandalone.document);
139
139
+
} else {
140
140
+
const docUri = new AtUri(publishedStandalone.document);
141
141
+
postShareLink = `/p/${docUri.host}/${docUri.rkey}`;
142
142
+
}
129
143
}
130
144
131
145
return {
+2
-3
components/PostListing.tsx
···
18
18
import { InteractionPreview } from "./InteractionsPreview";
19
19
import { useLocalizedDate } from "src/hooks/useLocalizedDate";
20
20
import { mergePreferences } from "src/utils/mergePreferences";
21
21
+
import { getDocumentURL } from "app/lish/createPub/getPublicationURL";
21
22
22
23
export const PostListing = (props: Post) => {
23
24
let pubRecord = props.publication?.pubRecord as
···
60
61
let tags = (postRecord?.tags as string[] | undefined) || [];
61
62
62
63
// For standalone posts, link directly to the document
63
63
-
let postHref = props.publication
64
64
-
? `${props.publication.href}/${postUri.rkey}`
65
65
-
: `/p/${postUri.host}/${postUri.rkey}`;
64
64
+
let postHref = getDocumentURL(postRecord, props.documents.uri, pubRecord);
66
65
67
66
return (
68
67
<BaseThemeProvider {...theme} local>