@notionx/create-notionx-app 1.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.
Files changed (239) hide show
  1. package/README.md +139 -0
  2. package/dist/answers.js +332 -0
  3. package/dist/answers.js.map +1 -0
  4. package/dist/cli-notionx.js +388 -0
  5. package/dist/cli-notionx.js.map +1 -0
  6. package/dist/cli-notionx.test.js +277 -0
  7. package/dist/cli-notionx.test.js.map +1 -0
  8. package/dist/diff.js +40 -0
  9. package/dist/diff.js.map +1 -0
  10. package/dist/diff.test.js +90 -0
  11. package/dist/diff.test.js.map +1 -0
  12. package/dist/index.js +99 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/locale-add/apply.js +39 -0
  15. package/dist/locale-add/apply.js.map +1 -0
  16. package/dist/locale-add/format.js +38 -0
  17. package/dist/locale-add/format.js.map +1 -0
  18. package/dist/locale-add/list.js +44 -0
  19. package/dist/locale-add/list.js.map +1 -0
  20. package/dist/locale-add/list.test.js +45 -0
  21. package/dist/locale-add/list.test.js.map +1 -0
  22. package/dist/locale-add/plan.js +128 -0
  23. package/dist/locale-add/plan.js.map +1 -0
  24. package/dist/locale-add/validate.js +46 -0
  25. package/dist/locale-add/validate.js.map +1 -0
  26. package/dist/metadata.js +41 -0
  27. package/dist/metadata.js.map +1 -0
  28. package/dist/notion-translation-sources/apply.js +61 -0
  29. package/dist/notion-translation-sources/apply.js.map +1 -0
  30. package/dist/notion-translation-sources/index.js +3 -0
  31. package/dist/notion-translation-sources/index.js.map +1 -0
  32. package/dist/notion-translation-sources/plan.js +33 -0
  33. package/dist/notion-translation-sources/plan.js.map +1 -0
  34. package/dist/notionx-source.js +142 -0
  35. package/dist/notionx-source.js.map +1 -0
  36. package/dist/notionx-source.test.js +144 -0
  37. package/dist/notionx-source.test.js.map +1 -0
  38. package/dist/password.js +18 -0
  39. package/dist/password.js.map +1 -0
  40. package/dist/presets.js +83 -0
  41. package/dist/presets.js.map +1 -0
  42. package/dist/presets.test.js +50 -0
  43. package/dist/presets.test.js.map +1 -0
  44. package/dist/prompt.js +218 -0
  45. package/dist/prompt.js.map +1 -0
  46. package/dist/provision/cloudflare.js +236 -0
  47. package/dist/provision/cloudflare.js.map +1 -0
  48. package/dist/provision/dependencies.js +219 -0
  49. package/dist/provision/dependencies.js.map +1 -0
  50. package/dist/provision/index.js +681 -0
  51. package/dist/provision/index.js.map +1 -0
  52. package/dist/provision/index.test.js +54 -0
  53. package/dist/provision/index.test.js.map +1 -0
  54. package/dist/provision/inspect.js +109 -0
  55. package/dist/provision/inspect.js.map +1 -0
  56. package/dist/provision/inspect.test.js +75 -0
  57. package/dist/provision/inspect.test.js.map +1 -0
  58. package/dist/provision/notion.js +1981 -0
  59. package/dist/provision/notion.js.map +1 -0
  60. package/dist/provision/notion.test.js +542 -0
  61. package/dist/provision/notion.test.js.map +1 -0
  62. package/dist/provision/ntn-credentials.js +198 -0
  63. package/dist/provision/ntn-credentials.js.map +1 -0
  64. package/dist/provision/options.js +15 -0
  65. package/dist/provision/options.js.map +1 -0
  66. package/dist/provision/password-hash.js +78 -0
  67. package/dist/provision/password-hash.js.map +1 -0
  68. package/dist/provision/prompts.js +115 -0
  69. package/dist/provision/prompts.js.map +1 -0
  70. package/dist/provision/repair.js +48 -0
  71. package/dist/provision/repair.js.map +1 -0
  72. package/dist/provision/repair.test.js +141 -0
  73. package/dist/provision/repair.test.js.map +1 -0
  74. package/dist/provision/shell.js +84 -0
  75. package/dist/provision/shell.js.map +1 -0
  76. package/dist/provision/wire.js +78 -0
  77. package/dist/provision/wire.js.map +1 -0
  78. package/dist/registry/doctor.js +181 -0
  79. package/dist/registry/doctor.js.map +1 -0
  80. package/dist/registry/doctor.test.js +180 -0
  81. package/dist/registry/doctor.test.js.map +1 -0
  82. package/dist/registry/install.js +217 -0
  83. package/dist/registry/install.js.map +1 -0
  84. package/dist/registry/install.test.js +168 -0
  85. package/dist/registry/install.test.js.map +1 -0
  86. package/dist/registry/load-registry.js +24 -0
  87. package/dist/registry/load-registry.js.map +1 -0
  88. package/dist/registry/load-registry.test.js +59 -0
  89. package/dist/registry/load-registry.test.js.map +1 -0
  90. package/dist/registry/migration-planner.js +204 -0
  91. package/dist/registry/migration-planner.js.map +1 -0
  92. package/dist/registry/migration-planner.test.js +340 -0
  93. package/dist/registry/migration-planner.test.js.map +1 -0
  94. package/dist/registry/migrations-store.js +125 -0
  95. package/dist/registry/migrations-store.js.map +1 -0
  96. package/dist/registry/migrations-store.test.js +163 -0
  97. package/dist/registry/migrations-store.test.js.map +1 -0
  98. package/dist/registry/migrations-types.js +25 -0
  99. package/dist/registry/migrations-types.js.map +1 -0
  100. package/dist/registry/project-meta.js +84 -0
  101. package/dist/registry/project-meta.js.map +1 -0
  102. package/dist/registry/registry-items.js +354 -0
  103. package/dist/registry/registry-items.js.map +1 -0
  104. package/dist/registry/registry-items.test.js +99 -0
  105. package/dist/registry/registry-items.test.js.map +1 -0
  106. package/dist/registry/registry-store.js +232 -0
  107. package/dist/registry/registry-store.js.map +1 -0
  108. package/dist/registry/registry-store.test.js +136 -0
  109. package/dist/registry/registry-store.test.js.map +1 -0
  110. package/dist/registry/registry-types.js +18 -0
  111. package/dist/registry/registry-types.js.map +1 -0
  112. package/dist/registry/registry-types.test.js +146 -0
  113. package/dist/registry/registry-types.test.js.map +1 -0
  114. package/dist/registry/render-content-source-files.js +158 -0
  115. package/dist/registry/render-content-source-files.js.map +1 -0
  116. package/dist/registry/render-multi-source.js +296 -0
  117. package/dist/registry/render-multi-source.js.map +1 -0
  118. package/dist/registry/render-multi-source.test.js +110 -0
  119. package/dist/registry/render-multi-source.test.js.map +1 -0
  120. package/dist/registry/text-utils.js +42 -0
  121. package/dist/registry/text-utils.js.map +1 -0
  122. package/dist/registry/uninstall.js +250 -0
  123. package/dist/registry/uninstall.js.map +1 -0
  124. package/dist/registry/uninstall.test.js +264 -0
  125. package/dist/registry/uninstall.test.js.map +1 -0
  126. package/dist/registry/update.js +280 -0
  127. package/dist/registry/update.js.map +1 -0
  128. package/dist/registry/update.test.js +229 -0
  129. package/dist/registry/update.test.js.map +1 -0
  130. package/dist/render.js +549 -0
  131. package/dist/render.js.map +1 -0
  132. package/dist/render.test.js +414 -0
  133. package/dist/render.test.js.map +1 -0
  134. package/dist/templates/.dev.vars.example.tmpl +32 -0
  135. package/dist/templates/.gitignore.tmpl +58 -0
  136. package/dist/templates/README.md.tmpl +417 -0
  137. package/dist/templates/app/[slug]/page.tsx.tmpl +55 -0
  138. package/dist/templates/app/admin/account/page.tsx.tmpl +18 -0
  139. package/dist/templates/app/admin/content-models/page.tsx.tmpl +6 -0
  140. package/dist/templates/app/admin/layout.tsx.tmpl +90 -0
  141. package/dist/templates/app/admin/loading.tsx.tmpl +6 -0
  142. package/dist/templates/app/admin/page.tsx.tmpl +17 -0
  143. package/dist/templates/app/api/auth/google/callback/route.ts.tmpl +3 -0
  144. package/dist/templates/app/api/auth/google/route.ts.tmpl +3 -0
  145. package/dist/templates/app/api/auth/verify-email/route.ts.tmpl +3 -0
  146. package/dist/templates/app/api/auth/viewer/route.ts.tmpl +3 -0
  147. package/dist/templates/app/api/health/route.ts.tmpl +3 -0
  148. package/dist/templates/app/api/{{contentSourceId}}/[slug]/route.ts.tmpl +27 -0
  149. package/dist/templates/app/api/{{contentSourceId}}/route.ts.tmpl +18 -0
  150. package/dist/templates/app/globals.css.tmpl +109 -0
  151. package/dist/templates/app/layout.tsx.tmpl +56 -0
  152. package/dist/templates/app/login/page.tsx.tmpl +154 -0
  153. package/dist/templates/app/page.fallback.tsx.tmpl +31 -0
  154. package/dist/templates/app/page.tsx.tmpl +42 -0
  155. package/dist/templates/app/register/page.tsx.tmpl +138 -0
  156. package/dist/templates/app/{{contentSourceListPath}}/[slug]/page.tsx.tmpl +113 -0
  157. package/dist/templates/app/{{contentSourceListPath}}/page.tsx.tmpl +74 -0
  158. package/dist/templates/components/content/post-card.tsx.tmpl +80 -0
  159. package/dist/templates/components/notion-blocks.tsx.tmpl +668 -0
  160. package/dist/templates/components/page-blocks/feature-grid-block.tsx.tmpl +68 -0
  161. package/dist/templates/components/page-blocks/hero-block.tsx.tmpl +73 -0
  162. package/dist/templates/components/page-blocks/latest-posts-block.tsx.tmpl +59 -0
  163. package/dist/templates/components/page-blocks/story-block.tsx.tmpl +70 -0
  164. package/dist/templates/components/page-blocks.fallback.tsx.tmpl +17 -0
  165. package/dist/templates/components/page-blocks.tsx.tmpl +32 -0
  166. package/dist/templates/components/search/search-dialog.tsx.tmpl +171 -0
  167. package/dist/templates/components/site/locale-switcher.tsx.tmpl +65 -0
  168. package/dist/templates/components/site/site-footer.tsx.tmpl +106 -0
  169. package/dist/templates/components/site/site-header.tsx.tmpl +80 -0
  170. package/dist/templates/components/site/site-shell.tsx.tmpl +20 -0
  171. package/dist/templates/components/site/theme-bootstrap.tsx.tmpl +51 -0
  172. package/dist/templates/components/theme-provider.tsx.tmpl +14 -0
  173. package/dist/templates/components/theme-toggle.tsx.tmpl +38 -0
  174. package/dist/templates/components/ui/accordion.tsx.tmpl +56 -0
  175. package/dist/templates/components/ui/alert.tsx.tmpl +59 -0
  176. package/dist/templates/components/ui/aspect-ratio.tsx.tmpl +8 -0
  177. package/dist/templates/components/ui/avatar.tsx.tmpl +44 -0
  178. package/dist/templates/components/ui/badge.tsx.tmpl +33 -0
  179. package/dist/templates/components/ui/button.tsx.tmpl +56 -0
  180. package/dist/templates/components/ui/card.tsx.tmpl +61 -0
  181. package/dist/templates/components/ui/checkbox.tsx.tmpl +28 -0
  182. package/dist/templates/components/ui/dialog.tsx.tmpl +104 -0
  183. package/dist/templates/components/ui/dropdown-menu.tsx.tmpl +183 -0
  184. package/dist/templates/components/ui/input.tsx.tmpl +21 -0
  185. package/dist/templates/components/ui/label.tsx.tmpl +25 -0
  186. package/dist/templates/components/ui/popover.tsx.tmpl +30 -0
  187. package/dist/templates/components/ui/radio-group.tsx.tmpl +44 -0
  188. package/dist/templates/components/ui/select.tsx.tmpl +150 -0
  189. package/dist/templates/components/ui/separator.tsx.tmpl +30 -0
  190. package/dist/templates/components/ui/sheet.tsx.tmpl +125 -0
  191. package/dist/templates/components/ui/skeleton.tsx.tmpl +15 -0
  192. package/dist/templates/components/ui/sonner.tsx.tmpl +30 -0
  193. package/dist/templates/components/ui/switch.tsx.tmpl +29 -0
  194. package/dist/templates/components/ui/table.tsx.tmpl +107 -0
  195. package/dist/templates/components/ui/tabs.tsx.tmpl +55 -0
  196. package/dist/templates/components/ui/textarea.tsx.tmpl +24 -0
  197. package/dist/templates/components/ui/tooltip.tsx.tmpl +30 -0
  198. package/dist/templates/components.json.tmpl +21 -0
  199. package/dist/templates/env.d.ts.tmpl +32 -0
  200. package/dist/templates/lib/admin/actions.ts.tmpl +43 -0
  201. package/dist/templates/lib/admin/context.tsx.tmpl +209 -0
  202. package/dist/templates/lib/admin/nav.ts.tmpl +23 -0
  203. package/dist/templates/lib/auth.config.fallback.ts.tmpl +10 -0
  204. package/dist/templates/lib/auth.config.ts.tmpl +45 -0
  205. package/dist/templates/lib/blocks/translations.ts.tmpl +44 -0
  206. package/dist/templates/lib/blog/translations.ts.tmpl +52 -0
  207. package/dist/templates/lib/content/models.ts.tmpl +53 -0
  208. package/dist/templates/lib/i18n/config.ts.tmpl +18 -0
  209. package/dist/templates/lib/i18n/index.ts.tmpl +1 -0
  210. package/dist/templates/lib/locale-contract/built-in.ts.tmpl +19 -0
  211. package/dist/templates/lib/locale-contract/index.ts.tmpl +3 -0
  212. package/dist/templates/lib/locale-contract/paths.ts.tmpl +29 -0
  213. package/dist/templates/lib/pages/model.ts.tmpl +16 -0
  214. package/dist/templates/lib/pages/source.ts.tmpl +566 -0
  215. package/dist/templates/lib/pages/translations.ts.tmpl +34 -0
  216. package/dist/templates/lib/search/config.fallback.ts.tmpl +11 -0
  217. package/dist/templates/lib/search/config.ts.tmpl +25 -0
  218. package/dist/templates/lib/site/config.ts.tmpl +120 -0
  219. package/dist/templates/lib/site/request-env.ts.tmpl +71 -0
  220. package/dist/templates/lib/site/settings.fallback.ts.tmpl +21 -0
  221. package/dist/templates/lib/site/settings.ts.tmpl +320 -0
  222. package/dist/templates/lib/site/translations.ts.tmpl +30 -0
  223. package/dist/templates/lib/utils.ts.tmpl +9 -0
  224. package/dist/templates/migrations/0001_init.sql.tmpl +57 -0
  225. package/dist/templates/migrations/0002_admin_seed.sql.tmpl +30 -0
  226. package/dist/templates/migrations/0003_search_index.sql.tmpl +29 -0
  227. package/dist/templates/next.config.ts.tmpl +18 -0
  228. package/dist/templates/package.json.tmpl +40 -0
  229. package/dist/templates/shims/cloudflare-workers-empty.mjs +4 -0
  230. package/dist/templates/shims/next-headers-empty.mjs +4 -0
  231. package/dist/templates/tests/smoke.test.ts.tmpl +83 -0
  232. package/dist/templates/tsconfig.json.tmpl +31 -0
  233. package/dist/templates/vite.config.ts.tmpl +53 -0
  234. package/dist/templates/vitest.config.ts.tmpl +13 -0
  235. package/dist/templates/worker/index.ts.tmpl +52 -0
  236. package/dist/templates/wrangler.jsonc.tmpl +44 -0
  237. package/dist/ui-presets.js +60 -0
  238. package/dist/ui-presets.js.map +1 -0
  239. package/package.json +60 -0
