@jant/core 0.7.0 → 0.7.1

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.
Files changed (158) hide show
  1. package/bin/commands/export.js +3 -1
  2. package/bin/commands/import-site.js +689 -216
  3. package/bin/commands/setup.js +136 -0
  4. package/bin/commands/site/export.js +71 -34
  5. package/bin/commands/site/pull-media.js +2 -6
  6. package/bin/commands/site/snapshot/export.js +18 -60
  7. package/bin/commands/site/snapshot/import.js +22 -23
  8. package/bin/lib/d1-query.js +87 -2
  9. package/bin/lib/hugo-markdown.js +4 -0
  10. package/bin/lib/site-pull-media.js +21 -28
  11. package/bin/lib/site-selection.js +10 -1
  12. package/bin/lib/site-snapshot.js +338 -3
  13. package/bin/lib/sql-export.js +68 -5
  14. package/bin/lib/wrangler-cli.js +9 -0
  15. package/bin/lib/zip-archive.js +187 -0
  16. package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
  17. package/dist/client/.vite/manifest.json +20 -20
  18. package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
  19. package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
  20. package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
  21. package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
  22. package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
  23. package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
  24. package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
  25. package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
  26. package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
  27. package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
  28. package/dist/index.js +2 -2
  29. package/dist/node.js +9 -5
  30. package/package.json +13 -3
  31. package/src/__tests__/dev-scripts.test.ts +203 -0
  32. package/src/__tests__/export-collection-order.test.ts +276 -0
  33. package/src/__tests__/export-feed-ids.test.ts +184 -0
  34. package/src/__tests__/export-feed-order.test.ts +294 -0
  35. package/src/__tests__/export-hugo-build.test.ts +130 -0
  36. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  37. package/src/__tests__/export-service.test.ts +611 -28
  38. package/src/__tests__/export-smart-collection.test.ts +329 -0
  39. package/src/__tests__/helpers/hugo-site.ts +146 -0
  40. package/src/__tests__/import-site-command.test.ts +487 -1
  41. package/src/__tests__/mise-config.test.ts +75 -4
  42. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  43. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  44. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  45. package/src/__tests__/snapshot-settings.test.ts +97 -0
  46. package/src/__tests__/snapshot-tables.test.ts +219 -0
  47. package/src/__tests__/sql-export.test.ts +173 -0
  48. package/src/__tests__/zip-archive.test.ts +106 -0
  49. package/src/app.tsx +15 -6
  50. package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
  51. package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
  52. package/src/client/components/jant-repo-picker-types.ts +6 -1
  53. package/src/client/components/jant-repo-picker.ts +4 -7
  54. package/src/client/components/jant-settings-general.ts +17 -12
  55. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  56. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  57. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  58. package/src/client/tiptap/extensions.ts +0 -3
  59. package/src/client/tiptap/structural-keymap.ts +158 -47
  60. package/src/db/__tests__/d1-query.test.ts +56 -1
  61. package/src/i18n/locales/settings/en.po +8 -8
  62. package/src/i18n/locales/settings/en.ts +1 -1
  63. package/src/i18n/locales/settings/zh-Hans.po +8 -8
  64. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  65. package/src/i18n/locales/settings/zh-Hant.po +8 -8
  66. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  67. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  68. package/src/lib/__tests__/image.test.ts +27 -1
  69. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  70. package/src/lib/__tests__/markdown.test.ts +10 -0
  71. package/src/lib/__tests__/resolve-config.test.ts +67 -0
  72. package/src/lib/__tests__/schemas.test.ts +27 -1
  73. package/src/lib/__tests__/timeline.test.ts +87 -0
  74. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  75. package/src/lib/discover.ts +3 -1
  76. package/src/lib/github-sync-repo-name.ts +45 -0
  77. package/src/lib/hugo-markdown.ts +46 -0
  78. package/src/lib/image.ts +17 -4
  79. package/src/lib/markdown-manager.ts +392 -2
  80. package/src/lib/post-body-html.ts +11 -4
  81. package/src/lib/resolve-config.ts +58 -1
  82. package/src/lib/schemas.ts +50 -5
  83. package/src/lib/thread-fold.ts +3 -3
  84. package/src/lib/timeline.ts +1 -1
  85. package/src/lib/tiptap-to-markdown.ts +13 -7
  86. package/src/lib/url.ts +20 -0
  87. package/src/lib/view.ts +2 -2
  88. package/src/node/__tests__/cli-setup.test.ts +163 -0
  89. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  90. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  91. package/src/node/__tests__/runtime.test.ts +38 -0
  92. package/src/node/index.ts +2 -0
  93. package/src/node/request-handler.ts +3 -1
  94. package/src/routes/api/__tests__/posts.test.ts +24 -0
  95. package/src/routes/api/__tests__/upload.test.ts +34 -0
  96. package/src/routes/api/export.ts +3 -3
  97. package/src/routes/api/internal/sites.ts +0 -1
  98. package/src/routes/api/posts.ts +2 -0
  99. package/src/routes/api/public/posts.ts +21 -1
  100. package/src/routes/api/upload.ts +10 -3
  101. package/src/routes/compose.tsx +4 -0
  102. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  103. package/src/routes/dash/settings.tsx +212 -70
  104. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  105. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  106. package/src/routes/pages/archive.tsx +17 -11
  107. package/src/routes/pages/featured.tsx +11 -5
  108. package/src/routes/pages/page.tsx +53 -31
  109. package/src/routes/pages/search.tsx +4 -2
  110. package/src/runtime/__tests__/readiness.test.ts +23 -0
  111. package/src/runtime/node.ts +49 -0
  112. package/src/runtime/readiness.ts +15 -0
  113. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  114. package/src/services/__tests__/custom-url.test.ts +34 -0
  115. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  116. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  117. package/src/services/__tests__/media.test.ts +31 -0
  118. package/src/services/__tests__/path.test.ts +56 -0
  119. package/src/services/__tests__/post-timeline.test.ts +127 -0
  120. package/src/services/__tests__/post.test.ts +74 -0
  121. package/src/services/bootstrap.ts +263 -44
  122. package/src/services/custom-url.ts +2 -2
  123. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  124. package/src/services/export-theme/layouts/_default/list.html +2 -63
  125. package/src/services/export-theme/layouts/_default/rss.xml +27 -38
  126. package/src/services/export-theme/layouts/collections/list.html +3 -3
  127. package/src/services/export-theme/layouts/featured/list.html +1 -4
  128. package/src/services/export-theme/layouts/index.html +40 -26
  129. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  130. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  131. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  132. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  133. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  134. package/src/services/export-theme/layouts/partials/head.html +1 -1
  135. package/src/services/export-theme/layouts/partials/header.html +5 -3
  136. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  137. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  138. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  139. package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
  140. package/src/services/export-theme/layouts/post/list.html +1 -1
  141. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  142. package/src/services/export-theme/styles/main.css +0 -1
  143. package/src/services/export-theme/theme.toml +1 -1
  144. package/src/services/export.ts +620 -71
  145. package/src/services/github-app-installations.ts +171 -4
  146. package/src/services/github-sync.ts +69 -15
  147. package/src/services/mcp.ts +19 -1
  148. package/src/services/media.ts +8 -1
  149. package/src/services/path.ts +34 -1
  150. package/src/services/post.ts +214 -39
  151. package/src/services/site-admin.ts +3 -7
  152. package/src/services/site.ts +83 -31
  153. package/src/styles/ui.css +7 -1
  154. package/src/types/app-context.ts +16 -0
  155. package/src/types/bindings.ts +5 -0
  156. package/src/types/operations.ts +10 -0
  157. package/src/ui/dash/settings/GeneralContent.tsx +10 -8
  158. package/src/client/tiptap/exitable-marks.ts +0 -73
