+3
-4
appview/pages/pages.go
+3
-4
appview/pages/pages.go
···
601
}
602
603
type FollowFragmentParams struct {
604
-
UserDid string
605
-
FollowStatus models.FollowStatus
606
-
FollowersCount int64
607
}
608
609
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
610
-
return p.executePlain("user/fragments/follow-oob", w, params)
611
}
612
613
type EditBioParams struct {
···
601
}
602
603
type FollowFragmentParams struct {
604
+
UserDid string
605
+
FollowStatus models.FollowStatus
606
}
607
608
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
609
+
return p.executePlain("user/fragments/follow", w, params)
610
}
611
612
type EditBioParams struct {
+7
-5
appview/pages/templates/fragments/tinyAvatarList.html
+7
-5
appview/pages/templates/fragments/tinyAvatarList.html
···
5
<div class="inline-flex items-center -space-x-3">
6
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
7
{{ range $i, $p := $ps }}
8
-
<img
9
-
src="{{ tinyAvatar . }}"
10
-
alt=""
11
-
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}"
12
-
/>
13
{{ end }}
14
15
{{ if gt (len $all) 5 }}
···
5
<div class="inline-flex items-center -space-x-3">
6
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
7
{{ range $i, $p := $ps }}
8
+
<a href="/{{ resolve . }}" title="{{ resolve . }}">
9
+
<img
10
+
src="{{ tinyAvatar . }}"
11
+
alt=""
12
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}"
13
+
/>
14
+
</a>
15
{{ end }}
16
17
{{ if gt (len $all) 5 }}
+30
-17
appview/pages/templates/labels/fragments/label.html
+30
-17
appview/pages/templates/labels/fragments/label.html
···
2
{{ $d := .def }}
3
{{ $v := .val }}
4
{{ $withPrefix := .withPrefix }}
5
-
<span class="w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm">
6
-
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
7
8
-
{{ $lhs := printf "%s" $d.Name }}
9
-
{{ $rhs := "" }}
10
11
-
{{ if not $d.ValueType.IsNull }}
12
-
{{ if $d.ValueType.IsDidFormat }}
13
-
{{ $v = resolve $v }}
14
-
{{ end }}
15
16
-
{{ if not $withPrefix }}
17
-
{{ $lhs = "" }}
18
-
{{ else }}
19
-
{{ $lhs = printf "%s/" $d.Name }}
20
-
{{ end }}
21
22
-
{{ $rhs = printf "%s" $v }}
23
-
{{ end }}
24
25
-
{{ printf "%s%s" $lhs $rhs }}
26
-
</span>
27
{{ end }}
28
29
···
2
{{ $d := .def }}
3
{{ $v := .val }}
4
{{ $withPrefix := .withPrefix }}
5
6
+
{{ $lhs := printf "%s" $d.Name }}
7
+
{{ $rhs := "" }}
8
+
{{ $isDid := false }}
9
+
{{ $resolvedVal := "" }}
10
11
+
{{ if not $d.ValueType.IsNull }}
12
+
{{ $isDid = $d.ValueType.IsDidFormat }}
13
+
{{ if $isDid }}
14
+
{{ $resolvedVal = resolve $v }}
15
+
{{ $v = $resolvedVal }}
16
+
{{ end }}
17
+
18
+
{{ if not $withPrefix }}
19
+
{{ $lhs = "" }}
20
+
{{ else }}
21
+
{{ $lhs = printf "%s/" $d.Name }}
22
+
{{ end }}
23
24
+
{{ $rhs = printf "%s" $v }}
25
+
{{ end }}
26
27
+
{{ $chipClasses := "w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm" }}
28
29
+
{{ if $isDid }}
30
+
<a href="/{{ $resolvedVal }}" class="{{ $chipClasses }} no-underline hover:underline">
31
+
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
32
+
{{ printf "%s%s" $lhs $rhs }}
33
+
</a>
34
+
{{ else }}
35
+
<span class="{{ $chipClasses }}">
36
+
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
37
+
{{ printf "%s%s" $lhs $rhs }}
38
+
</span>
39
+
{{ end }}
40
{{ end }}
41
42
-6
appview/pages/templates/user/fragments/follow-oob.html
-6
appview/pages/templates/user/fragments/follow-oob.html
+3
-5
appview/pages/templates/user/fragments/followCard.html
+3
-5
appview/pages/templates/user/fragments/followCard.html
···
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0">
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
<a href="/{{ $userIdent }}">
12
-
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{
13
-
$userIdent | truncateAt30 }}</span>
14
</a>
15
{{ with .Profile }}
16
<p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p>
17
{{ end }}
18
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
19
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
20
-
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
21
-
.FollowersCount }} followers</a></span>
22
<span class="select-none after:content-['ยท']"></span>
23
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span>
24
</div>
···
31
</div>
32
</div>
33
</div>
34
-
{{ end }}
···
9
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0">
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
<a href="/{{ $userIdent }}">
12
+
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span>
13
</a>
14
{{ with .Profile }}
15
<p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p>
16
{{ end }}
17
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
18
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
19
+
<span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .FollowersCount }} followers</a></span>
20
<span class="select-none after:content-['ยท']"></span>
21
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .FollowingCount }} following</a></span>
22
</div>
···
29
</div>
30
</div>
31
</div>
32
+
{{ end }}
+99
-97
appview/pages/templates/user/fragments/profileCard.html
+99
-97
appview/pages/templates/user/fragments/profileCard.html
···
1
{{ define "user/fragments/profileCard" }}
2
-
{{ $userIdent := resolve .UserDid }}
3
-
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
-
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
-
<div class="w-3/4 aspect-square relative">
6
-
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" />
7
-
</div>
8
-
</div>
9
-
<div class="col-span-2">
10
-
<div class="flex items-center flex-row flex-nowrap gap-2">
11
-
<p title="{{ $userIdent }}"
12
-
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
13
-
{{ $userIdent }}
14
-
</p>
15
-
{{ with .Profile }}
16
-
{{ if .Pronouns }}
17
-
<p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p>
18
-
{{ end }}
19
-
{{ end }}
20
-
</div>
21
22
-
<div class="md:hidden">
23
-
{{ block "followerFollowing" (list . $userIdent) }} {{ end }}
24
-
</div>
25
-
</div>
26
-
<div class="col-span-3 md:col-span-full">
27
-
<div id="profile-bio" class="text-sm">
28
-
{{ $profile := .Profile }}
29
-
{{ with .Profile }}
30
31
-
{{ if .Description }}
32
-
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
33
-
{{ end }}
34
35
-
<div class="hidden md:block">
36
-
{{ block "followerFollowing" (list $ $userIdent) }} {{ end }}
37
-
</div>
38
39
-
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
40
-
{{ if .Location }}
41
-
<div class="flex items-center gap-2">
42
-
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
43
-
<span>{{ .Location }}</span>
44
-
</div>
45
-
{{ end }}
46
-
{{ if .IncludeBluesky }}
47
-
<div class="flex items-center gap-2">
48
-
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white"
49
-
}}</span>
50
-
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a>
51
-
</div>
52
-
{{ end }}
53
-
{{ range $link := .Links }}
54
-
{{ if $link }}
55
-
<div class="flex items-center gap-2">
56
-
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
57
-
<a href="{{ $link }}">{{ $link }}</a>
58
-
</div>
59
-
{{ end }}
60
-
{{ end }}
61
-
{{ if not $profile.IsStatsEmpty }}
62
-
<div class="flex items-center justify-evenly gap-2 py-2">
63
-
{{ range $stat := .Stats }}
64
-
{{ if $stat.Kind }}
65
-
<div class="flex flex-col items-center gap-2">
66
-
<span class="text-xl font-bold">{{ $stat.Value }}</span>
67
-
<span>{{ $stat.Kind.String }}</span>
68
</div>
69
{{ end }}
70
-
{{ end }}
71
-
</div>
72
-
{{ end }}
73
-
</div>
74
-
{{ end }}
75
76
-
<div class="flex mt-2 items-center gap-2">
77
-
{{ if ne .FollowStatus.String "IsSelf" }}
78
-
{{ template "user/fragments/follow" . }}
79
-
{{ else }}
80
-
<button id="editBtn" class="btn w-full flex items-center gap-2 group" hx-target="#profile-bio"
81
-
hx-get="/profile/edit-bio" hx-swap="innerHTML">
82
-
{{ i "pencil" "w-4 h-4" }}
83
-
edit
84
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
85
-
</button>
86
-
{{ end }}
87
88
-
<a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
89
-
href="/{{ $userIdent }}/feed.atom">
90
-
{{ i "rss" "size-4" }}
91
-
</a>
92
-
</div>
93
94
</div>
95
-
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
96
-
</div>
97
-
</div>
98
{{ end }}
99
100
{{ define "followerFollowing" }}
101
-
{{ $root := index . 0 }}
102
-
{{ $userIdent := index . 1 }}
103
-
{{ with $root }}
104
-
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
105
-
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
106
-
<span id="followers" data-followers-did="{{ .UserDid }}"><a href="/{{ $userIdent }}?tab=followers">{{
107
-
.Stats.FollowersCount }} followers</a></span>
108
-
<span class="select-none after:content-['ยท']"></span>
109
-
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
110
-
</div>
111
{{ end }}
112
-
{{ end }}
···
1
{{ define "user/fragments/profileCard" }}
2
+
{{ $userIdent := resolve .UserDid }}
3
+
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
+
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
+
<div class="w-3/4 aspect-square relative">
6
+
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" />
7
+
</div>
8
+
</div>
9
+
<div class="col-span-2">
10
+
<div class="flex items-center flex-row flex-nowrap gap-2">
11
+
<p title="{{ $userIdent }}"
12
+
class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
13
+
{{ $userIdent }}
14
+
</p>
15
+
{{ with .Profile }}
16
+
{{ if .Pronouns }}
17
+
<p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p>
18
+
{{ end }}
19
+
{{ end }}
20
+
</div>
21
22
+
<div class="md:hidden">
23
+
{{ block "followerFollowing" (list . $userIdent) }} {{ end }}
24
+
</div>
25
+
</div>
26
+
<div class="col-span-3 md:col-span-full">
27
+
<div id="profile-bio" class="text-sm">
28
+
{{ $profile := .Profile }}
29
+
{{ with .Profile }}
30
31
+
{{ if .Description }}
32
+
<p class="text-base pb-4 md:pb-2">{{ .Description }}</p>
33
+
{{ end }}
34
35
+
<div class="hidden md:block">
36
+
{{ block "followerFollowing" (list $ $userIdent) }} {{ end }}
37
+
</div>
38
39
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
40
+
{{ if .Location }}
41
+
<div class="flex items-center gap-2">
42
+
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
43
+
<span>{{ .Location }}</span>
44
+
</div>
45
+
{{ end }}
46
+
{{ if .IncludeBluesky }}
47
+
<div class="flex items-center gap-2">
48
+
<span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span>
49
+
<a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a>
50
+
</div>
51
+
{{ end }}
52
+
{{ range $link := .Links }}
53
+
{{ if $link }}
54
+
<div class="flex items-center gap-2">
55
+
<span class="flex-shrink-0">{{ i "link" "size-4" }}</span>
56
+
<a href="{{ $link }}">{{ $link }}</a>
57
+
</div>
58
+
{{ end }}
59
+
{{ end }}
60
+
{{ if not $profile.IsStatsEmpty }}
61
+
<div class="flex items-center justify-evenly gap-2 py-2">
62
+
{{ range $stat := .Stats }}
63
+
{{ if $stat.Kind }}
64
+
<div class="flex flex-col items-center gap-2">
65
+
<span class="text-xl font-bold">{{ $stat.Value }}</span>
66
+
<span>{{ $stat.Kind.String }}</span>
67
+
</div>
68
+
{{ end }}
69
+
{{ end }}
70
+
</div>
71
+
{{ end }}
72
</div>
73
{{ end }}
74
75
+
<div class="flex mt-2 items-center gap-2">
76
+
{{ if ne .FollowStatus.String "IsSelf" }}
77
+
{{ template "user/fragments/follow" . }}
78
+
{{ else }}
79
+
<button id="editBtn"
80
+
class="btn w-full flex items-center gap-2 group"
81
+
hx-target="#profile-bio"
82
+
hx-get="/profile/edit-bio"
83
+
hx-swap="innerHTML">
84
+
{{ i "pencil" "w-4 h-4" }}
85
+
edit
86
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
87
+
</button>
88
+
{{ end }}
89
90
+
<a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
91
+
href="/{{ $userIdent }}/feed.atom">
92
+
{{ i "rss" "size-4" }}
93
+
</a>
94
+
</div>
95
96
+
</div>
97
+
<div id="update-profile" class="text-red-400 dark:text-red-500"></div>
98
+
</div>
99
</div>
100
{{ end }}
101
102
{{ define "followerFollowing" }}
103
+
{{ $root := index . 0 }}
104
+
{{ $userIdent := index . 1 }}
105
+
{{ with $root }}
106
+
<div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
107
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
108
+
<span id="followers"><a href="/{{ $userIdent }}?tab=followers">{{ .Stats.FollowersCount }} followers</a></span>
109
+
<span class="select-none after:content-['ยท']"></span>
110
+
<span id="following"><a href="/{{ $userIdent }}?tab=following">{{ .Stats.FollowingCount }} following</a></span>
111
+
</div>
112
+
{{ end }}
113
{{ end }}
114
+
+26
-1
appview/reporesolver/resolver.go
+26
-1
appview/reporesolver/resolver.go
···
63
}
64
65
// get dir/ref
66
+
currentDir := extractCurrentDir(r.URL.EscapedPath())
67
ref := chi.URLParam(r, "ref")
68
69
repoAt := repo.RepoAt()
···
130
}
131
132
return repoInfo
133
+
}
134
+
135
+
// extractCurrentDir gets the current directory for markdown link resolution.
136
+
// for blob paths, returns the parent dir. for tree paths, returns the path itself.
137
+
//
138
+
// /@user/repo/blob/main/docs/README.md => docs
139
+
// /@user/repo/tree/main/docs => docs
140
+
func extractCurrentDir(fullPath string) string {
141
+
fullPath = strings.TrimPrefix(fullPath, "/")
142
+
143
+
blobPattern := regexp.MustCompile(`blob/[^/]+/(.*)$`)
144
+
if matches := blobPattern.FindStringSubmatch(fullPath); len(matches) > 1 {
145
+
return path.Dir(matches[1])
146
+
}
147
+
148
+
treePattern := regexp.MustCompile(`tree/[^/]+/(.*)$`)
149
+
if matches := treePattern.FindStringSubmatch(fullPath); len(matches) > 1 {
150
+
dir := strings.TrimSuffix(matches[1], "/")
151
+
if dir == "" {
152
+
return "."
153
+
}
154
+
return dir
155
+
}
156
+
157
+
return "."
158
}
159
160
// extractPathAfterRef gets the actual repository path
+22
appview/reporesolver/resolver_test.go
+22
appview/reporesolver/resolver_test.go
···
···
1
+
package reporesolver
2
+
3
+
import "testing"
4
+
5
+
func TestExtractCurrentDir(t *testing.T) {
6
+
tests := []struct {
7
+
path string
8
+
want string
9
+
}{
10
+
{"/@user/repo/blob/main/docs/README.md", "docs"},
11
+
{"/@user/repo/blob/main/README.md", "."},
12
+
{"/@user/repo/tree/main/docs", "docs"},
13
+
{"/@user/repo/tree/main/docs/", "docs"},
14
+
{"/@user/repo/tree/main", "."},
15
+
}
16
+
17
+
for _, tt := range tests {
18
+
if got := extractCurrentDir(tt.path); got != tt.want {
19
+
t.Errorf("extractCurrentDir(%q) = %q, want %q", tt.path, got, tt.want)
20
+
}
21
+
}
22
+
}
+4
-16
appview/state/follow.go
+4
-16
appview/state/follow.go
···
75
76
s.notifier.NewFollow(r.Context(), follow)
77
78
-
followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
79
-
if err != nil {
80
-
log.Println("failed to get follow stats", err)
81
-
}
82
-
83
s.pages.FollowFragment(w, pages.FollowFragmentParams{
84
-
UserDid: subjectIdent.DID.String(),
85
-
FollowStatus: models.IsFollowing,
86
-
FollowersCount: followStats.Followers,
87
})
88
89
return
···
112
// this is not an issue, the firehose event might have already done this
113
}
114
115
-
followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
116
-
if err != nil {
117
-
log.Println("failed to get follow stats", err)
118
-
}
119
-
120
s.pages.FollowFragment(w, pages.FollowFragmentParams{
121
-
UserDid: subjectIdent.DID.String(),
122
-
FollowStatus: models.IsNotFollowing,
123
-
FollowersCount: followStats.Followers,
124
})
125
126
s.notifier.DeleteFollow(r.Context(), follow)
···
75
76
s.notifier.NewFollow(r.Context(), follow)
77
78
s.pages.FollowFragment(w, pages.FollowFragmentParams{
79
+
UserDid: subjectIdent.DID.String(),
80
+
FollowStatus: models.IsFollowing,
81
})
82
83
return
···
106
// this is not an issue, the firehose event might have already done this
107
}
108
109
s.pages.FollowFragment(w, pages.FollowFragmentParams{
110
+
UserDid: subjectIdent.DID.String(),
111
+
FollowStatus: models.IsNotFollowing,
112
})
113
114
s.notifier.DeleteFollow(r.Context(), follow)
+23
-25
docs/DOCS.md
+23
-25
docs/DOCS.md
···
2
title: Tangled docs
3
author: The Tangled Contributors
4
date: 21 Sun, Dec 2025
5
-
---
6
-
7
-
# Introduction
8
-
9
-
Tangled is a decentralized code hosting and collaboration
10
-
platform. Every component of Tangled is open-source and
11
-
self-hostable. [tangled.org](https://tangled.org) also
12
-
provides hosting and CI services that are free to use.
13
14
-
There are several models for decentralized code
15
-
collaboration platforms, ranging from ActivityPubโs
16
-
(Forgejo) federated model, to Radicleโs entirely P2P model.
17
-
Our approach attempts to be the best of both worlds by
18
-
adopting the AT Protocolโa protocol for building decentralized
19
-
social applications with a central identity
20
21
-
Our approach to this is the idea of โknotsโ. Knots are
22
-
lightweight, headless servers that enable users to host Git
23
-
repositories with ease. Knots are designed for either single
24
-
or multi-tenant use which is perfect for self-hosting on a
25
-
Raspberry Pi at home, or larger โcommunityโ servers. By
26
-
default, Tangled provides managed knots where you can host
27
-
your repositories for free.
28
29
-
The appview at tangled.org acts as a consolidated "view"
30
-
into the whole network, allowing users to access, clone and
31
-
contribute to repositories hosted across different knots
32
-
seamlessly.
33
34
# Quick start guide
35
···
2
title: Tangled docs
3
author: The Tangled Contributors
4
date: 21 Sun, Dec 2025
5
+
abstract: |
6
+
Tangled is a decentralized code hosting and collaboration
7
+
platform. Every component of Tangled is open-source and
8
+
self-hostable. [tangled.org](https://tangled.org) also
9
+
provides hosting and CI services that are free to use.
10
11
+
There are several models for decentralized code
12
+
collaboration platforms, ranging from ActivityPubโs
13
+
(Forgejo) federated model, to Radicleโs entirely P2P model.
14
+
Our approach attempts to be the best of both worlds by
15
+
adopting the AT Protocolโa protocol for building decentralized
16
+
social applications with a central identity
17
18
+
Our approach to this is the idea of โknotsโ. Knots are
19
+
lightweight, headless servers that enable users to host Git
20
+
repositories with ease. Knots are designed for either single
21
+
or multi-tenant use which is perfect for self-hosting on a
22
+
Raspberry Pi at home, or larger โcommunityโ servers. By
23
+
default, Tangled provides managed knots where you can host
24
+
your repositories for free.
25
26
+
The appview at tangled.org acts as a consolidated "view"
27
+
into the whole network, allowing users to access, clone and
28
+
contribute to repositories hosted across different knots
29
+
seamlessly.
30
+
---
31
32
# Quick start guide
33
+7
docs/search.html
+7
docs/search.html
···
···
1
+
<form action="https://google.com/search" role="search" aria-label="Sitewide" class="w-full">
2
+
<input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]">
3
+
<label>
4
+
<span style="display:none;">Search</span>
5
+
<input type="text" name="q" placeholder="Search docs ..." class="w-full font-normal">
6
+
</label>
7
+
</form>
+53
-41
docs/template.html
+53
-41
docs/template.html
···
37
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
38
39
</head>
40
-
<body class="bg-white dark:bg-gray-900 min-h-screen flex flex-col min-h-screen">
41
$for(include-before)$
42
$include-before$
43
$endfor$
···
60
id="mobile-toc-popover"
61
popover
62
class="mobile-toc-popover
63
-
bg-white dark:bg-gray-800
64
-
border-b border-gray-200 dark:border-gray-700
65
-
h-full overflow-y-auto
66
px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0"
67
>
68
-
<button
69
-
type="button"
70
-
popovertarget="mobile-toc-popover"
71
-
popovertargetaction="toggle"
72
-
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4">
73
-
${ x.svg() }
74
-
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
75
-
</button>
76
-
${ table-of-contents:toc.html() }
77
-
${ single-page:mode.html() }
78
</div>
79
-
80
81
<!-- desktop sidebar toc -->
82
-
<nav id="$idprefix$TOC" role="doc-toc" class="hidden md:block fixed left-0 top-0 w-80 h-screen bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-4 z-50">
83
-
$if(toc-title)$
84
-
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
85
-
$endif$
86
-
${ table-of-contents:toc.html() }
87
${ single-page:mode.html() }
88
</nav>
89
$endif$
···
91
<div class="$if(toc)$md:ml-80$endif$ flex-1 flex flex-col">
92
<main class="max-w-4xl w-full mx-auto p-6 flex-1">
93
$if(top)$
94
-
$-- only print title block if this is NOT the top page
95
$else$
96
$if(title)$
97
-
<header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
98
-
<h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1>
99
-
$if(subtitle)$
100
-
<p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p>
101
-
$endif$
102
-
$for(author)$
103
-
<p class="text-sm text-gray-500 dark:text-gray-400">$author$</p>
104
-
$endfor$
105
-
$if(date)$
106
-
<p class="text-sm text-gray-500 dark:text-gray-400">Updated on $date$</p>
107
-
$endif$
108
-
$if(abstract)$
109
-
<div class="mt-6 p-4 bg-gray-50 rounded-lg">
110
-
<div class="text-sm font-semibold text-gray-700 uppercase mb-2">$abstract-title$</div>
111
-
<div class="text-gray-700">$abstract$</div>
112
-
</div>
113
-
$endif$
114
-
$endif$
115
-
</header>
116
$endif$
117
<article class="prose dark:prose-invert max-w-none">
118
$body$
119
</article>
120
</main>
121
-
<nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 ">
122
<div class="max-w-4xl mx-auto px-8 py-4">
123
<div class="flex justify-between gap-4">
124
<span class="flex-1">
···
37
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
38
39
</head>
40
+
<body class="bg-white dark:bg-gray-900 flex flex-col min-h-svh">
41
$for(include-before)$
42
$include-before$
43
$endfor$
···
60
id="mobile-toc-popover"
61
popover
62
class="mobile-toc-popover
63
+
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
64
+
h-full overflow-y-auto shadow-sm
65
px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0"
66
>
67
+
<div class="flex flex-col min-h-full">
68
+
<div class="flex-1 space-y-4">
69
+
<button
70
+
type="button"
71
+
popovertarget="mobile-toc-popover"
72
+
popovertargetaction="toggle"
73
+
class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4">
74
+
${ x.svg() }
75
+
$if(toc-title)$$toc-title$$else$Table of Contents$endif$
76
+
</button>
77
+
${ search.html() }
78
+
${ table-of-contents:toc.html() }
79
+
</div>
80
+
${ single-page:mode.html() }
81
+
</div>
82
</div>
83
84
<!-- desktop sidebar toc -->
85
+
<nav
86
+
id="$idprefix$TOC"
87
+
role="doc-toc"
88
+
class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen
89
+
bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700
90
+
p-4 z-50 overflow-y-auto">
91
+
${ search.html() }
92
+
<div class="flex-1">
93
+
$if(toc-title)$
94
+
<h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2>
95
+
$endif$
96
+
${ table-of-contents:toc.html() }
97
+
</div>
98
${ single-page:mode.html() }
99
</nav>
100
$endif$
···
102
<div class="$if(toc)$md:ml-80$endif$ flex-1 flex flex-col">
103
<main class="max-w-4xl w-full mx-auto p-6 flex-1">
104
$if(top)$
105
+
$-- only print title block if this is NOT the top page
106
$else$
107
$if(title)$
108
+
<header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700">
109
+
<h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1>
110
+
$if(subtitle)$
111
+
<p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p>
112
+
$endif$
113
+
$for(author)$
114
+
<p class="text-sm text-gray-500 dark:text-gray-400">$author$</p>
115
+
$endfor$
116
+
$if(date)$
117
+
<p class="text-sm text-gray-500 dark:text-gray-400">Updated on $date$</p>
118
+
$endif$
119
+
$endif$
120
+
</header>
121
+
$endif$
122
+
123
+
$if(abstract)$
124
+
<article class="prose dark:prose-invert max-w-none">
125
+
$abstract$
126
+
</article>
127
$endif$
128
+
129
<article class="prose dark:prose-invert max-w-none">
130
$body$
131
</article>
132
</main>
133
+
<nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
134
<div class="max-w-4xl mx-auto px-8 py-4">
135
<div class="flex justify-between gap-4">
136
<span class="flex-1">
+3
-1
lexicons/pulls/pull.json
+3
-1
lexicons/pulls/pull.json