@anthusai/papyrus 1.0.0-next.9 → 1.1.0

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 (222) hide show
  1. package/amplify/auth/prospectus-google-client.js +66 -0
  2. package/amplify/auth/prospectus-google-client.ts +71 -0
  3. package/amplify/auth/redirect-urls.js +54 -0
  4. package/amplify/auth/redirect-urls.ts +66 -0
  5. package/amplify/auth/resource.js +23 -5
  6. package/amplify/auth/resource.ts +31 -5
  7. package/amplify/data/resource.js +42 -80
  8. package/amplify/data/resource.ts +42 -80
  9. package/amplify/data/schema.js +45 -62
  10. package/amplify/data/schema.ts +47 -62
  11. package/amplify/functions/category-action/handler.ts +5 -3
  12. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  13. package/amplify/functions/console-chat-responder/resource.js +0 -7
  14. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  15. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  16. package/amplify/functions/content-actions/handler.py +143 -1
  17. package/amplify/functions/content-actions/requirements.txt +2 -1
  18. package/amplify/functions/content-actions/resource.js +6 -1
  19. package/amplify/functions/content-actions/resource.ts +9 -2
  20. package/amplify/functions/email-submission-processor/handler.py +3 -1
  21. package/amplify/functions/renditions/handler.py +120 -0
  22. package/amplify/functions/renditions/requirements.txt +4 -0
  23. package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
  24. package/amplify/functions/shared/email-submission.ts +109 -1
  25. package/amplify/functions/shared/python-bundle.js +42 -2
  26. package/amplify/functions/shared/python-bundle.ts +47 -2
  27. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  28. package/amplify/inbound-email/ses-intake.js +86 -0
  29. package/amplify/inbound-email/ses-intake.ts +110 -0
  30. package/amplify/plugins/registry.js +19 -0
  31. package/amplify/plugins/registry.ts +23 -0
  32. package/amplify/plugins/signup/function/handler.py +200 -0
  33. package/amplify/plugins/signup/function/resource.js +27 -0
  34. package/amplify/plugins/signup/function/resource.ts +35 -0
  35. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  36. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  37. package/amplify/plugins/signup/schema.js +40 -0
  38. package/amplify/plugins/signup/schema.ts +41 -0
  39. package/amplify/plugins/signup/waf.js +48 -0
  40. package/amplify/plugins/signup/waf.ts +51 -0
  41. package/amplify/plugins/types.js +1 -0
  42. package/amplify/plugins/types.ts +39 -0
  43. package/amplify/plugins/wire.js +24 -0
  44. package/amplify/plugins/wire.ts +26 -0
  45. package/amplify/publication/default-publication.js +18 -0
  46. package/amplify/publication/default-publication.ts +20 -0
  47. package/amplify/publication/invalidation-alarm.js +26 -0
  48. package/amplify/publication/invalidation-alarm.ts +30 -0
  49. package/amplify/publication/redirect-and-index.js +37 -0
  50. package/amplify/publication/renditions-function.js +74 -0
  51. package/amplify/publication/renditions-function.ts +112 -0
  52. package/amplify/publication/renditions-settings.js +51 -0
  53. package/amplify/publication/renditions-settings.ts +58 -0
  54. package/amplify/publication/reproject-on-deploy.js +73 -0
  55. package/amplify/publication/reproject-on-deploy.ts +78 -0
  56. package/amplify/publication/s3-projection-settings.js +67 -0
  57. package/amplify/publication/s3-projection-settings.ts +101 -0
  58. package/amplify/publication/s3-projection.js +151 -0
  59. package/amplify/publication/s3-projection.ts +201 -0
  60. package/amplify/resolver-waves.js +26 -0
  61. package/amplify/resolver-waves.ts +34 -0
  62. package/amplify/revalidate-invalidation-settings.js +30 -0
  63. package/amplify/revalidate-invalidation-settings.ts +35 -0
  64. package/amplify/revalidate-secret-policy.js +13 -0
  65. package/amplify/revalidate-secret-policy.ts +19 -0
  66. package/amplify/site-backend-config.js +65 -1
  67. package/amplify/site-backend-config.ts +153 -2
  68. package/amplify/site-backend-names.js +103 -0
  69. package/amplify/site-backend-names.ts +166 -0
  70. package/amplify/site-backend.js +150 -176
  71. package/amplify/site-backend.ts +189 -201
  72. package/amplify/storage-backups/construct.js +31 -0
  73. package/amplify/storage-backups/construct.ts +47 -0
  74. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  75. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  76. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  77. package/app/[year]/[month]/[day]/page.tsx +1 -1
  78. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  79. package/app/api/archive/editions/route.ts +1 -1
  80. package/app/api/auth/prospectus/callback/route.ts +9 -0
  81. package/app/api/auth/prospectus/selection/route.ts +9 -0
  82. package/app/api/auth/prospectus/session/route.ts +9 -0
  83. package/app/api/auth/prospectus/sign-in/route.ts +9 -0
  84. package/app/api/auth/prospectus/sign-out/route.ts +9 -0
  85. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  86. package/app/api/media/[...path]/route.ts +5 -16
  87. package/app/api/revalidate/route.ts +15 -7
  88. package/app/archive/page.tsx +1 -1
  89. package/app/articles/[slug]/page.tsx +7 -5
  90. package/app/blog-rhythm.css +278 -0
  91. package/app/cyclotron/page.tsx +13 -0
  92. package/app/globals.css +2 -0
  93. package/app/layout.tsx +16 -0
  94. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  95. package/app/newsroom/[section]/page.tsx +6 -3
  96. package/app/newsroom/layout.tsx +4 -9
  97. package/app/newsroom/settings/page.tsx +6 -0
  98. package/app/newsroom/work/page.tsx +10 -0
  99. package/app/page.tsx +22 -8
  100. package/app/robots.ts +2 -5
  101. package/app/tailwind.css +1 -6
  102. package/components/amplify-client-provider.tsx +5 -22
  103. package/components/archive-shell.tsx +2 -22
  104. package/components/article-video.tsx +152 -5
  105. package/components/blog-page-background.tsx +23 -6
  106. package/components/news-desk-page.tsx +4 -15
  107. package/components/news-desk-taxonomy-client.ts +90 -3
  108. package/components/newspaper.tsx +6 -2
  109. package/components/newsroom-access-gate.tsx +2 -1
  110. package/components/newsroom-board-view.tsx +99 -0
  111. package/components/newsroom-client-shell.tsx +12 -0
  112. package/components/newsroom-curation-provider.tsx +17 -0
  113. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  114. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  115. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  116. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  117. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  118. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  119. package/components/newsroom-newsprint-board.css +48 -0
  120. package/components/newsroom-ops-overview.tsx +28 -11
  121. package/components/newsroom-ops-shell.tsx +7 -1
  122. package/components/newsroom-organizer-shell.tsx +130 -0
  123. package/components/newsroom-organizer.css +63 -0
  124. package/components/newsroom-references-view.tsx +92 -8
  125. package/components/papyrus-console-shell.tsx +4 -2
  126. package/components/pictogram-figure.tsx +9 -2
  127. package/components/presentation-header.tsx +135 -0
  128. package/components/presentation-rhythm-hrule.tsx +17 -0
  129. package/components/priming-proof-page.tsx +94 -0
  130. package/components/publication-cyclotron-panel.tsx +57 -0
  131. package/components/reader-auth-control.tsx +8 -5
  132. package/components/reference-curation-controls.tsx +77 -0
  133. package/components/reference-cyclotron-status.tsx +109 -0
  134. package/components/reference-priming.tsx +18 -0
  135. package/components/reference-relevance-review.tsx +75 -0
  136. package/components/settings-page.tsx +2 -1
  137. package/components/signup-entries-panel.tsx +94 -0
  138. package/components/topic-steering-workspace.tsx +78 -8
  139. package/components/use-blog-header-obstacles.ts +81 -0
  140. package/components/use-brand-fonts-ready.ts +28 -0
  141. package/components/use-near-viewport.ts +30 -0
  142. package/components/use-rhythm-overlay.ts +29 -0
  143. package/infra/amplify-app-shell.js +166 -27
  144. package/infra/amplify-app-shell.ts +180 -31
  145. package/infra/build-specs.js +23 -7
  146. package/infra/build-specs.ts +27 -10
  147. package/infra/site-config.js +248 -36
  148. package/infra/site-config.ts +313 -51
  149. package/lib/amplify-outputs-path.ts +5 -1
  150. package/lib/amplify-server-runtime.ts +36 -1
  151. package/lib/approved-priming-proof-reports.ts +6 -0
  152. package/lib/blog-feature-solver.ts +459 -0
  153. package/lib/blog-rhythm.ts +134 -0
  154. package/lib/brand-fonts-watch.ts +43 -0
  155. package/lib/brand-fonts.ts +112 -0
  156. package/lib/cached-content-repository.ts +12 -0
  157. package/lib/category-repository.ts +43 -93
  158. package/lib/content-item-lifecycle.ts +55 -0
  159. package/lib/content-repository.ts +3 -0
  160. package/lib/content-types.ts +4 -0
  161. package/lib/curation-client.ts +61 -0
  162. package/lib/edition-routes.ts +31 -29
  163. package/lib/graphql-content-repository.ts +56 -22
  164. package/lib/media-route-response.ts +45 -0
  165. package/lib/newspaper-layout.ts +3 -2
  166. package/lib/newsroom-base-path.ts +12 -3
  167. package/lib/newsroom-board.ts +99 -0
  168. package/lib/newsroom-category-drilldown.ts +3 -2
  169. package/lib/newsroom-forum-routes.ts +6 -4
  170. package/lib/newsroom-index-filters.ts +14 -10
  171. package/lib/newsroom-nav.ts +3 -1
  172. package/lib/newsroom-organizer-nav.ts +37 -0
  173. package/lib/newsroom-section-routing.ts +20 -0
  174. package/lib/oauth-redirect-priority.ts +40 -0
  175. package/lib/papyrus-web-locations.ts +7 -6
  176. package/lib/plugin-settings.ts +11 -0
  177. package/lib/presentation-footer.ts +3 -2
  178. package/lib/pretext-layout.ts +12 -3
  179. package/lib/priming-proof-report.ts +90 -0
  180. package/lib/prospectus-publication-access.ts +53 -0
  181. package/lib/prospectus-session.ts +196 -0
  182. package/lib/prospectus-sign-in-routes.ts +284 -0
  183. package/lib/prospectus-site-identity.ts +303 -0
  184. package/lib/publication-cyclotron.ts +74 -0
  185. package/lib/rate-limit-retry.ts +55 -0
  186. package/lib/reader-base-path.ts +24 -0
  187. package/lib/reader-cache-lifetime.ts +5 -0
  188. package/lib/reader-static-params.ts +85 -4
  189. package/lib/reader-storage-url.ts +9 -12
  190. package/lib/reference-policy.ts +5 -0
  191. package/lib/relevance-decisions.ts +103 -0
  192. package/lib/relevance-status.ts +63 -0
  193. package/lib/renderer.ts +7 -0
  194. package/lib/revalidate-prewarm.ts +53 -0
  195. package/lib/revalidate-secret.ts +59 -0
  196. package/lib/robots-policy.ts +14 -0
  197. package/lib/root-route-redirect.ts +16 -0
  198. package/lib/semantic-graph.ts +7 -0
  199. package/lib/shared-credentials-provider.ts +61 -0
  200. package/lib/signup-csv.ts +24 -0
  201. package/lib/site-brand.ts +59 -0
  202. package/lib/staging-gated-path.ts +23 -0
  203. package/lib/staging-preview-object.ts +21 -5
  204. package/lib/test-editor-override.ts +4 -0
  205. package/lib/video-loading.ts +22 -0
  206. package/lib/video-script.ts +14 -0
  207. package/middleware.ts +11 -19
  208. package/package.json +73 -8
  209. package/renderers/pretext/article-page.tsx +39 -3
  210. package/renderers/pretext/presentation-shared.tsx +113 -0
  211. package/renderers/pretext/presentation-shell.tsx +15 -102
  212. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  213. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  214. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  215. package/routes.manifest.json +123 -12
  216. package/src/sync.mjs +30 -6
  217. package/src/with-papyrus.d.mts +9 -0
  218. package/src/with-papyrus.mjs +48 -3
  219. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  220. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  221. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  222. package/components/ai-elements/message.tsx +0 -360