@@ -8,45 +8,7 @@
8
8
  fallback for unknown section types.
9
9
  */ -}}
10
10
  {{- if eq .Type "collection" -}}
11
- {{- $collectionSlug := .Params.slug | default .Slug -}}
12
- {{- /* Thread collection membership is stored only on root bundles. */ -}}
13
- {{- $allPosts := where (where .Site.Pages "Type" "post") "Kind" "section" -}}
14
- {{- $members := slice -}}
15
- {{- range $allPosts -}}
16
- {{- $post := . -}}
17
- {{- with $post.Params.collections -}}
18
- {{- range . -}}
19
- {{- if eq .slug $collectionSlug -}}
20
- {{- $members = $members | append (dict "post" $post "entry" .) -}}
21
- {{- end -}}
22
- {{- end -}}
23
- {{- end -}}
24
- {{- end -}}
25
-
26
- {{- /* Split into pinned + unpinned, sort each, then concat. */ -}}
27
- {{- $pinned := slice -}}
28
- {{- $unpinned := slice -}}
29
- {{- range $members -}}
30
- {{- if .entry.pinned_at -}}
31
- {{- $pinned = $pinned | append . -}}
32
- {{- else -}}
33
- {{- $unpinned = $unpinned | append . -}}
34
- {{- end -}}
35
- {{- end -}}
36
- {{- if $pinned -}}
37
- {{- $pinned = sort $pinned "entry.pinned_at" "desc" -}}
38
- {{- end -}}
39
- {{- $sortOrder := .Params.sort_order | default "position" -}}
40
- {{- if $unpinned -}}
41
- {{- if eq $sortOrder "collected_at_desc" -}}
42
- {{- $unpinned = sort $unpinned "entry.collected_at" "desc" -}}
43
- {{- else -}}
44
- {{- $unpinned = sort $unpinned "entry.position" "asc" -}}
45
- {{- end -}}
46
- {{- end -}}
47
- {{- $ordered := $pinned | append $unpinned -}}
48
- {{- $posts := slice -}}
49
- {{- range $ordered -}}{{- $posts = $posts | append .post -}}{{- end -}}
11
+ {{- $posts := partial "collection-members.html" (dict "page" . "feed" false) -}}
50
12
 
