@anthusai/papyrus 1.0.0-next.8 → 1.0.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 (209) hide show
  1. package/amplify/auth/redirect-urls.js +54 -0
  2. package/amplify/auth/redirect-urls.ts +66 -0
  3. package/amplify/auth/resource.js +20 -5
  4. package/amplify/auth/resource.ts +27 -5
  5. package/amplify/data/resource.js +42 -80
  6. package/amplify/data/resource.ts +42 -80
  7. package/amplify/data/schema.js +45 -60
  8. package/amplify/data/schema.ts +47 -60
  9. package/amplify/functions/category-action/handler.ts +5 -3
  10. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  11. package/amplify/functions/console-chat-responder/resource.js +0 -7
  12. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  13. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  14. package/amplify/functions/content-actions/handler.py +143 -1
  15. package/amplify/functions/content-actions/requirements.txt +2 -1
  16. package/amplify/functions/content-actions/resource.js +6 -1
  17. package/amplify/functions/content-actions/resource.ts +9 -2
  18. package/amplify/functions/renditions/handler.py +120 -0
  19. package/amplify/functions/renditions/requirements.txt +4 -0
  20. package/amplify/functions/shared/python-bundle.js +42 -2
  21. package/amplify/functions/shared/python-bundle.ts +47 -2
  22. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  23. package/amplify/inbound-email/ses-intake.js +86 -0
  24. package/amplify/inbound-email/ses-intake.ts +110 -0
  25. package/amplify/plugins/registry.js +19 -0
  26. package/amplify/plugins/registry.ts +23 -0
  27. package/amplify/plugins/signup/function/handler.py +200 -0
  28. package/amplify/plugins/signup/function/resource.js +27 -0
  29. package/amplify/plugins/signup/function/resource.ts +35 -0
  30. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  31. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  32. package/amplify/plugins/signup/schema.js +40 -0
  33. package/amplify/plugins/signup/schema.ts +41 -0
  34. package/amplify/plugins/signup/waf.js +48 -0
  35. package/amplify/plugins/signup/waf.ts +51 -0
  36. package/amplify/plugins/types.js +1 -0
  37. package/amplify/plugins/types.ts +39 -0
  38. package/amplify/plugins/wire.js +24 -0
  39. package/amplify/plugins/wire.ts +26 -0
  40. package/amplify/publication/default-publication.js +18 -0
  41. package/amplify/publication/default-publication.ts +20 -0
  42. package/amplify/publication/invalidation-alarm.js +26 -0
  43. package/amplify/publication/invalidation-alarm.ts +30 -0
  44. package/amplify/publication/redirect-and-index.js +37 -0
  45. package/amplify/publication/renditions-function.js +74 -0
  46. package/amplify/publication/renditions-function.ts +112 -0
  47. package/amplify/publication/renditions-settings.js +51 -0
  48. package/amplify/publication/renditions-settings.ts +58 -0
  49. package/amplify/publication/reproject-on-deploy.js +73 -0
  50. package/amplify/publication/reproject-on-deploy.ts +78 -0
  51. package/amplify/publication/s3-projection-settings.js +67 -0
  52. package/amplify/publication/s3-projection-settings.ts +101 -0
  53. package/amplify/publication/s3-projection.js +151 -0
  54. package/amplify/publication/s3-projection.ts +201 -0
  55. package/amplify/resolver-waves.js +26 -0
  56. package/amplify/resolver-waves.ts +34 -0
  57. package/amplify/revalidate-invalidation-settings.js +30 -0
  58. package/amplify/revalidate-invalidation-settings.ts +35 -0
  59. package/amplify/revalidate-secret-policy.js +13 -0
  60. package/amplify/revalidate-secret-policy.ts +19 -0
  61. package/amplify/site-backend-config.js +65 -1
  62. package/amplify/site-backend-config.ts +153 -2
  63. package/amplify/site-backend-names.js +103 -0
  64. package/amplify/site-backend-names.ts +166 -0
  65. package/amplify/site-backend.js +143 -176
  66. package/amplify/site-backend.ts +181 -201
  67. package/amplify/storage-backups/construct.js +31 -0
  68. package/amplify/storage-backups/construct.ts +47 -0
  69. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  70. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  71. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  72. package/app/[year]/[month]/[day]/page.tsx +1 -1
  73. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  74. package/app/api/archive/editions/route.ts +1 -1
  75. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  76. package/app/api/media/[...path]/route.ts +5 -16
  77. package/app/api/revalidate/route.ts +15 -7
  78. package/app/archive/page.tsx +1 -1
  79. package/app/articles/[slug]/page.tsx +7 -5
  80. package/app/blog-rhythm.css +278 -0
  81. package/app/cyclotron/page.tsx +13 -0
  82. package/app/globals.css +2 -0
  83. package/app/layout.tsx +16 -0
  84. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  85. package/app/newsroom/[section]/page.tsx +6 -3
  86. package/app/newsroom/layout.tsx +4 -9
  87. package/app/newsroom/settings/page.tsx +6 -0
  88. package/app/newsroom/work/page.tsx +10 -0
  89. package/app/page.tsx +22 -8
  90. package/app/robots.ts +2 -5
  91. package/app/tailwind.css +1 -0
  92. package/bin/papyrus-infra.mjs +3 -3
  93. package/components/amplify-client-provider.tsx +5 -22
  94. package/components/archive-shell.tsx +2 -22
  95. package/components/article-video.tsx +152 -5
  96. package/components/blog-page-background.tsx +23 -6
  97. package/components/news-desk-page.tsx +4 -15
  98. package/components/news-desk-taxonomy-client.ts +90 -3
  99. package/components/newspaper.tsx +6 -2
  100. package/components/newsroom-access-gate.tsx +2 -1
  101. package/components/newsroom-board-view.tsx +99 -0
  102. package/components/newsroom-client-shell.tsx +12 -0
  103. package/components/newsroom-curation-provider.tsx +17 -0
  104. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  105. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  106. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  107. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  108. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  109. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  110. package/components/newsroom-newsprint-board.css +48 -0
  111. package/components/newsroom-ops-overview.tsx +28 -11
  112. package/components/newsroom-ops-shell.tsx +7 -1
  113. package/components/newsroom-organizer-shell.tsx +130 -0
  114. package/components/newsroom-organizer.css +63 -0
  115. package/components/newsroom-references-view.tsx +92 -8
  116. package/components/papyrus-console-shell.tsx +4 -2
  117. package/components/pictogram-figure.tsx +9 -2
  118. package/components/presentation-header.tsx +135 -0
  119. package/components/presentation-rhythm-hrule.tsx +17 -0
  120. package/components/priming-proof-page.tsx +92 -0
  121. package/components/publication-cyclotron-panel.tsx +57 -0
  122. package/components/reader-auth-control.tsx +8 -5
  123. package/components/reference-curation-controls.tsx +77 -0
  124. package/components/reference-cyclotron-status.tsx +109 -0
  125. package/components/reference-priming.tsx +18 -0
  126. package/components/reference-relevance-review.tsx +75 -0
  127. package/components/settings-page.tsx +2 -1
  128. package/components/signup-entries-panel.tsx +94 -0
  129. package/components/topic-steering-workspace.tsx +78 -6
  130. package/components/use-blog-header-obstacles.ts +81 -0
  131. package/components/use-brand-fonts-ready.ts +28 -0
  132. package/components/use-near-viewport.ts +30 -0
  133. package/components/use-rhythm-overlay.ts +29 -0
  134. package/infra/amplify-app-shell.js +219 -71
  135. package/infra/amplify-app-shell.ts +231 -77
  136. package/infra/build-specs.js +23 -8
  137. package/infra/build-specs.ts +27 -11
  138. package/infra/site-config.js +277 -34
  139. package/infra/site-config.ts +340 -43
  140. package/lib/amplify-outputs-path.ts +5 -1
  141. package/lib/amplify-server-runtime.ts +36 -1
  142. package/lib/approved-priming-proof-reports.ts +6 -0
  143. package/lib/blog-feature-solver.ts +459 -0
  144. package/lib/blog-rhythm.ts +134 -0
  145. package/lib/brand-fonts-watch.ts +43 -0
  146. package/lib/brand-fonts.ts +112 -0
  147. package/lib/cached-content-repository.ts +12 -0
  148. package/lib/category-repository.ts +43 -93
  149. package/lib/content-item-lifecycle.ts +55 -0
  150. package/lib/content-repository.ts +3 -0
  151. package/lib/content-types.ts +4 -0
  152. package/lib/curation-client.ts +61 -0
  153. package/lib/edition-routes.ts +31 -29
  154. package/lib/graphql-content-repository.ts +56 -22
  155. package/lib/media-route-response.ts +45 -0
  156. package/lib/newspaper-layout.ts +3 -2
  157. package/lib/newsroom-base-path.ts +12 -3
  158. package/lib/newsroom-board.ts +99 -0
  159. package/lib/newsroom-category-drilldown.ts +3 -2
  160. package/lib/newsroom-forum-routes.ts +6 -4
  161. package/lib/newsroom-index-filters.ts +14 -10
  162. package/lib/newsroom-nav.ts +3 -1
  163. package/lib/newsroom-organizer-nav.ts +37 -0
  164. package/lib/newsroom-section-routing.ts +20 -0
  165. package/lib/oauth-redirect-priority.ts +40 -0
  166. package/lib/papyrus-web-locations.ts +7 -6
  167. package/lib/plugin-settings.ts +11 -0
  168. package/lib/presentation-footer.ts +3 -2
  169. package/lib/pretext-layout.ts +12 -3
  170. package/lib/priming-proof-report.ts +89 -0
  171. package/lib/prospectus-site-identity.ts +303 -0
  172. package/lib/publication-cyclotron.ts +74 -0
  173. package/lib/rate-limit-retry.ts +55 -0
  174. package/lib/reader-base-path.ts +24 -0
  175. package/lib/reader-cache-lifetime.ts +5 -0
  176. package/lib/reader-static-params.ts +85 -4
  177. package/lib/reader-storage-url.ts +9 -12
  178. package/lib/reference-policy.ts +5 -0
  179. package/lib/relevance-decisions.ts +103 -0
  180. package/lib/relevance-status.ts +63 -0
  181. package/lib/renderer.ts +7 -0
  182. package/lib/revalidate-prewarm.ts +53 -0
  183. package/lib/revalidate-secret.ts +59 -0
  184. package/lib/robots-policy.ts +14 -0
  185. package/lib/root-route-redirect.ts +16 -0
  186. package/lib/semantic-graph.ts +7 -0
  187. package/lib/shared-credentials-provider.ts +61 -0
  188. package/lib/signup-csv.ts +24 -0
  189. package/lib/site-brand.ts +59 -0
  190. package/lib/staging-gated-path.ts +23 -0
  191. package/lib/staging-preview-object.ts +21 -5
  192. package/lib/test-editor-override.ts +4 -0
  193. package/lib/video-loading.ts +22 -0
  194. package/lib/video-script.ts +14 -0
  195. package/middleware.ts +11 -19
  196. package/package.json +64 -1
  197. package/renderers/pretext/article-page.tsx +39 -3
  198. package/renderers/pretext/presentation-shared.tsx +113 -0
  199. package/renderers/pretext/presentation-shell.tsx +15 -102
  200. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  201. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  202. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  203. package/routes.manifest.json +68 -12
  204. package/src/sync.mjs +30 -6
  205. package/src/with-papyrus.d.mts +9 -0
  206. package/src/with-papyrus.mjs +48 -3
  207. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  208. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  209. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
