@notionx/create-notionx-app 1.0.0 → 2.0.1
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/dist/cli-notionx.js +25 -1
- package/dist/cli-notionx.js.map +1 -1
- package/dist/locale-add/persist.js +113 -0
- package/dist/locale-add/persist.js.map +1 -0
- package/dist/locale-add/plan.js +202 -21
- package/dist/locale-add/plan.js.map +1 -1
- package/dist/metadata.js.map +1 -1
- package/dist/notion-translation-sources/apply.js +11 -26
- package/dist/notion-translation-sources/apply.js.map +1 -1
- package/dist/notion-translation-sources/plan.js +25 -0
- package/dist/notion-translation-sources/plan.js.map +1 -1
- package/dist/provision/__tests__/translation-properties.test.js +86 -0
- package/dist/provision/__tests__/translation-properties.test.js.map +1 -0
- package/dist/provision/credentials.js +67 -0
- package/dist/provision/credentials.js.map +1 -0
- package/dist/provision/index.js +188 -11
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/notion.js +422 -269
- package/dist/provision/notion.js.map +1 -1
- package/dist/provision/notion.test.js +143 -116
- package/dist/provision/notion.test.js.map +1 -1
- package/dist/provision/wire.js +16 -0
- package/dist/provision/wire.js.map +1 -1
- package/dist/registry/install.test.js +2 -0
- package/dist/registry/install.test.js.map +1 -1
- package/dist/registry/project-meta.js +4 -2
- package/dist/registry/project-meta.js.map +1 -1
- package/dist/registry/registry-types.js.map +1 -1
- package/dist/registry/render-content-source-files.js +1 -0
- package/dist/registry/render-content-source-files.js.map +1 -1
- package/dist/registry/render-multi-source.js +72 -28
- package/dist/registry/render-multi-source.js.map +1 -1
- package/dist/registry/update.test.js +2 -0
- package/dist/registry/update.test.js.map +1 -1
- package/dist/render.js +2 -0
- package/dist/render.js.map +1 -1
- package/dist/render.test.js +18 -12
- package/dist/render.test.js.map +1 -1
- package/dist/templates/.dev.vars.example.tmpl +4 -0
- package/dist/templates/__tests__/middleware-integration.test.ts +58 -0
- package/dist/templates/app/{{contentSourceListPath}}/[slug]/page.tsx.tmpl +8 -4
- package/dist/templates/app/{{contentSourceListPath}}/page.tsx.tmpl +8 -4
- package/dist/templates/components/page-blocks/feature-grid-block.tsx.tmpl +4 -56
- package/dist/templates/components/page-blocks/hero-block.tsx.tmpl +6 -67
- package/dist/templates/components/page-blocks/latest-posts-block.tsx.tmpl +11 -19
- package/dist/templates/components/page-blocks/story-block.tsx.tmpl +4 -62
- package/dist/templates/components/page-blocks.tsx.tmpl +5 -5
- package/dist/templates/components/site/site-header.tsx.tmpl +5 -3
- package/dist/templates/env.d.ts.tmpl +8 -0
- package/dist/templates/lib/blocks/translations.ts.tmpl +22 -1
- package/dist/templates/lib/blog/translations.ts.tmpl +19 -3
- package/dist/templates/lib/content/models.ts.tmpl +6 -0
- package/dist/templates/lib/locale-contract/index.ts.tmpl +1 -1
- package/dist/templates/lib/pages/source.ts.tmpl +140 -335
- package/dist/templates/lib/pages/translations.ts.tmpl +23 -1
- package/dist/templates/lib/search/config.ts.tmpl +17 -4
- package/dist/templates/lib/site/request-env.ts.tmpl +21 -3
- package/dist/templates/lib/site/settings.ts.tmpl +99 -179
- package/dist/templates/lib/site/translations.ts.tmpl +34 -11
- package/dist/templates/middleware.ts.tmpl +56 -0
- package/dist/templates/worker/index.ts.tmpl +14 -5
- package/dist/templates/wrangler.jsonc.tmpl +5 -1
- package/package.json +1 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getGenericNotionContentBySlug,
|
|
3
2
|
type GenericContentDetail,
|
|
4
3
|
type NotionBlock,
|
|
5
4
|
} from "@notionx/core/notion";
|
|
5
|
+
import {
|
|
6
|
+
getGenericNotionContentBySlugForLocale,
|
|
7
|
+
getGenericNotionContentByIdForLocale,
|
|
8
|
+
} from "@notionx/core";
|
|
6
9
|
import {
|
|
7
10
|
createSitePagesApi,
|
|
8
11
|
type SitePage as BaseSitePage,
|
|
9
|
-
type SitePageBlockRef,
|
|
10
12
|
} from "@notionx/core/pages";
|
|
11
13
|
import { blocksSource, contentSources } from "@/lib/content/models";
|
|
14
|
+
import { getRequestLocale } from "@/lib/site/request-env";
|
|
15
|
+
import { i18n } from "@/lib/i18n";
|
|
12
16
|
import { pageFields, pagesDataSourceEnv } from "./model";
|
|
13
17
|
|
|
14
18
|
const pagesModel = {
|
|
@@ -35,167 +39,106 @@ const fallbackBlocks = (text: string): NotionBlock[] => [
|
|
|
35
39
|
},
|
|
36
40
|
];
|
|
37
41
|
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
type BlockCta = {
|
|
41
|
-
label: string;
|
|
42
|
-
href: string;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
type FeatureGridItem = {
|
|
46
|
-
title: string;
|
|
47
|
-
description: string;
|
|
48
|
-
icon: string;
|
|
49
|
-
href?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export type StructuredHeroBlock = {
|
|
53
|
-
type: "hero";
|
|
54
|
-
slug: string;
|
|
55
|
-
title: string;
|
|
56
|
-
description: string;
|
|
57
|
-
eyebrow: string;
|
|
58
|
-
headline: string;
|
|
59
|
-
subheadline: string;
|
|
60
|
-
primaryCta: BlockCta | null;
|
|
61
|
-
secondaryCta: BlockCta | null;
|
|
62
|
-
alignment: "left" | "center";
|
|
63
|
-
theme: "default" | "muted" | "inverse";
|
|
64
|
-
coverImage: string | null;
|
|
65
|
-
editUrl: string | null;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export type StructuredFeatureGridBlock = {
|
|
69
|
-
type: "feature-grid";
|
|
70
|
-
slug: string;
|
|
71
|
-
title: string;
|
|
72
|
-
description: string;
|
|
73
|
-
headline: string;
|
|
74
|
-
body: string;
|
|
75
|
-
columns: 2 | 3 | 4;
|
|
76
|
-
items: FeatureGridItem[];
|
|
77
|
-
coverImage: string | null;
|
|
78
|
-
editUrl: string | null;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export type StructuredStoryBlock = {
|
|
82
|
-
type: "story";
|
|
83
|
-
slug: string;
|
|
84
|
-
title: string;
|
|
85
|
-
description: string;
|
|
86
|
-
headline: string;
|
|
87
|
-
body: string;
|
|
88
|
-
quote: string;
|
|
89
|
-
quoteAttribution: string;
|
|
90
|
-
mediaUrl: string | null;
|
|
91
|
-
layout: "text-left" | "media-left" | "media-right";
|
|
92
|
-
coverImage: string | null;
|
|
93
|
-
editUrl: string | null;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export type StructuredLatestPostsBlock = {
|
|
97
|
-
type: "latest-posts";
|
|
42
|
+
export type StructuredPageBlock = {
|
|
98
43
|
slug: string;
|
|
99
|
-
title: string;
|
|
100
|
-
description: string;
|
|
101
|
-
headline: string;
|
|
102
|
-
body: string;
|
|
103
|
-
count: number;
|
|
104
|
-
primaryCta: BlockCta | null;
|
|
105
|
-
coverImage: string | null;
|
|
106
|
-
editUrl: string | null;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export type LegacyStructuredPageBlock = {
|
|
110
|
-
type: "legacy";
|
|
111
|
-
slug: string;
|
|
112
|
-
title: string;
|
|
113
|
-
description: string;
|
|
114
44
|
variant: string;
|
|
45
|
+
order: number;
|
|
115
46
|
coverImage: string | null;
|
|
116
47
|
editUrl: string | null;
|
|
117
48
|
blocks: NotionBlock[];
|
|
118
49
|
};
|
|
119
50
|
|
|
120
|
-
export type StructuredPageBlock =
|
|
121
|
-
| StructuredHeroBlock
|
|
122
|
-
| StructuredFeatureGridBlock
|
|
123
|
-
| StructuredStoryBlock
|
|
124
|
-
| StructuredLatestPostsBlock
|
|
125
|
-
| LegacyStructuredPageBlock;
|
|
126
|
-
|
|
127
51
|
export type SitePage = Omit<BaseSitePage, "structuredBlocks"> & {
|
|
128
52
|
structuredBlocks: StructuredPageBlock[];
|
|
129
53
|
};
|
|
130
54
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
55
|
+
// `SitePageBlockRef` is not exported from `@notionx/core/pages`,
|
|
56
|
+
// but it is the element type of `SitePage.structuredBlocks`.
|
|
57
|
+
type SitePageBlockRef = BaseSitePage["structuredBlocks"][number];
|
|
58
|
+
|
|
59
|
+
const fallbackStructuredBlocks: Record<string, StructuredPageBlock> = {
|
|
135
60
|
"home-hero": {
|
|
136
|
-
type: "hero",
|
|
137
61
|
slug: "home-hero",
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"Editable hero module seeded into the reusable blocks source for the homepage.",
|
|
141
|
-
eyebrow: "Notion + Cloudflare",
|
|
142
|
-
headline: "Start with a homepage you can keep editing",
|
|
143
|
-
subheadline:
|
|
144
|
-
"Keep the layout stable in code while the hero copy, positioning, and primary call to action evolve in Notion.",
|
|
145
|
-
primaryCta: { label: "Explore the blog", href: "{{contentSourceListPath}}" },
|
|
146
|
-
secondaryCta: { label: "Read the story", href: "/about" },
|
|
147
|
-
alignment: "center",
|
|
148
|
-
theme: "muted",
|
|
62
|
+
variant: "hero",
|
|
63
|
+
order: 10,
|
|
149
64
|
coverImage: null,
|
|
150
65
|
editUrl: null,
|
|
151
|
-
|
|
152
|
-
"home-feature-grid": {
|
|
153
|
-
type: "feature-grid",
|
|
154
|
-
slug: "home-feature-grid",
|
|
155
|
-
title: "Homepage Feature Grid",
|
|
156
|
-
description:
|
|
157
|
-
"Reusable mid-page section for capabilities, benefits, or service pillars.",
|
|
158
|
-
headline: "Show the system working together",
|
|
159
|
-
body:
|
|
160
|
-
"Use this grid to explain how editing, infrastructure, and publishing fit together without overwhelming the homepage.",
|
|
161
|
-
columns: 3,
|
|
162
|
-
items: [
|
|
66
|
+
blocks: [
|
|
163
67
|
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
68
|
+
id: "fallback-hero-heading",
|
|
69
|
+
type: "heading_1",
|
|
70
|
+
heading_1: {
|
|
71
|
+
rich_text: [
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: { content: "Start with a homepage you can keep editing" },
|
|
75
|
+
plain_text: "Start with a homepage you can keep editing",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
175
79
|
},
|
|
176
80
|
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
81
|
+
id: "fallback-hero-sub",
|
|
82
|
+
type: "paragraph",
|
|
83
|
+
paragraph: {
|
|
84
|
+
rich_text: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: {
|
|
88
|
+
content:
|
|
89
|
+
"Keep the layout stable in code while the hero copy, positioning, and primary call to action evolve in Notion.",
|
|
90
|
+
},
|
|
91
|
+
plain_text:
|
|
92
|
+
"Keep the layout stable in code while the hero copy, positioning, and primary call to action evolve in Notion.",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
182
96
|
},
|
|
183
97
|
],
|
|
98
|
+
},
|
|
99
|
+
"home-feature-grid": {
|
|
100
|
+
slug: "home-feature-grid",
|
|
101
|
+
variant: "feature-grid",
|
|
102
|
+
order: 20,
|
|
184
103
|
coverImage: null,
|
|
185
104
|
editUrl: null,
|
|
105
|
+
blocks: [
|
|
106
|
+
{
|
|
107
|
+
id: "fallback-feature-heading",
|
|
108
|
+
type: "heading_2",
|
|
109
|
+
heading_2: {
|
|
110
|
+
rich_text: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: { content: "Show the system working together" },
|
|
114
|
+
plain_text: "Show the system working together",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
186
120
|
},
|
|
187
121
|
"home-latest-posts": {
|
|
188
|
-
type: "latest-posts",
|
|
189
122
|
slug: "home-latest-posts",
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
headline: "Read the latest from the blog",
|
|
193
|
-
body:
|
|
194
|
-
"Use this section to prove the content model is working with a grid of recent published posts right on the homepage.",
|
|
195
|
-
count: 6,
|
|
196
|
-
primaryCta: { label: "View all posts", href: "{{contentSourceListPath}}" },
|
|
123
|
+
variant: "latest-posts",
|
|
124
|
+
order: 30,
|
|
197
125
|
coverImage: null,
|
|
198
126
|
editUrl: null,
|
|
127
|
+
blocks: [
|
|
128
|
+
{
|
|
129
|
+
id: "fallback-latest-heading",
|
|
130
|
+
type: "heading_2",
|
|
131
|
+
heading_2: {
|
|
132
|
+
rich_text: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: { content: "Read the latest from the blog" },
|
|
136
|
+
plain_text: "Read the latest from the blog",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
199
142
|
},
|
|
200
143
|
};
|
|
201
144
|
|
|
@@ -226,9 +169,9 @@ const fallbackPages: BaseSitePage[] = [
|
|
|
226
169
|
coverImage: null,
|
|
227
170
|
editUrl: null,
|
|
228
171
|
structuredBlocks: [
|
|
229
|
-
{ slug: "home-hero"
|
|
230
|
-
{ slug: "home-feature-grid"
|
|
231
|
-
{ slug: "home-latest-posts"
|
|
172
|
+
{ slug: "home-hero" },
|
|
173
|
+
{ slug: "home-feature-grid" },
|
|
174
|
+
{ slug: "home-latest-posts" },
|
|
232
175
|
],
|
|
233
176
|
blocks: fallbackBlocks("Configure Notion Pages to edit this homepage from Notion."),
|
|
234
177
|
},
|
|
@@ -320,241 +263,103 @@ const pagesApi = createSitePagesApi({
|
|
|
320
263
|
});
|
|
321
264
|
|
|
322
265
|
function fallbackPageBlock(ref: SitePageBlockRef): StructuredPageBlock | null {
|
|
266
|
+
if (!ref.slug) return null;
|
|
323
267
|
const fallback = fallbackStructuredBlocks[ref.slug];
|
|
324
268
|
if (!fallback) return null;
|
|
325
269
|
return fallback;
|
|
326
270
|
}
|
|
327
271
|
|
|
328
|
-
function
|
|
272
|
+
function mapGenericBlockToStructuredBlock(
|
|
329
273
|
detail: GenericContentDetail,
|
|
330
|
-
ref: SitePageBlockRef
|
|
331
|
-
|
|
332
|
-
|
|
274
|
+
ref: SitePageBlockRef,
|
|
275
|
+
index: number
|
|
276
|
+
): StructuredPageBlock {
|
|
277
|
+
const typeValue = detail.properties.type;
|
|
278
|
+
const variant =
|
|
279
|
+
(typeof typeValue === "string" ? typeValue.trim() : "") ||
|
|
280
|
+
ref.variant ||
|
|
281
|
+
"legacy";
|
|
282
|
+
const orderValue = detail.properties.order;
|
|
283
|
+
const order =
|
|
284
|
+
typeof orderValue === "number" && Number.isFinite(orderValue)
|
|
285
|
+
? orderValue
|
|
286
|
+
: index;
|
|
333
287
|
return {
|
|
334
|
-
type: "legacy",
|
|
335
288
|
slug: detail.slug,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
variant: ref.variant ?? "story",
|
|
289
|
+
variant,
|
|
290
|
+
order,
|
|
339
291
|
coverImage: detail.coverImage,
|
|
340
292
|
editUrl: detail.editUrl,
|
|
341
293
|
blocks: detail.blocks,
|
|
342
294
|
};
|
|
343
295
|
}
|
|
344
296
|
|
|
345
|
-
function readString(value: BlockPropertyValue | undefined) {
|
|
346
|
-
if (Array.isArray(value)) return String(value[0] ?? "").trim();
|
|
347
|
-
if (typeof value === "string") return value.trim();
|
|
348
|
-
return "";
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function readNumber(value: BlockPropertyValue | undefined) {
|
|
352
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function normalizeAlignment(value: string): StructuredHeroBlock["alignment"] {
|
|
356
|
-
return value === "left" ? "left" : "center";
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function normalizeTheme(value: string): StructuredHeroBlock["theme"] {
|
|
360
|
-
if (value === "default" || value === "inverse") return value;
|
|
361
|
-
return "muted";
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function normalizeColumns(value: number | null): StructuredFeatureGridBlock["columns"] {
|
|
365
|
-
if (value === 2 || value === 4) return value;
|
|
366
|
-
return 3;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function normalizeLayout(value: string): StructuredStoryBlock["layout"] {
|
|
370
|
-
if (value === "text-left" || value === "media-left") return value;
|
|
371
|
-
return "media-right";
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function normalizeCount(value: number | null) {
|
|
375
|
-
if (typeof value === "number" && value > 0) return Math.min(Math.floor(value), 12);
|
|
376
|
-
return 6;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function readCta(
|
|
380
|
-
labelValue: BlockPropertyValue | undefined,
|
|
381
|
-
hrefValue: BlockPropertyValue | undefined
|
|
382
|
-
): BlockCta | null {
|
|
383
|
-
const label = readString(labelValue);
|
|
384
|
-
const href = readString(hrefValue);
|
|
385
|
-
return label && href ? { label, href } : null;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function parseFeatureGridItems(
|
|
389
|
-
value: BlockPropertyValue | undefined
|
|
390
|
-
): FeatureGridItem[] {
|
|
391
|
-
const raw = readString(value);
|
|
392
|
-
if (!raw) return [];
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
396
|
-
if (!Array.isArray(parsed)) return [];
|
|
397
|
-
return parsed
|
|
398
|
-
.map((item): FeatureGridItem | null => {
|
|
399
|
-
if (!item || typeof item !== "object") return null;
|
|
400
|
-
const typed = item as {
|
|
401
|
-
title?: unknown;
|
|
402
|
-
description?: unknown;
|
|
403
|
-
icon?: unknown;
|
|
404
|
-
href?: unknown;
|
|
405
|
-
};
|
|
406
|
-
const title = typeof typed.title === "string" ? typed.title.trim() : "";
|
|
407
|
-
const description =
|
|
408
|
-
typeof typed.description === "string" ? typed.description.trim() : "";
|
|
409
|
-
if (!title || !description) return null;
|
|
410
|
-
return {
|
|
411
|
-
title,
|
|
412
|
-
description,
|
|
413
|
-
icon: typeof typed.icon === "string" ? typed.icon.trim() : "sparkles",
|
|
414
|
-
href: typeof typed.href === "string" ? typed.href.trim() : undefined,
|
|
415
|
-
};
|
|
416
|
-
})
|
|
417
|
-
.filter((item): item is FeatureGridItem => Boolean(item));
|
|
418
|
-
} catch {
|
|
419
|
-
return [];
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function resolveBlockType(detail: GenericContentDetail, ref: SitePageBlockRef) {
|
|
424
|
-
const fromRecord = readString(detail.properties.type);
|
|
425
|
-
return fromRecord || ref.variant || "";
|
|
426
|
-
}
|
|
427
|
-
|
|
428
297
|
async function resolveStructuredBlock(
|
|
429
|
-
ref: SitePageBlockRef
|
|
298
|
+
ref: SitePageBlockRef,
|
|
299
|
+
locale: string,
|
|
300
|
+
index: number
|
|
430
301
|
): Promise<StructuredPageBlock | null> {
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
ref: SitePageBlockRef
|
|
440
|
-
): StructuredPageBlock | null {
|
|
441
|
-
const type = resolveBlockType(detail, ref);
|
|
442
|
-
|
|
443
|
-
if (type === "hero") {
|
|
444
|
-
const headline = readString(detail.properties.headline);
|
|
445
|
-
if (!headline) return fallbackToLegacyNotionBlocks(detail, ref);
|
|
446
|
-
return {
|
|
447
|
-
type: "hero",
|
|
448
|
-
slug: detail.slug,
|
|
449
|
-
title: detail.title,
|
|
450
|
-
description: detail.description,
|
|
451
|
-
eyebrow: readString(detail.properties.eyebrow),
|
|
452
|
-
headline,
|
|
453
|
-
subheadline: readString(detail.properties.subheadline),
|
|
454
|
-
primaryCta: readCta(
|
|
455
|
-
detail.properties.primaryCtaLabel,
|
|
456
|
-
detail.properties.primaryCtaHref
|
|
457
|
-
),
|
|
458
|
-
secondaryCta: readCta(
|
|
459
|
-
detail.properties.secondaryCtaLabel,
|
|
460
|
-
detail.properties.secondaryCtaHref
|
|
461
|
-
),
|
|
462
|
-
alignment: normalizeAlignment(readString(detail.properties.alignment)),
|
|
463
|
-
theme: normalizeTheme(readString(detail.properties.theme)),
|
|
464
|
-
coverImage: detail.coverImage,
|
|
465
|
-
editUrl: detail.editUrl,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (type === "feature-grid") {
|
|
470
|
-
const headline = readString(detail.properties.headline);
|
|
471
|
-
const items = parseFeatureGridItems(detail.properties.items);
|
|
472
|
-
if (!headline || !items.length) return fallbackToLegacyNotionBlocks(detail, ref);
|
|
473
|
-
return {
|
|
474
|
-
type: "feature-grid",
|
|
475
|
-
slug: detail.slug,
|
|
476
|
-
title: detail.title,
|
|
477
|
-
description: detail.description,
|
|
478
|
-
headline,
|
|
479
|
-
body: readString(detail.properties.body),
|
|
480
|
-
columns: normalizeColumns(readNumber(detail.properties.columns)),
|
|
481
|
-
items,
|
|
482
|
-
coverImage: detail.coverImage,
|
|
483
|
-
editUrl: detail.editUrl,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (type === "story") {
|
|
488
|
-
const headline = readString(detail.properties.headline);
|
|
489
|
-
const body = readString(detail.properties.body);
|
|
490
|
-
if (!headline || !body) return fallbackToLegacyNotionBlocks(detail, ref);
|
|
491
|
-
return {
|
|
492
|
-
type: "story",
|
|
493
|
-
slug: detail.slug,
|
|
494
|
-
title: detail.title,
|
|
495
|
-
description: detail.description,
|
|
496
|
-
headline,
|
|
497
|
-
body,
|
|
498
|
-
quote: readString(detail.properties.quote),
|
|
499
|
-
quoteAttribution: readString(detail.properties.quoteAttribution),
|
|
500
|
-
mediaUrl: readString(detail.properties.mediaUrl) || null,
|
|
501
|
-
layout: normalizeLayout(readString(detail.properties.layout)),
|
|
502
|
-
coverImage: detail.coverImage,
|
|
503
|
-
editUrl: detail.editUrl,
|
|
504
|
-
};
|
|
302
|
+
// Try by pageId first (from Notion relation — preferred path).
|
|
303
|
+
if (ref.pageId) {
|
|
304
|
+
const detail = await getGenericNotionContentByIdForLocale(
|
|
305
|
+
blocksSource,
|
|
306
|
+
ref.pageId,
|
|
307
|
+
locale
|
|
308
|
+
);
|
|
309
|
+
if (detail) return mapGenericBlockToStructuredBlock(detail, ref, index);
|
|
505
310
|
}
|
|
506
311
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
body: readString(detail.properties.body),
|
|
517
|
-
count: normalizeCount(readNumber(detail.properties.count)),
|
|
518
|
-
primaryCta: readCta(
|
|
519
|
-
detail.properties.primaryCtaLabel,
|
|
520
|
-
detail.properties.primaryCtaHref
|
|
521
|
-
),
|
|
522
|
-
coverImage: detail.coverImage,
|
|
523
|
-
editUrl: detail.editUrl,
|
|
524
|
-
};
|
|
312
|
+
// Try by slug (fallback or legacy rich_text JSON).
|
|
313
|
+
if (ref.slug) {
|
|
314
|
+
const detail = await getGenericNotionContentBySlugForLocale(
|
|
315
|
+
blocksSource,
|
|
316
|
+
ref.slug,
|
|
317
|
+
locale
|
|
318
|
+
);
|
|
319
|
+
if (detail) return mapGenericBlockToStructuredBlock(detail, ref, index);
|
|
320
|
+
return fallbackPageBlock(ref);
|
|
525
321
|
}
|
|
526
322
|
|
|
527
|
-
return
|
|
323
|
+
return null;
|
|
528
324
|
}
|
|
529
325
|
|
|
530
|
-
export async function getPageBlocks(page: BaseSitePage | null) {
|
|
326
|
+
export async function getPageBlocks(page: BaseSitePage | null, locale?: string) {
|
|
531
327
|
if (!page) return [];
|
|
328
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
532
329
|
const resolved = await Promise.all(
|
|
533
|
-
(page.structuredBlocks ?? []).map((ref) =>
|
|
330
|
+
(page.structuredBlocks ?? []).map((ref, index) =>
|
|
331
|
+
resolveStructuredBlock(ref, resolvedLocale, index)
|
|
332
|
+
)
|
|
534
333
|
);
|
|
535
334
|
return resolved.filter((block): block is StructuredPageBlock => Boolean(block));
|
|
536
335
|
}
|
|
537
336
|
|
|
538
|
-
async function withStructuredBlocks(
|
|
337
|
+
async function withStructuredBlocks(
|
|
338
|
+
page: BaseSitePage | null,
|
|
339
|
+
locale?: string
|
|
340
|
+
): Promise<SitePage | null> {
|
|
539
341
|
if (!page) return null;
|
|
540
342
|
return {
|
|
541
343
|
...page,
|
|
542
|
-
structuredBlocks: await getPageBlocks(page),
|
|
344
|
+
structuredBlocks: await getPageBlocks(page, locale),
|
|
543
345
|
};
|
|
544
346
|
}
|
|
545
347
|
|
|
546
|
-
export async function listSitePages() {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
348
|
+
export async function listSitePages(locale?: string) {
|
|
349
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
350
|
+
const pages = await pagesApi.listSitePages(resolvedLocale);
|
|
351
|
+
const resolved = await Promise.all(pages.map((page) => withStructuredBlocks(page, resolvedLocale)));
|
|
549
352
|
return resolved.filter((page): page is SitePage => Boolean(page));
|
|
550
353
|
}
|
|
551
354
|
|
|
552
|
-
export async function getSitePageByKey(key: string) {
|
|
553
|
-
|
|
355
|
+
export async function getSitePageByKey(key: string, locale?: string) {
|
|
356
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
357
|
+
return withStructuredBlocks(await pagesApi.getSitePageByKey(key, resolvedLocale), resolvedLocale);
|
|
554
358
|
}
|
|
555
359
|
|
|
556
|
-
export async function getSitePageBySlug(slug: string) {
|
|
557
|
-
|
|
360
|
+
export async function getSitePageBySlug(slug: string, locale?: string) {
|
|
361
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
362
|
+
return withStructuredBlocks(await pagesApi.getSitePageBySlug(slug, resolvedLocale), resolvedLocale);
|
|
558
363
|
}
|
|
559
364
|
export const getSitePageForContentSource = pagesApi.getSitePageForContentSource;
|
|
560
365
|
export const getSiteNavigation = pagesApi.getSiteNavigation;
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
// localized page is missing in the target locale, do not pretend the
|
|
3
3
|
// page exists — let the caller render a localized not-found.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
pickTranslation,
|
|
7
|
+
mapNotionPageToLocalizedContentTranslation,
|
|
8
|
+
listGenericNotionContentForLocale,
|
|
9
|
+
} from "@notionx/core";
|
|
6
10
|
import { i18n } from "@/lib/i18n";
|
|
7
11
|
import { pagesContract } from "@/lib/locale-contract";
|
|
12
|
+
import { pageTranslationsSource } from "@/lib/content/models";
|
|
13
|
+
import type { NotionPageLike } from "@notionx/core";
|
|
8
14
|
|
|
9
15
|
export type PageTranslation = {
|
|
10
16
|
pageId: string;
|
|
@@ -32,3 +38,19 @@ export function getDefaultLocalePage(
|
|
|
32
38
|
): PageTranslation | null {
|
|
33
39
|
return pickTranslation(rows, i18n.defaultLocale, pagesContract, i18n.defaultLocale);
|
|
34
40
|
}
|
|
41
|
+
|
|
42
|
+
export async function listPageTranslations(locale: string): Promise<PageTranslation[]> {
|
|
43
|
+
if (!pageTranslationsSource) return [];
|
|
44
|
+
const pages = await listGenericNotionContentForLocale(pageTranslationsSource, locale);
|
|
45
|
+
return pages
|
|
46
|
+
.map((p) => mapNotionPageToLocalizedContentTranslation<PageTranslation>(p as unknown as NotionPageLike, {
|
|
47
|
+
fields: {
|
|
48
|
+
title: "Title",
|
|
49
|
+
source: "Source",
|
|
50
|
+
locale: "Locale",
|
|
51
|
+
slug: "Slug",
|
|
52
|
+
published: "Published",
|
|
53
|
+
},
|
|
54
|
+
}))
|
|
55
|
+
.filter((row): row is PageTranslation => row !== null);
|
|
56
|
+
}
|
|
@@ -9,15 +9,28 @@
|
|
|
9
9
|
|
|
10
10
|
import { createD1SearchAdapter } from "@notionx/core/search";
|
|
11
11
|
import type { SearchAdapter } from "@notionx/core/search";
|
|
12
|
+
import type { SqlDatabaseAdapter } from "@notionx/core/platform";
|
|
12
13
|
import { getRequestEnv } from "../site/request-env";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* Lazily resolve the D1 database binding from the request environment
|
|
16
|
-
*
|
|
16
|
+
* Lazily resolve the D1 database binding from the request environment
|
|
17
|
+
* and wrap it in the `SqlDatabaseAdapter` shape expected by
|
|
18
|
+
* `@notionx/core/search`. The adapter is created once and reused
|
|
19
|
+
* across requests.
|
|
17
20
|
*/
|
|
18
|
-
function getDatabase() {
|
|
21
|
+
function getDatabase(): SqlDatabaseAdapter {
|
|
19
22
|
const env = getRequestEnv();
|
|
20
|
-
|
|
23
|
+
if (!env?.DB) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"D1 database binding DB is not available in the request environment."
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
kind: "d1",
|
|
30
|
+
prepare: (query) => env.DB!.prepare(query),
|
|
31
|
+
batch: (statements) =>
|
|
32
|
+
env.DB!.batch(statements as D1PreparedStatement[]),
|
|
33
|
+
};
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export const searchAdapter: SearchAdapter = createD1SearchAdapter(getDatabase);
|
|
@@ -40,12 +40,20 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Shape of the Cloudflare `env` parameter as far as this project
|
|
43
|
-
* is concerned.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* is concerned. Bindings read outside the worker entry (e.g. D1
|
|
44
|
+
* from the search adapter, KV from site settings) are propagated
|
|
45
|
+
* through ALS; others are accessed by the foundation worker
|
|
46
|
+
* directly and don't need to be listed here.
|
|
46
47
|
*/
|
|
47
48
|
export interface RequestEnv {
|
|
48
49
|
CONTENT_CACHE?: KVNamespace;
|
|
50
|
+
DB?: D1Database;
|
|
51
|
+
/**
|
|
52
|
+
* Locale resolved by the middleware from the `/{locale}` path
|
|
53
|
+
* prefix. Server components and data loaders read this via
|
|
54
|
+
* `getRequestLocale()` to pick the right translation rows.
|
|
55
|
+
*/
|
|
56
|
+
NOTIONX_LOCALE?: string;
|
|
49
57
|
// Extend as new runtime consumers need to read env outside the
|
|
50
58
|
// worker entry (e.g. cron-triggered helpers).
|
|
51
59
|
}
|
|
@@ -69,3 +77,13 @@ export function runWithRequestEnv<T>(env: RequestEnv, fn: () => T): T {
|
|
|
69
77
|
export function getRequestEnv(): RequestEnv | undefined {
|
|
70
78
|
return _envStore.getStore();
|
|
71
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read the locale for the current request, set by the locale prefix
|
|
83
|
+
* middleware. Falls back to the default locale when called outside a
|
|
84
|
+
* request scope (build scripts, tests) or when the middleware did
|
|
85
|
+
* not run (e.g. single-locale projects).
|
|
86
|
+
*/
|
|
87
|
+
export function getRequestLocale(defaultLocale = "en"): string {
|
|
88
|
+
return getRequestEnv()?.NOTIONX_LOCALE ?? defaultLocale;
|
|
89
|
+
}
|