@@ -1,26 +1,38 @@
1
1
  import { defineBackend, secret } from "@aws-amplify/backend";
2
- import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
3
- import * as backup from "aws-cdk-lib/aws-backup";
4
- import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
2
+ import { Stack } from "aws-cdk-lib";
3
+ import type * as backup from "aws-cdk-lib/aws-backup";
5
4
  import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
6
- import * as events from "aws-cdk-lib/aws-events";
7
- import * as route53 from "aws-cdk-lib/aws-route53";
8
- import * as ses from "aws-cdk-lib/aws-ses";
9
- import * as sesActions from "aws-cdk-lib/aws-ses-actions";
10
5
  import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
11
6
  import { CfnEventSourceMapping, CfnFunction, Function as LambdaFunction, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
12
7
  import * as s3 from "aws-cdk-lib/aws-s3";
13
8
  import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
9
+ import type { Construct } from "constructs";
14
10
  import { dirname, resolve } from "node:path";
15
11
  import { fileURLToPath } from "node:url";
12
+ import { DEFAULT_RESOLVER_WAVE_SIZE, orderModelStacksInWaves } from "./resolver-waves";
13
+ import { resolveSitePublication } from "./publication/default-publication";
14
+ import { REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS, addReprojectOnDeploy, reprojectDeployToken } from "./publication/reproject-on-deploy";
15
+ import { addProjectionRetries, addS3ProjectionPublication, type S3ProjectionPublication } from "./publication/s3-projection";
16
+ import { planRenditions } from "./publication/renditions-settings";
17
+ import { contentActionsPublicationWiring, planS3ProjectionBackend } from "./publication/s3-projection-settings";
16
18
  import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
17
- import { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
18
- import { data } from "./data/resource";
19
+ import { assertValidPublicationConfig } from "./site-backend-config";
20
+ import { revalidateSecretReadStatement } from "./revalidate-secret-policy";
21
+ import {
22
+ revalidateDistributionEnvironment,
23
+ revalidateInvalidationStatement,
24
+ validateRevalidateDistributionIds,
25
+ } from "./revalidate-invalidation-settings";
26
+ import { addInvalidationPathAlarm } from "./publication/invalidation-alarm";
27
+ import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth, siteAuthRedirectUrls } from "./auth/resource";
28
+ import { PROSPECTUS_SIGN_IN_CALLBACK_PATH, addProspectusGoogleOnlyClient } from "./auth/prospectus-google-client";
29
+ import { defineSiteData } from "./data/resource";
30
+ import { resolvePlugins } from "./plugins/registry";
31
+ import { wirePlugins } from "./plugins/wire";
19
32
  import { assignmentAction } from "./functions/assignment-action/resource";
20
33
  import { categoryAction } from "./functions/category-action/resource";
21
34
  import { ConsoleChatResponderStack } from "./functions/console-chat-responder/resource";
22
- import { graphqlJwtAuthorizer } from "./functions/graphql-jwt-authorizer/resource";
23
- import { contentActions } from "./functions/content-actions/resource";
35
+ import { contentActions, setSiteFilesForContentActionsBundle } from "./functions/content-actions/resource";
24
36
  import { knowledgeQuery } from "./functions/knowledge-query/resource";
25
37
  import { manageUserRole } from "./functions/manage-user-role/resource";
26
38
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
@@ -31,7 +43,19 @@ import { emailSubmissionProcessor } from "./functions/email-submission-processor
31
43
  import { sesInboundReceive } from "./functions/ses-inbound-receive/resource";
32
44
  import { slackDelivery } from "./functions/slack-delivery/resource";
33
45
  import { slackEvents } from "./functions/slack-events/resource";
46
+ import { addInboundEmailSesIntake } from "./inbound-email/ses-intake";
34
47
  import { InboundEmailStack } from "./inbound-email/stack";
48
+ import {
49
+ deriveKnowledgeVectorIndexName,
50
+ deriveReceiptRuleName,
51
+ deriveReceiptRuleSetName,
52
+ deriveStorageBackupVaultName,
53
+ isLegacyProductionPipeline,
54
+ planInboundEmailSes,
55
+ resolveSiteBackendFeatureFlags,
56
+ type SiteBackendIdentity,
57
+ } from "./site-backend-names";
58
+ import { addStorageBackups } from "./storage-backups/construct";
35
59
  import { storage } from "./storage/resource";
36
60
  import type { PapyrusSite } from "../lib/define-site";
37
61
 
@@ -44,57 +68,48 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
44
68
 
45
69
  const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
46
70
  const amplifyAppId = (process.env.AWS_APP_ID ?? "").trim();
47
- // Amplify app id whose `main` branch is this site's production pipeline (owns
48
- // account-global resources: SES rule set, backup vault, legacy S3 Vectors name).
71
+ // Amplify app id whose `main` branch is the original p.apyr.us backend; only
72
+ // that app keeps the historical account-global names (see site-backend-names.ts).
49
73
  const productionAppId = (site.productionAppId ?? "").trim();
50
- const isAmplifyProductionPipeline =
51
- amplifyBranch === "main" && productionAppId !== "" && amplifyAppId === productionAppId;
52
-
53
- function readEnvFlag(name: string, defaultValue: boolean): boolean {
54
- const raw = (process.env[name] ?? "").trim().toLowerCase();
55
- if (["1", "true", "yes", "on"].includes(raw)) return true;
56
- if (["0", "false", "no", "off"].includes(raw)) return false;
57
- return defaultValue;
58
- }
59
-
60
- function sanitizeAwsName(value: string, maxLength = 50): string {
61
- return value
62
- .toLowerCase()
63
- .replace(/[^a-z0-9-]/g, "-")
64
- .replace(/-+/g, "-")
65
- .replace(/^-|-$/g, "")
66
- .slice(0, maxLength);
67
- }
68
-
69
- // S3 Vectors index names are account-global. Keep the original name for the
70
- // p.apyr.us production app so its existing index + data are not orphaned;
71
- // namespace every other app's index by site brand so a second CMS app can
72
- // deploy into the same account without colliding.
73
- const papyrusSiteBrand = (
74
- site.brandId
75
- ?? process.env.PAPYRUS_SITE_BRAND
76
- ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
77
- ?? "papyrus"
78
- ).trim().toLowerCase();
79
- const knowledgeVectorIndexName =
80
- productionAppId !== "" && amplifyAppId === productionAppId
81
- ? "papyrus-knowledge"
82
- : `papyrus-knowledge-${sanitizeAwsName(papyrusSiteBrand, 40)}`;
83
-
84
- // SES active receipt rule sets and shared backup vault names are account-global.
85
- // Keep them on the production pipeline only unless explicitly opted in.
86
- // A feature flag in the site config wins over the environment.
87
- const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
88
- const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
89
- const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
90
-
74
+ const siteBackendIdentity: SiteBackendIdentity = {
75
+ brandId: (
76
+ site.brandId
77
+ ?? process.env.PAPYRUS_SITE_BRAND
78
+ ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
79
+ ?? "papyrus"
80
+ ).trim().toLowerCase(),
81
+ amplifyAppId,
82
+ amplifyBranch,
83
+ productionAppId,
84
+ };
85
+ const isAmplifyProductionPipeline = isLegacyProductionPipeline(siteBackendIdentity);
86
+ const knowledgeVectorIndexName = deriveKnowledgeVectorIndexName(siteBackendIdentity);
87
+ const featureFlags = resolveSiteBackendFeatureFlags(site.features, process.env, siteBackendIdentity);
88
+ const enableInboundEmail = featureFlags.inboundEmail;
89
+ const enableSlackAgent = featureFlags.slack;
90
+ const enableStorageBackups = featureFlags.storageBackups;
91
+
92
+ const baseAuthConfig = site.auth ?? authConfigFromEnv();
93
+ const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
94
+ const authConfig = {
95
+ ...baseAuthConfig,
96
+ newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
97
+ };
98
+ const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
99
+ if (publication !== undefined) assertValidPublicationConfig(publication);
100
+ const publicationPlan = planS3ProjectionBackend(publication);
101
+ const renditionsPlan = planRenditions(publication, process.cwd());
102
+ setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
103
+ const plugins = resolvePlugins(site.plugins);
104
+ const pluginDefinitions = Object.assign({}, ...plugins.map(({ plugin }) => plugin.schemaDefinitions()));
105
+ const pluginFunctions: Record<never, never> = Object.assign({}, ...plugins.map(({ plugin }) => plugin.functions()));
91
106
  const backend = defineBackend({
107
+ ...pluginFunctions,
92
108
  assignmentAction,
93
- auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
109
+ auth: defineSiteAuth(authConfig),
94
110
  categoryAction,
95
111
  contentActions,
96
- data,
97
- graphqlJwtAuthorizer,
112
+ data: defineSiteData(pluginDefinitions),
98
113
  knowledgeQuery,
99
114
  manageUserRole,
100
115
  modelAttachmentUpload,
@@ -108,10 +123,29 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
108
123
  storage,
109
124
  });
110
125
 
126
+ orderModelStacksInWaves(
127
+ backend.data.resources.graphqlApi.node.scope as Construct,
128
+ Object.keys(backend.data.resources.tables),
129
+ site.resolverWaveSize ?? DEFAULT_RESOLVER_WAVE_SIZE,
130
+ );
131
+
132
+ const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
133
+ if (
134
+ authConfig.applyCognitoDomainPrefix === true
135
+ && requestedCognitoDomainPrefix !== ""
136
+ && authConfig.disableGoogleOAuth !== true
137
+ ) {
138
+ applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
139
+ }
140
+
141
+ const prospectusGoogleOnlyClient = authConfig.disableGoogleOAuth === true
142
+ ? undefined
143
+ : addProspectusGoogleOnlyClient(backend.auth.stack, backend.auth.resources.userPool, siteAuthRedirectUrls(authConfig));
144
+
111
145
  const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
112
146
  // Repo root in a Papyrus checkout; package root when installed from npm.
113
147
  const projectRoot = resolve(amplifyBackendDir, "..");
114
- const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
148
+ const enableConsoleResponder = featureFlags.consoleResponder;
115
149
 
116
150
  if (
117
151
  enableConsoleResponder
@@ -160,43 +194,19 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
160
194
  const messageThreadTable = backend.data.resources.tables.MessageThread;
161
195
  const dataStack = Stack.of(messageTable);
162
196
  const graphqlEndpoint = backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl;
163
- const jwtAuthorizerCfn = backend.graphqlJwtAuthorizer.resources.lambda.node.defaultChild as CfnFunction | undefined;
164
- const jwtAuthorizerEnvironment = jwtAuthorizerCfn?.environment;
165
- const jwtAuthorizerVariables =
166
- jwtAuthorizerEnvironment
167
- && typeof jwtAuthorizerEnvironment === "object"
168
- && "variables" in jwtAuthorizerEnvironment
169
- ? (jwtAuthorizerEnvironment as { variables?: Record<string, string> }).variables
170
- : undefined;
171
- const jwtSsmEnvConfig =
172
- process.env.AMPLIFY_SSM_ENV_CONFIG?.trim()
173
- || jwtAuthorizerVariables?.AMPLIFY_SSM_ENV_CONFIG?.trim()
174
- || "";
197
+ const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
175
198
 
176
199
  if (enableConsoleResponder) {
177
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
200
+ new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
178
201
  messageTable,
179
202
  messageStreamArn,
180
203
  threadTable: messageThreadTable,
181
204
  projectRoot,
182
205
  graphqlEndpoint,
183
- amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
184
206
  responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
185
207
  model: process.env.PAPYRUS_CONSOLE_MODEL,
186
208
  prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
187
209
  });
188
- if (jwtSsmEnvConfig) {
189
- consoleChatResponder.responderFunction.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
190
- }
191
- const jwtSecretSsmParam =
192
- process.env.PAPYRUS_JWT_SECRET_SSM_PARAM?.trim()
193
- || (productionAppId !== "" && amplifyAppId === productionAppId
194
- ? `/amplify/${amplifyAppId}/main-branch-cb38ada667/PAPYRUS_JWT_SECRET`
195
- : "/amplify/papyrus/ryan-sandbox-adcd88a186/PAPYRUS_JWT_SECRET");
196
- consoleChatResponder.responderFunction.addEnvironment(
197
- "PAPYRUS_JWT_SECRET_SSM_PARAM",
198
- jwtSecretSsmParam,
199
- );
200
210
  }