@@ -1,26 +1,37 @@
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 } from "./auth/resource";
28
+ import { defineSiteData } from "./data/resource";
29
+ import { resolvePlugins } from "./plugins/registry";
30
+ import { wirePlugins } from "./plugins/wire";
19
31
  import { assignmentAction } from "./functions/assignment-action/resource";
20
32
  import { categoryAction } from "./functions/category-action/resource";
21
33
  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";
34
+ import { contentActions, setSiteFilesForContentActionsBundle } from "./functions/content-actions/resource";
24
35
  import { knowledgeQuery } from "./functions/knowledge-query/resource";
25
36
  import { manageUserRole } from "./functions/manage-user-role/resource";
26
37
  import { modelAttachmentUpload } from "./functions/model-attachment-upload/resource";
@@ -31,7 +42,19 @@ import { emailSubmissionProcessor } from "./functions/email-submission-processor
31
42
  import { sesInboundReceive } from "./functions/ses-inbound-receive/resource";
32
43
  import { slackDelivery } from "./functions/slack-delivery/resource";
33
44
  import { slackEvents } from "./functions/slack-events/resource";
45
+ import { addInboundEmailSesIntake } from "./inbound-email/ses-intake";
34
46
  import { InboundEmailStack } from "./inbound-email/stack";
