@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/amplify/auth/resource.js +17 -4
  2. package/amplify/auth/resource.ts +21 -4
  3. package/amplify/data/resource.js +52 -70
  4. package/amplify/data/resource.ts +57 -70
  5. package/amplify/data/schema.js +52 -58
  6. package/amplify/data/schema.ts +57 -58
  7. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  8. package/amplify/functions/console-chat-responder/resource.js +0 -7
  9. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  10. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  11. package/amplify/functions/content-actions/handler.py +149 -0
  12. package/amplify/functions/content-actions/requirements.txt +4 -0
  13. package/amplify/functions/content-actions/resource.js +40 -0
  14. package/amplify/functions/content-actions/resource.ts +54 -0
  15. package/amplify/functions/shared/python-bundle.js +17 -2
  16. package/amplify/functions/shared/python-bundle.ts +23 -2
  17. package/amplify/rebuild-trigger-settings.js +22 -0
  18. package/amplify/rebuild-trigger-settings.ts +42 -0
  19. package/amplify/site-backend-config.ts +12 -1
  20. package/amplify/site-backend.js +51 -27
  21. package/amplify/site-backend.ts +66 -34
  22. package/amplify/storage/resource.js +3 -0
  23. package/amplify/storage/resource.ts +3 -0
  24. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  25. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +4 -2
  26. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  27. package/app/api/revalidate/route.ts +8 -7
  28. package/app/articles/[slug]/page.tsx +6 -4
  29. package/app/blog-rhythm.css +278 -0
  30. package/app/globals.css +22 -4
  31. package/app/layout.tsx +24 -12
  32. package/app/newsroom/articles/[id]/page.tsx +15 -0
  33. package/app/newsroom/articles/page.tsx +13 -0
  34. package/app/page.tsx +12 -4
  35. package/app/robots.ts +11 -0
  36. package/bin/papyrus-app.mjs +12 -2
  37. package/bin/papyrus-infra.mjs +74 -18
  38. package/components/archive-shell.tsx +2 -22
  39. package/components/article-video.tsx +247 -3
  40. package/components/blog-page-background.tsx +23 -6
  41. package/components/markus-ir-preview.tsx +300 -0
  42. package/components/news-desk-page.tsx +1 -4
  43. package/components/newspaper.tsx +6 -2
  44. package/components/newsroom-access-gate.tsx +66 -0
  45. package/components/newsroom-article-editor.tsx +741 -0
  46. package/components/newsroom-articles-view.tsx +129 -0
  47. package/components/newsroom-ops-overview.tsx +28 -11
  48. package/components/newsroom-ops-shell.tsx +3 -1
  49. package/components/pictogram-figure.tsx +9 -2
  50. package/components/presentation-header.tsx +135 -0
  51. package/components/presentation-rhythm-hrule.tsx +17 -0
  52. package/components/settings-page.tsx +2 -1
  53. package/components/staging-banner.tsx +11 -0
  54. package/components/topic-steering-workspace.tsx +1 -59
  55. package/components/use-blog-header-obstacles.ts +81 -0
  56. package/components/use-brand-fonts-ready.ts +28 -0
  57. package/components/use-rhythm-overlay.ts +29 -0
  58. package/infra/amplify-app-shell.js +285 -80
  59. package/infra/amplify-app-shell.ts +326 -113
  60. package/infra/build-specs.js +94 -0
  61. package/infra/build-specs.ts +141 -0
  62. package/infra/github-oidc-provider.js +24 -0
  63. package/infra/github-oidc-provider.ts +30 -0
  64. package/infra/index.js +3 -0
  65. package/infra/site-config.js +273 -0
  66. package/infra/site-config.ts +336 -0
  67. package/lib/blog-feature-solver.ts +459 -0
  68. package/lib/blog-rhythm.ts +134 -0
  69. package/lib/brand-fonts-watch.ts +43 -0
  70. package/lib/brand-fonts.ts +112 -0
  71. package/lib/cached-content-repository.ts +14 -1
  72. package/lib/category-repository.ts +1 -93
  73. package/lib/content-actions-client.ts +98 -0
  74. package/lib/content-actions-demo.ts +190 -0
  75. package/lib/content-repository.ts +6 -0
  76. package/lib/content-source-context.ts +26 -0
  77. package/lib/content-types.ts +4 -0
  78. package/lib/edition-routes.ts +31 -29
  79. package/lib/empty-theme.css +1 -0
  80. package/lib/graphql-content-repository.ts +239 -43
  81. package/lib/layout-scenarios.ts +8 -8
  82. package/lib/newspaper-layout.ts +3 -2
  83. package/lib/newsroom-articles.ts +325 -0
  84. package/lib/newsroom-demo-articles.ts +90 -0
  85. package/lib/newsroom-media-backend.ts +142 -0
  86. package/lib/newsroom-media.ts +182 -0
  87. package/lib/newsroom-nav.ts +3 -0
  88. package/lib/papyrus-web-locations.ts +7 -6
  89. package/lib/presentation-footer.ts +3 -2
  90. package/lib/pretext-layout.ts +12 -3
  91. package/lib/reader-base-path.ts +24 -0
  92. package/lib/reader-static-params.ts +8 -0
  93. package/lib/renderer.ts +7 -0
  94. package/lib/revalidate-secret.ts +59 -0
  95. package/lib/root-route-redirect.ts +16 -0
  96. package/lib/site-brand-tenant.ts +3 -22
  97. package/lib/site-brand.ts +95 -42
  98. package/lib/site-env.ts +52 -0
  99. package/lib/staging-gate.ts +37 -0
  100. package/lib/staging-gated-path.ts +22 -0
  101. package/lib/staging-preview-object.ts +98 -0
  102. package/lib/video-mode.ts +42 -0
  103. package/lib/video-script.ts +77 -0
  104. package/middleware.ts +56 -42
  105. package/package.json +40 -1
  106. package/renderers/pretext/article-page.tsx +40 -9
  107. package/renderers/pretext/presentation-shared.tsx +113 -0
  108. package/renderers/pretext/presentation-shell.tsx +18 -112
  109. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  110. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  111. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  112. package/routes.manifest.json +37 -2
  113. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  114. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  115. package/scripts/videoml/bundle-common.mjs +129 -0
  116. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  117. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  118. package/scripts/videoml/preview/preview-timing.ts +42 -0
  119. package/scripts/videoml/preview.html +77 -0
  120. package/scripts/videoml/standard-entry.tsx +46 -0
  121. package/src/sync.mjs +30 -6
  122. package/src/with-papyrus.mjs +15 -2
  123. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  124. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  125. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  126. package/publications/anth_us/brand.ts +0 -68
  127. package/publications/anth_us/theme.css +0 -66
  128. package/publications/pilobol_us/brand.ts +0 -37
  129. package/publications/pilobol_us/theme.css +0 -140
  130. package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
  131. package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
  132. package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
  133. package/publications/threat_intelligence/brand.ts +0 -40
  134. package/publications/threat_intelligence/pictograms/art.tsx +0 -608
  135. package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
  136. package/publications/threat_intelligence/pictograms/registry.ts +0 -30
  137. package/publications/threat_intelligence/pictograms/system.tsx +0 -305
  138. package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
  139. package/publications/threat_intelligence/theme.css +0 -1440
  140. /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
