@jant/core 0.6.2 → 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.
- package/dist/app-BX2XKxq0.js +6 -0
- package/dist/{app-Ct9c4zYF.js → app-CyysIxj_.js} +183 -205
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-BMPMuwvV.css +2 -0
- package/dist/client/_assets/{client-Bp2IPjDe.js → client-CTrEFM5W.js} +1 -1
- package/dist/client/_assets/{client-auth-C4hQWqH1.js → client-auth-LBSZxqNC.js} +21 -21
- package/dist/{export-O2w3AsZX.js → export-CzuQyg5h.js} +28 -15
- package/dist/{github-sync-BUzIYouS.js → github-sync-CerNYCAn.js} +2 -2
- package/dist/{github-sync-D49RADci.js → github-sync-Dbrb1DS5.js} +5 -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 +127 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +0 -42
- package/src/client/components/collection-manager-types.ts +0 -2
- package/src/client/components/jant-collection-directory.ts +0 -23
- package/src/i18n/locales/public/en.po +0 -12
- package/src/i18n/locales/public/zh-Hans.po +0 -12
- package/src/i18n/locales/public/zh-Hant.po +0 -12
- package/src/lib/github-sync-site-config.ts +4 -2
- package/src/services/export-theme/styles/main.css +4 -3
- package/src/services/export.ts +47 -17
- package/src/services/github-sync.ts +8 -2
- package/src/styles/ui.css +23 -46
- package/src/ui/pages/CollectionsPage.tsx +0 -22
- package/src/ui/shared/CollectionsManager.tsx +6 -41
- package/dist/app-CUZaVgsC.js +0 -6
- package/dist/client/_assets/client-YVrRjAid.css +0 -2
|
@@ -800,4 +800,131 @@ describe("createExportService (Hugo)", () => {
|
|
|
800
800
|
const files = filesToMap(await service.generateHugoFiles());
|
|
801
801
|
expect(files.has("static/media/med-x.webp")).toBe(false);
|
|
802
802
|
});
|
|
803
|
+
|
|
804
|
+
it("Sync mode (bundleMedia false) links media by absolute site URL without reading bytes", async () => {
|
|
805
|
+
const root = makePost({ id: "post-root", slug: "with-media" });
|
|
806
|
+
const media = makeMedia({
|
|
807
|
+
id: "med-1",
|
|
808
|
+
filename: "photo.webp",
|
|
809
|
+
storageKey: "media/med-1.webp",
|
|
810
|
+
});
|
|
811
|
+
const storage = {
|
|
812
|
+
// Must NOT be called — Sync links by URL, never reads attachment bytes.
|
|
813
|
+
get: async () => {
|
|
814
|
+
throw new Error("storage.get should not be called in Sync mode");
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
const service = createExportService(
|
|
818
|
+
buildServices({
|
|
819
|
+
posts: [root],
|
|
820
|
+
mediaByPost: new Map([["post-root", [media]]]),
|
|
821
|
+
}),
|
|
822
|
+
makeSiteConfig(),
|
|
823
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
824
|
+
{ storage: storage as any, bundleMedia: false },
|
|
825
|
+
);
|
|
826
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
827
|
+
expect(files.has("static/media/med-1.webp")).toBe(false);
|
|
828
|
+
const { frontMatter } = await parseFrontMatter(
|
|
829
|
+
files.get("content/with-media/_index.md") as string,
|
|
830
|
+
);
|
|
831
|
+
const entry = frontMatter.media![0];
|
|
832
|
+
expect(entry.src).toBe("https://example.com/media/med-1.webp");
|
|
833
|
+
expect(entry.storage_key).toBe("media/med-1.webp");
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it("Sync mode links the video poster by absolute site URL without bundling", async () => {
|
|
837
|
+
const root = makePost({ id: "post-root", slug: "with-video" });
|
|
838
|
+
const videoMedia = makeMedia({
|
|
839
|
+
id: "med-video",
|
|
840
|
+
filename: "clip.mp4",
|
|
841
|
+
mimeType: "video/mp4",
|
|
842
|
+
storageKey: "media/med-video.mp4",
|
|
843
|
+
mediaKind: "video",
|
|
844
|
+
posterKey: "media/posters/med-video.webp",
|
|
845
|
+
});
|
|
846
|
+
const storage = {
|
|
847
|
+
get: async () => {
|
|
848
|
+
throw new Error("storage.get should not be called in Sync mode");
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
const service = createExportService(
|
|
852
|
+
buildServices({
|
|
853
|
+
posts: [root],
|
|
854
|
+
mediaByPost: new Map([["post-root", [videoMedia]]]),
|
|
855
|
+
}),
|
|
856
|
+
makeSiteConfig(),
|
|
857
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
858
|
+
{ storage: storage as any, bundleMedia: false },
|
|
859
|
+
);
|
|
860
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
861
|
+
expect(files.has("static/media/med-video.mp4")).toBe(false);
|
|
862
|
+
expect(files.has("static/media/med-video-poster.webp")).toBe(false);
|
|
863
|
+
const { frontMatter } = await parseFrontMatter(
|
|
864
|
+
files.get("content/with-video/_index.md") as string,
|
|
865
|
+
);
|
|
866
|
+
const entry = frontMatter.media![0];
|
|
867
|
+
expect(entry.src).toBe("https://example.com/media/med-video.mp4");
|
|
868
|
+
expect(entry.poster).toBe(
|
|
869
|
+
"https://example.com/media/posters/med-video.webp",
|
|
870
|
+
);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("Sync mode still prefers a dedicated provider public URL when configured", async () => {
|
|
874
|
+
const root = makePost({ id: "post-root", slug: "with-cdn" });
|
|
875
|
+
const media = makeMedia({
|
|
876
|
+
id: "med-cdn",
|
|
877
|
+
filename: "cdn.webp",
|
|
878
|
+
storageKey: "media/med-cdn.webp",
|
|
879
|
+
});
|
|
880
|
+
const service = createExportService(
|
|
881
|
+
buildServices({
|
|
882
|
+
posts: [root],
|
|
883
|
+
mediaByPost: new Map([["post-root", [media]]]),
|
|
884
|
+
}),
|
|
885
|
+
makeSiteConfig({ r2PublicUrl: "https://cdn.example.com" }),
|
|
886
|
+
{ bundleMedia: false },
|
|
887
|
+
);
|
|
888
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
889
|
+
expect(files.has("static/media/med-cdn.webp")).toBe(false);
|
|
890
|
+
const { frontMatter } = await parseFrontMatter(
|
|
891
|
+
files.get("content/with-cdn/_index.md") as string,
|
|
892
|
+
);
|
|
893
|
+
expect(frontMatter.media![0].src).toBe(
|
|
894
|
+
"https://cdn.example.com/media/med-cdn.webp",
|
|
895
|
+
);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it("Sync mode falls back to bundling bytes when the site URL is unknown", async () => {
|
|
899
|
+
const root = makePost({ id: "post-root", slug: "with-media" });
|
|
900
|
+
const media = makeMedia({
|
|
901
|
+
id: "med-1",
|
|
902
|
+
filename: "photo.webp",
|
|
903
|
+
storageKey: "media/med-1.webp",
|
|
904
|
+
});
|
|
905
|
+
const storage = {
|
|
906
|
+
get: async (key: string) =>
|
|
907
|
+
key === "media/med-1.webp"
|
|
908
|
+
? { body: new Blob([new Uint8Array([1, 2, 3])]).stream() }
|
|
909
|
+
: null,
|
|
910
|
+
};
|
|
911
|
+
const service = createExportService(
|
|
912
|
+
buildServices({
|
|
913
|
+
posts: [root],
|
|
914
|
+
mediaByPost: new Map([["post-root", [media]]]),
|
|
915
|
+
}),
|
|
916
|
+
makeSiteConfig({ siteUrl: "" }),
|
|
917
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
918
|
+
{ storage: storage as any, bundleMedia: false },
|
|
919
|
+
);
|
|
920
|
+
const files = filesToMap(await service.generateHugoFiles());
|
|
921
|
+
// No resolvable URL — bundling is the only way to avoid a broken link.
|
|
922
|
+
expect(files.get("static/media/med-1.webp")).toEqual(
|
|
923
|
+
new Uint8Array([1, 2, 3]),
|
|
924
|
+
);
|
|
925
|
+
const { frontMatter } = await parseFrontMatter(
|
|
926
|
+
files.get("content/with-media/_index.md") as string,
|
|
927
|
+
);
|
|
928
|
+
expect(frontMatter.media![0].src).toBe("/media/med-1.webp");
|
|
929
|
+
});
|
|
803
930
|
});
|
|
@@ -24,8 +24,6 @@ import type { JantCollectionsManager } from "../jant-collection-directory.js";
|
|
|
24
24
|
|
|
25
25
|
const labels: CollectionManagerLabels = {
|
|
26
26
|
collectionsTitle: "Collections",
|
|
27
|
-
collectionSingular: "collection",
|
|
28
|
-
collectionPlural: "collections",
|
|
29
27
|
organize: "Organize",
|
|
30
28
|
done: "Done",
|
|
31
29
|
organizeHint: "Drag to reorder.",
|
|
@@ -186,7 +184,6 @@ async function createElementWithManagerRoot(): Promise<JantCollectionsManager> {
|
|
|
186
184
|
const root = document.createElement("div");
|
|
187
185
|
root.setAttribute("data-collections-manager-root", "");
|
|
188
186
|
root.innerHTML = `
|
|
189
|
-
<p data-collections-count></p>
|
|
190
187
|
<div data-collections-reorder-actions hidden>
|
|
191
188
|
<button type="button" data-collections-action="divider">New divider</button>
|
|
192
189
|
<button type="button" data-collections-action="done">Done</button>
|
|
@@ -208,32 +205,6 @@ async function createElementWithManagerRoot(): Promise<JantCollectionsManager> {
|
|
|
208
205
|
return el;
|
|
209
206
|
}
|
|
210
207
|
|
|
211
|
-
async function createEmptyElementWithManagerRoot(): Promise<JantCollectionsManager> {
|
|
212
|
-
const root = document.createElement("div");
|
|
213
|
-
root.setAttribute("data-collections-manager-root", "");
|
|
214
|
-
root.innerHTML = `
|
|
215
|
-
<p data-collections-count hidden></p>
|
|
216
|
-
<div data-collections-reorder-actions hidden>
|
|
217
|
-
<button type="button" data-collections-action="divider">New divider</button>
|
|
218
|
-
<button type="button" data-collections-action="done">Done</button>
|
|
219
|
-
</div>
|
|
220
|
-
<div data-collections-toolbar></div>
|
|
221
|
-
<p data-collections-hint hidden></p>
|
|
222
|
-
<div data-collections-more-menu hidden></div>
|
|
223
|
-
<button type="button" data-collections-action="toggle-menu"></button>
|
|
224
|
-
`;
|
|
225
|
-
|
|
226
|
-
const el = document.createElement(
|
|
227
|
-
"jant-collections-manager",
|
|
228
|
-
) as JantCollectionsManager;
|
|
229
|
-
el.labels = labels;
|
|
230
|
-
el.items = [];
|
|
231
|
-
root.appendChild(el);
|
|
232
|
-
document.body.appendChild(root);
|
|
233
|
-
await el.updateComplete;
|
|
234
|
-
return el;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
208
|
describe("JantCollectionsManager", () => {
|
|
238
209
|
beforeEach(() => {
|
|
239
210
|
document.body.innerHTML = "";
|
|
@@ -308,19 +279,6 @@ describe("JantCollectionsManager", () => {
|
|
|
308
279
|
expect(toolbar?.hidden).toBe(true);
|
|
309
280
|
});
|
|
310
281
|
|
|
311
|
-
it("keeps the collection count visible when the list is empty", async () => {
|
|
312
|
-
const el = await createEmptyElementWithManagerRoot();
|
|
313
|
-
const root = el.closest<HTMLElement>("[data-collections-manager-root]");
|
|
314
|
-
|
|
315
|
-
expect(root).not.toBeNull();
|
|
316
|
-
if (!root) throw new Error("Expected collections manager root");
|
|
317
|
-
|
|
318
|
-
const count = root.querySelector<HTMLElement>("[data-collections-count]");
|
|
319
|
-
|
|
320
|
-
expect(count?.hidden).toBe(false);
|
|
321
|
-
expect(count?.textContent).toBe("0 collections");
|
|
322
|
-
});
|
|
323
|
-
|
|
324
282
|
it("renders divider labels as aggregate links when followed by a grouped section", async () => {
|
|
325
283
|
const el = await createElementWithItems(groupedItems);
|
|
326
284
|
|
|
@@ -245,21 +245,6 @@ export class JantCollectionsManager extends LitElement {
|
|
|
245
245
|
);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
#collectionCount() {
|
|
249
|
-
return this._items.filter(
|
|
250
|
-
(item) => item.type === "collection" && item.collection,
|
|
251
|
-
).length;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
#collectionCountLabel() {
|
|
255
|
-
const count = this.#collectionCount();
|
|
256
|
-
return `${count} ${
|
|
257
|
-
count === 1
|
|
258
|
-
? this.labels.collectionSingular
|
|
259
|
-
: this.labels.collectionPlural
|
|
260
|
-
}`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
248
|
#countLabel(count: number) {
|
|
264
249
|
return `${count} ${
|
|
265
250
|
count === 1 ? this.labels.entrySingular : this.labels.entryPlural
|
|
@@ -280,14 +265,6 @@ export class JantCollectionsManager extends LitElement {
|
|
|
280
265
|
}
|
|
281
266
|
|
|
282
267
|
#syncHeaderState() {
|
|
283
|
-
const countEl = this.#queryHeaderElement<HTMLElement>(
|
|
284
|
-
"[data-collections-count]",
|
|
285
|
-
);
|
|
286
|
-
if (countEl) {
|
|
287
|
-
countEl.textContent = this.#collectionCountLabel();
|
|
288
|
-
countEl.hidden = false;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
268
|
const doneButton = this.#queryHeaderElement<HTMLButtonElement>(
|
|
292
269
|
'[data-collections-action="done"]',
|
|
293
270
|
);
|
|
@@ -371,14 +371,8 @@ msgstr "Clear filter"
|
|
|
371
371
|
msgid "Close menu"
|
|
372
372
|
msgstr "Close menu"
|
|
373
373
|
|
|
374
|
-
#. @context: Singular collection count label
|
|
375
|
-
#. @context: Singular collection count label
|
|
376
|
-
#. @context: Singular collection count label
|
|
377
374
|
#. @context: Singular collection count label on an aggregate collection page
|
|
378
375
|
#: src/ui/pages/CollectionPage.tsx
|
|
379
|
-
#: src/ui/pages/CollectionsPage.tsx
|
|
380
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
381
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
382
376
|
msgid "collection"
|
|
383
377
|
msgstr "collection"
|
|
384
378
|
|
|
@@ -412,14 +406,8 @@ msgstr "Collection order updated."
|
|
|
412
406
|
msgid "Collection saved."
|
|
413
407
|
msgstr "Collection saved."
|
|
414
408
|
|
|
415
|
-
#. @context: Plural collection count label
|
|
416
|
-
#. @context: Plural collection count label
|
|
417
|
-
#. @context: Plural collection count label
|
|
418
409
|
#. @context: Plural collection count label on an aggregate collection page
|
|
419
410
|
#: src/ui/pages/CollectionPage.tsx
|
|
420
|
-
#: src/ui/pages/CollectionsPage.tsx
|
|
421
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
422
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
423
411
|
msgid "collections"
|
|
424
412
|
msgstr "collections"
|
|
425
413
|
|
|
@@ -371,14 +371,8 @@ msgstr ""
|
|
|
371
371
|
msgid "Close menu"
|
|
372
372
|
msgstr ""
|
|
373
373
|
|
|
374
|
-
#. @context: Singular collection count label
|
|
375
|
-
#. @context: Singular collection count label
|
|
376
|
-
#. @context: Singular collection count label
|
|
377
374
|
#. @context: Singular collection count label on an aggregate collection page
|
|
378
375
|
#: src/ui/pages/CollectionPage.tsx
|
|
379
|
-
#: src/ui/pages/CollectionsPage.tsx
|
|
380
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
381
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
382
376
|
msgid "collection"
|
|
383
377
|
msgstr ""
|
|
384
378
|
|
|
@@ -412,14 +406,8 @@ msgstr ""
|
|
|
412
406
|
msgid "Collection saved."
|
|
413
407
|
msgstr ""
|
|
414
408
|
|
|
415
|
-
#. @context: Plural collection count label
|
|
416
|
-
#. @context: Plural collection count label
|
|
417
|
-
#. @context: Plural collection count label
|
|
418
409
|
#. @context: Plural collection count label on an aggregate collection page
|
|
419
410
|
#: src/ui/pages/CollectionPage.tsx
|
|
420
|
-
#: src/ui/pages/CollectionsPage.tsx
|
|
421
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
422
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
423
411
|
msgid "collections"
|
|
424
412
|
msgstr ""
|
|
425
413
|
|
|
@@ -371,14 +371,8 @@ msgstr ""
|
|
|
371
371
|
msgid "Close menu"
|
|
372
372
|
msgstr ""
|
|
373
373
|
|
|
374
|
-
#. @context: Singular collection count label
|
|
375
|
-
#. @context: Singular collection count label
|
|
376
|
-
#. @context: Singular collection count label
|
|
377
374
|
#. @context: Singular collection count label on an aggregate collection page
|
|
378
375
|
#: src/ui/pages/CollectionPage.tsx
|
|
379
|
-
#: src/ui/pages/CollectionsPage.tsx
|
|
380
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
381
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
382
376
|
msgid "collection"
|
|
383
377
|
msgstr ""
|
|
384
378
|
|
|
@@ -412,14 +406,8 @@ msgstr ""
|
|
|
412
406
|
msgid "Collection saved."
|
|
413
407
|
msgstr ""
|
|
414
408
|
|
|
415
|
-
#. @context: Plural collection count label
|
|
416
|
-
#. @context: Plural collection count label
|
|
417
|
-
#. @context: Plural collection count label
|
|
418
409
|
#. @context: Plural collection count label on an aggregate collection page
|
|
419
410
|
#: src/ui/pages/CollectionPage.tsx
|
|
420
|
-
#: src/ui/pages/CollectionsPage.tsx
|
|
421
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
422
|
-
#: src/ui/shared/CollectionsManager.tsx
|
|
423
411
|
msgid "collections"
|
|
424
412
|
msgstr ""
|
|
425
413
|
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* call site had its own near-identical copy.
|
|
6
6
|
*
|
|
7
7
|
* The shape mirrors `routes/api/export.ts` so `jant site export` and
|
|
8
|
-
* `jant github sync` produce
|
|
9
|
-
*
|
|
8
|
+
* `jant github sync` produce matching Hugo sites. They differ only in
|
|
9
|
+
* media: `site export` bundles attachment bytes into `static/media/` for
|
|
10
|
+
* a self-contained archive, while Sync links attachments by URL and
|
|
11
|
+
* never writes their bytes into the repo. If you add a field to the
|
|
10
12
|
* export route, add it here too.
|
|
11
13
|
*/
|
|
12
14
|
|
|
@@ -379,11 +379,12 @@ time {
|
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
.site-logo-avatar {
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
box-sizing: border-box;
|
|
383
|
+
width: calc(var(--avatar-size) + 4px);
|
|
384
|
+
height: calc(var(--avatar-size) + 4px);
|
|
384
385
|
border-radius: var(--avatar-radius);
|
|
385
386
|
object-fit: cover;
|
|
386
|
-
|
|
387
|
+
border: 1px solid color-mix(in srgb, var(--site-divider) 82%, transparent);
|
|
387
388
|
flex: none;
|
|
388
389
|
}
|
|
389
390
|
|
package/src/services/export.ts
CHANGED
|
@@ -227,7 +227,17 @@ export function createExportService(
|
|
|
227
227
|
media: MediaService;
|
|
228
228
|
},
|
|
229
229
|
siteConfig: SiteConfig,
|
|
230
|
-
deps: {
|
|
230
|
+
deps: {
|
|
231
|
+
storage?: StorageDriver | null;
|
|
232
|
+
/**
|
|
233
|
+
* Whether to bundle media bytes into the exported site under
|
|
234
|
+
* `static/media/`. Defaults to `true` (the `jant site export`
|
|
235
|
+
* archive, which must be self-contained). GitHub Sync passes
|
|
236
|
+
* `false` so media is linked by URL instead — it never reads or
|
|
237
|
+
* base64-encodes attachment bytes.
|
|
238
|
+
*/
|
|
239
|
+
bundleMedia?: boolean;
|
|
240
|
+
} = {},
|
|
231
241
|
): ExportService {
|
|
232
242
|
return {
|
|
233
243
|
async generateHugoFiles() {
|
|
@@ -308,6 +318,7 @@ export function createExportService(
|
|
|
308
318
|
|
|
309
319
|
// 3. Build file list
|
|
310
320
|
const exportFiles: ExportFile[] = [];
|
|
321
|
+
const bundleMedia = deps.bundleMedia ?? true;
|
|
311
322
|
|
|
312
323
|
// Generate thread bundles (root _index.md + per-reply index.md).
|
|
313
324
|
for (const root of roots) {
|
|
@@ -335,6 +346,7 @@ export function createExportService(
|
|
|
335
346
|
rawMediaByPost,
|
|
336
347
|
siteConfig,
|
|
337
348
|
deps.storage ?? null,
|
|
349
|
+
bundleMedia,
|
|
338
350
|
);
|
|
339
351
|
exportFiles.push(...bundleFiles);
|
|
340
352
|
}
|
|
@@ -662,14 +674,13 @@ interface MediaEmission {
|
|
|
662
674
|
entry: JantMedia;
|
|
663
675
|
/**
|
|
664
676
|
* Site-relative path under `static/` where the primary bytes should
|
|
665
|
-
* land, or null when the media
|
|
666
|
-
*
|
|
677
|
+
* land, or null when the media is linked by URL and no bytes need to
|
|
678
|
+
* be emitted.
|
|
667
679
|
*/
|
|
668
680
|
inlinePath: string | null;
|
|
669
681
|
/**
|
|
670
682
|
* Site-relative path under `static/` where the poster bytes should
|
|
671
|
-
* land, or null when there's no poster or the poster is linked
|
|
672
|
-
* a public URL.
|
|
683
|
+
* land, or null when there's no poster or the poster is linked by URL.
|
|
673
684
|
*/
|
|
674
685
|
inlinePosterPath: string | null;
|
|
675
686
|
}
|
|
@@ -682,26 +693,42 @@ interface MediaEmission {
|
|
|
682
693
|
* When the media's provider has a reachable public URL (R2/S3/local
|
|
683
694
|
* proxy configured with a `*_public_url`), `src` points at that absolute
|
|
684
695
|
* URL and no bytes are emitted — the exported site stays small and the
|
|
685
|
-
* media keeps being served from wherever it already lives.
|
|
686
|
-
*
|
|
687
|
-
*
|
|
696
|
+
* media keeps being served from wherever it already lives.
|
|
697
|
+
*
|
|
698
|
+
* Otherwise behavior depends on `bundleMedia`:
|
|
699
|
+
* - `true` (the `jant site export` archive): bytes are written to
|
|
700
|
+
* `static/media/{id}.ext` and `src` is the site-relative path, so the
|
|
701
|
+
* archive is self-contained.
|
|
702
|
+
* - `false` (GitHub Sync): no bytes are emitted. `src` falls back to the
|
|
703
|
+
* site's own URL so it stays an absolute, resolvable link — the worker
|
|
704
|
+
* already serves these objects at `/{storageKey}`. This keeps Sync
|
|
705
|
+
* from reading and base64-encoding every attachment on every push.
|
|
706
|
+
*
|
|
707
|
+
* When `bundleMedia` is false but the site URL is unknown, bundling is
|
|
708
|
+
* used as a last resort to avoid emitting a broken relative link.
|
|
688
709
|
*/
|
|
689
710
|
function buildMediaEmission(
|
|
690
711
|
media: Media,
|
|
691
712
|
siteConfig: SiteConfig,
|
|
713
|
+
bundleMedia: boolean,
|
|
692
714
|
): MediaEmission {
|
|
693
|
-
const
|
|
715
|
+
const dedicatedPublicUrl = getPublicUrlForProvider(
|
|
694
716
|
media.provider,
|
|
695
717
|
siteConfig.r2PublicUrl,
|
|
696
718
|
siteConfig.s3PublicUrl,
|
|
697
719
|
siteConfig.localPublicUrl,
|
|
698
720
|
);
|
|
699
|
-
const
|
|
721
|
+
const siteFallbackUrl =
|
|
722
|
+
!bundleMedia && siteConfig.siteUrl.trim() ? siteConfig.siteUrl : undefined;
|
|
723
|
+
const mediaBaseUrl = dedicatedPublicUrl || siteFallbackUrl;
|
|
724
|
+
const hasRemoteUrl = Boolean(mediaBaseUrl);
|
|
700
725
|
|
|
701
726
|
const ext = extOfFilename(media.filename);
|
|
702
727
|
const localName = `${media.id}${ext}`;
|
|
703
728
|
const localPath = `/media/${localName}`;
|
|
704
|
-
const src =
|
|
729
|
+
const src = hasRemoteUrl
|
|
730
|
+
? getMediaUrl(media.storageKey, mediaBaseUrl)
|
|
731
|
+
: localPath;
|
|
705
732
|
|
|
706
733
|
const entry: JantMedia = {
|
|
707
734
|
id: media.id,
|
|
@@ -730,18 +757,18 @@ function buildMediaEmission(
|
|
|
730
757
|
if (media.posterKey) {
|
|
731
758
|
const posterExt = extOfStorageKey(media.posterKey);
|
|
732
759
|
const posterLocalName = `${media.id}-poster.${posterExt}`;
|
|
733
|
-
entry.poster =
|
|
734
|
-
? getMediaUrl(media.posterKey,
|
|
760
|
+
entry.poster = hasRemoteUrl
|
|
761
|
+
? getMediaUrl(media.posterKey, mediaBaseUrl)
|
|
735
762
|
: `/media/${posterLocalName}`;
|
|
736
763
|
entry.poster_key = media.posterKey;
|
|
737
|
-
if (!
|
|
764
|
+
if (!hasRemoteUrl) {
|
|
738
765
|
inlinePosterPath = `static/media/${posterLocalName}`;
|
|
739
766
|
}
|
|
740
767
|
}
|
|
741
768
|
|
|
742
769
|
return {
|
|
743
770
|
entry,
|
|
744
|
-
inlinePath:
|
|
771
|
+
inlinePath: hasRemoteUrl ? null : `static/media/${localName}`,
|
|
745
772
|
inlinePosterPath,
|
|
746
773
|
};
|
|
747
774
|
}
|
|
@@ -793,6 +820,7 @@ async function buildThreadBundle(
|
|
|
793
820
|
mediaByPost: Map<string, Media[]>,
|
|
794
821
|
siteConfig: SiteConfig,
|
|
795
822
|
storage: StorageDriver | null,
|
|
823
|
+
bundleMedia: boolean,
|
|
796
824
|
): Promise<ExportFile[]> {
|
|
797
825
|
const files: ExportFile[] = [];
|
|
798
826
|
|
|
@@ -807,7 +835,9 @@ async function buildThreadBundle(
|
|
|
807
835
|
|
|
808
836
|
// Root front matter.
|
|
809
837
|
const rootMedia = mediaByPost.get(root.id) ?? [];
|
|
810
|
-
const rootEmissions = rootMedia.map((m) =>
|
|
838
|
+
const rootEmissions = rootMedia.map((m) =>
|
|
839
|
+
buildMediaEmission(m, siteConfig, bundleMedia),
|
|
840
|
+
);
|
|
811
841
|
const rootMediaList = rootEmissions.map((e) => e.entry);
|
|
812
842
|
const rootFrontMatter: HugoFrontMatter = {
|
|
813
843
|
id: root.id,
|
|
@@ -894,7 +924,7 @@ async function buildThreadBundle(
|
|
|
894
924
|
const replySlug = slugMap.get(reply.id) ?? reply.slug;
|
|
895
925
|
const replyMedia = mediaByPost.get(reply.id) ?? [];
|
|
896
926
|
const replyEmissions = replyMedia.map((m) =>
|
|
897
|
-
buildMediaEmission(m, siteConfig),
|
|
927
|
+
buildMediaEmission(m, siteConfig, bundleMedia),
|
|
898
928
|
);
|
|
899
929
|
const replyMediaList = replyEmissions.map((e) => e.entry);
|
|
900
930
|
const replyCollectionEntries = buildExportedCollectionEntriesForPost(
|
|
@@ -451,8 +451,14 @@ export function createGitHubSyncService(
|
|
|
451
451
|
if (!config) throw new Error("GitHub Sync is not configured");
|
|
452
452
|
const { client, owner, repo } = createClient(config);
|
|
453
453
|
|
|
454
|
-
// Generate full Hugo site via the shared export service
|
|
455
|
-
|
|
454
|
+
// Generate full Hugo site via the shared export service.
|
|
455
|
+
// `bundleMedia: false` — Sync links attachments by URL instead of
|
|
456
|
+
// writing their bytes into the repo, so a push never reads or
|
|
457
|
+
// base64-encodes media. Media stays served from the live site.
|
|
458
|
+
const exportService = createExportService(services, siteConfig, {
|
|
459
|
+
storage: deps.storage,
|
|
460
|
+
bundleMedia: false,
|
|
461
|
+
});
|
|
456
462
|
const exportFiles = await exportService.generateHugoFiles();
|
|
457
463
|
|
|
458
464
|
// Resolve HEAD before building the tree — needed as the commit
|
package/src/styles/ui.css
CHANGED
|
@@ -611,12 +611,12 @@
|
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
.site-logo-avatar {
|
|
614
|
-
|
|
615
|
-
|
|
614
|
+
box-sizing: border-box;
|
|
615
|
+
width: calc(var(--avatar-size) + 4px);
|
|
616
|
+
height: calc(var(--avatar-size) + 4px);
|
|
616
617
|
border-radius: var(--avatar-radius);
|
|
617
618
|
object-fit: cover;
|
|
618
|
-
|
|
619
|
-
color-mix(in srgb, var(--site-divider) 82%, transparent);
|
|
619
|
+
border: 1px solid color-mix(in srgb, var(--site-divider) 82%, transparent);
|
|
620
620
|
}
|
|
621
621
|
|
|
622
622
|
.jant-brand-mark {
|
|
@@ -1115,6 +1115,10 @@
|
|
|
1115
1115
|
flex: 1 1 18rem;
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
|
+
.collections-page-heading .page-intro-title-row {
|
|
1119
|
+
align-items: center;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1118
1122
|
.collections-page-actions {
|
|
1119
1123
|
display: flex;
|
|
1120
1124
|
align-items: center;
|
|
@@ -1129,7 +1133,7 @@
|
|
|
1129
1133
|
}
|
|
1130
1134
|
|
|
1131
1135
|
.collections-page-action-group[data-collections-toolbar] {
|
|
1132
|
-
gap: 0.
|
|
1136
|
+
gap: 0.25rem;
|
|
1133
1137
|
}
|
|
1134
1138
|
|
|
1135
1139
|
.collections-page-toolbar-button {
|
|
@@ -1137,13 +1141,13 @@
|
|
|
1137
1141
|
display: inline-flex;
|
|
1138
1142
|
align-items: center;
|
|
1139
1143
|
justify-content: center;
|
|
1140
|
-
width:
|
|
1141
|
-
height:
|
|
1144
|
+
width: 2.25rem;
|
|
1145
|
+
height: 2.25rem;
|
|
1142
1146
|
padding: 0;
|
|
1143
1147
|
border: none;
|
|
1144
1148
|
border-radius: 999px;
|
|
1145
1149
|
background: transparent;
|
|
1146
|
-
color: var(--
|
|
1150
|
+
color: var(--site-text-secondary);
|
|
1147
1151
|
cursor: pointer;
|
|
1148
1152
|
transition:
|
|
1149
1153
|
background-color 0.16s ease,
|
|
@@ -1152,41 +1156,15 @@
|
|
|
1152
1156
|
border-color 0.16s ease;
|
|
1153
1157
|
}
|
|
1154
1158
|
|
|
1155
|
-
.collections-page-toolbar-button:hover
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
.collections-page-toolbar-button:focus-visible {
|
|
1160
|
-
outline: none;
|
|
1161
|
-
background: color-mix(in srgb, var(--accent) 46%, transparent);
|
|
1162
|
-
box-shadow: 0 0 0 3px
|
|
1163
|
-
color-mix(in srgb, var(--site-accent) 12%, transparent);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
.collections-page-more-btn {
|
|
1167
|
-
min-width: 2.1rem;
|
|
1168
|
-
width: 2.1rem;
|
|
1169
|
-
height: 2.1rem;
|
|
1170
|
-
border: 1px solid color-mix(in srgb, var(--border) 76%, transparent);
|
|
1171
|
-
background: color-mix(
|
|
1172
|
-
in srgb,
|
|
1173
|
-
var(--background) 90%,
|
|
1174
|
-
var(--site-page-bg) 10%
|
|
1175
|
-
);
|
|
1176
|
-
color: var(--site-text-secondary);
|
|
1177
|
-
box-shadow: 0 12px 24px -24px rgba(15, 23, 42, 0.26);
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
.collections-page-more-btn:hover,
|
|
1181
|
-
.collections-page-more-btn[aria-expanded="true"] {
|
|
1182
|
-
border-color: color-mix(in srgb, var(--site-accent) 18%, var(--border));
|
|
1183
|
-
background: color-mix(in srgb, var(--accent) 34%, var(--background));
|
|
1159
|
+
.collections-page-toolbar-button:hover,
|
|
1160
|
+
.collections-page-toolbar-button[aria-expanded="true"] {
|
|
1161
|
+
background: var(--accent);
|
|
1184
1162
|
color: var(--foreground);
|
|
1185
1163
|
}
|
|
1186
1164
|
|
|
1187
|
-
.collections-page-
|
|
1165
|
+
.collections-page-toolbar-button:focus-visible {
|
|
1188
1166
|
outline: none;
|
|
1189
|
-
|
|
1167
|
+
background: var(--accent);
|
|
1190
1168
|
box-shadow: 0 0 0 3px
|
|
1191
1169
|
color-mix(in srgb, var(--site-accent) 12%, transparent);
|
|
1192
1170
|
}
|
|
@@ -1377,7 +1355,6 @@
|
|
|
1377
1355
|
line-height: var(--collection-directory-title-line-height);
|
|
1378
1356
|
letter-spacing: -0.02em;
|
|
1379
1357
|
text-wrap: pretty;
|
|
1380
|
-
user-select: all;
|
|
1381
1358
|
}
|
|
1382
1359
|
|
|
1383
1360
|
.collection-directory-title-marker {
|
|
@@ -2210,7 +2187,6 @@
|
|
|
2210
2187
|
|
|
2211
2188
|
.feed-link-title-link {
|
|
2212
2189
|
text-decoration: none;
|
|
2213
|
-
user-select: all;
|
|
2214
2190
|
}
|
|
2215
2191
|
|
|
2216
2192
|
.feed-link-title-link:hover {
|
|
@@ -2228,10 +2204,6 @@
|
|
|
2228
2204
|
text-wrap: pretty;
|
|
2229
2205
|
}
|
|
2230
2206
|
|
|
2231
|
-
.feed-note-title a {
|
|
2232
|
-
user-select: all;
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
2207
|
.feed-continue-link {
|
|
2236
2208
|
display: inline-block;
|
|
2237
2209
|
margin-top: 0.5rem;
|
|
@@ -7002,10 +6974,15 @@
|
|
|
7002
6974
|
opacity: 0.82;
|
|
7003
6975
|
}
|
|
7004
6976
|
|
|
7005
|
-
/* Single image: constrain to container width
|
|
6977
|
+
/* Single image: constrain to container width. min-width keeps the preview
|
|
6978
|
+
wide enough for the remove button; a very long image overflows it and is
|
|
6979
|
+
cropped from the top (object-fit: cover) instead of shrinking to a sliver. */
|
|
7006
6980
|
.compose-attachment:only-child .compose-attachment-img {
|
|
7007
6981
|
max-width: 100%;
|
|
7008
6982
|
max-height: min(200px, 22dvh);
|
|
6983
|
+
min-width: 48px;
|
|
6984
|
+
object-fit: cover;
|
|
6985
|
+
object-position: center top;
|
|
7009
6986
|
}
|
|
7010
6987
|
|
|
7011
6988
|
.compose-attachment:only-child .compose-attachment-preview-fallback {
|