@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
package/src/preset.css
CHANGED
|
@@ -182,9 +182,9 @@ html {
|
|
|
182
182
|
--tw-prose-quote-borders: var(--color-border);
|
|
183
183
|
--tw-prose-captions: var(--color-muted-foreground);
|
|
184
184
|
--tw-prose-kbd: var(--color-foreground);
|
|
185
|
-
--tw-prose-code: var(--
|
|
186
|
-
--tw-prose-pre-code: var(--
|
|
187
|
-
--tw-prose-pre-bg: var(--
|
|
185
|
+
--tw-prose-code: var(--site-code-text);
|
|
186
|
+
--tw-prose-pre-code: var(--site-code-text);
|
|
187
|
+
--tw-prose-pre-bg: var(--site-code-block-bg);
|
|
188
188
|
--tw-prose-th-borders: var(--color-border);
|
|
189
189
|
--tw-prose-td-borders: var(--color-border);
|
|
190
190
|
font-family: var(--font-body);
|
|
@@ -213,14 +213,12 @@ html {
|
|
|
213
213
|
|
|
214
214
|
:where(h2) {
|
|
215
215
|
font-size: var(--type-content-title);
|
|
216
|
-
font-style: italic;
|
|
217
216
|
margin-top: 2.1rem;
|
|
218
217
|
margin-bottom: 1.4rem;
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
:where(h3) {
|
|
222
221
|
font-size: var(--type-content-subtitle);
|
|
223
|
-
font-style: italic;
|
|
224
222
|
margin-top: 2rem;
|
|
225
223
|
margin-bottom: 1.4rem;
|
|
226
224
|
}
|
|
@@ -236,12 +234,12 @@ html {
|
|
|
236
234
|
|
|
237
235
|
:where(ul, ol) {
|
|
238
236
|
margin: 1.25em 0;
|
|
239
|
-
padding-left: 1.
|
|
237
|
+
padding-left: 1.4em;
|
|
240
238
|
}
|
|
241
239
|
|
|
242
240
|
:where(li, dt) {
|
|
243
|
-
margin-top:
|
|
244
|
-
margin-bottom:
|
|
241
|
+
margin-top: 0.65em;
|
|
242
|
+
margin-bottom: 0.65em;
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
/* Normalize li > p so list spacing is controlled by li alone,
|
|
@@ -257,18 +255,35 @@ html {
|
|
|
257
255
|
:where(code, pre code) {
|
|
258
256
|
font-size: var(--type-code);
|
|
259
257
|
line-height: 1.42;
|
|
258
|
+
color: var(--site-code-text);
|
|
260
259
|
overflow-wrap: break-word;
|
|
261
260
|
}
|
|
262
261
|
|
|
263
262
|
:where(:not(pre) > code) {
|
|
264
|
-
padding: 0.
|
|
265
|
-
background-color: var(--site-
|
|
266
|
-
border:
|
|
267
|
-
|
|
263
|
+
padding: 0.08em 0.28em;
|
|
264
|
+
background-color: var(--site-code-bg);
|
|
265
|
+
border-radius: 0.22em;
|
|
266
|
+
box-decoration-break: clone;
|
|
267
|
+
-webkit-box-decoration-break: clone;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
:where(:not(pre) > code)::before,
|
|
271
|
+
:where(:not(pre) > code)::after {
|
|
272
|
+
content: none;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
:where(pre) {
|
|
276
|
+
background-color: var(--site-code-block-bg);
|
|
277
|
+
border: 1px solid var(--site-code-block-border);
|
|
278
|
+
border-radius: 6px;
|
|
268
279
|
}
|
|
269
280
|
|
|
270
281
|
:where(pre code) {
|
|
271
282
|
font-size: var(--type-code-block);
|
|
283
|
+
color: inherit;
|
|
284
|
+
background: transparent;
|
|
285
|
+
border: 0;
|
|
286
|
+
padding: 0;
|
|
272
287
|
}
|
|
273
288
|
|
|
274
289
|
:where(h1, h2, h3) :where(code) {
|
|
@@ -425,6 +440,37 @@ html {
|
|
|
425
440
|
margin-bottom: 0;
|
|
426
441
|
}
|
|
427
442
|
|
|
443
|
+
/*
|
|
444
|
+
* Feed body headings
|
|
445
|
+
*
|
|
446
|
+
* In list contexts, the post title owns the card heading level. Authored
|
|
447
|
+
* headings inside the body should remain visible, but they should read as
|
|
448
|
+
* sections within the card rather than as peer post titles.
|
|
449
|
+
*/
|
|
450
|
+
[data-post]:not([data-page="post"]) [data-post-body].prose {
|
|
451
|
+
:where(h1, h2) {
|
|
452
|
+
font-size: calc(var(--type-content-body) * 1.12);
|
|
453
|
+
font-weight: var(--fw-medium);
|
|
454
|
+
margin-top: 1.45rem;
|
|
455
|
+
margin-bottom: 0.55rem;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
:where(h3, h4) {
|
|
459
|
+
font-size: var(--type-content-body);
|
|
460
|
+
font-weight: var(--fw-medium);
|
|
461
|
+
margin-top: 1.2rem;
|
|
462
|
+
margin-bottom: 0.45rem;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
:where(h5, h6) {
|
|
466
|
+
font-size: var(--type-secondary);
|
|
467
|
+
font-weight: var(--fw-medium);
|
|
468
|
+
color: var(--site-text-secondary);
|
|
469
|
+
margin-top: 1rem;
|
|
470
|
+
margin-bottom: 0.35rem;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
428
474
|
/*
|
|
429
475
|
* Detail-page reading prose
|
|
430
476
|
*
|
|
@@ -455,12 +501,8 @@ html {
|
|
|
455
501
|
);
|
|
456
502
|
--tw-prose-captions: var(--site-reading-caption);
|
|
457
503
|
--tw-prose-kbd: var(--site-reading-heading);
|
|
458
|
-
--tw-prose-code: var(--site-
|
|
459
|
-
--tw-prose-pre-code:
|
|
460
|
-
in srgb,
|
|
461
|
-
var(--site-reading-caption) 86%,
|
|
462
|
-
var(--site-reading-body)
|
|
463
|
-
);
|
|
504
|
+
--tw-prose-code: var(--site-code-text);
|
|
505
|
+
--tw-prose-pre-code: var(--site-code-text);
|
|
464
506
|
--tw-prose-th-borders: color-mix(
|
|
465
507
|
in srgb,
|
|
466
508
|
var(--site-divider) 88%,
|
|
@@ -77,6 +77,32 @@ describe("Nav Items API Routes", () => {
|
|
|
77
77
|
expect(body.label).toBe("GitHub");
|
|
78
78
|
expect(body.url).toBe("https://github.com");
|
|
79
79
|
expect(body.type).toBe("link");
|
|
80
|
+
expect(body.headerHtml).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("includes a site header fragment for navigation editor requests", async () => {
|
|
84
|
+
const { app } = createTestApp({ authenticated: true });
|
|
85
|
+
app.route("/api/nav-items", navItemsApiRoutes);
|
|
86
|
+
|
|
87
|
+
const res = await app.request("/api/nav-items", {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
"X-Jant-Site-Header": "include",
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
type: "link",
|
|
95
|
+
label: "Docs",
|
|
96
|
+
url: "/docs",
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(res.status).toBe(201);
|
|
101
|
+
const body = await res.json();
|
|
102
|
+
expect(body.label).toBe("Docs");
|
|
103
|
+
expect(body.headerHtml).toContain('data-site-header-fragment="header"');
|
|
104
|
+
expect(body.headerHtml).toContain('data-site-header-fragment="drawer"');
|
|
105
|
+
expect(body.headerHtml).toContain("Docs");
|
|
80
106
|
});
|
|
81
107
|
|
|
82
108
|
it("creates a system nav item when authenticated", async () => {
|
package/src/routes/api/export.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../types/app-context.js";
|
|
8
|
-
import { getHomeDefaultViewFromNavItems } from "../../lib/navigation.js";
|
|
9
8
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
10
9
|
import { createExportService } from "../../services/export.js";
|
|
11
10
|
|
|
@@ -25,7 +24,6 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
|
|
|
25
24
|
siteDescription: appConfig.siteDescription,
|
|
26
25
|
siteLanguage: appConfig.siteLanguage,
|
|
27
26
|
showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
|
|
28
|
-
homeDefaultView: getHomeDefaultViewFromNavItems(navItems),
|
|
29
27
|
mainRssFeed: appConfig.mainRssFeed,
|
|
30
28
|
siteFooter: appConfig.siteFooter,
|
|
31
29
|
showHeaderAvatar: appConfig.showHeaderAvatar,
|
|
@@ -136,7 +136,7 @@ describe("Internal upload admin routes", () => {
|
|
|
136
136
|
expect(storage.files.has(row.tempStorageKey)).toBe(false);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
it("
|
|
139
|
+
it("keeps finalized unattached media during upload cleanup", async () => {
|
|
140
140
|
const storage = createMockStorage();
|
|
141
141
|
const { app, services, sqlite } = createTestApp({
|
|
142
142
|
authenticated: false,
|
|
@@ -187,28 +187,24 @@ describe("Internal upload admin routes", () => {
|
|
|
187
187
|
await expect(res.json()).resolves.toEqual({
|
|
188
188
|
abortedMultipartUploads: 0,
|
|
189
189
|
deletedSessions: 0,
|
|
190
|
-
deletedOrphanMedia:
|
|
191
|
-
// The reaped orphan's object is freshly enqueued (30 days out), so the
|
|
192
|
-
// same sweep purges nothing yet.
|
|
190
|
+
deletedOrphanMedia: 0,
|
|
193
191
|
purgedStorageObjects: 0,
|
|
194
192
|
});
|
|
195
193
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
194
|
+
// Finalized media may be referenced from post body JSON without being a
|
|
195
|
+
// post attachment, so upload cleanup must not delete it from `postId IS
|
|
196
|
+
// NULL` alone.
|
|
198
197
|
expect(
|
|
199
198
|
sqlite.prepare("select id from media where id = ?").get(oldOrphan.id),
|
|
200
|
-
).
|
|
201
|
-
expect(storage.files.has(oldStorageKey)).toBe(
|
|
202
|
-
expect([...storage.files.keys()].some((k) => k.startsWith("trash/"))).toBe(
|
|
203
|
-
true,
|
|
204
|
-
);
|
|
199
|
+
).toBeDefined();
|
|
200
|
+
expect(storage.files.has(oldStorageKey)).toBe(true);
|
|
205
201
|
expect(
|
|
206
202
|
sqlite
|
|
207
203
|
.prepare("select id from storage_purge where original_key = ?")
|
|
208
204
|
.get(oldStorageKey),
|
|
209
|
-
).
|
|
205
|
+
).toBeUndefined();
|
|
210
206
|
|
|
211
|
-
// Fresh
|
|
207
|
+
// Fresh unattached media is also untouched.
|
|
212
208
|
expect(
|
|
213
209
|
sqlite.prepare("select id from media where id = ?").get(freshOrphan.id),
|
|
214
210
|
).toBeDefined();
|
|
@@ -163,11 +163,11 @@ internalSitesRoutes.get(
|
|
|
163
163
|
},
|
|
164
164
|
);
|
|
165
165
|
|
|
166
|
-
// Clean up a single managed site's expired upload sessions and
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
166
|
+
// Clean up a single managed site's expired upload sessions and due trash
|
|
167
|
+
// objects. Invoked per-site by the hosted control plane's scheduled
|
|
168
|
+
// maintenance. Self-hosted operators use the host-scoped
|
|
169
|
+
// `POST /api/internal/uploads/cleanup` route / `jant uploads cleanup` CLI
|
|
170
|
+
// instead.
|
|
171
171
|
internalSitesRoutes.post(
|
|
172
172
|
"/:siteId/uploads/cleanup",
|
|
173
173
|
requireInternalAdminApi(),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
+
import type { Context } from "hono";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import type { Bindings } from "../../types.js";
|
|
8
9
|
import type { AppVariables } from "../../types/app-context.js";
|
|
@@ -16,16 +17,36 @@ import {
|
|
|
16
17
|
import { assertFound, parseIdParam, NotFoundError } from "../../lib/errors.js";
|
|
17
18
|
import { ID_PREFIX } from "../../lib/ids.js";
|
|
18
19
|
import { getCollectionPagePath } from "../../lib/collection-paths.js";
|
|
20
|
+
import { renderSiteHeaderHtml } from "../../lib/site-header-fragment.js";
|
|
19
21
|
|
|
20
22
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
21
23
|
|
|
22
24
|
export const navItemsApiRoutes = new Hono<Env>();
|
|
23
25
|
|
|
26
|
+
const INCLUDE_SITE_HEADER_RESPONSE = "include";
|
|
27
|
+
const SITE_HEADER_RESPONSE_HEADER = "x-jant-site-header";
|
|
28
|
+
|
|
24
29
|
const MoveSchema = z.object({
|
|
25
30
|
after: NavItemIdSchema.nullable().optional(),
|
|
26
31
|
before: NavItemIdSchema.nullable().optional(),
|
|
27
32
|
});
|
|
28
33
|
|
|
34
|
+
async function withSiteHeaderHtml<T extends object>(
|
|
35
|
+
c: Context<Env>,
|
|
36
|
+
body: T,
|
|
37
|
+
): Promise<T | (T & { headerHtml: string })> {
|
|
38
|
+
if (
|
|
39
|
+
c.req.header(SITE_HEADER_RESPONSE_HEADER) !== INCLUDE_SITE_HEADER_RESPONSE
|
|
40
|
+
) {
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...body,
|
|
46
|
+
headerHtml: await renderSiteHeaderHtml(c),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
// List nav items
|
|
30
51
|
navItemsApiRoutes.get("/", async (c) => {
|
|
31
52
|
const items = await c.var.services.navItems.list();
|
|
@@ -46,7 +67,7 @@ navItemsApiRoutes.put("/:id/move", requireAuthApi(), async (c) => {
|
|
|
46
67
|
"Nav item",
|
|
47
68
|
);
|
|
48
69
|
|
|
49
|
-
return c.json(item);
|
|
70
|
+
return c.json(await withSiteHeaderHtml(c, item));
|
|
50
71
|
});
|
|
51
72
|
|
|
52
73
|
// Create nav item (requires auth)
|
|
@@ -82,7 +103,7 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
82
103
|
});
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
return c.json(item, 201);
|
|
106
|
+
return c.json(await withSiteHeaderHtml(c, item), 201);
|
|
86
107
|
});
|
|
87
108
|
|
|
88
109
|
// Update nav item (requires auth)
|
|
@@ -95,7 +116,7 @@ navItemsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
95
116
|
"Nav item",
|
|
96
117
|
);
|
|
97
118
|
|
|
98
|
-
return c.json(item);
|
|
119
|
+
return c.json(await withSiteHeaderHtml(c, item));
|
|
99
120
|
});
|
|
100
121
|
|
|
101
122
|
// Delete nav item (requires auth)
|
|
@@ -105,5 +126,5 @@ navItemsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
|
105
126
|
const success = await c.var.services.navItems.delete(id);
|
|
106
127
|
if (!success) throw new NotFoundError("Nav item");
|
|
107
128
|
|
|
108
|
-
return c.json({ success: true });
|
|
129
|
+
return c.json(await withSiteHeaderHtml(c, { success: true }));
|
|
109
130
|
});
|
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
isSyncPending,
|
|
76
76
|
markSyncPending,
|
|
77
77
|
runBackgroundSync,
|
|
78
|
+
triggerGitHubSyncInline,
|
|
78
79
|
} from "../../lib/github-sync-trigger.js";
|
|
79
80
|
import { buildSyncSiteConfig } from "../../lib/github-sync-site-config.js";
|
|
80
81
|
import {
|
|
@@ -117,6 +118,10 @@ function publicPath(c: Context<Env>, path: string): string {
|
|
|
117
118
|
return toPublicPath(path, c.var.appConfig.sitePathPrefix);
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
function aboutEditPath(c: Context<Env>): string {
|
|
122
|
+
return `${publicPath(c, "/about")}?edit=1`;
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
/**
|
|
121
126
|
* Breadcrumb labels for admin settings pages.
|
|
122
127
|
*
|
|
@@ -184,7 +189,10 @@ function breadcrumbLabel(
|
|
|
184
189
|
);
|
|
185
190
|
case "account":
|
|
186
191
|
return i18n._(
|
|
187
|
-
msg({
|
|
192
|
+
msg({
|
|
193
|
+
message: "Account & Data",
|
|
194
|
+
comment: "@context: Breadcrumb label",
|
|
195
|
+
}),
|
|
188
196
|
);
|
|
189
197
|
case "sessions":
|
|
190
198
|
return i18n._(
|
|
@@ -302,7 +310,10 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
302
310
|
const dbSiteDescription = allSettings["SITE_DESCRIPTION"] ?? "";
|
|
303
311
|
|
|
304
312
|
const saved = c.req.query("saved") !== undefined;
|
|
305
|
-
const navData = await
|
|
313
|
+
const [navData, aboutPage] = await Promise.all([
|
|
314
|
+
getNavigationData(c),
|
|
315
|
+
c.var.services.aboutPage.getStatus(),
|
|
316
|
+
]);
|
|
306
317
|
const siteUrlForDisplay =
|
|
307
318
|
appConfig.siteUrl || new URL(publicPath(c, "/"), c.req.url).toString();
|
|
308
319
|
|
|
@@ -356,6 +367,9 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
356
367
|
noindex={appConfig.noindex}
|
|
357
368
|
demoMode={appConfig.demoMode}
|
|
358
369
|
timezones={TIMEZONES}
|
|
370
|
+
aboutPage={aboutPage}
|
|
371
|
+
aboutEditUrl={aboutEditPath(c)}
|
|
372
|
+
aboutCreateUrl={publicPath(c, "/settings/general/about-page")}
|
|
359
373
|
/>
|
|
360
374
|
</>
|
|
361
375
|
),
|
|
@@ -534,6 +548,32 @@ settingsRoutes.post("/general/search", async (c) => {
|
|
|
534
548
|
);
|
|
535
549
|
});
|
|
536
550
|
|
|
551
|
+
settingsRoutes.post("/general/about-page", async (c) => {
|
|
552
|
+
try {
|
|
553
|
+
await c.var.services.aboutPage.ensurePage();
|
|
554
|
+
await triggerGitHubSyncInline(c);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (error instanceof ValidationError) {
|
|
557
|
+
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
558
|
+
if (wantsJson) {
|
|
559
|
+
return c.json({ error: error.message, code: error.code }, 400);
|
|
560
|
+
}
|
|
561
|
+
return c.text(error.message, 400);
|
|
562
|
+
}
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
567
|
+
if (wantsJson) {
|
|
568
|
+
return c.json({
|
|
569
|
+
status: "redirect" as const,
|
|
570
|
+
url: aboutEditPath(c),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return c.redirect(aboutEditPath(c));
|
|
575
|
+
});
|
|
576
|
+
|
|
537
577
|
// ===========================================================================
|
|
538
578
|
// Avatar
|
|
539
579
|
// ===========================================================================
|
|
@@ -743,9 +783,13 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
743
783
|
// ===========================================================================
|
|
744
784
|
|
|
745
785
|
settingsRoutes.get("/navigation", async (c) => {
|
|
746
|
-
const [navItems, directoryData] = await Promise.all([
|
|
786
|
+
const [navItems, directoryData, suggestedLinks] = await Promise.all([
|
|
747
787
|
c.var.services.navItems.list(),
|
|
748
788
|
c.var.services.collections.listDirectoryData(),
|
|
789
|
+
c.var.services.navItems.listSuggestedLinks({
|
|
790
|
+
siteOrigin: c.var.appConfig.siteOrigin,
|
|
791
|
+
sitePathPrefix: c.var.appConfig.sitePathPrefix,
|
|
792
|
+
}),
|
|
749
793
|
]);
|
|
750
794
|
const navData = await getNavigationData(c);
|
|
751
795
|
|
|
@@ -762,6 +806,7 @@ settingsRoutes.get("/navigation", async (c) => {
|
|
|
762
806
|
<NavigationContent
|
|
763
807
|
navItems={navItems}
|
|
764
808
|
directoryData={directoryData}
|
|
809
|
+
suggestedLinks={suggestedLinks}
|
|
765
810
|
mainRssFeed={c.var.appConfig.mainRssFeed}
|
|
766
811
|
siteName={navData.siteName}
|
|
767
812
|
sitePathPrefix={c.var.appConfig.sitePathPrefix}
|
|
@@ -121,6 +121,48 @@ describe("Atom Feed Routes", () => {
|
|
|
121
121
|
expect(xml).not.toContain("Hidden Post");
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it("ignores pinned ordering when the main feed is latest", async () => {
|
|
125
|
+
const { app, services } = createFeedTestApp({
|
|
126
|
+
RSS_FEED_LIMIT: "2",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await services.settings.set("MAIN_RSS_FEED", "latest");
|
|
130
|
+
|
|
131
|
+
await services.posts.create({
|
|
132
|
+
format: "note",
|
|
133
|
+
title: "Older pinned",
|
|
134
|
+
bodyMarkdown: "Pinned but old",
|
|
135
|
+
status: "published",
|
|
136
|
+
pinned: true,
|
|
137
|
+
publishedAt: 1000,
|
|
138
|
+
});
|
|
139
|
+
await services.posts.create({
|
|
140
|
+
format: "note",
|
|
141
|
+
title: "Newer unpinned",
|
|
142
|
+
bodyMarkdown: "Newer",
|
|
143
|
+
status: "published",
|
|
144
|
+
publishedAt: 2000,
|
|
145
|
+
});
|
|
146
|
+
await services.posts.create({
|
|
147
|
+
format: "note",
|
|
148
|
+
title: "Newest unpinned",
|
|
149
|
+
bodyMarkdown: "Newest",
|
|
150
|
+
status: "published",
|
|
151
|
+
publishedAt: 3000,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const res = await app.request("/feed");
|
|
155
|
+
expect(res.status).toBe(200);
|
|
156
|
+
|
|
157
|
+
const xml = await res.text();
|
|
158
|
+
expect(xml).toContain("Newest unpinned");
|
|
159
|
+
expect(xml).toContain("Newer unpinned");
|
|
160
|
+
expect(xml).not.toContain("Older pinned");
|
|
161
|
+
expect(xml.indexOf("Newest unpinned")).toBeLessThan(
|
|
162
|
+
xml.indexOf("Newer unpinned"),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
124
166
|
it("returns Atom content type", async () => {
|
|
125
167
|
const { app } = createFeedTestApp();
|
|
126
168
|
|
|
@@ -345,19 +345,8 @@ describe("Sitemap Routes", () => {
|
|
|
345
345
|
expect(xml).toContain(`${TEST_SITE_ORIGIN}/archive`);
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
it("includes /
|
|
349
|
-
const { app
|
|
350
|
-
await services.settings.set("HOME_DEFAULT_VIEW", "featured");
|
|
351
|
-
|
|
352
|
-
const xml = await (await app.request("/sitemap-pages.xml")).text();
|
|
353
|
-
expect(xml).toContain(`${TEST_SITE_ORIGIN}/latest`);
|
|
354
|
-
expect(xml).not.toContain(`${TEST_SITE_ORIGIN}/featured`);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("includes /featured when the homepage default is 'latest'", async () => {
|
|
358
|
-
const { app, services } = createSitemapTestApp();
|
|
359
|
-
await services.settings.set("HOME_DEFAULT_VIEW", "latest");
|
|
360
|
-
|
|
348
|
+
it("includes /featured as the secondary aggregate page", async () => {
|
|
349
|
+
const { app } = createSitemapTestApp();
|
|
361
350
|
const xml = await (await app.request("/sitemap-pages.xml")).text();
|
|
362
351
|
expect(xml).toContain(`${TEST_SITE_ORIGIN}/featured`);
|
|
363
352
|
expect(xml).not.toContain(`${TEST_SITE_ORIGIN}/latest`);
|
package/src/routes/feed/feed.ts
CHANGED
|
@@ -185,7 +185,7 @@ sitemapRoutes.get("/sitemap-collections.xml", async (c) => {
|
|
|
185
185
|
|
|
186
186
|
sitemapRoutes.get("/sitemap-pages.xml", async (c) => {
|
|
187
187
|
const { appConfig } = c.var;
|
|
188
|
-
const { siteUrl, sitePathPrefix
|
|
188
|
+
const { siteUrl, sitePathPrefix } = appConfig;
|
|
189
189
|
|
|
190
190
|
const urls: SitemapUrlEntry[] = [
|
|
191
191
|
{
|
|
@@ -200,12 +200,8 @@ sitemapRoutes.get("/sitemap-pages.xml", async (c) => {
|
|
|
200
200
|
},
|
|
201
201
|
];
|
|
202
202
|
|
|
203
|
-
// Whichever of /latest and /featured is NOT the homepage default is a
|
|
204
|
-
// standalone URL worth indexing; the other 302-redirects to `/`.
|
|
205
|
-
const secondaryAggregate =
|
|
206
|
-
homeDefaultView === "featured" ? "/latest" : "/featured";
|
|
207
203
|
urls.push({
|
|
208
|
-
loc: absoluteUrl(
|
|
204
|
+
loc: absoluteUrl("/featured", siteUrl, sitePathPrefix),
|
|
209
205
|
priority: "0.6",
|
|
210
206
|
changefreq: "daily",
|
|
211
207
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import { pageRoutes } from "../page.js";
|
|
4
|
+
|
|
5
|
+
function createPageTestApp(options: { authenticated?: boolean } = {}) {
|
|
6
|
+
const testApp = createTestApp(options);
|
|
7
|
+
const { app } = testApp;
|
|
8
|
+
|
|
9
|
+
app.use("*", async (c, next) => {
|
|
10
|
+
c.set("publicPath", c.req.path);
|
|
11
|
+
c.set("publicRequestUrl", c.req.url);
|
|
12
|
+
await next();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.route("/", pageRoutes);
|
|
16
|
+
|
|
17
|
+
return testApp;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function createDraftAbout(
|
|
21
|
+
services: ReturnType<typeof createTestApp>["services"],
|
|
22
|
+
) {
|
|
23
|
+
return services.posts.create({
|
|
24
|
+
format: "note",
|
|
25
|
+
title: "About",
|
|
26
|
+
slug: "about",
|
|
27
|
+
bodyMarkdown: "Draft About body",
|
|
28
|
+
status: "draft",
|
|
29
|
+
visibility: "latest_hidden",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("draft About edit route", () => {
|
|
34
|
+
it("keeps draft About hidden from anonymous edit requests", async () => {
|
|
35
|
+
const { app, services } = createPageTestApp();
|
|
36
|
+
await createDraftAbout(services);
|
|
37
|
+
|
|
38
|
+
const res = await app.request("/about?edit=1");
|
|
39
|
+
|
|
40
|
+
expect(res.status).toBe(404);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps draft About hidden from normal authenticated page requests", async () => {
|
|
44
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
45
|
+
await createDraftAbout(services);
|
|
46
|
+
|
|
47
|
+
const res = await app.request("/about");
|
|
48
|
+
|
|
49
|
+
expect(res.status).toBe(404);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders draft About for authenticated edit requests", async () => {
|
|
53
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
54
|
+
const post = await createDraftAbout(services);
|
|
55
|
+
|
|
56
|
+
const res = await app.request("/about?edit=1");
|
|
57
|
+
|
|
58
|
+
expect(res.status).toBe(200);
|
|
59
|
+
const html = await res.text();
|
|
60
|
+
expect(html).toContain(`data-post-id="${post.id}"`);
|
|
61
|
+
expect(html).toContain("data-authenticated");
|
|
62
|
+
expect(html).not.toContain('rel="canonical"');
|
|
63
|
+
expect(html).not.toContain("application/ld+json");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -630,6 +630,7 @@ async function buildArchiveFeedData(
|
|
|
630
630
|
hasMedia: params.hasMedia,
|
|
631
631
|
hasTitle: params.hasTitle,
|
|
632
632
|
hasReplies: params.hasReplies,
|
|
633
|
+
ignorePinnedSort: true,
|
|
633
634
|
...(params.validYear
|
|
634
635
|
? {
|
|
635
636
|
publishedAfter: Date.UTC(params.validYear, 0, 1) / 1000,
|
|
@@ -27,10 +27,6 @@ featuredRoutes.get("/", async (c) => {
|
|
|
27
27
|
const navData = await getNavigationData(c);
|
|
28
28
|
const i18n = getI18n(c);
|
|
29
29
|
|
|
30
|
-
// When homepage already shows featured, redirect to avoid duplicate content
|
|
31
|
-
if (navData.homeDefaultView === "featured") {
|
|
32
|
-
return c.redirect(toPublicPath("/", navData.sitePathPrefix), 302);
|
|
33
|
-
}
|
|
34
30
|
const page = parsePageNumber(c.req.query("page"));
|
|
35
31
|
const featuredTitle = i18n._(
|
|
36
32
|
msg({
|