@@ -8,17 +8,18 @@ import * as route53 from "aws-cdk-lib/aws-route53";
8
8
  import * as ses from "aws-cdk-lib/aws-ses";
9
9
  import * as sesActions from "aws-cdk-lib/aws-ses-actions";
10
10
  import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
11
- import { CfnEventSourceMapping, CfnFunction, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
11
+ import { CfnEventSourceMapping, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
12
12
  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 { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
16
+ import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
17
+ import { applyCognitoDomainPrefix, 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
- import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
22
+ import { contentActions } from "./functions/content-actions/resource";
22
23
  import { knowledgeQuery } from "./functions/knowledge-query/resource";
23
24
  import { manageUserRole } from "./functions/manage-user-role/resource";
24
25
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
@@ -86,12 +87,13 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
86
87
  const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
87
88
  const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
88
89
 
90
+ const authConfig = site.auth ?? authConfigFromEnv();
89
91
  const backend = defineBackend({
90
92
  assignmentAction,
91
- auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
93
+ auth: defineSiteAuth(authConfig),
92
94
  categoryAction,
95
+ contentActions,
93
96
  data,
94
- graphqlJwtAuthorizer,
95
97
  knowledgeQuery,
96
98
  manageUserRole,
97
99
  modelAttachmentUpload,
@@ -105,6 +107,15 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
105
107
  storage,
106
108
  });
107
109
 
110
+ const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
111
+ if (
112
+ authConfig.applyCognitoDomainPrefix === true
113
+ && requestedCognitoDomainPrefix !== ""
114
+ && authConfig.disableGoogleOAuth !== true
115
+ ) {
116
+ applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
117
+ }
118
+
108
119
  const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
109
120
  // Repo root in a Papyrus checkout; package root when installed from npm.
110
121
  const projectRoot = resolve(amplifyBackendDir, "..");
@@ -157,43 +168,19 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
157
168
  const messageThreadTable = backend.data.resources.tables.MessageThread;
158
169
  const dataStack = Stack.of(messageTable);
159
170
  const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
160
- const jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild as CfnFunction | undefined;
161
- const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
162
- const jwtAuthorizerVariables =
163
- jwtAuthorizerEnvironment
164
- && typeof jwtAuthorizerEnvironment === "object"
165
- && "variables" in jwtAuthorizerEnvironment
166
- ? (jwtAuthorizerEnvironment as { variables?: Record<string, string> }).variables
167
- : undefined;
168
- const jwtSsmEnvConfig =
169
- process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
170
- || jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
171
- || "";
171
+ const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
172
172
 
173
173
  if (enableConsoleResponder) {
174
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
174
+ new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
175
175
  messageTable,
176
176
  messageStreamArn,
177
177
  threadTable: messageThreadTable,
178
178
  projectRoot,
179
179
  graphqlEndpoint,
180
- amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
181
180
  responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
182
181
  model: process.env.PAPYRUS_CONSOLE_MODEL,
183
182
  prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
184
183
  });
185
- if (jwtSsmEnvConfig) {
186
- consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
187
- }
188
- const jwtSecretSsmParam =
189
- process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
190
- || (productionAppId !== "" && amplifyAppId === productionAppId
191
- ? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
192
- : "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
193
- consoleChatResponder.responderFunction.addEnvironment(
194
- "PAPYRUS_JWT_SECRET_SSM_PARAM",
195
- jwtSecretSsmParam,
196
- );
197
184
  }
198
185
 
199
186
  if (enableSlackAgent) {
@@ -214,9 +201,9 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
214
201
  "PAPYRUS_SLACK_ALLOWED_USER_IDS",
215
202
  (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim(),
216
203
  );
217
- if (jwtSsmEnvConfig) {
218
- backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
219
- backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
204
+ if (amplifySsmEnvConfig) {
205
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
206
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
220
207
  }
221
208
  backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
222
209
  backend.slackDelivery.addEnvironment(
@@ -386,6 +373,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
386
373
  for (const resource of [
387
374
  backend.assignmentAction,
388
375
  backend.categoryAction,
376
+ backend.contentActions,
389
377
  backend.emailSubmissionProcessor,
390
378
  backend.knowledgeQuery,
391
379
  backend.modelAttachmentUpload,
@@ -399,6 +387,50 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
399
387
  grantNewsroomReadWrite(backend.assignmentAction.resources.lambda as LambdaFunction);
400
388
  grantNewsroomReadWrite(backend.categoryAction.resources.lambda as LambdaFunction);
401
389
  grantNewsroomReadWrite(backend.newsroomSummary.resources.lambda as LambdaFunction);
390
+ const contentActionsLambda = backend.contentActions.resources.lambda as LambdaFunction;
391
+ grantNewsroomReadWriteDelete(contentActionsLambda);
392
+ contentActionsLambda.addEnvironment(
393
+ "PAPYRUS_GRAPHQL_ENDPOINT",
394
+ backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
395
+ );
396
+ contentActionsLambda.addToRolePolicy(
397
+ new PolicyStatement({
398
+ actions: ["appsync:GraphQL"],
399
+ resources: ["*"],
400
+ }),
401
+ );
402
+ const rebuildSettings = rebuildTriggerSettings({
403
+ reader: site.reader,
404
+ stagingBuildEnabled: site.stagingBuild?.enabled === true,
405
+ cmsAppId: amplifyAppId,
406
+ region: Stack.of(contentActionsLambda).region,
407
+ account: Stack.of(contentActionsLambda).account,
408
+ });
409
+ for (const [name, value] of Object.entries(rebuildSettings.environment)) {
410
+ contentActionsLambda.addEnvironment(name, value);
411
+ }
412
+ if (rebuildSettings.resources.length > 0) {
413
+ contentActionsLambda.addToRolePolicy(
414
+ new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
415
+ );
416
+ }
417
+ if ((site.revalidateBaseUrl ?? "").trim() !== "") {
418
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
419
+ const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
420
+ if (!revalidateSecretParameter.startsWith("/")) {
421
+ throw new Error(
422
+ "revalidateBaseUrl is set but PAPYRUS_REVALIDATE_SECRET_PARAMETER is not (an SSM parameter name starting with /). The app-shell template sets it on the production branch for Pretext sites; see docs/site-hosting.md.",
423
+ );
424
+ }
425
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
426
+ const revalidateStack = Stack.of(contentActionsLambda);
427
+ contentActionsLambda.addToRolePolicy(
428
+ new PolicyStatement({
429
+ actions: ["ssm:GetParameter"],
430
+ resources: [`arn:${revalidateStack.partition}:ssm:${revalidateStack.region}:${revalidateStack.account}:parameter${revalidateSecretParameter}`],
431
+ }),
432
+ );
433
+ }
402
434
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
403
435
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
404
436
  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,10 +52,12 @@ export default async function DateScopedArticlePage({ params }: DateScopedArticl
52
52
  editionFooter={{
53
53
  editionBasePath,
54
54
  entries: buildPresentationFooterEntries(content),
55
- subtitle: SITE_BRAND.id === "threat-intelligence" ? "" : (content.description?.trim() || "Inside Papyrus"),
56
- title: SITE_BRAND.id === "threat-intelligence" ? "ANTHUS THREAT INTELLIGENCE" : undefined,
55
+ sections: content.sections.map((section) => ({ key: section.key, label: section.label })),
56
+ subtitle: SITE_BRAND.footerSubtitleOverride ?? (content.description?.trim() || "Inside Papyrus"),
57
+ title: SITE_BRAND.footerTitle,
57
58
  }}
58
59
  item={item}
60
+ videoScript={content.videoScripts?.[item.slug] ?? null}
59
61
  backHref={`${editionBasePath}#${item.slug}`}
60
62
  />
61
63
  );
@@ -1,4 +1,5 @@
1
1
  import { notFound, redirect } from "next/navigation";
2
+ import { siteServesNextReaderRoutes } from "../../../../lib/reader-static-params";
2
3
  import { getSiteRenderer } from "../../../../lib/site-renderer";
3
4
  import type { PresentationTarget } from "../../../../renderers/pretext";
4
5
  import { loadCachedEditionContent } from "../../../../lib/cached-content-repository";
@@ -22,6 +23,7 @@ export async function EditionRoutePage({
22
23
  lockedPresentation,
23
24
  target = { kind: "edition" },
24
25
  }: EditionRoutePageProps) {
26
+ if (!siteServesNextReaderRoutes()) notFound();
25
27
  const route = parseEditionDateRoute({ year, month, day });
26
28
  if (!route) notFound();
27
29
  if (!route.isCanonical) redirect(route.canonicalPath);
@@ -1,3 +1,4 @@
1
+ import { toPublicReaderPath } from "../../../lib/reader-base-path";
1
2
  import { revalidatePath, revalidateTag } from "next/cache";
2
3
  import { NextResponse } from "next/server";
3
4
  import {
@@ -8,6 +9,7 @@ import {
8
9
  editionContentCacheTag,
9
10
  editionItemCacheTag,
10
11
  } from "../../../lib/cached-content-repository";
12
+ import { isAuthorizedRevalidateRequest } from "../../../lib/revalidate-secret";
11
13
  import { getEditionArticlePath, getEditionDatePath } from "../../../lib/edition-routes";
12
14
 
13
15
  export const dynamic = "force-dynamic";
@@ -21,9 +23,8 @@ type RevalidateRequestBody = {
21
23
  };
22
24
 
23
25
  export async function POST(request: Request) {
24
- const configuredSecret = process.env.PAPYRUS_REVALIDATE_SECRET?.trim();
25
- const providedSecret = request.headers.get("x-papyrus-revalidate-secret")?.trim();
26
- if (!configuredSecret || providedSecret !== configuredSecret) {
26
+ const providedSecret = request.headers.get("x-papyrus-revalidate-secret");
27
+ if (!(await isAuthorizedRevalidateRequest(providedSecret))) {
27
28
  return NextResponse.json({ ok: false, error: "Unauthorized." }, { status: 401 });
28
29
  }
29
30
 
@@ -44,8 +45,8 @@ export async function POST(request: Request) {
44
45
  if (!normalized) continue;
45
46
  revalidateTag(articleCacheTag(normalized));
46
47
  revalidatedTags.add(articleCacheTag(normalized));
47
- revalidatePath(`/articles/${encodeURIComponent(normalized)}`);
48
- revalidatedPaths.add(`/articles/${encodeURIComponent(normalized)}`);
48
+ revalidatePath(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
49
+ revalidatedPaths.add(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
49
50
  if (editionDate) {
50
51
  revalidateTag(editionItemCacheTag(editionDate, normalized));
51
52
  revalidatedTags.add(editionItemCacheTag(editionDate, normalized));
@@ -76,8 +77,8 @@ export async function POST(request: Request) {
76
77
  revalidatedTags.add(ARCHIVE_CACHE_TAG);
77
78
  revalidateTag(ARTICLES_CACHE_TAG);
78
79
  revalidatedTags.add(ARTICLES_CACHE_TAG);
79
- revalidatePath("/archive");
80
- revalidatedPaths.add("/archive");
80
+ revalidatePath(toPublicReaderPath("/archive"));
81
+ revalidatedPaths.add(toPublicReaderPath("/archive"));
81
82
 
82
83
  for (const path of body.paths ?? []) {
83
84
  const normalized = path.trim();
@@ -1,7 +1,7 @@
1
1
  import { notFound } from "next/navigation";
2
2
  import { getSiteRenderer } from "../../../lib/site-renderer";
3
- import { getCachedArticle } from "../../../lib/cached-content-repository";
4
- import { generateArticleStaticParams } from "../../../lib/reader-static-params";
3
+ import { getCachedArticle, getCachedVideoScript } from "../../../lib/cached-content-repository";
4
+ import { generateArticleStaticParams, siteServesNextReaderRoutes } from "../../../lib/reader-static-params";
5
5
  import { SITE_BRAND } from "../../../lib/site-brand";
6
6
 
7
7
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
@@ -16,6 +16,7 @@ type ArticlePageProps = {
16
16
  };
17
17
 
18
18
  export async function generateMetadata({ params }: ArticlePageProps) {
19
+ if (!siteServesNextReaderRoutes()) return {};
19
20
  const { slug } = await params;
20
21
  const article = await getCachedArticle(slug);
21
22
  if (!article) return {};
@@ -26,10 +27,11 @@ export async function generateMetadata({ params }: ArticlePageProps) {
26
27
  }
27
28
 
28
29
  export default async function ArticlePage({ params }: ArticlePageProps) {
30
+ if (!siteServesNextReaderRoutes()) notFound();
29
31
  const { slug } = await params;
30
- const article = await getCachedArticle(slug);
32
+ const [article, videoScript] = await Promise.all([getCachedArticle(slug), getCachedVideoScript(slug)]);
31
33
  if (!article) notFound();
32
34
 
33
35
  const siteRenderer = getSiteRenderer();
34
- return <siteRenderer.renderArticle article={article} backHref="/" />;
36
+ return <siteRenderer.renderArticle article={article} videoScript={videoScript} backHref="/" />;
35
37
  }
@@ -0,0 +1,278 @@
1
+ /*
2
+ Structural CSS for the rhythm blog layout (blogLayout: "rhythm").
3
+ Every rule is scoped to .blog-rhythm-shell, which only the rhythm layout
4
+ renders, so classic layouts are unaffected. The shell sets --blog-rhythm,
5
+ --blog-row-height, --blog-paint-buffer and --blog-paint-height inline.
6
+ Typography, colors and the header look belong to the publication's theme.css.
7
+ Optional tokens a publication may set:
8
+ --blog-hrule-foreground, --blog-section-rule, --blog-section-band-height,
9
+ --blog-page-padding-x, --blog-secondary-pair-min-width.
10
+ */
11
+
12
+ .blog-rhythm-shell {
13
+ --blog-hrule-foreground: var(--border);
14
+ --blog-section-rule: var(--border);
15
+ --blog-section-band-height: calc(var(--blog-row-height) * 2);
16
+ --blog-page-padding-x: calc(var(--blog-rhythm) * 3);
17
+ --paper-rhythm: var(--blog-row-height);
18
+
19
+ isolation: isolate;
20
+ position: relative;
21
+ }
22
+
23
+ .blog-rhythm-shell.presentation-page--blog {
24
+ padding: calc(var(--blog-row-height) * 4) var(--blog-page-padding-x) 0;
25
+ }
26
+
27
+ .blog-rhythm-shell > :not(.blog-page-background) {
28
+ position: relative;
29
+ z-index: 1;
30
+ }
31
+
32
+ .blog-rhythm-shell[data-rhythm-overlay="true"]::before {
33
+ background-image:
34
+ repeating-linear-gradient(
35
+ to bottom,
36
+ rgb(var(--rhythm-overlay-rule-rgb) / var(--rhythm-overlay-rule-alpha)) 0,
37
+ rgb(var(--rhythm-overlay-rule-rgb) / var(--rhythm-overlay-rule-alpha)) 1px,
38
+ transparent 1px,
39
+ transparent var(--blog-row-height)
40
+ ),
41
+ repeating-linear-gradient(
42
+ to bottom,
43
+ rgb(var(--rhythm-overlay-blue-rgb) / var(--rhythm-overlay-blue-alpha)) 0,
44
+ rgb(var(--rhythm-overlay-blue-rgb) / var(--rhythm-overlay-blue-alpha)) var(--blog-row-height),
45
+ rgb(var(--rhythm-overlay-yellow-rgb) / var(--rhythm-overlay-yellow-alpha)) var(--blog-row-height),
46
+ rgb(var(--rhythm-overlay-yellow-rgb) / var(--rhythm-overlay-yellow-alpha)) calc(var(--blog-row-height) * 2)
47
+ );
48
+ content: "";
49
+ inset: 0;
50
+ pointer-events: none;
51
+ position: absolute;
52
+ z-index: 900;
53
+ }
54
+
55
+ .blog-rhythm-shell .presentation-header {
56
+ max-width: 100%;
57
+ min-width: 0;
58
+ width: 100%;
59
+ }
60
+
61
+ .blog-rhythm-shell .presentation-section-header,
62
+ .blog-rhythm-shell .presentation-item,
63
+ .blog-rhythm-shell .presentation-item__copy {
64
+ max-width: 100%;
65
+ min-width: 0;
66
+ }
67
+
68
+ .blog-rhythm-shell .presentation-section-header {
69
+ align-items: stretch;
70
+ border-bottom: 0;
71
+ border-top: 0;
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: var(--blog-row-height);
75
+ margin: 0;
76
+ min-height: 0;
77
+ padding: calc(var(--blog-row-height) * 4) 0 calc(var(--blog-row-height) * 2);
78
+ width: 100%;
79
+ }
80
+
81
+ .blog-rhythm-shell .presentation-section-header__band {
82
+ align-items: center;
83
+ display: grid;
84
+ gap: 0 var(--blog-rhythm);
85
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
86
+ grid-template-rows: var(--blog-section-band-height);
87
+ width: 100%;
88
+ }
89
+
90
+ .blog-rhythm-shell .presentation-section-header__band::before,
91
+ .blog-rhythm-shell .presentation-section-header__band::after {
92
+ background: var(--blog-section-rule);
93
+ content: "";
94
+ grid-row: 1;
95
+ height: var(--blog-section-band-height);
96
+ }
97
+
98
+ .blog-rhythm-shell .presentation-section-header__band::before {
99
+ grid-column: 1;
100
+ }
101
+
102
+ .blog-rhythm-shell .presentation-section-header__band::after {
103
+ grid-column: 3;
104
+ }
105
+
106
+ .blog-rhythm-shell .presentation-section-header__band p {
107
+ grid-column: 2;
108
+ grid-row: 1;
109
+ height: var(--blog-section-band-height);
110
+ justify-self: center;
111
+ line-height: var(--blog-section-band-height);
112
+ margin: 0;
113
+ padding: 0 var(--blog-rhythm);
114
+ width: max-content;
115
+ }
116
+
117
+ .blog-rhythm-shell .presentation-item {
118
+ border-bottom: 0;
119
+ gap: var(--blog-row-height) calc(var(--blog-rhythm) * 4);
120
+ padding-bottom: 0;
121
+ }
122
+
123
+ .blog-rhythm-shell .presentation-item--blog {
124
+ grid-template-columns: minmax(0, 1fr);
125
+ }
126
+
127
+ .blog-rhythm-shell .presentation-rhythm-hrule {
128
+ display: grid;
129
+ grid-column: 1 / -1;
130
+ grid-template-rows: repeat(5, var(--blog-row-height));
131
+ height: calc(var(--blog-row-height) * 5);
132
+ width: 100%;
133
+ }
134
+
135
+ .blog-rhythm-shell .presentation-rhythm-hrule__line {
136
+ background: var(--blog-hrule-foreground);
137
+ grid-row: 3;
138
+ height: var(--blog-row-height);
139
+ }
140
+
141
+ @media (min-width: 1100px) {
142
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) {
143
+ column-gap: calc(var(--blog-row-height) * 2);
144
+ grid-template-columns: repeat(2, minmax(0, 1fr));
145
+ }
146
+
147
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) > .presentation-section-header,
148
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) > .presentation-item[data-item-index="0"] {
149
+ grid-column: 1 / -1;
150
+ }
151
+
152
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) > .presentation-rhythm-hrule[data-hide-in-secondary-pair="true"] {
153
+ display: none;
154
+ }
155
+ }
156
+
157
+ .blog-rhythm-shell .presentation-item[data-feature-layout][data-has-image="true"] {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 0;
161
+ grid-template-columns: minmax(0, 1fr);
162
+ }
163
+
164
+ .blog-rhythm-shell .presentation-item[data-feature-layout] .presentation-item__text-frame {
165
+ height: var(--feature-text-frame-height);
166
+ max-width: none;
167
+ overflow: hidden;
168
+ position: relative;
169
+ width: 100%;
170
+ }
171
+
172
+ .blog-rhythm-shell .presentation-item[data-feature-layout] .presentation-item__media {
173
+ height: var(--feature-media-height);
174
+ width: var(--feature-image-width);
175
+ }
176
+
177
+ .blog-rhythm-shell .presentation-item[data-feature-layout] .pictogram-figure__frame {
178
+ aspect-ratio: auto;
179
+ height: var(--feature-image-height);
180
+ width: var(--feature-image-width);
181
+ }
182
+
183
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__copy {
184
+ display: contents;
185
+ }
186
+
187
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__header {
188
+ width: 100%;
189
+ }
190
+
191
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__body {
192
+ min-width: 0;
193
+ position: relative;
194
+ width: 100%;
195
+ }
196
+
197
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__body .presentation-item__text-frame {
198
+ margin-top: 0;
199
+ }
200
+
201
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__media {
202
+ margin: 0;
203
+ position: absolute;
204
+ right: 0;
205
+ top: 0;
206
+ z-index: 1;
207
+ }
208
+
209
+ .blog-rhythm-shell .presentation-item__image img {
210
+ height: 100%;
211
+ object-fit: contain;
212
+ width: 100%;
213
+ }
214
+
215
+ .blog-rhythm-shell .presentation-item__video {
216
+ grid-column: 1 / -1;
217
+ margin: var(--blog-row-height) 0 0;
218
+ }
219
+
220
+ .blog-rhythm-shell .article-page__hero-video {
221
+ margin: 0 0 calc(var(--blog-rhythm) * 5);
222
+ }
223
+
224
+ .blog-rhythm-shell .article-page__hero-video .article-video,
225
+ .blog-rhythm-shell .presentation-item__video-figure {
226
+ container-type: inline-size;
227
+ }
228
+
229
+ .blog-rhythm-shell .presentation-item__video-figure .article-video__media,
230
+ .blog-rhythm-shell .article-page__hero-video .article-video__media {
231
+ height: round(up, calc(100cqw * 9 / 16), var(--blog-row-height));
232
+ }
233
+
234
+ .blog-rhythm-shell .article-float-grid[data-feature-layout][data-has-image="true"] {
235
+ display: grid;
236
+ gap: var(--feature-layout-gap);
237
+ grid-template-columns: minmax(0, 1fr) var(--feature-image-width);
238
+ }
239
+
240
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-page__hero-video,
241
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__header {
242
+ grid-column: 1 / -1;
243
+ }
244
+
245
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__body {
246
+ align-self: start;
247
+ grid-column: 1;
248
+ max-width: var(--feature-copy-width);
249
+ min-width: 0;
250
+ width: var(--feature-copy-width);
251
+ }
252
+
253
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__media,
254
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .pictogram-figure__frame {
255
+ height: var(--feature-image-height);
256
+ width: var(--feature-image-width);
257
+ }
258
+
259
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .pictogram-figure__frame {
260
+ aspect-ratio: auto;
261
+ }
262
+
263
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__media {
264
+ align-self: start;
265
+ grid-column: 2;
266
+ margin: 0;
267
+ position: relative;
268
+ }
269
+
270
+ .blog-rhythm-shell .article-float-grid .presentation-item__image {
271
+ margin: 0;
272
+ }
273
+
274
+ @media (max-width: 900px) {
275
+ .blog-rhythm-shell .presentation-item--blog {
276
+ grid-template-columns: 1fr;
277
+ }
278
+ }
package/app/globals.css CHANGED
@@ -1,7 +1,4 @@
1
- @import "../publications/threat_intelligence/theme.css";
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";
@@ -48,6 +45,7 @@
48
45
  --color-surface-foreground: var(--surface-foreground);
49
46
  --color-foreground-strong: var(--foreground-strong);
50
47
  --color-foreground-muted: var(--foreground-muted);
48
+ --color-foreground-subtle: var(--foreground-subtle);
51
49
  }
52
50
 
53
51
  :root {
@@ -74,6 +72,7 @@
74
72
  --surface-foreground: var(--card-foreground);
75
73
  --foreground-strong: var(--slate-12);
76
74
  --foreground-muted: var(--slate-10);
75
+ --foreground-subtle: var(--slate-9);
77
76
  --border: var(--slate-6);
78
77
  --input: var(--slate-6);
79
78
  --ring: var(--slate-8);
@@ -2285,6 +2284,25 @@ img {
2285
2284
  width: 100%;
2286
2285
  }
2287
2286
 
2287
+ .article-video__preview-frame {
2288
+ aspect-ratio: 16 / 9;
2289
+ background: #000;
2290
+ border: 0;
2291
+ display: block;
2292
+ width: 100%;
2293
+ }
2294
+
2295
+ .article-video__preview-controls {
2296
+ align-items: center;
2297
+ display: flex;
2298
+ gap: 0.75rem;
2299
+ margin-top: 0.5rem;
2300
+ }
2301
+
2302
+ .article-video__preview-controls input[type="range"] {
2303
+ flex: 1;
2304
+ }
2305
+
2288
2306
  .article-video__credit {
2289
2307
  color: rgb(var(--ink-solid-rgb) / 0.62);
2290
2308
  font-size: 0.82rem;