@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.3
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/data/resource.js +26 -0
- package/amplify/data/resource.ts +30 -0
- package/amplify/data/schema.js +26 -0
- package/amplify/data/schema.ts +30 -0
- package/amplify/functions/content-actions/handler.py +107 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- package/amplify/site-backend.js +10 -0
- package/amplify/site-backend.ts +15 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
- package/app/globals.css +1 -4
- package/app/layout.tsx +4 -12
- package/components/news-desk-page.tsx +1 -4
- package/lib/content-actions-client.ts +98 -0
- package/lib/empty-theme.css +1 -0
- package/lib/layout-scenarios.ts +8 -8
- package/lib/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +44 -42
- package/middleware.ts +10 -35
- package/package.json +2 -1
- package/renderers/pretext/article-page.tsx +1 -6
- package/renderers/pretext/presentation-shell.tsx +2 -9
- package/routes.manifest.json +1 -1
- package/src/with-papyrus.mjs +15 -2
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { generateClient } from "aws-amplify/data";
|
|
2
|
+
import type { Schema } from "../amplify/data/resource";
|
|
3
|
+
|
|
4
|
+
export type ContentActionError = { code: string; message: string; line: number | null };
|
|
5
|
+
|
|
6
|
+
export type DeriveMarkusInput = { frontMatterYaml: string | null; bodyMarkus: string; includeIr?: boolean };
|
|
7
|
+
export type SaveItemDraftInput = {
|
|
8
|
+
id: string | null;
|
|
9
|
+
type: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
section: string | null;
|
|
12
|
+
frontMatterYaml: string | null;
|
|
13
|
+
bodyMarkus: string;
|
|
14
|
+
aliases: string[];
|
|
15
|
+
expectedContentHash: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type DeriveMarkusResult = { ok: true; bodyIrBytes: number; bodyIr?: unknown; errors: [] };
|
|
19
|
+
export type SavedItemSummary = {
|
|
20
|
+
id: string;
|
|
21
|
+
contentHash: string;
|
|
22
|
+
status: string;
|
|
23
|
+
slug: string;
|
|
24
|
+
versionNumber: number | null;
|
|
25
|
+
};
|
|
26
|
+
export type SaveItemDraftResult = { ok: true; item: SavedItemSummary };
|
|
27
|
+
export type PublishItemResult = { ok: true; changed: boolean; publishedId: string; versionNumber: number | null };
|
|
28
|
+
export type UnpublishItemResult = { ok: true; changed: boolean };
|
|
29
|
+
|
|
30
|
+
export class ContentActionFailure extends Error {
|
|
31
|
+
readonly errors: ContentActionError[];
|
|
32
|
+
|
|
33
|
+
constructor(errors: ContentActionError[]) {
|
|
34
|
+
super(errors.map((error) => error.message).join("; ") || "Content action failed");
|
|
35
|
+
this.name = "ContentActionFailure";
|
|
36
|
+
this.errors = errors;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type GraphqlEnvelope = { data?: unknown; errors?: { message?: string }[] | null };
|
|
41
|
+
|
|
42
|
+
export type ContentActionsTransport = {
|
|
43
|
+
queries: { deriveMarkus: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope> };
|
|
44
|
+
mutations: {
|
|
45
|
+
saveItemDraft: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope>;
|
|
46
|
+
publishItem: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope>;
|
|
47
|
+
unpublishItem: (args: { input: string }, options: { authMode: "userPool" }) => Promise<GraphqlEnvelope>;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const USER_POOL = { authMode: "userPool" } as const;
|
|
52
|
+
|
|
53
|
+
function parseActionResponse<T>(envelope: GraphqlEnvelope): T {
|
|
54
|
+
if (envelope.errors && envelope.errors.length > 0) {
|
|
55
|
+
throw new Error(envelope.errors.map((error) => error.message ?? "GraphQL error").join("; "));
|
|
56
|
+
}
|
|
57
|
+
const raw = envelope.data;
|
|
58
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
59
|
+
if (!parsed || typeof parsed !== "object") {
|
|
60
|
+
throw new Error("The content action returned no data.");
|
|
61
|
+
}
|
|
62
|
+
const response = parsed as { ok?: boolean; errors?: ContentActionError[] };
|
|
63
|
+
if (response.ok !== true) {
|
|
64
|
+
throw new ContentActionFailure(response.errors ?? []);
|
|
65
|
+
}
|
|
66
|
+
return parsed as T;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createContentActionsClient(transport: ContentActionsTransport) {
|
|
70
|
+
return {
|
|
71
|
+
async deriveMarkus(input: DeriveMarkusInput): Promise<DeriveMarkusResult> {
|
|
72
|
+
return parseActionResponse(await transport.queries.deriveMarkus({ input: JSON.stringify(input) }, USER_POOL));
|
|
73
|
+
},
|
|
74
|
+
async saveItemDraft(input: SaveItemDraftInput): Promise<SaveItemDraftResult> {
|
|
75
|
+
return parseActionResponse(await transport.mutations.saveItemDraft({ input: JSON.stringify(input) }, USER_POOL));
|
|
76
|
+
},
|
|
77
|
+
async publishItem(id: string): Promise<PublishItemResult> {
|
|
78
|
+
return parseActionResponse(await transport.mutations.publishItem({ input: JSON.stringify({ id }) }, USER_POOL));
|
|
79
|
+
},
|
|
80
|
+
async unpublishItem(id: string): Promise<UnpublishItemResult> {
|
|
81
|
+
return parseActionResponse(await transport.mutations.unpublishItem({ input: JSON.stringify({ id }) }, USER_POOL));
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let defaultClient: ReturnType<typeof createContentActionsClient> | null = null;
|
|
87
|
+
|
|
88
|
+
function contentActionsClient() {
|
|
89
|
+
if (!defaultClient) {
|
|
90
|
+
defaultClient = createContentActionsClient(generateClient<Schema>() as unknown as ContentActionsTransport);
|
|
91
|
+
}
|
|
92
|
+
return defaultClient;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const deriveMarkus = (input: DeriveMarkusInput) => contentActionsClient().deriveMarkus(input);
|
|
96
|
+
export const saveItemDraft = (input: SaveItemDraftInput) => contentActionsClient().saveItemDraft(input);
|
|
97
|
+
export const publishItem = (id: string) => contentActionsClient().publishItem(id);
|
|
98
|
+
export const unpublishItem = (id: string) => contentActionsClient().unpublishItem(id);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Default for `papyrus-site-theme` when a publication ships no publication/theme.css. */
|
package/lib/layout-scenarios.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { createDefaultEditionLayoutPlan, type EditionLayoutPlan } from "./layout
|
|
|
5
5
|
import { articleToPublicationItem, cloneArticle } from "./publication-items";
|
|
6
6
|
import { PILOBOL_SAMPLE_SCENARIO_ID, createPilobolSampleEditionContent } from "./pilobol-sample";
|
|
7
7
|
import { SITE_BRAND } from "./site-brand";
|
|
8
|
-
import threatIntelligenceSeedContent from "../publications/threat_intelligence/seed/seed-edition-content.json";
|
|
9
8
|
|
|
10
9
|
export type LayoutScenario = EditionContent & {
|
|
11
10
|
source: "scenario";
|
|
@@ -190,7 +189,8 @@ function getDefaultScenarioSeed(): {
|
|
|
190
189
|
editionVideo?: ArticleVideoAsset;
|
|
191
190
|
newsDeskAppendix?: NewsDeskAppendix | null;
|
|
192
191
|
} {
|
|
193
|
-
|
|
192
|
+
const demoEdition = SITE_BRAND.demoEdition;
|
|
193
|
+
if (!demoEdition) {
|
|
194
194
|
return {
|
|
195
195
|
title: "Current Edition",
|
|
196
196
|
editionDate,
|
|
@@ -201,12 +201,12 @@ function getDefaultScenarioSeed(): {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
return {
|
|
204
|
-
title:
|
|
205
|
-
editionDate:
|
|
206
|
-
description:
|
|
207
|
-
articles:
|
|
208
|
-
suppressNewsDeskAppendix:
|
|
209
|
-
editionVideo:
|
|
204
|
+
title: demoEdition.title,
|
|
205
|
+
editionDate: demoEdition.editionDate,
|
|
206
|
+
description: demoEdition.description,
|
|
207
|
+
articles: demoEdition.articles,
|
|
208
|
+
suppressNewsDeskAppendix: demoEdition.suppressNewsDeskAppendix === true,
|
|
209
|
+
editionVideo: demoEdition.editionVideo,
|
|
210
210
|
newsDeskAppendix: null,
|
|
211
211
|
};
|
|
212
212
|
}
|
package/lib/site-brand-tenant.ts
CHANGED
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
getSiteBrand,
|
|
4
|
-
resolveRuntimeSiteBrandId,
|
|
5
|
-
resolveSiteBrandId,
|
|
6
|
-
type SiteBrand,
|
|
7
|
-
type SiteBrandId,
|
|
8
|
-
} from "./site-brand";
|
|
2
|
+
import { getSiteBrand, resolveSiteBrandId, type SiteBrand, type SiteBrandId } from "./site-brand";
|
|
9
3
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
export function resolveActiveSiteBrandId(
|
|
13
|
-
cookieOverride: string | undefined | null = null,
|
|
14
|
-
envBrandId: SiteBrandId = resolveSiteBrandId(),
|
|
15
|
-
): SiteBrandId {
|
|
16
|
-
return resolveRuntimeSiteBrandId(cookieOverride) ?? envBrandId;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function resolveActiveSiteBrand(
|
|
20
|
-
cookieOverride: string | undefined | null = null,
|
|
21
|
-
brandId?: SiteBrandId,
|
|
22
|
-
): SiteBrand {
|
|
23
|
-
const resolvedId = brandId ?? resolveActiveSiteBrandId(cookieOverride);
|
|
24
|
-
return getSiteBrand(resolvedId);
|
|
4
|
+
export function resolveActiveSiteBrand(brandId?: SiteBrandId): SiteBrand {
|
|
5
|
+
return getSiteBrand(brandId ?? resolveSiteBrandId());
|
|
25
6
|
}
|
|
26
7
|
|
|
27
8
|
export function resolveBrandConfigPath(relativePath: string): string {
|
package/lib/site-brand.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
1
2
|
import type { EditionPresentationFormat } from "./content-types";
|
|
3
|
+
import type { Article, ArticleVideoAsset } from "./articles";
|
|
4
|
+
import type { BlogPageBackgroundProps } from "../components/blog-page-background";
|
|
5
|
+
import type { PictogramFigureProps } from "../components/pictogram-figure";
|
|
2
6
|
import papyrusSite from "papyrus-site";
|
|
3
|
-
import { threatIntelligenceBrand } from "../publications/threat_intelligence/brand";
|
|
4
|
-
import { pilobolUsBrand } from "../publications/pilobol_us/brand";
|
|
5
|
-
import { anthUsBrand } from "../publications/anth_us/brand";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_THEME_PACK_TOKENS,
|
|
8
9
|
type HostingConfig,
|
|
@@ -17,6 +18,22 @@ import {
|
|
|
17
18
|
/** Open string: a publication registers its own brand through `papyrus.config.ts` (`defineSite`). */
|
|
18
19
|
export type SiteBrandId = string;
|
|
19
20
|
|
|
21
|
+
/** Brand-specific replacements for generic Pretext components. */
|
|
22
|
+
export type SiteBrandComponents = {
|
|
23
|
+
PictogramFigure?: ComponentType<PictogramFigureProps>;
|
|
24
|
+
BlogPageBackground?: ComponentType<BlogPageBackgroundProps>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Default scenario content a brand supplies for the layout lab. */
|
|
28
|
+
export type SiteBrandDemoEdition = {
|
|
29
|
+
title: string;
|
|
30
|
+
editionDate: string;
|
|
31
|
+
description: string;
|
|
32
|
+
articles: Article[];
|
|
33
|
+
suppressNewsDeskAppendix?: boolean;
|
|
34
|
+
editionVideo?: ArticleVideoAsset;
|
|
35
|
+
};
|
|
36
|
+
|
|
20
37
|
export type SiteBrand = {
|
|
21
38
|
id: SiteBrandId;
|
|
22
39
|
appTitle: string;
|
|
@@ -57,11 +74,13 @@ export type SiteBrand = {
|
|
|
57
74
|
newsroomSectionsConfigPath: string;
|
|
58
75
|
analysisProfilesPath: string;
|
|
59
76
|
publicationName: string;
|
|
77
|
+
components?: SiteBrandComponents;
|
|
78
|
+
demoEdition?: SiteBrandDemoEdition;
|
|
60
79
|
};
|
|
61
80
|
|
|
62
81
|
const SERIF_TEXT_FONT = 'Georgia, "Times New Roman", serif';
|
|
63
82
|
|
|
64
|
-
const
|
|
83
|
+
const REFERENCE_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
65
84
|
papyrus: {
|
|
66
85
|
id: "papyrus",
|
|
67
86
|
appTitle: "Papyrus",
|
|
@@ -88,63 +107,46 @@ const BUILT_IN_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
|
88
107
|
analysisProfilesPath: "corpora/papyrus-analysis-profiles.yml",
|
|
89
108
|
publicationName: "Anthus Threat Intelligence",
|
|
90
109
|
},
|
|
91
|
-
"threat-intelligence": threatIntelligenceBrand,
|
|
92
|
-
"pilobol-us": pilobolUsBrand,
|
|
93
|
-
"anth-us": anthUsBrand,
|
|
94
110
|
};
|
|
95
111
|
|
|
96
112
|
/**
|
|
97
|
-
*
|
|
113
|
+
* The Papyrus reference brand plus whatever the publication registered in its
|
|
98
114
|
* own `papyrus.config.ts` (resolved through the `papyrus-site` alias).
|
|
99
115
|
*/
|
|
100
116
|
const SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
|
|
101
|
-
...
|
|
117
|
+
...REFERENCE_SITE_BRANDS,
|
|
102
118
|
...Object.fromEntries((papyrusSite.brands ?? []).map((brand) => [brand.id, brand])),
|
|
103
119
|
};
|
|
104
120
|
|
|
105
121
|
export function normalizeSiteBrandId(value: string | undefined | null): SiteBrandId | null {
|
|
106
122
|
if (!value) return null;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
if (normalized === "papyrus") return "papyrus";
|
|
110
|
-
// NOTE: the bare alias `anthus` has resolved to `threat-intelligence` since
|
|
111
|
-
// that brand was added and is kept pointing there so existing
|
|
112
|
-
// PAPYRUS_SITE_BRAND values do not silently change meaning. The Anth.us
|
|
113
|
-
// publication is therefore addressed as `anth-us` / `anth.us` / `anth_us`
|
|
114
|
-
// only. Do not "tidy" this by moving `anthus` across.
|
|
115
|
-
if (normalized === "threat-intelligence" || normalized === "threat-intel" || normalized === "anthus") {
|
|
116
|
-
return "threat-intelligence";
|
|
117
|
-
}
|
|
118
|
-
if (normalized === "anth-us") {
|
|
119
|
-
// `normalized` has already mapped `.` and `_` to `-`, so this covers
|
|
120
|
-
// `anth.us` and `anth_us` too.
|
|
121
|
-
return "anth-us";
|
|
122
|
-
}
|
|
123
|
-
// Bare `pilobol` is deliberately NOT an alias (f268e72): the brand name is
|
|
124
|
-
// Pilobolus; Pilobol.us is the domain and `pilobol-us` the technical id.
|
|
125
|
-
if (normalized === "pilobol-us" || normalized === "pilobolus") {
|
|
126
|
-
return "pilobol-us";
|
|
127
|
-
}
|
|
128
|
-
// Registered brands (publication-owned `papyrus.config.ts`): exact id only.
|
|
129
|
-
if (Object.prototype.hasOwnProperty.call(SITE_BRANDS, normalized)) return normalized;
|
|
130
|
-
return null;
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
return Object.prototype.hasOwnProperty.call(SITE_BRANDS, trimmed) ? trimmed : null;
|
|
131
125
|
}
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
): SiteBrandId {
|
|
136
|
-
return normalizeSiteBrandId(raw) ?? papyrusSite.defaultBrand ?? "papyrus";
|
|
127
|
+
function unknownBrandError(value: string): Error {
|
|
128
|
+
return new Error(`Unknown brand '${value}'. Registered: ${Object.keys(SITE_BRANDS).join(", ")}`);
|
|
137
129
|
}
|
|
138
130
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
cookieOverride: string | undefined | null = null,
|
|
131
|
+
export function resolveSiteBrandId(
|
|
132
|
+
raw: string | undefined | null = process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND ?? process.env.PAPYRUS_SITE_BRAND,
|
|
142
133
|
): SiteBrandId {
|
|
143
|
-
|
|
134
|
+
const requested = raw?.trim();
|
|
135
|
+
if (requested) {
|
|
136
|
+
const normalized = normalizeSiteBrandId(requested);
|
|
137
|
+
if (normalized === null) throw unknownBrandError(requested);
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
const fallback = papyrusSite.defaultBrand ?? "papyrus";
|
|
141
|
+
const normalizedFallback = normalizeSiteBrandId(fallback);
|
|
142
|
+
if (normalizedFallback === null) throw unknownBrandError(fallback);
|
|
143
|
+
return normalizedFallback;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
export function getSiteBrand(id: SiteBrandId = resolveSiteBrandId()): SiteBrand {
|
|
147
|
-
|
|
147
|
+
const brand = Object.prototype.hasOwnProperty.call(SITE_BRANDS, id) ? SITE_BRANDS[id] : undefined;
|
|
148
|
+
if (!brand) throw unknownBrandError(id);
|
|
149
|
+
return brand;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
export const SITE_BRAND = getSiteBrand();
|
package/middleware.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import type { NextRequest } from "next/server";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
import { getSiteBrand
|
|
3
|
+
import { getSiteBrand } from "./lib/site-brand";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const cookieBrand = request.cookies.get(BRAND_OVERRIDE_COOKIE)?.value;
|
|
9
|
-
const brandId = normalizeSiteBrandId(cookieBrand)
|
|
10
|
-
?? normalizeSiteBrandId(request.nextUrl.searchParams.get("brand"))
|
|
11
|
-
?? resolveSiteBrandId();
|
|
12
|
-
// Brand property (not a hard-coded id) so publication-registered brands work.
|
|
13
|
-
return getSiteBrand(brandId).rootRoute?.kind === "newsroom" && getSiteBrand(brandId).newsroomBasePath === "";
|
|
5
|
+
function usesNewsroomRootPaths(): boolean {
|
|
6
|
+
const brand = getSiteBrand();
|
|
7
|
+
return brand.rootRoute?.kind === "newsroom" && brand.newsroomBasePath === "";
|
|
14
8
|
}
|
|
15
9
|
|
|
16
10
|
function isStaticOrApiPath(pathname: string): boolean {
|
|
@@ -24,26 +18,13 @@ function isStaticOrApiPath(pathname: string): boolean {
|
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
export function middleware(request: NextRequest) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let response: NextResponse | null = null;
|
|
30
|
-
|
|
31
|
-
if (brandId) {
|
|
32
|
-
response = NextResponse.next();
|
|
33
|
-
response.cookies.set(BRAND_OVERRIDE_COOKIE, brandId, {
|
|
34
|
-
path: "/",
|
|
35
|
-
maxAge: 60 * 60 * 24 * 30,
|
|
36
|
-
sameSite: "lax",
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!usesNewsroomRootPaths(request)) {
|
|
41
|
-
return response ?? NextResponse.next();
|
|
21
|
+
if (!usesNewsroomRootPaths()) {
|
|
22
|
+
return NextResponse.next();
|
|
42
23
|
}
|
|
43
24
|
|
|
44
25
|
const { pathname } = request.nextUrl;
|
|
45
26
|
if (isStaticOrApiPath(pathname)) {
|
|
46
|
-
return
|
|
27
|
+
return NextResponse.next();
|
|
47
28
|
}
|
|
48
29
|
|
|
49
30
|
if (pathname === "/newsroom" || pathname === "/newsroom/") {
|
|
@@ -55,22 +36,16 @@ export function middleware(request: NextRequest) {
|
|
|
55
36
|
}
|
|
56
37
|
|
|
57
38
|
if (pathname === "/" || pathname === "") {
|
|
58
|
-
return
|
|
39
|
+
return NextResponse.next();
|
|
59
40
|
}
|
|
60
41
|
|
|
61
42
|
if (!pathname.startsWith("/newsroom")) {
|
|
62
43
|
const rewriteUrl = request.nextUrl.clone();
|
|
63
44
|
rewriteUrl.pathname = `/newsroom${pathname}`;
|
|
64
|
-
|
|
65
|
-
if (response) {
|
|
66
|
-
for (const cookie of response.cookies.getAll()) {
|
|
67
|
-
rewrite.cookies.set(cookie);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return rewrite;
|
|
45
|
+
return NextResponse.rewrite(rewriteUrl);
|
|
71
46
|
}
|
|
72
47
|
|
|
73
|
-
return
|
|
48
|
+
return NextResponse.next();
|
|
74
49
|
}
|
|
75
50
|
|
|
76
51
|
export const config = {
|
package/package.json
CHANGED
|
@@ -126,6 +126,7 @@
|
|
|
126
126
|
"./lib/category-repository": "./lib/category-repository.ts",
|
|
127
127
|
"./lib/console-chat-client": "./lib/console-chat-client.ts",
|
|
128
128
|
"./lib/console-web-context": "./lib/console-web-context.ts",
|
|
129
|
+
"./lib/content-actions-client": "./lib/content-actions-client.ts",
|
|
129
130
|
"./lib/content-load-timing": "./lib/content-load-timing.ts",
|
|
130
131
|
"./lib/content-repository": "./lib/content-repository.ts",
|
|
131
132
|
"./lib/content-types": "./lib/content-types.ts",
|
|
@@ -256,7 +257,7 @@
|
|
|
256
257
|
"optional": true
|
|
257
258
|
}
|
|
258
259
|
},
|
|
259
|
-
"version": "1.0.0-next.
|
|
260
|
+
"version": "1.0.0-next.3",
|
|
260
261
|
"dependencies": {
|
|
261
262
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
262
263
|
"@aws-sdk/client-cognito-identity-provider": "3.1045.0",
|
|
@@ -7,15 +7,10 @@ import { getPublicationItemVideoAsset } from "../../lib/publication-items";
|
|
|
7
7
|
import type { ArticlePageEditionFooter, RenderArticleProps, RenderItemProps } from "../../lib/renderer";
|
|
8
8
|
import { SITE_BRAND } from "../../lib/site-brand";
|
|
9
9
|
import { ArticleVideoFigure } from "../../components/article-video";
|
|
10
|
-
import { PictogramFigure as ThreatIntelligencePictogramFigure } from "../../publications/threat_intelligence/pictograms/figure";
|
|
11
10
|
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
12
11
|
import { PresentationFooter } from "../../components/presentation-footer";
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
// publication gets the generic fallback (components/pictogram-figure.tsx).
|
|
16
|
-
// Same decoupling as renderers/pretext/presentation-shell.tsx -- see
|
|
17
|
-
// docs/pluggable-publishers.md section 2.6 and PPY-d79ff2.
|
|
18
|
-
const PictogramFigure = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligencePictogramFigure : GenericPictogramFigure;
|
|
13
|
+
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
19
14
|
|
|
20
15
|
export type { ArticlePageEditionFooter };
|
|
21
16
|
|
|
@@ -18,21 +18,14 @@ import type { ArticleVideoAsset } from "../../lib/articles";
|
|
|
18
18
|
import type { PresentationTarget, RenderEditionProps } from "../../lib/renderer";
|
|
19
19
|
import { SITE_BRAND, enforcePresentation, getDefaultPretextLayout } from "../../lib/site-brand";
|
|
20
20
|
import { ArticleVideoFigure } from "../../components/article-video";
|
|
21
|
-
import { BlogPageBackground as ThreatIntelligenceBlogPageBackground } from "../../publications/threat_intelligence/blog-defense/page-background";
|
|
22
21
|
import { BlogPageBackground as GenericBlogPageBackground } from "../../components/blog-page-background";
|
|
23
22
|
import { Newspaper } from "../../components/newspaper";
|
|
24
|
-
import { PictogramFigure as ThreatIntelligencePictogramFigure } from "../../publications/threat_intelligence/pictograms/figure";
|
|
25
23
|
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
26
24
|
import { PresentationFooter } from "../../components/presentation-footer";
|
|
27
25
|
import { readLocalReaderSettings, resolveReaderSettings, subscribeReaderSettingsChanges } from "../../components/reader-settings";
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// coupling docs/pluggable-publishers.md section 2.6 flags, decoupled here
|
|
32
|
-
// per PPY-d79ff2 by making the choice publication-conditional instead of
|
|
33
|
-
// hardcoded.
|
|
34
|
-
const BlogPageBackground = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligenceBlogPageBackground : GenericBlogPageBackground;
|
|
35
|
-
const PictogramFigure = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligencePictogramFigure : GenericPictogramFigure;
|
|
27
|
+
const BlogPageBackground = SITE_BRAND.components?.BlogPageBackground ?? GenericBlogPageBackground;
|
|
28
|
+
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
36
29
|
|
|
37
30
|
export type { PresentationTarget };
|
|
38
31
|
|
package/routes.manifest.json
CHANGED
package/src/with-papyrus.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Wrap a publication repo's Next config for Papyrus (SPIKE, PPY-82be6c).
|
|
5
7
|
* - transpile the package (it ships TypeScript source),
|
|
6
|
-
* - alias `papyrus-site` -> ./papyrus.config.ts
|
|
7
|
-
* `papyrus-amplify-outputs` -> ./amplify_outputs.json
|
|
8
|
+
* - alias `papyrus-site` -> ./papyrus.config.ts,
|
|
9
|
+
* `papyrus-amplify-outputs` -> ./amplify_outputs.json and
|
|
10
|
+
* `papyrus-site-theme` -> ./publication/theme.css when it exists, else the shipped
|
|
11
|
+
* empty lib/empty-theme.css (both webpack and turbopack),
|
|
8
12
|
* - carry Papyrus's own server-external packages and image settings.
|
|
9
13
|
*/
|
|
10
14
|
const SERVER_EXTERNAL = [
|
|
@@ -18,11 +22,19 @@ const SERVER_EXTERNAL = [
|
|
|
18
22
|
"@aws-amplify/data-schema",
|
|
19
23
|
];
|
|
20
24
|
|
|
25
|
+
const EMPTY_THEME = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../lib/empty-theme.css");
|
|
26
|
+
|
|
21
27
|
export function withPapyrus(nextConfig = {}, options = {}) {
|
|
22
28
|
const root = options.root ?? process.cwd();
|
|
29
|
+
const hasPublicationTheme = fs.existsSync(path.join(root, "publication/theme.css"));
|
|
30
|
+
const toProjectRelative = (absolute) => {
|
|
31
|
+
const relative = path.relative(root, absolute).split(path.sep).join("/");
|
|
32
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
33
|
+
};
|
|
23
34
|
const aliases = {
|
|
24
35
|
"papyrus-site": path.join(root, "papyrus.config.ts"),
|
|
25
36
|
"papyrus-amplify-outputs": path.join(root, "amplify_outputs.json"),
|
|
37
|
+
"papyrus-site-theme": hasPublicationTheme ? path.join(root, "publication/theme.css") : EMPTY_THEME,
|
|
26
38
|
};
|
|
27
39
|
return {
|
|
28
40
|
distDir: process.env.NEXT_DIST_DIR || ".next",
|
|
@@ -37,6 +49,7 @@ export function withPapyrus(nextConfig = {}, options = {}) {
|
|
|
37
49
|
...nextConfig.turbopack?.resolveAlias,
|
|
38
50
|
"papyrus-site": "./papyrus.config.ts",
|
|
39
51
|
"papyrus-amplify-outputs": "./amplify_outputs.json",
|
|
52
|
+
"papyrus-site-theme": toProjectRelative(hasPublicationTheme ? path.join(root, "publication/theme.css") : EMPTY_THEME),
|
|
40
53
|
},
|
|
41
54
|
},
|
|
42
55
|
webpack(config, context) {
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { SiteBrand } from "../../lib/site-brand";
|
|
2
|
-
import { ANTH_US_THEME_PACK_TOKENS } from "../../lib/site-stack";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Site definition for the Anth.us publication on Papyrus.
|
|
6
|
-
*
|
|
7
|
-
* This is the **site**: masthead, theme pack, corpus wiring, deployment
|
|
8
|
-
* metadata. It is a sibling of `publications/anthus/`, which is the
|
|
9
|
-
* **editorial identity** (`style-profile.yml`, `editorial-rewrite-skill.yml`,
|
|
10
|
-
* `reference-samples/`) and is not touched by this file.
|
|
11
|
-
*
|
|
12
|
-
* Every string below is read off the live Gatsby site at `AnthusAI/Anth.us`:
|
|
13
|
-
* `gatsby-config.mjs` `siteMetadata` for the title/description/author,
|
|
14
|
-
* `src/components/header.js` for the wordmark, `gatsby-plugin-manifest` for
|
|
15
|
-
* "Anth.us AI Solutions", and `src/components/seo.js` for the
|
|
16
|
-
* `"<page> | Anthus"` title pattern.
|
|
17
|
-
*/
|
|
18
|
-
export const anthUsBrand: SiteBrand = {
|
|
19
|
-
id: "anth-us",
|
|
20
|
-
appTitle: "Anthus",
|
|
21
|
-
appDescription:
|
|
22
|
-
"Anthus builds and operates self-aligning AI systems — custom models, agent harnesses, and evaluation loops with a human in the loop, grounded in 14 years of production operations.",
|
|
23
|
-
// The live header renders the bare word "Anthus" (Jersey 25, weight 900),
|
|
24
|
-
// not a dotted domain wordmark, so this is not "ANTH.US".
|
|
25
|
-
mastheadTitle: "Anthus",
|
|
26
|
-
mastheadSubtitle: "AI Solutions",
|
|
27
|
-
backToHomeLabel: "Back to Anthus",
|
|
28
|
-
articleTitleSuffix: "Anthus",
|
|
29
|
-
placeholderByline: "Ryan Porter",
|
|
30
|
-
// The source site is a blog with a long single-column measure, not a
|
|
31
|
-
// newsprint grid.
|
|
32
|
-
defaultPresentation: "blog",
|
|
33
|
-
textFont:
|
|
34
|
-
'"Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
35
|
-
mastheadWordSplit: false,
|
|
36
|
-
mastheadDateFormat: "formatted",
|
|
37
|
-
mastheadSource: "brand",
|
|
38
|
-
sectionLinkStrategy: "route",
|
|
39
|
-
themePack: "anth-us",
|
|
40
|
-
themeTokens: ANTH_US_THEME_PACK_TOKENS,
|
|
41
|
-
renderer: { kind: "markus" },
|
|
42
|
-
hosting: { kind: "amplify-ssr" },
|
|
43
|
-
readerDeployment: {
|
|
44
|
-
kind: "amplify-static",
|
|
45
|
-
repository: "https://github.com/AnthusAI/Anth.us-Papyrus",
|
|
46
|
-
// `domain` and `amplifyAppId` are deliberately unset.
|
|
47
|
-
//
|
|
48
|
-
// This reader is a **staging** port of a live marketing site. Production
|
|
49
|
-
// `anth.us` stays pointed at the existing Gatsby build until the port has
|
|
50
|
-
// been reviewed, so writing `domain: "anth.us"` here would assert a
|
|
51
|
-
// canonical host this build does not serve. Staging runs on whatever
|
|
52
|
-
// hostname Amplify hands out. No DNS or domain association is configured
|
|
53
|
-
// anywhere in this repo for it.
|
|
54
|
-
//
|
|
55
|
-
// The repository is private while the port is half-finished; the URL above
|
|
56
|
-
// will 404 for anyone without access. That is expected, not a typo.
|
|
57
|
-
},
|
|
58
|
-
rootRoute: { kind: "newsroom" },
|
|
59
|
-
newsroomBasePath: "",
|
|
60
|
-
opsChrome: "app",
|
|
61
|
-
corpusKey: "anth-us",
|
|
62
|
-
// Declared paths. The YAML files themselves are not in the repo yet — see
|
|
63
|
-
// README.md "Not wired yet".
|
|
64
|
-
steeringConfigPath: "corpora/anth-us-steering.yml",
|
|
65
|
-
newsroomSectionsConfigPath: "corpora/anth-us-newsroom-sections.yml",
|
|
66
|
-
analysisProfilesPath: "corpora/anth-us-analysis-profiles.yml",
|
|
67
|
-
publicationName: "Anthus",
|
|
68
|
-
};
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/* Anthus theme pack — colors only.
|
|
2
|
-
Hex values are read off the live Gatsby site at AnthusAI/Anth.us
|
|
3
|
-
(`src/styles/variables.scss` `:root`). That file is not loaded at runtime.
|
|
4
|
-
Shadcn owns typography. Do not set a font stack here.
|
|
5
|
-
|
|
6
|
-
Slot-by-slot provenance (including the three derived values) is documented
|
|
7
|
-
on ANTH_US_THEME_PACK_TOKENS in lib/site-stack.ts — keep the two in sync.
|
|
8
|
-
|
|
9
|
-
NO DARK BLOCK ON PURPOSE. The source site has no dark mode: its only
|
|
10
|
-
`prefers-color-scheme: dark` rule swaps two code-highlight tints. Porting a
|
|
11
|
-
light-only publication means pinning `color-scheme: light` rather than
|
|
12
|
-
inventing a dark palette. If Anthus later designs one, add it here and to
|
|
13
|
-
`ANTH_US_THEME_PACK_TOKENS.dark` together. */
|
|
14
|
-
|
|
15
|
-
:root[data-theme-pack="anth-us"],
|
|
16
|
-
:root[data-site-brand="anth-us"] {
|
|
17
|
-
color-scheme: light;
|
|
18
|
-
--theme-ink: #333333;
|
|
19
|
-
--theme-muted: #5f6b73;
|
|
20
|
-
--theme-paper: #f1f9fe;
|
|
21
|
-
--theme-card: #ffffff;
|
|
22
|
-
--theme-line: #cccccc;
|
|
23
|
-
--theme-moss: #dc5497;
|
|
24
|
-
--theme-ochre: #ff57b4;
|
|
25
|
-
--theme-quote: #9b165d;
|
|
26
|
-
--theme-tip: #0389d7;
|
|
27
|
-
--theme-caution: #e95800;
|
|
28
|
-
--theme-stage: #fedded;
|
|
29
|
-
--background: var(--theme-paper);
|
|
30
|
-
--foreground: var(--theme-ink);
|
|
31
|
-
--foreground-strong: var(--theme-ink);
|
|
32
|
-
--foreground-muted: var(--theme-muted);
|
|
33
|
-
--card: var(--theme-card);
|
|
34
|
-
--card-foreground: var(--theme-ink);
|
|
35
|
-
--popover: var(--theme-card);
|
|
36
|
-
--popover-foreground: var(--theme-ink);
|
|
37
|
-
--primary: var(--theme-moss);
|
|
38
|
-
--primary-foreground: var(--theme-card);
|
|
39
|
-
--secondary: var(--theme-stage);
|
|
40
|
-
--secondary-foreground: var(--theme-ink);
|
|
41
|
-
--muted: var(--theme-stage);
|
|
42
|
-
--muted-foreground: var(--theme-muted);
|
|
43
|
-
--accent: var(--theme-ochre);
|
|
44
|
-
--accent-foreground: var(--theme-card);
|
|
45
|
-
--border: var(--theme-line);
|
|
46
|
-
--input: var(--theme-line);
|
|
47
|
-
--ring: var(--theme-moss);
|
|
48
|
-
--destructive: var(--theme-caution);
|
|
49
|
-
--destructive-foreground: var(--theme-card);
|
|
50
|
-
--danger: var(--theme-caution);
|
|
51
|
-
--danger-foreground: var(--theme-card);
|
|
52
|
-
--surface: var(--theme-card);
|
|
53
|
-
--surface-foreground: var(--theme-ink);
|
|
54
|
-
--paper: var(--theme-paper);
|
|
55
|
-
--ink: var(--theme-ink);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* The source site is light-only, so a dark-preferring browser must not be
|
|
59
|
-
handed a half-dark Anthus. Re-assert the light tokens inside the dark media
|
|
60
|
-
query for parity with how the other packs are structured. */
|
|
61
|
-
@media (prefers-color-scheme: dark) {
|
|
62
|
-
:root[data-theme-pack="anth-us"],
|
|
63
|
-
:root[data-site-brand="anth-us"] {
|
|
64
|
-
color-scheme: light;
|
|
65
|
-
}
|
|
66
|
-
}
|