@anthusai/papyrus 1.0.0-next.17 → 1.0.0-next.19
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.
- package/amplify/auth/resource.js +17 -4
- package/amplify/auth/resource.ts +21 -4
- package/amplify/site-backend-config.ts +6 -1
- package/amplify/site-backend.js +9 -2
- package/amplify/site-backend.ts +12 -2
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/api/revalidate/route.ts +5 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/page.tsx +5 -2
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +136 -1
- package/components/blog-page-background.tsx +23 -6
- package/components/newspaper.tsx +2 -1
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/settings-page.tsx +2 -1
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +31 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +24 -12
- package/infra/amplify-app-shell.ts +24 -12
- package/infra/build-specs.js +2 -1
- package/infra/build-specs.ts +4 -2
- package/infra/site-config.js +37 -11
- package/infra/site-config.ts +40 -12
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/content-types.ts +3 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +8 -3
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-nav.ts +1 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +7 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/renderer.ts +7 -0
- package/lib/site-brand.ts +38 -0
- package/lib/staging-gated-path.ts +22 -0
- package/middleware.ts +5 -16
- package/package.json +15 -1
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +280 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +11 -2
- package/src/sync.mjs +30 -6
package/amplify/auth/resource.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineAuth, secret } from "@aws-amplify/backend";
|
|
2
2
|
import { manageUserRole } from "../functions/manage-user-role/resource.js";
|
|
3
|
+
import { CfnUserPoolDomain } from "aws-cdk-lib/aws-cognito";
|
|
3
4
|
// Default OAuth redirect URLs for the p.apyr.us app. Other publications pass
|
|
4
5
|
// `auth.redirectUrls` to `defineSiteBackend` (or, for Papyrus's own backend,
|
|
5
6
|
// the PAPYRUS_OAUTH_REDIRECT_URLS branch env var, comma-separated).
|
|
@@ -25,15 +26,13 @@ export function authConfigFromEnv() {
|
|
|
25
26
|
return {
|
|
26
27
|
redirectUrls: resolveAuthRedirectUrls(),
|
|
27
28
|
disableGoogleOAuth: process.env.PAPYRUS_DISABLE_GOOGLE_OAUTH === "1",
|
|
28
|
-
// Stable Cognito hosted-UI domain prefix for Google OAuth redirect URIs.
|
|
29
|
-
// See docs/google-oauth-setup.md.
|
|
30
29
|
cognitoDomainPrefix: (process.env.PAPYRUS_COGNITO_DOMAIN_PREFIX ?? "").trim(),
|
|
30
|
+
applyCognitoDomainPrefix: process.env.PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX === "true",
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
export function defineSiteAuth(config) {
|
|
34
34
|
const authRedirectUrls = config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS;
|
|
35
35
|
const disableGoogleOAuth = config.disableGoogleOAuth === true;
|
|
36
|
-
const cognitoDomainPrefix = (config.cognitoDomainPrefix ?? "").trim();
|
|
37
36
|
return defineAuth({
|
|
38
37
|
loginWith: {
|
|
39
38
|
email: true,
|
|
@@ -49,7 +48,6 @@ export function defineSiteAuth(config) {
|
|
|
49
48
|
scopes: ["EMAIL", "PROFILE", "OPENID"],
|
|
50
49
|
callbackUrls: authRedirectUrls,
|
|
51
50
|
logoutUrls: authRedirectUrls,
|
|
52
|
-
...(cognitoDomainPrefix ? { domainPrefix: cognitoDomainPrefix } : {}),
|
|
53
51
|
},
|
|
54
52
|
}),
|
|
55
53
|
},
|
|
@@ -59,3 +57,18 @@ export function defineSiteAuth(config) {
|
|
|
59
57
|
],
|
|
60
58
|
});
|
|
61
59
|
}
|
|
60
|
+
// Amplify Gen2's auth factory overwrites `externalProviders.domainPrefix` with a
|
|
61
|
+
// hash of the backend id, so the prefix cannot be passed through `defineAuth`.
|
|
62
|
+
// This sets the user pool domain on the synthesized resource instead. Cognito
|
|
63
|
+
// cannot rename a pool domain in place: CloudFormation replaces the domain,
|
|
64
|
+
// which changes the hosted-UI URL and the Google redirect URI. Callers must
|
|
65
|
+
// only use it when the site opted in (see docs/google-oauth-setup.md).
|
|
66
|
+
export function applyCognitoDomainPrefix(authStack, cognitoDomainPrefix) {
|
|
67
|
+
const domainResources = authStack.node
|
|
68
|
+
.findAll()
|
|
69
|
+
.filter((construct) => construct instanceof CfnUserPoolDomain);
|
|
70
|
+
if (domainResources.length !== 1) {
|
|
71
|
+
throw new Error(`Expected exactly one Cognito user pool domain in the auth stack to apply cognitoDomainPrefix, found ${domainResources.length}`);
|
|
72
|
+
}
|
|
73
|
+
domainResources[0].domain = cognitoDomainPrefix;
|
|
74
|
+
}
|
package/amplify/auth/resource.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { defineAuth, secret } from "@aws-amplify/backend";
|
|
2
2
|
import { manageUserRole } from "../functions/manage-user-role/resource";
|
|
3
|
+
import { Stack } from "aws-cdk-lib";
|
|
4
|
+
import { CfnUserPoolDomain } from "aws-cdk-lib/aws-cognito";
|
|
3
5
|
import type { PapyrusAuthConfig } from "../site-backend-config";
|
|
4
6
|
|
|
5
7
|
// Default OAuth redirect URLs for the p.apyr.us app. Other publications pass
|
|
@@ -28,16 +30,14 @@ export function authConfigFromEnv(): PapyrusAuthConfig {
|
|
|
28
30
|
return {
|
|
29
31
|
redirectUrls: resolveAuthRedirectUrls(),
|
|
30
32
|
disableGoogleOAuth: process.env.PAPYRUS_DISABLE_GOOGLE_OAUTH === "1",
|
|
31
|
-
// Stable Cognito hosted-UI domain prefix for Google OAuth redirect URIs.
|
|
32
|
-
// See docs/google-oauth-setup.md.
|
|
33
33
|
cognitoDomainPrefix: (process.env.PAPYRUS_COGNITO_DOMAIN_PREFIX ?? "").trim(),
|
|
34
|
+
applyCognitoDomainPrefix: process.env.PAPYRUS_APPLY_COGNITO_DOMAIN_PREFIX === "true",
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function defineSiteAuth(config: PapyrusAuthConfig) {
|
|
38
39
|
const authRedirectUrls = config.redirectUrls ?? DEFAULT_AUTH_REDIRECT_URLS;
|
|
39
40
|
const disableGoogleOAuth = config.disableGoogleOAuth === true;
|
|
40
|
-
const cognitoDomainPrefix = (config.cognitoDomainPrefix ?? "").trim();
|
|
41
41
|
return defineAuth({
|
|
42
42
|
loginWith: {
|
|
43
43
|
email: true,
|
|
@@ -53,7 +53,6 @@ export function defineSiteAuth(config: PapyrusAuthConfig) {
|
|
|
53
53
|
scopes: ["EMAIL", "PROFILE", "OPENID"],
|
|
54
54
|
callbackUrls: authRedirectUrls,
|
|
55
55
|
logoutUrls: authRedirectUrls,
|
|
56
|
-
...(cognitoDomainPrefix ? { domainPrefix: cognitoDomainPrefix } : {}),
|
|
57
56
|
},
|
|
58
57
|
}),
|
|
59
58
|
},
|
|
@@ -63,3 +62,21 @@ export function defineSiteAuth(config: PapyrusAuthConfig) {
|
|
|
63
62
|
],
|
|
64
63
|
});
|
|
65
64
|
}
|
|
65
|
+
|
|
66
|
+
// Amplify Gen2's auth factory overwrites `externalProviders.domainPrefix` with a
|
|
67
|
+
// hash of the backend id, so the prefix cannot be passed through `defineAuth`.
|
|
68
|
+
// This sets the user pool domain on the synthesized resource instead. Cognito
|
|
69
|
+
// cannot rename a pool domain in place: CloudFormation replaces the domain,
|
|
70
|
+
// which changes the hosted-UI URL and the Google redirect URI. Callers must
|
|
71
|
+
// only use it when the site opted in (see docs/google-oauth-setup.md).
|
|
72
|
+
export function applyCognitoDomainPrefix(authStack: Stack, cognitoDomainPrefix: string): void {
|
|
73
|
+
const domainResources = authStack.node
|
|
74
|
+
.findAll()
|
|
75
|
+
.filter((construct): construct is CfnUserPoolDomain => construct instanceof CfnUserPoolDomain);
|
|
76
|
+
if (domainResources.length !== 1) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Expected exactly one Cognito user pool domain in the auth stack to apply cognitoDomainPrefix, found ${domainResources.length}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
domainResources[0].domain = cognitoDomainPrefix;
|
|
82
|
+
}
|
|
@@ -7,8 +7,13 @@ export type PapyrusAuthConfig = {
|
|
|
7
7
|
/** OAuth callback/logout URLs for Google sign-in (ignored when Google is disabled). */
|
|
8
8
|
redirectUrls?: string[];
|
|
9
9
|
disableGoogleOAuth?: boolean;
|
|
10
|
-
/**
|
|
10
|
+
/** Desired Cognito hosted-UI domain prefix. Only applied when `applyCognitoDomainPrefix` is true. */
|
|
11
11
|
cognitoDomainPrefix?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Opt in to the prefix. Off by default because changing a live pool's
|
|
14
|
+
* domain replaces it and changes the Google redirect URI.
|
|
15
|
+
*/
|
|
16
|
+
applyCognitoDomainPrefix?: boolean;
|
|
12
17
|
};
|
|
13
18
|
|
|
14
19
|
export type PapyrusSiteBackendConfig = {
|
package/amplify/site-backend.js
CHANGED
|
@@ -13,7 +13,7 @@ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aw
|
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
|
|
16
|
-
import { authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
|
|
16
|
+
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
|
|
17
17
|
import { data } from "./data/resource.js";
|
|
18
18
|
import { assignmentAction } from "./functions/assignment-action/resource.js";
|
|
19
19
|
import { categoryAction } from "./functions/category-action/resource.js";
|
|
@@ -76,9 +76,10 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
76
76
|
const enableInboundEmail = site.features?.inboundEmail ?? readEnvFlag("PAPYRUS_ENABLE_INBOUND_EMAIL", isAmplifyProductionPipeline);
|
|
77
77
|
const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
|
|
78
78
|
const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
|
|
79
|
+
const authConfig = site.auth ?? authConfigFromEnv();
|
|
79
80
|
const backend = defineBackend({
|
|
80
81
|
assignmentAction,
|
|
81
|
-
auth: defineSiteAuth(
|
|
82
|
+
auth: defineSiteAuth(authConfig),
|
|
82
83
|
categoryAction,
|
|
83
84
|
contentActions,
|
|
84
85
|
data,
|
|
@@ -94,6 +95,12 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
94
95
|
slackDelivery,
|
|
95
96
|
storage,
|
|
96
97
|
});
|
|
98
|
+
const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
|
|
99
|
+
if (authConfig.applyCognitoDomainPrefix === true
|
|
100
|
+
&& requestedCognitoDomainPrefix !== ""
|
|
101
|
+
&& authConfig.disableGoogleOAuth !== true) {
|
|
102
|
+
applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
|
|
103
|
+
}
|
|
97
104
|
const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
|
|
98
105
|
// Repo root in a Papyrus checkout; package root when installed from npm.
|
|
99
106
|
const projectRoot = resolve(amplifyBackendDir, "..");
|
package/amplify/site-backend.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aw
|
|
|
14
14
|
import { dirname, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
|
|
17
|
-
import { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
|
|
17
|
+
import { applyCognitoDomainPrefix, authConfigFromEnv, defineSiteAuth } from "./auth/resource";
|
|
18
18
|
import { data } from "./data/resource";
|
|
19
19
|
import { assignmentAction } from "./functions/assignment-action/resource";
|
|
20
20
|
import { categoryAction } from "./functions/category-action/resource";
|
|
@@ -87,9 +87,10 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
87
87
|
const enableSlackAgent = site.features?.slack ?? readEnvFlag("PAPYRUS_ENABLE_SLACK", false);
|
|
88
88
|
const enableStorageBackups = site.features?.storageBackups ?? readEnvFlag("PAPYRUS_ENABLE_STORAGE_BACKUPS", isAmplifyProductionPipeline);
|
|
89
89
|
|
|
90
|
+
const authConfig = site.auth ?? authConfigFromEnv();
|
|
90
91
|
const backend = defineBackend({
|
|
91
92
|
assignmentAction,
|
|
92
|
-
auth: defineSiteAuth(
|
|
93
|
+
auth: defineSiteAuth(authConfig),
|
|
93
94
|
categoryAction,
|
|
94
95
|
contentActions,
|
|
95
96
|
data,
|
|
@@ -106,6 +107,15 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
106
107
|
storage,
|
|
107
108
|
});
|
|
108
109
|
|
|
110
|
+
const requestedCognitoDomainPrefix = (authConfig.cognitoDomainPrefix ?? "").trim();
|
|
111
|
+
if (
|
|
112
|
+
authConfig.applyCognitoDomainPrefix === true
|
|
113
|
+
&& requestedCognitoDomainPrefix !== ""
|
|
114
|
+
&& authConfig.disableGoogleOAuth !== true
|
|
115
|
+
) {
|
|
116
|
+
applyCognitoDomainPrefix(backend.auth.stack, requestedCognitoDomainPrefix);
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
const amplifyBackendDir = dirname(fileURLToPath(import.meta.url));
|
|
110
120
|
// Repo root in a Papyrus checkout; package root when installed from npm.
|
|
111
121
|
const projectRoot = resolve(amplifyBackendDir, "..");
|
|
@@ -52,10 +52,12 @@ export default async function DateScopedArticlePage({ params }: DateScopedArticl
|
|
|
52
52
|
editionFooter={{
|
|
53
53
|
editionBasePath,
|
|
54
54
|
entries: buildPresentationFooterEntries(content),
|
|
55
|
+
sections: content.sections.map((section) => ({ key: section.key, label: section.label })),
|
|
55
56
|
subtitle: SITE_BRAND.footerSubtitleOverride ?? (content.description?.trim() || "Inside Papyrus"),
|
|
56
57
|
title: SITE_BRAND.footerTitle,
|
|
57
58
|
}}
|
|
58
59
|
item={item}
|
|
60
|
+
videoScript={content.videoScripts?.[item.slug] ?? null}
|
|
59
61
|
backHref={`${editionBasePath}#${item.slug}`}
|
|
60
62
|
/>
|
|
61
63
|
);
|
|
@@ -1,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 {
|
|
@@ -44,8 +45,8 @@ export async function POST(request: Request) {
|
|
|
44
45
|
if (!normalized) continue;
|
|
45
46
|
revalidateTag(articleCacheTag(normalized));
|
|
46
47
|
revalidatedTags.add(articleCacheTag(normalized));
|
|
47
|
-
revalidatePath(`/articles/${encodeURIComponent(normalized)}`);
|
|
48
|
-
revalidatedPaths.add(`/articles/${encodeURIComponent(normalized)}`);
|
|
48
|
+
revalidatePath(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
|
|
49
|
+
revalidatedPaths.add(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
|
|
49
50
|
if (editionDate) {
|
|
50
51
|
revalidateTag(editionItemCacheTag(editionDate, normalized));
|
|
51
52
|
revalidatedTags.add(editionItemCacheTag(editionDate, normalized));
|
|
@@ -76,8 +77,8 @@ export async function POST(request: Request) {
|
|
|
76
77
|
revalidatedTags.add(ARCHIVE_CACHE_TAG);
|
|
77
78
|
revalidateTag(ARTICLES_CACHE_TAG);
|
|
78
79
|
revalidatedTags.add(ARTICLES_CACHE_TAG);
|
|
79
|
-
revalidatePath("/archive");
|
|
80
|
-
revalidatedPaths.add("/archive");
|
|
80
|
+
revalidatePath(toPublicReaderPath("/archive"));
|
|
81
|
+
revalidatedPaths.add(toPublicReaderPath("/archive"));
|
|
81
82
|
|
|
82
83
|
for (const path of body.paths ?? []) {
|
|
83
84
|
const normalized = path.trim();
|
|
@@ -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);
|
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
|
{`(() => {
|
package/app/page.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type { PublicationItem } from "../lib/publication-items";
|
|
17
17
|
import { SITE_BRAND, rootRoute } from "../lib/site-brand";
|
|
18
18
|
import { notFound, redirect } from "next/navigation";
|
|
19
|
+
import { getReaderBasePath } from "../lib/reader-base-path";
|
|
19
20
|
import { buildRootRedirectTarget } from "../lib/root-route-redirect";
|
|
20
21
|
import NewsDeskRootPage, { type NewsDeskRootSearchParams } from "./newsroom/page";
|
|
21
22
|
|
|
@@ -38,11 +39,13 @@ export default async function Home({ searchParams }: HomePageProps) {
|
|
|
38
39
|
// co-hosts reader + CMS uses the default "reader" (render the publication
|
|
39
40
|
// home page here). A CMS-only site points the root at the newsroom or
|
|
40
41
|
// anywhere else. Preserve the scenario and OAuth-callback paths.
|
|
41
|
-
|
|
42
|
+
const publicationOwnsRoot = getReaderBasePath() !== "";
|
|
43
|
+
|
|
44
|
+
if (!publicationOwnsRoot && rootRoute.kind === "newsroom" && !scenarioId) {
|
|
42
45
|
return <NewsDeskRootPage searchParams={searchParams} />;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
if (rootRoute.kind === "redirect") {
|
|
48
|
+
if (!publicationOwnsRoot && rootRoute.kind === "redirect") {
|
|
46
49
|
redirect(buildRootRedirectTarget(rootRoute.destination, resolvedSearchParams));
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { useRhythmOverlay } from "./use-rhythm-overlay";
|
|
5
5
|
|
|
6
6
|
type ArchiveShellProps = {
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export function ArchiveShell({ children }: ArchiveShellProps) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
15
|
-
if (isEditableEventTarget(event.target)) return;
|
|
16
|
-
if ((event.key === "=" || event.code === "Equal") && event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
17
|
-
event.preventDefault();
|
|
18
|
-
setShowRhythmOverlay((current) => !current);
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
23
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
24
|
-
}, []);
|
|
11
|
+
const showRhythmOverlay = useRhythmOverlay();
|
|
25
12
|
|
|
26
13
|
return (
|
|
27
14
|
<main
|
|
@@ -32,10 +19,3 @@ export function ArchiveShell({ children }: ArchiveShellProps) {
|
|
|
32
19
|
</main>
|
|
33
20
|
);
|
|
34
21
|
}
|
|
35
|
-
|
|
36
|
-
function isEditableEventTarget(target: EventTarget | null): boolean {
|
|
37
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
38
|
-
const tagName = target.tagName.toLowerCase();
|
|
39
|
-
if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
|
|
40
|
-
return target.isContentEditable || target.closest("[contenteditable='true']") !== null;
|
|
41
|
-
}
|