51
13
  {{- $pageSize := .Site.Params.page_size | default 10 -}}
52
14
  {{- $paginator := .Paginate $posts $pageSize -}}
@@ -62,30 +24,7 @@
62
24
  {{- end -}}
63
25
  {{ .Content }}
64
26
  </header>
65
- {{- if $paginator.Pages -}}
66
- <div class="post-list">
67
- {{- range $i, $p := $paginator.Pages -}}
68
- {{- if gt $i 0 -}}<hr class="feed-divider" aria-hidden="true" />{{- end -}}
69
- {{- $replies := $p.Pages.ByDate -}}
70
- {{- $hasReplies := gt (len $replies) 0 -}}
71
- <div class="thread thread-full{{ if $hasReplies }} thread-has-replies{{ end }}" data-slug="{{ $p.Params.slug | default $p.Slug }}">
72
- <div class="thread-item thread-item-root">
73
- {{ partial "post-card.html" $p }}
74
- </div>
75
- {{- if $hasReplies -}}
76
- <section class="thread-replies" aria-label="Replies">
77
- {{- range $replies -}}
78
- {{ partial "reply.html" . }}
79
- {{- end -}}
80
- </section>
81
- {{- end -}}
82
- </div>
83
- {{- end -}}
84
- </div>
85
- {{ partial "pagination.html" $paginator }}
86
- {{- else -}}
87
- <p class="empty-state">This collection doesn't contain any threads.</p>
88
- {{- end -}}
27
+ {{ partial "collection-threads.html" (dict "paginator" $paginator "empty" "This collection doesn't contain any threads.") }}
89
28
  </section>
90
29
  {{- else -}}
91
30
  <section class="section">
@@ -2,18 +2,23 @@
2
2
  Atom 2005 feed template — mirrors the main site's `lib/feed.ts`.
3
3
 
4
4
  One template covers every page that opts into the custom `RSS` output
5
- format (home, featured, archive, and each collection section). Root-post
6
- sections do NOT opt in, so no `/{slug}/index.xml` gets emitted.
5
+ format (home, featured, archive, and each collection and smart collection
6
+ section). Root-post sections do NOT opt in, so no `/{slug}/index.xml` gets
7
+ emitted.
7
8
 
8
- Feed filter parity with the corresponding HTML list view:
9
- - home (`.Kind == "home"`): visibility == "public" (drops latest_hidden)
10
- - featured (`.Type == "featured"`): one Root per Thread whose derived
11
- featured projection is non-empty, ordered by selected Post publication
9
+ Each feed carries what the matching Jant feed carries, in its order:
10
+ - home (`.Kind == "home"`): `/latest/feed`, public roots by Thread
11
+ activity, pins ignored (`latest-members`)
12
+ - featured (`.Type == "featured"`): one root per Thread with a featured
13
+ post, by that post's publication (`featured-members`)
12
14
  - archive (`.Type == "archive"`): all published (includes latest_hidden)
13
- - collection (`.Type == "collection"`): Params.collections.slug match,
14
- ordered per `sort_order`; pinning is ignored for RSS
15
+ - collection (`.Type == "collection"`): its Threads by activity, newest
16
+ first, pins and `sort_order` ignored (`collection-members`)
17
+ - smart collection (`.Type == "smart_collection"`): the roots its
18
+ conditions select, in its stored order (`smart-collection-members`)
15
19
 
16
20
  Per-entry payload mirrors `buildSinglePostContent` + `buildFeedContent`:
21
+ - Id: `feed_id` from front matter, the id Jant's feed used; see below
17
22
  - Title: empty for quote posts, else .Title
18
23
  - Link rel=alternate: external URL for `link` format, else permalink;
19
24
  link-format posts also get a <link rel="related"> back to the
@@ -33,7 +38,6 @@
33
38
  {{- $page := . -}}
34
39
  {{- $limit := int (.Site.Params.rss_feed_limit | default 50) -}}
35
40
  {{- $allPosts := where .Site.Pages "Type" "post" -}}
36
- {{- $rootPosts := where $allPosts "Kind" "section" -}}
37
41
 