@@ -0,0 +1,566 @@
1
+ import {
2
+ getGenericNotionContentBySlug,
3
+ type GenericContentDetail,
4
+ type NotionBlock,
5
+ } from "@notionx/core/notion";
6
+ import {
7
+ createSitePagesApi,
8
+ type SitePage as BaseSitePage,
9
+ type SitePageBlockRef,
10
+ } from "@notionx/core/pages";
11
+ import { blocksSource, contentSources } from "@/lib/content/models";
12
+ import { pageFields, pagesDataSourceEnv } from "./model";
13
+
14
+ const pagesModel = {
15
+ source: {
16
+ tokenEnv: "NOTION_TOKEN",
17
+ dataSourceEnv: pagesDataSourceEnv,
18
+ fields: pageFields,
19
+ },
20
+ };
21
+
22
+ const fallbackBlocks = (text: string): NotionBlock[] => [
23
+ {
24
+ id: "fallback-intro",
25
+ type: "paragraph",
26
+ paragraph: {
27
+ rich_text: [
28
+ {
29
+ type: "text",
30
+ text: { content: text },
31
+ plain_text: text,
32
+ },
33
+ ],
34
+ },
35
+ },
36
+ ];
37
+
38
+ type BlockPropertyValue = string | string[] | number | boolean;
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";
111
+ slug: string;
112
+ title: string;
113
+ description: string;
114
+ variant: string;
115
+ coverImage: string | null;
116
+ editUrl: string | null;
117
+ blocks: NotionBlock[];
118
+ };
119
+
120
+ export type StructuredPageBlock =
121
+ | StructuredHeroBlock
122
+ | StructuredFeatureGridBlock
123
+ | StructuredStoryBlock
124
+ | StructuredLatestPostsBlock
125
+ | LegacyStructuredPageBlock;
126
+
127
+ export type SitePage = Omit<BaseSitePage, "structuredBlocks"> & {
128
+ structuredBlocks: StructuredPageBlock[];
129
+ };
130
+
131
+ const fallbackStructuredBlocks: Record<
132
+ string,
133
+ Exclude<StructuredPageBlock, LegacyStructuredPageBlock>
134
+ > = {
135
+ "home-hero": {
136
+ type: "hero",
137
+ slug: "home-hero",
138
+ title: "Homepage Hero",
139
+ description:
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",
149
+ coverImage: null,
150
+ 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: [
163
+ {
164
+ title: "Editorial workflows",
165
+ description:
166
+ "Use Notion as the editor for pages, posts, and reusable sections.",
167
+ icon: "pen-square",
168
+ href: "/about",
169
+ },
170
+ {
171
+ title: "Cloudflare runtime",
172
+ description:
173
+ "Ship on Workers with storage and caching primitives ready to grow.",
174
+ icon: "cloud",
175
+ },
176
+ {
177
+ title: "{{contentSourceListTitle}} updates",
178
+ description:
179
+ "Publish new entries and surface them through the generated routes automatically.",
180
+ icon: "newspaper",
181
+ href: "{{contentSourceListPath}}",
182
+ },
183
+ ],
184
+ coverImage: null,
185
+ editUrl: null,
186
+ },
187
+ "home-latest-posts": {
188
+ type: "latest-posts",
189
+ slug: "home-latest-posts",
190
+ title: "Homepage Latest Posts",
191
+ description: "Reusable homepage section for surfacing the latest published posts.",
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}}" },
197
+ coverImage: null,
198
+ editUrl: null,
199
+ },
200
+ };
201
+
202
+ const fallbackPages: BaseSitePage[] = [
203
+ {
204
+ pageId: "fallback-home",
205
+ key: "home",
206
+ slug: "",
207
+ href: "/",
208
+ title: "Home",
209
+ description:
210
+ "A Notion-powered site built on @notionx/core, running on Cloudflare Workers with D1, R2, and Cloudflare Images.",
211
+ seoTitle: "Home",
212
+ seoDescription:
213
+ "A Notion-powered site built on @notionx/core, running on Cloudflare Workers with D1, R2, and Cloudflare Images.",
214
+ layout: "home",
215
+ published: true,
216
+ showHeader: true,
217
+ showFooter: true,
218
+ showInNav: true,
219
+ navLabel: "Home",
220
+ navOrder: 0,
221
+ showInFooter: true,
222
+ footerLabel: "Home",
223
+ footerGroup: "Company",
224
+ footerOrder: 0,
225
+ contentSource: "",
226
+ coverImage: null,
227
+ editUrl: null,
228
+ structuredBlocks: [
229
+ { slug: "home-hero", variant: "hero", order: 10 },
230
+ { slug: "home-feature-grid", variant: "feature-grid", order: 20 },
231
+ { slug: "home-latest-posts", order: 30 },
232
+ ],
233
+ blocks: fallbackBlocks("Configure Notion Pages to edit this homepage from Notion."),
234
+ },
235
+ {
236
+ pageId: "fallback-about",
237
+ key: "about",
238
+ slug: "about",
239
+ href: "/about",
240
+ title: "About",
241
+ description:
242
+ "Use this page to explain the project, the team, or the editorial point of view behind the site.",
243
+ seoTitle: "About",
244
+ seoDescription:
245
+ "Use this page to explain the project, the team, or the editorial point of view behind the site.",
246
+ layout: "default",
247
+ published: true,
248
+ showHeader: true,
249
+ showFooter: true,
250
+ showInNav: true,
251
+ navLabel: "About",
252
+ navOrder: 10,
253
+ showInFooter: true,
254
+ footerLabel: "About",
255
+ footerGroup: "Company",
256
+ footerOrder: 10,
257
+ contentSource: "",
258
+ coverImage: null,
259
+ editUrl: null,
260
+ structuredBlocks: [],
261
+ blocks: fallbackBlocks("Configure Notion Pages to edit this About page from Notion."),
262
+ },
263
+ {
264
+ pageId: "fallback-blog",
265
+ key: "{{contentSourceId}}",
266
+ slug: "{{contentSourceId}}",
267
+ href: "{{contentSourceListPath}}",
268
+ title: "{{contentSourceListTitle}}",
269
+ description: "{{contentSourceListDescription}}",
270
+ seoTitle: "{{contentSourceListTitle}}",
271
+ seoDescription: "{{contentSourceListDescription}}",
272
+ layout: "content-list",
273
+ published: true,
274
+ showHeader: true,
275
+ showFooter: true,
276
+ showInNav: true,
277
+ navLabel: "{{contentSourceNavLabel}}",
278
+ navOrder: 20,
279
+ showInFooter: true,
280
+ footerLabel: "{{contentSourceNavLabel}}",
281
+ footerGroup: "Content",
282
+ footerOrder: 10,
283
+ contentSource: "{{contentSourceId}}",
284
+ coverImage: null,
285
+ editUrl: null,
286
+ structuredBlocks: [],
287
+ blocks: fallbackBlocks("Latest posts from the default Notion content source."),
288
+ },
289
+ {
290
+ pageId: "fallback-privacy",
291
+ key: "privacy",
292
+ slug: "privacy",
293
+ href: "/privacy",
294
+ title: "Privacy",
295
+ description: "Publish your privacy policy here once the project is live.",
296
+ seoTitle: "Privacy",
297
+ seoDescription: "Publish your privacy policy here once the project is live.",
298
+ layout: "default",
299
+ published: true,
300
+ showHeader: true,
301
+ showFooter: true,
302
+ showInNav: false,
303
+ navLabel: "Privacy",
304
+ navOrder: 0,
305
+ showInFooter: true,
306
+ footerLabel: "Privacy",
307
+ footerGroup: "Legal",
308
+ footerOrder: 10,
309
+ contentSource: "",
310
+ coverImage: null,
311
+ editUrl: null,
312
+ structuredBlocks: [],
313
+ blocks: fallbackBlocks("Replace this placeholder with your privacy policy in Notion."),
314
+ },
315
+ ];
316
+
317
+ const pagesApi = createSitePagesApi({
318
+ model: pagesModel,
319
+ fallbackPages,
320
+ });
321
+
322
+ function fallbackPageBlock(ref: SitePageBlockRef): StructuredPageBlock | null {
323
+ const fallback = fallbackStructuredBlocks[ref.slug];
324
+ if (!fallback) return null;
325
+ return fallback;
326
+ }
327
+
328
+ function fallbackToLegacyNotionBlocks(
329
+ detail: GenericContentDetail,
330
+ ref: SitePageBlockRef
331
+ ): LegacyStructuredPageBlock | null {
332
+ if (!detail.blocks.length) return null;
333
+ return {
334
+ type: "legacy",
335
+ slug: detail.slug,
336
+ title: detail.title,
337
+ description: detail.description,
338
+ variant: ref.variant ?? "story",
339
+ coverImage: detail.coverImage,
340
+ editUrl: detail.editUrl,
341
+ blocks: detail.blocks,
342
+ };
343
+ }
344
+
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
+ async function resolveStructuredBlock(
429
+ ref: SitePageBlockRef
430
+ ): Promise<StructuredPageBlock | null> {
431
+ const detail = await getGenericNotionContentBySlug(blocksSource, ref.slug);
432
+ if (!detail) return fallbackPageBlock(ref);
433
+
434
+ return mapGenericBlockToStructuredBlock(detail, ref);
435
+ }
436
+
437
+ function mapGenericBlockToStructuredBlock(
438
+ detail: GenericContentDetail,
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
+ };
505
+ }
506
+
507
+ if (type === "latest-posts") {
508
+ const headline = readString(detail.properties.headline);
509
+ if (!headline) return fallbackToLegacyNotionBlocks(detail, ref);
510
+ return {
511
+ type: "latest-posts",
512
+ slug: detail.slug,
513
+ title: detail.title,
514
+ description: detail.description,
515
+ headline,
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
+ };
525
+ }
526
+
527
+ return fallbackToLegacyNotionBlocks(detail, ref);
528
+ }
529
+
530
+ export async function getPageBlocks(page: BaseSitePage | null) {
531
+ if (!page) return [];
532
+ const resolved = await Promise.all(
533
+ (page.structuredBlocks ?? []).map((ref) => resolveStructuredBlock(ref))
534
+ );
535
+ return resolved.filter((block): block is StructuredPageBlock => Boolean(block));
536
+ }
537
+
538
+ async function withStructuredBlocks(page: BaseSitePage | null): Promise<SitePage | null> {
539
+ if (!page) return null;
540
+ return {
541
+ ...page,
542
+ structuredBlocks: await getPageBlocks(page),
543
+ };
544
+ }
545
+
546
+ export async function listSitePages() {
547
+ const pages = await pagesApi.listSitePages();
548
+ const resolved = await Promise.all(pages.map((page) => withStructuredBlocks(page)));
549
+ return resolved.filter((page): page is SitePage => Boolean(page));
550
+ }
551
+
552
+ export async function getSitePageByKey(key: string) {
553
+ return withStructuredBlocks(await pagesApi.getSitePageByKey(key));
554
+ }
555
+
556
+ export async function getSitePageBySlug(slug: string) {
557
+ return withStructuredBlocks(await pagesApi.getSitePageBySlug(slug));
558
+ }
559
+ export const getSitePageForContentSource = pagesApi.getSitePageForContentSource;
560
+ export const getSiteNavigation = pagesApi.getSiteNavigation;
561
+ export const getSiteFooterGroups = pagesApi.getSiteFooterGroups;
562
+ export type { SitePageFooterGroup, SitePageNavItem } from "@notionx/core/pages";
563
+
564
+ export function findContentSource(id: string) {
565
+ return contentSources.find((source) => source.id === id) ?? null;
566
+ }
@@ -0,0 +1,34 @@
1
+ // Locale-aware page lookup. Uses the strict-missing rule: if a
2
+ // localized page is missing in the target locale, do not pretend the
3
+ // page exists — let the caller render a localized not-found.
4
+
5
+ import { pickTranslation } from "@notionx/core";
6
+ import { i18n } from "@/lib/i18n";
7
+ import { pagesContract } from "@/lib/locale-contract";
8
+
9
+ export type PageTranslation = {
10
+ pageId: string;
11
+ sourcePageId: string;
12
+ locale: string;
13
+ slug: string;
14
+ title: string;
15
+ description: string;
16
+ seoTitle: string;
17
+ seoDescription: string;
18
+ navLabel: string;
19
+ footerLabel: string;
20
+ published: boolean;
21
+ };
22
+
23
+ export function pickPageTranslation(
24
+ rows: readonly PageTranslation[],
25
+ locale: string
26
+ ): PageTranslation | null {
27
+ return pickTranslation(rows, locale, pagesContract, i18n.defaultLocale);
28
+ }
29
+
30
+ export function getDefaultLocalePage(
31
+ rows: readonly PageTranslation[]
32
+ ): PageTranslation | null {
33
+ return pickTranslation(rows, i18n.defaultLocale, pagesContract, i18n.defaultLocale);
34
+ }
@@ -0,0 +1,11 @@
1
+ // Search is disabled in this project (scaffolded with `--no-search`
2
+ // or `notionx remove search`). This stub exports `undefined` so the
3
+ // worker entry point can skip passing `searchAdapter` to
4
+ // `createNotionxWorker`, which in turn skips registering the
5
+ // `/api/search` route.
6
+
7
+ import type { SearchAdapter } from "@notionx/core/search";
8
+
9
+ export const searchAdapter: SearchAdapter | undefined = undefined;
10
+
11
+ export default searchAdapter;
@@ -0,0 +1,25 @@
1
+ // Search adapter configuration. Exports a `D1SearchAdapter` instance
2
+ // backed by the project's D1 database. The worker entry point passes
3
+ // this to `createNotionxWorker`, which registers the `/api/search`
4
+ // route automatically.
5
+ //
6
+ // To switch to a different backend (e.g. Cloudflare Vectorize), replace
7
+ // the adapter factory below. The `SearchAdapter` interface in
8
+ // `@notionx/core/search` defines the contract.
9
+
10
+ import { createD1SearchAdapter } from "@notionx/core/search";
11
+ import type { SearchAdapter } from "@notionx/core/search";
12
+ import { getRequestEnv } from "../site/request-env";
13
+
14
+ /**
15
+ * Lazily resolve the D1 database binding from the request environment.
16
+ * The adapter is created once and reused across requests.
17
+ */
18
+ function getDatabase() {
19
+ const env = getRequestEnv();
20
+ return env.DB;
21
+ }
22
+
23
+ export const searchAdapter: SearchAdapter = createD1SearchAdapter(getDatabase);
24
+
25
+ export default searchAdapter;