@jant/core 0.6.1 → 0.6.3

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 (62) hide show
  1. package/dist/app-BX2XKxq0.js +6 -0
  2. package/dist/{app-DYQdDMs8.js → app-CyysIxj_.js} +464 -240
  3. package/dist/client/.vite/manifest.json +3 -3
  4. package/dist/client/_assets/client-BMPMuwvV.css +2 -0
  5. package/dist/client/_assets/client-CTrEFM5W.js +275 -0
  6. package/dist/client/_assets/{client-auth-CSNcTJwP.js → client-auth-LBSZxqNC.js} +24 -24
  7. package/dist/{env-C7e2Nlnt.js → env-CoSe-1y4.js} +1 -1
  8. package/dist/{export-Bbn86HmS.js → export-CzuQyg5h.js} +32 -19
  9. package/dist/{github-api-Bh0PH3zr.js → github-api-UD4u_7fa.js} +1 -1
  10. package/dist/{github-app-D0GvNnqp.js → github-app-DeX6Td1O.js} +1 -1
  11. package/dist/{github-sync-dXsiZa_e.js → github-sync-CerNYCAn.js} +3 -3
  12. package/dist/{github-sync-CBQPRZ8H.js → github-sync-Dbrb1DS5.js} +7 -4
  13. package/dist/index.js +5 -5
  14. package/dist/node.js +6 -6
  15. package/dist/{url-umUptr5z.js → url-XF0GbKGO.js} +22 -1
  16. package/package.json +1 -1
  17. package/src/__tests__/export-service.test.ts +127 -0
  18. package/src/client/__tests__/image-processor.test.ts +64 -0
  19. package/src/client/components/__tests__/jant-collection-directory.test.ts +0 -42
  20. package/src/client/components/__tests__/jant-media-lightbox.test.ts +79 -8
  21. package/src/client/components/collection-manager-types.ts +0 -2
  22. package/src/client/components/jant-collection-directory.ts +0 -23
  23. package/src/client/components/jant-compose-editor.ts +2 -2
  24. package/src/client/components/jant-media-lightbox.ts +33 -5
  25. package/src/client/image-processor.ts +89 -30
  26. package/src/client/media-scroll-hint.ts +62 -9
  27. package/src/i18n/coverage.generated.ts +2 -2
  28. package/src/i18n/locales/public/en.po +0 -12
  29. package/src/i18n/locales/public/zh-Hans.po +0 -12
  30. package/src/i18n/locales/public/zh-Hant.po +0 -12
  31. package/src/i18n/locales/settings/zh-Hans.po +24 -24
  32. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  33. package/src/i18n/locales/settings/zh-Hant.po +24 -24
  34. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  35. package/src/lib/__tests__/structured-data.test.ts +87 -0
  36. package/src/lib/github-sync-site-config.ts +4 -2
  37. package/src/lib/post-display.ts +78 -1
  38. package/src/lib/render.tsx +28 -0
  39. package/src/lib/structured-data.ts +113 -0
  40. package/src/lib/url.ts +26 -0
  41. package/src/routes/api/internal/__tests__/sites.test.ts +65 -0
  42. package/src/routes/api/internal/sites.ts +19 -0
  43. package/src/routes/pages/home.tsx +21 -1
  44. package/src/routes/pages/page.tsx +53 -2
  45. package/src/services/export-theme/assets/client-site.css +1 -1
  46. package/src/services/export-theme/assets/client-site.js +30 -29
  47. package/src/services/export-theme/layouts/partials/media-gallery.html +16 -7
  48. package/src/services/export-theme/styles/main.css +4 -3
  49. package/src/services/export.ts +47 -17
  50. package/src/services/github-sync.ts +8 -2
  51. package/src/services/site-admin.ts +53 -1
  52. package/src/styles/site-media.css +70 -24
  53. package/src/styles/ui.css +30 -48
  54. package/src/ui/layouts/BaseLayout.tsx +110 -16
  55. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +146 -0
  56. package/src/ui/pages/CollectionsPage.tsx +0 -22
  57. package/src/ui/shared/CollectionsManager.tsx +6 -41
  58. package/src/ui/shared/MediaGallery.tsx +50 -7
  59. package/src/ui/shared/__tests__/media-gallery.test.ts +31 -0
  60. package/dist/app-CMSW_AYG.js +0 -6
  61. package/dist/client/_assets/client-BRTh1ii1.js +0 -274
  62. package/dist/client/_assets/client-CO4b-RKd.css +0 -2
