+1
.gitignore
+1
.gitignore
+17
-1
.vscode/settings.json
+17
-1
.vscode/settings.json
···
3
3
"css.validate": false,
4
4
"tailwindCSS.includeLanguages": {
5
5
"svelte": "html"
6
-
}
6
+
},
7
+
"cSpell.words": [
8
+
"atproto",
9
+
"Decentralised",
10
+
"diddoc",
11
+
"Dids",
12
+
"ewan",
13
+
"ewanc",
14
+
"Linkat",
15
+
"mkizka",
16
+
"pdsurl",
17
+
"prerender",
18
+
"prerendering",
19
+
"rkey",
20
+
"utilises",
21
+
"xrpc"
22
+
]
7
23
}
+1
-1
src/routes/+layout.svelte
+1
-1
src/routes/+layout.svelte
+76
-50
src/routes/+page.svelte
+76
-50
src/routes/+page.svelte
···
1
1
<script lang="ts">
2
-
import { onMount } from "svelte";
3
2
import { getStores } from "$app/stores";
4
-
const { page } = getStores();
3
+
import { env } from "$env/dynamic/public";
5
4
import UserDirectory from "$lib/components/archive/UserDirectory.svelte";
6
5
import DynamicHead from "$lib/components/layout/DynamicHead.svelte";
6
+
import { getProfile } from "$lib/components/profile/profile";
7
7
8
+
const { page } = getStores();
8
9
let { data } = $props();
10
+
11
+
// Environment variable for directory owner
12
+
let directoryOwner = env.DIRECTORY_OWNER ?? "";
13
+
14
+
// Profile state for directory owner - initialize with data.profile if available
15
+
let ownerProfile = $state<{ displayName?: string; handle?: string } | null>(
16
+
data.profile || null
17
+
);
18
+
19
+
// Load the directory owner's profile only if we don't already have it
20
+
$effect(() => {
21
+
if (directoryOwner && !ownerProfile) {
22
+
const loadOwner = async () => {
23
+
try {
24
+
const result = await getProfile(fetch);
25
+
ownerProfile = result;
26
+
} catch (err) {
27
+
console.error("Could not fetch owner profile:", err);
28
+
ownerProfile = null;
29
+
}
30
+
};
31
+
loadOwner();
32
+
}
33
+
});
34
+
35
+
// Derived reactive values for user display options
9
36
let displayUserBanner = $derived(data.displayUserBanner);
10
37
let displayUserDescription = $derived(data.displayUserDescription);
11
38
···
17
44
function shuffleArray<T>(array: T[]): T[] {
18
45
let currentIndex = array.length, randomIndex;
19
46
20
-
// While there remain elements to shuffle.
21
47
while (currentIndex !== 0) {
22
-
// Pick a remaining element.
23
48
randomIndex = Math.floor(Math.random() * currentIndex);
24
49
currentIndex--;
25
50
26
-
// And swap it with the current element.
27
51
[array[currentIndex], array[randomIndex]] = [
28
52
array[randomIndex],
29
53
array[currentIndex],
···
32
56
return array;
33
57
}
34
58
35
-
// State to track if locale has been properly loaded
36
-
let localeLoaded = $state(false);
59
+
const getDisplayName = (p: { displayName?: string; handle?: string } | null | undefined) =>
60
+
p?.displayName || p?.handle || null;
37
61
38
-
onMount(() => {
39
-
// Set a brief timeout to ensure the browser has time to determine locale
40
-
setTimeout(() => {
41
-
localeLoaded = true;
42
-
}, 10);
62
+
// Computed title that prioritizes display name, then handle, then DID
63
+
const pageTitle = $derived(() => {
64
+
if (!directoryOwner) return "Linkat Directory";
65
+
66
+
const displayName = getDisplayName(ownerProfile);
67
+
if (displayName) {
68
+
return `${displayName}'s Linkat Directory`;
69
+
}
70
+
71
+
// Fallback to directoryOwner (DID) while loading
72
+
return `${directoryOwner}'s Linkat Directory`;
43
73
});
44
74
45
-
import { getProfile } from "$lib/components/profile/profile";
46
-
let profile = $state<{ displayName?: string; handle?: string } | null>(null);
47
-
let loading = $state(true);
48
-
let error = $state<string | null>(null);
75
+
const pageDescription = $derived(() => {
76
+
if (!directoryOwner) return "Discover amazing users curated by the Linkat community";
77
+
78
+
const displayName = getDisplayName(ownerProfile) || directoryOwner;
79
+
return `Discover users' links curated by ${displayName} in ${displayName}'s Linkat Directory`;
80
+
});
49
81
50
-
$effect(() => {
51
-
if (import.meta.env.DIRECTORY_OWNER) {
52
-
loading = true;
53
-
getProfile(fetch)
54
-
.then((p) => {
55
-
profile = p;
56
-
error = null;
57
-
})
58
-
.catch((err) => {
59
-
console.error('Failed to load profile:', err);
60
-
error = err.message;
61
-
profile = null;
62
-
})
63
-
.finally(() => {
64
-
loading = false;
65
-
});
66
-
} else {
67
-
loading = false;
68
-
}
82
+
const pageKeywords = $derived(() => {
83
+
const baseKeywords = "Linkat, directory, links, Bluesky, community, curation";
84
+
if (!directoryOwner) return baseKeywords;
85
+
86
+
const displayName = getDisplayName(ownerProfile) || directoryOwner;
87
+
return `${baseKeywords}, ${displayName}`;
69
88
});
70
89
</script>
71
90
72
91
<DynamicHead
73
-
title={profile?.displayName || "Linkat Directory"}
74
-
description={profile?.displayName ? `Discover users' links curated by ${profile.displayName}` : "Discover amazing users curated by the Linkat community"}
75
-
keywords={`Linkat, directory, links, Bluesky, community, curation${profile?.displayName ? `, ${profile.displayName}` : ''}`}
76
-
ogTitle={profile?.displayName || "Linkat Directory"}
77
-
ogDescription={profile?.displayName ? `Discover users' links curated by ${profile.displayName}` : "Discover amazing users' links curated by the Linkat community"}
78
-
twitterTitle={profile?.displayName || "Linkat Directory"}
79
-
twitterDescription={profile?.displayName ? `Discover users' links curated by ${profile.displayName}` : "Discover amazing users' links curated by the Linkat community"}
92
+
title={pageTitle()}
93
+
description={pageDescription()}
94
+
keywords={pageKeywords()}
95
+
ogTitle={pageTitle()}
96
+
ogDescription={pageDescription()}
97
+
twitterTitle={pageTitle()}
98
+
twitterDescription={pageDescription()}
80
99
/>
81
100
82
101
<div class="container mx-auto px-4 py-8">
83
-
{#if data.noUsersConfigured}
102
+
{#if data.noUsersConfigured}
84
103
<div class="text-center py-12">
85
-
<div class="max-w-md mx-auto">
104
+
<div class="max-w-4xl mx-auto px-4">
86
105
<p class="text-lg mb-4 opacity-75">
87
106
Welcome to Linkat Directory! No users are currently configured.
88
107
</p>
89
-
<div class="bg-[var(--muted-bg)] rounded-lg p-6 text-left">
108
+
<div class="bg-[var(--muted-bg)] rounded-lg p-6 text-left overflow-hidden">
90
109
<h3 class="font-semibold mb-2">To get started:</h3>
91
110
<ol class="list-decimal list-inside space-y-2 text-sm">
92
-
<li>Create a <code>.env</code> file in your project root</li>
93
-
<li>Add your user DID: <code>DIRECTORY_OWNER=did:plc:your-did-here</code></li>
94
-
<li>Or add multiple users: <code>PUBLIC_LINKAT_USERS=did:plc:user1,did:web:user2</code></li>
95
-
<li>Restart the development server</li>
111
+
<li class="break-words">Copy <code class="break-all bg-[var(--card-bg)] px-1 py-0.5 rounded text-xs">. env.example</code> to <code class="break-all bg-[var(--card-bg)] px-1 py-0.5 rounded text-xs">.env</code></li>
112
+
<li class="break-words">Set your DID: <code class="break-all bg-[var(--card-bg)] px-1 py-0.5 rounded text-xs">DIRECTORY_OWNER=did:plc:your-did-here</code></li>
113
+
<li class="break-words">Set the origin: <code class="break-all bg-[var(--card-bg)] px-1 py-0.5 rounded text-xs">PUBLIC_ORIGIN=http://localhost:5713</code></li>
114
+
<li class="break-words">Optionally add more users: <code class="break-all bg-[var(--card-bg)] px-1 py-0.5 rounded text-xs">PUBLIC_LINKAT_USERS=did:plc:user1,did:plc:user2</code></li>
115
+
<li class="break-words">Restart the development server</li>
96
116
</ol>
97
117
</div>
98
118
</div>
99
119
</div>
100
120
{:else}
101
-
<UserDirectory users={shuffleArray([...data.linkatUsers]).map(did => ({ did }))} primaryUserDid={data.primaryUserDid} userLinkBoards={data.userLinkBoards} displayBanner={displayUserBanner} displayDescription={displayUserDescription} />
121
+
<UserDirectory
122
+
users={shuffleArray([...data.linkatUsers]).map(did => ({ did }))}
123
+
primaryUserDid={directoryOwner}
124
+
userLinkBoards={data.userLinkBoards}
125
+
displayBanner={displayUserBanner}
126
+
displayDescription={displayUserDescription}
127
+
/>
102
128
{/if}
103
129
</div>
+56
-9
src/routes/user/[did]/+page.svelte
+56
-9
src/routes/user/[did]/+page.svelte
···
2
2
import DynamicLinks from "$lib/components/layout/main/DynamicLinks.svelte";
3
3
import DynamicHead from "$lib/components/layout/DynamicHead.svelte";
4
4
import { getStores } from "$app/stores";
5
+
import { env } from "$env/dynamic/public";
6
+
import { getProfile } from "$components/profile/profile";
7
+
5
8
const { page } = getStores();
6
-
7
9
let { data } = $props();
8
-
10
+
9
11
let profile = $derived(data.profile);
10
12
let dynamicLinks = $derived(data.dynamicLinks);
11
13
let error = $derived(data.error);
12
14
let did = $derived(data.did);
15
+
16
+
let directoryOwner = env.DIRECTORY_OWNER;
17
+
let ownerProfile = $state<{ displayName?: string; handle?: string } | null>(null);
18
+
19
+
$effect(() => {
20
+
if (directoryOwner) {
21
+
const loadOwner = async () => {
22
+
try {
23
+
const result = await getProfile(fetch);
24
+
ownerProfile = result;
25
+
} catch (err) {
26
+
console.error("Could not fetch owner profile:", err);
27
+
ownerProfile = null;
28
+
}
29
+
};
30
+
loadOwner();
31
+
}
32
+
});
33
+
34
+
const getDisplayName = (p: { displayName?: string; handle?: string } | null | undefined) =>
35
+
p?.displayName || p?.handle || null;
13
36
</script>
14
37
15
38
<DynamicHead
16
-
title={profile?.displayName || did + " - Linkat Directory"}
17
-
description={"View " + (profile?.displayName || did) + "'s curated Linkat links"}
18
-
ogTitle={profile?.displayName || did + " - Linkat Directory"}
19
-
ogDescription={"View " + (profile?.displayName || did) + "'s curated Linkat links"}
20
-
twitterTitle={profile?.displayName || did + " - Linkat Directory"}
21
-
twitterDescription={"View " + (profile?.displayName || did) + "'s curated Linkat links"}
22
-
keywords={`Linkat, directory, links, Bluesky, curation, ${profile?.displayName || did}`}
39
+
title={
40
+
directoryOwner
41
+
? `${getDisplayName(profile) || did} – ${getDisplayName(ownerProfile) || directoryOwner}'s Linkat Directory`
42
+
: `${getDisplayName(profile) || did} – Linkat Directory`
43
+
}
44
+
description={
45
+
directoryOwner
46
+
? `View ${getDisplayName(profile) || did}'s curated links in ${getDisplayName(ownerProfile) || directoryOwner}'s Linkat Directory`
47
+
: `View ${getDisplayName(profile) || did}'s curated links in the Linkat Directory`
48
+
}
49
+
ogTitle={
50
+
directoryOwner
51
+
? `${getDisplayName(profile) || did} – ${getDisplayName(ownerProfile) || directoryOwner}'s Linkat Directory`
52
+
: `${getDisplayName(profile) || did} – Linkat Directory`
53
+
}
54
+
ogDescription={
55
+
directoryOwner
56
+
? `View ${getDisplayName(profile) || did}'s curated links in ${getDisplayName(ownerProfile) || directoryOwner}'s Linkat Directory`
57
+
: `View ${getDisplayName(profile) || did}'s curated links in the Linkat Directory`
58
+
}
59
+
twitterTitle={
60
+
directoryOwner
61
+
? `${getDisplayName(profile) || did} – ${getDisplayName(ownerProfile) || directoryOwner}'s Linkat Directory`
62
+
: `${getDisplayName(profile) || did} – Linkat Directory`
63
+
}
64
+
twitterDescription={
65
+
directoryOwner
66
+
? `View ${getDisplayName(profile) || did}'s curated links in ${getDisplayName(ownerProfile) || directoryOwner}'s Linkat Directory`
67
+
: `View ${getDisplayName(profile) || did}'s curated links in the Linkat Directory`
68
+
}
69
+
keywords={`Linkat, directory, links, Bluesky, curation, ${getDisplayName(profile) || did}, ${getDisplayName(ownerProfile) || directoryOwner}`}
23
70
/>
24
71
25
72
<div class="container mx-auto px-4 py-8">