@jant/core 0.6.12 → 0.6.14
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 +2 -10
- 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-D-zAhayc.js → app-2i7-WQHg.js} +426 -450
- package/dist/app-DZDlmh7C.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-S1cdvulk.js → client-BvID5ucz.js} +1 -1
- package/dist/client/_assets/client-Cevfpm8H.css +2 -0
- package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-v3yvhbgZ.js} +596 -590
- package/dist/{export-Ckwh4BiE.js → export-DWn149b7.js} +24 -19
- package/dist/{github-sync-BMmK3JZ0.js → github-sync-C0zyF6XS.js} +2 -2
- package/dist/{github-sync-Dl9wQMar.js → github-sync-Ckk0sJxK.js} +1 -1
- 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 +1 -2
- 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 +137 -10
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +107 -22
- package/src/client/components/__tests__/jant-settings-general.test.ts +1 -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 +100 -70
- package/src/client/components/jant-compose-dialog.ts +3 -1
- 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 +245 -155
- package/src/client/components/nav-manager-types.ts +0 -10
- 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__/footnotes.test.ts +284 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +148 -1
- package/src/client/tiptap/bubble-menu.ts +3 -3
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/footnotes.ts +156 -0
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/markdown-clipboard.ts +7 -2
- package/src/client-auth.ts +0 -1
- 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 +20 -4
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +20 -4
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +23 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +23 -20
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +23 -20
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +4 -0
- 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/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/markdown-manager.ts +14 -5
- package/src/lib/navigation.ts +5 -22
- 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/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 +8 -2
- 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/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/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +41 -0
- package/src/services/__tests__/post-timeline.test.ts +56 -0
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +5 -15
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/navigation.ts +44 -13
- package/src/services/post.ts +80 -57
- 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/tokens.css +2 -2
- package/src/styles/ui.css +316 -34
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +1 -1
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/feed/__tests__/timeline-cards.test.ts +8 -3
- package/src/ui/layouts/BaseLayout.tsx +1 -1
- package/src/ui/layouts/SiteLayout.tsx +266 -227
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +48 -0
- package/dist/app-B5P1b4TF.js +0 -6
- package/dist/client/_assets/client-BUxR-E8O.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -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
|
});
|
|
@@ -189,7 +189,10 @@ function breadcrumbLabel(
|
|
|
189
189
|
);
|
|
190
190
|
case "account":
|
|
191
191
|
return i18n._(
|
|
192
|
-
msg({
|
|
192
|
+
msg({
|
|
193
|
+
message: "Account & Data",
|
|
194
|
+
comment: "@context: Breadcrumb label",
|
|
195
|
+
}),
|
|
193
196
|
);
|
|
194
197
|
case "sessions":
|
|
195
198
|
return i18n._(
|
|
@@ -783,7 +786,10 @@ settingsRoutes.get("/navigation", async (c) => {
|
|
|
783
786
|
const [navItems, directoryData, suggestedLinks] = await Promise.all([
|
|
784
787
|
c.var.services.navItems.list(),
|
|
785
788
|
c.var.services.collections.listDirectoryData(),
|
|
786
|
-
c.var.services.navItems.listSuggestedLinks(
|
|
789
|
+
c.var.services.navItems.listSuggestedLinks({
|
|
790
|
+
siteOrigin: c.var.appConfig.siteOrigin,
|
|
791
|
+
sitePathPrefix: c.var.appConfig.sitePathPrefix,
|
|
792
|
+
}),
|
|
787
793
|
]);
|
|
788
794
|
const navData = await getNavigationData(c);
|
|
789
795
|
|
|
@@ -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
|
});
|
|
@@ -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({
|
|
@@ -3,31 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Timeline feed with per-type card components and thread previews.
|
|
5
5
|
* Uses page-based pagination.
|
|
6
|
-
*
|
|
7
|
-
* The homepage shows whichever built-in feed comes first between
|
|
8
|
-
* Latest and Featured in navigation. The explicit feed routes still work.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
import { Hono } from "hono";
|
|
12
9
|
import { msg } from "@lingui/core/macro";
|
|
13
10
|
import type { Bindings } from "../../types.js";
|
|
14
11
|
import type { AppVariables } from "../../types/app-context.js";
|
|
15
|
-
import {
|
|
16
|
-
getHomeDefaultViewFromNavItems,
|
|
17
|
-
getNavigationData,
|
|
18
|
-
} from "../../lib/navigation.js";
|
|
12
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
19
13
|
import { getI18n } from "../../i18n/index.js";
|
|
20
14
|
import { formatPageLabel, parsePageNumber } from "../../lib/pagination.js";
|
|
21
15
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
22
16
|
import { renderPublicPage } from "../../lib/render.js";
|
|
23
|
-
import {
|
|
24
|
-
assembleFeaturedTimeline,
|
|
25
|
-
assembleTimeline,
|
|
26
|
-
} from "../../lib/timeline.js";
|
|
17
|
+
import { assembleTimeline } from "../../lib/timeline.js";
|
|
27
18
|
import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
|
|
28
19
|
import { buildWebSiteJsonLd } from "../../lib/structured-data.js";
|
|
29
20
|
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
30
|
-
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
31
21
|
|
|
32
22
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
33
23
|
|
|
@@ -39,19 +29,10 @@ homeRoutes.get("/", async (c) => {
|
|
|
39
29
|
const paginatedPageTitle = formatPageLabel(page);
|
|
40
30
|
const isAuthenticated = c.var.isAuthenticated;
|
|
41
31
|
|
|
42
|
-
|
|
43
|
-
// to assemble, but `getNavigationData` also consumes them. Passing them
|
|
44
|
-
// through avoids a duplicate DB query and unlocks the Promise.all below.
|
|
45
|
-
const navItems = await c.var.services.navItems.list();
|
|
46
|
-
const homeDefaultView = getHomeDefaultViewFromNavItems(navItems);
|
|
47
|
-
|
|
48
|
-
const timelinePromise =
|
|
49
|
-
homeDefaultView === "featured"
|
|
50
|
-
? assembleFeaturedTimeline(c, { page, isAuthenticated })
|
|
51
|
-
: assembleTimeline(c, { page, isAuthenticated });
|
|
32
|
+
const timelinePromise = assembleTimeline(c, { page, isAuthenticated });
|
|
52
33
|
|
|
53
34
|
const [navData, timeline] = await Promise.all([
|
|
54
|
-
getNavigationData(c
|
|
35
|
+
getNavigationData(c),
|
|
55
36
|
timelinePromise,
|
|
56
37
|
]);
|
|
57
38
|
|
|
@@ -74,34 +55,6 @@ homeRoutes.get("/", async (c) => {
|
|
|
74
55
|
})
|
|
75
56
|
: undefined;
|
|
76
57
|
|
|
77
|
-
if (homeDefaultView === "featured") {
|
|
78
|
-
const featuredTitle = i18n._(
|
|
79
|
-
msg({
|
|
80
|
-
message: "Featured",
|
|
81
|
-
comment: "@context: Browser page title for the featured feed",
|
|
82
|
-
}),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
return renderPublicPage(c, {
|
|
86
|
-
title:
|
|
87
|
-
page > 1
|
|
88
|
-
? buildPageTitle(featuredTitle, paginatedPageTitle, navData.siteName)
|
|
89
|
-
: navData.siteName,
|
|
90
|
-
jsonLd: websiteJsonLd,
|
|
91
|
-
navData,
|
|
92
|
-
showHomeBranding:
|
|
93
|
-
c.var.appConfig.showJantBrandingOnHome && currentPage === 1,
|
|
94
|
-
content: (
|
|
95
|
-
<FeaturedPage
|
|
96
|
-
items={items}
|
|
97
|
-
currentPage={currentPage}
|
|
98
|
-
totalPages={totalPages}
|
|
99
|
-
baseUrl={toPublicPath("/", navData.sitePathPrefix)}
|
|
100
|
-
/>
|
|
101
|
-
),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
58
|
const latestTitle = i18n._(
|
|
106
59
|
msg({
|
|
107
60
|
message: "Latest",
|
|
@@ -1,71 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Latest Page Route
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* When Latest is the homepage default, this redirects to / to avoid
|
|
6
|
-
* duplicate content. When Featured comes first, this serves as the
|
|
7
|
-
* explicit latest view.
|
|
4
|
+
* The homepage is the canonical latest timeline, so /latest redirects to /.
|
|
8
5
|
*/
|
|
9
6
|
|
|
10
7
|
import { Hono } from "hono";
|
|
11
|
-
import { msg } from "@lingui/core/macro";
|
|
12
8
|
import type { Bindings } from "../../types.js";
|
|
13
9
|
import type { AppVariables } from "../../types/app-context.js";
|
|
14
|
-
import { getI18n } from "../../i18n/index.js";
|
|
15
|
-
import { getNavigationData } from "../../lib/navigation.js";
|
|
16
|
-
import { formatPageLabel, parsePageNumber } from "../../lib/pagination.js";
|
|
17
|
-
import { buildPageTitle } from "../../lib/page-title.js";
|
|
18
|
-
import { renderPublicPage } from "../../lib/render.js";
|
|
19
|
-
import { assembleTimeline } from "../../lib/timeline.js";
|
|
20
10
|
import { toPublicPath } from "../../lib/url.js";
|
|
21
11
|
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
22
12
|
import { buildFeedData, parseFormatQuery, renderFeed } from "../feed/feed.js";
|
|
23
|
-
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
24
13
|
|
|
25
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
26
15
|
|
|
27
16
|
export const latestRoutes = new Hono<Env>();
|
|
28
17
|
|
|
29
18
|
latestRoutes.get("/", async (c) => {
|
|
30
|
-
|
|
31
|
-
const i18n = getI18n(c);
|
|
32
|
-
|
|
33
|
-
// When homepage already shows latest, redirect to avoid duplicate content
|
|
34
|
-
if (navData.homeDefaultView !== "featured") {
|
|
35
|
-
return c.redirect(toPublicPath("/", navData.sitePathPrefix), 302);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const page = parsePageNumber(c.req.query("page"));
|
|
39
|
-
const latestTitle = i18n._(
|
|
40
|
-
msg({
|
|
41
|
-
message: "Latest",
|
|
42
|
-
comment: "@context: Browser page title for the latest feed",
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
const paginatedPageTitle = formatPageLabel(page);
|
|
46
|
-
|
|
47
|
-
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
48
|
-
page,
|
|
49
|
-
isAuthenticated: navData.isAuthenticated,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return renderPublicPage(c, {
|
|
53
|
-
title:
|
|
54
|
-
page > 1
|
|
55
|
-
? buildPageTitle(latestTitle, paginatedPageTitle, navData.siteName)
|
|
56
|
-
: buildPageTitle(latestTitle, navData.siteName),
|
|
57
|
-
navData,
|
|
58
|
-
content: (
|
|
59
|
-
<HomePage
|
|
60
|
-
items={items}
|
|
61
|
-
baseUrl={toPublicPath("/latest", navData.sitePathPrefix)}
|
|
62
|
-
currentPage={currentPage}
|
|
63
|
-
totalPages={totalPages}
|
|
64
|
-
isAuthenticated={navData.isAuthenticated}
|
|
65
|
-
signinUrl={`${toPublicPath("/signin", navData.sitePathPrefix)}?redirect=${encodeURIComponent(toPublicPath("/latest", navData.sitePathPrefix))}`}
|
|
66
|
-
/>
|
|
67
|
-
),
|
|
68
|
-
});
|
|
19
|
+
return c.redirect(toPublicPath("/", c.var.appConfig.sitePathPrefix), 302);
|
|
69
20
|
});
|
|
70
21
|
|
|
71
22
|
// Atom — /latest/feed (canonical latest feed; accepts ?format=note|link|quote)
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
const SECOND_SITE_ID = "sit_second00000000000000000000000";
|
|
15
15
|
const SECOND_SITE_KEY = "second";
|
|
16
16
|
|
|
17
|
-
function insertSecondSite(
|
|
17
|
+
function insertSecondSite(
|
|
18
|
+
sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
|
|
19
|
+
) {
|
|
18
20
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
19
21
|
sqlite
|
|
20
22
|
.prepare(
|
|
@@ -48,9 +50,9 @@ describe("GitHubAppInstallationsService", () => {
|
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
it("returns empty list for an unknown site", async () => {
|
|
51
|
-
expect(
|
|
52
|
-
|
|
53
|
-
);
|
|
53
|
+
expect(
|
|
54
|
+
await service.listInstallationsForSite(DEFAULT_TEST_SITE_ID),
|
|
55
|
+
).toEqual([]);
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
it("returns empty list of sites for an unknown installation", async () => {
|
|
@@ -78,9 +80,8 @@ describe("GitHubAppInstallationsService", () => {
|
|
|
78
80
|
DEFAULT_TEST_SITE_ID,
|
|
79
81
|
makeAccount({ login: "old-name" }),
|
|
80
82
|
);
|
|
81
|
-
const initial =
|
|
82
|
-
DEFAULT_TEST_SITE_ID
|
|
83
|
-
);
|
|
83
|
+
const initial =
|
|
84
|
+
await service.listInstallationsForSite(DEFAULT_TEST_SITE_ID);
|
|
84
85
|
const addedAt = initial[0]!.addedAt;
|
|
85
86
|
|
|
86
87
|
await service.upsertInstallation(
|
|
@@ -88,9 +89,8 @@ describe("GitHubAppInstallationsService", () => {
|
|
|
88
89
|
DEFAULT_TEST_SITE_ID,
|
|
89
90
|
makeAccount({ login: "renamed-org", avatarUrl: "https://x.png" }),
|
|
90
91
|
);
|
|
91
|
-
const updated =
|
|
92
|
-
DEFAULT_TEST_SITE_ID
|
|
93
|
-
);
|
|
92
|
+
const updated =
|
|
93
|
+
await service.listInstallationsForSite(DEFAULT_TEST_SITE_ID);
|
|
94
94
|
expect(updated).toHaveLength(1);
|
|
95
95
|
expect(updated[0]!.addedAt).toBe(addedAt);
|
|
96
96
|
expect(updated[0]!.account.login).toBe("renamed-org");
|
|
@@ -446,6 +446,47 @@ describe("NavItemService", () => {
|
|
|
446
446
|
expect(suggestions).toEqual([]);
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
+
it("hides suggestions already represented by a same-site absolute URL", async () => {
|
|
450
|
+
insertTestPost(sqlite, {
|
|
451
|
+
id: TEST_POST_ID,
|
|
452
|
+
slug: "about",
|
|
453
|
+
title: "About",
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await navItemService.create({
|
|
457
|
+
type: "link",
|
|
458
|
+
label: "About",
|
|
459
|
+
url: "https://preview-test.owenyoung.com/about",
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const suggestions = await navItemService.listSuggestedLinks({
|
|
463
|
+
siteOrigin: "https://preview-test.owenyoung.com",
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(suggestions).toEqual([]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("hides suggestions already represented by a public path prefix", async () => {
|
|
470
|
+
insertTestPost(sqlite, {
|
|
471
|
+
id: TEST_POST_ID,
|
|
472
|
+
slug: "about",
|
|
473
|
+
title: "About",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
await navItemService.create({
|
|
477
|
+
type: "link",
|
|
478
|
+
label: "About",
|
|
479
|
+
url: "https://example.com/blog/about",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const suggestions = await navItemService.listSuggestedLinks({
|
|
483
|
+
siteOrigin: "https://example.com",
|
|
484
|
+
sitePathPrefix: "/blog",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
expect(suggestions).toEqual([]);
|
|
488
|
+
});
|
|
489
|
+
|
|
449
490
|
it("does not suggest draft or private posts", async () => {
|
|
450
491
|
insertTestPost(sqlite, {
|
|
451
492
|
id: TEST_POST_ID,
|
|
@@ -591,6 +591,62 @@ describe("PostService - Timeline features", () => {
|
|
|
591
591
|
expect(entries[1]?.collectedAt).toBe(100);
|
|
592
592
|
});
|
|
593
593
|
|
|
594
|
+
it("can ignore collection pins for subscription feed ordering", async () => {
|
|
595
|
+
const collection = await collectionService.create({
|
|
596
|
+
slug: "rss",
|
|
597
|
+
title: "RSS",
|
|
598
|
+
});
|
|
599
|
+
const olderRoot = await postService.create({
|
|
600
|
+
format: "note",
|
|
601
|
+
bodyMarkdown: "Older pinned root",
|
|
602
|
+
publishedAt: 1000,
|
|
603
|
+
});
|
|
604
|
+
const newerRoot = await postService.create({
|
|
605
|
+
format: "note",
|
|
606
|
+
bodyMarkdown: "Newer unpinned root",
|
|
607
|
+
publishedAt: 2000,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
await db.insert(postCollections).values([
|
|
611
|
+
{
|
|
612
|
+
siteId: DEFAULT_TEST_SITE_ID,
|
|
613
|
+
postId: olderRoot.id,
|
|
614
|
+
collectionId: collection.id,
|
|
615
|
+
createdAt: 100,
|
|
616
|
+
pinnedAt: 5000,
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
siteId: DEFAULT_TEST_SITE_ID,
|
|
620
|
+
postId: newerRoot.id,
|
|
621
|
+
collectionId: collection.id,
|
|
622
|
+
createdAt: 200,
|
|
623
|
+
},
|
|
624
|
+
]);
|
|
625
|
+
|
|
626
|
+
const pageEntries = await postService.listCollectionFeedEntries(
|
|
627
|
+
collection.id,
|
|
628
|
+
{
|
|
629
|
+
status: "published",
|
|
630
|
+
},
|
|
631
|
+
);
|
|
632
|
+
const feedEntries = await postService.listCollectionFeedEntries(
|
|
633
|
+
collection.id,
|
|
634
|
+
{
|
|
635
|
+
status: "published",
|
|
636
|
+
ignoreCollectionPinnedSort: true,
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
expect(pageEntries.map((entry) => entry.post.id)).toEqual([
|
|
641
|
+
olderRoot.id,
|
|
642
|
+
newerRoot.id,
|
|
643
|
+
]);
|
|
644
|
+
expect(feedEntries.map((entry) => entry.post.id)).toEqual([
|
|
645
|
+
newerRoot.id,
|
|
646
|
+
olderRoot.id,
|
|
647
|
+
]);
|
|
648
|
+
});
|
|
649
|
+
|
|
594
650
|
it("dedupes shared threads across multiple collections", async () => {
|
|
595
651
|
const smart = await collectionService.create({
|
|
596
652
|
slug: "smart",
|
|
@@ -786,6 +786,39 @@ describe("PostService", () => {
|
|
|
786
786
|
]);
|
|
787
787
|
});
|
|
788
788
|
|
|
789
|
+
it("can ignore pinned sort for subscription feed queries", async () => {
|
|
790
|
+
await postService.create({
|
|
791
|
+
format: "note",
|
|
792
|
+
bodyMarkdown: "older pinned",
|
|
793
|
+
pinned: true,
|
|
794
|
+
status: "published",
|
|
795
|
+
publishedAt: 1000,
|
|
796
|
+
});
|
|
797
|
+
await postService.create({
|
|
798
|
+
format: "note",
|
|
799
|
+
bodyMarkdown: "newer unpinned",
|
|
800
|
+
status: "published",
|
|
801
|
+
publishedAt: 2000,
|
|
802
|
+
});
|
|
803
|
+
await postService.create({
|
|
804
|
+
format: "note",
|
|
805
|
+
bodyMarkdown: "newest unpinned",
|
|
806
|
+
status: "published",
|
|
807
|
+
publishedAt: 3000,
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const results = await postService.list({
|
|
811
|
+
status: "published",
|
|
812
|
+
ignorePinnedSort: true,
|
|
813
|
+
limit: 2,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
expect(results.map((p) => p.bodyText)).toEqual([
|
|
817
|
+
"newest unpinned",
|
|
818
|
+
"newer unpinned",
|
|
819
|
+
]);
|
|
820
|
+
});
|
|
821
|
+
|
|
789
822
|
it("excludes deleted posts", async () => {
|
|
790
823
|
const post = await postService.create({
|
|
791
824
|
format: "note",
|
|
@@ -164,7 +164,6 @@ describe("SettingsService", () => {
|
|
|
164
164
|
siteLanguage: "en",
|
|
165
165
|
cjkSerifFont: "off",
|
|
166
166
|
showJantBrandingOnHome: false,
|
|
167
|
-
homeDefaultView: "latest",
|
|
168
167
|
mainRssFeed: "featured",
|
|
169
168
|
timeZone: "UTC",
|
|
170
169
|
};
|
|
@@ -198,25 +197,6 @@ describe("SettingsService", () => {
|
|
|
198
197
|
expect(await settingsService.get("SITE_NAME")).toBe("Trimmed");
|
|
199
198
|
});
|
|
200
199
|
|
|
201
|
-
it("removes HOME_DEFAULT_VIEW when set to default", async () => {
|
|
202
|
-
await settingsService.set("HOME_DEFAULT_VIEW", "featured");
|
|
203
|
-
await settingsService.updateGeneral(
|
|
204
|
-
{ ...defaults, homeDefaultView: "latest" },
|
|
205
|
-
{ oldLanguage: "en", fallbackSiteName: "Jant" },
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
expect(await settingsService.get("HOME_DEFAULT_VIEW")).toBeNull();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("stores HOME_DEFAULT_VIEW when set to featured", async () => {
|
|
212
|
-
await settingsService.updateGeneral(
|
|
213
|
-
{ ...defaults, homeDefaultView: "featured" },
|
|
214
|
-
{ oldLanguage: "en", fallbackSiteName: "Jant" },
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
expect(await settingsService.get("HOME_DEFAULT_VIEW")).toBe("featured");
|
|
218
|
-
});
|
|
219
|
-
|
|
220
200
|
it("removes MAIN_RSS_FEED when set to default (featured)", async () => {
|
|
221
201
|
await settingsService.set("MAIN_RSS_FEED", "latest");
|
|
222
202
|
await settingsService.updateGeneral(
|
|
@@ -514,6 +494,11 @@ describe("SettingsService", () => {
|
|
|
514
494
|
|
|
515
495
|
await settingsService.updateSearchSettings(true, { demoMode: false });
|
|
516
496
|
expect(await settingsService.get("NOINDEX")).toBeNull();
|
|
497
|
+
|
|
498
|
+
await settingsService.updateHomeBranding(false);
|
|
499
|
+
expect(
|
|
500
|
+
await settingsService.get("SHOW_JANT_BRANDING_ON_HOME"),
|
|
501
|
+
).toBeNull();
|
|
517
502
|
});
|
|
518
503
|
});
|
|
519
504
|
});
|