@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.41

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 (147) 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 +28 -70
  6. package/amplify/data/resource.ts +29 -70
  7. package/amplify/data/schema.js +33 -58
  8. package/amplify/data/schema.ts +34 -58
  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 +45 -3
  15. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  16. package/amplify/inbound-email/ses-intake.js +86 -0
  17. package/amplify/inbound-email/ses-intake.ts +110 -0
  18. package/amplify/rebuild-trigger-settings.js +22 -0
  19. package/amplify/rebuild-trigger-settings.ts +42 -0
  20. package/amplify/resolver-waves.js +26 -0
  21. package/amplify/resolver-waves.ts +34 -0
  22. package/amplify/site-backend-config.ts +36 -1
  23. package/amplify/site-backend-names.js +103 -0
  24. package/amplify/site-backend-names.ts +166 -0
  25. package/amplify/site-backend.js +93 -170
  26. package/amplify/site-backend.ts +126 -196
  27. package/amplify/storage/resource.js +3 -0
  28. package/amplify/storage/resource.ts +3 -0
  29. package/amplify/storage-backups/construct.js +31 -0
  30. package/amplify/storage-backups/construct.ts +47 -0
  31. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  32. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
  33. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  34. package/app/api/media/[...path]/route.ts +5 -16
  35. package/app/api/revalidate/route.ts +8 -7
  36. package/app/articles/[slug]/page.tsx +6 -4
  37. package/app/blog-rhythm.css +278 -0
  38. package/app/globals.css +21 -0
  39. package/app/layout.tsx +16 -0
  40. package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
  41. package/app/newsroom/[section]/page.tsx +4 -3
  42. package/app/newsroom/articles/[id]/page.tsx +15 -0
  43. package/app/newsroom/articles/page.tsx +13 -0
  44. package/app/newsroom/layout.tsx +4 -9
  45. package/app/page.tsx +18 -5
  46. package/app/robots.ts +2 -5
  47. package/bin/papyrus-app.mjs +12 -2
  48. package/bin/papyrus-infra.mjs +29 -3
  49. package/components/amplify-client-provider.tsx +4 -22
  50. package/components/archive-shell.tsx +2 -22
  51. package/components/article-video.tsx +247 -3
  52. package/components/blog-page-background.tsx +23 -6
  53. package/components/markus-ir-preview.tsx +300 -0
  54. package/components/news-desk-taxonomy-client.ts +51 -0
  55. package/components/newspaper.tsx +6 -2
  56. package/components/newsroom-access-gate.tsx +67 -0
  57. package/components/newsroom-article-editor.tsx +741 -0
  58. package/components/newsroom-articles-view.tsx +129 -0
  59. package/components/newsroom-client-shell.tsx +12 -0
  60. package/components/newsroom-ops-overview.tsx +28 -11
  61. package/components/newsroom-ops-shell.tsx +3 -1
  62. package/components/newsroom-references-view.tsx +83 -8
  63. package/components/papyrus-console-shell.tsx +4 -2
  64. package/components/pictogram-figure.tsx +9 -2
  65. package/components/presentation-header.tsx +135 -0
  66. package/components/presentation-rhythm-hrule.tsx +17 -0
  67. package/components/reference-cyclotron-status.tsx +109 -0
  68. package/components/reference-relevance-review.tsx +75 -0
  69. package/components/settings-page.tsx +2 -1
  70. package/components/topic-steering-workspace.tsx +54 -64
  71. package/components/use-blog-header-obstacles.ts +81 -0
  72. package/components/use-brand-fonts-ready.ts +28 -0
  73. package/components/use-rhythm-overlay.ts +29 -0
  74. package/infra/amplify-app-shell.js +228 -50
  75. package/infra/amplify-app-shell.ts +256 -53
  76. package/infra/build-specs.js +15 -5
  77. package/infra/build-specs.ts +18 -7
  78. package/infra/github-oidc-provider.js +24 -0
  79. package/infra/github-oidc-provider.ts +30 -0
  80. package/infra/index.js +1 -0
  81. package/infra/site-config.js +236 -11
  82. package/infra/site-config.ts +276 -15
  83. package/lib/amplify-outputs-path.ts +5 -1
  84. package/lib/blog-feature-solver.ts +459 -0
  85. package/lib/blog-rhythm.ts +134 -0
  86. package/lib/brand-fonts-watch.ts +43 -0
  87. package/lib/brand-fonts.ts +112 -0
  88. package/lib/cached-content-repository.ts +12 -0
  89. package/lib/category-repository.ts +42 -93
  90. package/lib/content-actions-demo.ts +190 -0
  91. package/lib/content-repository.ts +3 -0
  92. package/lib/content-source-context.ts +12 -2
  93. package/lib/content-types.ts +4 -0
  94. package/lib/edition-routes.ts +31 -29
  95. package/lib/graphql-content-repository.ts +61 -9
  96. package/lib/media-route-response.ts +45 -0
  97. package/lib/newspaper-layout.ts +3 -2
  98. package/lib/newsroom-articles.ts +325 -0
  99. package/lib/newsroom-base-path.ts +12 -3
  100. package/lib/newsroom-category-drilldown.ts +3 -2
  101. package/lib/newsroom-demo-articles.ts +90 -0
  102. package/lib/newsroom-forum-routes.ts +6 -4
  103. package/lib/newsroom-index-filters.ts +14 -10
  104. package/lib/newsroom-media-backend.ts +142 -0
  105. package/lib/newsroom-media.ts +182 -0
  106. package/lib/newsroom-nav.ts +3 -0
  107. package/lib/oauth-redirect-priority.ts +40 -0
  108. package/lib/papyrus-web-locations.ts +7 -6
  109. package/lib/presentation-footer.ts +3 -2
  110. package/lib/pretext-layout.ts +12 -3
  111. package/lib/reader-base-path.ts +24 -0
  112. package/lib/reader-static-params.ts +13 -0
  113. package/lib/reference-policy.ts +5 -0
  114. package/lib/relevance-decisions.ts +103 -0
  115. package/lib/relevance-status.ts +63 -0
  116. package/lib/renderer.ts +7 -0
  117. package/lib/revalidate-secret.ts +59 -0
  118. package/lib/robots-policy.ts +14 -0
  119. package/lib/root-route-redirect.ts +16 -0
  120. package/lib/semantic-graph.ts +7 -0
  121. package/lib/site-brand.ts +51 -0
  122. package/lib/staging-gated-path.ts +23 -0
  123. package/lib/staging-preview-object.ts +114 -0
  124. package/lib/video-mode.ts +42 -0
  125. package/lib/video-script.ts +77 -0
  126. package/middleware.ts +23 -18
  127. package/package.json +45 -1
  128. package/renderers/pretext/article-page.tsx +39 -3
  129. package/renderers/pretext/presentation-shared.tsx +113 -0
  130. package/renderers/pretext/presentation-shell.tsx +15 -102
  131. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  132. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  133. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  134. package/routes.manifest.json +45 -2
  135. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  136. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  137. package/scripts/videoml/bundle-common.mjs +129 -0
  138. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  139. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  140. package/scripts/videoml/preview/preview-timing.ts +42 -0
  141. package/scripts/videoml/preview.html +77 -0
  142. package/scripts/videoml/standard-entry.tsx +46 -0
  143. package/src/sync.mjs +30 -6
  144. package/src/with-papyrus.mjs +19 -2
  145. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  146. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  147. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