47
+ import {
48
+ deriveKnowledgeVectorIndexName,
49
+ deriveReceiptRuleName,
50
+ deriveReceiptRuleSetName,
51
+ deriveStorageBackupVaultName,
52
+ isLegacyProductionPipeline,
53
+ planInboundEmailSes,
54
+ resolveSiteBackendFeatureFlags,
55
+ type SiteBackendIdentity,
56
+ } from "./site-backend-names";
57
+ import { addStorageBackups } from "./storage-backups/construct";
35
58
  import { storage } from "./storage/resource";
36
59
  import type { PapyrusSite } from "../lib/define-site";
37
60
 
@@ -44,57 +67,48 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
44
67
 
45
68
  const amplifyBranch = (process.env.AWS_BRANCH ?? "").trim();
46
69
  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).
70
+ // Amplify app id whose `main` branch is the original p.apyr.us backend; only
71
+ // that app keeps the historical account-global names (see site-backend-names.ts).
49
72
  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
-
73
+ const siteBackendIdentity: SiteBackendIdentity = {
74
+ brandId: (
75
+ site.brandId
76
+ ?? process.env.PAPYRUS_SITE_BRAND
77
+ ?? process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND
78
+ ?? "papyrus"
79
+ ).trim().toLowerCase(),
80
+ amplifyAppId,
81
+ amplifyBranch,
82
+ productionAppId,
83
+ };
84
+ const isAmplifyProductionPipeline = isLegacyProductionPipeline(siteBackendIdentity);
85
+ const knowledgeVectorIndexName = deriveKnowledgeVectorIndexName(siteBackendIdentity);
86
+ const featureFlags = resolveSiteBackendFeatureFlags(site.features, process.env, siteBackendIdentity);
87
+ const enableInboundEmail = featureFlags.inboundEmail;
88
+ const enableSlackAgent = featureFlags.slack;
89
+ const enableStorageBackups = featureFlags.storageBackups;
90
+
91
+ const baseAuthConfig = site.auth ?? authConfigFromEnv();
92
+ const activeBrand = (papyrusSite.brands ?? []).find((brand) => brand.id.toLowerCase() === siteBackendIdentity.brandId);
93
+ const authConfig = {
94
+ ...baseAuthConfig,
95
+ newsroomBasePath: baseAuthConfig.newsroomBasePath ?? activeBrand?.newsroomBasePath,
96
+ };
97
+ const publication = resolveSitePublication(site, activeBrand?.renderer?.kind);
98
+ if (publication !== undefined) assertValidPublicationConfig(publication);
99
+ const publicationPlan = planS3ProjectionBackend(publication);
100
+ const renditionsPlan = planRenditions(publication, process.cwd());
101
+ setSiteFilesForContentActionsBundle(publicationPlan.enabled ? publicationPlan.siteFiles : undefined);
102
+ const plugins = resolvePlugins(site.plugins);
103
+ const pluginDefinitions = Object.assign({}, ...plugins.map(({ plugin }) => plugin.schemaDefinitions()));
104
+ const pluginFunctions: Record<never, never> = Object.assign({}, ...plugins.map(({ plugin }) => plugin.functions()));
91
105
  const backend = defineBackend({
106
+ ...pluginFunctions,
92
107
  assignmentAction,
93
- auth: defineSiteAuth(site.auth ?? authConfigFromEnv()),
108
+ auth: defineSiteAuth(authConfig),
94
109
  categoryAction,
95
110
  contentActions,
96
- data,
97
- graphqlJwtAuthorizer,
111
+ data: defineSiteData(pluginDefinitions),
98
112
  knowledgeQuery,
99
113
  manageUserRole,
100
114
  modelAttachmentUpload,
@@ -108,10 +122,25 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
108
122
  storage,
109
123
  });
