@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.
Files changed (42) hide show
  1. package/amplify/data/resource.js +26 -0
  2. package/amplify/data/resource.ts +30 -0
  3. package/amplify/data/schema.js +26 -0
  4. package/amplify/data/schema.ts +30 -0
  5. package/amplify/functions/content-actions/handler.py +107 -0
  6. package/amplify/functions/content-actions/requirements.txt +4 -0
  7. package/amplify/functions/content-actions/resource.js +40 -0
  8. package/amplify/functions/content-actions/resource.ts +54 -0
  9. package/amplify/functions/shared/python-bundle.js +17 -2
  10. package/amplify/functions/shared/python-bundle.ts +23 -2
  11. package/amplify/site-backend.js +10 -0
  12. package/amplify/site-backend.ts +15 -0
  13. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
  14. package/app/globals.css +1 -4
  15. package/app/layout.tsx +4 -12
  16. package/components/news-desk-page.tsx +1 -4
  17. package/lib/content-actions-client.ts +98 -0
  18. package/lib/empty-theme.css +1 -0
  19. package/lib/layout-scenarios.ts +8 -8
  20. package/lib/site-brand-tenant.ts +3 -22
  21. package/lib/site-brand.ts +44 -42
  22. package/middleware.ts +10 -35
  23. package/package.json +2 -1
  24. package/renderers/pretext/article-page.tsx +1 -6
  25. package/renderers/pretext/presentation-shell.tsx +2 -9
  26. package/routes.manifest.json +1 -1
  27. package/src/with-papyrus.mjs +15 -2
  28. package/publications/anth_us/brand.ts +0 -68
  29. package/publications/anth_us/theme.css +0 -66
  30. package/publications/pilobol_us/brand.ts +0 -37
  31. package/publications/pilobol_us/theme.css +0 -140
  32. package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
  33. package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
  34. package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
  35. package/publications/threat_intelligence/brand.ts +0 -40
  36. package/publications/threat_intelligence/pictograms/art.tsx +0 -608
  37. package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
  38. package/publications/threat_intelligence/pictograms/registry.ts +0 -30
  39. package/publications/threat_intelligence/pictograms/system.tsx +0 -305
  40. package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
  41. package/publications/threat_intelligence/theme.css +0 -1440
  42. /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. */
@@ -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
- if (SITE_BRAND.id !== "threat-intelligence") {
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: threatIntelligenceSeedContent.title,
205
- editionDate: threatIntelligenceSeedContent.publishDate,
206
- description: threatIntelligenceSeedContent.description,
207
- articles: threatIntelligenceSeedContent.articles as Article[],
208
- suppressNewsDeskAppendix: threatIntelligenceSeedContent.suppressNewsDeskAppendix === true,
209
- editionVideo: threatIntelligenceSeedContent.video as ArticleVideoAsset | undefined,
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
  }
@@ -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 const BRAND_OVERRIDE_COOKIE = "papyrus-site-brand-override";
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 BUILT_IN_SITE_BRANDS: Record<SiteBrandId, SiteBrand> = {
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
- * Built-in reference brands plus whatever the publication registered in its
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
- ...BUILT_IN_SITE_BRANDS,
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 normalized = value.trim().toLowerCase().replace(/[._]/g, "-");
108
- if (!normalized) return null;
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
- export function resolveSiteBrandId(
134
- raw: string | undefined | null = process.env.NEXT_PUBLIC_PAPYRUS_SITE_BRAND ?? process.env.PAPYRUS_SITE_BRAND,
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
- /** Cookie set by middleware when `?brand=` is present (demo without rebuild). */
140
- export function resolveRuntimeSiteBrandId(
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
- return normalizeSiteBrandId(cookieOverride) ?? resolveSiteBrandId();
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
- return SITE_BRANDS[id];
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, normalizeSiteBrandId, resolveSiteBrandId } from "./lib/site-brand";
3
+ import { getSiteBrand } from "./lib/site-brand";
4
4
 
5
- const BRAND_OVERRIDE_COOKIE = "papyrus-site-brand-override";
6
-
7
- function usesNewsroomRootPaths(request: NextRequest): boolean {
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
- const brandParam = request.nextUrl.searchParams.get("brand");
28
- const brandId = normalizeSiteBrandId(brandParam);
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 response ?? NextResponse.next();
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 response ?? NextResponse.next();
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
- const rewrite = NextResponse.rewrite(rewriteUrl);
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 response ?? NextResponse.next();
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.2",
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
- // Threat Intelligence supplies its own pictogram figure media; any other
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
- // Threat Intelligence supplies its own blog background and figure media; any
30
- // other publication gets the generic fallback (components/). This is the
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
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-next.2",
2
+ "version": "1.0.0-next.3",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/[year]/[month]/[day]/[articleSlug]/page.tsx",
@@ -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 and
7
- * `papyrus-amplify-outputs` -> ./amplify_outputs.json (both webpack and turbopack),
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
- }