@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +20 -5
- package/amplify/auth/resource.ts +27 -5
- package/amplify/data/resource.js +28 -70
- package/amplify/data/resource.ts +29 -70
- package/amplify/data/schema.js +33 -58
- package/amplify/data/schema.ts +34 -58
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +45 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/site-backend-config.ts +36 -1
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +93 -170
- package/amplify/site-backend.ts +126 -196
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +21 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
- package/app/newsroom/[section]/page.tsx +4 -3
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/layout.tsx +4 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +2 -5
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +29 -3
- package/components/amplify-client-provider.tsx +4 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-taxonomy-client.ts +51 -0
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +67 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-references-view.tsx +83 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/topic-steering-workspace.tsx +54 -64
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +228 -50
- package/infra/amplify-app-shell.ts +256 -53
- package/infra/build-specs.js +15 -5
- package/infra/build-specs.ts +18 -7
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +236 -11
- package/infra/site-config.ts +276 -15
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +42 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +12 -2
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +61 -9
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +13 -0
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/site-brand.ts +51 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +114 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +23 -18
- package/package.json +45 -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 +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +45 -2
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.mjs +19 -2
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export type SiteBrandFontFace = {
|
|
2
|
+
/** Public URL of the font file, e.g. `/fonts/acme-serif-regular.woff2`. */
|
|
3
|
+
src: string;
|
|
4
|
+
weight?: number | string;
|
|
5
|
+
style?: "normal" | "italic";
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type SiteBrandGoogleFontSource = {
|
|
9
|
+
weights: number[];
|
|
10
|
+
/** Also load the italic style of every listed weight. */
|
|
11
|
+
italics?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One brand-declared font family. Declare exactly one source: `google` loads
|
|
16
|
+
* the family from the Google Fonts stylesheet API; `faces` self-hosts files
|
|
17
|
+
* with `@font-face`. The root layout exposes `cssVariable` as
|
|
18
|
+
* `"<family>", <fallback>` on `<html>`, so publication CSS writes
|
|
19
|
+
* `font-family: var(--font-masthead)`.
|
|
20
|
+
*/
|
|
21
|
+
export type SiteBrandFont = {
|
|
22
|
+
family: string;
|
|
23
|
+
cssVariable: string;
|
|
24
|
+
fallback?: string;
|
|
25
|
+
google?: SiteBrandGoogleFontSource;
|
|
26
|
+
faces?: SiteBrandFontFace[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type BrandFontAssets = {
|
|
30
|
+
preconnectOrigins: string[];
|
|
31
|
+
stylesheetHrefs: string[];
|
|
32
|
+
fontFaceCss: string;
|
|
33
|
+
rootStyle: Record<string, string>;
|
|
34
|
+
isEmpty: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const GOOGLE_STYLESHEET_ORIGIN = "https://fonts.googleapis.com";
|
|
38
|
+
const GOOGLE_FILE_ORIGIN = "https://fonts.gstatic.com";
|
|
39
|
+
const DEFAULT_FALLBACK = "sans-serif";
|
|
40
|
+
const CSS_VARIABLE_PATTERN = /^--[a-z][a-z0-9-]*$/;
|
|
41
|
+
|
|
42
|
+
export function validateBrandFonts(fonts: SiteBrandFont[]): void {
|
|
43
|
+
const seenVariables = new Set<string>();
|
|
44
|
+
for (const font of fonts) {
|
|
45
|
+
if (!font.family.trim()) throw new Error("A brand font needs a family name.");
|
|
46
|
+
if (!CSS_VARIABLE_PATTERN.test(font.cssVariable)) {
|
|
47
|
+
throw new Error(`Brand font "${font.family}" cssVariable must look like --font-name, got "${font.cssVariable}".`);
|
|
48
|
+
}
|
|
49
|
+
if (seenVariables.has(font.cssVariable)) {
|
|
50
|
+
throw new Error(`Brand font cssVariable ${font.cssVariable} is declared twice.`);
|
|
51
|
+
}
|
|
52
|
+
seenVariables.add(font.cssVariable);
|
|
53
|
+
const sourceCount = Number(Boolean(font.google)) + Number(Boolean(font.faces?.length));
|
|
54
|
+
if (sourceCount !== 1) {
|
|
55
|
+
throw new Error(`Brand font "${font.family}" must declare exactly one source: google or faces.`);
|
|
56
|
+
}
|
|
57
|
+
if (font.google && font.google.weights.length === 0) {
|
|
58
|
+
throw new Error(`Brand font "${font.family}" google source needs at least one weight.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildGoogleFamilyQuery(font: SiteBrandFont): string {
|
|
64
|
+
const google = font.google!;
|
|
65
|
+
const weights = [...new Set(google.weights)].sort((left, right) => left - right);
|
|
66
|
+
const familyName = font.family.trim().replace(/\s+/g, "+");
|
|
67
|
+
if (!google.italics) return `family=${familyName}:wght@${weights.join(";")}`;
|
|
68
|
+
const axisValues = [0, 1].flatMap((italic) => weights.map((weight) => `${italic},${weight}`));
|
|
69
|
+
return `family=${familyName}:ital,wght@${axisValues.join(";")}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildGoogleFontsHref(fonts: SiteBrandFont[]): string | null {
|
|
73
|
+
const googleFonts = fonts.filter((font) => font.google);
|
|
74
|
+
if (googleFonts.length === 0) return null;
|
|
75
|
+
return `${GOOGLE_STYLESHEET_ORIGIN}/css2?${googleFonts.map(buildGoogleFamilyQuery).join("&")}&display=swap`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildFontFaceCss(font: SiteBrandFont): string {
|
|
79
|
+
return (font.faces ?? [])
|
|
80
|
+
.map((face) => {
|
|
81
|
+
const declarations = [
|
|
82
|
+
`font-family:${JSON.stringify(font.family)}`,
|
|
83
|
+
`src:url(${JSON.stringify(face.src)})`,
|
|
84
|
+
`font-weight:${face.weight ?? 400}`,
|
|
85
|
+
`font-style:${face.style ?? "normal"}`,
|
|
86
|
+
"font-display:swap",
|
|
87
|
+
];
|
|
88
|
+
return `@font-face{${declarations.join(";")}}`;
|
|
89
|
+
})
|
|
90
|
+
.join("");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveBrandFontAssets(fonts: SiteBrandFont[] | undefined): BrandFontAssets {
|
|
94
|
+
if (!fonts || fonts.length === 0) {
|
|
95
|
+
return { preconnectOrigins: [], stylesheetHrefs: [], fontFaceCss: "", rootStyle: {}, isEmpty: true };
|
|
96
|
+
}
|
|
97
|
+
validateBrandFonts(fonts);
|
|
98
|
+
const googleHref = buildGoogleFontsHref(fonts);
|
|
99
|
+
return {
|
|
100
|
+
preconnectOrigins: googleHref ? [GOOGLE_STYLESHEET_ORIGIN, GOOGLE_FILE_ORIGIN] : [],
|
|
101
|
+
stylesheetHrefs: googleHref ? [googleHref] : [],
|
|
102
|
+
fontFaceCss: fonts.map(buildFontFaceCss).join(""),
|
|
103
|
+
rootStyle: Object.fromEntries(
|
|
104
|
+
fonts.map((font) => [font.cssVariable, `${JSON.stringify(font.family)}, ${font.fallback ?? DEFAULT_FALLBACK}`]),
|
|
105
|
+
),
|
|
106
|
+
isEmpty: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function listBrandFontFamilies(fonts: SiteBrandFont[] | undefined): string[] {
|
|
111
|
+
return (fonts ?? []).map((font) => font.family);
|
|
112
|
+
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "./content-types";
|
|
11
11
|
import { graphqlContentRepository } from "./graphql-content-repository";
|
|
12
12
|
import { READER_REVALIDATE_SECONDS } from "./reader-route-config";
|
|
13
|
+
import type { VideoScriptRef } from "./video-script";
|
|
13
14
|
import { getContentSource } from "./site-env";
|
|
14
15
|
|
|
15
16
|
const bypassReaderCacheInDevelopment = process.env.NODE_ENV === "development";
|
|
@@ -88,3 +89,14 @@ export function getCachedLatestPublishedEdition(): Promise<EditionRouteSummary |
|
|
|
88
89
|
async () => graphqlContentRepository.getLatestPublishedEdition(),
|
|
89
90
|
);
|
|
90
91
|
}
|
|
92
|
+
|
|
93
|
+
export function getCachedVideoScript(targetSlug: string): Promise<VideoScriptRef | null> {
|
|
94
|
+
if (bypassReaderCacheInDevelopment) {
|
|
95
|
+
return Promise.resolve(graphqlContentRepository.loadVideoScript(targetSlug)).then((result) => result ?? null);
|
|
96
|
+
}
|
|
97
|
+
return withReaderCache(
|
|
98
|
+
["reader-video-script", targetSlug],
|
|
99
|
+
[articleCacheTag(targetSlug), ARTICLES_CACHE_TAG],
|
|
100
|
+
async () => graphqlContentRepository.loadVideoScript(targetSlug),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -769,85 +769,7 @@ function knowledgeCorpusIdFromKey(corpusKey: string): string {
|
|
|
769
769
|
}
|
|
770
770
|
|
|
771
771
|
export async function loadCategorySteeringDashboard(): Promise<CategorySteeringDashboard> {
|
|
772
|
-
|
|
773
|
-
if (proposals.length === 0) return createEmptyCategorySteeringDashboard();
|
|
774
|
-
return {
|
|
775
|
-
...createEmptyCategorySteeringDashboard(),
|
|
776
|
-
isPublicSkeleton: false,
|
|
777
|
-
proposals: sortProposals(proposals),
|
|
778
|
-
loadError: null,
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const SERVER_STEERING_PROPOSALS_QUERY = `
|
|
783
|
-
query ListSteeringProposals($limit: Int, $nextToken: String) {
|
|
784
|
-
listSteeringProposals(limit: $limit, nextToken: $nextToken) {
|
|
785
|
-
items {
|
|
786
|
-
id
|
|
787
|
-
categorySetId
|
|
788
|
-
corpusId
|
|
789
|
-
proposalKind
|
|
790
|
-
steeringDomain
|
|
791
|
-
status
|
|
792
|
-
title
|
|
793
|
-
summary
|
|
794
|
-
categoryKey
|
|
795
|
-
targetCategoryKey
|
|
796
|
-
graphEntityId
|
|
797
|
-
relationshipType
|
|
798
|
-
displayName
|
|
799
|
-
shortTitle
|
|
800
|
-
subtitle
|
|
801
|
-
description
|
|
802
|
-
evidenceItemIds
|
|
803
|
-
suggestedSeedItemIds
|
|
804
|
-
suggestedHoldoutItemIds
|
|
805
|
-
sourceSnapshotId
|
|
806
|
-
proposedAt
|
|
807
|
-
reviewedAt
|
|
808
|
-
reviewedBy
|
|
809
|
-
updatedAt
|
|
810
|
-
}
|
|
811
|
-
nextToken
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
`;
|
|
815
|
-
|
|
816
|
-
async function loadServerSteeringProposals(): Promise<CategorySteeringProposal[]> {
|
|
817
|
-
const endpoint = process.env.PAPYRUS_GRAPHQL_ENDPOINT;
|
|
818
|
-
const jwt = process.env.PAPYRUS_GRAPHQL_JWT;
|
|
819
|
-
if (!endpoint || !jwt) return [];
|
|
820
|
-
|
|
821
|
-
const items: CategorySteeringProposal[] = [];
|
|
822
|
-
let nextToken: string | null = null;
|
|
823
|
-
|
|
824
|
-
for (let page = 0; page < 20; page += 1) {
|
|
825
|
-
const response = await fetch(endpoint, {
|
|
826
|
-
method: "POST",
|
|
827
|
-
headers: {
|
|
828
|
-
"content-type": "application/json",
|
|
829
|
-
Authorization: `Bearer ${jwt}`,
|
|
830
|
-
},
|
|
831
|
-
body: JSON.stringify({
|
|
832
|
-
query: SERVER_STEERING_PROPOSALS_QUERY,
|
|
833
|
-
variables: { limit: 500, nextToken },
|
|
834
|
-
}),
|
|
835
|
-
cache: "no-store",
|
|
836
|
-
});
|
|
837
|
-
if (!response.ok) return [];
|
|
838
|
-
const payload = await response.json() as {
|
|
839
|
-
data?: { listSteeringProposals?: { items?: Array<CategorySteeringProposal | null> | null; nextToken?: string | null } | null } | null;
|
|
840
|
-
errors?: Array<{ message?: string }> | null;
|
|
841
|
-
};
|
|
842
|
-
if (payload.errors?.length) return [];
|
|
843
|
-
const connection = payload.data?.listSteeringProposals;
|
|
844
|
-
if (!connection) return [];
|
|
845
|
-
items.push(...(connection.items ?? []).filter(Boolean) as CategorySteeringProposal[]);
|
|
846
|
-
nextToken = connection.nextToken ?? null;
|
|
847
|
-
if (!nextToken) break;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
return items;
|
|
772
|
+
return createEmptyCategorySteeringDashboard();
|
|
851
773
|
}
|
|
852
774
|
|
|
853
775
|
function sortCategorySets(categorySets: CategorySteeringCategorySet[], importRuns: CategorySteeringImportRun[]): CategorySteeringCategorySet[] {
|
|
@@ -895,20 +817,6 @@ function sortCategorys(categorys: CategorySteeringCategory[]): CategorySteeringC
|
|
|
895
817
|
});
|
|
896
818
|
}
|
|
897
819
|
|
|
898
|
-
function sortProposals(proposals: CategorySteeringProposal[]): CategorySteeringProposal[] {
|
|
899
|
-
const statusWeight = new Map([
|
|
900
|
-
["proposed", 0],
|
|
901
|
-
["deferred", 1],
|
|
902
|
-
["accepted", 2],
|
|
903
|
-
["rejected", 3],
|
|
904
|
-
]);
|
|
905
|
-
return [...proposals].sort((left, right) => {
|
|
906
|
-
const statusDiff = (statusWeight.get(left.status) ?? 9) - (statusWeight.get(right.status) ?? 9);
|
|
907
|
-
if (statusDiff !== 0) return statusDiff;
|
|
908
|
-
return (right.proposedAt ?? right.updatedAt ?? "").localeCompare(left.proposedAt ?? left.updatedAt ?? "");
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
|
|
912
820
|
function defaultNewsroomSections(importedAt: string, sectionsConfigPath = DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH): NewsroomSectionRecord[] {
|
|
913
821
|
return loadNewsroomSectionSeedRows(sectionsConfigPath).map((row, index) => ({
|
|
914
822
|
...row,
|
|
@@ -1752,6 +1660,47 @@ export function createDemoCategorySteeringDashboard(
|
|
|
1752
1660
|
},
|
|
1753
1661
|
],
|
|
1754
1662
|
semanticRelations: [
|
|
1663
|
+
{
|
|
1664
|
+
// The relevance cyclotron's decision about the pending demo reference.
|
|
1665
|
+
id: "semantic-relation-demo-relevance-history-002",
|
|
1666
|
+
relationState: "current",
|
|
1667
|
+
predicate: "relevance_decision_is",
|
|
1668
|
+
relationTypeKey: "relevance_decision_is",
|
|
1669
|
+
relationDomain: "workflow",
|
|
1670
|
+
subjectKind: "reference",
|
|
1671
|
+
subjectId: referenceHistoryTwoId,
|
|
1672
|
+
subjectLineageId: referenceHistoryTwoLineageId,
|
|
1673
|
+
objectKind: "semanticNode",
|
|
1674
|
+
objectId: "semantic-node-relevance-include-v1",
|
|
1675
|
+
objectLineageId: "semantic-node-relevance-include",
|
|
1676
|
+
subjectStateKey: `reference#${referenceHistoryTwoLineageId}#current`,
|
|
1677
|
+
objectStateKey: "semanticNode#semantic-node-relevance-include#current",
|
|
1678
|
+
objectSubjectStateKey: "semanticNode#semantic-node-relevance-include#current#reference",
|
|
1679
|
+
predicateObjectStateKey: "relevance_decision_is#semanticNode#semantic-node-relevance-include#current",
|
|
1680
|
+
subjectVersionKey: `reference#${referenceHistoryTwoId}`,
|
|
1681
|
+
objectVersionKey: "semanticNode#semantic-node-relevance-include-v1",
|
|
1682
|
+
score: 0.82,
|
|
1683
|
+
confidence: 0.82,
|
|
1684
|
+
rank: 1,
|
|
1685
|
+
classifierId: "papyrus-relevance/relevant",
|
|
1686
|
+
modelVersion: "1",
|
|
1687
|
+
reviewRecommended: true,
|
|
1688
|
+
importRunId: "knowledge-import-demo-projection",
|
|
1689
|
+
importedAt,
|
|
1690
|
+
metadata: JSON.stringify({
|
|
1691
|
+
decisionId: "decision-demo-history-002",
|
|
1692
|
+
cyclotronId: "papyrus-relevance",
|
|
1693
|
+
label: "include",
|
|
1694
|
+
probabilities: { include: 0.82, exclude: 0.18 },
|
|
1695
|
+
labels: ["include", "exclude"],
|
|
1696
|
+
positiveLabel: "include",
|
|
1697
|
+
question: "Should this candidate source become a reference for this publication?",
|
|
1698
|
+
reviewControl: "thumbs",
|
|
1699
|
+
reviewReason: "program",
|
|
1700
|
+
reviewDetail: "Every decision is reviewed while the cyclotron is onboarding.",
|
|
1701
|
+
version: 1,
|
|
1702
|
+
}),
|
|
1703
|
+
},
|
|
1755
1704
|
{
|
|
1756
1705
|
id: "semantic-relation-demo-reporting-context-comment",
|
|
1757
1706
|
relationState: "current",
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { ContentActionFailure } from "./content-actions-client";
|
|
2
|
+
import type {
|
|
3
|
+
DeriveMarkusInput,
|
|
4
|
+
DeriveMarkusResult,
|
|
5
|
+
PublishItemResult,
|
|
6
|
+
SaveItemDraftInput,
|
|
7
|
+
SaveItemDraftResult,
|
|
8
|
+
UnpublishItemResult,
|
|
9
|
+
} from "./content-actions-client";
|
|
10
|
+
import {
|
|
11
|
+
createDemoArticleRecords,
|
|
12
|
+
demoContentHash,
|
|
13
|
+
type DemoArticleRecord,
|
|
14
|
+
} from "./newsroom-demo-articles";
|
|
15
|
+
import { sourceLineOffset, type ArticlesBackend, type EditableArticle, type NewsroomArticleRow } from "./newsroom-articles";
|
|
16
|
+
|
|
17
|
+
function titleFromYaml(frontMatterYaml: string | null, fallback: string): string {
|
|
18
|
+
for (const line of (frontMatterYaml ?? "").split("\n")) {
|
|
19
|
+
const match = /^title\s*:\s*(.*)$/.exec(line);
|
|
20
|
+
if (match && match[1].trim()) return match[1].trim().replace(/^["']|["']$/g, "");
|
|
21
|
+
}
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEMO_IMAGE_DIRECTIVE = /^::image\{([^}]*)\}$/;
|
|
26
|
+
|
|
27
|
+
function demoAttribute(attributes: string, name: string): string {
|
|
28
|
+
const match = new RegExp(`${name}="([^"]*)"`).exec(attributes);
|
|
29
|
+
return match ? match[1] : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function demoEnvelope(bodyMarkus: string) {
|
|
33
|
+
const images: Record<string, Record<string, unknown>> = {};
|
|
34
|
+
const children = bodyMarkus
|
|
35
|
+
.split(/\n{2,}/)
|
|
36
|
+
.map((block) => block.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map((block) => {
|
|
39
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(block);
|
|
40
|
+
if (heading) {
|
|
41
|
+
return { type: "heading", level: heading[1].length, inline: [{ type: "text", text: heading[2] }], line: null };
|
|
42
|
+
}
|
|
43
|
+
const image = DEMO_IMAGE_DIRECTIVE.exec(block);
|
|
44
|
+
if (image) {
|
|
45
|
+
const token = `PAPYRUSMARKUP${String(Object.keys(images).length + 1).padStart(5, "0")}END`;
|
|
46
|
+
images[token] = {
|
|
47
|
+
src: demoAttribute(image[1], "src"),
|
|
48
|
+
alt: demoAttribute(image[1], "alt"),
|
|
49
|
+
layout: demoAttribute(image[1], "layout") || null,
|
|
50
|
+
};
|
|
51
|
+
return { type: "paragraph", inline: [{ type: "text", text: token }], line: null };
|
|
52
|
+
}
|
|
53
|
+
return { type: "paragraph", inline: [{ type: "text", text: block }], line: null };
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
markus: { version: "demo", irSchemaVersion: 1 },
|
|
58
|
+
document: { type: "document", schema_version: 1, front_matter: {}, children },
|
|
59
|
+
papyrus: { images, citations: {}, citationLists: {}, entries: {}, bibliography: [] },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createDemoArticlesBackend(): ArticlesBackend {
|
|
64
|
+
const records = new Map<string, DemoArticleRecord>(createDemoArticleRecords().map((record) => [record.id, record]));
|
|
65
|
+
let created = 0;
|
|
66
|
+
|
|
67
|
+
const requireRecord = (id: string): DemoArticleRecord => {
|
|
68
|
+
const record = records.get(id);
|
|
69
|
+
if (!record) throw new ContentActionFailure([{ code: "not-found", message: "That article was not found.", line: null }]);
|
|
70
|
+
return record;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
actions: {
|
|
75
|
+
async deriveMarkus(input: DeriveMarkusInput): Promise<DeriveMarkusResult> {
|
|
76
|
+
const lines = input.bodyMarkus.split("\n");
|
|
77
|
+
const invalidIndex = lines.findIndex((line) => line.includes(":::nope"));
|
|
78
|
+
if (invalidIndex >= 0) {
|
|
79
|
+
throw new ContentActionFailure([
|
|
80
|
+
{ code: "markus-validation", message: 'Unknown directive "nope".', line: invalidIndex + 1 + sourceLineOffset(input.frontMatterYaml) },
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
const envelope = demoEnvelope(input.bodyMarkus);
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
bodyIrBytes: JSON.stringify(envelope).length,
|
|
87
|
+
...(input.includeIr ? { bodyIr: envelope } : {}),
|
|
88
|
+
errors: [],
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
async saveItemDraft(input: SaveItemDraftInput): Promise<SaveItemDraftResult> {
|
|
92
|
+
const existing = input.id ? requireRecord(input.id) : null;
|
|
93
|
+
if (existing && input.expectedContentHash !== null && input.expectedContentHash !== existing.contentHash) {
|
|
94
|
+
throw new ContentActionFailure([
|
|
95
|
+
{ code: "conflict", message: "The item changed since it was loaded.", line: null },
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
if (existing && existing.everPublished && existing.slug !== input.slug) {
|
|
99
|
+
throw new ContentActionFailure([
|
|
100
|
+
{ code: "slug-locked", message: "The slug cannot change once the item has been published.", line: null },
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
const id = existing?.id ?? `demo-article-new-${(created += 1)}`;
|
|
104
|
+
const title = titleFromYaml(input.frontMatterYaml, input.slug);
|
|
105
|
+
const frontMatterYaml = input.frontMatterYaml ?? "";
|
|
106
|
+
const record: DemoArticleRecord = {
|
|
107
|
+
id,
|
|
108
|
+
type: input.type,
|
|
109
|
+
slug: input.slug,
|
|
110
|
+
section: input.section,
|
|
111
|
+
title,
|
|
112
|
+
status: existing?.status ?? "draft",
|
|
113
|
+
frontMatterYaml,
|
|
114
|
+
bodyMarkus: input.bodyMarkus,
|
|
115
|
+
contentHash: demoContentHash(frontMatterYaml, input.bodyMarkus),
|
|
116
|
+
publishedContentHash: existing?.publishedContentHash ?? null,
|
|
117
|
+
versionNumber: existing?.versionNumber ?? null,
|
|
118
|
+
everPublished: existing?.everPublished ?? false,
|
|
119
|
+
updatedAt: new Date().toISOString(),
|
|
120
|
+
};
|
|
121
|
+
records.set(id, record);
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
item: {
|
|
125
|
+
id,
|
|
126
|
+
contentHash: record.contentHash,
|
|
127
|
+
status: record.status,
|
|
128
|
+
slug: record.slug,
|
|
129
|
+
versionNumber: record.versionNumber,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
async publishItem(id: string): Promise<PublishItemResult> {
|
|
134
|
+
const record = requireRecord(id);
|
|
135
|
+
const changed = record.status !== "published" || record.publishedContentHash !== record.contentHash;
|
|
136
|
+
const versionNumber = changed ? (record.versionNumber ?? 0) + 1 : record.versionNumber;
|
|
137
|
+
records.set(id, {
|
|
138
|
+
...record,
|
|
139
|
+
status: "published",
|
|
140
|
+
everPublished: true,
|
|
141
|
+
publishedContentHash: record.contentHash,
|
|
142
|
+
versionNumber,
|
|
143
|
+
updatedAt: new Date().toISOString(),
|
|
144
|
+
});
|
|
145
|
+
return { ok: true, changed, publishedId: `published-${id}`, versionNumber };
|
|
146
|
+
},
|
|
147
|
+
async unpublishItem(id: string): Promise<UnpublishItemResult> {
|
|
148
|
+
const record = requireRecord(id);
|
|
149
|
+
const changed = record.status === "published";
|
|
150
|
+
records.set(id, {
|
|
151
|
+
...record,
|
|
152
|
+
status: "draft",
|
|
153
|
+
publishedContentHash: null,
|
|
154
|
+
updatedAt: new Date().toISOString(),
|
|
155
|
+
});
|
|
156
|
+
return { ok: true, changed };
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
async listRows(): Promise<NewsroomArticleRow[]> {
|
|
160
|
+
return [...records.values()]
|
|
161
|
+
.map((record) => ({
|
|
162
|
+
id: record.id,
|
|
163
|
+
type: record.type,
|
|
164
|
+
slug: record.slug,
|
|
165
|
+
section: record.section,
|
|
166
|
+
title: record.title || record.slug,
|
|
167
|
+
status: record.status,
|
|
168
|
+
pending: record.status === "published" && record.publishedContentHash !== record.contentHash,
|
|
169
|
+
updatedAt: record.updatedAt,
|
|
170
|
+
}))
|
|
171
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
172
|
+
},
|
|
173
|
+
async loadArticle(id: string): Promise<EditableArticle> {
|
|
174
|
+
const record = requireRecord(id);
|
|
175
|
+
return {
|
|
176
|
+
id: record.id,
|
|
177
|
+
type: record.type,
|
|
178
|
+
section: record.section,
|
|
179
|
+
slug: record.slug,
|
|
180
|
+
frontMatterYaml: record.frontMatterYaml,
|
|
181
|
+
bodyMarkus: record.bodyMarkus,
|
|
182
|
+
contentHash: record.contentHash,
|
|
183
|
+
status: record.status,
|
|
184
|
+
versionNumber: record.versionNumber,
|
|
185
|
+
aliases: [],
|
|
186
|
+
everPublished: record.everPublished,
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -31,6 +31,9 @@ export const contentRepository: ContentRepository = {
|
|
|
31
31
|
listArticleSlugs() {
|
|
32
32
|
return graphqlContentRepository.listArticleSlugs();
|
|
33
33
|
},
|
|
34
|
+
loadVideoScript(targetSlug: string) {
|
|
35
|
+
return graphqlContentRepository.loadVideoScript(targetSlug);
|
|
36
|
+
},
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
export function getScenarioIdParam(scenario: string | string[] | null | undefined): string | null {
|
|
@@ -3,12 +3,22 @@ import { getContentSource, type ContentSource } from "./site-env";
|
|
|
3
3
|
|
|
4
4
|
export type ContentSourceContext = {
|
|
5
5
|
source: ContentSource;
|
|
6
|
+
/** Request-scoped data client (drafts mode: carries the visitor's session cookies). */
|
|
7
|
+
client?: unknown;
|
|
6
8
|
};
|
|
7
9
|
|
|
8
10
|
const contentSourceStorage = new AsyncLocalStorage<ContentSourceContext>();
|
|
9
11
|
|
|
10
|
-
export function runWithContentSource<T>(
|
|
11
|
-
|
|
12
|
+
export function runWithContentSource<T>(
|
|
13
|
+
source: ContentSource,
|
|
14
|
+
operation: () => Promise<T>,
|
|
15
|
+
client?: unknown,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
return contentSourceStorage.run({ source, client }, operation);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function currentRequestClient<T>(): T | undefined {
|
|
21
|
+
return contentSourceStorage.getStore()?.client as T | undefined;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
export function currentContentSource(): ContentSource {
|
package/lib/content-types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Article, ArticleVideoAsset } from "./articles";
|
|
2
2
|
import type { EditionLayoutPlan } from "./layout-plan";
|
|
3
3
|
import type { PublicationItem } from "./publication-items";
|
|
4
|
+
import type { VideoScriptRef } from "./video-script";
|
|
4
5
|
|
|
5
6
|
export type ContentSource = "scenario" | "graphql";
|
|
6
7
|
|
|
@@ -55,6 +56,8 @@ export type EditionContent = {
|
|
|
55
56
|
scenarioId?: string;
|
|
56
57
|
description?: string;
|
|
57
58
|
editionVideo?: ArticleVideoAsset | null;
|
|
59
|
+
/** Video scripts by item slug (`"edition-overview"` for the edition video), for the VideoML preview. */
|
|
60
|
+
videoScripts?: Record<string, VideoScriptRef>;
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
export type EditionRouteSummary = {
|
|
@@ -101,4 +104,5 @@ export type ContentRepository = {
|
|
|
101
104
|
getEditionArticle(options: GetEditionArticleOptions): Article | undefined | Promise<Article | undefined>;
|
|
102
105
|
getEditionItem(options: GetEditionItemOptions): PublicationItem | undefined | Promise<PublicationItem | undefined>;
|
|
103
106
|
listArticleSlugs(): string[] | Promise<string[]>;
|
|
107
|
+
loadVideoScript(targetSlug: string): VideoScriptRef | null | Promise<VideoScriptRef | null>;
|
|
104
108
|
};
|
package/lib/edition-routes.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getReaderBasePath, toPublicReaderPath } from "./reader-base-path";
|
|
2
|
+
|
|
1
3
|
const MONTH_NAMES = [
|
|
2
4
|
"january",
|
|
3
5
|
"february",
|
|
@@ -39,52 +41,52 @@ type ParsedEditionSectionRoute = ParsedEditionDateRoute & {
|
|
|
39
41
|
|
|
40
42
|
const RESERVED_DATE_CHILD_SEGMENTS = new Set(["page", "section"]);
|
|
41
43
|
|
|
42
|
-
export function getEditionDatePath(editionDate: string): string {
|
|
44
|
+
export function getEditionDatePath(editionDate: string, readerBasePath: string = getReaderBasePath()): string {
|
|
43
45
|
const match = editionDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
44
|
-
if (!match) return "/";
|
|
46
|
+
if (!match) return toPublicReaderPath("/", readerBasePath);
|
|
45
47
|
|
|
46
48
|
const monthIndex = Number(match[2]) - 1;
|
|
47
49
|
const monthName = MONTH_NAMES[monthIndex];
|
|
48
|
-
if (!monthName) return "/";
|
|
50
|
+
if (!monthName) return toPublicReaderPath("/", readerBasePath);
|
|
49
51
|
|
|
50
|
-
return `/${match[1]}/${monthName}/${match[3]}
|
|
52
|
+
return toPublicReaderPath(`/${match[1]}/${monthName}/${match[3]}`, readerBasePath);
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
export function getEditionPagePath(editionDate: string, pageNumber: number): string {
|
|
54
|
-
const basePath = getEditionDatePath(editionDate);
|
|
55
|
+
export function getEditionPagePath(editionDate: string, pageNumber: number, readerBasePath: string = getReaderBasePath()): string {
|
|
56
|
+
const basePath = getEditionDatePath(editionDate, readerBasePath);
|
|
55
57
|
return pageNumber <= 1 ? basePath : `${basePath}/page/${pageNumber}`;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
export function getEditionSectionPath(editionDate: string, sectionKey: string): string {
|
|
59
|
-
return `${getEditionDatePath(editionDate)}/section/${encodeURIComponent(sectionKey)}`;
|
|
60
|
+
export function getEditionSectionPath(editionDate: string, sectionKey: string, readerBasePath: string = getReaderBasePath()): string {
|
|
61
|
+
return `${getEditionDatePath(editionDate, readerBasePath)}/section/${encodeURIComponent(sectionKey)}`;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
export function getEditionArticlePath(editionDate: string, articleSlug: string): string {
|
|
63
|
-
return `${getEditionDatePath(editionDate)}/${encodeURIComponent(articleSlug)}`;
|
|
64
|
+
export function getEditionArticlePath(editionDate: string, articleSlug: string, readerBasePath: string = getReaderBasePath()): string {
|
|
65
|
+
return `${getEditionDatePath(editionDate, readerBasePath)}/${encodeURIComponent(articleSlug)}`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export const getEditionItemPath = getEditionArticlePath;
|
|
67
69
|
|
|
68
|
-
export function parseEditionDateRoute(input: EditionDateRouteInput): ParsedEditionDateRoute | null {
|
|
69
|
-
const parsed = parseEditionDateSegments(input);
|
|
70
|
+
export function parseEditionDateRoute(input: EditionDateRouteInput, readerBasePath: string = getReaderBasePath()): ParsedEditionDateRoute | null {
|
|
71
|
+
const parsed = parseEditionDateSegments(input, readerBasePath);
|
|
70
72
|
if (!parsed) return null;
|
|
71
73
|
|
|
72
74
|
return {
|
|
73
75
|
...parsed,
|
|
74
|
-
isCanonical: getCurrentDatePath(input) === parsed.canonicalPath,
|
|
76
|
+
isCanonical: getCurrentDatePath(input, readerBasePath) === parsed.canonicalPath,
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
export function parseEditionPageRoute(input: EditionDateRouteInput & { pageNumber: string }): ParsedEditionPageRoute | null {
|
|
79
|
-
const parsed = parseEditionDateSegments(input);
|
|
80
|
+
export function parseEditionPageRoute(input: EditionDateRouteInput & { pageNumber: string }, readerBasePath: string = getReaderBasePath()): ParsedEditionPageRoute | null {
|
|
81
|
+
const parsed = parseEditionDateSegments(input, readerBasePath);
|
|
80
82
|
if (!parsed) return null;
|
|
81
83
|
if (!/^\d+$/.test(input.pageNumber)) return null;
|
|
82
84
|
|
|
83
85
|
const pageNumber = Number(input.pageNumber);
|
|
84
86
|
if (!Number.isSafeInteger(pageNumber) || pageNumber < 1) return null;
|
|
85
87
|
|
|
86
|
-
const canonicalPath = getEditionPagePath(parsed.editionDate, pageNumber);
|
|
87
|
-
const currentPath = `${getCurrentDatePath(input)}/page/${input.pageNumber}`;
|
|
88
|
+
const canonicalPath = getEditionPagePath(parsed.editionDate, pageNumber, readerBasePath);
|
|
89
|
+
const currentPath = `${getCurrentDatePath(input, readerBasePath)}/page/${input.pageNumber}`;
|
|
88
90
|
return {
|
|
89
91
|
editionDate: parsed.editionDate,
|
|
90
92
|
canonicalPath,
|
|
@@ -93,13 +95,13 @@ export function parseEditionPageRoute(input: EditionDateRouteInput & { pageNumbe
|
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
export function parseEditionArticleRoute(input: EditionDateRouteInput & { articleSlug: string }): ParsedEditionArticleRoute | null {
|
|
97
|
-
const parsed = parseEditionDateSegments(input);
|
|
98
|
+
export function parseEditionArticleRoute(input: EditionDateRouteInput & { articleSlug: string }, readerBasePath: string = getReaderBasePath()): ParsedEditionArticleRoute | null {
|
|
99
|
+
const parsed = parseEditionDateSegments(input, readerBasePath);
|
|
98
100
|
if (!parsed) return null;
|
|
99
101
|
if (RESERVED_DATE_CHILD_SEGMENTS.has(input.articleSlug.toLowerCase())) return null;
|
|
100
102
|
|
|
101
|
-
const canonicalPath = getEditionArticlePath(parsed.editionDate, input.articleSlug);
|
|
102
|
-
const currentPath = `${getCurrentDatePath(input)}/${input.articleSlug}`;
|
|
103
|
+
const canonicalPath = getEditionArticlePath(parsed.editionDate, input.articleSlug, readerBasePath);
|
|
104
|
+
const currentPath = `${getCurrentDatePath(input, readerBasePath)}/${input.articleSlug}`;
|
|
103
105
|
return {
|
|
104
106
|
editionDate: parsed.editionDate,
|
|
105
107
|
canonicalPath,
|
|
@@ -108,13 +110,13 @@ export function parseEditionArticleRoute(input: EditionDateRouteInput & { articl
|
|
|
108
110
|
};
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
export function parseEditionSectionRoute(input: EditionDateRouteInput & { sectionKey: string }): ParsedEditionSectionRoute | null {
|
|
112
|
-
const parsed = parseEditionDateSegments(input);
|
|
113
|
+
export function parseEditionSectionRoute(input: EditionDateRouteInput & { sectionKey: string }, readerBasePath: string = getReaderBasePath()): ParsedEditionSectionRoute | null {
|
|
114
|
+
const parsed = parseEditionDateSegments(input, readerBasePath);
|
|
113
115
|
if (!parsed) return null;
|
|
114
116
|
if (!input.sectionKey.trim()) return null;
|
|
115
117
|
|
|
116
|
-
const canonicalPath = getEditionSectionPath(parsed.editionDate, input.sectionKey);
|
|
117
|
-
const currentPath = `${getCurrentDatePath(input)}/section/${input.sectionKey}`;
|
|
118
|
+
const canonicalPath = getEditionSectionPath(parsed.editionDate, input.sectionKey, readerBasePath);
|
|
119
|
+
const currentPath = `${getCurrentDatePath(input, readerBasePath)}/section/${input.sectionKey}`;
|
|
118
120
|
return {
|
|
119
121
|
editionDate: parsed.editionDate,
|
|
120
122
|
canonicalPath,
|
|
@@ -123,7 +125,7 @@ export function parseEditionSectionRoute(input: EditionDateRouteInput & { sectio
|
|
|
123
125
|
};
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
function parseEditionDateSegments(input: EditionDateRouteInput): Omit<ParsedEditionDateRoute, "isCanonical"> | null {
|
|
128
|
+
function parseEditionDateSegments(input: EditionDateRouteInput, readerBasePath: string): Omit<ParsedEditionDateRoute, "isCanonical"> | null {
|
|
127
129
|
if (!/^\d{4}$/.test(input.year)) return null;
|
|
128
130
|
|
|
129
131
|
const monthIndex = MONTH_NAMES.indexOf(input.month.toLowerCase() as (typeof MONTH_NAMES)[number]);
|
|
@@ -140,12 +142,12 @@ function parseEditionDateSegments(input: EditionDateRouteInput): Omit<ParsedEdit
|
|
|
140
142
|
|
|
141
143
|
return {
|
|
142
144
|
editionDate,
|
|
143
|
-
canonicalPath: getEditionDatePath(editionDate),
|
|
145
|
+
canonicalPath: getEditionDatePath(editionDate, readerBasePath),
|
|
144
146
|
};
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
function getCurrentDatePath(input: EditionDateRouteInput): string {
|
|
148
|
-
return `/${input.year}/${input.month}/${input.day}
|
|
149
|
+
function getCurrentDatePath(input: EditionDateRouteInput, readerBasePath: string): string {
|
|
150
|
+
return toPublicReaderPath(`/${input.year}/${input.month}/${input.day}`, readerBasePath);
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
function isValidIsoDate(editionDate: string): boolean {
|