201
211
 
202
212
  if (enableSlackAgent) {
@@ -217,9 +227,9 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
217
227
  "PAPYRUS_SLACK_ALLOWED_USER_IDS",
218
228
  (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim(),
219
229
  );
220
- if (jwtSsmEnvConfig) {
221
- backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
222
- backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
230
+ if (amplifySsmEnvConfig) {
231
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
232
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
223
233
  }
224
234
  backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
225
235
  backend.slackDelivery.addEnvironment(
@@ -297,8 +307,8 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
297
307
  let storageBackupVault: backup.BackupVault | undefined;
298
308
 
299
309
  const storageBucketCfn = storageBucket.node.defaultChild as s3.CfnBucket | undefined;
300
- if ((enableStorageBackups || enableInboundEmail) && storageBucketCfn) {
301
- // Backups and inbound-email intake both rely on S3 → EventBridge notifications.
310
+ if ((enableStorageBackups || enableInboundEmail || renditionsPlan.enabled) && storageBucketCfn) {
311
+ // Backups, inbound-email intake and upload-time renditions all rely on S3 → EventBridge notifications.
302
312
  storageBucketCfn.addPropertyOverride(
303
313
  "NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled",
304
314
  true,
@@ -307,41 +317,17 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
307
317
 
308
318
  if (enableStorageBackups) {
309
319
  const storageBackupsStack = backend.createStack("storage-backups");
310
- const storageBackupVaultName = sanitizeAwsName(
311
- process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME?.trim()
312
- || (isAmplifyProductionPipeline
313
- ? `papyrus-${productionAppId}-main-media-backup-vault`
314
- : `papyrus-${sanitizeAwsName(storageBackupsStack.stackName, 32)}-media-vault`),
320
+ const storageBackupVaultName = deriveStorageBackupVaultName(
321
+ siteBackendIdentity,
322
+ process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME,
315
323
  );
316
324
 
317
- storageBackupVault = new backup.BackupVault(storageBackupsStack, "PapyrusStorageBackupVault", {
325
+ const storageBackups = addStorageBackups(storageBackupsStack, {
326
+ storageBucket,
318
327
  backupVaultName: storageBackupVaultName,
319
- removalPolicy: RemovalPolicy.RETAIN,
320
- });
321
- storageBackupPlan = new backup.BackupPlan(storageBackupsStack, "PapyrusStorageBackupPlan", {
322
- backupVault: storageBackupVault,
323
- });
324
-
325
- storageBackupPlan.addRule(
326
- new backup.BackupPlanRule({
327
- ruleName: "papyrus-storage-pitr-35d",
328
- enableContinuousBackup: true,
329
- deleteAfter: Duration.days(35),
330
- }),
331
- );
332
-
333
- storageBackupPlan.addRule(
334
- new backup.BackupPlanRule({
335
- ruleName: "papyrus-storage-daily-365d",
336
- scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
337
- deleteAfter: Duration.days(365),
338
- }),
339
- );
340
-
341
- storageBackupPlan.addSelection("PapyrusStorageBackupSelection", {
342
- allowRestores: true,
343
- resources: [backup.BackupResource.fromArn(storageBucket.bucketArn)],
344
328
  });
329
+ storageBackupVault = storageBackups.vault;
330
+ storageBackupPlan = storageBackups.plan;
345
331
  }
346
332
 
347
333
  // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
@@ -430,9 +416,66 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
430
416
  new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
431
417
  );
432
418
  }
419
+ let s3ProjectionPublication: S3ProjectionPublication | undefined;
420
+ if (publication !== undefined) {
421
+ s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), publication, {
422
+ mediaBucket: storageBucket,
423
+ renditions: renditionsPlan,
424
+ });
425
+ (contentActionsLambda.node.defaultChild as CfnFunction).memorySize = publicationPlan.contentActionsMemorySize;
426
+ if (publication.reprojectOnDeploy === true) {
427
+ (contentActionsLambda.node.defaultChild as CfnFunction).timeout = REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS;
428
+ addReprojectOnDeploy(backend.createStack("reproject-on-deploy"), contentActionsLambda, reprojectDeployToken(process.env));
429
+ }
430
+ }
431
+ if (s3ProjectionPublication !== undefined) {
432
+ addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
433
+ }
434
+ const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
435
+ for (const [name, value] of Object.entries(publicationWiring.environment)) {
436
+ contentActionsLambda.addEnvironment(name, value);
437
+ }
438
+ for (const statement of publicationWiring.policyStatements) {
439
+ contentActionsLambda.addToRolePolicy(statement);
440
+ }
433
441
  if ((site.revalidateBaseUrl ?? "").trim() !== "") {
434
442
  backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
435
- backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
443
+ const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
444
+ if (!revalidateSecretParameter.startsWith("/")) {
445
+ throw new Error(
446
+ "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.",
447
+ );
448
+ }
449
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
450
+ const revalidateStack = Stack.of(contentActionsLambda);
451
+ contentActionsLambda.addToRolePolicy(
452
+ new PolicyStatement(
453
+ revalidateSecretReadStatement({
454
+ partition: revalidateStack.partition,
455
+ region: revalidateStack.region,
456
+ account: revalidateStack.account,
457
+ parameterName: revalidateSecretParameter,
458
+ }),
459
+ ),
460
+ );
461
+ const distributionProblems = validateRevalidateDistributionIds(site.revalidateDistributionIds, site.revalidateBaseUrl);
462
+ if (distributionProblems.length > 0) throw new Error(`Invalid backend config: ${distributionProblems.join("; ")}`);
463
+ const distributionIds = site.revalidateDistributionIds ?? [];
464
+ if (distributionIds.length > 0) {
465
+ for (const [name, value] of Object.entries(revalidateDistributionEnvironment(distributionIds))) {
466
+ backend.contentActions.addEnvironment(name, value);
467
+ }
468
+ contentActionsLambda.addToRolePolicy(
469
+ new PolicyStatement(
470
+ revalidateInvalidationStatement({ partition: revalidateStack.partition, account: revalidateStack.account, distributionIds }),
471
+ ),
472
+ );
473
+ for (const id of distributionIds) {
474
+ addInvalidationPathAlarm(revalidateStack, `ReaderEdgeInvalidationPaths${id}`, id);
475
+ }
476
+ }
477
+ } else if (site.revalidateDistributionIds !== undefined) {
478
+ throw new Error("Invalid backend config: revalidateDistributionIds requires revalidateBaseUrl");
436
479
  }
437
480
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
438
481
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
@@ -527,86 +570,26 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
527
570
 
528
571
  const inboundEventStack = Stack.of(receiveLambda);
529
572
 
530
- // SES receipt rule sets are account-global; only provision on the production pipeline.
531
- if (isAmplifyProductionPipeline) {
532
- const inboundDnsZone = route53.HostedZone.fromHostedZoneAttributes(storageStack, "PapyrusInboundDnsZone", {
533
- hostedZoneId: inboundDnsZoneId,
534
- zoneName: inboundDnsZoneName,
535
- });
536
- const verifyInboundEmailDomain = new AwsCustomResource(storageStack, "VerifyInboundEmailDomain", {
537
- onCreate: {
538
- service: "SES",
539
- action: "verifyDomainIdentity",
540
- parameters: {
541
- Domain: inboundEmailDomain,
542
- },
543
- physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
544
- },
545
- onUpdate: {
546
- service: "SES",
547
- action: "verifyDomainIdentity",
548
- parameters: {
549
- Domain: inboundEmailDomain,
550
- },
551
- physicalResourceId: PhysicalResourceId.of(`ses-domain-verify-${inboundEmailDomain}`),
552
- },
553
- policy: AwsCustomResourcePolicy.fromSdkCalls({
554
- resources: AwsCustomResourcePolicy.ANY_RESOURCE,
555
- }),
556
- timeout: Duration.minutes(2),
557
- });
558
- new route53.TxtRecord(storageStack, "PapyrusInboundEmailDomainVerification", {
559
- zone: inboundDnsZone,
560
- recordName: `_amazonses.${inboundDnsRecordName}`,
561
- ttl: Duration.minutes(5),
562
- values: [verifyInboundEmailDomain.getResponseField("VerificationToken")],
563
- });
564
-
565
- const inboundRuleSet = new ses.ReceiptRuleSet(storageStack, "PapyrusInboundEmailRuleSet", {
566
- receiptRuleSetName: `papyrus-inbound-${inboundEmailDomain.replace(/\./g, "-")}`,
567
- });
568
- inboundRuleSet.addRule("PapyrusInboundSubmissions", {
569
- recipients: inboundRecipients,
570
- enabled: true,
571
- scanEnabled: true,
572
- tlsPolicy: ses.TlsPolicy.REQUIRE,
573
- actions: [
574
- new sesActions.S3({
575
- bucket: storageBucket,
576
- objectKeyPrefix: "inbound-email/",
577
- }),
578
- ],
579
- });
580
-
581
- new AwsCustomResource(storageStack, "ActivateInboundEmailRuleSet", {
582
- onCreate: {
583
- service: "SES",
584
- action: "setActiveReceiptRuleSet",
585
- parameters: {
586
- RuleSetName: inboundRuleSet.receiptRuleSetName,
587
- },
588
- physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
589
- },
590
- onUpdate: {
591
- service: "SES",
592
- action: "setActiveReceiptRuleSet",
593
- parameters: {
594
- RuleSetName: inboundRuleSet.receiptRuleSetName,
595
- },
596
- physicalResourceId: PhysicalResourceId.of(`activate-${inboundRuleSet.receiptRuleSetName}`),
597
- },
598
- onDelete: {
599
- service: "SES",
600
- action: "setActiveReceiptRuleSet",
601
- parameters: {
602
- RuleSetName: null,
603
- },
604
- },
605
- policy: AwsCustomResourcePolicy.fromSdkCalls({
606
- resources: AwsCustomResourcePolicy.ANY_RESOURCE,
607
- }),
608
- timeout: Duration.minutes(2),
609
- });
573
+ // SES allows one active receipt rule set per region, so a non-legacy site
574
+ // creates its own brand-named rule set and never activates it here.
575
+ const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
576
+ if (inboundSesPlan.createReceiptRules) {
577
+ addInboundEmailSesIntake(storageStack, {
578
+ storageBucket,
579
+ recipients: inboundRecipients,
580
+ ruleSetName: deriveReceiptRuleSetName(siteBackendIdentity, inboundEmailDomain),
581
+ existingRuleSetName: site.inboundEmailSes?.existingReceiptRuleSetName,
582
+ ruleName: deriveReceiptRuleName(siteBackendIdentity),
583
+ activateRuleSet: inboundSesPlan.activateReceiptRuleSet,
584
+ domainIdentity: inboundSesPlan.manageDomainIdentity
585
+ ? {
586
+ domain: inboundEmailDomain,
587
+ hostedZoneId: inboundDnsZoneId,
588
+ hostedZoneName: inboundDnsZoneName,
589
+ recordName: inboundDnsRecordName,
590
+ }
591
+ : undefined,
592
+ });
610
593
  }
611
594
 
612
595
  new InboundEmailStack(inboundEventStack, "InboundEmail", {
@@ -704,6 +687,9 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
704
687
 
705
688
  backend.addOutput({
706
689
  custom: {
690
+ prospectusSignIn: prospectusGoogleOnlyClient
691
+ ? { clientId: prospectusGoogleOnlyClient.ref, callbackPath: PROSPECTUS_SIGN_IN_CALLBACK_PATH }
692
+ : null,
707
693
  inboundEmail: enableInboundEmail
708
694
  ? {
709
695
  domain: inboundEmailDomain,
@@ -735,5 +721,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
735
721
  : null,
736
722
  },
737
723
  });
724
+ wirePlugins(backend, plugins, contentActionsLambda);
725
+
738
726
  return backend;
739
727
  }
@@ -0,0 +1,31 @@
1
+ import { Duration, RemovalPolicy } from "aws-cdk-lib";
2
+ import * as backup from "aws-cdk-lib/aws-backup";
3
+ import * as events from "aws-cdk-lib/aws-events";
4
+ /**
5
+ * Adds the resources directly to `scope` (no wrapper construct) so the logical
6
+ * ids of the live p.apyr.us backup stack stay unchanged.
7
+ */
8
+ export function addStorageBackups(scope, props) {
9
+ const vault = new backup.BackupVault(scope, "PapyrusStorageBackupVault", {
10
+ backupVaultName: props.backupVaultName,
11
+ removalPolicy: RemovalPolicy.RETAIN,
12
+ });
13
+ const plan = new backup.BackupPlan(scope, "PapyrusStorageBackupPlan", {
14
+ backupVault: vault,
15
+ });
16
+ plan.addRule(new backup.BackupPlanRule({
17
+ ruleName: "papyrus-storage-pitr-35d",
18
+ enableContinuousBackup: true,
19
+ deleteAfter: Duration.days(35),
20
+ }));
21
+ plan.addRule(new backup.BackupPlanRule({
22
+ ruleName: "papyrus-storage-daily-365d",
23
+ scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
24
+ deleteAfter: Duration.days(365),
25
+ }));
26
+ plan.addSelection("PapyrusStorageBackupSelection", {
27
+ allowRestores: true,
28
+ resources: [backup.BackupResource.fromArn(props.storageBucket.bucketArn)],
29
+ });
30
+ return { plan, vault };
31
+ }
@@ -0,0 +1,47 @@
1
+ import { Duration, RemovalPolicy } from "aws-cdk-lib";
2
+ import * as backup from "aws-cdk-lib/aws-backup";
3
+ import * as events from "aws-cdk-lib/aws-events";
4
+ import type * as s3 from "aws-cdk-lib/aws-s3";
5
+ import type { Construct } from "constructs";
6
+
7
+ export type StorageBackupsProps = {
8
+ storageBucket: s3.IBucket;
9
+ backupVaultName: string;
10
+ };
11
+
12
+ /**
13
+ * Adds the resources directly to `scope` (no wrapper construct) so the logical
14
+ * ids of the live p.apyr.us backup stack stay unchanged.
15
+ */
16
+ export function addStorageBackups(scope: Construct, props: StorageBackupsProps) {
17
+ const vault = new backup.BackupVault(scope, "PapyrusStorageBackupVault", {
18
+ backupVaultName: props.backupVaultName,
19
+ removalPolicy: RemovalPolicy.RETAIN,
20
+ });
21
+ const plan = new backup.BackupPlan(scope, "PapyrusStorageBackupPlan", {
22
+ backupVault: vault,
23
+ });
24
+
25
+ plan.addRule(
26
+ new backup.BackupPlanRule({
27
+ ruleName: "papyrus-storage-pitr-35d",
28
+ enableContinuousBackup: true,
29
+ deleteAfter: Duration.days(35),
30
+ }),
31
+ );
32
+
33
+ plan.addRule(
34
+ new backup.BackupPlanRule({
35
+ ruleName: "papyrus-storage-daily-365d",
36
+ scheduleExpression: events.Schedule.cron({ minute: "0", hour: "5" }),
37
+ deleteAfter: Duration.days(365),
38
+ }),
39
+ );
40
+
41
+ plan.addSelection("PapyrusStorageBackupSelection", {
42
+ allowRestores: true,
43
+ resources: [backup.BackupResource.fromArn(props.storageBucket.bucketArn)],
44
+ });
45
+
46
+ return { plan, vault };
47
+ }
@@ -4,8 +4,13 @@ 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
- export const revalidate = 3600;
9
+ export const revalidate = 60;
10
+
11
+ export async function generateStaticParams() {
12
+ return generateEditionItemStaticParams();
13
+ }
9
14
 
10
15
  type DateScopedArticlePageProps = {
11
16
  params: Promise<{
@@ -52,10 +57,12 @@ export default async function DateScopedArticlePage({ params }: DateScopedArticl
52
57
  editionFooter={{
53
58
  editionBasePath,
54
59
  entries: buildPresentationFooterEntries(content),
60
+ sections: content.sections.map((section) => ({ key: section.key, label: section.label })),
55
61
  subtitle: SITE_BRAND.footerSubtitleOverride ?? (content.description?.trim() || "Inside Papyrus"),
56
62
  title: SITE_BRAND.footerTitle,
57
63
  }}
58
64
  item={item}
65
+ videoScript={content.videoScripts?.[item.slug] ?? null}
59
66
  backHref={`${editionBasePath}#${item.slug}`}
60
67
  />
61
68
  );
@@ -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);
@@ -2,8 +2,13 @@ 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
- export const revalidate = 3600;
7
+ export const revalidate = 60;
8
+
9
+ export async function generateStaticParams() {
10
+ return generateEditionPageStaticParams();
11
+ }
7
12
 
8
13
  type EditionPageRouteProps = {
9
14
  params: Promise<{
@@ -2,7 +2,7 @@ import { EditionRoutePage } from "./edition-route-page";
2
2
  import { generateEditionDateStaticParams } from "../../../../lib/reader-static-params";
3
3
 
4
4
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
5
- export const revalidate = 3600;
5
+ export const revalidate = 60;
6
6
 
7
7
  export async function generateStaticParams() {
8
8
  return generateEditionDateStaticParams();