@jant/core 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +47 -30
- package/bin/commands/migrate-text-attachments.js +1 -3
- package/bin/commands/site/export.js +1 -3
- package/bin/commands/telegram/register-webhooks.js +9 -3
- package/bin/lib/site-snapshot.js +1 -5
- package/dist/app-CWJFPjuR.js +6 -0
- package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
- package/dist/client/_assets/client-DCrcHVJl.css +2 -0
- package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
- package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
- package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
- package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +1 -2
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +76 -2
- package/src/__tests__/vite-dev-plugins.test.ts +34 -0
- package/src/client/__tests__/compose-launch.test.ts +50 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
- package/src/client/__tests__/site-header-nav.test.ts +36 -2
- package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
- package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
- package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +72 -0
- package/src/client/components/compose-title-from-heading.ts +66 -0
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/inline-image-issues.ts +17 -0
- package/src/client/components/jant-command-palette.ts +67 -2
- package/src/client/components/jant-compose-dialog.ts +10 -2
- package/src/client/components/jant-compose-editor.ts +78 -1
- package/src/client/components/jant-compose-fullscreen.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +382 -152
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -10
- package/src/client/components/settings-types.ts +30 -0
- package/src/client/compose-launch.ts +30 -0
- package/src/client/compose-shortcuts.ts +46 -3
- package/src/client/site-header-nav.d.ts +1 -0
- package/src/client/site-header-nav.js +105 -47
- package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/rehost-images.ts +5 -1
- package/src/client-auth.ts +0 -1
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- package/src/i18n/locales/public/en.po +20 -4
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +22 -6
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +22 -6
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +77 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +85 -28
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +79 -22
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- package/src/lib/__tests__/navigation.test.ts +4 -37
- package/src/lib/__tests__/resolve-config.test.ts +6 -1
- package/src/lib/__tests__/view.test.ts +6 -30
- package/src/lib/feed.ts +63 -3
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/navigation.ts +5 -22
- package/src/lib/post-display.ts +14 -8
- package/src/lib/resolve-config.ts +9 -2
- package/src/lib/site-header-fragment.tsx +37 -0
- package/src/lib/view.ts +2 -7
- package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
- package/src/preset.css +60 -18
- package/src/routes/api/__tests__/nav-items.test.ts +26 -0
- package/src/routes/api/export.ts +0 -2
- package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
- package/src/routes/api/internal/sites.ts +5 -5
- package/src/routes/api/nav-items.ts +25 -4
- package/src/routes/dash/settings.tsx +48 -3
- package/src/routes/feed/__tests__/feed.test.ts +42 -0
- package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
- package/src/routes/feed/feed.ts +1 -0
- package/src/routes/feed/sitemap.ts +2 -6
- package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
- package/src/routes/pages/archive.tsx +1 -0
- package/src/routes/pages/collection.tsx +1 -0
- package/src/routes/pages/featured.tsx +0 -4
- package/src/routes/pages/home.tsx +4 -51
- package/src/routes/pages/latest.tsx +2 -51
- package/src/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +231 -1
- package/src/services/__tests__/post-timeline.test.ts +175 -2
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +7 -16
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
- package/src/services/export-theme/styles/main.css +46 -14
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/index.ts +27 -17
- package/src/services/navigation.ts +186 -1
- package/src/services/post.ts +103 -63
- package/src/services/settings.ts +0 -10
- package/src/services/site-admin.ts +0 -2
- package/src/services/upload-session.ts +4 -18
- package/src/styles/components.css +20 -0
- package/src/styles/tokens.css +28 -10
- package/src/styles/ui.css +349 -13
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/types/views.ts +13 -0
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/src/ui/layouts/SiteLayout.tsx +249 -216
- package/dist/app-DqKkZenB.js +0 -6
- package/dist/client/_assets/client-BhHHVvSY.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- featured (`.Type == "featured"`): featured_at != nil
|
|
11
11
|
- archive (`.Type == "archive"`): all published (includes latest_hidden)
|
|
12
12
|
- collection (`.Type == "collection"`): Params.collections.slug match,
|
|
13
|
-
|
|
13
|
+
ordered per `sort_order`; pinning is ignored for RSS
|
|
14
14
|
|
|
15
15
|
Per-entry payload mirrors `buildSinglePostContent` + `buildFeedContent`:
|
|
16
16
|
- Title: empty for quote posts, else .Title
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
body → Hugo-rendered .Content (stripped of <script>/<style>)
|
|
24
24
|
rating → <p>★★★★☆ 4/5</p>
|
|
25
25
|
link → trailing <p><a href=permalink> ★ </a></p>
|
|
26
|
-
replies → <hr/> + <p><small><time>{dt}</time></small></p>
|
|
26
|
+
replies → <hr/> + <p><small><time>{dt}</time></small></p>
|
|
27
|
+
+ inline title/link metadata + content
|
|
27
28
|
|
|
28
29
|
Hugo's default RSS media-type override (`[outputFormats.RSS]`) makes this
|
|
29
30
|
template produce application/atom+xml at /{section}/index.xml.
|
|
@@ -59,25 +60,15 @@
|
|
|
59
60
|
{{- end -}}
|
|
60
61
|
{{- end -}}
|
|
61
62
|
{{- end -}}
|
|
62
|
-
{{- $pinned := slice -}}
|
|
63
|
-
{{- $unpinned := slice -}}
|
|
64
|
-
{{- range $members -}}
|
|
65
|
-
{{- if .entry.pinned_at -}}
|
|
66
|
-
{{- $pinned = $pinned | append . -}}
|
|
67
|
-
{{- else -}}
|
|
68
|
-
{{- $unpinned = $unpinned | append . -}}
|
|
69
|
-
{{- end -}}
|
|
70
|
-
{{- end -}}
|
|
71
|
-
{{- if $pinned -}}{{- $pinned = sort $pinned "entry.pinned_at" "desc" -}}{{- end -}}
|
|
72
63
|
{{- $sortOrder := $page.Params.sort_order | default "position" -}}
|
|
73
|
-
{{-
|
|
64
|
+
{{- $ordered := $members -}}
|
|
65
|
+
{{- if $ordered -}}
|
|
74
66
|
{{- if eq $sortOrder "collected_at_desc" -}}
|
|
75
|
-
{{- $
|
|
67
|
+
{{- $ordered = sort $ordered "entry.collected_at" "desc" -}}
|
|
76
68
|
{{- else -}}
|
|
77
|
-
{{- $
|
|
69
|
+
{{- $ordered = sort $ordered "entry.position" "asc" -}}
|
|
78
70
|
{{- end -}}
|
|
79
71
|
{{- end -}}
|
|
80
|
-
{{- $ordered := $pinned | append $unpinned -}}
|
|
81
72
|
{{- range $ordered -}}{{- $posts = $posts | append .post -}}{{- end -}}
|
|
82
73
|
{{- end -}}
|
|
83
74
|
|
|
@@ -1,41 +1,28 @@
|
|
|
1
|
-
{{ define "main" }}
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{{- $main := slice -}}
|
|
9
|
-
{{- if eq $homeDefaultView "featured" -}}
|
|
10
|
-
{{- $featured := where $allPosts "Params.featured_at" "ne" nil -}}
|
|
11
|
-
{{- $main = sort $featured "Params.featured_at" "desc" -}}
|
|
12
|
-
{{- else -}}
|
|
13
|
-
{{- $public := where $allPosts "Params.visibility" "public" -}}
|
|
14
|
-
{{- $public = where $public "Params.pinned_at" nil -}}
|
|
15
|
-
{{- $main = $public.ByDate.Reverse -}}
|
|
16
|
-
{{- end -}}
|
|
17
|
-
{{- $paginator := .Paginate $main $pageSize -}}
|
|
18
|
-
|
|
19
|
-
<section class="home">
|
|
20
|
-
{{- if and (eq $paginator.PageNumber 1) $pinned -}}
|
|
21
|
-
<div class="post-list post-list-pinned" aria-label="Pinned posts">
|
|
22
|
-
{{- range $i, $p := $pinned -}}
|
|
23
|
-
{{- if gt $i 0 -}}<hr class="feed-divider" aria-hidden="true" />{{- end -}}
|
|
24
|
-
{{ partial "thread-preview.html" $p }}
|
|
25
|
-
{{- end -}}
|
|
26
|
-
</div>
|
|
27
|
-
{{- end -}}
|
|
1
|
+
{{ define "main" }} {{- $pageSize := .Site.Params.page_size | default 10 -}} {{-
|
|
2
|
+
/* Root posts are branch bundles (kind=section), so we iterate .Site.Pages, not
|
|
3
|
+
.Site.RegularPages. */ -}} {{- $allPosts := where .Site.Pages "Type" "post" -}}
|
|
4
|
+
{{- $pinned := where $allPosts "Params.pinned_at" "ne" nil -}} {{- $pinned =
|
|
5
|
+
$pinned.ByDate.Reverse -}} {{- $public := where $allPosts "Params.visibility"
|
|
6
|
+
"public" -}} {{- $public = where $public "Params.pinned_at" nil -}} {{- $main :=
|
|
7
|
+
$public.ByDate.Reverse -}} {{- $paginator := .Paginate $main $pageSize -}}
|
|
28
8
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
9
|
+
<section class="home">
|
|
10
|
+
{{- if and (eq $paginator.PageNumber 1) $pinned -}}
|
|
11
|
+
<div class="post-list post-list-pinned" aria-label="Pinned posts">
|
|
12
|
+
{{- range $i, $p := $pinned -}} {{- if gt $i 0 -}}
|
|
13
|
+
<hr class="feed-divider" aria-hidden="true" />
|
|
14
|
+
{{- end -}} {{ partial "thread-preview.html" $p }} {{- end -}}
|
|
15
|
+
</div>
|
|
16
|
+
{{- end -}} {{- if $paginator.Pages -}}
|
|
17
|
+
<div class="post-list post-list-main">
|
|
18
|
+
{{- range $i, $p := $paginator.Pages -}} {{- if gt $i 0 -}}
|
|
19
|
+
<hr class="feed-divider" aria-hidden="true" />
|
|
20
|
+
{{- end -}} {{ partial "thread-preview.html" $p }} {{- end -}}
|
|
21
|
+
</div>
|
|
22
|
+
{{ partial "pagination.html" $paginator }} {{- else if not $pinned -}}
|
|
23
|
+
<p class="empty-state">
|
|
24
|
+
Nothing published yet. Write your first post to get started.
|
|
25
|
+
</p>
|
|
26
|
+
{{- end -}}
|
|
27
|
+
</section>
|
|
41
28
|
{{ end }}
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
link-format "★" suffix)
|
|
9
9
|
includeReplies — when true, appends each public reply separated by
|
|
10
10
|
<hr/> + <small><time></time></small>
|
|
11
|
+
inline — when true, renders title/link metadata that top-level
|
|
12
|
+
Atom entries expose through their entry fields
|
|
11
13
|
|
|
12
14
|
Output is raw HTML suitable for embedding inside Atom
|
|
13
15
|
<content type="html"><![CDATA[...]]></content>. Since CDATA ends at the
|
|
@@ -18,9 +20,31 @@
|
|
|
18
20
|
{{- $post := .post -}}
|
|
19
21
|
{{- $permalink := .permalink -}}
|
|
20
22
|
{{- $includeReplies := .includeReplies -}}
|
|
23
|
+
{{- $inline := false -}}
|
|
24
|
+
{{- with .inline -}}{{- $inline = . -}}{{- end -}}
|
|
21
25
|
{{- $format := $post.Params.format | default "note" -}}
|
|
22
26
|
{{- $parts := slice -}}
|
|
23
27
|
|
|
28
|
+
{{- /* --- Inline title/link metadata for thread replies --- */ -}}
|
|
29
|
+
{{- if and $inline (ne $format "quote") -}}
|
|
30
|
+
{{- $title := $post.Title | default "" -}}
|
|
31
|
+
{{- $linkUrl := string ($post.Params.link_url | default "") -}}
|
|
32
|
+
{{- if eq $format "link" -}}
|
|
33
|
+
{{- if ne $linkUrl "" -}}
|
|
34
|
+
{{- $domain := $linkUrl -}}
|
|
35
|
+
{{- with urls.Parse $linkUrl -}}{{- if .Host -}}{{- $domain = replaceRE `^(www|m|mobile)\.` "" .Host -}}{{- end -}}{{- end -}}
|
|
36
|
+
{{- $parts = $parts | append (printf `<p><a href=%q>%s</a></p>` $linkUrl (transform.XMLEscape $domain)) -}}
|
|
37
|
+
{{- end -}}
|
|
38
|
+
{{- if ne $title "" -}}
|
|
39
|
+
{{- $titleHref := $permalink -}}
|
|
40
|
+
{{- if ne $linkUrl "" -}}{{- $titleHref = $linkUrl -}}{{- end -}}
|
|
41
|
+
{{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $titleHref (transform.XMLEscape $title)) -}}
|
|
42
|
+
{{- end -}}
|
|
43
|
+
{{- else if ne $title "" -}}
|
|
44
|
+
{{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $permalink (transform.XMLEscape $title)) -}}
|
|
45
|
+
{{- end -}}
|
|
46
|
+
{{- end -}}
|
|
47
|
+
|
|
24
48
|
{{- /* --- Quote block (format=quote) --- */ -}}
|
|
25
49
|
{{- if eq $format "quote" -}}
|
|
26
50
|
{{- $quoteText := string ($post.Params.quote_text | default "") -}}
|
|
@@ -100,7 +124,7 @@
|
|
|
100
124
|
{{- $replyLabel := .Date.Format "Jan 2, 2006 15:04" -}}
|
|
101
125
|
{{- $parts = $parts | append "<hr/>" -}}
|
|
102
126
|
{{- $parts = $parts | append (printf `<p><small><time datetime=%q>%s</time></small></p>` $replyDatetime (transform.XMLEscape $replyLabel)) -}}
|
|
103
|
-
{{- $parts = $parts | append (partial "feed-post-content.xml" (dict "post" . "permalink" $replyPermalink "includeReplies" false)) -}}
|
|
127
|
+
{{- $parts = $parts | append (partial "feed-post-content.xml" (dict "post" . "permalink" $replyPermalink "includeReplies" false "inline" true)) -}}
|
|
104
128
|
{{- end -}}
|
|
105
129
|
{{- end -}}
|
|
106
130
|
|
|
@@ -84,13 +84,11 @@ h1 {
|
|
|
84
84
|
|
|
85
85
|
h2 {
|
|
86
86
|
font-size: var(--type-content-title);
|
|
87
|
-
font-style: italic;
|
|
88
87
|
margin: 2.1rem 0 1.4rem;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
h3 {
|
|
92
91
|
font-size: var(--type-content-subtitle);
|
|
93
|
-
font-style: italic;
|
|
94
92
|
margin: 2rem 0 1.4rem;
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -119,6 +117,12 @@ kbd,
|
|
|
119
117
|
samp {
|
|
120
118
|
font-family: var(--font-mono);
|
|
121
119
|
font-size: var(--type-code);
|
|
120
|
+
line-height: 1.42;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
code {
|
|
124
|
+
color: var(--site-code-text);
|
|
125
|
+
overflow-wrap: break-word;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
pre {
|
|
@@ -127,23 +131,26 @@ pre {
|
|
|
127
131
|
line-height: 1.5;
|
|
128
132
|
padding: 1rem;
|
|
129
133
|
overflow-x: auto;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
border
|
|
134
|
+
color: var(--site-code-text);
|
|
135
|
+
background-color: var(--site-code-block-bg);
|
|
136
|
+
border: 1px solid var(--site-code-block-border);
|
|
137
|
+
border-radius: 6px;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
pre code {
|
|
136
141
|
padding: 0;
|
|
137
142
|
background: transparent;
|
|
138
143
|
border: 0;
|
|
144
|
+
color: inherit;
|
|
139
145
|
font-size: inherit;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
:not(pre) > code {
|
|
143
|
-
padding: 0.
|
|
144
|
-
background-color: var(--site-
|
|
145
|
-
border:
|
|
146
|
-
|
|
149
|
+
padding: 0.08em 0.28em;
|
|
150
|
+
background-color: var(--site-code-bg);
|
|
151
|
+
border-radius: 0.22em;
|
|
152
|
+
box-decoration-break: clone;
|
|
153
|
+
-webkit-box-decoration-break: clone;
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
/* Blockquotes — tinted background card with quote icon (matches main site). */
|
|
@@ -217,13 +224,13 @@ blockquote cite {
|
|
|
217
224
|
|
|
218
225
|
ul,
|
|
219
226
|
ol {
|
|
220
|
-
padding-left: 1.
|
|
227
|
+
padding-left: 1.4em;
|
|
221
228
|
margin: 1.25em 0;
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
li {
|
|
225
|
-
margin-top:
|
|
226
|
-
margin-bottom:
|
|
232
|
+
margin-top: 0.65em;
|
|
233
|
+
margin-bottom: 0.65em;
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
/* Normalize li > p so list spacing is controlled by li alone,
|
|
@@ -850,6 +857,31 @@ hr.feed-divider {
|
|
|
850
857
|
color: var(--site-reading-body);
|
|
851
858
|
}
|
|
852
859
|
|
|
860
|
+
.post-card > .post-card-summary :is(h1, h2),
|
|
861
|
+
.post-card > .post-card-quote-commentary :is(h1, h2) {
|
|
862
|
+
font-size: calc(var(--type-content-body) * 1.12);
|
|
863
|
+
font-weight: var(--fw-medium);
|
|
864
|
+
margin-top: 1.45rem;
|
|
865
|
+
margin-bottom: 0.55rem;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.post-card > .post-card-summary :is(h3, h4),
|
|
869
|
+
.post-card > .post-card-quote-commentary :is(h3, h4) {
|
|
870
|
+
font-size: var(--type-content-body);
|
|
871
|
+
font-weight: var(--fw-medium);
|
|
872
|
+
margin-top: 1.2rem;
|
|
873
|
+
margin-bottom: 0.45rem;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.post-card > .post-card-summary :is(h5, h6),
|
|
877
|
+
.post-card > .post-card-quote-commentary :is(h5, h6) {
|
|
878
|
+
font-size: var(--type-secondary);
|
|
879
|
+
font-weight: var(--fw-medium);
|
|
880
|
+
color: var(--site-text-secondary);
|
|
881
|
+
margin-top: 1rem;
|
|
882
|
+
margin-bottom: 0.35rem;
|
|
883
|
+
}
|
|
884
|
+
|
|
853
885
|
/* Link card domain row — shown ABOVE the title (matches main site's
|
|
854
886
|
`.feed-link-domain` pattern). Inline flex with icon + host text. */
|
|
855
887
|
.post-card-link-domain,
|
|
@@ -1009,8 +1041,8 @@ hr.feed-divider {
|
|
|
1009
1041
|
.post-card-quote-content {
|
|
1010
1042
|
font-family: var(--font-serif);
|
|
1011
1043
|
color: var(--site-text-primary);
|
|
1012
|
-
font-size: var(--type-content-
|
|
1013
|
-
line-height: var(--type-
|
|
1044
|
+
font-size: var(--type-content-quote);
|
|
1045
|
+
line-height: var(--type-content-quote-leading);
|
|
1014
1046
|
white-space: pre-line;
|
|
1015
1047
|
text-wrap: pretty;
|
|
1016
1048
|
margin: 0;
|
package/src/services/export.ts
CHANGED
|
@@ -102,7 +102,6 @@ export interface SiteConfig {
|
|
|
102
102
|
siteDescription: string;
|
|
103
103
|
siteLanguage: string;
|
|
104
104
|
showJantBrandingOnHome: boolean;
|
|
105
|
-
homeDefaultView: string;
|
|
106
105
|
/** "latest" or "featured" — drives the default RSS nav link in the exported site. */
|
|
107
106
|
mainRssFeed: string;
|
|
108
107
|
siteFooter: string;
|
|
@@ -1374,9 +1373,8 @@ function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
|
|
|
1374
1373
|
* Resolve a nav item's final href for the Hugo export.
|
|
1375
1374
|
*
|
|
1376
1375
|
* Mirrors the runtime logic in `lib/view.ts:toNavItemView`. System URLs
|
|
1377
|
-
* stored in the DB
|
|
1378
|
-
*
|
|
1379
|
-
* resolve to the dedicated path.
|
|
1376
|
+
* stored in the DB are canonicalized during export so stale DB values do not
|
|
1377
|
+
* leak into the static site's navigation.
|
|
1380
1378
|
*
|
|
1381
1379
|
* The "rss" system nav item points at whichever Atom feed the site has
|
|
1382
1380
|
* configured as its main feed: `mainRssFeed === "featured"` → the featured
|
|
@@ -1385,14 +1383,13 @@ function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
|
|
|
1385
1383
|
*/
|
|
1386
1384
|
function resolveNavItemUrl(
|
|
1387
1385
|
item: SiteConfig["navItems"][number],
|
|
1388
|
-
homeDefaultView: string,
|
|
1389
1386
|
mainRssFeed: string,
|
|
1390
1387
|
): string {
|
|
1391
1388
|
if (item.systemKey === "latest") {
|
|
1392
|
-
return
|
|
1389
|
+
return "/";
|
|
1393
1390
|
}
|
|
1394
1391
|
if (item.systemKey === "featured") {
|
|
1395
|
-
return
|
|
1392
|
+
return "/featured/";
|
|
1396
1393
|
}
|
|
1397
1394
|
if (item.systemKey === "collections") return "/collections/";
|
|
1398
1395
|
if (item.systemKey === "archive") return "/archive/";
|
|
@@ -1474,7 +1471,6 @@ function buildHugoToml(config: SiteConfig): string {
|
|
|
1474
1471
|
"",
|
|
1475
1472
|
"[params]",
|
|
1476
1473
|
` description = "${escapeTomlString(config.siteDescription)}"`,
|
|
1477
|
-
` home_default_view = "${escapeTomlString(config.homeDefaultView)}"`,
|
|
1478
1474
|
` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
|
|
1479
1475
|
` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
|
|
1480
1476
|
` show_header_avatar = ${config.showHeaderAvatar}`,
|
|
@@ -1514,7 +1510,6 @@ function buildJantDataToml(
|
|
|
1514
1510
|
`site_name = "${escapeTomlString(config.siteName)}"`,
|
|
1515
1511
|
`site_description = "${escapeTomlString(config.siteDescription)}"`,
|
|
1516
1512
|
`site_language = "${escapeTomlString(config.siteLanguage)}"`,
|
|
1517
|
-
`home_default_view = "${escapeTomlString(config.homeDefaultView)}"`,
|
|
1518
1513
|
`main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
|
|
1519
1514
|
`show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
|
|
1520
1515
|
`show_header_avatar = ${config.showHeaderAvatar}`,
|
|
@@ -1558,7 +1553,7 @@ function buildJantDataToml(
|
|
|
1558
1553
|
parts.push(`type = "${escapeTomlString(item.type)}"`);
|
|
1559
1554
|
parts.push(`label = "${escapeTomlString(resolveNavItemLabel(item))}"`);
|
|
1560
1555
|
parts.push(
|
|
1561
|
-
`url = "${escapeTomlString(resolveNavItemUrl(item, config.
|
|
1556
|
+
`url = "${escapeTomlString(resolveNavItemUrl(item, config.mainRssFeed))}"`,
|
|
1562
1557
|
);
|
|
1563
1558
|
parts.push(`system_key = "${escapeTomlString(item.systemKey ?? "")}"`);
|
|
1564
1559
|
parts.push(`placement = "${escapeTomlString(item.placement ?? "header")}"`);
|
|
@@ -149,9 +149,7 @@ export function createGitHubAppInstallationsService(
|
|
|
149
149
|
.select()
|
|
150
150
|
.from(githubAppInstallation)
|
|
151
151
|
.where(eq(githubAppInstallation.siteId, siteId));
|
|
152
|
-
return rows
|
|
153
|
-
.map(toStored)
|
|
154
|
-
.sort((a, b) => b.addedAt - a.addedAt);
|
|
152
|
+
return rows.map(toStored).sort((a, b) => b.addedAt - a.addedAt);
|
|
155
153
|
},
|
|
156
154
|
|
|
157
155
|
async listSitesForInstallation(installationId) {
|
package/src/services/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
createSiteProfileService,
|
|
36
36
|
type SiteProfileService,
|
|
37
37
|
} from "./site-profile.js";
|
|
38
|
+
import { createAboutPageService, type AboutPageService } from "./about-page.js";
|
|
38
39
|
import {
|
|
39
40
|
createUploadSessionService,
|
|
40
41
|
type UploadSessionService,
|
|
@@ -64,6 +65,7 @@ export interface Services {
|
|
|
64
65
|
siteAdmin: SiteAdminService;
|
|
65
66
|
siteMembers: SiteMemberService;
|
|
66
67
|
siteProfile: SiteProfileService;
|
|
68
|
+
aboutPage: AboutPageService;
|
|
67
69
|
githubAppInstallations: GitHubAppInstallationsService;
|
|
68
70
|
telegram: TelegramService;
|
|
69
71
|
}
|
|
@@ -88,6 +90,23 @@ export function createServices(
|
|
|
88
90
|
const settings = createSettingsService(db, siteId, databaseSchema, dialect);
|
|
89
91
|
const paths = createPathService(db, siteId, databaseSchema);
|
|
90
92
|
const navItems = createNavItemService(db, siteId, databaseSchema);
|
|
93
|
+
const posts = createPostService(
|
|
94
|
+
db,
|
|
95
|
+
{
|
|
96
|
+
slugIdLength: config?.slugIdLength ?? 5,
|
|
97
|
+
databaseDialect: dialect,
|
|
98
|
+
},
|
|
99
|
+
siteId,
|
|
100
|
+
paths,
|
|
101
|
+
databaseSchema,
|
|
102
|
+
);
|
|
103
|
+
const collections = createCollectionService(
|
|
104
|
+
db,
|
|
105
|
+
siteId,
|
|
106
|
+
paths,
|
|
107
|
+
databaseSchema,
|
|
108
|
+
dialect,
|
|
109
|
+
);
|
|
91
110
|
const media = createMediaService(db, siteId, databaseSchema, dialect, {
|
|
92
111
|
enforceHostedQuota: config?.enforceHostedMediaQuota ?? false,
|
|
93
112
|
hostedControlPlane: config?.hostedControlPlane ?? null,
|
|
@@ -97,26 +116,11 @@ export function createServices(
|
|
|
97
116
|
settings,
|
|
98
117
|
site,
|
|
99
118
|
paths,
|
|
100
|
-
posts
|
|
101
|
-
db,
|
|
102
|
-
{
|
|
103
|
-
slugIdLength: config?.slugIdLength ?? 5,
|
|
104
|
-
databaseDialect: dialect,
|
|
105
|
-
},
|
|
106
|
-
siteId,
|
|
107
|
-
paths,
|
|
108
|
-
databaseSchema,
|
|
109
|
-
),
|
|
119
|
+
posts,
|
|
110
120
|
customUrls: createCustomUrlService(db, siteId, paths, databaseSchema),
|
|
111
121
|
media,
|
|
112
122
|
uploads: createUploadSessionService(db, siteId, media, databaseSchema),
|
|
113
|
-
collections
|
|
114
|
-
db,
|
|
115
|
-
siteId,
|
|
116
|
-
paths,
|
|
117
|
-
databaseSchema,
|
|
118
|
-
dialect,
|
|
119
|
-
),
|
|
123
|
+
collections,
|
|
120
124
|
search: createSearchService(rawQuery, siteId, dialect),
|
|
121
125
|
navItems,
|
|
122
126
|
auth: createAuthService(
|
|
@@ -148,6 +152,11 @@ export function createServices(
|
|
|
148
152
|
);
|
|
149
153
|
},
|
|
150
154
|
}),
|
|
155
|
+
aboutPage: createAboutPageService({
|
|
156
|
+
paths,
|
|
157
|
+
posts,
|
|
158
|
+
collections,
|
|
159
|
+
}),
|
|
151
160
|
githubAppInstallations: createGitHubAppInstallationsService(
|
|
152
161
|
db,
|
|
153
162
|
databaseSchema,
|
|
@@ -166,6 +175,7 @@ export type { UploadSessionService } from "./upload-session.js";
|
|
|
166
175
|
export type { CollectionService } from "./collection.js";
|
|
167
176
|
export type { SearchService, SearchResult, SearchOptions } from "./search.js";
|
|
168
177
|
export type { NavItemService } from "./navigation.js";
|
|
178
|
+
export type { AboutPageService, AboutPageStatus } from "./about-page.js";
|
|
169
179
|
export type { AuthService, DeleteAccountDeps } from "./auth.js";
|
|
170
180
|
export type { ApiTokenService } from "./api-token.js";
|
|
171
181
|
export type {
|
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
import { createEntityId } from "../lib/ids.js";
|
|
16
16
|
import { ValidationError } from "../lib/errors.js";
|
|
17
17
|
import { now } from "../lib/time.js";
|
|
18
|
+
import {
|
|
19
|
+
normalizePath,
|
|
20
|
+
normalizeSitePathPrefix,
|
|
21
|
+
stripSitePathPrefix,
|
|
22
|
+
toSameSitePath,
|
|
23
|
+
} from "../lib/url.js";
|
|
18
24
|
import { COLLECTION_FRESHNESS_WINDOW_SECONDS } from "../types.js";
|
|
19
25
|
import type {
|
|
20
26
|
NavItem,
|
|
@@ -23,10 +29,20 @@ import type {
|
|
|
23
29
|
CreateNavItem,
|
|
24
30
|
UpdateNavItem,
|
|
25
31
|
SystemNavKey,
|
|
32
|
+
SuggestedNavLink,
|
|
26
33
|
} from "../types.js";
|
|
27
34
|
import { SYSTEM_NAV_KEYS } from "../types.js";
|
|
28
35
|
|
|
29
36
|
const POSITION_RETRY_ATTEMPTS = 5;
|
|
37
|
+
const SUGGESTED_NAV_LINK_CANDIDATES = [
|
|
38
|
+
{ key: "about", path: "/about", label: "About" },
|
|
39
|
+
{ key: "now", path: "/now", label: "Now" },
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export interface ListSuggestedLinksOptions {
|
|
43
|
+
siteOrigin?: string;
|
|
44
|
+
sitePathPrefix?: string;
|
|
45
|
+
}
|
|
30
46
|
|
|
31
47
|
// Re-export shared constraint detection — see db/dialect.ts
|
|
32
48
|
import { isUniqueConstraintError } from "../db/dialect.js";
|
|
@@ -45,6 +61,9 @@ export interface NavItemService {
|
|
|
45
61
|
afterId: string | null,
|
|
46
62
|
beforeId: string | null,
|
|
47
63
|
): Promise<NavItem | null>;
|
|
64
|
+
listSuggestedLinks(
|
|
65
|
+
options?: ListSuggestedLinksOptions,
|
|
66
|
+
): Promise<SuggestedNavLink[]>;
|
|
48
67
|
getCollectionFreshness(collectionIds: string[]): Promise<Map<string, number>>;
|
|
49
68
|
}
|
|
50
69
|
|
|
@@ -53,7 +72,8 @@ export function createNavItemService(
|
|
|
53
72
|
siteId: string,
|
|
54
73
|
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
55
74
|
): NavItemService {
|
|
56
|
-
const { navItems, postCollections, posts } =
|
|
75
|
+
const { navItems, postCollections, posts, pathRegistry, collections } =
|
|
76
|
+
databaseSchema;
|
|
57
77
|
|
|
58
78
|
const defaultSystemOrder = [
|
|
59
79
|
"latest",
|
|
@@ -121,6 +141,58 @@ export function createNavItemService(
|
|
|
121
141
|
};
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
function withLeadingSlash(path: string): string {
|
|
145
|
+
const normalized = normalizePath(path);
|
|
146
|
+
return normalized ? `/${normalized}` : "/";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getComparableInternalPath(
|
|
150
|
+
url: string,
|
|
151
|
+
options: ListSuggestedLinksOptions = {},
|
|
152
|
+
): string | null {
|
|
153
|
+
const value = url.trim();
|
|
154
|
+
const sitePathPrefix = normalizeSitePathPrefix(
|
|
155
|
+
options.sitePathPrefix ?? "",
|
|
156
|
+
);
|
|
157
|
+
const sameSitePath = toSameSitePath(value, options.siteOrigin ?? "");
|
|
158
|
+
if (sameSitePath !== null) {
|
|
159
|
+
try {
|
|
160
|
+
const pathname = new URL(sameSitePath, "https://jant.invalid").pathname;
|
|
161
|
+
const internalPath = stripSitePathPrefix(pathname, sitePathPrefix);
|
|
162
|
+
return internalPath ? withLeadingSlash(internalPath) : null;
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
value.startsWith("http://") ||
|
|
170
|
+
value.startsWith("https://") ||
|
|
171
|
+
value.startsWith("//") ||
|
|
172
|
+
value.startsWith("mailto:") ||
|
|
173
|
+
value.startsWith("tel:") ||
|
|
174
|
+
value.startsWith("#")
|
|
175
|
+
) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const pathname = new URL(value, "https://jant.invalid").pathname;
|
|
181
|
+
const internalPath = stripSitePathPrefix(pathname, sitePathPrefix);
|
|
182
|
+
return internalPath ? withLeadingSlash(internalPath) : null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeSuggestedLabel(
|
|
189
|
+
label: string | null | undefined,
|
|
190
|
+
fallback: string,
|
|
191
|
+
): string {
|
|
192
|
+
const trimmed = label?.trim();
|
|
193
|
+
return trimmed && trimmed.length > 0 ? trimmed : fallback;
|
|
194
|
+
}
|
|
195
|
+
|
|
124
196
|
async function getLastPosition(): Promise<string | null> {
|
|
125
197
|
const rows = await db
|
|
126
198
|
.select({ position: navItems.position })
|
|
@@ -414,6 +486,119 @@ export function createNavItemService(
|
|
|
414
486
|
throw new Error("Failed to assign a unique nav item position");
|
|
415
487
|
},
|
|
416
488
|
|
|
489
|
+
async listSuggestedLinks(options: ListSuggestedLinksOptions = {}) {
|
|
490
|
+
const existingRows = await db
|
|
491
|
+
.select({
|
|
492
|
+
url: navItems.url,
|
|
493
|
+
collectionId: navItems.collectionId,
|
|
494
|
+
})
|
|
495
|
+
.from(navItems)
|
|
496
|
+
.where(eq(navItems.siteId, siteId));
|
|
497
|
+
const existingPaths = new Set(
|
|
498
|
+
existingRows.flatMap((item) => {
|
|
499
|
+
const path = getComparableInternalPath(item.url, options);
|
|
500
|
+
return path ? [path] : [];
|
|
501
|
+
}),
|
|
502
|
+
);
|
|
503
|
+
const existingCollectionIds = new Set(
|
|
504
|
+
existingRows.flatMap((item) =>
|
|
505
|
+
item.collectionId ? [item.collectionId] : [],
|
|
506
|
+
),
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const suggestions: SuggestedNavLink[] = [];
|
|
510
|
+
|
|
511
|
+
for (const candidate of SUGGESTED_NAV_LINK_CANDIDATES) {
|
|
512
|
+
const path = withLeadingSlash(candidate.path);
|
|
513
|
+
if (existingPaths.has(path)) continue;
|
|
514
|
+
|
|
515
|
+
const pathRows = await db
|
|
516
|
+
.select()
|
|
517
|
+
.from(pathRegistry)
|
|
518
|
+
.where(
|
|
519
|
+
and(
|
|
520
|
+
eq(pathRegistry.siteId, siteId),
|
|
521
|
+
eq(pathRegistry.path, normalizePath(candidate.path)),
|
|
522
|
+
),
|
|
523
|
+
)
|
|
524
|
+
.limit(1);
|
|
525
|
+
const record = pathRows[0];
|
|
526
|
+
if (!record || record.kind === "redirect") continue;
|
|
527
|
+
|
|
528
|
+
if (record.postId) {
|
|
529
|
+
const postRows = await db
|
|
530
|
+
.select({
|
|
531
|
+
title: posts.title,
|
|
532
|
+
status: posts.status,
|
|
533
|
+
visibility: posts.visibility,
|
|
534
|
+
})
|
|
535
|
+
.from(posts)
|
|
536
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.id, record.postId)))
|
|
537
|
+
.limit(1);
|
|
538
|
+
const post = postRows[0];
|
|
539
|
+
if (
|
|
540
|
+
!post ||
|
|
541
|
+
post.status !== "published" ||
|
|
542
|
+
post.visibility === "private"
|
|
543
|
+
) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
suggestions.push({
|
|
548
|
+
key: candidate.key,
|
|
549
|
+
label: normalizeSuggestedLabel(post.title, candidate.label),
|
|
550
|
+
url: path,
|
|
551
|
+
targetType: "page",
|
|
552
|
+
navItemType: "link",
|
|
553
|
+
});
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (record.collectionId) {
|
|
558
|
+
if (existingCollectionIds.has(record.collectionId)) continue;
|
|
559
|
+
|
|
560
|
+
const collectionRows = await db
|
|
561
|
+
.select({
|
|
562
|
+
title: collections.title,
|
|
563
|
+
})
|
|
564
|
+
.from(collections)
|
|
565
|
+
.where(
|
|
566
|
+
and(
|
|
567
|
+
eq(collections.siteId, siteId),
|
|
568
|
+
eq(collections.id, record.collectionId),
|
|
569
|
+
),
|
|
570
|
+
)
|
|
571
|
+
.limit(1);
|
|
572
|
+
const collection = collectionRows[0];
|
|
573
|
+
if (!collection) continue;
|
|
574
|
+
|
|
575
|
+
suggestions.push({
|
|
576
|
+
key: candidate.key,
|
|
577
|
+
label: normalizeSuggestedLabel(collection.title, candidate.label),
|
|
578
|
+
url: path,
|
|
579
|
+
targetType: "collection",
|
|
580
|
+
navItemType: record.kind === "slug" ? "collection" : "link",
|
|
581
|
+
...(record.kind === "slug" && {
|
|
582
|
+
collectionId: record.collectionId,
|
|
583
|
+
}),
|
|
584
|
+
});
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (record.kind === "archive") {
|
|
589
|
+
suggestions.push({
|
|
590
|
+
key: candidate.key,
|
|
591
|
+
label: candidate.label,
|
|
592
|
+
url: path,
|
|
593
|
+
targetType: "archive",
|
|
594
|
+
navItemType: "link",
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return suggestions;
|
|
600
|
+
},
|
|
601
|
+
|
|
417
602
|
async getCollectionFreshness(collectionIds) {
|
|
418
603
|
if (collectionIds.length === 0) return new Map<string, number>();
|
|
419
604
|
|