38
42
  {{- /* ------------------------------------------------------------------
39
43
  Post selection per feed kind
@@ -42,36 +46,15 @@
42
46
  {{- $isFeatured := eq $page.Type "featured" -}}
43
47
 
44
48
  {{- if eq $page.Kind "home" -}}
45
- {{- $public := where $allPosts "Params.visibility" "public" -}}
46
- {{- $posts = $public.ByDate.Reverse -}}
49
+ {{- $posts = partial "latest-members.html" (dict "feed" true) -}}
47
50
  {{- else if $isFeatured -}}
48
- {{- $featured := where $rootPosts "Params.featured_sort_at" "ne" nil -}}
49
- {{- $posts = sort $featured "Params.featured_sort_at" "desc" -}}
51
+ {{- $posts = partial "featured-members.html" . -}}
50
52
  {{- else if eq $page.Type "archive" -}}
51
53
  {{- $posts = $allPosts.ByDate.Reverse -}}
52
54
  {{- else if eq $page.Type "collection" -}}
53
- {{- $collectionSlug := $page.Params.slug | default $page.Slug -}}
54
- {{- $members := slice -}}
55
- {{- range $rootPosts -}}
56
- {{- $post := . -}}
57
- {{- with $post.Params.collections -}}
58
- {{- range . -}}
59
- {{- if eq .slug $collectionSlug -}}
60
- {{- $members = $members | append (dict "post" $post "entry" .) -}}
61
- {{- end -}}
62
- {{- end -}}
63
- {{- end -}}
64
- {{- end -}}
65
- {{- $sortOrder := $page.Params.sort_order | default "position" -}}
66
- {{- $ordered := $members -}}
67
- {{- if $ordered -}}
68
- {{- if eq $sortOrder "collected_at_desc" -}}
69
- {{- $ordered = sort $ordered "entry.collected_at" "desc" -}}
70
- {{- else -}}
71
- {{- $ordered = sort $ordered "entry.position" "asc" -}}
72
- {{- end -}}
73
- {{- end -}}
74
- {{- range $ordered -}}{{- $posts = $posts | append .post -}}{{- end -}}
55
+ {{- $posts = partial "collection-members.html" (dict "page" $page "feed" true) -}}
56
+ {{- else if eq $page.Type "smart_collection" -}}
57
+ {{- $posts = partial "smart-collection-members.html" (dict "page" $page "feed" true) -}}
75
58
  {{- end -}}
76
59
 
77
60
  {{- if gt (len $posts) $limit -}}
@@ -88,7 +71,7 @@
88
71
  {{- $feedTitle = printf "%s - Featured posts" .Site.Title -}}
89
72
  {{- else if eq $page.Type "archive" -}}
90
73
  {{- $feedTitle = printf "%s - Archive" .Site.Title -}}
91
- {{- else if eq $page.Type "collection" -}}
74
+ {{- else if in (slice "collection" "smart_collection") $page.Type -}}
92
75
  {{- $feedTitle = printf "%s - %s" .Site.Title $page.Title -}}
93
76
  {{- end -}}
94
77
 
@@ -117,6 +100,12 @@
117
100
  {{- $post := . -}}
118
101
  {{- $format := .Params.format | default "note" -}}
119
102
  {{- $permalink := .Permalink -}}
103
+ {{- /* <id>: the string Jant's feeds gave this entry, which the exporter
104
+ writes as `feed_id`. Readers recognise an entry by it alone, and
105
+ `.Permalink` differs from it (trailing slash; slug in place of a custom
106
+ path), so using the page URL would show every post again after a move.
107
+ A post written in Hugo has no `feed_id` and takes its page URL. */ -}}
108
+ {{- $entryId := string (.Params.feed_id | default $permalink) -}}
120
109
  {{- $isLink := and (eq $format "link") .Params.link_url -}}
121
110
  {{- $alternate := cond $isLink (string .Params.link_url) $permalink -}}
122
111
  {{- /* Atom <title>: empty for quotes (matches main site's getAtomTitle). */ -}}
@@ -148,7 +137,7 @@
148
137
  newest included — as `lib/thread-fold.ts` defines it. A thread of five
149
138
  replies or fewer is shown whole; `first` and `last` clamp to the slice,
150
139
  so the two windows simply overlap there. */ -}}
151
- {{- $replies := (where $post.Pages "Params.visibility" "public").ByDate -}}
140
+ {{- $replies := (where $post.Pages "Params.visibility" "public").ByWeight -}}
152
141
  {{- $truncated := $post.Params.truncated | default false -}}
153
142
  {{- range first 2 $replies -}}
154
143
  {{- if .Params.truncated -}}{{- $truncated = true -}}{{- end -}}
@@ -184,7 +173,7 @@
184
173
  <link rel="enclosure" type="{{ (string ($m.mime_type | default "application/octet-stream")) | transform.XMLEscape }}" href="{{ (string $m.src) | absURL | transform.XMLEscape }}"{{ with $m.size }} length="{{ . }}"{{ end }}{{ with $m.original_name }} title="{{ . | transform.XMLEscape }}"{{ end }}/>
185
174
  {{- end }}
186
175
  {{- end }}
