@jant/core 0.6.0 → 0.6.2
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-CUZaVgsC.js +6 -0
- package/dist/{app-BIkkbVQk.js → app-Ct9c4zYF.js} +298 -59
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-Bp2IPjDe.js +275 -0
- package/dist/client/_assets/client-YVrRjAid.css +2 -0
- package/dist/client/_assets/{client-auth-D1jDQgbH.js → client-auth-C4hQWqH1.js} +4 -4
- package/dist/{env-C7e2Nlnt.js → env-CoSe-1y4.js} +1 -1
- package/dist/{export-Bbn86HmS.js → export-O2w3AsZX.js} +4 -4
- package/dist/{github-api-Bh0PH3zr.js → github-api-UD4u_7fa.js} +1 -1
- package/dist/{github-app-D0GvNnqp.js → github-app-DeX6Td1O.js} +1 -1
- package/dist/{github-sync-dXsiZa_e.js → github-sync-BUzIYouS.js} +3 -3
- package/dist/{github-sync-CBQPRZ8H.js → github-sync-D49RADci.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-umUptr5z.js → url-XF0GbKGO.js} +22 -1
- package/package.json +1 -1
- package/src/client/__tests__/image-processor.test.ts +64 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +79 -8
- package/src/client/components/jant-compose-editor.ts +2 -2
- package/src/client/components/jant-media-lightbox.ts +33 -5
- package/src/client/image-processor.ts +89 -30
- package/src/client/media-scroll-hint.ts +62 -9
- package/src/i18n/coverage.generated.ts +2 -2
- package/src/i18n/locales/settings/zh-Hans.po +24 -24
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +24 -24
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/structured-data.test.ts +87 -0
- package/src/lib/post-display.ts +78 -1
- package/src/lib/render.tsx +28 -0
- package/src/lib/structured-data.ts +113 -0
- package/src/lib/url.ts +26 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +65 -0
- package/src/routes/api/internal/sites.ts +19 -0
- package/src/routes/pages/home.tsx +21 -1
- package/src/routes/pages/page.tsx +53 -2
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +30 -29
- package/src/services/export-theme/layouts/partials/media-gallery.html +16 -7
- package/src/services/site-admin.ts +53 -1
- package/src/styles/site-media.css +70 -24
- package/src/styles/ui.css +24 -18
- package/src/ui/feed/ThreadPreview.tsx +0 -1
- package/src/ui/feed/__tests__/thread-preview.test.ts +0 -1
- package/src/ui/layouts/BaseLayout.tsx +110 -16
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +146 -0
- package/src/ui/pages/PostPage.tsx +0 -1
- package/src/ui/shared/MediaGallery.tsx +50 -7
- package/src/ui/shared/__tests__/media-gallery.test.ts +31 -0
- package/dist/app-Bcr5_wZI.js +0 -6
- package/dist/client/_assets/client-Bo7sKkAQ.js +0 -274
- package/dist/client/_assets/client-QHRvzZwk.css +0 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildArticleJsonLd, buildWebSiteJsonLd } from "../structured-data.js";
|
|
3
|
+
|
|
4
|
+
describe("buildArticleJsonLd", () => {
|
|
5
|
+
it("builds a BlogPosting with required fields", () => {
|
|
6
|
+
const data = buildArticleJsonLd({
|
|
7
|
+
headline: "Hello world",
|
|
8
|
+
url: "https://site.com/hello",
|
|
9
|
+
datePublished: "2026-01-01T00:00:00.000Z",
|
|
10
|
+
dateModified: "2026-01-02T00:00:00.000Z",
|
|
11
|
+
authorName: "Jant",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(data).toMatchObject({
|
|
15
|
+
"@context": "https://schema.org",
|
|
16
|
+
"@type": "BlogPosting",
|
|
17
|
+
headline: "Hello world",
|
|
18
|
+
datePublished: "2026-01-01T00:00:00.000Z",
|
|
19
|
+
dateModified: "2026-01-02T00:00:00.000Z",
|
|
20
|
+
url: "https://site.com/hello",
|
|
21
|
+
mainEntityOfPage: { "@type": "WebPage", "@id": "https://site.com/hello" },
|
|
22
|
+
author: { "@type": "Person", name: "Jant" },
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("omits description and image when not provided", () => {
|
|
27
|
+
const data = buildArticleJsonLd({
|
|
28
|
+
headline: "Hello",
|
|
29
|
+
url: "https://site.com/hello",
|
|
30
|
+
datePublished: "2026-01-01T00:00:00.000Z",
|
|
31
|
+
dateModified: "2026-01-01T00:00:00.000Z",
|
|
32
|
+
authorName: "Jant",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(data).not.toHaveProperty("description");
|
|
36
|
+
expect(data).not.toHaveProperty("image");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("includes description and image when provided", () => {
|
|
40
|
+
const data = buildArticleJsonLd({
|
|
41
|
+
headline: "Hello",
|
|
42
|
+
description: "A short note.",
|
|
43
|
+
url: "https://site.com/hello",
|
|
44
|
+
datePublished: "2026-01-01T00:00:00.000Z",
|
|
45
|
+
dateModified: "2026-01-01T00:00:00.000Z",
|
|
46
|
+
imageUrl: "https://site.com/m/cover.png",
|
|
47
|
+
authorName: "Jant",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(data.description).toBe("A short note.");
|
|
51
|
+
expect(data.image).toBe("https://site.com/m/cover.png");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("buildWebSiteJsonLd", () => {
|
|
56
|
+
it("builds a WebSite with a SearchAction when a search template is given", () => {
|
|
57
|
+
const data = buildWebSiteJsonLd({
|
|
58
|
+
name: "Jant",
|
|
59
|
+
url: "https://site.com/",
|
|
60
|
+
searchUrlTemplate: "https://site.com/search?q={search_term_string}",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(data).toMatchObject({
|
|
64
|
+
"@context": "https://schema.org",
|
|
65
|
+
"@type": "WebSite",
|
|
66
|
+
name: "Jant",
|
|
67
|
+
url: "https://site.com/",
|
|
68
|
+
potentialAction: {
|
|
69
|
+
"@type": "SearchAction",
|
|
70
|
+
target: {
|
|
71
|
+
"@type": "EntryPoint",
|
|
72
|
+
urlTemplate: "https://site.com/search?q={search_term_string}",
|
|
73
|
+
},
|
|
74
|
+
"query-input": "required name=search_term_string",
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("omits the SearchAction when no search template is given", () => {
|
|
80
|
+
const data = buildWebSiteJsonLd({
|
|
81
|
+
name: "Jant",
|
|
82
|
+
url: "https://site.com/",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(data).not.toHaveProperty("potentialAction");
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/lib/post-display.ts
CHANGED
|
@@ -13,9 +13,70 @@ import { createMediaContext, toPostView } from "./view.js";
|
|
|
13
13
|
|
|
14
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
15
|
|
|
16
|
+
/** Social/preview image picked for a post page, with metadata when known. */
|
|
17
|
+
export interface PostSocialImage {
|
|
18
|
+
/** Image URL — may be app-local or an already-absolute CDN URL. */
|
|
19
|
+
url: string;
|
|
20
|
+
/** Pixel width, when known (image attachments only, not link previews). */
|
|
21
|
+
width?: number;
|
|
22
|
+
/** Pixel height, when known. */
|
|
23
|
+
height?: number;
|
|
24
|
+
/** Alt text, when the source image attachment has one. */
|
|
25
|
+
alt?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export interface PostPageDisplayData {
|
|
17
29
|
postView: PostView;
|
|
18
30
|
threadPostViews?: PostView[];
|
|
31
|
+
/**
|
|
32
|
+
* Image to use as og:image / twitter:image for this post page. Prefers an
|
|
33
|
+
* image attached to the current post, then its link-preview thumbnail, then
|
|
34
|
+
* any image found elsewhere in the thread. Undefined when the thread has no
|
|
35
|
+
* images — BaseLayout then falls back to the site avatar or the default
|
|
36
|
+
* Jant social image.
|
|
37
|
+
*/
|
|
38
|
+
socialImage?: PostSocialImage;
|
|
39
|
+
/**
|
|
40
|
+
* ISO 8601 publish time for `article:published_time`. A post page renders
|
|
41
|
+
* the whole thread as one "article", so this is the thread root's publish
|
|
42
|
+
* time (the post itself when it is not part of a thread).
|
|
43
|
+
*/
|
|
44
|
+
articlePublishedTime: string;
|
|
45
|
+
/**
|
|
46
|
+
* ISO 8601 last-modified time for `article:modified_time`: the most recent
|
|
47
|
+
* update across every post in the thread.
|
|
48
|
+
*/
|
|
49
|
+
articleModifiedTime: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findFirstImage(post: PostView): PostSocialImage | undefined {
|
|
53
|
+
const image = post.media.find((m) => m.mimeType.startsWith("image/"));
|
|
54
|
+
if (image) {
|
|
55
|
+
return {
|
|
56
|
+
url: image.url,
|
|
57
|
+
width: image.width,
|
|
58
|
+
height: image.height,
|
|
59
|
+
alt: image.altText,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Link-preview thumbnails are transformed images with no known dimensions.
|
|
63
|
+
return post.previewImageUrl ? { url: post.previewImageUrl } : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolvePostSocialImage(
|
|
67
|
+
postView: PostView,
|
|
68
|
+
threadPostViews: PostView[] | undefined,
|
|
69
|
+
): PostSocialImage | undefined {
|
|
70
|
+
const direct = findFirstImage(postView);
|
|
71
|
+
if (direct) return direct;
|
|
72
|
+
|
|
73
|
+
if (!threadPostViews) return undefined;
|
|
74
|
+
for (const p of threadPostViews) {
|
|
75
|
+
if (p.id === postView.id) continue;
|
|
76
|
+
const found = findFirstImage(p);
|
|
77
|
+
if (found) return found;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
19
80
|
}
|
|
20
81
|
|
|
21
82
|
function canViewPost(post: Post, isAuthenticated: boolean): boolean {
|
|
@@ -146,5 +207,21 @@ export async function assemblePostPageDisplay(
|
|
|
146
207
|
)
|
|
147
208
|
: undefined;
|
|
148
209
|
|
|
149
|
-
|
|
210
|
+
const socialImage = resolvePostSocialImage(postView, threadPostViews);
|
|
211
|
+
|
|
212
|
+
const rootView = threadPostViews?.[0] ?? postView;
|
|
213
|
+
const allViews = threadPostViews ?? [postView];
|
|
214
|
+
// ISO 8601 strings from toISOString() are all UTC and zero-padded, so
|
|
215
|
+
// lexical comparison matches chronological order.
|
|
216
|
+
const articleModifiedTime = allViews
|
|
217
|
+
.map((p) => p.updatedAt)
|
|
218
|
+
.reduce((latest, t) => (t > latest ? t : latest));
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
postView,
|
|
222
|
+
threadPostViews,
|
|
223
|
+
socialImage,
|
|
224
|
+
articlePublishedTime: rootView.publishedAt,
|
|
225
|
+
articleModifiedTime,
|
|
226
|
+
};
|
|
150
227
|
}
|
package/src/lib/render.tsx
CHANGED
|
@@ -24,6 +24,20 @@ export interface RenderPublicPageOptions {
|
|
|
24
24
|
appleTouchHref?: string;
|
|
25
25
|
/** Optional explicit social image href */
|
|
26
26
|
socialImageUrl?: string;
|
|
27
|
+
/** Alt text describing an explicit `socialImageUrl`. */
|
|
28
|
+
socialImageAlt?: string;
|
|
29
|
+
/** Pixel width of an explicit `socialImageUrl`, when known. */
|
|
30
|
+
socialImageWidth?: number;
|
|
31
|
+
/** Pixel height of an explicit `socialImageUrl`, when known. */
|
|
32
|
+
socialImageHeight?: number;
|
|
33
|
+
/** Open Graph object type. Defaults to "website" in BaseLayout. */
|
|
34
|
+
ogType?: "website" | "article";
|
|
35
|
+
/** ISO 8601 publish time, rendered as `article:published_time` for articles. */
|
|
36
|
+
articlePublishedTime?: string;
|
|
37
|
+
/** ISO 8601 modified time, rendered as `article:modified_time` for articles. */
|
|
38
|
+
articleModifiedTime?: string;
|
|
39
|
+
/** JSON-LD structured data object (or array) for this page. */
|
|
40
|
+
jsonLd?: unknown;
|
|
27
41
|
/**
|
|
28
42
|
* Absolute canonical URL for this page. Forwarded to `BaseLayout` and
|
|
29
43
|
* rendered as `<link rel="canonical">`. Only set when the page has a
|
|
@@ -73,6 +87,13 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
73
87
|
faviconHref,
|
|
74
88
|
appleTouchHref,
|
|
75
89
|
socialImageUrl,
|
|
90
|
+
socialImageAlt,
|
|
91
|
+
socialImageWidth,
|
|
92
|
+
socialImageHeight,
|
|
93
|
+
ogType,
|
|
94
|
+
articlePublishedTime,
|
|
95
|
+
articleModifiedTime,
|
|
96
|
+
jsonLd,
|
|
76
97
|
canonicalHref,
|
|
77
98
|
navData,
|
|
78
99
|
content,
|
|
@@ -127,6 +148,13 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
127
148
|
faviconHref={faviconHref}
|
|
128
149
|
appleTouchHref={appleTouchHref}
|
|
129
150
|
socialImageUrl={socialImageUrl}
|
|
151
|
+
socialImageAlt={socialImageAlt}
|
|
152
|
+
socialImageWidth={socialImageWidth}
|
|
153
|
+
socialImageHeight={socialImageHeight}
|
|
154
|
+
ogType={ogType}
|
|
155
|
+
articlePublishedTime={articlePublishedTime}
|
|
156
|
+
articleModifiedTime={articleModifiedTime}
|
|
157
|
+
jsonLd={jsonLd}
|
|
130
158
|
canonicalHref={canonicalHref}
|
|
131
159
|
faviconUrl={faviconUrl}
|
|
132
160
|
faviconVersion={faviconVersion}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-LD structured data builders.
|
|
3
|
+
*
|
|
4
|
+
* Produces schema.org objects rendered as `<script type="application/ld+json">`
|
|
5
|
+
* in BaseLayout. Builders return plain objects; serialization and script-safe
|
|
6
|
+
* escaping happen at render time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ArticleJsonLdInput {
|
|
10
|
+
/** Bare post title (without the site-name suffix). */
|
|
11
|
+
headline: string;
|
|
12
|
+
/** Meta description, when available. */
|
|
13
|
+
description?: string;
|
|
14
|
+
/** Canonical absolute URL of the post page. */
|
|
15
|
+
url: string;
|
|
16
|
+
/** ISO 8601 publish time. */
|
|
17
|
+
datePublished: string;
|
|
18
|
+
/** ISO 8601 last-modified time. */
|
|
19
|
+
dateModified: string;
|
|
20
|
+
/** Absolute URL of the post's social/preview image, when available. */
|
|
21
|
+
imageUrl?: string;
|
|
22
|
+
/** Display name of the site author. */
|
|
23
|
+
authorName: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a schema.org `BlogPosting` object for a single post page.
|
|
28
|
+
*
|
|
29
|
+
* `BlogPosting` (a subtype of `Article`) fits a personal microblog better than
|
|
30
|
+
* the generic `Article` type.
|
|
31
|
+
*
|
|
32
|
+
* @param input - Post metadata, with absolute URLs already resolved
|
|
33
|
+
* @returns A JSON-LD-ready `BlogPosting` object
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* buildArticleJsonLd({
|
|
38
|
+
* headline: "Hello",
|
|
39
|
+
* url: "https://site.com/hello",
|
|
40
|
+
* datePublished: "2026-01-01T00:00:00.000Z",
|
|
41
|
+
* dateModified: "2026-01-02T00:00:00.000Z",
|
|
42
|
+
* authorName: "Jant",
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function buildArticleJsonLd(
|
|
47
|
+
input: ArticleJsonLdInput,
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
const data: Record<string, unknown> = {
|
|
50
|
+
"@context": "https://schema.org",
|
|
51
|
+
"@type": "BlogPosting",
|
|
52
|
+
headline: input.headline,
|
|
53
|
+
datePublished: input.datePublished,
|
|
54
|
+
dateModified: input.dateModified,
|
|
55
|
+
url: input.url,
|
|
56
|
+
mainEntityOfPage: { "@type": "WebPage", "@id": input.url },
|
|
57
|
+
author: { "@type": "Person", name: input.authorName },
|
|
58
|
+
};
|
|
59
|
+
if (input.description) data.description = input.description;
|
|
60
|
+
if (input.imageUrl) data.image = input.imageUrl;
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface WebSiteJsonLdInput {
|
|
65
|
+
/** Site display name. */
|
|
66
|
+
name: string;
|
|
67
|
+
/** Absolute URL of the site root. */
|
|
68
|
+
url: string;
|
|
69
|
+
/**
|
|
70
|
+
* Absolute search URL template containing the literal placeholder
|
|
71
|
+
* `{search_term_string}`, e.g. `https://site.com/search?q={search_term_string}`.
|
|
72
|
+
* Omit to skip the sitelinks search box action.
|
|
73
|
+
*/
|
|
74
|
+
searchUrlTemplate?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a schema.org `WebSite` object, optionally with a `SearchAction` that
|
|
79
|
+
* enables Google's sitelinks search box.
|
|
80
|
+
*
|
|
81
|
+
* @param input - Site metadata, with absolute URLs already resolved
|
|
82
|
+
* @returns A JSON-LD-ready `WebSite` object
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* buildWebSiteJsonLd({
|
|
87
|
+
* name: "Jant",
|
|
88
|
+
* url: "https://site.com/",
|
|
89
|
+
* searchUrlTemplate: "https://site.com/search?q={search_term_string}",
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function buildWebSiteJsonLd(
|
|
94
|
+
input: WebSiteJsonLdInput,
|
|
95
|
+
): Record<string, unknown> {
|
|
96
|
+
const data: Record<string, unknown> = {
|
|
97
|
+
"@context": "https://schema.org",
|
|
98
|
+
"@type": "WebSite",
|
|
99
|
+
name: input.name,
|
|
100
|
+
url: input.url,
|
|
101
|
+
};
|
|
102
|
+
if (input.searchUrlTemplate) {
|
|
103
|
+
data.potentialAction = {
|
|
104
|
+
"@type": "SearchAction",
|
|
105
|
+
target: {
|
|
106
|
+
"@type": "EntryPoint",
|
|
107
|
+
urlTemplate: input.searchUrlTemplate,
|
|
108
|
+
},
|
|
109
|
+
"query-input": "required name=search_term_string",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return data;
|
|
113
|
+
}
|
package/src/lib/url.ts
CHANGED
|
@@ -372,3 +372,29 @@ export function toAbsoluteSiteUrl(
|
|
|
372
372
|
if (!siteUrl) return toPublicPath(path, sitePathPrefix);
|
|
373
373
|
return new URL(toPublicPath(path, sitePathPrefix), siteUrl).toString();
|
|
374
374
|
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Resolve a possibly-relative asset URL to an absolute URL, leaving
|
|
378
|
+
* already-absolute (`http(s):`) and protocol-relative (`//host`) URLs
|
|
379
|
+
* untouched. Use for assets — like media — whose stored URL may be either an
|
|
380
|
+
* app-local path or a full CDN URL.
|
|
381
|
+
*
|
|
382
|
+
* @param url - Asset URL: an internal path or an already-absolute URL
|
|
383
|
+
* @param siteUrl - Normalized site URL
|
|
384
|
+
* @param sitePathPrefix - Public site path prefix
|
|
385
|
+
* @returns Absolute URL, or the original value when it is already absolute
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```ts
|
|
389
|
+
* toAbsoluteAssetUrl("/m/a.png", "https://site.com"); // "https://site.com/m/a.png"
|
|
390
|
+
* toAbsoluteAssetUrl("https://cdn.example/a.png", "x"); // "https://cdn.example/a.png"
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
export function toAbsoluteAssetUrl(
|
|
394
|
+
url: string,
|
|
395
|
+
siteUrl: string,
|
|
396
|
+
sitePathPrefix = "",
|
|
397
|
+
): string {
|
|
398
|
+
if (isFullUrl(url) || url.startsWith("//")) return url;
|
|
399
|
+
return toAbsoluteSiteUrl(url, siteUrl, sitePathPrefix);
|
|
400
|
+
}
|
|
@@ -264,6 +264,71 @@ describe("Internal site admin routes", () => {
|
|
|
264
264
|
});
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
it("returns published post counts for hosted sites", async () => {
|
|
268
|
+
const { app, sqlite } = createTestApp({
|
|
269
|
+
authenticated: false,
|
|
270
|
+
internalAdminToken: "internal-secret",
|
|
271
|
+
siteResolutionMode: "host-based",
|
|
272
|
+
});
|
|
273
|
+
app.route("/api/internal/sites", internalSitesRoutes);
|
|
274
|
+
|
|
275
|
+
const insertPost = sqlite.prepare(
|
|
276
|
+
`INSERT INTO "post" ("id", "site_id", "format", "status", "thread_id", "created_at", "updated_at")
|
|
277
|
+
VALUES (?, ?, 'note', ?, ?, 1774200002, 1774200002)`,
|
|
278
|
+
);
|
|
279
|
+
insertPost.run(
|
|
280
|
+
"pst_count_1",
|
|
281
|
+
DEFAULT_TEST_SITE_ID,
|
|
282
|
+
"published",
|
|
283
|
+
"pst_count_1",
|
|
284
|
+
);
|
|
285
|
+
insertPost.run(
|
|
286
|
+
"pst_count_2",
|
|
287
|
+
DEFAULT_TEST_SITE_ID,
|
|
288
|
+
"published",
|
|
289
|
+
"pst_count_2",
|
|
290
|
+
);
|
|
291
|
+
insertPost.run("pst_count_3", DEFAULT_TEST_SITE_ID, "draft", "pst_count_3");
|
|
292
|
+
|
|
293
|
+
const res = await app.request("/api/internal/sites/post-counts", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: {
|
|
296
|
+
Authorization: "Bearer internal-secret",
|
|
297
|
+
"Content-Type": "application/json",
|
|
298
|
+
},
|
|
299
|
+
body: JSON.stringify({
|
|
300
|
+
siteIds: [DEFAULT_TEST_SITE_ID, "sit_does_not_exist"],
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(res.status).toBe(200);
|
|
305
|
+
// Drafts are excluded, and an unknown site id resolves to 0 rather than
|
|
306
|
+
// failing the whole batch.
|
|
307
|
+
await expect(res.json()).resolves.toEqual({
|
|
308
|
+
counts: [
|
|
309
|
+
{ publishedPostCount: 2, siteId: DEFAULT_TEST_SITE_ID },
|
|
310
|
+
{ publishedPostCount: 0, siteId: "sit_does_not_exist" },
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("rejects post-count lookups without an admin token", async () => {
|
|
316
|
+
const { app } = createTestApp({
|
|
317
|
+
authenticated: false,
|
|
318
|
+
internalAdminToken: "internal-secret",
|
|
319
|
+
siteResolutionMode: "host-based",
|
|
320
|
+
});
|
|
321
|
+
app.route("/api/internal/sites", internalSitesRoutes);
|
|
322
|
+
|
|
323
|
+
const res = await app.request("/api/internal/sites/post-counts", {
|
|
324
|
+
method: "POST",
|
|
325
|
+
headers: { "Content-Type": "application/json" },
|
|
326
|
+
body: JSON.stringify({ siteIds: [DEFAULT_TEST_SITE_ID] }),
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(res.status).toBe(401);
|
|
330
|
+
});
|
|
331
|
+
|
|
267
332
|
it("deletes a managed site without clearing other sites", async () => {
|
|
268
333
|
const { app, sqlite } = createTestApp({
|
|
269
334
|
authenticated: false,
|
|
@@ -42,6 +42,10 @@ const CreateManagedSiteSchema = z.object({
|
|
|
42
42
|
idempotencyKey: z.string().trim().min(1).max(128).optional(),
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
const SitePostCountsSchema = z.object({
|
|
46
|
+
siteIds: z.array(z.string().trim().min(1)).max(200),
|
|
47
|
+
});
|
|
48
|
+
|
|
45
49
|
const ManagedSiteDomainSchema = z.object({
|
|
46
50
|
host: z
|
|
47
51
|
.string()
|
|
@@ -99,6 +103,21 @@ internalSitesRoutes.get(
|
|
|
99
103
|
},
|
|
100
104
|
);
|
|
101
105
|
|
|
106
|
+
internalSitesRoutes.post(
|
|
107
|
+
"/post-counts",
|
|
108
|
+
requireInternalAdminApi(),
|
|
109
|
+
async (c) => {
|
|
110
|
+
assertHostBasedMode(c.env);
|
|
111
|
+
|
|
112
|
+
const body = parseValidated(SitePostCountsSchema, await c.req.json());
|
|
113
|
+
const counts = await c.var.services.siteAdmin.getManagedSitePostCounts(
|
|
114
|
+
body.siteIds,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return c.json({ counts });
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
102
121
|
internalSitesRoutes.delete("/:siteId", requireInternalAdminApi(), async (c) => {
|
|
103
122
|
assertHostBasedMode(c.env);
|
|
104
123
|
|
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
assembleFeaturedTimeline,
|
|
25
25
|
assembleTimeline,
|
|
26
26
|
} from "../../lib/timeline.js";
|
|
27
|
-
import { toPublicPath } from "../../lib/url.js";
|
|
27
|
+
import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
|
|
28
|
+
import { buildWebSiteJsonLd } from "../../lib/structured-data.js";
|
|
28
29
|
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
29
30
|
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
30
31
|
|
|
@@ -56,6 +57,23 @@ homeRoutes.get("/", async (c) => {
|
|
|
56
57
|
|
|
57
58
|
const { items, currentPage, totalPages } = timeline;
|
|
58
59
|
|
|
60
|
+
// WebSite + SearchAction structured data, emitted only on the first page
|
|
61
|
+
// (the canonical site entry point) and only when a site URL is configured
|
|
62
|
+
// so the search-box action resolves to an absolute URL.
|
|
63
|
+
const { siteUrl } = c.var.appConfig;
|
|
64
|
+
const websiteJsonLd =
|
|
65
|
+
page === 1 && siteUrl
|
|
66
|
+
? buildWebSiteJsonLd({
|
|
67
|
+
name: navData.siteName,
|
|
68
|
+
url: toAbsoluteSiteUrl("/", siteUrl, navData.sitePathPrefix),
|
|
69
|
+
searchUrlTemplate: `${toAbsoluteSiteUrl(
|
|
70
|
+
"/search",
|
|
71
|
+
siteUrl,
|
|
72
|
+
navData.sitePathPrefix,
|
|
73
|
+
)}?q={search_term_string}`,
|
|
74
|
+
})
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
59
77
|
if (homeDefaultView === "featured") {
|
|
60
78
|
const featuredTitle = i18n._(
|
|
61
79
|
msg({
|
|
@@ -69,6 +87,7 @@ homeRoutes.get("/", async (c) => {
|
|
|
69
87
|
page > 1
|
|
70
88
|
? buildPageTitle(featuredTitle, paginatedPageTitle, navData.siteName)
|
|
71
89
|
: navData.siteName,
|
|
90
|
+
jsonLd: websiteJsonLd,
|
|
72
91
|
navData,
|
|
73
92
|
showHomeBranding:
|
|
74
93
|
c.var.appConfig.showJantBrandingOnHome && currentPage === 1,
|
|
@@ -95,6 +114,7 @@ homeRoutes.get("/", async (c) => {
|
|
|
95
114
|
page > 1
|
|
96
115
|
? buildPageTitle(latestTitle, paginatedPageTitle, navData.siteName)
|
|
97
116
|
: navData.siteName,
|
|
117
|
+
jsonLd: websiteJsonLd,
|
|
98
118
|
navData,
|
|
99
119
|
showHomeBranding:
|
|
100
120
|
c.var.appConfig.showJantBrandingOnHome && currentPage === 1,
|
|
@@ -13,8 +13,16 @@ import { getNavigationData } from "../../lib/navigation.js";
|
|
|
13
13
|
import { renderPublicPage } from "../../lib/render.js";
|
|
14
14
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
15
15
|
import { buildPostMeta } from "../../lib/post-meta.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
assemblePostPageDisplay,
|
|
18
|
+
type PostPageDisplayData,
|
|
19
|
+
} from "../../lib/post-display.js";
|
|
20
|
+
import { buildArticleJsonLd } from "../../lib/structured-data.js";
|
|
21
|
+
import {
|
|
22
|
+
toAbsoluteAssetUrl,
|
|
23
|
+
toPublicHref,
|
|
24
|
+
toPublicPath,
|
|
25
|
+
} from "../../lib/url.js";
|
|
18
26
|
import { isTextAttachment } from "../../services/media.js";
|
|
19
27
|
import type { Post } from "../../types.js";
|
|
20
28
|
import { renderArchivePage } from "./archive.js";
|
|
@@ -56,6 +64,33 @@ function buildPostCanonicalHref(
|
|
|
56
64
|
return new URL(rootPermalink, siteUrl).toString();
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Build the `BlogPosting` JSON-LD for a post page. The post page renders the
|
|
69
|
+
* whole thread, so the structured data describes the thread as one article:
|
|
70
|
+
* canonical URL, root publish time, latest thread modification time.
|
|
71
|
+
*/
|
|
72
|
+
function buildPostJsonLd(
|
|
73
|
+
c: Context<Env>,
|
|
74
|
+
display: PostPageDisplayData,
|
|
75
|
+
meta: { title: string; description?: string },
|
|
76
|
+
canonicalHref: string,
|
|
77
|
+
siteName: string,
|
|
78
|
+
): Record<string, unknown> {
|
|
79
|
+
const { siteUrl, sitePathPrefix } = c.var.appConfig;
|
|
80
|
+
const imageUrl = display.socialImage
|
|
81
|
+
? toAbsoluteAssetUrl(display.socialImage.url, siteUrl, sitePathPrefix)
|
|
82
|
+
: undefined;
|
|
83
|
+
return buildArticleJsonLd({
|
|
84
|
+
headline: meta.title,
|
|
85
|
+
description: meta.description,
|
|
86
|
+
url: canonicalHref,
|
|
87
|
+
datePublished: display.articlePublishedTime,
|
|
88
|
+
dateModified: display.articleModifiedTime,
|
|
89
|
+
imageUrl,
|
|
90
|
+
authorName: siteName,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
59
94
|
async function renderPostWithTextPreview(
|
|
60
95
|
c: Context<Env>,
|
|
61
96
|
post: Post,
|
|
@@ -97,6 +132,14 @@ async function renderPostWithTextPreview(
|
|
|
97
132
|
title: buildPageTitle(pageTitle, navData.siteName),
|
|
98
133
|
description: meta.description,
|
|
99
134
|
canonicalHref,
|
|
135
|
+
socialImageUrl: display.socialImage?.url,
|
|
136
|
+
socialImageAlt: display.socialImage?.alt,
|
|
137
|
+
socialImageWidth: display.socialImage?.width,
|
|
138
|
+
socialImageHeight: display.socialImage?.height,
|
|
139
|
+
ogType: "article",
|
|
140
|
+
articlePublishedTime: display.articlePublishedTime,
|
|
141
|
+
articleModifiedTime: display.articleModifiedTime,
|
|
142
|
+
jsonLd: buildPostJsonLd(c, display, meta, canonicalHref, navData.siteName),
|
|
100
143
|
navData,
|
|
101
144
|
content: (
|
|
102
145
|
<>
|
|
@@ -166,6 +209,14 @@ async function renderPost(c: Context<Env>, post: Post) {
|
|
|
166
209
|
title: buildPageTitle(meta.title, navData.siteName),
|
|
167
210
|
description: meta.description,
|
|
168
211
|
canonicalHref,
|
|
212
|
+
socialImageUrl: display.socialImage?.url,
|
|
213
|
+
socialImageAlt: display.socialImage?.alt,
|
|
214
|
+
socialImageWidth: display.socialImage?.width,
|
|
215
|
+
socialImageHeight: display.socialImage?.height,
|
|
216
|
+
ogType: "article",
|
|
217
|
+
articlePublishedTime: display.articlePublishedTime,
|
|
218
|
+
articleModifiedTime: display.articleModifiedTime,
|
|
219
|
+
jsonLd: buildPostJsonLd(c, display, meta, canonicalHref, navData.siteName),
|
|
169
220
|
navData,
|
|
170
221
|
content: (
|
|
171
222
|
<PostPage post={display.postView} threadPosts={display.threadPostViews} />
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
@keyframes lightbox-fade-in{0%{opacity:0}to{opacity:1}}@keyframes lightbox-scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.media-lightbox{background:0 0;border:none;outline:none;width:100%;max-width:100%;height:100%;max-height:100%;padding:0}.media-lightbox[open]{animation:.2s both lightbox-fade-in}.media-lightbox::backdrop{background-color:#000}.media-lightbox-content{outline:none;width:100%;height:100dvh;position:relative}.media-lightbox-stage{box-sizing:border-box;justify-content:center;align-items:center;width:100%;height:100%;padding:64px 96px;display:flex;overflow:hidden}.media-lightbox-stage-scroll{overscroll-behavior:contain;scrollbar-gutter:stable both-edges;-webkit-overflow-scrolling:touch;align-items:flex-start;overflow:hidden auto}.media-lightbox-img{object-fit:contain;border-radius:4px;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in;display:block}.media-lightbox-img-scroll{width:min(100%,44rem);max-width:none;height:auto;max-height:none;margin:0 auto}.media-lightbox-close{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:40px;height:40px;transition:background-color .15s;display:flex;position:fixed;top:16px;left:16px}.media-lightbox-close:hover{background-color:#000000b3}.media-lightbox-nav{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s;display:flex;position:fixed;top:50%;transform:translateY(-50%)}.media-lightbox-nav:hover{background-color:#000000b3}.media-lightbox-nav-prev{left:16px}.media-lightbox-nav-next{right:16px}.media-lightbox-counter{z-index:10;font-size:var(--type-xs);color:#ffffffb3;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;position:fixed;top:20px;left:50%;transform:translate(-50%)}.media-lightbox-short-frame{max-width:100%;max-height:100%;display:block;position:relative}.media-lightbox-short-controls{z-index:2;pointer-events:none;height:86px;position:absolute;bottom:0;left:0;right:0}.media-lightbox-short-progress{--media-progress:0%;z-index:1;appearance:none;pointer-events:auto;cursor:pointer;background:0 0;width:auto;height:34px;margin:0;padding:0;display:block;position:absolute;bottom:0;left:16px;right:16px}.media-lightbox-short-progress::-webkit-slider-runnable-track{background:linear-gradient(to right, #ffffffeb 0, #ffffffeb var(--media-progress), #ffffff2e var(--media-progress), #ffffff2e 100%);border-radius:999px;height:2px;transition:height .15s,background .15s}.media-lightbox-short-progress::-webkit-slider-thumb{appearance:none;opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;margin-top:-4px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-progress::-moz-range-track{background:#ffffff2e;border:none;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-progress{background:#ffffffeb;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-thumb{opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-controls-portrait .media-lightbox-short-progress{width:calc(100% - 48px);right:auto}.media-lightbox-short-progress:hover::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:focus-visible::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-track{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-progress{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-progress{height:4px}.media-lightbox-short-progress:hover::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:hover::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-mute{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#777;border:none;border-radius:999px;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:30px;right:24px}.media-lightbox-short-mute svg{flex-shrink:0;width:16px;height:16px;display:block}.media-lightbox-short-mute:hover{background-color:#686868;transform:scale(1.03)}.media-lightbox-short-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}@media (max-width:640px){.media-lightbox-stage{padding:48px 16px}.media-lightbox-img{border-radius:0}.media-lightbox-img-scroll{width:100%}.media-lightbox-close{width:36px;height:36px;top:12px;left:12px}.media-lightbox-nav{width:36px;height:36px}.media-lightbox-nav-prev{left:8px}.media-lightbox-nav-next{right:8px}.media-lightbox-short-mute{bottom:26px;right:24px}}.media-visual-frame{border-radius:var(--media-radius,.5rem);background-color:var(--color-muted);display:block;overflow:hidden}.media-visual{background-position:50%;background-repeat:no-repeat;display:block}.media-video-wrap{position:relative}.media-video-link{cursor:pointer;display:block}.media-video-wrap video{object-fit:contain;background-color:var(--color-muted);width:100%;max-height:24rem}.media-video-wrap-short video{background-color:#000}.media-feed-video-mute{z-index:1;color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:999px;justify-content:center;align-items:center;width:28px;height:28px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:16px;right:16px}.media-feed-video-mute svg{width:12px;height:12px}.media-feed-video-mute:hover{background-color:#0000009e;transform:scale(1.03)}.media-feed-video-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-feed-video-icon{transition:opacity .15s,transform .15s;position:absolute}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-muted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-unmuted{opacity:1;transform:scale(1)}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-unmuted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-muted{opacity:0;transform:scale(.92)}.media-video-play-overlay{pointer-events:none;justify-content:center;align-items:center;transition:opacity .15s;display:flex;position:absolute;inset:0}.media-video-play-overlay svg{filter:drop-shadow(0 2px 6px #0006);opacity:.85;width:48px;height:48px}.media-gallery-card.media-audio-card{flex-direction:column;display:flex}.media-audio-card .media-audio-el{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.media-audio-card .media-audio-artwork{background:linear-gradient(160deg,#8080801f 0%,#80808008 100%);flex:1;justify-content:center;align-items:center;width:100%;min-height:0;display:flex}.media-audio-card .media-audio-artwork svg{width:32px;height:32px;color:var(--site-text-secondary);opacity:.3}.media-audio-card .media-audio-waveform{cursor:pointer;touch-action:none;width:100%;height:24px;display:none}.media-audio-card.has-waveform .media-audio-waveform{display:block}.media-audio-card.has-waveform .media-audio-range{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.media-audio-card .media-audio-controls{flex-direction:column;flex-shrink:0;padding:0 0 6px;display:flex}.media-audio-card .media-audio-range{appearance:none;cursor:pointer;touch-action:none;background:0 0;width:100%;height:20px;margin:0;padding:0}.media-audio-card .media-audio-range::-webkit-slider-runnable-track{background:#80808026;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-track{background:#80808026;border:none;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-progress{background:var(--site-text-primary);border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-webkit-slider-thumb{-webkit-appearance:none;background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;margin-top:-3.5px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-webkit-slider-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-webkit-slider-thumb{opacity:1}.media-audio-card .media-audio-range::-moz-range-thumb{background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-moz-range-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-moz-range-thumb{opacity:1}.media-audio-card .media-audio-range:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:2px}.media-audio-card .media-audio-row{align-items:center;gap:6px;min-width:0;padding:6px 8px 0;display:flex}.media-audio-card .media-audio-info{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.media-audio-card .media-audio-title{font-size:var(--type-2xs);font-weight:var(--fw-medium,500);color:var(--site-text-primary);text-overflow:ellipsis;white-space:nowrap;line-height:1.3;overflow:hidden}.media-audio-card .media-audio-time{font-size:var(--type-2xs);color:var(--site-text-secondary);font-variant-numeric:tabular-nums;line-height:1}.media-audio-card .media-audio-play-btn{cursor:pointer;background:var(--site-text-primary);width:28px;height:28px;color:var(--background,#fff);border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;transition:transform .12s;display:flex}.media-audio-card .media-audio-play-btn:hover{transform:scale(1.1)}.media-audio-card .media-audio-play-btn:active{transform:scale(.92)}.media-audio-card .media-audio-play-btn svg{width:14px;height:14px}.media-audio-card .media-audio-icon-play{margin-left:2px}.media-audio-card .media-audio-icon-pause,.media-audio-card.is-playing .media-audio-icon-play{display:none}.media-audio-card.is-playing .media-audio-icon-pause{display:block}.media-gallery-card{color:var(--site-text-primary);border-radius:var(--media-radius,.5rem);background-color:var(--site-nav-hover-bg);border:1px solid var(--site-divider);text-decoration:none;transition:background-color .15s;display:block;overflow:hidden}a.media-gallery-card:hover,button.media-gallery-card:hover{background-color:var(--site-divider)}button.media-gallery-card{cursor:pointer;font:inherit;text-align:inherit}.media-gallery-card-inner{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:8px;width:100%;height:100%;padding:16px 12px;display:flex}.media-gallery-card-icon{color:var(--site-text-secondary);opacity:.6}.media-gallery-card-summary{font-size:var(--type-xs);color:var(--site-text-secondary);-webkit-line-clamp:2;word-break:break-word;-webkit-box-orient:vertical;line-height:1.4;display:-webkit-box;overflow:hidden}.media-gallery-card-meta{font-size:var(--type-xs);color:var(--site-text-secondary)}.media-lightbox-video{background-color:#000;border-radius:4px;outline:none;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in}.media-lightbox-video:focus,.media-lightbox-video:focus-visible{outline:none}.media-lightbox-video-short{object-fit:contain;width:100%;max-width:none;height:100%;max-height:none;display:block}@media (max-width:640px){.media-lightbox-video{border-radius:0}}[data-post-media] img{background:0 0}.media-gallery-scroll-wrap{--_fade:1.5rem;--_mask-left:black;--_mask-right:black}.media-gallery-scroll-wrap>[data-post-media]{-webkit-mask-image:linear-gradient(to right, var(--_mask-left) 0%, black var(--_fade), black calc(100% - var(--_fade)), var(--_mask-right) 100%);mask-image:linear-gradient(to right, var(--_mask-left) 0%, black var(--_fade), black calc(100% - var(--_fade)), var(--_mask-right) 100%);-webkit-mask-image:linear-gradient(to right, var(--_mask-left) 0%, black var(--_fade), black calc(100% - var(--_fade)), var(--_mask-right) 100%)}.media-gallery-scroll-wrap.can-scroll-start{--_mask-left:transparent}.media-gallery-scroll-wrap.can-scroll-end{--_mask-right:transparent}
|
|
1
|
+
@keyframes lightbox-fade-in{0%{opacity:0}to{opacity:1}}@keyframes lightbox-scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.media-lightbox{background:0 0;border:none;outline:none;width:100%;max-width:100%;height:100%;max-height:100%;padding:0}.media-lightbox[open]{animation:.2s both lightbox-fade-in}.media-lightbox::backdrop{background-color:#000}.media-lightbox-content{outline:none;width:100%;height:100dvh;position:relative}.media-lightbox-stage{box-sizing:border-box;justify-content:center;align-items:center;width:100%;height:100%;padding:64px 96px;display:flex;overflow:hidden}.media-lightbox-stage-scroll{overscroll-behavior:contain;scrollbar-gutter:stable both-edges;-webkit-overflow-scrolling:touch;align-items:flex-start;overflow:hidden auto}.media-lightbox-img{object-fit:contain;border-radius:4px;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in;display:block}.media-lightbox-img-zoomable{cursor:zoom-in}.media-lightbox-img-scroll{width:min(100%,44rem);max-width:none;height:auto;max-height:none;margin:0 auto}.media-lightbox-img-zoomable.media-lightbox-img-scroll{cursor:zoom-out}.media-lightbox-close{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:40px;height:40px;transition:background-color .15s;display:flex;position:fixed;top:16px;left:16px}.media-lightbox-close:hover{background-color:#000000b3}.media-lightbox-nav{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s;display:flex;position:fixed;top:50%;transform:translateY(-50%)}.media-lightbox-nav:hover{background-color:#000000b3}.media-lightbox-nav-prev{left:16px}.media-lightbox-nav-next{right:16px}.media-lightbox-counter{z-index:10;font-size:var(--type-xs);color:#ffffffb3;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;position:fixed;top:20px;left:50%;transform:translate(-50%)}.media-lightbox-short-frame{max-width:100%;max-height:100%;display:block;position:relative}.media-lightbox-short-controls{z-index:2;pointer-events:none;height:86px;position:absolute;bottom:0;left:0;right:0}.media-lightbox-short-progress{--media-progress:0%;z-index:1;appearance:none;pointer-events:auto;cursor:pointer;background:0 0;width:auto;height:34px;margin:0;padding:0;display:block;position:absolute;bottom:0;left:16px;right:16px}.media-lightbox-short-progress::-webkit-slider-runnable-track{background:linear-gradient(to right, #ffffffeb 0, #ffffffeb var(--media-progress), #ffffff2e var(--media-progress), #ffffff2e 100%);border-radius:999px;height:2px;transition:height .15s,background .15s}.media-lightbox-short-progress::-webkit-slider-thumb{appearance:none;opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;margin-top:-4px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-progress::-moz-range-track{background:#ffffff2e;border:none;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-progress{background:#ffffffeb;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-thumb{opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-controls-portrait .media-lightbox-short-progress{width:calc(100% - 48px);right:auto}.media-lightbox-short-progress:hover::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:focus-visible::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-track{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-progress{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-progress{height:4px}.media-lightbox-short-progress:hover::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:hover::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-mute{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#777;border:none;border-radius:999px;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:30px;right:24px}.media-lightbox-short-mute svg{flex-shrink:0;width:16px;height:16px;display:block}.media-lightbox-short-mute:hover{background-color:#686868;transform:scale(1.03)}.media-lightbox-short-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}@media (max-width:640px){.media-lightbox-stage{padding:48px 16px}.media-lightbox-img{border-radius:0}.media-lightbox-img-scroll{width:100%}.media-lightbox-close{width:36px;height:36px;top:12px;left:12px}.media-lightbox-nav{width:36px;height:36px}.media-lightbox-nav-prev{left:8px}.media-lightbox-nav-next{right:8px}.media-lightbox-short-mute{bottom:26px;right:24px}}.media-visual-frame{border-radius:var(--media-radius,.5rem);background-color:var(--color-muted);display:block;overflow:hidden}.media-visual{background-position:50%;background-repeat:no-repeat;display:block}.media-video-wrap{position:relative}.media-video-link{cursor:pointer;display:block}.media-video-wrap video{object-fit:contain;background-color:var(--color-muted);width:100%;max-height:24rem}.media-video-wrap-short video{background-color:#000}.media-feed-video-mute{z-index:1;color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:999px;justify-content:center;align-items:center;width:28px;height:28px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:16px;right:16px}.media-feed-video-mute svg{width:12px;height:12px}.media-feed-video-mute:hover{background-color:#0000009e;transform:scale(1.03)}.media-feed-video-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-feed-video-icon{transition:opacity .15s,transform .15s;position:absolute}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-muted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-unmuted{opacity:1;transform:scale(1)}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-unmuted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-muted{opacity:0;transform:scale(.92)}.media-video-play-overlay{pointer-events:none;justify-content:center;align-items:center;transition:opacity .15s;display:flex;position:absolute;inset:0}.media-video-play-overlay svg{filter:drop-shadow(0 2px 6px #0006);opacity:.85;width:48px;height:48px}.media-gallery-card.media-audio-card{flex-direction:column;display:flex}.media-audio-card .media-audio-el{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.media-audio-card .media-audio-artwork{background:linear-gradient(160deg,#8080801f 0%,#80808008 100%);flex:1;justify-content:center;align-items:center;width:100%;min-height:0;display:flex}.media-audio-card .media-audio-artwork svg{width:32px;height:32px;color:var(--site-text-secondary);opacity:.3}.media-audio-card .media-audio-waveform{cursor:pointer;touch-action:none;width:100%;height:24px;display:none}.media-audio-card.has-waveform .media-audio-waveform{display:block}.media-audio-card.has-waveform .media-audio-range{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.media-audio-card .media-audio-controls{flex-direction:column;flex-shrink:0;padding:0 0 6px;display:flex}.media-audio-card .media-audio-range{appearance:none;cursor:pointer;touch-action:none;background:0 0;width:100%;height:20px;margin:0;padding:0}.media-audio-card .media-audio-range::-webkit-slider-runnable-track{background:#80808026;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-track{background:#80808026;border:none;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-progress{background:var(--site-text-primary);border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-webkit-slider-thumb{-webkit-appearance:none;background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;margin-top:-3.5px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-webkit-slider-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-webkit-slider-thumb{opacity:1}.media-audio-card .media-audio-range::-moz-range-thumb{background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-moz-range-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-moz-range-thumb{opacity:1}.media-audio-card .media-audio-range:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:2px}.media-audio-card .media-audio-row{align-items:center;gap:6px;min-width:0;padding:6px 8px 0;display:flex}.media-audio-card .media-audio-info{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.media-audio-card .media-audio-title{font-size:var(--type-2xs);font-weight:var(--fw-medium,500);color:var(--site-text-primary);text-overflow:ellipsis;white-space:nowrap;line-height:1.3;overflow:hidden}.media-audio-card .media-audio-time{font-size:var(--type-2xs);color:var(--site-text-secondary);font-variant-numeric:tabular-nums;line-height:1}.media-audio-card .media-audio-play-btn{cursor:pointer;background:var(--site-text-primary);width:28px;height:28px;color:var(--background,#fff);border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;transition:transform .12s;display:flex}.media-audio-card .media-audio-play-btn:hover{transform:scale(1.1)}.media-audio-card .media-audio-play-btn:active{transform:scale(.92)}.media-audio-card .media-audio-play-btn svg{width:14px;height:14px}.media-audio-card .media-audio-icon-play{margin-left:2px}.media-audio-card .media-audio-icon-pause,.media-audio-card.is-playing .media-audio-icon-play{display:none}.media-audio-card.is-playing .media-audio-icon-pause{display:block}.media-gallery-card{color:var(--site-text-primary);border-radius:var(--media-radius,.5rem);background-color:var(--site-nav-hover-bg);border:1px solid var(--site-divider);text-decoration:none;transition:background-color .15s;display:block;overflow:hidden}a.media-gallery-card:hover,button.media-gallery-card:hover{background-color:var(--site-divider)}button.media-gallery-card{cursor:pointer;font:inherit;text-align:inherit}.media-gallery-card-inner{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:8px;width:100%;height:100%;padding:16px 12px;display:flex}.media-gallery-card-icon{color:var(--site-text-secondary);opacity:.6}.media-gallery-card-summary{font-size:var(--type-xs);color:var(--site-text-secondary);-webkit-line-clamp:2;word-break:break-word;-webkit-box-orient:vertical;line-height:1.4;display:-webkit-box;overflow:hidden}.media-gallery-card-meta{font-size:var(--type-xs);color:var(--site-text-secondary)}.media-lightbox-video{background-color:#000;border-radius:4px;outline:none;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in}.media-lightbox-video:focus,.media-lightbox-video:focus-visible{outline:none}.media-lightbox-video-short{object-fit:contain;width:100%;max-width:none;height:100%;max-height:none;display:block}@media (max-width:640px){.media-lightbox-video{border-radius:0}}[data-post-media] img{background:0 0}.media-gallery-scroll-wrap{position:relative}.media-gallery-nav{z-index:2;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;cursor:pointer;opacity:0;pointer-events:none;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:34px;height:34px;padding:0;transition:opacity .15s,background-color .15s;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.media-gallery-nav:hover{background-color:#000000b3}.media-gallery-nav svg{width:18px;height:18px;display:block}.media-gallery-nav-prev{left:8px}.media-gallery-nav-next{right:8px}@media (hover:hover) and (pointer:fine){.media-gallery-scroll-wrap.can-scroll-start:hover .media-gallery-nav-prev,.media-gallery-scroll-wrap.can-scroll-end:hover .media-gallery-nav-next{opacity:1;pointer-events:auto}}.media-gallery-scroll-wrap>[data-post-media]:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:4px}
|
|
2
2
|
/*$vite$:1*/
|