@notionx/create-notionx-app 1.0.0 → 2.0.0
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 +18 -2
- package/dist/templates/lib/content/models.ts.tmpl +6 -0
- package/dist/templates/lib/pages/source.ts.tmpl +136 -334
- package/dist/templates/lib/pages/translations.ts.tmpl +23 -1
- package/dist/templates/lib/site/request-env.ts.tmpl +16 -0
- package/dist/templates/lib/site/settings.ts.tmpl +96 -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,19 @@
|
|
|
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
12
|
type SitePageBlockRef,
|
|
10
13
|
} from "@notionx/core/pages";
|
|
11
14
|
import { blocksSource, contentSources } from "@/lib/content/models";
|
|
15
|
+
import { getRequestLocale } from "@/lib/site/request-env";
|
|
16
|
+
import { i18n } from "@/lib/i18n";
|
|
12
17
|
import { pageFields, pagesDataSourceEnv } from "./model";
|
|
13
18
|
|
|
14
19
|
const pagesModel = {
|
|
@@ -35,167 +40,102 @@ const fallbackBlocks = (text: string): NotionBlock[] => [
|
|
|
35
40
|
},
|
|
36
41
|
];
|
|
37
42
|
|
|
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";
|
|
98
|
-
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";
|
|
43
|
+
export type StructuredPageBlock = {
|
|
111
44
|
slug: string;
|
|
112
|
-
title: string;
|
|
113
|
-
description: string;
|
|
114
45
|
variant: string;
|
|
46
|
+
order: number;
|
|
115
47
|
coverImage: string | null;
|
|
116
48
|
editUrl: string | null;
|
|
117
49
|
blocks: NotionBlock[];
|
|
118
50
|
};
|
|
119
51
|
|
|
120
|
-
export type StructuredPageBlock =
|
|
121
|
-
| StructuredHeroBlock
|
|
122
|
-
| StructuredFeatureGridBlock
|
|
123
|
-
| StructuredStoryBlock
|
|
124
|
-
| StructuredLatestPostsBlock
|
|
125
|
-
| LegacyStructuredPageBlock;
|
|
126
|
-
|
|
127
52
|
export type SitePage = Omit<BaseSitePage, "structuredBlocks"> & {
|
|
128
53
|
structuredBlocks: StructuredPageBlock[];
|
|
129
54
|
};
|
|
130
55
|
|
|
131
|
-
const fallbackStructuredBlocks: Record<
|
|
132
|
-
string,
|
|
133
|
-
Exclude<StructuredPageBlock, LegacyStructuredPageBlock>
|
|
134
|
-
> = {
|
|
56
|
+
const fallbackStructuredBlocks: Record<string, StructuredPageBlock> = {
|
|
135
57
|
"home-hero": {
|
|
136
|
-
type: "hero",
|
|
137
58
|
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",
|
|
59
|
+
variant: "hero",
|
|
60
|
+
order: 10,
|
|
149
61
|
coverImage: null,
|
|
150
62
|
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: [
|
|
63
|
+
blocks: [
|
|
163
64
|
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
65
|
+
id: "fallback-hero-heading",
|
|
66
|
+
type: "heading_1",
|
|
67
|
+
heading_1: {
|
|
68
|
+
rich_text: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: { content: "Start with a homepage you can keep editing" },
|
|
72
|
+
plain_text: "Start with a homepage you can keep editing",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
175
76
|
},
|
|
176
77
|
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
78
|
+
id: "fallback-hero-sub",
|
|
79
|
+
type: "paragraph",
|
|
80
|
+
paragraph: {
|
|
81
|
+
rich_text: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: {
|
|
85
|
+
content:
|
|
86
|
+
"Keep the layout stable in code while the hero copy, positioning, and primary call to action evolve in Notion.",
|
|
87
|
+
},
|
|
88
|
+
plain_text:
|
|
89
|
+
"Keep the layout stable in code while the hero copy, positioning, and primary call to action evolve in Notion.",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
182
93
|
},
|
|
183
94
|
],
|
|
95
|
+
},
|
|
96
|
+
"home-feature-grid": {
|
|
97
|
+
slug: "home-feature-grid",
|
|
98
|
+
variant: "feature-grid",
|
|
99
|
+
order: 20,
|
|
184
100
|
coverImage: null,
|
|
185
101
|
editUrl: null,
|
|
102
|
+
blocks: [
|
|
103
|
+
{
|
|
104
|
+
id: "fallback-feature-heading",
|
|
105
|
+
type: "heading_2",
|
|
106
|
+
heading_2: {
|
|
107
|
+
rich_text: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: { content: "Show the system working together" },
|
|
111
|
+
plain_text: "Show the system working together",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
186
117
|
},
|
|
187
118
|
"home-latest-posts": {
|
|
188
|
-
type: "latest-posts",
|
|
189
119
|
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}}" },
|
|
120
|
+
variant: "latest-posts",
|
|
121
|
+
order: 30,
|
|
197
122
|
coverImage: null,
|
|
198
123
|
editUrl: null,
|
|
124
|
+
blocks: [
|
|
125
|
+
{
|
|
126
|
+
id: "fallback-latest-heading",
|
|
127
|
+
type: "heading_2",
|
|
128
|
+
heading_2: {
|
|
129
|
+
rich_text: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: { content: "Read the latest from the blog" },
|
|
133
|
+
plain_text: "Read the latest from the blog",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
199
139
|
},
|
|
200
140
|
};
|
|
201
141
|
|
|
@@ -226,9 +166,9 @@ const fallbackPages: BaseSitePage[] = [
|
|
|
226
166
|
coverImage: null,
|
|
227
167
|
editUrl: null,
|
|
228
168
|
structuredBlocks: [
|
|
229
|
-
{ slug: "home-hero"
|
|
230
|
-
{ slug: "home-feature-grid"
|
|
231
|
-
{ slug: "home-latest-posts"
|
|
169
|
+
{ slug: "home-hero" },
|
|
170
|
+
{ slug: "home-feature-grid" },
|
|
171
|
+
{ slug: "home-latest-posts" },
|
|
232
172
|
],
|
|
233
173
|
blocks: fallbackBlocks("Configure Notion Pages to edit this homepage from Notion."),
|
|
234
174
|
},
|
|
@@ -320,241 +260,103 @@ const pagesApi = createSitePagesApi({
|
|
|
320
260
|
});
|
|
321
261
|
|
|
322
262
|
function fallbackPageBlock(ref: SitePageBlockRef): StructuredPageBlock | null {
|
|
263
|
+
if (!ref.slug) return null;
|
|
323
264
|
const fallback = fallbackStructuredBlocks[ref.slug];
|
|
324
265
|
if (!fallback) return null;
|
|
325
266
|
return fallback;
|
|
326
267
|
}
|
|
327
268
|
|
|
328
|
-
function
|
|
269
|
+
function mapGenericBlockToStructuredBlock(
|
|
329
270
|
detail: GenericContentDetail,
|
|
330
|
-
ref: SitePageBlockRef
|
|
331
|
-
|
|
332
|
-
|
|
271
|
+
ref: SitePageBlockRef,
|
|
272
|
+
index: number
|
|
273
|
+
): StructuredPageBlock {
|
|
274
|
+
const typeValue = detail.properties.type;
|
|
275
|
+
const variant =
|
|
276
|
+
(typeof typeValue === "string" ? typeValue.trim() : "") ||
|
|
277
|
+
ref.variant ||
|
|
278
|
+
"legacy";
|
|
279
|
+
const orderValue = detail.properties.order;
|
|
280
|
+
const order =
|
|
281
|
+
typeof orderValue === "number" && Number.isFinite(orderValue)
|
|
282
|
+
? orderValue
|
|
283
|
+
: index;
|
|
333
284
|
return {
|
|
334
|
-
type: "legacy",
|
|
335
285
|
slug: detail.slug,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
variant: ref.variant ?? "story",
|
|
286
|
+
variant,
|
|
287
|
+
order,
|
|
339
288
|
coverImage: detail.coverImage,
|
|
340
289
|
editUrl: detail.editUrl,
|
|
341
290
|
blocks: detail.blocks,
|
|
342
291
|
};
|
|
343
292
|
}
|
|
344
293
|
|
|
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
294
|
async function resolveStructuredBlock(
|
|
429
|
-
ref: SitePageBlockRef
|
|
295
|
+
ref: SitePageBlockRef,
|
|
296
|
+
locale: string,
|
|
297
|
+
index: number
|
|
430
298
|
): 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
|
-
};
|
|
299
|
+
// Try by pageId first (from Notion relation — preferred path).
|
|
300
|
+
if (ref.pageId) {
|
|
301
|
+
const detail = await getGenericNotionContentByIdForLocale(
|
|
302
|
+
blocksSource,
|
|
303
|
+
ref.pageId,
|
|
304
|
+
locale
|
|
305
|
+
);
|
|
306
|
+
if (detail) return mapGenericBlockToStructuredBlock(detail, ref, index);
|
|
505
307
|
}
|
|
506
308
|
|
|
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
|
-
};
|
|
309
|
+
// Try by slug (fallback or legacy rich_text JSON).
|
|
310
|
+
if (ref.slug) {
|
|
311
|
+
const detail = await getGenericNotionContentBySlugForLocale(
|
|
312
|
+
blocksSource,
|
|
313
|
+
ref.slug,
|
|
314
|
+
locale
|
|
315
|
+
);
|
|
316
|
+
if (detail) return mapGenericBlockToStructuredBlock(detail, ref, index);
|
|
317
|
+
return fallbackPageBlock(ref);
|
|
525
318
|
}
|
|
526
319
|
|
|
527
|
-
return
|
|
320
|
+
return null;
|
|
528
321
|
}
|
|
529
322
|
|
|
530
|
-
export async function getPageBlocks(page: BaseSitePage | null) {
|
|
323
|
+
export async function getPageBlocks(page: BaseSitePage | null, locale?: string) {
|
|
531
324
|
if (!page) return [];
|
|
325
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
532
326
|
const resolved = await Promise.all(
|
|
533
|
-
(page.structuredBlocks ?? []).map((ref) =>
|
|
327
|
+
(page.structuredBlocks ?? []).map((ref, index) =>
|
|
328
|
+
resolveStructuredBlock(ref, resolvedLocale, index)
|
|
329
|
+
)
|
|
534
330
|
);
|
|
535
331
|
return resolved.filter((block): block is StructuredPageBlock => Boolean(block));
|
|
536
332
|
}
|
|
537
333
|
|
|
538
|
-
async function withStructuredBlocks(
|
|
334
|
+
async function withStructuredBlocks(
|
|
335
|
+
page: BaseSitePage | null,
|
|
336
|
+
locale?: string
|
|
337
|
+
): Promise<SitePage | null> {
|
|
539
338
|
if (!page) return null;
|
|
540
339
|
return {
|
|
541
340
|
...page,
|
|
542
|
-
structuredBlocks: await getPageBlocks(page),
|
|
341
|
+
structuredBlocks: await getPageBlocks(page, locale),
|
|
543
342
|
};
|
|
544
343
|
}
|
|
545
344
|
|
|
546
|
-
export async function listSitePages() {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
345
|
+
export async function listSitePages(locale?: string) {
|
|
346
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
347
|
+
const pages = await pagesApi.listSitePages(resolvedLocale);
|
|
348
|
+
const resolved = await Promise.all(pages.map((page) => withStructuredBlocks(page, resolvedLocale)));
|
|
549
349
|
return resolved.filter((page): page is SitePage => Boolean(page));
|
|
550
350
|
}
|
|
551
351
|
|
|
552
|
-
export async function getSitePageByKey(key: string) {
|
|
553
|
-
|
|
352
|
+
export async function getSitePageByKey(key: string, locale?: string) {
|
|
353
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
354
|
+
return withStructuredBlocks(await pagesApi.getSitePageByKey(key, resolvedLocale), resolvedLocale);
|
|
554
355
|
}
|
|
555
356
|
|
|
556
|
-
export async function getSitePageBySlug(slug: string) {
|
|
557
|
-
|
|
357
|
+
export async function getSitePageBySlug(slug: string, locale?: string) {
|
|
358
|
+
const resolvedLocale = locale ?? getRequestLocale(i18n.defaultLocale);
|
|
359
|
+
return withStructuredBlocks(await pagesApi.getSitePageBySlug(slug, resolvedLocale), resolvedLocale);
|
|
558
360
|
}
|
|
559
361
|
export const getSitePageForContentSource = pagesApi.getSitePageForContentSource;
|
|
560
362
|
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
|
+
}
|
|
@@ -46,6 +46,12 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
46
46
|
*/
|
|
47
47
|
export interface RequestEnv {
|
|
48
48
|
CONTENT_CACHE?: KVNamespace;
|
|
49
|
+
/**
|
|
50
|
+
* Locale resolved by the middleware from the `/{locale}` path
|
|
51
|
+
* prefix. Server components and data loaders read this via
|
|
52
|
+
* `getRequestLocale()` to pick the right translation rows.
|
|
53
|
+
*/
|
|
54
|
+
NOTIONX_LOCALE?: string;
|
|
49
55
|
// Extend as new runtime consumers need to read env outside the
|
|
50
56
|
// worker entry (e.g. cron-triggered helpers).
|
|
51
57
|
}
|
|
@@ -69,3 +75,13 @@ export function runWithRequestEnv<T>(env: RequestEnv, fn: () => T): T {
|
|
|
69
75
|
export function getRequestEnv(): RequestEnv | undefined {
|
|
70
76
|
return _envStore.getStore();
|
|
71
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read the locale for the current request, set by the locale prefix
|
|
81
|
+
* middleware. Falls back to the default locale when called outside a
|
|
82
|
+
* request scope (build scripts, tests) or when the middleware did
|
|
83
|
+
* not run (e.g. single-locale projects).
|
|
84
|
+
*/
|
|
85
|
+
export function getRequestLocale(defaultLocale = "en"): string {
|
|
86
|
+
return getRequestEnv()?.NOTIONX_LOCALE ?? defaultLocale;
|
|
87
|
+
}
|