@@ -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
@@ -45,6 +45,7 @@
45
45
  --color-surface-foreground: var(--surface-foreground);
46
46
  --color-foreground-strong: var(--foreground-strong);
47
47
  --color-foreground-muted: var(--foreground-muted);
48
+ --color-foreground-subtle: var(--foreground-subtle);
48
49
  }
49
50
 
50
51
  :root {
@@ -71,6 +72,7 @@
71
72
  --surface-foreground: var(--card-foreground);
72
73
  --foreground-strong: var(--slate-12);
73
74
  --foreground-muted: var(--slate-10);
75
+ --foreground-subtle: var(--slate-9);
74
76
  --border: var(--slate-6);
75
77
  --input: var(--slate-6);
76
78
  --ring: var(--slate-8);
@@ -2282,6 +2284,25 @@ img {
2282
2284
  width: 100%;
2283
2285
  }
2284
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
+
2285
2306
  .article-video__credit {
2286
2307
  color: rgb(var(--ink-solid-rgb) / 0.62);
2287
2308
  font-size: 0.82rem;
package/app/layout.tsx CHANGED
@@ -4,12 +4,14 @@ import Script from "next/script";
4
4
  import { AmplifyClientProvider } from "../components/amplify-client-provider";
5
5
  import { PapyrusConsoleShell } from "../components/papyrus-console-shell";
6
6
  import { TooltipProvider } from "../components/ui/tooltip";
7
+ import { resolveBrandFontAssets } from "../lib/brand-fonts";
7
8
  import { getPresentationChoices, getSiteBrand } from "../lib/site-brand";
8
9
  import { getSiteStack } from "../lib/site-stack";
9
10
  import { isIndexable, showStagingBanner } from "../lib/site-env";
10
11
  import { StagingBanner } from "../components/staging-banner";
11
12
  import "./tailwind.css";
12
13
  import "./globals.css";
14
+ import "./blog-rhythm.css";
13
15
  import "papyrus-site-theme";
14
16
 
15
17
  const playfairDisplay = Playfair_Display({
@@ -43,6 +45,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
43
45
  const siteBrand = getSiteBrand();
44
46
  const presentationChoices = getPresentationChoices(siteBrand);
45
47
  const siteStack = getSiteStack(siteBrand);
48
+ const brandFonts = resolveBrandFontAssets(siteBrand.fonts);
46
49
 
47
50
  return (
48
51
  <html
@@ -56,8 +59,21 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
56
59
  data-default-presentation={siteBrand.defaultPresentation}
57
60
  data-presentation-choices={presentationChoices.join(",")}
58
61
  {...(siteBrand.forcedPresentation ? { "data-forced-presentation": siteBrand.forcedPresentation } : {})}
62
+ {...(siteBrand.blogLayout === "rhythm" ? { "data-blog-layout": "rhythm" } : {})}
63
+ {...(brandFonts.isEmpty ? {} : { style: brandFonts.rootStyle as React.CSSProperties })}
59
64
  suppressHydrationWarning
60
65
  >
66
+ {brandFonts.isEmpty ? null : (
67
+ <head>
68
+ {brandFonts.preconnectOrigins.map((origin) => (
69
+ <link crossOrigin="anonymous" href={origin} key={origin} rel="preconnect" />
70
+ ))}
71
+ {brandFonts.stylesheetHrefs.map((href) => (
72
+ <link href={href} key={href} rel="stylesheet" />
73
+ ))}
74
+ {brandFonts.fontFaceCss ? <style data-brand-fonts="true">{brandFonts.fontFaceCss}</style> : null}
75
+ </head>
76
+ )}
61
77
  <body className={`${playfairDisplay.variable} ${plusJakartaSans.variable}`}>
62
78
  <Script id="papyrus-favicon-color-scheme" strategy="beforeInteractive">
63
79
  {`(() => {
@@ -1,4 +1,5 @@
1
1
  import { notFound, redirect } from "next/navigation";
2
+ import { toPublicNewsroomPath } from "../../../../lib/newsroom-base-path";
2
3
  import { NewsDeskPage } from "../../../../components/news-desk-page";
3
4
  import type { NewsDeskTab } from "../../../../components/topic-steering-workspace";
4
5
 
@@ -37,13 +38,13 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
37
38
 
38
39
  export default async function NewsDeskNestedSectionPage({ params, searchParams }: NewsDeskNestedSectionPageProps) {
39
40
  const { section, selection } = await params;
40
- if (section === "users") redirect("/newsroom/administration/users");
41
- if (section === "doctrine") redirect("/newsroom/administration/policies");
42
- if (section === "administration" && selection[0] === "analysis") redirect("/newsroom/assignments");
41
+ if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
42
+ if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
43
+ if (section === "administration" && selection[0] === "analysis") redirect(toPublicNewsroomPath("/newsroom/assignments"));
43
44
  if (section === "desks") {
44
45
  const category = selection[0] ? decodeURIComponent(selection[0]) : null;
45
- if (category) redirect(`/newsroom/topics?category=${encodeURIComponent(category)}`);
46
- redirect("/newsroom/topics");
46
+ if (category) redirect(toPublicNewsroomPath(`/newsroom/topics?category=${encodeURIComponent(category)}`));
47
+ redirect(toPublicNewsroomPath("/newsroom/topics"));
47
48
  }
48
49
  if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
49
50
  return <NewsDeskPage section={section} selectionPath={selection} searchParams={searchParams} />;
@@ -1,4 +1,5 @@
1
1
  import { notFound, redirect } from "next/navigation";
2
+ import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
2
3
  import { NewsDeskPage } from "../../../components/news-desk-page";
3
4
  import type { NewsDeskTab } from "../../../components/topic-steering-workspace";
4
5
 
@@ -39,9 +40,9 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
39
40
 
40
41
  export default async function NewsDeskSectionPage({ params, searchParams }: NewsDeskSectionPageProps) {
41
42
  const { section } = await params;
42
- if (section === "users") redirect("/newsroom/administration/users");
43
- if (section === "doctrine") redirect("/newsroom/administration/policies");
44
- if (section === "desks") redirect("/newsroom/topics");
43
+ if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
44
+ if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
45
+ if (section === "desks") redirect(toPublicNewsroomPath("/newsroom/topics"));
45
46
  if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
46
47
  return <NewsDeskPage section={section} searchParams={searchParams} />;
47
48
  }
@@ -0,0 +1,15 @@
1
+ import { NewsroomArticleEditor } from "../../../../components/newsroom-article-editor";
2
+
3
+ export const dynamic = "force-dynamic";
4
+
5
+ type NewsroomArticleEditorPageProps = {
6
+ params: Promise<{ id: string }>;
7
+ searchParams?: Promise<{ demo?: string | string[] }>;
8
+ };
9
+
10
+ export default async function NewsroomArticleEditorPage({ params, searchParams }: NewsroomArticleEditorPageProps) {
11
+ const { id } = await params;
12
+ const resolved = await searchParams;
13
+ const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
14
+ return <NewsroomArticleEditor articleId={decodeURIComponent(id)} demo={demoValue === "1"} />;
15
+ }
@@ -0,0 +1,13 @@
1
+ import { NewsroomArticlesView } from "../../../components/newsroom-articles-view";
2
+
3
+ export const dynamic = "force-dynamic";
4
+
5
+ type NewsroomArticlesPageProps = {
6
+ searchParams?: Promise<{ demo?: string | string[] }>;
7
+ };
8
+
9
+ export default async function NewsroomArticlesPage({ searchParams }: NewsroomArticlesPageProps) {
10
+ const resolved = await searchParams;
11
+ const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
12
+ return <NewsroomArticlesView demo={demoValue === "1"} />;
13
+ }
@@ -1,12 +1,7 @@
1
- import { NewsDeskClientProvider } from "../../components/news-desk-client-provider";
2
- import { resolveDevSandboxEditorAuth } from "../../lib/dev-sandbox-editor-auth";
1
+ // Component rules only (no reset, no theme): the Cyclotron review control and status widget.
2
+ import "cyclotron/styles/components.css";
3
+ import { NewsroomClientShell } from "../../components/newsroom-client-shell";
3
4
 
4
5
  export default function NewsroomLayout({ children }: Readonly<{ children: React.ReactNode }>) {
5
- const devSandboxEditorAuth = resolveDevSandboxEditorAuth();
6
-
7
- return (
8
- <NewsDeskClientProvider devSandboxEditorAuth={devSandboxEditorAuth}>
9
- {children}
10
- </NewsDeskClientProvider>
11
- );
6
+ return <NewsroomClientShell>{children}</NewsroomClientShell>;
12
7
  }
package/app/page.tsx CHANGED
@@ -15,7 +15,10 @@ import {
15
15
  } from "../lib/public-placeholder-config";
16
16
  import type { PublicationItem } from "../lib/publication-items";
17
17
  import { SITE_BRAND, rootRoute } from "../lib/site-brand";
18
- import { redirect } from "next/navigation";
18
+ import { notFound, redirect } from "next/navigation";
19
+ import { getReaderBasePath } from "../lib/reader-base-path";
20
+ import { buildRootRedirectTarget } from "../lib/root-route-redirect";
21
+ import { NewsroomClientShell } from "../components/newsroom-client-shell";
19
22
  import NewsDeskRootPage, { type NewsDeskRootSearchParams } from "./newsroom/page";
20
23
 
21
24
  export const dynamic = "force-dynamic";
@@ -37,12 +40,22 @@ export default async function Home({ searchParams }: HomePageProps) {
37
40
  // co-hosts reader + CMS uses the default "reader" (render the publication
38
41
  // home page here). A CMS-only site points the root at the newsroom or
39
42
  // anywhere else. Preserve the scenario and OAuth-callback paths.
40
- if (rootRoute.kind === "newsroom" && !scenarioId) {
41
- return <NewsDeskRootPage searchParams={searchParams} />;
43
+ const publicationOwnsRoot = getReaderBasePath() !== "";
44
+
45
+ if (!publicationOwnsRoot && rootRoute.kind === "newsroom" && !scenarioId) {
46
+ return (
47
+ <NewsroomClientShell>
48
+ <NewsDeskRootPage searchParams={searchParams} />
49
+ </NewsroomClientShell>
50
+ );
51
+ }
52
+
53
+ if (!publicationOwnsRoot && rootRoute.kind === "redirect") {
54
+ redirect(buildRootRedirectTarget(rootRoute.destination, resolvedSearchParams));
42
55
  }
43
56
 
44
- if (rootRoute.kind === "redirect" && !scenarioId && !hasOAuthRedirectParams(resolvedSearchParams)) {
45
- redirect(rootRoute.destination);
57
+ if (SITE_BRAND.renderer.kind === "markus") {
58
+ notFound();
46
59
  }
47
60
 
48
61
  // Resolved only on the reader path: Markus brands (renderer "markus") have
package/app/robots.ts CHANGED
@@ -1,11 +1,8 @@
1
1
  import type { MetadataRoute } from "next";
2
- import { isIndexable } from "../lib/site-env";
2
+ import { buildRobots } from "../lib/robots-policy";
3
3
 
4
4
  export const dynamic = "force-dynamic";
5
5
 
6
6
  export default function robots(): MetadataRoute.Robots {
7
- if (isIndexable()) {
8
- return { rules: { userAgent: "*", allow: "/" } };
9
- }
10
- return { rules: { userAgent: "*", disallow: "/" } };
7
+ return buildRobots();
11
8
  }
@@ -1,10 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { sync } from "../src/sync.mjs";
3
3
 
4
- const [command] = process.argv.slice(2);
4
+ const [command, ...rest] = process.argv.slice(2);
5
5
  if (command === "sync") {
6
6
  sync();
7
+ } else if (command === "videoml-bundle") {
8
+ const preview = rest.includes("--preview");
9
+ const { buildBrowserBundle } = await import("../scripts/videoml/build-browser-bundle.mjs");
10
+ const { buildPreviewBundle } = await import("../scripts/videoml/build-preview-bundle.mjs");
11
+ try {
12
+ await (preview ? buildPreviewBundle() : buildBrowserBundle());
13
+ } catch (error) {
14
+ console.error(error.message);
15
+ process.exit(1);
16
+ }
7
17
  } else {
8
- console.error("Usage: papyrus-app sync");
18
+ console.error("Usage: papyrus-app sync | papyrus-app videoml-bundle [--preview]");
9
19
  process.exit(command ? 1 : 0);
10
20
  }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]
4
+ * papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]
4
5
  * papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader
5
6
  * Synthesizes the app-shell stack for a publication from its own site.json, or
6
7
  * prints one of the generated Amplify build specs. Synth only: deploying is a
@@ -10,10 +11,11 @@ import fs from "node:fs";
10
11
  import path from "node:path";
11
12
  import { createRequire } from "node:module";
12
13
  import { buildSpecFor } from "../infra/build-specs.js";
13
- import { parseSiteConfig } from "../infra/site-config.js";
14
+ import { parseSiteConfig, resolveStackName } from "../infra/site-config.js";
14
15
 
15
16
  const USAGE = [
16
17
  "Usage: papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]",
18
+ " papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]",
17
19
  " papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader",
18
20
  ].join("\n");
19
21
 
@@ -28,6 +30,30 @@ if (command !== "synth" && command !== "print-buildspec") {
28
30
  process.exit(command ? 1 : 0);
29
31
  }
30
32
 
33
+ const accountStack = opt("account-stack");
34
+ if (accountStack !== undefined) {
35
+ if (command !== "synth" || accountStack !== "github-oidc") {
36
+ console.error(USAGE);
37
+ process.exit(1);
38
+ }
39
+ let GithubOidcProviderStack;
40
+ try {
41
+ ({ GithubOidcProviderStack } = await import("../infra/index.js"));
42
+ } catch (error) {
43
+ console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
44
+ process.exit(1);
45
+ }
46
+ const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib");
47
+ const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
48
+ new GithubOidcProviderStack(app, "GithubOidcProviderStack", {
49
+ env: { account: opt("account", "335163751677"), region: opt("region", "us-east-1") },
50
+ stackName: "github-oidc-provider",
51
+ });
52
+ app.synth();
53
+ console.log(`synthesized github-oidc-provider -> ${path.resolve(opt("out", "cdk.out"))}`);
54
+ process.exit(0);
55
+ }
56
+
31
57
  let config;
32
58
  try {
33
59
  config = parseSiteConfig(JSON.parse(fs.readFileSync(path.resolve(opt("site", "infra/site.json")), "utf8")));
@@ -63,7 +89,7 @@ const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib"
63
89
  const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
64
90
  new AmplifyAppShellStack(app, `AmplifyAppShell-${config.siteId}`, config, {
65
91
  env: { account: opt("account", "335163751677"), region: opt("region", "us-east-1") },
66
- stackName: `amplify-app-shell-${config.siteId}`,
92
+ stackName: resolveStackName(config),
67
93
  });
68
94
  app.synth();
69
- console.log(`synthesized amplify-app-shell-${config.siteId} -> ${path.resolve(opt("out", "cdk.out"))}`);
95
+ console.log(`synthesized ${resolveStackName(config)} -> ${path.resolve(opt("out", "cdk.out"))}`);