187
- <id>{{ $permalink | transform.XMLEscape }}</id>
176
+ <id>{{ $entryId | transform.XMLEscape }}</id>
188
177
  <published>{{ $published }}</published>
189
178
  <updated>{{ $updated }}</updated>
190
179
  <jant:format>{{ $format | transform.XMLEscape }}</jant:format>
@@ -1,9 +1,9 @@
1
1
  {{ define "main" }}
2
2
  {{- $items := slice -}}
3
- {{- with hugo.Data.jant -}}{{- with .directory -}}{{- $items = . -}}{{- end -}}{{- end -}}
3
+ {{- with partial "jant-data.html" . -}}{{- with .directory -}}{{- $items = . -}}{{- end -}}{{- end -}}
4
4
  {{- $collectionCount := 0 -}}
5
5
  {{- range $items -}}
6
- {{- if eq .type "collection" -}}{{- $collectionCount = add $collectionCount 1 -}}{{- end -}}
6
+ {{- if in (slice "collection" "smart_collection") .type -}}{{- $collectionCount = add $collectionCount 1 -}}{{- end -}}
7
7
  {{- end -}}
8
8
 
9
9
  <div class="section section-collections" data-page="collections">
@@ -68,7 +68,7 @@
68
68
  {{- end -}}
69
69
  </div>
70
70
  </div>
71
- {{- else if eq .type "collection" -}}
71
+ {{- else if in (slice "collection" "smart_collection") .type -}}
72
72
  {{- $entryCount := .entry_count -}}
73
73
  {{- $activityLabel := .recent_activity_label -}}
74
74
  {{- $activityIso := .recent_activity_iso -}}
@@ -1,9 +1,6 @@
1
1
  {{ define "main" }}
2
2
  {{- /* Root bundles carry a derived projection of every Featured Post in the Thread. */ -}}
3
- {{- $posts := where .Site.Pages "Type" "post" -}}
4
- {{- $roots := where $posts "Kind" "section" -}}
5
- {{- $featured := where $roots "Params.featured_sort_at" "ne" nil -}}
6
- {{- $featured = sort $featured "Params.featured_sort_at" "desc" -}}
3
+ {{- $featured := partial "featured-members.html" . -}}
7
4
  {{- $pageSize := .Site.Params.page_size | default 10 -}}
8
5
  {{- $paginator := .Paginate $featured $pageSize -}}
9
6
 
@@ -1,28 +1,42 @@
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 -}}
8
-
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>
1
+ {{ define "main" }}
2
+ {{- /*
3
+ The home page lists Latest the way Jant's does: one list, pinned roots
4
+ first, paginated as a whole (`latest-members`). The pinned roots on a
5
+ page, which only lead the list, keep a block of their own.
6
+ */ -}}
7
+ {{- $pageSize := .Site.Params.page_size | default 10 -}}
8
+ {{- $paginator := .Paginate (partial "latest-members.html" (dict "feed" false)) $pageSize -}}
9
+ {{- $pinned := slice -}}
10
+ {{- $main := slice -}}
11
+ {{- range $paginator.Pages -}}
12
+ {{- if .Params.pinned_at -}}
13
+ {{- $pinned = $pinned | append . -}}
14
+ {{- else -}}
15
+ {{- $main = $main | append . -}}
16
+ {{- end -}}
26
17
  {{- end -}}
27
- </section>
18
+
19
+ <section class="home">
20
+ {{- if $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 -}}
28
+ {{- if $main -}}
29
+ <div class="post-list post-list-main">
30
+ {{- range $i, $p := $main -}}
31
+ {{- if gt $i 0 -}}<hr class="feed-divider" aria-hidden="true" />{{- end -}}
32
+ {{ partial "thread-preview.html" $p }}
33
+ {{- end -}}
34
+ </div>
35
+ {{- end -}}
36
+ {{- if $paginator.Pages -}}
37
+ {{ partial "pagination.html" $paginator }}
38
+ {{- else -}}
39
+ <p class="empty-state">Nothing published yet. Write your first post to get started.</p>
40
+ {{- end -}}
41
+ </section>
28
42
  {{ end }}
