@anthusai/papyrus 1.0.0-next.1 → 1.0.0-next.10
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/amplify/data/resource.js +32 -0
- package/amplify/data/resource.ts +37 -0
- package/amplify/data/schema.js +32 -0
- package/amplify/data/schema.ts +37 -0
- package/amplify/functions/content-actions/handler.py +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.ts +6 -0
- package/amplify/site-backend.js +28 -0
- package/amplify/site-backend.ts +35 -0
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
- package/app/globals.css +20 -4
- package/app/layout.tsx +8 -12
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/robots.ts +11 -0
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +74 -18
- package/components/article-video.tsx +112 -3
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-page.tsx +1 -4
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +1 -59
- package/infra/amplify-app-shell.js +205 -80
- package/infra/amplify-app-shell.ts +242 -113
- package/infra/build-specs.js +85 -0
- package/infra/build-specs.ts +131 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +3 -0
- package/infra/site-config.js +186 -0
- package/infra/site-config.ts +235 -0
- package/lib/cached-content-repository.ts +2 -1
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/empty-theme.css +1 -0
- package/lib/graphql-content-repository.ts +193 -39
- package/lib/layout-scenarios.ts +8 -8
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +2 -0
- package/lib/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +57 -42
- package/lib/site-env.ts +52 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +58 -33
- package/package.json +23 -1
- package/renderers/pretext/article-page.tsx +1 -6
- package/renderers/pretext/presentation-shell.tsx +2 -9
- package/routes.manifest.json +27 -1
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/with-papyrus.mjs +15 -2
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
package/amplify/site-backend.js
CHANGED
|
@@ -12,12 +12,14 @@ import { CfnEventSourceMapping, FunctionUrlAuthType } from "aws-cdk-lib/aws-lamb
|
|
|
12
12
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
|
|
15
16
|
import { authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
|
|
16
17
|
import { data } from "./data/resource.js";
|
|
17
18
|
import { assignmentAction } from "./functions/assignment-action/resource.js";
|
|
18
19
|
import { categoryAction } from "./functions/category-action/resource.js";
|
|
19
20
|
import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource.js";
|
|
20
21
|
import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource.js";
|
|
22
|
+
import { contentActions } from "./functions/content-actions/resource.js";
|
|
21
23
|
import { knowledgeQuery } from "./functions/knowledge-query/resource.js";
|
|
22
24
|
import { manageUserRole } from "./functions/manage-user-role/resource.js";
|
|
23
25
|
import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource.js";
|
|
@@ -79,6 +81,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
79
81
|
assignmentAction,
|
|
80
82
|
auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
|
|
81
83
|
categoryAction,
|
|
84
|
+
contentActions,
|
|
82
85
|
data,
|
|
83
86
|
graphqlJwtAuthorizer,
|
|
84
87
|
knowledgeQuery,
|
|
@@ -310,6 +313,7 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
310
313
|
for (const resource of [
|
|
311
314
|
backend.assignmentAction,
|
|
312
315
|
backend.categoryAction,
|
|
316
|
+
backend.contentActions,
|
|
313
317
|
backend.emailSubmissionProcessor,
|
|
314
318
|
backend.knowledgeQuery,
|
|
315
319
|
backend.modelAttachmentUpload,
|
|
@@ -323,6 +327,30 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
323
327
|
grantNewsroomReadWrite(backend.assignmentAction.resources.lambda);
|
|
324
328
|
grantNewsroomReadWrite(backend.categoryAction.resources.lambda);
|
|
325
329
|
grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda);
|
|
330
|
+
const contentActionsLambda = backend.contentActions.resources.lambda;
|
|
331
|
+
grantNewsroomReadWriteDelete(contentActionsLambda);
|
|
332
|
+
contentActionsLambda.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl);
|
|
333
|
+
contentActionsLambda.addToRolePolicy(new PolicyStatement({
|
|
334
|
+
actions: ["appsync:GraphQL"],
|
|
335
|
+
resources: ["*"],
|
|
336
|
+
}));
|
|
337
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
338
|
+
reader: site.reader,
|
|
339
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
340
|
+
cmsAppId: amplifyAppId,
|
|
341
|
+
region: Stack.of(contentActionsLambda).region,
|
|
342
|
+
account: Stack.of(contentActionsLambda).account,
|
|
343
|
+
});
|
|
344
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
345
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
346
|
+
}
|
|
347
|
+
if (rebuildSettings.resources.length > 0) {
|
|
348
|
+
contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
|
|
349
|
+
}
|
|
350
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
351
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
352
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
|
|
353
|
+
}
|
|
326
354
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
327
355
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
328
356
|
if (backend.sesInboundReceive) {
|
package/amplify/site-backend.ts
CHANGED
|
@@ -13,12 +13,14 @@ import * as s3 from "aws-cdk-lib/aws-s3";
|
|
|
13
13
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
14
14
|
import { dirname, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
|
|
16
17
|
import { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
|
|
17
18
|
import { data } from "./data/resource";
|
|
18
19
|
import { assignmentAction } from "./functions/assignment-action/resource";
|
|
19
20
|
import { categoryAction } from "./functions/category-action/resource";
|
|
20
21
|
import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
|
|
21
22
|
import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
|
|
23
|
+
import { contentActions } from "./functions/content-actions/resource";
|
|
22
24
|
import { knowledgeQuery } from "./functions/knowledge-query/resource";
|
|
23
25
|
import { manageUserRole } from "./functions/manage-user-role/resource";
|
|
24
26
|
import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
|
|
@@ -90,6 +92,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
90
92
|
assignmentAction,
|
|
91
93
|
auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
|
|
92
94
|
categoryAction,
|
|
95
|
+
contentActions,
|
|
93
96
|
data,
|
|
94
97
|
graphqlJwtAuthorizer,
|
|
95
98
|
knowledgeQuery,
|
|
@@ -386,6 +389,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
386
389
|
for (const resource of [
|
|
387
390
|
backend.assignmentAction,
|
|
388
391
|
backend.categoryAction,
|
|
392
|
+
backend.contentActions,
|
|
389
393
|
backend.emailSubmissionProcessor,
|
|
390
394
|
backend.knowledgeQuery,
|
|
391
395
|
backend.modelAttachmentUpload,
|
|
@@ -399,6 +403,37 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
399
403
|
grantNewsroomReadWrite(backend.assignmentAction.resources.lambda as LambdaFunction);
|
|
400
404
|
grantNewsroomReadWrite(backend.categoryAction.resources.lambda as LambdaFunction);
|
|
401
405
|
grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda as LambdaFunction);
|
|
406
|
+
const contentActionsLambda = backend.contentActions.resources.lambda as LambdaFunction;
|
|
407
|
+
grantNewsroomReadWriteDelete(contentActionsLambda);
|
|
408
|
+
contentActionsLambda.addEnvironment(
|
|
409
|
+
"PAPYRUS_GRAPHQL_ENDPOINT",
|
|
410
|
+
backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
|
|
411
|
+
);
|
|
412
|
+
contentActionsLambda.addToRolePolicy(
|
|
413
|
+
new PolicyStatement({
|
|
414
|
+
actions: ["appsync:GraphQL"],
|
|
415
|
+
resources: ["*"],
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
419
|
+
reader: site.reader,
|
|
420
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
421
|
+
cmsAppId: amplifyAppId,
|
|
422
|
+
region: Stack.of(contentActionsLambda).region,
|
|
423
|
+
account: Stack.of(contentActionsLambda).account,
|
|
424
|
+
});
|
|
425
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
426
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
427
|
+
}
|
|
428
|
+
if (rebuildSettings.resources.length > 0) {
|
|
429
|
+
contentActionsLambda.addToRolePolicy(
|
|
430
|
+
new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
434
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
435
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
|
|
436
|
+
}
|
|
402
437
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
403
438
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
404
439
|
if (backend.sesInboundReceive) {
|
|
@@ -11,6 +11,9 @@ export const storage = defineStorage({
|
|
|
11
11
|
"corpora/*": [
|
|
12
12
|
allow.groups(["editor", "admin"]).to(["read"]),
|
|
13
13
|
],
|
|
14
|
+
"preview/*": [
|
|
15
|
+
allow.groups(["editor", "admin"]).to(["read"]),
|
|
16
|
+
],
|
|
14
17
|
"newsroom/*": [
|
|
15
18
|
allow.groups(["editor", "admin"]).to(["read", "write", "delete"]),
|
|
16
19
|
],
|
|
@@ -12,6 +12,9 @@ export const storage = defineStorage({
|
|
|
12
12
|
"corpora/*": [
|
|
13
13
|
allow.groups(["editor", "admin"]).to(["read"]),
|
|
14
14
|
],
|
|
15
|
+
"preview/*": [
|
|
16
|
+
allow.groups(["editor", "admin"]).to(["read"]),
|
|
17
|
+
],
|
|
15
18
|
"newsroom/*": [
|
|
16
19
|
allow.groups(["editor", "admin"]).to(["read", "write", "delete"]),
|
|
17
20
|
],
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { streamPreviewObject } from "../../../lib/staging-preview-object";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET(request: NextRequest): Promise<Response> {
|
|
7
|
+
return streamPreviewObject(request.nextUrl.pathname);
|
|
8
|
+
}
|
|
@@ -52,8 +52,8 @@ export default async function DateScopedArticlePage({ params }: DateScopedArticl
|
|
|
52
52
|
editionFooter={{
|
|
53
53
|
editionBasePath,
|
|
54
54
|
entries: buildPresentationFooterEntries(content),
|
|
55
|
-
subtitle: SITE_BRAND.
|
|
56
|
-
title: SITE_BRAND.
|
|
55
|
+
subtitle: SITE_BRAND.footerSubtitleOverride ?? (content.description?.trim() || "Inside Papyrus"),
|
|
56
|
+
title: SITE_BRAND.footerTitle,
|
|
57
57
|
}}
|
|
58
58
|
item={item}
|
|
59
59
|
backHref={`${editionBasePath}#${item.slug}`}
|
package/app/globals.css
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
@import "
|
|
2
|
-
@import "../publications/papyrus/theme.css";
|
|
3
|
-
@import "../publications/pilobol_us/theme.css";
|
|
4
|
-
@import "../publications/anth_us/theme.css";
|
|
1
|
+
@import "./papyrus-theme.css";
|
|
5
2
|
|
|
6
3
|
@import "tailwindcss";
|
|
7
4
|
@import "tw-animate-css";
|
|
@@ -2285,6 +2282,25 @@ img {
|
|
|
2285
2282
|
width: 100%;
|
|
2286
2283
|
}
|
|
2287
2284
|
|
|
2285
|
+
.article-video__preview-frame {
|
|
2286
|
+
aspect-ratio: 16 / 9;
|
|
2287
|
+
background: #000;
|
|
2288
|
+
border: 0;
|
|
2289
|
+
display: block;
|
|
2290
|
+
width: 100%;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
.article-video__preview-controls {
|
|
2294
|
+
align-items: center;
|
|
2295
|
+
display: flex;
|
|
2296
|
+
gap: 0.75rem;
|
|
2297
|
+
margin-top: 0.5rem;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
.article-video__preview-controls input[type="range"] {
|
|
2301
|
+
flex: 1;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2288
2304
|
.article-video__credit {
|
|
2289
2305
|
color: rgb(var(--ink-solid-rgb) / 0.62);
|
|
2290
2306
|
font-size: 0.82rem;
|
package/app/layout.tsx
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import { Playfair_Display, Plus_Jakarta_Sans } from "next/font/google";
|
|
3
|
-
import { cookies } from "next/headers";
|
|
4
3
|
import Script from "next/script";
|
|
5
4
|
import { AmplifyClientProvider } from "../components/amplify-client-provider";
|
|
6
5
|
import { PapyrusConsoleShell } from "../components/papyrus-console-shell";
|
|
7
6
|
import { TooltipProvider } from "../components/ui/tooltip";
|
|
8
|
-
import { getPresentationChoices, getSiteBrand
|
|
7
|
+
import { getPresentationChoices, getSiteBrand } from "../lib/site-brand";
|
|
9
8
|
import { getSiteStack } from "../lib/site-stack";
|
|
9
|
+
import { isIndexable, showStagingBanner } from "../lib/site-env";
|
|
10
|
+
import { StagingBanner } from "../components/staging-banner";
|
|
10
11
|
import "./tailwind.css";
|
|
11
12
|
import "./globals.css";
|
|
13
|
+
import "papyrus-site-theme";
|
|
12
14
|
|
|
13
15
|
const playfairDisplay = Playfair_Display({
|
|
14
16
|
subsets: ["latin"],
|
|
@@ -22,19 +24,12 @@ const plusJakartaSans = Plus_Jakarta_Sans({
|
|
|
22
24
|
|
|
23
25
|
const faviconVersion = "20260517-1";
|
|
24
26
|
const defaultTheme = "system";
|
|
25
|
-
const BRAND_OVERRIDE_COOKIE = "papyrus-site-brand-override";
|
|
26
|
-
|
|
27
|
-
async function resolveLayoutSiteBrand() {
|
|
28
|
-
const cookieStore = await cookies();
|
|
29
|
-
const brandId = resolveRuntimeSiteBrandId(cookieStore.get(BRAND_OVERRIDE_COOKIE)?.value ?? null);
|
|
30
|
-
return getSiteBrand(brandId);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
27
|
export async function generateMetadata(): Promise<Metadata> {
|
|
34
|
-
const siteBrand =
|
|
28
|
+
const siteBrand = getSiteBrand();
|
|
35
29
|
return {
|
|
36
30
|
title: siteBrand.appTitle,
|
|
37
31
|
description: siteBrand.appDescription,
|
|
32
|
+
...(isIndexable() ? {} : { robots: { index: false, follow: false } }),
|
|
38
33
|
icons: {
|
|
39
34
|
icon: [
|
|
40
35
|
{ url: `/icon-light.png?v=${faviconVersion}`, type: "image/png" },
|
|
@@ -45,7 +40,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
48
|
-
const siteBrand =
|
|
43
|
+
const siteBrand = getSiteBrand();
|
|
49
44
|
const presentationChoices = getPresentationChoices(siteBrand);
|
|
50
45
|
const siteStack = getSiteStack(siteBrand);
|
|
51
46
|
|
|
@@ -135,6 +130,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
|
|
|
135
130
|
window.addEventListener("papyrus:settings-changed", apply);
|
|
136
131
|
})();`}
|
|
137
132
|
</Script>
|
|
133
|
+
{showStagingBanner() ? <StagingBanner /> : null}
|
|
138
134
|
<AmplifyClientProvider>
|
|
139
135
|
<TooltipProvider>
|
|
140
136
|
<PapyrusConsoleShell>{children}</PapyrusConsoleShell>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NewsroomArticleEditor } from "../../../../components/newsroom-article-editor";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
|
|
5
|
+
type NewsroomArticleEditorPageProps = {
|
|
6
|
+
params: Promise<{ id: string }>;
|
|
7
|
+
searchParams?: Promise<{ demo?: string | string[] }>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default async function NewsroomArticleEditorPage({ params, searchParams }: NewsroomArticleEditorPageProps) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const resolved = await searchParams;
|
|
13
|
+
const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
|
|
14
|
+
return <NewsroomArticleEditor articleId={decodeURIComponent(id)} demo={demoValue === "1"} />;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NewsroomArticlesView } from "../../../components/newsroom-articles-view";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
|
|
5
|
+
type NewsroomArticlesPageProps = {
|
|
6
|
+
searchParams?: Promise<{ demo?: string | string[] }>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default async function NewsroomArticlesPage({ searchParams }: NewsroomArticlesPageProps) {
|
|
10
|
+
const resolved = await searchParams;
|
|
11
|
+
const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
|
|
12
|
+
return <NewsroomArticlesView demo={demoValue === "1"} />;
|
|
13
|
+
}
|
package/app/robots.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { isIndexable } from "../lib/site-env";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export default function robots(): MetadataRoute.Robots {
|
|
7
|
+
if (isIndexable()) {
|
|
8
|
+
return { rules: { userAgent: "*", allow: "/" } };
|
|
9
|
+
}
|
|
10
|
+
return { rules: { userAgent: "*", disallow: "/" } };
|
|
11
|
+
}
|
package/bin/papyrus-app.mjs
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { sync } from "../src/sync.mjs";
|
|
3
3
|
|
|
4
|
-
const [command] = process.argv.slice(2);
|
|
4
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
5
5
|
if (command === "sync") {
|
|
6
6
|
sync();
|
|
7
|
+
} else if (command === "videoml-bundle") {
|
|
8
|
+
const preview = rest.includes("--preview");
|
|
9
|
+
const { buildBrowserBundle } = await import("../scripts/videoml/build-browser-bundle.mjs");
|
|
10
|
+
const { buildPreviewBundle } = await import("../scripts/videoml/build-preview-bundle.mjs");
|
|
11
|
+
try {
|
|
12
|
+
await (preview ? buildPreviewBundle() : buildBrowserBundle());
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error(error.message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
7
17
|
} else {
|
|
8
|
-
console.error("Usage: papyrus-app sync");
|
|
18
|
+
console.error("Usage: papyrus-app sync | papyrus-app videoml-bundle [--preview]");
|
|
9
19
|
process.exit(command ? 1 : 0);
|
|
10
20
|
}
|
package/bin/papyrus-infra.mjs
CHANGED
|
@@ -1,39 +1,95 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* papyrus-infra synth --site infra/site.json [--out cdk.out]
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]
|
|
4
|
+
* papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]
|
|
5
|
+
* papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader
|
|
6
|
+
* Synthesizes the app-shell stack for a publication from its own site.json, or
|
|
7
|
+
* prints one of the generated Amplify build specs. Synth only: deploying is a
|
|
8
|
+
* separate, reviewed step.
|
|
6
9
|
*/
|
|
7
10
|
import fs from "node:fs";
|
|
8
11
|
import path from "node:path";
|
|
9
12
|
import { createRequire } from "node:module";
|
|
13
|
+
import { buildSpecFor } from "../infra/build-specs.js";
|
|
14
|
+
import { parseSiteConfig, resolveStackName } from "../infra/site-config.js";
|
|
15
|
+
|
|
16
|
+
const USAGE = [
|
|
17
|
+
"Usage: papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]",
|
|
18
|
+
" papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]",
|
|
19
|
+
" papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader",
|
|
20
|
+
].join("\n");
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const opt = (name, fallback) => {
|
|
24
|
+
const i = args.indexOf(`--${name}`);
|
|
25
|
+
return i >= 0 ? args[i + 1] : fallback;
|
|
26
|
+
};
|
|
27
|
+
const command = args[0];
|
|
28
|
+
if (command !== "synth" && command !== "print-buildspec") {
|
|
29
|
+
console.error(USAGE);
|
|
30
|
+
process.exit(command ? 1 : 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const accountStack = opt("account-stack");
|
|
34
|
+
if (accountStack !== undefined) {
|
|
35
|
+
if (command !== "synth" || accountStack !== "github-oidc") {
|
|
36
|
+
console.error(USAGE);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
let GithubOidcProviderStack;
|
|
40
|
+
try {
|
|
41
|
+
({ GithubOidcProviderStack } = await import("../infra/index.js"));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib");
|
|
47
|
+
const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
|
|
48
|
+
new GithubOidcProviderStack(app, "GithubOidcProviderStack", {
|
|
49
|
+
env: { account: opt("account", "335163751677"), region: opt("region", "us-east-1") },
|
|
50
|
+
stackName: "github-oidc-provider",
|
|
51
|
+
});
|
|
52
|
+
app.synth();
|
|
53
|
+
console.log(`synthesized github-oidc-provider -> ${path.resolve(opt("out", "cdk.out"))}`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let config;
|
|
58
|
+
try {
|
|
59
|
+
config = parseSiteConfig(JSON.parse(fs.readFileSync(path.resolve(opt("site", "infra/site.json")), "utf8")));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (command === "print-buildspec") {
|
|
66
|
+
const app = opt("app");
|
|
67
|
+
if (!["cms-production", "cms-staging", "reader"].includes(app)) {
|
|
68
|
+
console.error(USAGE);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
process.stdout.write(buildSpecFor(config, app));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
10
79
|
|
|
11
80
|
let AmplifyAppShellStack;
|
|
12
81
|
try {
|
|
13
82
|
({ AmplifyAppShellStack } = await import("../infra/index.js"));
|
|
14
83
|
} catch (error) {
|
|
15
|
-
// infra/index.js explains the missing optional peers; print it without a stack trace.
|
|
16
84
|
console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
|
|
17
85
|
process.exit(1);
|
|
18
86
|
}
|
|
19
87
|
|
|
20
|
-
// aws-cdk-lib is an optional peer: resolve it from the publication's infra/ package (cwd).
|
|
21
88
|
const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib");
|
|
22
|
-
|
|
23
|
-
const args = process.argv.slice(2);
|
|
24
|
-
const opt = (name, fallback) => {
|
|
25
|
-
const i = args.indexOf(`--${name}`);
|
|
26
|
-
return i >= 0 ? args[i + 1] : fallback;
|
|
27
|
-
};
|
|
28
|
-
if (args[0] !== "synth") {
|
|
29
|
-
console.error("Usage: papyrus-infra synth --site infra/site.json [--out cdk.out]");
|
|
30
|
-
process.exit(args[0] ? 1 : 0);
|
|
31
|
-
}
|
|
32
|
-
const config = JSON.parse(fs.readFileSync(path.resolve(opt("site", "infra/site.json")), "utf8"));
|
|
33
89
|
const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
|
|
34
90
|
new AmplifyAppShellStack(app, `AmplifyAppShell-${config.siteId}`, config, {
|
|
35
91
|
env: { account: opt("account", "335163751677"), region: opt("region", "us-east-1") },
|
|
36
|
-
stackName:
|
|
92
|
+
stackName: resolveStackName(config),
|
|
37
93
|
});
|
|
38
94
|
app.synth();
|
|
39
|
-
console.log(`synthesized
|
|
95
|
+
console.log(`synthesized ${resolveStackName(config)} -> ${path.resolve(opt("out", "cdk.out"))}`);
|
|
@@ -1,31 +1,140 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import type { ArticleVideoAsset } from "../lib/articles";
|
|
5
5
|
import { resolveThemedVideoSrc } from "../lib/themed-image";
|
|
6
6
|
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
7
|
+
import { getSiteBrand } from "../lib/site-brand";
|
|
8
|
+
import { normalizeDevPreviewDsl, type VideoScriptRef } from "../lib/video-script";
|
|
9
|
+
import { resolveVideoMode, resolveVideoModeFromEnv, type VideoMode } from "../lib/video-mode";
|
|
7
10
|
|
|
8
11
|
type ArticleVideoFigureProps = {
|
|
9
12
|
video: ArticleVideoAsset;
|
|
10
13
|
slug: string;
|
|
11
14
|
figureClassName?: string;
|
|
12
15
|
priority?: boolean;
|
|
16
|
+
videoScript?: VideoScriptRef | null;
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
const PREVIEW_STORAGE_PREFIX = "papyrus-video-preview:";
|
|
20
|
+
|
|
21
|
+
export function ArticleVideoFigure({
|
|
22
|
+
video,
|
|
23
|
+
slug,
|
|
24
|
+
figureClassName = "article-photo article-video",
|
|
25
|
+
videoScript = null,
|
|
26
|
+
}: ArticleVideoFigureProps) {
|
|
16
27
|
const theme = useResolvedPapyrusTheme();
|
|
17
28
|
const [hasHydrated, setHasHydrated] = useState(false);
|
|
29
|
+
const [videoMode, setVideoMode] = useState<VideoMode>(() => resolveVideoModeFromEnv());
|
|
30
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
31
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
32
|
+
const [duration, setDuration] = useState(0);
|
|
33
|
+
const [previewReady, setPreviewReady] = useState(false);
|
|
34
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
18
35
|
|
|
19
36
|
useEffect(() => {
|
|
20
37
|
setHasHydrated(true);
|
|
38
|
+
setVideoMode(resolveVideoMode());
|
|
21
39
|
}, []);
|
|
22
40
|
|
|
23
41
|
// Match SSR to the default dark MP4 in `video.src` until after hydration.
|
|
24
42
|
const resolvedTheme = hasHydrated ? theme : "dark";
|
|
25
43
|
const src = resolveThemedVideoSrc(video.src, video.themeVariants, resolvedTheme);
|
|
44
|
+
const usePreview = videoMode === "preview" && Boolean(videoScript?.dsl);
|
|
45
|
+
const previewDsl = videoScript?.dsl
|
|
46
|
+
? normalizeDevPreviewDsl(videoScript.dsl, getSiteBrand().video?.sceneComponents)
|
|
47
|
+
: null;
|
|
48
|
+
const previewSrc = `/videoml/preview.html?target=${encodeURIComponent(slug)}&theme=${resolvedTheme}${
|
|
49
|
+
src ? `&audio=${encodeURIComponent(src)}` : ""
|
|
50
|
+
}`;
|
|
51
|
+
|
|
52
|
+
if (usePreview && previewDsl && typeof window !== "undefined") {
|
|
53
|
+
sessionStorage.setItem(`${PREVIEW_STORAGE_PREFIX}${slug}`, previewDsl);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setPreviewReady(false);
|
|
58
|
+
setIsPlaying(false);
|
|
59
|
+
setCurrentTime(0);
|
|
60
|
+
setDuration(0);
|
|
61
|
+
}, [previewSrc, usePreview]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!usePreview || !previewReady) return;
|
|
65
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
66
|
+
{ kind: "papyrus-video-refresh", target: slug, theme: resolvedTheme, audio: src || undefined },
|
|
67
|
+
"*",
|
|
68
|
+
);
|
|
69
|
+
}, [previewReady, resolvedTheme, slug, src, usePreview]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!usePreview) return;
|
|
73
|
+
const onMessage = (event: MessageEvent) => {
|
|
74
|
+
const payload = event.data;
|
|
75
|
+
if (!payload || typeof payload !== "object" || payload.kind !== "papyrus-video-time") return;
|
|
76
|
+
if (typeof payload.currentTime === "number") setCurrentTime(payload.currentTime);
|
|
77
|
+
if (typeof payload.duration === "number") setDuration(payload.duration);
|
|
78
|
+
};
|
|
79
|
+
window.addEventListener("message", onMessage);
|
|
80
|
+
return () => window.removeEventListener("message", onMessage);
|
|
81
|
+
}, [usePreview]);
|
|
82
|
+
|
|
83
|
+
const togglePreviewPlayback = useCallback(() => {
|
|
84
|
+
iframeRef.current?.contentWindow?.postMessage({ kind: isPlaying ? "papyrus-video-pause" : "papyrus-video-play" }, "*");
|
|
85
|
+
setIsPlaying(!isPlaying);
|
|
86
|
+
}, [isPlaying]);
|
|
87
|
+
|
|
88
|
+
const seekPreview = useCallback((nextTime: number) => {
|
|
89
|
+
const clamped = Math.max(0, Number.isFinite(nextTime) ? nextTime : 0);
|
|
90
|
+
iframeRef.current?.contentWindow?.postMessage({ kind: "papyrus-video-seek", time: clamped }, "*");
|
|
91
|
+
setCurrentTime(clamped);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
if (usePreview) {
|
|
95
|
+
return (
|
|
96
|
+
<figure
|
|
97
|
+
className={`${figureClassName} article-video--preview`}
|
|
98
|
+
data-media-type="videoml-preview"
|
|
99
|
+
data-video-theme={resolvedTheme}
|
|
100
|
+
data-video-mode={videoMode}
|
|
101
|
+
>
|
|
102
|
+
{hasHydrated ? (
|
|
103
|
+
<iframe
|
|
104
|
+
key={`${slug}-${resolvedTheme}-${src}`}
|
|
105
|
+
ref={iframeRef}
|
|
106
|
+
className="article-video__preview-frame"
|
|
107
|
+
src={previewSrc}
|
|
108
|
+
title={video.alt}
|
|
109
|
+
onLoad={() => setPreviewReady(true)}
|
|
110
|
+
/>
|
|
111
|
+
) : null}
|
|
112
|
+
<div className="article-video__preview-controls">
|
|
113
|
+
<button type="button" onClick={togglePreviewPlayback} aria-pressed={isPlaying}>
|
|
114
|
+
{isPlaying ? "Pause video" : "Play video"}
|
|
115
|
+
</button>
|
|
116
|
+
<input
|
|
117
|
+
type="range"
|
|
118
|
+
min={0}
|
|
119
|
+
max={duration || 0}
|
|
120
|
+
step={0.1}
|
|
121
|
+
value={Math.min(currentTime, duration || 0)}
|
|
122
|
+
onChange={(event) => seekPreview(Number(event.target.value))}
|
|
123
|
+
aria-label="Seek video"
|
|
124
|
+
disabled={!duration}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
{video.caption ? <figcaption>{video.caption}</figcaption> : null}
|
|
128
|
+
{video.credit ? <p className="article-video__credit">{video.credit}</p> : null}
|
|
129
|
+
<span className="sr-only" data-video-slug={slug}>
|
|
130
|
+
{video.alt}
|
|
131
|
+
</span>
|
|
132
|
+
</figure>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
26
135
|
|
|
27
136
|
return (
|
|
28
|
-
<figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme}>
|
|
137
|
+
<figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme} data-video-mode={videoMode}>
|
|
29
138
|
<video
|
|
30
139
|
controls
|
|
31
140
|
playsInline
|