110
124
 
125
+ orderModelStacksInWaves(
126
+ backend.data.resources.graphqlApi.node.scope as Construct,
127
+ Object.keys(backend.data.resources.tables),
128
+ site.resolverWaveSize ?? DEFAULT_RESOLVER_WAVE_SIZE,
129
+ );
130
+
131
+ const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
132
+ if (
133
+ authConfig.applyCognitoDomainPrefix === true
134
+ && requestedCognitoDomainPrefix !== ""
135
+ && authConfig.disableGoogleOAuth !== true
136
+ ) {
137
+ applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
138
+ }
139
+
111
140
  const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
112
141
  // Repo root in a Papyrus checkout; package root when installed from npm.
113
142
  const projectRoot = resolve(amplifyBackendDir, "..");
114
- const enableConsoleResponder = site.features?.consoleResponder ?? readEnvFlag("PAPYRUS_ENABLE_CONSOLE_RESPONDER", true);
143
+ const enableConsoleResponder = featureFlags.consoleResponder;
115
144
 
116
145
  if (
117
146
  enableConsoleResponder
@@ -160,43 +189,19 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
160
189
  const messageThreadTable = backend.data.resources.tables.MessageThread;
161
190
  const dataStack = Stack.of(messageTable);
162
191
  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
- || "";
192
+ const amplifySsmEnvConfig = process.env.AMPLIFY_SSM_ENV_CONFIG?.trim() || "";
175
193
 
176
194
  if (enableConsoleResponder) {
177
- const consoleChatResponder = new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
195
+ new ConsoleChatResponderStack(dataStack, "ConsoleChatResponder", {
178
196
  messageTable,
179
197
  messageStreamArn,
180
198
  threadTable: messageThreadTable,
181
199
  projectRoot,
182
200
  graphqlEndpoint,
183
- amplifySsmEnvConfig: jwtSsmEnvConfig || undefined,
184
201
  responseTarget: process.env.PAPYRUS_CONSOLE_RESPONSE_TARGET,
185
202
  model: process.env.PAPYRUS_CONSOLE_MODEL,
186
203
  prebuiltImageUri: process.env.PAPYRUS_CONSOLE_RESPONDER_IMAGE_URI,
187
204
  });
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
205
  }
201
206
 
202
207
  if (enableSlackAgent) {
@@ -217,9 +222,9 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
217
222
  "PAPYRUS_SLACK_ALLOWED_USER_IDS",
218
223
  (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim(),
219
224
  );
220
- if (jwtSsmEnvConfig) {
221
- backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
222
- backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", jwtSsmEnvConfig);
225
+ if (amplifySsmEnvConfig) {
226
+ backend.slackEvents.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
227
+ backend.slackDelivery.addEnvironment("AMPLIFY_SSM_ENV_CONFIG", amplifySsmEnvConfig);
223
228
  }
224
229
  backend.slackDelivery.addEnvironment("PAPYRUS_GRAPHQL_ENDPOINT", graphqlEndpoint);
225
230
  backend.slackDelivery.addEnvironment(
@@ -297,8 +302,8 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
297
302
  let storageBackupVault: backup.BackupVault | undefined;
298
303
 
299
304
  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.
305
+ if ((enableStorageBackups || enableInboundEmail || renditionsPlan.enabled) && storageBucketCfn) {
306
+ // Backups, inbound-email intake and upload-time renditions all rely on S3 → EventBridge notifications.
302
307
  storageBucketCfn.addPropertyOverride(
303
308
  "NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled",
304
309
  true,
@@ -307,41 +312,17 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
307
312
 
308
313
  if (enableStorageBackups) {
309
314
  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`),
315
+ const storageBackupVaultName = deriveStorageBackupVaultName(
316
+ siteBackendIdentity,
317
+ process.env.PAPYRUS_STORAGE_BACKUP_VAULT_NAME,
315
318
  );
316
319
 
317
- storageBackupVault = new backup.BackupVault(storageBackupsStack, "PapyrusStorageBackupVault", {
320
+ const storageBackups = addStorageBackups(storageBackupsStack, {
321
+ storageBucket,
318
322
  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
323
  });
324
+ storageBackupVault = storageBackups.vault;
325
+ storageBackupPlan = storageBackups.plan;
345
326
  }
346
327
 
347
328
  // Grant S3 access on Lambda roles only (not storage bucket policies) to avoid
@@ -430,9 +411,66 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
430
411
  new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
431
412
  );
432
413
  }
414
+ let s3ProjectionPublication: S3ProjectionPublication | undefined;
415
+ if (publication !== undefined) {
416
+ s3ProjectionPublication = addS3ProjectionPublication(backend.createStack("s3-projection"), publication, {
417
+ mediaBucket: storageBucket,
418
+ renditions: renditionsPlan,
419
+ });
420
+ (contentActionsLambda.node.defaultChild as CfnFunction).memorySize = publicationPlan.contentActionsMemorySize;
421
+ if (publication.reprojectOnDeploy === true) {
422
+ (contentActionsLambda.node.defaultChild as CfnFunction).timeout = REPROJECT_ON_DEPLOY_TIMEOUT_SECONDS;
423
+ addReprojectOnDeploy(backend.createStack("reproject-on-deploy"), contentActionsLambda, reprojectDeployToken(process.env));
424
+ }
425
+ }
426
+ if (s3ProjectionPublication !== undefined) {
427
+ addProjectionRetries(contentActionsLambda, s3ProjectionPublication.deadLetterQueue);
428
+ }
429
+ const publicationWiring = contentActionsPublicationWiring(s3ProjectionPublication);
430
+ for (const [name, value] of Object.entries(publicationWiring.environment)) {
431
+ contentActionsLambda.addEnvironment(name, value);
432
+ }
433
+ for (const statement of publicationWiring.policyStatements) {
434
+ contentActionsLambda.addToRolePolicy(statement);
435
+ }
433
436
  if ((site.revalidateBaseUrl ?? "").trim() !== "") {
434
437
  backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
435
- backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
438
+ const revalidateSecretParameter = (process.env.PAPYRUS_REVALIDATE_SECRET_PARAMETER ?? "").trim();
439
+ if (!revalidateSecretParameter.startsWith("/")) {
440
+ throw new Error(
441
+ "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.",
442
+ );
443
+ }
444
+ backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET_PARAMETER", revalidateSecretParameter);
445
+ const revalidateStack = Stack.of(contentActionsLambda);
446
+ contentActionsLambda.addToRolePolicy(
447
+ new PolicyStatement(
448
+ revalidateSecretReadStatement({
449
+ partition: revalidateStack.partition,
450
+ region: revalidateStack.region,
451
+ account: revalidateStack.account,
452
+ parameterName: revalidateSecretParameter,
453
+ }),
454
+ ),
455
+ );
456
+ const distributionProblems = validateRevalidateDistributionIds(site.revalidateDistributionIds, site.revalidateBaseUrl);
457
+ if (distributionProblems.length > 0) throw new Error(`Invalid backend config: ${distributionProblems.join("; ")}`);
458
+ const distributionIds = site.revalidateDistributionIds ?? [];
459
+ if (distributionIds.length > 0) {
460
+ for (const [name, value] of Object.entries(revalidateDistributionEnvironment(distributionIds))) {
461
+ backend.contentActions.addEnvironment(name, value);
462
+ }
463
+ contentActionsLambda.addToRolePolicy(
464
+ new PolicyStatement(
465
+ revalidateInvalidationStatement({ partition: revalidateStack.partition, account: revalidateStack.account, distributionIds }),
466
+ ),
467
+ );
468
+ for (const id of distributionIds) {
469
+ addInvalidationPathAlarm(revalidateStack, `ReaderEdgeInvalidationPaths${id}`, id);
470
+ }
471
+ }
472
+ } else if (site.revalidateDistributionIds !== undefined) {
473
+ throw new Error("Invalid backend config: revalidateDistributionIds requires revalidateBaseUrl");
436
474
  }
437
475
  grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
438
476
  grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
@@ -527,86 +565,26 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
527
565
 
528
566
  const inboundEventStack = Stack.of(receiveLambda);
529
567
 
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
- });
568
+ // SES allows one active receipt rule set per region, so a non-legacy site
569
+ // creates its own brand-named rule set and never activates it here.
570
+ const inboundSesPlan = planInboundEmailSes(siteBackendIdentity, enableInboundEmail, site.inboundEmailSes);
571
+ if (inboundSesPlan.createReceiptRules) {
572
+ addInboundEmailSesIntake(storageStack, {
573
+ storageBucket,
574
+ recipients: inboundRecipients,
575
+ ruleSetName: deriveReceiptRuleSetName(siteBackendIdentity, inboundEmailDomain),
576
+ existingRuleSetName: site.inboundEmailSes?.existingReceiptRuleSetName,
577
+ ruleName: deriveReceiptRuleName(siteBackendIdentity),
578
+ activateRuleSet: inboundSesPlan.activateReceiptRuleSet,
579
+ domainIdentity: inboundSesPlan.manageDomainIdentity
580
+ ? {
581
+ domain: inboundEmailDomain,
582
+ hostedZoneId: inboundDnsZoneId,
583
+ hostedZoneName: inboundDnsZoneName,
584
+ recordName: inboundDnsRecordName,
585
+ }
586
+ : undefined,
587
+ });
610
588
  }
611
589
 
612
590
  new InboundEmailStack(inboundEventStack, "InboundEmail", {
@@ -735,5 +713,7 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
735
713
  : null,
736
714
  },
737
715
  });
716
+ wirePlugins(backend, plugins, contentActionsLambda);
717
+
738
718
  return backend;
739
719
  }
@@ -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();
@@ -2,8 +2,13 @@ import { notFound, redirect } from "next/navigation";
2
2
  import { EditionRoutePage } from "../../edition-route-page";
3
3
  import { createSectionKey } from "../../../../../../lib/edition-sections";
4
4
  import { parseEditionSectionRoute } from "../../../../../../lib/edition-routes";
5
+ import { generateEditionSectionStaticParams } from "../../../../../../lib/reader-static-params";
5
6
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
6
- export const revalidate = 3600;
7
+ export const revalidate = 60;
8
+
9
+ export async function generateStaticParams() {
10
+ return generateEditionSectionStaticParams();
11
+ }
7
12
 
8
13
  type EditionSectionRouteProps = {
9
14
  params: Promise<{