@@ -0,0 +1,100 @@
1
+ {{- /*
2
+ The Thread roots in a collection, in the order Jant lists them: the page
3
+ (`listCollectionThreadRootIdsForCollections` in `services/post.ts`) or the
4
+ feed (`listCollectionFeedEntriesForCollections`).
5
+
6
+ Takes a dict: `page`, the collection's section page, and `feed`, true for
7
+ its Atom feed.
8
+
9
+ A Thread belongs when its root lists the collection in `collections`.
10
+ Drafts and private roots are exported as Hugo drafts and never reach here;
11
+ they are checked again so a `--buildDrafts` preview agrees with Jant.
12
+
13
+ The page puts Threads pinned in this collection first, most recently
14
+ pinned first, then orders by `sort_order`, each order ending on the root
15
+ ID so no two Threads tie:
16
+ newest Thread activity (`last_activity_at`, else `date`), newest first
17
+ oldest the Thread's earliest published post, oldest first
18
+ rating_desc Threads with a rating before those without, by the highest
19
+ rating on any published post in the Thread, then as newest.
20
+ With fewer than two rated Threads it falls back to newest,
21
+ as Jant's page does.
22
+ The feed ignores pins and `sort_order`: Thread activity, newest first.
23
+
24
+ `src/__tests__/export-collection-order.test.ts` builds a site with Hugo
25
+ and compares the page and the feed with Jant's.
26
+ */ -}}
27
+ {{- $page := .page -}}
28
+ {{- $feed := .feed -}}
29
+ {{- $slug := $page.Params.slug | default $page.Slug -}}
30
+ {{- $roots := where (where site.Pages "Type" "post") "Kind" "section" -}}
31
+ {{- $members := slice -}}
32
+ {{- range $roots -}}
33
+ {{- $post := . -}}
34
+ {{- $params := .Params -}}
35
+ {{- $entry := false -}}
36
+ {{- range $params.collections -}}
37
+ {{- if eq .slug $slug -}}{{- $entry = . -}}{{- end -}}
38
+ {{- end -}}
39
+ {{- $visible := and (eq ($params.status | default "published") "published") (ne $params.visibility "private") -}}
40
+ {{- if and $entry $visible -}}
41
+ {{- $pinned := 0 -}}
42
+ {{- with $entry.pinned_at -}}{{- $pinned = (time.AsTime .).Unix -}}{{- end -}}
43
+ {{- $activity := $post.Date.Unix -}}
44
+ {{- with $params.last_activity_at -}}{{- $activity = (time.AsTime .).Unix -}}{{- end -}}
45
+ {{- $first := $post.Date.Unix -}}
46
+ {{- $rated := ne $params.rating nil -}}
47
+ {{- $rating := 0 -}}
48
+ {{- if $rated -}}{{- $rating = int $params.rating -}}{{- end -}}
49
+ {{- range $post.Pages -}}
50
+ {{- if eq (.Params.status | default "published") "published" -}}
51
+ {{- if lt .Date.Unix $first -}}{{- $first = .Date.Unix -}}{{- end -}}
52
+ {{- if ne .Params.rating nil -}}
53
+ {{- $rated = true -}}
54
+ {{- if gt (int .Params.rating) $rating -}}{{- $rating = int .Params.rating -}}{{- end -}}
55
+ {{- end -}}
56
+ {{- end -}}
57
+ {{- end -}}
58
+ {{- $members = $members | append (dict
59
+ "post" $post
60
+ "id" $params.id
61
+ "pinned" (cond $feed 0 $pinned)
62
+ "first" $first
63
+ "activity" $activity
64
+ "rated" $rated
65
+ "rating" $rating
66
+ ) -}}
67
+ {{- end -}}
68
+ {{- end -}}
69
+
70
+ {{- $order := "newest" -}}
71
+ {{- if not $feed -}}
72
+ {{- $order = $page.Params.sort_order | default "newest" -}}
73
+ {{- if and (eq $order "rating_desc") (le (len (where $members "rated" true)) 1) -}}
74
+ {{- $order = "newest" -}}
75
+ {{- end -}}
76
+ {{- end -}}
77
+
78
+ {{- /* One string key per Thread, fixed-width numbers first, so a single sort
79
+ orders by every key in turn. `oldest` sorts ascending, so its pin rank
80
+ counts down: the most recently pinned Thread gets the smallest. */ -}}
81
+ {{- $unpinnedRank := 99999999999 -}}
82
+ {{- $keyed := slice -}}
83
+ {{- range $members -}}
84
+ {{- $key := printf "%020d %020d %s" .pinned .activity .id -}}
85
+ {{- if eq $order "oldest" -}}
86
+ {{- $pinRank := cond (gt .pinned 0) (sub $unpinnedRank .pinned) $unpinnedRank -}}
87
+ {{- $key = printf "%020d %020d %s" $pinRank .first .id -}}
88
+ {{- else if eq $order "rating_desc" -}}
89
+ {{- $key = printf "%020d %d %02d %020d %s" .pinned (cond .rated 1 0) .rating .activity .id -}}
90
+ {{- end -}}
91
+ {{- $keyed = $keyed | append (dict "key" $key "post" .post) -}}
92
+ {{- end -}}
93
+
94
+ {{- $ordered := slice -}}
95
+ {{- if $keyed -}}
96
+ {{- range sort $keyed "key" (cond (eq $order "oldest") "asc" "desc") -}}
97
+ {{- $ordered = $ordered | append .post -}}
98
+ {{- end -}}
99
+ {{- end -}}
100
+ {{- return $ordered -}}
@@ -0,0 +1,33 @@
1
+ {{- /*
2
+ A collection page's Threads, each root with its replies in full, then the
3
+ pagination. Shared by manual and smart collections, which look the same to
4
+ a reader.
5
+
6
+ Takes a dict: `paginator`, and `empty`, the text shown when there are no
7
+ Threads.
8
+ */ -}}
9
+ {{- $paginator := .paginator -}}
10
+ {{- if $paginator.Pages -}}
11
+ <div class="post-list">
12
+ {{- range $i, $p := $paginator.Pages -}}
13
+ {{- if gt $i 0 -}}<hr class="feed-divider" aria-hidden="true" />{{- end -}}
14
+ {{- $replies := $p.Pages.ByWeight -}}
15
+ {{- $hasReplies := gt (len $replies) 0 -}}
16
+ <div class="thread thread-full{{ if $hasReplies }} thread-has-replies{{ end }}" data-slug="{{ $p.Params.slug | default $p.Slug }}">
17
+ <div class="thread-item thread-item-root">
18
+ {{ partial "post-card.html" $p }}
19
+ </div>
20
+ {{- if $hasReplies -}}
21
+ <section class="thread-replies" aria-label="Replies">
22
+ {{- range $replies -}}
23
+ {{ partial "reply.html" . }}
24
+ {{- end -}}
25
+ </section>
26
+ {{- end -}}
27
+ </div>
28
+ {{- end -}}
29
+ </div>
30
+ {{ partial "pagination.html" $paginator }}
31
+ {{- else -}}
32
+ <p class="empty-state">{{ .empty }}</p>
33
+ {{- end -}}
@@ -0,0 +1,35 @@
1
+ {{- /*
2
+ The Threads on Featured, in the order Jant lists them on the Featured page
3
+ and in `/featured/feed` (`listFeaturedThreadRootIds`): a Thread belongs
4
+ when any published post in it is featured and it is not private. Hidden
5
+ from Latest is no bar here.
6
+
7
+ Order: the newest publication among the Thread's featured posts
8
+ (`featured_sort_at`, which the export derives exactly that way), newest
9
+ first, then the root ID, newest first, so Threads whose featured posts
10
+ share a second don't tie.
11
+
12
+ `src/__tests__/export-feed-order.test.ts` builds a site with Hugo and
13
+ compares the page and the feed with Jant's.
14
+ */ -}}
15
+ {{- $roots := where (where site.Pages "Type" "post") "Kind" "section" -}}
16
+ {{- $keyed := slice -}}
17
+ {{- range $roots -}}
18
+ {{- $post := . -}}
19
+ {{- $params := .Params -}}
20
+ {{- $visible := and (eq ($params.status | default "published") "published") (ne $params.visibility "private") -}}
21
+ {{- if and $visible $params.featured_sort_at -}}
22
+ {{- $keyed = $keyed | append (dict
23
+ "key" (printf "%020d %s" (time.AsTime $params.featured_sort_at).Unix $params.id)
24
+ "post" $post
25
+ ) -}}
26
+ {{- end -}}
27
+ {{- end -}}
28
+
29
+ {{- $ordered := slice -}}
30
+ {{- if $keyed -}}
31
+ {{- range sort $keyed "key" "desc" -}}
32
+ {{- $ordered = $ordered | append .post -}}
33
+ {{- end -}}
34
+ {{- end -}}
35
+ {{- return $ordered -}}
@@ -8,7 +8,7 @@
8
8
  {{- $root := . -}}