@@ -12,6 +12,7 @@ function createContext(
12
12
  assetBasePath?: string;
13
13
  sitePathPrefix?: string;
14
14
  siteUrl?: string;
15
+ siteAvatarUrl?: string;
15
16
  themeMode?: "auto" | "light" | "dark";
16
17
  themeId?: string;
17
18
  defaultThemeId?: string;
@@ -22,6 +23,7 @@ function createContext(
22
23
  mainRssFeed,
23
24
  sitePathPrefix: overrides?.sitePathPrefix ?? "",
24
25
  siteUrl: overrides?.siteUrl ?? "https://example.com",
26
+ siteAvatarUrl: overrides?.siteAvatarUrl,
25
27
  siteLanguage: "en",
26
28
  noindex: false,
27
29
  customCSS: "",
@@ -101,6 +103,27 @@ describe("BaseLayout", () => {
101
103
  );
102
104
  });
103
105
 
106
+ it("falls back to the bundled social image when appConfig.siteAvatarUrl is an empty string", async () => {
107
+ // resolve-config initializes siteAvatarUrl to "" (not undefined) when no
108
+ // avatar is configured. Regression test: the fallback chain must skip
109
+ // empty strings so og:image / twitter:image are never blank.
110
+ const { BaseLayout } = await loadBaseLayout();
111
+ const html = renderToString(
112
+ BaseLayout({
113
+ title: "Jant",
114
+ c: createContext("featured", { siteAvatarUrl: "" }),
115
+ children: "Test",
116
+ }),
117
+ );
118
+
119
+ expect(html).toContain(
120
+ 'meta property="og:image" content="https://example.com/_/brand/assets/jant-social-preview.png"',
121
+ );
122
+ expect(html).toContain(
123
+ 'meta name="twitter:image" content="https://example.com/_/brand/assets/jant-social-preview.png"',
124
+ );
125
+ });
126
+
104
127
  it("uses an explicit social image when provided", async () => {
105
128
  const { BaseLayout } = await loadBaseLayout();
106
129
  const html = renderToString(
@@ -119,6 +142,129 @@ describe("BaseLayout", () => {
119
142
  );
120
143
  });
121
144
 
145
+ it("defaults og:type to website without article timestamps", async () => {
146
+ const { BaseLayout } = await loadBaseLayout();
147
+ const html = renderToString(
148
+ BaseLayout({
149
+ title: "Jant",
150
+ children: "Test",
151
+ }),
152
+ );
153
+
154
+ expect(html).toContain('meta property="og:type" content="website"');
155
+ expect(html).not.toContain("article:published_time");
156
+ expect(html).not.toContain("article:modified_time");
157
+ });
158
+
159
+ it("renders article og:type with published and modified timestamps", async () => {
160
+ const { BaseLayout } = await loadBaseLayout();
161
+ const html = renderToString(
162
+ BaseLayout({
163
+ title: "A post",
164
+ ogType: "article",
165
+ articlePublishedTime: "2026-01-02T03:04:05.000Z",
166
+ articleModifiedTime: "2026-03-04T05:06:07.000Z",
167
+ children: "Test",
168
+ }),
169
+ );
170
+
171
+ expect(html).toContain('meta property="og:type" content="article"');
172
+ expect(html).toContain(
173
+ 'meta property="article:published_time" content="2026-01-02T03:04:05.000Z"',
174
+ );
175
+ expect(html).toContain(
176
+ 'meta property="article:modified_time" content="2026-03-04T05:06:07.000Z"',
177
+ );
178
+ });
179
+
180
+ it("keeps the small twitter card and omits dimensions for the default social image", async () => {
181
+ const { BaseLayout } = await loadBaseLayout();
182
+ const html = renderToString(
183
+ BaseLayout({
184
+ title: "Jant",
185
+ children: "Test",
186
+ }),
187
+ );
188
+
189
+ expect(html).toContain('meta name="twitter:card" content="summary"');
190
+ expect(html).not.toContain('content="summary_large_image"');
191
+ expect(html).not.toContain("og:image:width");
192
+ });
193
+
194
+ it("renders dimensions, alt, and a large twitter card for a landscape post image", async () => {
195
+ const { BaseLayout } = await loadBaseLayout();
196
+ const html = renderToString(
197
+ BaseLayout({
198
+ title: "A post",
199
+ socialImageUrl: "https://cdn.example.com/photo.jpg",
200
+ socialImageWidth: 1600,
201
+ socialImageHeight: 900,
202
+ socialImageAlt: "A wide landscape photo",
203
+ children: "Test",
204
+ }),
205
+ );
206
+
207
+ expect(html).toContain('meta property="og:image:width" content="1600"');
208
+ expect(html).toContain('meta property="og:image:height" content="900"');
209
+ expect(html).toContain(
210
+ 'meta property="og:image:alt" content="A wide landscape photo"',
211
+ );
212
+ expect(html).toContain(
213
+ 'meta name="twitter:image:alt" content="A wide landscape photo"',
214
+ );
215
+ expect(html).toContain(
216
+ 'meta name="twitter:card" content="summary_large_image"',
217
+ );
218
+ });
219
+
220
+ it("keeps the small twitter card for a portrait post image", async () => {
221
+ const { BaseLayout } = await loadBaseLayout();
222
+ const html = renderToString(
223
+ BaseLayout({
224
+ title: "A post",
225
+ socialImageUrl: "https://cdn.example.com/tall.jpg",
226
+ socialImageWidth: 600,
227
+ socialImageHeight: 900,
228
+ children: "Test",
229
+ }),
230
+ );
231
+
232
+ expect(html).toContain('meta name="twitter:card" content="summary"');
233
+ expect(html).not.toContain('content="summary_large_image"');
234
+ });
235
+
236
+ it("renders JSON-LD structured data and escapes script-breaking characters", async () => {
237
+ const { BaseLayout } = await loadBaseLayout();
238
+ const html = renderToString(
239
+ BaseLayout({
240
+ title: "A post",
241
+ jsonLd: {
242
+ "@type": "BlogPosting",
243
+ headline: "Mind the </script> gap",
244
+ },
245
+ children: "Test",
246
+ }),
247
+ );
248
+
249
+ expect(html).toContain('<script type="application/ld+json">');
250
+ expect(html).toContain("\\u003c/script\\u003e");
251
+ expect(html).not.toContain("</script> gap");
252
+ });
253
+
254
+ it("skips JSON-LD when the page is noindex", async () => {
255
+ const { BaseLayout } = await loadBaseLayout();
256
+ const html = renderToString(
257
+ BaseLayout({
258
+ title: "A post",
259
+ noindex: true,
260
+ jsonLd: { "@type": "WebSite" },
261
+ children: "Test",
262
+ }),
263
+ );
264
+
265
+ expect(html).not.toContain("application/ld+json");
266
+ });
267
+
122
268
  it("exposes the main and alternate feed links without duplicating featured", async () => {
123
269
  const { BaseLayout } = await loadBaseLayout();
124
270
  const html = renderToString(
@@ -11,37 +11,18 @@ import type { CollectionsPageProps } from "../../types.js";
11
11
  import { CollectionDirectory } from "../shared/CollectionDirectory.js";
12
12
  import { CollectionsManager } from "../shared/CollectionsManager.js";
13
13
 
14
- const countCollections = (items: CollectionsPageProps["items"]) =>
15
- items.filter((item) => item.type === "collection" && item.collection).length;
16
-
17
14
  export const CollectionsPage: FC<CollectionsPageProps> = ({
18
15
  items,
19
16
  isAuthenticated,
20
17
  sitePathPrefix = "",
21
18
  }) => {
22
19
  const { i18n } = useLingui();
23
- const collectionCount = countCollections(items);
24
20
  const emptyMessage = i18n._(
25
21
  msg({
26
22
  message: "No collections yet. Start one to organize posts by topic.",
27
23
  comment: "@context: Empty state message on collections page",
28
24
  }),
29
25
  );
30
- const collectionCountLabel = `${collectionCount} ${
31
- collectionCount === 1
32
- ? i18n._(
33
- msg({
34
- message: "collection",
35
- comment: "@context: Singular collection count label",
36
- }),
37
- )
38
- : i18n._(
39
- msg({
40
- message: "collections",
41
- comment: "@context: Plural collection count label",
42
- }),
43
- )
44
- }`;
45
26
 
46
27
  if (isAuthenticated) {
47
28
  return (
@@ -66,9 +47,6 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({
66
47
  )}
67
48
  </h1>
68
49
  </div>
69
- <div class="page-intro-meta-row">
70
- <p class="page-intro-meta">{collectionCountLabel}</p>
71
- </div>
72
50
  </div>
73
51
  </header>
74
52
 
@@ -13,9 +13,6 @@ import { getCollectionMutationLabels } from "./collection-management-labels.js";
13
13
  const escapeJson = (data: unknown) =>
14
14
  JSON.stringify(data).replace(/</g, "\\u003c");
15
15
 
16
- const countCollections = (items: CollectionDirectoryItem[]) =>
17
- items.filter((item) => item.type === "collection" && item.collection).length;
18
-
19
16
  export interface CollectionsManagerProps {
20
17
  items: CollectionDirectoryItem[];
21
18
  sitePathPrefix?: string;
@@ -34,22 +31,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
34
31
  `${getNewCollectionPath()}?returnTo=${encodeURIComponent(collectionsHref)}`,
35
32
  sitePathPrefix,
36
33
  );
37
- const collectionCount = countCollections(items);
38
- const collectionCountLabel = `${collectionCount} ${
39
- collectionCount === 1
40
- ? i18n._(
41
- msg({
42
- message: "collection",
43
- comment: "@context: Singular collection count label",
44
- }),
45
- )
46
- : i18n._(
47
- msg({
48
- message: "collections",
49
- comment: "@context: Plural collection count label",
50
- }),
51
- )
52
- }`;
53
34
  const mutationLabels = getCollectionMutationLabels(i18n);
54
35
 
55
36
  const labels = {
@@ -59,18 +40,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
59
40
  comment: "@context: Collections page heading",
60
41
  }),
61
42
  ),
62
- collectionSingular: i18n._(
63
- msg({
64
- message: "collection",
65
- comment: "@context: Singular collection count label",
66
- }),
67
- ),
68
- collectionPlural: i18n._(
69
- msg({
70
- message: "collections",
71
- comment: "@context: Plural collection count label",
72
- }),
73
- ),
74
43
  organize: i18n._(
75
44
  msg({
76
45
  message: "Organize",
@@ -164,11 +133,6 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
164
133
  <div class="collections-page-heading page-intro">
165
134
  <div class="page-intro-title-row">
166
135
  <h1 class="page-intro-title">{labels.collectionsTitle}</h1>
167
- </div>
168
- <div class="page-intro-meta-row">
169
- <p class="page-intro-meta" data-collections-count>
170
- {collectionCountLabel}
171
- </p>
172
136
  <div class="collections-page-actions">
173
137
  <div
174
138
  class="collections-page-action-group"
@@ -202,14 +166,15 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
202
166
  >
203
167
  <svg
204
168
  xmlns="http://www.w3.org/2000/svg"
205
- width="16"
206
- height="16"
169
+ width="18"
170
+ height="18"
207
171
  viewBox="0 0 24 24"
208
172
  fill="none"
209
173
  stroke="currentColor"
210
174
  stroke-width="2"
211
175
  stroke-linecap="round"
212
176
  stroke-linejoin="round"
177
+ aria-hidden="true"
213
178
  >
214
179
  <path d="M12 5v14" />
215
180
  <path d="M5 12h14" />
@@ -218,7 +183,7 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
218
183
  <div class="relative">
219
184
  <button
220
185
  type="button"
221
- class="collections-page-toolbar-button collections-page-more-btn"
186
+ class="collections-page-toolbar-button"
222
187
  aria-label={labels.moreActions}
223
188
  aria-expanded="false"
224
189
  aria-haspopup="menu"
@@ -227,8 +192,8 @@ export const CollectionsManager: FC<CollectionsManagerProps> = ({
227
192
  >
228
193
  <svg
229
194
  xmlns="http://www.w3.org/2000/svg"
230
- width="16"
231
- height="16"
195
+ width="18"
196
+ height="18"
232
197
  viewBox="0 0 24 24"
233
198
  fill="currentColor"
234
199
  >
@@ -289,12 +289,15 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
289
289
  ? JSON.stringify(lightboxItems)
290
290
  : undefined
291
291
  }
292
- class={`flex gap-2 ${singleVisual ? "" : "overflow-x-auto scroll-smooth snap-x snap-mandatory"}`}
292
+ class={`flex gap-2 ${singleVisual ? "" : "overflow-x-auto scroll-smooth"}`}
293
293
  style={
294
294
  singleVisual
295
295
  ? undefined
296
296
  : "scrollbar-width: none; -ms-overflow-style: none;"
297
297
  }
298
+ tabindex={singleVisual ? undefined : 0}
299
+ role={singleVisual ? undefined : "group"}
300
+ aria-label={singleVisual ? undefined : "Media gallery"}
298
301
  >
299
302
  {galleryItems.map((item) => {
300
303
  if (item._kind === "image") {
@@ -329,7 +332,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
329
332
  key={item.id}
330
333
  href={item.url}
331
334
  data-lightbox-index={item._lbIdx}
332
- class={`${singleVisual ? "" : "shrink-0 snap-start"} media-visual-frame`}
335
+ class={`${singleVisual ? "" : "shrink-0"} media-visual-frame`}
333
336
  style={{
334
337
  ...(singleVisual
335
338
  ? {
@@ -391,7 +394,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
391
394
  return (
392
395
  <div
393
396
  key={item.id}
394
- class={`${singleVisual ? "" : "shrink-0 snap-start"} media-video-wrap media-video-wrap-short`}
397
+ class={`${singleVisual ? "" : "shrink-0"} media-video-wrap media-video-wrap-short`}
395
398
  style={
396
399
  singleVisual
397
400
  ? {
@@ -464,7 +467,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
464
467
  key={item.id}
465
468
  href={item.url}
466
469
  data-lightbox-index={item._lbIdx}
467
- class={`${singleVisual ? "" : "shrink-0 snap-start"} media-video-wrap media-visual-frame`}
470
+ class={`${singleVisual ? "" : "shrink-0"} media-video-wrap media-visual-frame`}
468
471
  style={
469
472
  singleVisual
470
473
  ? {
@@ -502,7 +505,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
502
505
  return (
503
506
  <div
504
507
  key={item.id}
505
- class={`media-gallery-card media-audio-card shrink-0 snap-start${item.waveform ? " has-waveform" : ""}`}
508
+ class={`media-gallery-card media-audio-card shrink-0${item.waveform ? " has-waveform" : ""}`}
506
509
  style={{
507
510
  width: `${docCardWidth}px`,
508
511
  height: `${rowHeight}px`,
@@ -593,7 +596,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
593
596
  href={item.url}
594
597
  target="_blank"
595
598
  rel="noopener noreferrer"
596
- class="media-gallery-card shrink-0 snap-start"
599
+ class="media-gallery-card shrink-0"
597
600
  style={{
598
601
  width: `${docCardWidth}px`,
599
602
  height: `${rowHeight}px`,
@@ -627,7 +630,7 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
627
630
  ? `${postPermalink}/text/${item.id}`
628
631
  : undefined
629
632
  }
630
- class="media-gallery-card shrink-0 snap-start"
633
+ class="media-gallery-card shrink-0"
631
634
  style={{
632
635
  width: `${docCardWidth}px`,
633
636
  height: `${rowHeight}px`,
@@ -650,6 +653,46 @@ export const MediaGallery: FC<MediaGalleryProps> = ({
650
653
  );
651
654
  })}
652
655
  </div>
656
+ {!singleVisual && (
657
+ <>
658
+ <button
659
+ type="button"
660
+ class="media-gallery-nav media-gallery-nav-prev"
661
+ tabindex={-1}
662
+ aria-label="Scroll to previous media"
663
+ >
664
+ <svg
665
+ viewBox="0 0 24 24"
666
+ fill="none"
667
+ stroke="currentColor"
668
+ stroke-width="2.5"
669
+ stroke-linecap="round"
670
+ stroke-linejoin="round"
671
+ aria-hidden="true"
672
+ >
673
+ <path d="M15 18l-6-6 6-6" />
674
+ </svg>
675
+ </button>
676
+ <button
677
+ type="button"
678
+ class="media-gallery-nav media-gallery-nav-next"
679
+ tabindex={-1}
680
+ aria-label="Scroll to next media"
681
+ >
682
+ <svg
683
+ viewBox="0 0 24 24"
684
+ fill="none"
685
+ stroke="currentColor"
686
+ stroke-width="2.5"
687
+ stroke-linecap="round"
688
+ stroke-linejoin="round"
689
+ aria-hidden="true"
690
+ >
691
+ <path d="M9 18l6-6-6-6" />
692
+ </svg>
693
+ </button>
694
+ </>
695
+ )}
653
696
  </div>
654
697
  )}
655
698
  </>
@@ -108,4 +108,35 @@ describe("MediaGallery", () => {
108
108
  expect(html).not.toContain("media-video-play-overlay");
109
109
  expect(html).not.toContain("media-short-video-progress");
110
110
  });
111
+
112
+ it("adds scroll arrows and a focusable strip for multi-item galleries", () => {
113
+ const html = renderToString(
114
+ MediaGallery({
115
+ attachments: [
116
+ createMediaView({ id: "m-1", width: 1600, height: 900 }),
117
+ createMediaView({ id: "m-2", width: 1600, height: 900 }),
118
+ createMediaView({ id: "m-3", width: 1600, height: 900 }),
119
+ ],
120
+ }),
121
+ );
122
+
123
+ expect(html).toContain("media-gallery-nav-prev");
124
+ expect(html).toContain("media-gallery-nav-next");
125
+ expect(html).toContain('aria-label="Scroll to previous media"');
126
+ expect(html).toContain('aria-label="Scroll to next media"');
127
+ // The strip is a keyboard tab stop so Arrow keys can scroll it.
128
+ expect(html).toContain('tabindex="0"');
129
+ expect(html).toContain('role="group"');
130
+ });
131
+
132
+ it("omits scroll arrows and the tab stop for a single visual", () => {
133
+ const html = renderToString(
134
+ MediaGallery({
135
+ attachments: [createMediaView({ width: 1600, height: 900 })],
136
+ }),
137
+ );
138
+
139
+ expect(html).not.toContain("media-gallery-nav");
140
+ expect(html).not.toContain('tabindex="0"');
141
+ });
111
142
  });
@@ -1,6 +0,0 @@
1
- import "./url-umUptr5z.js";
2
- import { t as createApp } from "./app-DYQdDMs8.js";
3
- import "./export-Bbn86HmS.js";
4
- import "./env-C7e2Nlnt.js";
5
- import "./github-sync-CBQPRZ8H.js";
6
- export { createApp };