@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
@@ -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<{
@@ -3,7 +3,7 @@ import { loadArchiveEditionPreviews, clampArchiveLimit } from "../../../../lib/a
3
3
  import { ARCHIVE_BATCH_SIZE } from "../../../../lib/archive-types";
4
4
 
5
5
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
6
- export const revalidate = 3600;
6
+ export const revalidate = 60;
7
7
 
8
8
  export async function GET(request: Request) {
9
9
  const url = new URL(request.url);
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleProspectusSignInCallback } from "../../../../../lib/prospectus-sign-in-routes";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function GET(request: NextRequest) {
8
+ return handleProspectusSignInCallback(request);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleProspectusAccountSelection } from "../../../../../lib/prospectus-sign-in-routes";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function POST(request: NextRequest) {
8
+ return handleProspectusAccountSelection(request);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleProspectusSessionRead } from "../../../../../lib/prospectus-sign-in-routes";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function GET(request: NextRequest) {
8
+ return handleProspectusSessionRead(request);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleProspectusSignInStart } from "../../../../../lib/prospectus-sign-in-routes";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function GET(request: NextRequest) {
8
+ return handleProspectusSignInStart(request);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { handleProspectusSignOut } from "../../../../../lib/prospectus-sign-in-routes";
3
+
4
+ export const dynamic = "force-dynamic";
5
+ export const runtime = "nodejs";
6
+
7
+ export function POST(request: NextRequest) {
8
+ return handleProspectusSignOut(request);
9
+ }
@@ -0,0 +1,11 @@
1
+ import { NextResponse } from "next/server";
2
+ import { approvedPrimingProofReport } from "../../../../lib/priming-proof-report";
3
+ import { SITE_BRAND } from "../../../../lib/site-brand";
4
+
5
+ export function GET() {
6
+ const report = approvedPrimingProofReport(SITE_BRAND.id);
7
+ if (report === null) {
8
+ return NextResponse.json({ error: "No approved priming proof report for this publication yet." }, { status: 404 });
9
+ }
10
+ return NextResponse.json(report);
11
+ }
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { buildMediaRouteResponse } from "../../../../lib/media-route-response";
2
3
  import { isGuestReadableStoragePath, signStorageUrl } from "../../../../lib/reader-storage-url";
3
4
 
4
5
  export const dynamic = "force-dynamic";
@@ -16,20 +17,8 @@ export async function GET(_request: Request, context: MediaRouteContext) {
16
17
  return NextResponse.json({ error: "Forbidden storage path." }, { status: 403 });
17
18
  }
18
19
 
19
- const signedUrl = await signStorageUrl(storagePath);
20
- const upstream = await fetch(signedUrl, { cache: "no-store" });
21
- if (!upstream.ok) {
22
- return NextResponse.json(
23
- { error: `Upstream media fetch failed (${upstream.status}).` },
24
- { status: upstream.status === 404 ? 404 : 502 },
25
- );
26
- }
27
-
28
- const headers = new Headers();
29
- const contentType = upstream.headers.get("content-type");
30
- if (contentType) {
31
- headers.set("content-type", contentType);
32
- }
33
- headers.set("cache-control", "public, max-age=3600, stale-while-revalidate=86400");
34
- return new NextResponse(upstream.body, { status: 200, headers });
20
+ return buildMediaRouteResponse(storagePath, {
21
+ signStorageUrl,
22
+ fetchUpstream: (signedUrl) => fetch(signedUrl, { cache: "no-store" }),
23
+ });
35
24
  }
@@ -1,3 +1,4 @@
1
+ import { toPublicReaderPath } from "../../../lib/reader-base-path";
1
2
  import { revalidatePath, revalidateTag } from "next/cache";
2
3
  import { NextResponse } from "next/server";
3
4
  import {
@@ -8,6 +9,8 @@ import {
8
9
  editionContentCacheTag,
9
10
  editionItemCacheTag,
10
11
  } from "../../../lib/cached-content-repository";
12
+ import { isAuthorizedRevalidateRequest } from "../../../lib/revalidate-secret";
13
+ import { prewarmPaths, resolvePrewarmOrigin } from "../../../lib/revalidate-prewarm";
11
14
  import { getEditionArticlePath, getEditionDatePath } from "../../../lib/edition-routes";
12
15
 
13
16
  export const dynamic = "force-dynamic";
@@ -18,12 +21,12 @@ type RevalidateRequestBody = {
18
21
  itemSlugs?: string[];
19
22
  paths?: string[];
20
23
  tags?: string[];
24
+ prewarm?: boolean;
21
25
  };
22
26
 
23
27
  export async function POST(request: Request) {
24
- const configuredSecret = process.env.PAPYRUS_REVALIDATE_SECRET?.trim();
25
- const providedSecret = request.headers.get("x-papyrus-revalidate-secret")?.trim();
26
- if (!configuredSecret || providedSecret !== configuredSecret) {
28
+ const providedSecret = request.headers.get("x-papyrus-revalidate-secret");
29
+ if (!(await isAuthorizedRevalidateRequest(providedSecret))) {
27
30
  return NextResponse.json({ ok: false, error: "Unauthorized." }, { status: 401 });
28
31
  }
29
32
 
@@ -44,8 +47,8 @@ export async function POST(request: Request) {
44
47
  if (!normalized) continue;
45
48
  revalidateTag(articleCacheTag(normalized));
46
49
  revalidatedTags.add(articleCacheTag(normalized));
47
- revalidatePath(`/articles/${encodeURIComponent(normalized)}`);
48
- revalidatedPaths.add(`/articles/${encodeURIComponent(normalized)}`);
50
+ revalidatePath(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
51
+ revalidatedPaths.add(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
49
52
  if (editionDate) {
50
53
  revalidateTag(editionItemCacheTag(editionDate, normalized));
51
54
  revalidatedTags.add(editionItemCacheTag(editionDate, normalized));
@@ -76,8 +79,8 @@ export async function POST(request: Request) {
76
79
  revalidatedTags.add(ARCHIVE_CACHE_TAG);
77
80
  revalidateTag(ARTICLES_CACHE_TAG);
78
81
  revalidatedTags.add(ARTICLES_CACHE_TAG);
79
- revalidatePath("/archive");
80
- revalidatedPaths.add("/archive");
82
+ revalidatePath(toPublicReaderPath("/archive"));
83
+ revalidatedPaths.add(toPublicReaderPath("/archive"));
81
84
 
82
85
  for (const path of body.paths ?? []) {
83
86
  const normalized = path.trim();
@@ -86,9 +89,14 @@ export async function POST(request: Request) {
86
89
  revalidatedPaths.add(normalized);
87
90
  }
88
91
 
92
+ const prewarmed = body.prewarm === false
93
+ ? []
94
+ : await prewarmPaths(resolvePrewarmOrigin(request.url), [...revalidatedPaths]);
95
+
89
96
  return NextResponse.json({
90
97
  ok: true,
91
98
  revalidatedTags: [...revalidatedTags],
92
99
  revalidatedPaths: [...revalidatedPaths],
100
+ prewarmed,
93
101
  });
94
102
  }
@@ -4,7 +4,7 @@ import { loadArchiveEditionPreviews } from "../../lib/archive-data";
4
4
  import { ARCHIVE_BATCH_SIZE } from "../../lib/archive-types";
5
5
 
6
6
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
7
- export const revalidate = 3600;
7
+ export const revalidate = 60;
8
8
 
9
9
  export default async function ArchivePage() {
10
10
  const initialBatch = await loadArchiveEditionPreviews({ limit: ARCHIVE_BATCH_SIZE });
@@ -1,11 +1,11 @@
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
8
- export const revalidate = 3600;
8
+ export const revalidate = 60;
9
9
 
10
10
  export async function generateStaticParams() {
11
11
  return generateArticleStaticParams();
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ import type { Metadata } from "next";
2
+ import { PrimingProofPage } from "../../components/priming-proof-page";
3
+ import { approvedPrimingProofReport } from "../../lib/priming-proof-report";
4
+ import { SITE_BRAND } from "../../lib/site-brand";
5
+
6
+ export const metadata: Metadata = {
7
+ title: `Cyclotron proof | ${SITE_BRAND.articleTitleSuffix}`,
8
+ description: "How often the relevance classifier agrees with editors on a fixed held-out set, before and after editor labels.",
9
+ };
10
+
11
+ export default function CyclotronProof() {
12
+ return <PrimingProofPage report={approvedPrimingProofReport(SITE_BRAND.id)} />;
13
+ }
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);
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,6 @@
1
+ import { resolveActiveSiteBrand } from "../../../../lib/site-brand-tenant";
1
2
  import { notFound, redirect } from "next/navigation";
3
+ import { toPublicNewsroomPath } from "../../../../lib/newsroom-base-path";
2
4
  import { NewsDeskPage } from "../../../../components/news-desk-page";
3
5
  import type { NewsDeskTab } from "../../../../components/topic-steering-workspace";
4
6
 
@@ -26,6 +28,7 @@ type NewsDeskNestedSectionPageProps = {
26
28
  };
27
29
 
28
30
  const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
31
+ "desks",
29
32
  "administration",
30
33
  "topics",
31
34
  "concepts",
@@ -37,13 +40,13 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
37
40
 
38
41
  export default async function NewsDeskNestedSectionPage({ params, searchParams }: NewsDeskNestedSectionPageProps) {
39
42
  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");
43
- if (section === "desks") {
43
+ if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
44
+ if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
45
+ if (section === "administration" && selection[0] === "analysis") redirect(toPublicNewsroomPath("/newsroom/assignments"));
46
+ if (section === "desks" && resolveActiveSiteBrand().newsroomLayout !== "organizer") {
44
47
  const category = selection[0] ? decodeURIComponent(selection[0]) : null;
45
- if (category) redirect(`/newsroom/topics?category=${encodeURIComponent(category)}`);
46
- redirect("/newsroom/topics");
48
+ if (category) redirect(toPublicNewsroomPath(`/newsroom/topics?category=${encodeURIComponent(category)}`));
49
+ redirect(toPublicNewsroomPath("/newsroom/topics"));
47
50
  }
48
51
  if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
49
52
  return <NewsDeskPage section={section} selectionPath={selection} searchParams={searchParams} />;
@@ -1,4 +1,6 @@
1
+ import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
1
2
  import { notFound, redirect } from "next/navigation";
3
+ import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
2
4
  import { NewsDeskPage } from "../../../components/news-desk-page";
3
5
  import type { NewsDeskTab } from "../../../components/topic-steering-workspace";
4
6
 
@@ -27,6 +29,7 @@ type NewsDeskSectionPageProps = {
27
29
  };
28
30
 
29
31
  const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
32
+ "desks",
30
33
  "administration",
31
34
  "topics",
32
35
  "concepts",
@@ -39,9 +42,9 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
39
42
 
40
43
  export default async function NewsDeskSectionPage({ params, searchParams }: NewsDeskSectionPageProps) {
41
44
  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");
45
+ if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
46
+ if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
47
+ if (section === "desks" && resolveActiveSiteBrand().newsroomLayout !== "organizer") redirect(toPublicNewsroomPath("/newsroom/topics"));
45
48
  if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
46
49
  return <NewsDeskPage section={section} searchParams={searchParams} />;
47
50
  }