9
9
  {{- $featuredIds := $root.Params.featured_post_ids | default slice -}}
10
10
  {{- $posts := slice $root -}}
11
- {{- range $root.Pages.ByDate -}}
11
+ {{- range $root.Pages.ByWeight -}}
12
12
  {{- $posts = $posts | append . -}}
13
13
  {{- end -}}
14
14
  {{- $lastIndex := sub (len $posts) 1 -}}
@@ -1,4 +1,4 @@
1
- {{- $jant := hugo.Data.jant -}}
1
+ {{- $jant := partial "jant-data.html" . -}}
2
2
  {{- $footerHtml := "" -}}
3
3
  {{- with $jant -}}{{- with .site_footer_html -}}{{- $footerHtml = . -}}{{- end -}}{{- end -}}
4
4
  {{- $showBranding := false -}}
@@ -1,4 +1,4 @@
1
- {{- $jant := hugo.Data.jant -}}
1
+ {{- $jant := partial "jant-data.html" . -}}
2
2
  {{- $siteTitle := .Site.Title -}}
3
3
  {{- $pageTitle := "" -}}
4
4
  {{- if .IsHome -}}
@@ -1,14 +1,16 @@
1
- {{- $jant := hugo.Data.jant -}}
1
+ {{- $jant := partial "jant-data.html" . -}}
2
2
  {{- $showAvatar := false -}}
