@anthusai/papyrus 1.0.0-next.51 → 1.0.0-next.53
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/app/[year]/[month]/[day]/[articleSlug]/page.tsx +5 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +5 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +5 -0
- package/app/api/revalidate/route.ts +7 -0
- package/app/page.tsx +4 -3
- package/components/article-video.tsx +19 -7
- package/components/use-near-viewport.ts +30 -0
- package/lib/reader-static-params.ts +73 -5
- package/lib/revalidate-prewarm.ts +53 -0
- package/lib/video-loading.ts +22 -0
- package/package.json +4 -1
- package/routes.manifest.json +9 -4
|
@@ -4,9 +4,14 @@ import { getCachedEditionItem, loadCachedEditionContent } from "../../../../../l
|
|
|
4
4
|
import { getEditionDatePath, parseEditionArticleRoute } from "../../../../../lib/edition-routes";
|
|
5
5
|
import { buildPresentationFooterEntries } from "../../../../../lib/presentation-footer";
|
|
6
6
|
import { SITE_BRAND } from "../../../../../lib/site-brand";
|
|
7
|
+
import { generateEditionItemStaticParams } from "../../../../../lib/reader-static-params";
|
|
7
8
|
// Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
|
|
8
9
|
export const revalidate = 60;
|
|
9
10
|
|
|
11
|
+
export async function generateStaticParams() {
|
|
12
|
+
return generateEditionItemStaticParams();
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
type DateScopedArticlePageProps = {
|
|
11
16
|
params: Promise<{
|
|
12
17
|
year: string;
|
|
@@ -2,9 +2,14 @@ import { notFound, redirect } from "next/navigation";
|
|
|
2
2
|
import { EditionRoutePage } from "../../edition-route-page";
|
|
3
3
|
import { parseEditionPageRoute } from "../../../../../../lib/edition-routes";
|
|
4
4
|
import { getDefaultPretextLayout, getForcedPresentation } from "../../../../../../lib/site-brand";
|
|
5
|
+
import { generateEditionPageStaticParams } from "../../../../../../lib/reader-static-params";
|
|
5
6
|
// Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
|
|
6
7
|
export const revalidate = 60;
|
|
7
8
|
|
|
9
|
+
export async function generateStaticParams() {
|
|
10
|
+
return generateEditionPageStaticParams();
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
type EditionPageRouteProps = {
|
|
9
14
|
params: Promise<{
|
|
10
15
|
year: string;
|
|
@@ -2,9 +2,14 @@ import { notFound, redirect } from "next/navigation";
|
|
|
2
2
|
import { EditionRoutePage } from "../../edition-route-page";
|
|
3
3
|
import { createSectionKey } from "../../../../../../lib/edition-sections";
|
|
4
4
|
import { parseEditionSectionRoute } from "../../../../../../lib/edition-routes";
|
|
5
|
+
import { generateEditionSectionStaticParams } from "../../../../../../lib/reader-static-params";
|
|
5
6
|
// Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
|
|
6
7
|
export const revalidate = 60;
|
|
7
8
|
|
|
9
|
+
export async function generateStaticParams() {
|
|
10
|
+
return generateEditionSectionStaticParams();
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
type EditionSectionRouteProps = {
|
|
9
14
|
params: Promise<{
|
|
10
15
|
year: string;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
editionItemCacheTag,
|
|
11
11
|
} from "../../../lib/cached-content-repository";
|
|
12
12
|
import { isAuthorizedRevalidateRequest } from "../../../lib/revalidate-secret";
|
|
13
|
+
import { prewarmPaths, resolvePrewarmOrigin } from "../../../lib/revalidate-prewarm";
|
|
13
14
|
import { getEditionArticlePath, getEditionDatePath } from "../../../lib/edition-routes";
|
|
14
15
|
|
|
15
16
|
export const dynamic = "force-dynamic";
|
|
@@ -20,6 +21,7 @@ type RevalidateRequestBody = {
|
|
|
20
21
|
itemSlugs?: string[];
|
|
21
22
|
paths?: string[];
|
|
22
23
|
tags?: string[];
|
|
24
|
+
prewarm?: boolean;
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export async function POST(request: Request) {
|
|
@@ -87,9 +89,14 @@ export async function POST(request: Request) {
|
|
|
87
89
|
revalidatedPaths.add(normalized);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
const prewarmed = body.prewarm === false
|
|
93
|
+
? []
|
|
94
|
+
: await prewarmPaths(resolvePrewarmOrigin(request.url), [...revalidatedPaths]);
|
|
95
|
+
|
|
90
96
|
return NextResponse.json({
|
|
91
97
|
ok: true,
|
|
92
98
|
revalidatedTags: [...revalidatedTags],
|
|
93
99
|
revalidatedPaths: [...revalidatedPaths],
|
|
100
|
+
prewarmed,
|
|
94
101
|
});
|
|
95
102
|
}
|
package/app/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { getSiteRenderer } from "../lib/site-renderer";
|
|
|
2
2
|
import { contentRepository, getScenarioIdParam } from "../lib/content-repository";
|
|
3
3
|
import type { EditionContent } from "../lib/content-types";
|
|
4
4
|
import { createEditionSectionPlan } from "../lib/edition-sections";
|
|
5
|
+
import { getCachedLatestPublishedEdition, loadCachedEditionContent } from "../lib/cached-content-repository";
|
|
5
6
|
import { getEditionDatePath } from "../lib/edition-routes";
|
|
6
7
|
import {
|
|
7
8
|
EMPTY_EDITION_PLACEHOLDER_SLUG,
|
|
@@ -77,7 +78,7 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const latestEdition = await
|
|
81
|
+
const latestEdition = await getCachedLatestPublishedEdition();
|
|
81
82
|
if (latestEdition) redirect(getEditionDatePath(latestEdition.editionDate));
|
|
82
83
|
return <siteRenderer.renderEdition content={createEmptyGraphQLEdition()} />;
|
|
83
84
|
}
|
|
@@ -88,9 +89,9 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async function loadLatestGraphQLEdition(): Promise<EditionContent | null> {
|
|
91
|
-
const latestEdition = await
|
|
92
|
+
const latestEdition = await getCachedLatestPublishedEdition();
|
|
92
93
|
if (!latestEdition) return null;
|
|
93
|
-
return
|
|
94
|
+
return loadCachedEditionContent({ editionDate: latestEdition.editionDate });
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
async function loadFirstPublishedEditionPath(): Promise<string | undefined> {
|
|
@@ -7,6 +7,8 @@ import { resolveThemedVideoSrc } from "../lib/themed-image";
|
|
|
7
7
|
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
8
8
|
import { getSiteBrand } from "../lib/site-brand";
|
|
9
9
|
import { normalizeDevPreviewDsl, type VideoScriptRef } from "../lib/video-script";
|
|
10
|
+
import { useNearViewport } from "./use-near-viewport";
|
|
11
|
+
import { planVideoLoading, videoElementKey } from "../lib/video-loading";
|
|
10
12
|
import { resolveVideoMode, resolveVideoModeFromEnv, type VideoMode } from "../lib/video-mode";
|
|
11
13
|
|
|
12
14
|
type ArticleVideoFigureProps = {
|
|
@@ -18,6 +20,7 @@ type ArticleVideoFigureProps = {
|
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
const PREVIEW_STORAGE_PREFIX = "papyrus-video-preview:";
|
|
23
|
+
const VIDEO_LOAD_ROOT_MARGIN = "200px 0px";
|
|
21
24
|
|
|
22
25
|
export function ArticleVideoFigure({
|
|
23
26
|
video,
|
|
@@ -34,6 +37,8 @@ export function ArticleVideoFigure({
|
|
|
34
37
|
const [previewReady, setPreviewReady] = useState(false);
|
|
35
38
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
36
39
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
40
|
+
const figureRef = useRef<HTMLElement>(null);
|
|
41
|
+
const isNearViewport = useNearViewport(figureRef, VIDEO_LOAD_ROOT_MARGIN);
|
|
37
42
|
const isFramed = getSiteBrand().videoPlayer === "framed";
|
|
38
43
|
|
|
39
44
|
useEffect(() => {
|
|
@@ -44,6 +49,12 @@ export function ArticleVideoFigure({
|
|
|
44
49
|
// Match SSR to the default dark MP4 in `video.src` until after hydration.
|
|
45
50
|
const resolvedTheme = hasHydrated ? theme : "dark";
|
|
46
51
|
const src = resolveThemedVideoSrc(video.src, video.themeVariants, resolvedTheme);
|
|
52
|
+
const loadingPlan = planVideoLoading({
|
|
53
|
+
hasHydrated,
|
|
54
|
+
isNearViewport,
|
|
55
|
+
hasPoster: Boolean(video.posterSrc),
|
|
56
|
+
});
|
|
57
|
+
const videoKey = videoElementKey(loadingPlan.attachSource, src);
|
|
47
58
|
const usePreview = videoMode === "preview" && Boolean(videoScript?.dsl);
|
|
48
59
|
const previewDsl = videoScript?.dsl
|
|
49
60
|
? normalizeDevPreviewDsl(videoScript.dsl, getSiteBrand().video?.sceneComponents)
|
|
@@ -191,6 +202,7 @@ export function ArticleVideoFigure({
|
|
|
191
202
|
if (isFramed) {
|
|
192
203
|
return (
|
|
193
204
|
<figure
|
|
205
|
+
ref={figureRef}
|
|
194
206
|
className={`${figureClassName} article-video--framed`}
|
|
195
207
|
data-media-type="video"
|
|
196
208
|
data-video-theme={resolvedTheme}
|
|
@@ -200,11 +212,11 @@ export function ArticleVideoFigure({
|
|
|
200
212
|
<video
|
|
201
213
|
ref={videoRef}
|
|
202
214
|
playsInline
|
|
203
|
-
preload=
|
|
215
|
+
preload={loadingPlan.preload}
|
|
204
216
|
poster={video.posterSrc}
|
|
205
217
|
aria-label={video.alt}
|
|
206
218
|
className="article-video__player"
|
|
207
|
-
key={
|
|
219
|
+
key={videoKey}
|
|
208
220
|
onPlay={() => setIsPlaying(true)}
|
|
209
221
|
onPause={() => setIsPlaying(false)}
|
|
210
222
|
onEnded={() => setIsPlaying(false)}
|
|
@@ -214,7 +226,7 @@ export function ArticleVideoFigure({
|
|
|
214
226
|
if (Number.isFinite(nextDuration) && nextDuration > 0) setDuration(nextDuration);
|
|
215
227
|
}}
|
|
216
228
|
>
|
|
217
|
-
<source src={src} type="video/mp4" />
|
|
229
|
+
{loadingPlan.attachSource ? <source src={src} type="video/mp4" /> : null}
|
|
218
230
|
</video>
|
|
219
231
|
</div>
|
|
220
232
|
{framedControls}
|
|
@@ -226,17 +238,17 @@ export function ArticleVideoFigure({
|
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
return (
|
|
229
|
-
<figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme} data-video-mode={videoMode}>
|
|
241
|
+
<figure ref={figureRef} className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme} data-video-mode={videoMode}>
|
|
230
242
|
<video
|
|
231
243
|
controls
|
|
232
244
|
playsInline
|
|
233
|
-
preload=
|
|
245
|
+
preload={loadingPlan.preload}
|
|
234
246
|
poster={video.posterSrc}
|
|
235
247
|
aria-label={video.alt}
|
|
236
248
|
className="article-video__player"
|
|
237
|
-
key={
|
|
249
|
+
key={videoKey}
|
|
238
250
|
>
|
|
239
|
-
<source src={src} type="video/mp4" />
|
|
251
|
+
{loadingPlan.attachSource ? <source src={src} type="video/mp4" /> : null}
|
|
240
252
|
</video>
|
|
241
253
|
{video.caption ? <figcaption>{video.caption}</figcaption> : null}
|
|
242
254
|
{video.credit ? <p className="article-video__credit">{video.credit}</p> : null}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type RefObject } from "react";
|
|
4
|
+
|
|
5
|
+
export function useNearViewport(targetRef: RefObject<Element | null>, rootMargin: string): boolean {
|
|
6
|
+
const [isNearViewport, setIsNearViewport] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (isNearViewport) return;
|
|
10
|
+
const target = targetRef.current;
|
|
11
|
+
if (!target) return;
|
|
12
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
13
|
+
setIsNearViewport(true);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const observer = new IntersectionObserver(
|
|
17
|
+
(entries) => {
|
|
18
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
19
|
+
setIsNearViewport(true);
|
|
20
|
+
observer.disconnect();
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{ rootMargin },
|
|
24
|
+
);
|
|
25
|
+
observer.observe(target);
|
|
26
|
+
return () => observer.disconnect();
|
|
27
|
+
}, [isNearViewport, rootMargin, targetRef]);
|
|
28
|
+
|
|
29
|
+
return isNearViewport;
|
|
30
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ContentRepository, EditionContent } from "./content-types";
|
|
1
2
|
import { graphqlContentRepository } from "./graphql-content-repository";
|
|
2
3
|
import { SITE_BRAND } from "./site-brand";
|
|
3
4
|
import { getContentSource } from "./site-env";
|
|
@@ -32,11 +33,23 @@ type EditionDateStaticParam = {
|
|
|
32
33
|
day: string;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const PUBLISHED_EDITION_ENUMERATION_LIMIT = 100;
|
|
37
|
+
const DEEP_ROUTE_EDITION_LIMIT = 10;
|
|
38
|
+
const RESERVED_DATE_CHILD_SEGMENTS = new Set(["page", "section"]);
|
|
39
|
+
|
|
40
|
+
type EnumerationRepository = Pick<ContentRepository, "listPublishedEditions" | "loadEditionContent">;
|
|
41
|
+
|
|
42
|
+
function shouldEnumerateAtBuild(): boolean {
|
|
43
|
+
if (process.env.NODE_ENV === "development") return false;
|
|
44
|
+
return siteEnumeratesReaderRoutesAtBuild();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function generateEditionDateStaticParams(
|
|
48
|
+
repository: EnumerationRepository = graphqlContentRepository,
|
|
49
|
+
): Promise<EditionDateStaticParam[]> {
|
|
50
|
+
if (!shouldEnumerateAtBuild()) return [];
|
|
38
51
|
|
|
39
|
-
const { editions } = await
|
|
52
|
+
const { editions } = await repository.listPublishedEditions({ limit: PUBLISHED_EDITION_ENUMERATION_LIMIT });
|
|
40
53
|
const params: EditionDateStaticParam[] = [];
|
|
41
54
|
for (const edition of editions) {
|
|
42
55
|
const route = editionDateToStaticParam(edition.editionDate);
|
|
@@ -45,6 +58,61 @@ export async function generateEditionDateStaticParams(): Promise<EditionDateStat
|
|
|
45
58
|
return params;
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
async function loadRecentEditionContents(repository: EnumerationRepository): Promise<EditionContent[]> {
|
|
62
|
+
const { editions } = await repository.listPublishedEditions({ limit: PUBLISHED_EDITION_ENUMERATION_LIMIT });
|
|
63
|
+
const recent = [...editions]
|
|
64
|
+
.sort((left, right) => right.editionDate.localeCompare(left.editionDate))
|
|
65
|
+
.slice(0, DEEP_ROUTE_EDITION_LIMIT);
|
|
66
|
+
return Promise.all(recent.map((edition) => repository.loadEditionContent({ editionDate: edition.editionDate })));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function generateEditionItemStaticParams(
|
|
70
|
+
repository: EnumerationRepository = graphqlContentRepository,
|
|
71
|
+
): Promise<Array<EditionDateStaticParam & { articleSlug: string }>> {
|
|
72
|
+
if (!shouldEnumerateAtBuild()) return [];
|
|
73
|
+
|
|
74
|
+
const params: Array<EditionDateStaticParam & { articleSlug: string }> = [];
|
|
75
|
+
for (const content of await loadRecentEditionContents(repository)) {
|
|
76
|
+
const date = editionDateToStaticParam(content.editionDate);
|
|
77
|
+
if (!date) continue;
|
|
78
|
+
for (const item of content.items) {
|
|
79
|
+
if (RESERVED_DATE_CHILD_SEGMENTS.has(item.slug.toLowerCase())) continue;
|
|
80
|
+
params.push({ ...date, articleSlug: item.slug });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return params;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function generateEditionPageStaticParams(
|
|
87
|
+
repository: EnumerationRepository = graphqlContentRepository,
|
|
88
|
+
): Promise<Array<EditionDateStaticParam & { pageNumber: string }>> {
|
|
89
|
+
if (!shouldEnumerateAtBuild()) return [];
|
|
90
|
+
|
|
91
|
+
const params: Array<EditionDateStaticParam & { pageNumber: string }> = [];
|
|
92
|
+
for (const content of await loadRecentEditionContents(repository)) {
|
|
93
|
+
const date = editionDateToStaticParam(content.editionDate);
|
|
94
|
+
if (!date) continue;
|
|
95
|
+
for (let pageNumber = 2; pageNumber <= content.layoutPlan.pages.length; pageNumber += 1) {
|
|
96
|
+
params.push({ ...date, pageNumber: String(pageNumber) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return params;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function generateEditionSectionStaticParams(
|
|
103
|
+
repository: EnumerationRepository = graphqlContentRepository,
|
|
104
|
+
): Promise<Array<EditionDateStaticParam & { sectionKey: string }>> {
|
|
105
|
+
if (!shouldEnumerateAtBuild()) return [];
|
|
106
|
+
|
|
107
|
+
const params: Array<EditionDateStaticParam & { sectionKey: string }> = [];
|
|
108
|
+
for (const content of await loadRecentEditionContents(repository)) {
|
|
109
|
+
const date = editionDateToStaticParam(content.editionDate);
|
|
110
|
+
if (!date) continue;
|
|
111
|
+
for (const section of content.sections) params.push({ ...date, sectionKey: section.key });
|
|
112
|
+
}
|
|
113
|
+
return params;
|
|
114
|
+
}
|
|
115
|
+
|
|
48
116
|
export async function generateArticleStaticParams(): Promise<Array<{ slug: string }>> {
|
|
49
117
|
if (process.env.NODE_ENV === "development") return [];
|
|
50
118
|
if (!siteEnumeratesReaderRoutesAtBuild()) return [];
|
|
@@ -64,6 +132,6 @@ function editionDateToStaticParam(editionDate: string): EditionDateStaticParam |
|
|
|
64
132
|
return {
|
|
65
133
|
year: match[1],
|
|
66
134
|
month: monthName,
|
|
67
|
-
day:
|
|
135
|
+
day: match[3],
|
|
68
136
|
};
|
|
69
137
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const PREWARM_ORIGIN_ENV = "PAPYRUS_PREWARM_ORIGIN";
|
|
2
|
+
export const PREWARM_CONCURRENCY = 4;
|
|
3
|
+
export const PREWARM_MAX_PATHS = 40;
|
|
4
|
+
export const PREWARM_TIMEOUT_MILLISECONDS = 25_000;
|
|
5
|
+
export const PREWARM_REQUEST_HEADER = "x-papyrus-prewarm";
|
|
6
|
+
|
|
7
|
+
export type PrewarmFetch = (url: string, init: { headers: Record<string, string>; signal: AbortSignal; redirect: "manual" }) => Promise<{ status: number }>;
|
|
8
|
+
|
|
9
|
+
export type PrewarmResult = {
|
|
10
|
+
path: string;
|
|
11
|
+
status: number | null;
|
|
12
|
+
milliseconds: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function resolvePrewarmOrigin(requestUrl: string, environment: Record<string, string | undefined> = process.env): string {
|
|
16
|
+
const configured = environment[PREWARM_ORIGIN_ENV]?.trim();
|
|
17
|
+
if (configured) return configured.replace(/\/+$/, "");
|
|
18
|
+
return new URL(requestUrl).origin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function prewarmPaths(
|
|
22
|
+
origin: string,
|
|
23
|
+
paths: string[],
|
|
24
|
+
options: { fetchPage?: PrewarmFetch; now?: () => number } = {},
|
|
25
|
+
): Promise<PrewarmResult[]> {
|
|
26
|
+
const fetchPage = options.fetchPage ?? (fetch as unknown as PrewarmFetch);
|
|
27
|
+
const now = options.now ?? Date.now;
|
|
28
|
+
const queue = [...new Set(paths)].filter((path) => path.startsWith("/")).slice(0, PREWARM_MAX_PATHS);
|
|
29
|
+
const results: PrewarmResult[] = [];
|
|
30
|
+
|
|
31
|
+
async function worker(): Promise<void> {
|
|
32
|
+
for (let path = queue.shift(); path !== undefined; path = queue.shift()) {
|
|
33
|
+
const startedAt = now();
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), PREWARM_TIMEOUT_MILLISECONDS);
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetchPage(`${origin}${path}`, {
|
|
38
|
+
headers: { [PREWARM_REQUEST_HEADER]: "1" },
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
redirect: "manual",
|
|
41
|
+
});
|
|
42
|
+
results.push({ path, status: response.status, milliseconds: now() - startedAt });
|
|
43
|
+
} catch {
|
|
44
|
+
results.push({ path, status: null, milliseconds: now() - startedAt });
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await Promise.all(Array.from({ length: Math.min(PREWARM_CONCURRENCY, queue.length) }, worker));
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type VideoPreload = "none" | "metadata";
|
|
2
|
+
|
|
3
|
+
export type VideoLoadingPlan = {
|
|
4
|
+
attachSource: boolean;
|
|
5
|
+
preload: VideoPreload;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type VideoLoadingInput = {
|
|
9
|
+
hasHydrated: boolean;
|
|
10
|
+
isNearViewport: boolean;
|
|
11
|
+
hasPoster: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function planVideoLoading({ hasHydrated, isNearViewport, hasPoster }: VideoLoadingInput): VideoLoadingPlan {
|
|
15
|
+
const attachSource = hasHydrated && isNearViewport;
|
|
16
|
+
const preload: VideoPreload = attachSource && !hasPoster ? "metadata" : "none";
|
|
17
|
+
return { attachSource, preload };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function videoElementKey(attachSource: boolean, resolvedSource: string): string {
|
|
21
|
+
return attachSource ? resolvedSource : "unattached";
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
"./components/ui/tooltip": "./components/ui/tooltip.tsx",
|
|
128
128
|
"./components/use-blog-header-obstacles": "./components/use-blog-header-obstacles.ts",
|
|
129
129
|
"./components/use-brand-fonts-ready": "./components/use-brand-fonts-ready.ts",
|
|
130
|
+
"./components/use-near-viewport": "./components/use-near-viewport.ts",
|
|
130
131
|
"./components/use-resolved-papyrus-theme": "./components/use-resolved-papyrus-theme.ts",
|
|
131
132
|
"./components/use-rhythm-overlay": "./components/use-rhythm-overlay.ts",
|
|
132
133
|
"./lib/amplify-outputs-guard": "./lib/amplify-outputs-guard.ts",
|
|
@@ -209,6 +210,7 @@
|
|
|
209
210
|
"./lib/renderer-config": "./lib/renderer-config.ts",
|
|
210
211
|
"./lib/renderer": "./lib/renderer.ts",
|
|
211
212
|
"./lib/reporting-story-budget": "./lib/reporting-story-budget.ts",
|
|
213
|
+
"./lib/revalidate-prewarm": "./lib/revalidate-prewarm.ts",
|
|
212
214
|
"./lib/revalidate-secret": "./lib/revalidate-secret.ts",
|
|
213
215
|
"./lib/robots-policy": "./lib/robots-policy.ts",
|
|
214
216
|
"./lib/root-route-redirect": "./lib/root-route-redirect.ts",
|
|
@@ -224,6 +226,7 @@
|
|
|
224
226
|
"./lib/staging-preview-object": "./lib/staging-preview-object.ts",
|
|
225
227
|
"./lib/themed-image": "./lib/themed-image.ts",
|
|
226
228
|
"./lib/utils": "./lib/utils.ts",
|
|
229
|
+
"./lib/video-loading": "./lib/video-loading.ts",
|
|
227
230
|
"./lib/video-mode": "./lib/video-mode.ts",
|
|
228
231
|
"./lib/video-script": "./lib/video-script.ts",
|
|
229
232
|
"./renderers/markus/index": "./renderers/markus/index.ts",
|
|
@@ -306,7 +309,7 @@
|
|
|
306
309
|
"optional": true
|
|
307
310
|
}
|
|
308
311
|
},
|
|
309
|
-
"version": "1.0.0-next.
|
|
312
|
+
"version": "1.0.0-next.53",
|
|
310
313
|
"dependencies": {
|
|
311
314
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
312
315
|
"@aws-sdk/client-cognito-identity-provider": "3.1045.0",
|
package/routes.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.0-next.
|
|
2
|
+
"version": "1.0.0-next.53",
|
|
3
3
|
"routes": [
|
|
4
4
|
{
|
|
5
5
|
"file": "app/%5F_preview/[[...path]]/route.ts",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"reader": true,
|
|
17
17
|
"hasDefault": true,
|
|
18
18
|
"exports": [
|
|
19
|
-
"generateMetadata"
|
|
19
|
+
"generateMetadata",
|
|
20
|
+
"generateStaticParams"
|
|
20
21
|
],
|
|
21
22
|
"config": {
|
|
22
23
|
"revalidate": "60"
|
|
@@ -37,7 +38,9 @@
|
|
|
37
38
|
"file": "app/[year]/[month]/[day]/page/[pageNumber]/page.tsx",
|
|
38
39
|
"reader": true,
|
|
39
40
|
"hasDefault": true,
|
|
40
|
-
"exports": [
|
|
41
|
+
"exports": [
|
|
42
|
+
"generateStaticParams"
|
|
43
|
+
],
|
|
41
44
|
"config": {
|
|
42
45
|
"revalidate": "60"
|
|
43
46
|
}
|
|
@@ -46,7 +49,9 @@
|
|
|
46
49
|
"file": "app/[year]/[month]/[day]/section/[sectionKey]/page.tsx",
|
|
47
50
|
"reader": true,
|
|
48
51
|
"hasDefault": true,
|
|
49
|
-
"exports": [
|
|
52
|
+
"exports": [
|
|
53
|
+
"generateStaticParams"
|
|
54
|
+
],
|
|
50
55
|
"config": {
|
|
51
56
|
"revalidate": "60"
|
|
52
57
|
}
|