3
3
  {{- with $jant -}}{{- if eq .show_header_avatar true -}}{{- $showAvatar = true -}}{{- end -}}{{- end -}}
4
4
  {{- $avatarUrl := "" -}}
5
5
  {{- with $jant -}}{{- with .site_avatar_url -}}{{- $avatarUrl = . -}}{{- end -}}{{- end -}}
6
6
 
7
- {{- /* Split nav items by placement: "header" (inline) vs "more" (supplemental dropdown). */ -}}
7
+ {{- /* Split nav items by placement: "header" (inline) vs "more" (supplemental dropdown).
8
+ `settings` is exported for an import back into Jant; the static site has no page for it. */ -}}
8
9
  {{- $headerItems := slice -}}
9
10
  {{- $moreItems := slice -}}
10
11
  {{- with $jant -}}{{- range .nav -}}
11
- {{- if eq .placement "more" -}}
12
+ {{- if eq .system_key "settings" -}}
13
+ {{- else if eq .placement "more" -}}
12
14
  {{- $moreItems = $moreItems | append . -}}
13
15
  {{- else -}}
14
16
  {{- $headerItems = $headerItems | append . -}}
@@ -0,0 +1,23 @@
1
+ {{- /*
2
+ Returns the `jant` table from `data/jant.toml` — nav, branding, and the
3
+ collections directory.
4
+
5
+ Hugo moved site data to `hugo.Data` in v0.160.0 and deprecated `site.Data`
6
+ in v0.156.0, so neither accessor works everywhere: `hugo.Data` is a render
7
+ error before it existed, and `site.Data` is a deprecation warning today and
8
+ a removal tomorrow. Only the taken branch of an `if` is evaluated, so
9
+ branching on the running version reads whichever one that version has.
10
+ `hugo.Version` compares semantically, not as a string.
11
+
12
+ Hugo rewrites a single `return` per template, hence the assignment rather
13
+ than a `return` in each branch. Every template reads the data through this
14
+ partial, so the day the old branch stops being worth carrying there is one
15
+ place to change.
16
+ */ -}}
17
+ {{- $data := dict -}}
18
+ {{- if ge hugo.Version "0.160.0" -}}
19
+ {{- $data = hugo.Data -}}
20
+ {{- else -}}
21
+ {{- $data = site.Data -}}
22
+ {{- end -}}
23
+ {{- return (index $data "jant") -}}
@@ -0,0 +1,48 @@
1
+ {{- /*
2
+ The Thread roots on Latest, in the order Jant lists them: the home page
3
+ (`assembleTimeline`, `posts.list`) or `/latest/feed`
4
+ (`latestFeedSelection` with `ignorePinnedSort`).
5
+
6
+ Takes a dict: `feed`, true for the Atom feed.
7
+
8
+ A root is on Latest when it is published and public: Hidden from Latest
9
+ and private roots are left out, as are drafts, which are exported as Hugo
10
+ drafts and never reach here; status and visibility are checked again so a
11
+ `--buildDrafts` preview agrees with Jant.
12
+
13
+ Order, each key after the one before it:
14
+ pinned page only: pinned roots first, most recently pinned first
15
+ activity Thread activity (`last_activity_at`, else `date`), newest
16
+ first, so a Thread that gains a reply comes back up; a quiet
17
+ reply doesn't move it, because Jant doesn't count it
18
+ ID newest first, so no two roots tie
19
+
20
+ `src/__tests__/export-feed-order.test.ts` builds a site with Hugo and
21
+ compares the page and the feed with Jant's.
22
+ */ -}}
23
+ {{- $feed := .feed -}}
24
+ {{- $roots := where (where site.Pages "Type" "post") "Kind" "section" -}}
25
+ {{- $keyed := slice -}}
26
+ {{- range $roots -}}
27
+ {{- $params := .Params -}}
28
+ {{- if and (eq ($params.status | default "published") "published") (eq $params.visibility "public") -}}
29
+ {{- $pinned := 0 -}}
30
+ {{- if not $feed -}}
31
+ {{- with $params.pinned_at -}}{{- $pinned = (time.AsTime .).Unix -}}{{- end -}}
32
+ {{- end -}}
33
+ {{- $activity := .Date.Unix -}}
34
+ {{- with $params.last_activity_at -}}{{- $activity = (time.AsTime .).Unix -}}{{- end -}}
35
+ {{- $keyed = $keyed | append (dict
36
+ "key" (printf "%020d %020d %s" $pinned $activity $params.id)
37
+ "post" .
38
+ ) -}}
39
+ {{- end -}}
40
+ {{- end -}}
41
+
42
+ {{- $ordered := slice -}}
43
+ {{- if $keyed -}}
44
+ {{- range sort $keyed "key" "desc" -}}
45
+ {{- $ordered = $ordered | append .post -}}
46
+ {{- end -}}
47
+ {{- end -}}
48
+ {{- return $ordered -}}